openmates 0.15.0-alpha.97 → 0.15.0-alpha.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,14 +144,23 @@ review the spend confirmation before live runs.
144
144
  ### Local source bridge
145
145
 
146
146
  ```bash
147
- openmates remote-access start --path ./my-repo --source-id repo-1 --local-only
148
- openmates remote-access status --json
149
- openmates remote-access search --source repo-1 "TODO" --limit 20
147
+ openmates remote-access
148
+ openmates remote-access --path ./my-repo --path ../shared
149
+ openmates remote-access --path ./my-repo --json
150
150
  ```
151
151
 
152
- `openmates remote-access` stores source metadata under
153
- `~/.openmates/remote-sources.json`, searches with `rg` inside the approved root,
154
- and does not upload repository files by default.
152
+ The command discovers Git repositories below the current folder by default;
153
+ explicit `--path` values replace that scope. It remains in the foreground,
154
+ reconnects after temporary network loss, and marks sources offline when it
155
+ stops. OpenMates can request bounded directory listings, text search, and
156
+ selected text previews. The bridge is read-only, excludes ignored, protected,
157
+ binary, and out-of-root files, and never uploads a preview unless you explicitly
158
+ choose to upload it.
159
+
160
+ Project keys authenticate end-to-end encryption between trusted clients. The
161
+ backend receives routing metadata and opaque ciphertext, not filesystem paths,
162
+ queries, snippets, file contents, or encryption keys. Local source associations
163
+ are stored with owner-only permissions under `~/.openmates/remote-sources.json`.
155
164
 
156
165
  ## Self-Host Server Management
157
166
 
@@ -1165,8 +1165,8 @@ function buildSession(onDisk, masterKey, emailEncryptionKey) {
1165
1165
  var SYNC_CACHE_FILE = "sync_cache.json";
1166
1166
  var LOCAL_TEAM_KEYS_FILE = "team_keys.json";
1167
1167
  function teamKeyStorageId(hashedEmail, teamId) {
1168
- const digest = createHash3("sha256").update(teamId).digest("hex").slice(0, 32);
1169
- return `${hashedEmail}:team:${digest}`;
1168
+ const digest2 = createHash3("sha256").update(teamId).digest("hex").slice(0, 32);
1169
+ return `${hashedEmail}:team:${digest2}`;
1170
1170
  }
1171
1171
  function saveLocalTeamKey(hashedEmail, teamId, teamKeyB64) {
1172
1172
  const storageId = teamKeyStorageId(hashedEmail, teamId);
@@ -1224,8 +1224,8 @@ function pruneLocalTeamArtifacts(hashedEmail, teamIds) {
1224
1224
  }
1225
1225
  function syncCacheFile(teamId) {
1226
1226
  if (!teamId) return SYNC_CACHE_FILE;
1227
- const digest = createHash3("sha256").update(teamId).digest("hex").slice(0, 32);
1228
- return `sync_cache.team.${digest}.json`;
1227
+ const digest2 = createHash3("sha256").update(teamId).digest("hex").slice(0, 32);
1228
+ return `sync_cache.team.${digest2}.json`;
1229
1229
  }
1230
1230
  function saveSyncCache(cache, teamId) {
1231
1231
  const filePath = join(ensureStateDir(), syncCacheFile(teamId));
@@ -1567,6 +1567,27 @@ var OpenMatesWsClient = class {
1567
1567
  this.socket.on("message", onMessage);
1568
1568
  return () => this.socket.off("message", onMessage);
1569
1569
  }
1570
+ onProjectRemoteAccessRequest(handler) {
1571
+ const onMessage = (rawData) => {
1572
+ try {
1573
+ const parsed = JSON.parse(rawData.toString());
1574
+ if (parsed.type !== "project_remote_access_request") return;
1575
+ const payload = parsed.payload ?? {};
1576
+ if (typeof payload.request_id !== "string" || typeof payload.project_id !== "string" || typeof payload.source_id !== "string" || typeof payload.source_session_id !== "string" || typeof payload.requesting_client_id !== "string" || !["list", "search", "read_text"].includes(String(payload.operation)) || typeof payload.key_epoch !== "number" || typeof payload.encrypted_envelope !== "string") return;
1577
+ void handler(payload);
1578
+ } catch {
1579
+ }
1580
+ };
1581
+ this.socket.on("message", onMessage);
1582
+ return () => this.socket.off("message", onMessage);
1583
+ }
1584
+ waitForClose() {
1585
+ return new Promise((resolve10) => {
1586
+ this.socket.once("close", (code, reason) => {
1587
+ resolve10({ code, reason: reason.toString("utf-8") });
1588
+ });
1589
+ });
1590
+ }
1570
1591
  bufferPassiveTaskUpdateJobs(rawData) {
1571
1592
  try {
1572
1593
  const parsed = JSON.parse(rawData.toString());
@@ -4843,8 +4864,8 @@ function parseStringArray(value) {
4843
4864
  function deriveShortId(record) {
4844
4865
  const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
4845
4866
  const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
4846
- const digest = createHash7("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
4847
- return `${prefix}-${parseInt(digest, 16) % 1e4}`;
4867
+ const digest2 = createHash7("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
4868
+ return `${prefix}-${parseInt(digest2, 16) % 1e4}`;
4848
4869
  }
4849
4870
  function nowSeconds() {
4850
4871
  return Math.floor(Date.now() / 1e3);
@@ -5842,8 +5863,8 @@ function parseEmbedContentObject(rawContent) {
5842
5863
  }
5843
5864
  async function encodeEmbedContentObject(content) {
5844
5865
  try {
5845
- const { encode } = await import("@toon-format/toon");
5846
- return encode(content);
5866
+ const { encode: encode2 } = await import("@toon-format/toon");
5867
+ return encode2(content);
5847
5868
  } catch {
5848
5869
  return JSON.stringify(content);
5849
5870
  }
@@ -7271,7 +7292,7 @@ var OpenMatesClient = class _OpenMatesClient {
7271
7292
  session,
7272
7293
  { chatId, requestId: audioEmbed.embed.embedId }
7273
7294
  );
7274
- const transcript = transcription.transcript_corrected ?? transcription.transcript ?? transcription.transcript_original ?? "";
7295
+ const transcript2 = transcription.transcript_corrected ?? transcription.transcript ?? transcription.transcript_original ?? "";
7275
7296
  audioEmbed.embed.content = toonEncodeContent({
7276
7297
  app_id: "audio",
7277
7298
  skill_id: "transcribe",
@@ -7302,7 +7323,7 @@ var OpenMatesClient = class _OpenMatesClient {
7302
7323
  const audioMarkdown = [
7303
7324
  `Audio: ${audioEmbed.displayName}`,
7304
7325
  referenceBlock,
7305
- transcript ? `Transcript: ${transcript}` : null
7326
+ transcript2 ? `Transcript: ${transcript2}` : null
7306
7327
  ].filter((line) => Boolean(line)).join("\n");
7307
7328
  return await this.addIdeaBucketPreparedIdeas({
7308
7329
  chatId,
@@ -7313,12 +7334,12 @@ var OpenMatesClient = class _OpenMatesClient {
7313
7334
  preparedEmbeds: [audioEmbed.embed],
7314
7335
  ideas: [{
7315
7336
  content: audioMarkdown,
7316
- preview: transcript || audioEmbed.displayName,
7337
+ preview: transcript2 || audioEmbed.displayName,
7317
7338
  payload: {
7318
7339
  type: "audio",
7319
7340
  filename: audioEmbed.displayName,
7320
7341
  embed_ref: embedRef,
7321
- transcript,
7342
+ transcript: transcript2,
7322
7343
  transcript_original: transcription.transcript_original,
7323
7344
  transcript_corrected: transcription.transcript_corrected,
7324
7345
  use_corrected: transcription.use_corrected,
@@ -10418,6 +10439,14 @@ var OpenMatesClient = class _OpenMatesClient {
10418
10439
  }
10419
10440
  return response.data.source;
10420
10441
  }
10442
+ async openProjectRemoteAccessWebSocket() {
10443
+ const opened = await this.openWsClient({ taskUpdateJobs: false });
10444
+ if (!opened.ownerId) {
10445
+ opened.ws.close();
10446
+ throw new Error("Authenticated user identity unavailable for remote access");
10447
+ }
10448
+ return { ws: opened.ws, ownerId: opened.ownerId };
10449
+ }
10421
10450
  async createProjectItem(projectId, input) {
10422
10451
  this.requireSession();
10423
10452
  const response = await this.http.post(
@@ -18270,8 +18299,8 @@ function parseStringArray2(value) {
18270
18299
  }
18271
18300
  function deriveShortId2(record) {
18272
18301
  const source = record.plan_id || `${record.created_at ?? ""}-${record.updated_at ?? ""}`;
18273
- const digest = createHash10("sha256").update(source).digest("hex").slice(0, 6).toUpperCase();
18274
- return `${DEFAULT_PLAN_PREFIX}-${digest}`;
18302
+ const digest2 = createHash10("sha256").update(source).digest("hex").slice(0, 6).toUpperCase();
18303
+ return `${DEFAULT_PLAN_PREFIX}-${digest2}`;
18275
18304
  }
18276
18305
  function nowSeconds2() {
18277
18306
  return Math.floor(Date.now() / 1e3);
@@ -23201,8 +23230,8 @@ function renderVideoTranscriptFullscreen(c) {
23201
23230
  if (url) process.stdout.write(`\x1B[2m${url}\x1B[0m
23202
23231
  `);
23203
23232
  console.log();
23204
- const transcript = str(c.transcript) ?? str(c.text) ?? "";
23205
- if (transcript) console.log(transcript);
23233
+ const transcript2 = str(c.transcript) ?? str(c.text) ?? "";
23234
+ if (transcript2) console.log(transcript2);
23206
23235
  else console.log("No transcript available.");
23207
23236
  }
23208
23237
  async function renderHealthSearchPreview(c, ln, _client) {
@@ -67303,8 +67332,22 @@ function clamp(value, min, max) {
67303
67332
 
67304
67333
  // src/remoteAccess.ts
67305
67334
  import { homedir as homedir7 } from "os";
67306
- import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync8, realpathSync, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
67307
- import { spawn as spawn2 } from "child_process";
67335
+ import {
67336
+ chmodSync as chmodSync4,
67337
+ closeSync as closeSync2,
67338
+ constants,
67339
+ existsSync as existsSync10,
67340
+ fstatSync,
67341
+ mkdirSync as mkdirSync6,
67342
+ openSync as openSync2,
67343
+ readFileSync as readFileSync8,
67344
+ readdirSync as readdirSync5,
67345
+ readSync as readSync2,
67346
+ realpathSync,
67347
+ statSync as statSync2,
67348
+ writeFileSync as writeFileSync6
67349
+ } from "fs";
67350
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
67308
67351
  import { join as join8, resolve as resolve8, relative as relative2 } from "path";
67309
67352
 
67310
67353
  // src/projectFileRisk.ts
@@ -67354,9 +67397,492 @@ var BUILT_IN_PATTERNS = [
67354
67397
  { reason: "migration_schema_or_ci_file", patterns: MIGRATION_SCHEMA_CI_PATTERNS }
67355
67398
  ];
67356
67399
  var PROJECT_HIGH_RISK_GLOBS = BUILT_IN_PATTERNS.flatMap((group) => group.patterns);
67400
+ function classifyProjectFileRisk(path, userProtectedPatterns = []) {
67401
+ const normalizedPath = normalizePath(path);
67402
+ const reasons = [];
67403
+ for (const group of BUILT_IN_PATTERNS) {
67404
+ if (group.patterns.some((pattern) => matchesPattern2(normalizedPath, pattern))) {
67405
+ reasons.push(group.reason);
67406
+ }
67407
+ }
67408
+ if (userProtectedPatterns.some((pattern) => matchesPattern2(normalizedPath, normalizePath(pattern)))) {
67409
+ reasons.push("user_protected_pattern");
67410
+ }
67411
+ return { isHighRisk: reasons.length > 0, reasons: [...new Set(reasons)] };
67412
+ }
67413
+ function normalizePath(path) {
67414
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
67415
+ }
67416
+ function matchesPattern2(path, pattern) {
67417
+ const regex = new RegExp(`^${globToRegex(pattern)}$`);
67418
+ return regex.test(path);
67419
+ }
67420
+ function globToRegex(pattern) {
67421
+ let output = "";
67422
+ for (let index = 0; index < pattern.length; index += 1) {
67423
+ const char = pattern[index];
67424
+ const next = pattern[index + 1];
67425
+ if (char === "*" && next === "*") {
67426
+ output += ".*";
67427
+ index += 1;
67428
+ } else if (char === "*") {
67429
+ output += "[^/]*";
67430
+ } else {
67431
+ output += escapeRegex(char ?? "");
67432
+ }
67433
+ }
67434
+ return output;
67435
+ }
67436
+ function escapeRegex(value) {
67437
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
67438
+ }
67439
+
67440
+ // src/remoteAccessCrypto.ts
67441
+ import { webcrypto as webcrypto6 } from "crypto";
67442
+ import nacl2 from "tweetnacl";
67443
+ var cryptoApi4 = globalThis.crypto ?? webcrypto6;
67444
+ var PROTOCOL_VERSION = 1;
67445
+ var KEY_BYTES = 32;
67446
+ var NONCE_BYTES = 12;
67447
+ var MAX_PAYLOAD_BYTES = 200 * 1024;
67448
+ async function createRemoteAccessHandshake(projectKey, identity, role) {
67449
+ assertProjectKey(projectKey);
67450
+ validateIdentity(identity);
67451
+ const privateKey = cryptoApi4.getRandomValues(new Uint8Array(KEY_BYTES));
67452
+ const publicKey = nacl2.scalarMult.base(privateKey);
67453
+ const publicKeyText = toBase64Url(publicKey);
67454
+ return {
67455
+ privateKey: toBase64Url(privateKey),
67456
+ handshake: {
67457
+ version: PROTOCOL_VERSION,
67458
+ role,
67459
+ publicKey: publicKeyText,
67460
+ authenticationTag: await authenticateHandshake(projectKey, identity, role, publicKeyText)
67461
+ }
67462
+ };
67463
+ }
67464
+ async function deriveRemoteAccessSessionKey(projectKey, identity, localRole, localPrivateKey, localHandshake, remoteHandshake) {
67465
+ assertProjectKey(projectKey);
67466
+ validateIdentity(identity);
67467
+ const remoteRole = localRole === "requester" ? "source" : "requester";
67468
+ if (localHandshake.role !== localRole || remoteHandshake.role !== remoteRole) {
67469
+ throw new Error("handshake authentication failed");
67470
+ }
67471
+ await verifyHandshake(projectKey, identity, localHandshake);
67472
+ await verifyHandshake(projectKey, identity, remoteHandshake);
67473
+ const privateKey = fromBase64Url(localPrivateKey, KEY_BYTES);
67474
+ const remotePublicKey = fromBase64Url(remoteHandshake.publicKey, KEY_BYTES);
67475
+ const sharedSecret = nacl2.scalarMult(privateKey, remotePublicKey);
67476
+ if (sharedSecret.every((value) => value === 0)) throw new Error("handshake shared secret invalid");
67477
+ const requesterPublicKey = localRole === "requester" ? localHandshake.publicKey : remoteHandshake.publicKey;
67478
+ const sourcePublicKey = localRole === "source" ? localHandshake.publicKey : remoteHandshake.publicKey;
67479
+ return hkdf(
67480
+ sharedSecret,
67481
+ new TextEncoder().encode("openmates:project-remote-access:v1"),
67482
+ await digest(transcript(identity, requesterPublicKey, sourcePublicKey))
67483
+ );
67484
+ }
67485
+ async function sealRemoteAccessEnvelope(sessionKey, identity, requestId, direction, plaintext) {
67486
+ assertSessionKey(sessionKey);
67487
+ validateIdentity(identity);
67488
+ if (!requestId || plaintext.length > MAX_PAYLOAD_BYTES) throw new Error("invalid remote-access envelope payload");
67489
+ const nonce = cryptoApi4.getRandomValues(new Uint8Array(NONCE_BYTES));
67490
+ const key = await cryptoApi4.subtle.importKey("raw", arrayBuffer(sessionKey), "AES-GCM", false, ["encrypt"]);
67491
+ const ciphertext = await cryptoApi4.subtle.encrypt(
67492
+ { name: "AES-GCM", iv: arrayBuffer(nonce), additionalData: arrayBuffer(envelopeAad(identity, requestId, direction)) },
67493
+ key,
67494
+ arrayBuffer(plaintext)
67495
+ );
67496
+ return { version: PROTOCOL_VERSION, nonce: toBase64Url(nonce), ciphertext: toBase64Url(new Uint8Array(ciphertext)) };
67497
+ }
67498
+ async function authenticateHandshake(projectKey, identity, role, publicKey) {
67499
+ const key = await cryptoApi4.subtle.importKey("raw", arrayBuffer(projectKey), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
67500
+ const tag = await cryptoApi4.subtle.sign("HMAC", key, arrayBuffer(handshakeAad(identity, role, publicKey)));
67501
+ return toBase64Url(new Uint8Array(tag));
67502
+ }
67503
+ async function verifyHandshake(projectKey, identity, handshake) {
67504
+ if (handshake.version !== PROTOCOL_VERSION) throw new Error("handshake authentication failed");
67505
+ fromBase64Url(handshake.publicKey, KEY_BYTES);
67506
+ const expected = await authenticateHandshake(projectKey, identity, handshake.role, handshake.publicKey);
67507
+ const actualBytes = fromBase64Url(handshake.authenticationTag, KEY_BYTES);
67508
+ const expectedBytes = fromBase64Url(expected, KEY_BYTES);
67509
+ if (!constantTimeEqual(actualBytes, expectedBytes)) throw new Error("handshake authentication failed");
67510
+ }
67511
+ function handshakeAad(identity, role, publicKey) {
67512
+ return encode(["OMRA-HANDSHAKE-1", ...identityValues(identity), role, publicKey]);
67513
+ }
67514
+ function transcript(identity, requesterPublicKey, sourcePublicKey) {
67515
+ return encode(["OMRA-TRANSCRIPT-1", ...identityValues(identity), requesterPublicKey, sourcePublicKey]);
67516
+ }
67517
+ function envelopeAad(identity, requestId, direction) {
67518
+ return encode(["OMRA-ENVELOPE-1", ...identityValues(identity), requestId, direction]);
67519
+ }
67520
+ function identityValues(identity) {
67521
+ return [
67522
+ identity.ownerId,
67523
+ identity.projectId,
67524
+ identity.sourceId,
67525
+ identity.sourceSessionId,
67526
+ identity.requestingClientId,
67527
+ identity.keyEpoch
67528
+ ];
67529
+ }
67530
+ function encode(values) {
67531
+ return new TextEncoder().encode(JSON.stringify(values));
67532
+ }
67533
+ async function hkdf(secret, salt, info) {
67534
+ const key = await cryptoApi4.subtle.importKey("raw", arrayBuffer(secret), "HKDF", false, ["deriveBits"]);
67535
+ const bits = await cryptoApi4.subtle.deriveBits(
67536
+ { name: "HKDF", hash: "SHA-256", salt: arrayBuffer(salt), info: arrayBuffer(info) },
67537
+ key,
67538
+ 256
67539
+ );
67540
+ return new Uint8Array(bits);
67541
+ }
67542
+ async function digest(value) {
67543
+ return new Uint8Array(await cryptoApi4.subtle.digest("SHA-256", arrayBuffer(value)));
67544
+ }
67545
+ function validateIdentity(identity) {
67546
+ const strings = [identity.ownerId, identity.projectId, identity.sourceId, identity.sourceSessionId, identity.requestingClientId];
67547
+ if (strings.some((value) => !value || value.length > 128) || !Number.isInteger(identity.keyEpoch) || identity.keyEpoch < 1) {
67548
+ throw new Error("invalid remote-access identity");
67549
+ }
67550
+ }
67551
+ function assertProjectKey(value) {
67552
+ if (value.length !== KEY_BYTES) throw new Error("Project key must be 32 bytes");
67553
+ }
67554
+ function assertSessionKey(value) {
67555
+ if (value.length !== KEY_BYTES) throw new Error("source-session key must be 32 bytes");
67556
+ }
67557
+ function toBase64Url(value) {
67558
+ return Buffer.from(value).toString("base64url");
67559
+ }
67560
+ function fromBase64Url(value, expectedLength) {
67561
+ if (!value || value.includes("=") || !/^[A-Za-z0-9_-]+$/.test(value)) {
67562
+ throw new Error("invalid remote-access key or envelope field");
67563
+ }
67564
+ const decoded = new Uint8Array(Buffer.from(value, "base64url"));
67565
+ if (toBase64Url(decoded) !== value || expectedLength !== void 0 && decoded.length !== expectedLength) {
67566
+ throw new Error("invalid remote-access key or envelope field");
67567
+ }
67568
+ return decoded;
67569
+ }
67570
+ function constantTimeEqual(left, right) {
67571
+ if (left.length !== right.length) return false;
67572
+ let difference = 0;
67573
+ for (let index = 0; index < left.length; index += 1) difference |= left[index] ^ right[index];
67574
+ return difference === 0;
67575
+ }
67576
+ function arrayBuffer(value) {
67577
+ return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
67578
+ }
67357
67579
 
67358
67580
  // src/remoteAccess.ts
67581
+ var DEFAULT_MAX_SEARCH_RESULTS = 20;
67582
+ var MAX_SEARCH_SNIPPET_CHARS = 500;
67583
+ var SEARCH_TIMEOUT_MS = 1e4;
67584
+ var MAX_APPROVED_ROOTS = 16;
67585
+ var DEFAULT_MAX_DIRECTORY_ENTRIES = 500;
67586
+ var DEFAULT_MAX_READ_BYTES = 200 * 1024;
67587
+ var DEFAULT_MAX_READ_LINES = 4e3;
67588
+ var BINARY_PROBE_BYTES = 8 * 1024;
67359
67589
  var MAX_SOURCE_ID_LENGTH = 128;
67590
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
67591
+ ".png",
67592
+ ".jpg",
67593
+ ".jpeg",
67594
+ ".gif",
67595
+ ".webp",
67596
+ ".pdf",
67597
+ ".zip",
67598
+ ".gz",
67599
+ ".tar",
67600
+ ".mp3",
67601
+ ".mp4",
67602
+ ".mov"
67603
+ ]);
67604
+ function resolveRemoteAccessRoots(pathFlag, cwd = process.cwd()) {
67605
+ const requested = pathFlag === void 0 ? [cwd] : pathFlag.split("\n");
67606
+ if (requested.some((value) => !value)) throw new Error("--path requires a non-empty folder value");
67607
+ if (requested.length > MAX_APPROVED_ROOTS) {
67608
+ throw new Error(`remote-access accepts at most ${MAX_APPROVED_ROOTS} approved roots`);
67609
+ }
67610
+ const roots = [];
67611
+ for (const value of requested) {
67612
+ const candidate = resolve8(cwd, value);
67613
+ if (!existsSync10(candidate) || !statSync2(candidate).isDirectory()) {
67614
+ throw new Error(`Remote source path does not exist or is not a directory: ${candidate}`);
67615
+ }
67616
+ const canonical = realpathSync(candidate);
67617
+ if (!roots.includes(canonical)) roots.push(canonical);
67618
+ }
67619
+ return roots;
67620
+ }
67621
+ function discoverRemoteAccessRepositories(roots) {
67622
+ const repositoryRoots = /* @__PURE__ */ new Set();
67623
+ const permissionDenied = [];
67624
+ const visit = (directory, approvedRoot) => {
67625
+ let entries;
67626
+ try {
67627
+ entries = readdirSync5(directory, { withFileTypes: true });
67628
+ } catch (error) {
67629
+ const code = error.code;
67630
+ if (code === "EACCES" || code === "EPERM") {
67631
+ permissionDenied.push(relative2(approvedRoot, directory) || ".");
67632
+ return;
67633
+ }
67634
+ throw error;
67635
+ }
67636
+ if (entries.some((entry) => entry.name === ".git" && (entry.isDirectory() || entry.isFile()))) {
67637
+ repositoryRoots.add(realpathSync(directory));
67638
+ }
67639
+ for (const entry of entries) {
67640
+ if (entry.name === ".git" || entry.isSymbolicLink() || !entry.isDirectory()) continue;
67641
+ visit(join8(directory, entry.name), approvedRoot);
67642
+ }
67643
+ };
67644
+ for (const root of roots) visit(root, root);
67645
+ return {
67646
+ repositories: [...repositoryRoots].sort().map((rootPath) => ({ rootPath, displayName: rootPath.split(/[\\/]/).filter(Boolean).pop() ?? rootPath })),
67647
+ permissionDenied: permissionDenied.sort()
67648
+ };
67649
+ }
67650
+ function remoteAccessSourceType(rootPath) {
67651
+ const gitMarker = join8(rootPath, ".git");
67652
+ return existsSync10(gitMarker) ? "local_git_repository" : "local_folder";
67653
+ }
67654
+ function listRemoteAccessDirectory(options) {
67655
+ const root = realpathSync(options.sourceRoot);
67656
+ const directory = resolveApprovedPath(root, options.relativePath);
67657
+ if (!statSync2(directory).isDirectory()) throw new Error("Remote source path is not a directory");
67658
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_DIRECTORY_ENTRIES;
67659
+ if (!Number.isInteger(maxEntries) || maxEntries <= 0 || maxEntries > DEFAULT_MAX_DIRECTORY_ENTRIES) {
67660
+ throw new Error(`Remote directory entry limit must be between 1 and ${DEFAULT_MAX_DIRECTORY_ENTRIES}`);
67661
+ }
67662
+ const entries = [];
67663
+ let omitted = 0;
67664
+ let excluded = 0;
67665
+ for (const entry of readdirSync5(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
67666
+ const entryPath = relative2(root, join8(directory, entry.name)).replace(/\\/g, "/");
67667
+ if (entry.name === ".git" || entry.isSymbolicLink() || isGitIgnoredPath(root, entryPath) || classifyProjectFileRisk(entryPath, options.userProtectedPatterns ?? []).isHighRisk || entry.isFile() && isBinaryFile(join8(directory, entry.name))) {
67668
+ excluded += 1;
67669
+ continue;
67670
+ }
67671
+ if (!entry.isFile() && !entry.isDirectory()) {
67672
+ excluded += 1;
67673
+ continue;
67674
+ }
67675
+ if (entries.length >= maxEntries) {
67676
+ omitted += 1;
67677
+ continue;
67678
+ }
67679
+ entries.push({ path: entryPath, kind: entry.isDirectory() ? "directory" : "file" });
67680
+ }
67681
+ return { entries, omitted, excluded };
67682
+ }
67683
+ function readRemoteAccessTextFile(options) {
67684
+ const root = realpathSync(options.sourceRoot);
67685
+ const normalizedRelative = options.relativePath.replace(/\\/g, "/");
67686
+ if (classifyProjectFileRisk(normalizedRelative, options.userProtectedPatterns ?? []).isHighRisk) {
67687
+ throw new Error("Remote source file is protected");
67688
+ }
67689
+ if (isGitIgnoredPath(root, normalizedRelative)) throw new Error("Remote source file is ignored");
67690
+ const requested = resolveApprovedPath(root, normalizedRelative);
67691
+ options.beforeOpen?.();
67692
+ const maxBytes = normalizeBound(options.maxBytes, DEFAULT_MAX_READ_BYTES, "byte");
67693
+ const maxLines = normalizeBound(options.maxLines, DEFAULT_MAX_READ_LINES, "line");
67694
+ let descriptor;
67695
+ try {
67696
+ descriptor = openSync2(requested, constants.O_RDONLY | constants.O_NOFOLLOW);
67697
+ const openedPath = openedDescriptorPath(descriptor, requested);
67698
+ assertInsideRoot(root, openedPath);
67699
+ const stats = fstatSync(descriptor);
67700
+ if (!stats.isFile()) throw new Error("Remote source path is not a regular file");
67701
+ const bytesToRead = Math.min(stats.size, maxBytes + 1);
67702
+ const buffer = Buffer.alloc(bytesToRead);
67703
+ const bytesRead = readSync2(descriptor, buffer, 0, bytesToRead, 0);
67704
+ const bytes = new Uint8Array(buffer.subarray(0, bytesRead));
67705
+ if (bytes.subarray(0, BINARY_PROBE_BYTES).includes(0)) throw new Error("Remote source file is binary or unsupported");
67706
+ let decoded;
67707
+ try {
67708
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, Math.min(bytes.length, maxBytes)));
67709
+ } catch {
67710
+ throw new Error("Remote source file is binary or unsupported");
67711
+ }
67712
+ const lines = decoded.split(/(?<=\n)/);
67713
+ const content = lines.slice(0, maxLines).join("");
67714
+ return {
67715
+ content,
67716
+ truncated: stats.size > maxBytes || lines.length > maxLines,
67717
+ sizeBytes: stats.size,
67718
+ lineCount: decoded.length === 0 ? 0 : decoded.split("\n").length
67719
+ };
67720
+ } catch (error) {
67721
+ const code = error.code;
67722
+ if (code === "ELOOP") throw new Error("Remote source path is a symbolic link");
67723
+ throw error;
67724
+ } finally {
67725
+ if (descriptor !== void 0) closeSync2(descriptor);
67726
+ }
67727
+ }
67728
+ async function runRemoteAccessBridge(options) {
67729
+ let reconnectAttempt = 0;
67730
+ while (!options.signal.aborted) {
67731
+ options.onLifecycle?.({ state: reconnectAttempt === 0 ? "connecting" : "reconnecting", attempt: reconnectAttempt });
67732
+ let ws = null;
67733
+ let removeRequestListener = null;
67734
+ let heartbeatTimer = null;
67735
+ try {
67736
+ const opened = await options.client.openProjectRemoteAccessWebSocket();
67737
+ ws = opened.ws;
67738
+ await ws.sendAsync("project_remote_access_register", {
67739
+ source_session_id: options.sourceSessionId,
67740
+ confirmed_takeover: options.confirmedTakeover === true,
67741
+ bindings: options.bindings.map((binding) => ({
67742
+ project_id: binding.source.projectId,
67743
+ source_id: binding.source.sourceId,
67744
+ capabilities: ["read", "search", "import"],
67745
+ key_epoch: binding.keyEpoch
67746
+ }))
67747
+ });
67748
+ await ws.waitForMessage("project_remote_access_registered", void 0, 2e4);
67749
+ reconnectAttempt = 0;
67750
+ options.onLifecycle?.({ state: "connected" });
67751
+ removeRequestListener = ws.onProjectRemoteAccessRequest((frame) => {
67752
+ void handleLiveRemoteAccessRequest(ws, opened.ownerId, options.sourceSessionId, options.bindings, frame);
67753
+ });
67754
+ heartbeatTimer = setInterval(() => {
67755
+ void ws?.sendAsync("project_remote_access_heartbeat", { source_session_id: options.sourceSessionId });
67756
+ }, 15e3);
67757
+ await Promise.race([ws.waitForClose(), waitForAbort2(options.signal)]);
67758
+ if (options.signal.aborted) {
67759
+ await ws.sendAsync("project_remote_access_disconnect", { source_session_id: options.sourceSessionId }).catch(() => void 0);
67760
+ options.onLifecycle?.({ state: "disconnected" });
67761
+ return;
67762
+ }
67763
+ } catch (error) {
67764
+ if (error instanceof WebSocketProtocolError) throw error;
67765
+ const message = error instanceof Error ? error.message : String(error);
67766
+ if (/session expired|invalid|not logged in/i.test(message)) throw error;
67767
+ } finally {
67768
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
67769
+ removeRequestListener?.();
67770
+ ws?.close();
67771
+ }
67772
+ reconnectAttempt += 1;
67773
+ const delayMs = reconnectDelayMs(reconnectAttempt);
67774
+ options.onLifecycle?.({ state: "reconnecting", attempt: reconnectAttempt, delayMs });
67775
+ await waitForAbort2(options.signal, delayMs);
67776
+ }
67777
+ }
67778
+ async function handleLiveRemoteAccessRequest(ws, ownerId, sourceSessionId, bindings, frame) {
67779
+ const binding = bindings.find(
67780
+ (item) => item.source.sourceId === frame.source_id && item.source.projectId === frame.project_id
67781
+ );
67782
+ if (!binding || frame.source_session_id !== sourceSessionId || frame.key_epoch !== binding.keyEpoch) return;
67783
+ const bootstrapText = await decryptWithAesGcmCombined(frame.encrypted_envelope, binding.projectKey);
67784
+ if (!bootstrapText) return;
67785
+ let bootstrap;
67786
+ try {
67787
+ bootstrap = JSON.parse(bootstrapText);
67788
+ } catch {
67789
+ return;
67790
+ }
67791
+ if (!bootstrap.requesting_client_id || bootstrap.requesting_client_id !== frame.requesting_client_id || !bootstrap.requester_handshake || bootstrap.operation !== frame.operation || !bootstrap.arguments) return;
67792
+ const identity = {
67793
+ ownerId,
67794
+ projectId: frame.project_id,
67795
+ sourceId: frame.source_id,
67796
+ sourceSessionId,
67797
+ requestingClientId: bootstrap.requesting_client_id,
67798
+ keyEpoch: binding.keyEpoch
67799
+ };
67800
+ try {
67801
+ const sourceHandshake = await createRemoteAccessHandshake(binding.projectKey, identity, "source");
67802
+ const sessionKey = await deriveRemoteAccessSessionKey(
67803
+ binding.projectKey,
67804
+ identity,
67805
+ "source",
67806
+ sourceHandshake.privateKey,
67807
+ sourceHandshake.handshake,
67808
+ bootstrap.requester_handshake
67809
+ );
67810
+ let response;
67811
+ try {
67812
+ response = {
67813
+ ok: true,
67814
+ result: await executeRemoteAccessOperation(binding.source.rootPath, frame.operation, bootstrap.arguments)
67815
+ };
67816
+ } catch (error) {
67817
+ response = { ok: false, error: remoteAccessOperationErrorCode(error) };
67818
+ }
67819
+ const envelope = await sealRemoteAccessEnvelope(
67820
+ sessionKey,
67821
+ identity,
67822
+ frame.request_id,
67823
+ "result",
67824
+ new TextEncoder().encode(JSON.stringify(response))
67825
+ );
67826
+ await ws.sendAsync("project_remote_access_complete", {
67827
+ source_session_id: sourceSessionId,
67828
+ project_id: frame.project_id,
67829
+ source_id: frame.source_id,
67830
+ request_id: frame.request_id,
67831
+ key_epoch: binding.keyEpoch,
67832
+ encrypted_envelope: JSON.stringify({ source_handshake: sourceHandshake.handshake, envelope })
67833
+ });
67834
+ } catch {
67835
+ }
67836
+ }
67837
+ async function executeRemoteAccessOperation(sourceRoot, operation, args) {
67838
+ const relativePath = typeof args.path === "string" ? args.path : ".";
67839
+ if (operation === "list") {
67840
+ return listRemoteAccessDirectory({ sourceRoot, relativePath });
67841
+ }
67842
+ if (operation === "read_text") {
67843
+ return readRemoteAccessTextFile({ sourceRoot, relativePath });
67844
+ }
67845
+ const query = typeof args.query === "string" ? args.query : "";
67846
+ if (!query) throw new Error("Search query is required");
67847
+ return searchRemoteSource({ query, sourceRoot, runRg: runRgCommand });
67848
+ }
67849
+ function remoteAccessOperationErrorCode(error) {
67850
+ const message = error instanceof Error ? error.message : "";
67851
+ if (/protected|ignored/i.test(message)) return "protected_path";
67852
+ if (/binary|unsupported/i.test(message)) return "unsupported_file";
67853
+ if (/symbolic link|approved source root|not a directory|not a regular file|ENOENT/i.test(message)) return "invalid_path";
67854
+ if (/Search query is required/i.test(message)) return "search_query_required";
67855
+ return "operation_failed";
67856
+ }
67857
+ function isGitIgnoredPath(sourceRoot, relativePath) {
67858
+ if (!existsSync10(join8(sourceRoot, ".git"))) return false;
67859
+ const result = spawnSync2("git", ["check-ignore", "--quiet", "--", relativePath], {
67860
+ cwd: sourceRoot,
67861
+ stdio: "ignore"
67862
+ });
67863
+ if (result.error) throw result.error;
67864
+ if (result.status === 0) return true;
67865
+ if (result.status === 1) return false;
67866
+ throw new Error("Could not evaluate Git ignored-file policy");
67867
+ }
67868
+ function reconnectDelayMs(attempt) {
67869
+ const base = Math.min(3e4, 1e3 * 2 ** Math.max(0, attempt - 1));
67870
+ return Math.round(base * (0.8 + Math.random() * 0.4));
67871
+ }
67872
+ function waitForAbort2(signal, timeoutMs) {
67873
+ if (signal.aborted) return Promise.resolve();
67874
+ return new Promise((resolvePromise) => {
67875
+ const onAbort = () => {
67876
+ if (timer) clearTimeout(timer);
67877
+ resolvePromise();
67878
+ };
67879
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
67880
+ signal.removeEventListener("abort", onAbort);
67881
+ resolvePromise();
67882
+ }, timeoutMs);
67883
+ signal.addEventListener("abort", onAbort, { once: true });
67884
+ });
67885
+ }
67360
67886
  function resolveRemoteCachePath(sourceId, homeDirectory = homedir7()) {
67361
67887
  assertSafeSourceId(sourceId);
67362
67888
  return join8(homeDirectory, ".openmates", "remote-cache", sourceId);
@@ -67377,7 +67903,7 @@ function startRemoteAccessSource(input) {
67377
67903
  rootPath,
67378
67904
  displayName: input.displayName ?? input.sourceId,
67379
67905
  cachePath: resolveRemoteCachePath(input.sourceId, homeDirectory),
67380
- status: "connected",
67906
+ status: "offline",
67381
67907
  createdAt: now,
67382
67908
  updatedAt: now
67383
67909
  };
@@ -67401,11 +67927,190 @@ function listRemoteAccessSources(homeDirectory = homedir7()) {
67401
67927
  throw new Error(`Failed to read remote source store at ${filePath}: ${message}`);
67402
67928
  }
67403
67929
  }
67930
+ async function searchRemoteSource(options) {
67931
+ const sourceRoot = resolve8(options.sourceRoot);
67932
+ const maxResults = normalizeMaxResults(options.maxResults);
67933
+ const output = await options.runRg(
67934
+ buildRgSearchArgs(options.query, options.userProtectedPatterns ?? []),
67935
+ sourceRoot,
67936
+ maxResults + 1
67937
+ );
67938
+ const matches2 = [];
67939
+ let omitted = 0;
67940
+ let excluded = 0;
67941
+ for (const line of output.split("\n")) {
67942
+ if (!line.trim()) continue;
67943
+ const match = parseRgMatch(line);
67944
+ if (!match) continue;
67945
+ if (shouldExcludePath(sourceRoot, match.path, options.userProtectedPatterns ?? [])) {
67946
+ excluded += 1;
67947
+ continue;
67948
+ }
67949
+ if (matches2.length >= maxResults) {
67950
+ omitted += 1;
67951
+ continue;
67952
+ }
67953
+ matches2.push(match);
67954
+ }
67955
+ return { matches: matches2, omitted, excluded };
67956
+ }
67404
67957
  function assertSafeSourceId(sourceId) {
67405
67958
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(sourceId) || sourceId.length > MAX_SOURCE_ID_LENGTH) {
67406
67959
  throw new Error("Remote source ID must be 1-128 characters and contain only letters, numbers, dot, underscore, or hyphen");
67407
67960
  }
67408
67961
  }
67962
+ function normalizeMaxResults(value) {
67963
+ const maxResults = value ?? DEFAULT_MAX_SEARCH_RESULTS;
67964
+ if (!Number.isInteger(maxResults) || maxResults <= 0 || maxResults > DEFAULT_MAX_SEARCH_RESULTS) {
67965
+ throw new Error(`Remote source search limit must be between 1 and ${DEFAULT_MAX_SEARCH_RESULTS}`);
67966
+ }
67967
+ return maxResults;
67968
+ }
67969
+ function buildRgSearchArgs(query, userProtectedPatterns) {
67970
+ const args = ["--json", "--line-number"];
67971
+ for (const pattern of [...PROJECT_HIGH_RISK_GLOBS, ...binaryRgGlobs(), ...userProtectedPatterns]) {
67972
+ args.push("--glob", `!${pattern.replace(/\\/g, "/")}`);
67973
+ }
67974
+ args.push("--", query, ".");
67975
+ return args;
67976
+ }
67977
+ function binaryRgGlobs() {
67978
+ return [...BINARY_EXTENSIONS].flatMap((extension) => [`*${extension}`, `**/*${extension}`]);
67979
+ }
67980
+ async function runRgCommand(args, cwd, maxOutputMatches) {
67981
+ return new Promise((resolvePromise, reject) => {
67982
+ const child = spawn2("rg", args, {
67983
+ cwd,
67984
+ stdio: ["ignore", "pipe", "pipe"]
67985
+ });
67986
+ const stdoutLines = [];
67987
+ const stderrChunks = [];
67988
+ let pendingStdout = "";
67989
+ let matchCount = 0;
67990
+ let killedForCap = false;
67991
+ let timedOut = false;
67992
+ const timeout = setTimeout(() => {
67993
+ timedOut = true;
67994
+ child.kill();
67995
+ }, SEARCH_TIMEOUT_MS);
67996
+ child.stdout.setEncoding("utf-8");
67997
+ child.stdout.on("data", (chunk) => {
67998
+ pendingStdout += chunk;
67999
+ const lines = pendingStdout.split("\n");
68000
+ pendingStdout = lines.pop() ?? "";
68001
+ for (const line of lines) {
68002
+ if (!line) continue;
68003
+ stdoutLines.push(line);
68004
+ if (line.includes('"type":"match"')) {
68005
+ matchCount += 1;
68006
+ }
68007
+ if (maxOutputMatches !== void 0 && matchCount >= maxOutputMatches) {
68008
+ killedForCap = true;
68009
+ child.kill();
68010
+ break;
68011
+ }
68012
+ }
68013
+ });
68014
+ child.stderr.setEncoding("utf-8");
68015
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
68016
+ child.on("error", (error) => {
68017
+ clearTimeout(timeout);
68018
+ reject(error);
68019
+ });
68020
+ child.on("close", (code) => {
68021
+ clearTimeout(timeout);
68022
+ if (pendingStdout) stdoutLines.push(pendingStdout);
68023
+ if (timedOut) {
68024
+ reject(new Error("Remote source search timed out"));
68025
+ return;
68026
+ }
68027
+ if (code === 0 || code === 1 || killedForCap) {
68028
+ resolvePromise(stdoutLines.join("\n"));
68029
+ return;
68030
+ }
68031
+ reject(new Error(`rg failed with exit code ${code}: ${stderrChunks.join("").trim()}`));
68032
+ });
68033
+ });
68034
+ }
68035
+ function shouldExcludePath(sourceRoot, relativePath, userProtectedPatterns) {
68036
+ if (!isPathInsideRoot(sourceRoot, relativePath)) return true;
68037
+ if (isBinaryPath(relativePath)) return true;
68038
+ return classifyProjectFileRisk(relativePath, userProtectedPatterns).isHighRisk;
68039
+ }
68040
+ function isPathInsideRoot(sourceRoot, relativePath) {
68041
+ const resolvedPath = resolve8(sourceRoot, relativePath);
68042
+ const relation = relative2(sourceRoot, resolvedPath);
68043
+ return relation === "" || !relation.startsWith("..") && !resolve8(relation).startsWith("/..");
68044
+ }
68045
+ function isBinaryPath(path) {
68046
+ const lowerPath = path.toLowerCase();
68047
+ for (const extension of BINARY_EXTENSIONS) {
68048
+ if (lowerPath.endsWith(extension)) return true;
68049
+ }
68050
+ return false;
68051
+ }
68052
+ function parseRgMatch(line) {
68053
+ try {
68054
+ const parsed = JSON.parse(line);
68055
+ if (parsed.type !== "match") return null;
68056
+ const path = parsed.data?.path?.text;
68057
+ const lineNumber = parsed.data?.line_number;
68058
+ const snippet = parsed.data?.lines?.text;
68059
+ if (typeof path !== "string" || typeof lineNumber !== "number" || typeof snippet !== "string") {
68060
+ return null;
68061
+ }
68062
+ return { path, line: lineNumber, snippet: snippet.slice(0, MAX_SEARCH_SNIPPET_CHARS) };
68063
+ } catch {
68064
+ return null;
68065
+ }
68066
+ }
68067
+ function resolveApprovedPath(sourceRoot, relativePath) {
68068
+ if (!relativePath || relativePath.includes("\0")) throw new Error("Remote source path is invalid");
68069
+ const lexical = resolve8(sourceRoot, relativePath);
68070
+ assertInsideRoot(sourceRoot, lexical);
68071
+ const canonical = realpathSync(lexical);
68072
+ assertInsideRoot(sourceRoot, canonical);
68073
+ return canonical;
68074
+ }
68075
+ function assertInsideRoot(sourceRoot, path) {
68076
+ const relation = relative2(sourceRoot, path);
68077
+ if (relation.startsWith("..") || resolve8(sourceRoot, relation) !== resolve8(path)) {
68078
+ throw new Error("Remote source path escapes the approved source root");
68079
+ }
68080
+ }
68081
+ function openedDescriptorPath(descriptor, requestedPath) {
68082
+ const procPath = `/proc/self/fd/${descriptor}`;
68083
+ if (existsSync10(procPath)) return realpathSync(procPath);
68084
+ return realpathSync(requestedPath);
68085
+ }
68086
+ function isBinaryFile(path) {
68087
+ let descriptor;
68088
+ try {
68089
+ descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
68090
+ const stats = fstatSync(descriptor);
68091
+ const buffer = Buffer.alloc(Math.min(stats.size, BINARY_PROBE_BYTES));
68092
+ const bytesRead = readSync2(descriptor, buffer, 0, buffer.length, 0);
68093
+ const bytes = new Uint8Array(buffer.subarray(0, bytesRead));
68094
+ if (bytes.includes(0)) return true;
68095
+ try {
68096
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
68097
+ return false;
68098
+ } catch {
68099
+ return true;
68100
+ }
68101
+ } catch {
68102
+ return true;
68103
+ } finally {
68104
+ if (descriptor !== void 0) closeSync2(descriptor);
68105
+ }
68106
+ }
68107
+ function normalizeBound(value, maximum, label) {
68108
+ const result = value ?? maximum;
68109
+ if (!Number.isInteger(result) || result <= 0 || result > maximum) {
68110
+ throw new Error(`Remote source ${label} limit must be between 1 and ${maximum}`);
68111
+ }
68112
+ return result;
68113
+ }
67409
68114
  function remoteAccessStorePath(homeDirectory) {
67410
68115
  return join8(homeDirectory, ".openmates", "remote-sources.json");
67411
68116
  }
@@ -67434,7 +68139,7 @@ function assertRemoteAccessSourceRecord(value, index) {
67434
68139
  }
67435
68140
 
67436
68141
  // src/selfUpdate.ts
67437
- import { spawnSync as spawnSync2 } from "child_process";
68142
+ import { spawnSync as spawnSync3 } from "child_process";
67438
68143
  import { readFileSync as readFileSync9 } from "fs";
67439
68144
  var PACKAGE_NAME = "openmates";
67440
68145
  var DEFAULT_TARGET = "latest";
@@ -67481,7 +68186,7 @@ function checkSelfUpdateStatus(plan) {
67481
68186
  }
67482
68187
  function runSelfUpdate(plan, options = {}) {
67483
68188
  if (plan.dryRun) return;
67484
- const result = spawnSync2(plan.command, plan.args, {
68189
+ const result = spawnSync3(plan.command, plan.args, {
67485
68190
  encoding: "utf-8",
67486
68191
  stdio: ["ignore", "pipe", "pipe"]
67487
68192
  });
@@ -67503,7 +68208,7 @@ function resolveTargetVersion(target) {
67503
68208
  if (/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9._-]+)?$/.test(target)) {
67504
68209
  return { version: target, error: null };
67505
68210
  }
67506
- const result = spawnSync2(commandName("npm"), ["view", `${PACKAGE_NAME}@${target}`, "version"], {
68211
+ const result = spawnSync3(commandName("npm"), ["view", `${PACKAGE_NAME}@${target}`, "version"], {
67507
68212
  encoding: "utf-8",
67508
68213
  stdio: ["ignore", "pipe", "pipe"]
67509
68214
  });
@@ -69940,34 +70645,62 @@ async function handleRemoteAccess(client, subcommand, rest, flags) {
69940
70645
  printRemoteAccessHelp();
69941
70646
  return;
69942
70647
  }
69943
- const rootPath = parseRemoteAccessRootPath(flags);
69944
- const sourceId = typeof flags["source-id"] === "string" ? flags["source-id"] : randomUUID10();
69945
- const projectId = typeof flags.project === "string" ? flags.project : void 0;
69946
- validateRemoteSourceRegistrationFlags(projectId, flags);
69947
- const sourceType = parseRemoteAccessSourceType(flags.type);
69948
- const displayName = typeof flags.name === "string" ? flags.name : sourceId;
69949
- const source = startRemoteAccessSource({ sourceId, projectId, rootPath, sourceType, displayName });
69950
- await maybeRegisterRemoteSource(client, source, flags);
69951
- if (flags.json === true) {
69952
- printJson2({ source, foreground: true });
69953
- } else {
69954
- console.log(`Remote access foreground session started: ${source.sourceId}`);
69955
- console.log(`Root: ${source.rootPath}`);
69956
- console.log(`Cache: ${source.cachePath}`);
70648
+ assertRemoteAccessPublicFlags(flags);
70649
+ const roots = resolveRemoteAccessRoots(typeof flags.path === "string" ? flags.path : void 0);
70650
+ const discovery = discoverRemoteAccessRepositories(roots);
70651
+ const candidateRoots = typeof flags.path === "string" ? roots : discovery.repositories.length > 0 ? discovery.repositories.map((candidate) => candidate.rootPath) : roots;
70652
+ const masterKey = client.getMasterKeyBytes();
70653
+ const projects = await loadProjects(client, masterKey, flags);
70654
+ const bindings = await resolveRemoteAccessBindings(client, masterKey, projects, candidateRoots, flags);
70655
+ if (bindings.length === 0) throw new Error("No Project folders were approved for remote access.");
70656
+ const sourceSessionId = randomUUID10();
70657
+ const controller = new AbortController();
70658
+ const stop = () => controller.abort();
70659
+ process.once("SIGINT", stop);
70660
+ process.once("SIGTERM", stop);
70661
+ if (flags.json !== true) {
70662
+ console.log(`Remote access connecting ${bindings.length} Project source${bindings.length === 1 ? "" : "s"}.`);
69957
70663
  console.log("Keep this terminal open. For long-running work, start it inside zellij, tmux, or screen.");
70664
+ if (discovery.permissionDenied.length > 0) {
70665
+ console.warn(`Skipped ${discovery.permissionDenied.length} unreadable folder${discovery.permissionDenied.length === 1 ? "" : "s"}.`);
70666
+ }
70667
+ }
70668
+ try {
70669
+ let confirmedTakeover = false;
70670
+ while (!controller.signal.aborted) {
70671
+ try {
70672
+ await runRemoteAccessBridge({
70673
+ client,
70674
+ sourceSessionId,
70675
+ bindings,
70676
+ signal: controller.signal,
70677
+ confirmedTakeover,
70678
+ onLifecycle: (event) => {
70679
+ if (flags.json === true) printJson2({ type: "remote_access_lifecycle", ...event });
70680
+ else if (event.state === "connected") console.log("Remote access connected.");
70681
+ else if (event.state === "reconnecting") console.log(`Remote access reconnecting in ${event.delayMs ?? 0}ms.`);
70682
+ else if (event.state === "disconnected") console.log("Remote access disconnected.");
70683
+ }
70684
+ });
70685
+ break;
70686
+ } catch (error) {
70687
+ if (!(error instanceof WebSocketProtocolError) || error.code !== "takeover_confirmation_required") throw error;
70688
+ if (flags.json === true || !process.stdin.isTTY) {
70689
+ throw new Error("Another remote-access process owns one of these sources; rerun interactively to confirm takeover.");
70690
+ }
70691
+ const answer = (await promptLine2("Another process owns one of these sources. Take over? [y/N] ")).trim().toLowerCase();
70692
+ if (answer !== "y" && answer !== "yes") throw new Error("Remote access takeover cancelled.");
70693
+ confirmedTakeover = true;
70694
+ }
70695
+ }
70696
+ } finally {
70697
+ process.off("SIGINT", stop);
70698
+ process.off("SIGTERM", stop);
69958
70699
  }
69959
70700
  return;
69960
70701
  }
69961
70702
  throw new Error("openmates remote-access does not accept public subcommands. Run 'openmates remote-access --help'.");
69962
70703
  }
69963
- function parseRemoteAccessRootPath(flags) {
69964
- const rootPath = flags.path;
69965
- if (rootPath === void 0) return process.cwd();
69966
- if (typeof rootPath !== "string") {
69967
- throw new Error("--path requires a folder value.");
69968
- }
69969
- return rootPath;
69970
- }
69971
70704
  function parsePositiveIntegerFlag(value, flagName) {
69972
70705
  if (value === void 0) return void 0;
69973
70706
  if (typeof value !== "string") {
@@ -69983,38 +70716,101 @@ function parseResponseTimeoutMs2(flags) {
69983
70716
  const seconds = parsePositiveIntegerFlag(flags["response-timeout-seconds"], "--response-timeout-seconds");
69984
70717
  return seconds === void 0 ? void 0 : seconds * 1e3;
69985
70718
  }
69986
- function parseRemoteAccessSourceType(value) {
69987
- if (value === void 0) return "local_folder";
69988
- if (value === "local_folder" || value === "local_git_repository") {
69989
- return value;
70719
+ function assertRemoteAccessPublicFlags(flags) {
70720
+ const internalFlags = ["source-id", "project", "type", "local-only", "encrypted-display-name", "encrypted-metadata"];
70721
+ const supplied = internalFlags.filter((name) => flags[name] !== void 0);
70722
+ if (supplied.length > 0) {
70723
+ throw new Error(`Unsupported remote-access option${supplied.length === 1 ? "" : "s"}: ${supplied.map((name) => `--${name}`).join(", ")}`);
69990
70724
  }
69991
- throw new Error("--type must be one of local_folder or local_git_repository for the local remote-access bridge");
69992
70725
  }
69993
- async function maybeRegisterRemoteSource(client, source, flags) {
69994
- if (!source.projectId || flags["local-only"] === true) return;
69995
- const encryptedDisplayName = flags["encrypted-display-name"];
69996
- const encryptedMetadata = flags["encrypted-metadata"];
69997
- if (typeof encryptedDisplayName !== "string" || typeof encryptedMetadata !== "string") {
69998
- throw new Error("Missing encrypted Project source metadata after registration validation");
70726
+ async function resolveRemoteAccessBindings(client, masterKey, projects, candidateRoots, flags) {
70727
+ const stored = listRemoteAccessSources();
70728
+ const resolved = [];
70729
+ const unresolved = [];
70730
+ for (const rootPath of candidateRoots) {
70731
+ const existingSource = stored.find((source) => source.rootPath === rootPath && source.projectId);
70732
+ const project = existingSource ? projects.find((item) => item.projectId === existingSource.projectId) : void 0;
70733
+ if (existingSource && project) {
70734
+ resolved.push({ rootPath, project, sourceId: existingSource.sourceId });
70735
+ } else {
70736
+ unresolved.push(rootPath);
70737
+ }
70738
+ }
70739
+ if (unresolved.length > 0) {
70740
+ if (flags.json === true || !process.stdin.isTTY) {
70741
+ throw new Error(`Remote access needs interactive Project review for: ${unresolved.join(", ")}`);
70742
+ }
70743
+ console.log("The following folders need OpenMates Projects:");
70744
+ unresolved.forEach((rootPath) => console.log(` - ${rootPath}`));
70745
+ const answer = (await promptLine2(`Create ${unresolved.length} missing Project${unresolved.length === 1 ? "" : "s"}? [y/N] `)).trim().toLowerCase();
70746
+ if (answer !== "y" && answer !== "yes") throw new Error("Remote access Project creation cancelled.");
70747
+ for (const rootPath of unresolved) {
70748
+ const project = await createEncryptedRemoteAccessProject(client, masterKey, basename3(rootPath));
70749
+ resolved.push({ rootPath, project, sourceId: randomUUID10() });
70750
+ }
70751
+ }
70752
+ const bindings = [];
70753
+ for (const item of resolved) {
70754
+ const sourceType = remoteAccessSourceType(item.rootPath);
70755
+ const source = startRemoteAccessSource({
70756
+ sourceId: item.sourceId,
70757
+ projectId: item.project.projectId,
70758
+ rootPath: item.rootPath,
70759
+ sourceType,
70760
+ displayName: basename3(item.rootPath)
70761
+ });
70762
+ const remoteSources = await client.listProjectSources(item.project.projectId);
70763
+ const remoteSource = remoteSources.find((entry) => entry.source_id === source.sourceId);
70764
+ if (!remoteSource) {
70765
+ const timestamp = Math.floor(Date.now() / 1e3);
70766
+ await client.createProjectSource(item.project.projectId, {
70767
+ source_id: source.sourceId,
70768
+ source_type: sourceType,
70769
+ encrypted_display_name: await encryptWithAesGcmCombined(source.displayName, item.project.projectKey),
70770
+ encrypted_metadata: await encryptWithAesGcmCombined(
70771
+ JSON.stringify({ root: source.rootPath, binding_root: source.rootPath }),
70772
+ item.project.projectKey
70773
+ ),
70774
+ capabilities: ["read", "search", "import"],
70775
+ status: "offline",
70776
+ created_at: timestamp,
70777
+ updated_at: timestamp
70778
+ });
70779
+ }
70780
+ bindings.push({ source, projectKey: item.project.projectKey, keyEpoch: 1 });
69999
70781
  }
70782
+ return bindings;
70783
+ }
70784
+ async function createEncryptedRemoteAccessProject(client, masterKey, name) {
70785
+ const projectKey = randomBytes8(32);
70786
+ const projectId = randomUUID10();
70000
70787
  const timestamp = Math.floor(Date.now() / 1e3);
70001
- await client.createProjectSource(source.projectId, {
70002
- source_id: source.sourceId,
70003
- source_type: source.sourceType,
70004
- encrypted_display_name: encryptedDisplayName,
70005
- encrypted_metadata: encryptedMetadata,
70006
- capabilities: ["read", "search", "import"],
70007
- status: source.status,
70788
+ const payload = {
70789
+ project_id: projectId,
70790
+ encrypted_project_key: await encryptBytesWithAesGcm(projectKey, masterKey),
70791
+ encrypted_name: await encryptWithAesGcmCombined(name, projectKey),
70792
+ encrypted_description: await encryptWithAesGcmCombined("", projectKey),
70793
+ encrypted_icon: await encryptWithAesGcmCombined("folder", projectKey),
70794
+ encrypted_color: await encryptWithAesGcmCombined("default", projectKey),
70795
+ pinned: false,
70796
+ archived: false,
70008
70797
  created_at: timestamp,
70009
- updated_at: timestamp
70010
- });
70011
- }
70012
- function validateRemoteSourceRegistrationFlags(projectId, flags) {
70013
- if (!projectId || flags["local-only"] === true) return;
70014
- if (typeof flags["encrypted-display-name"] === "string" && typeof flags["encrypted-metadata"] === "string") return;
70015
- throw new Error(
70016
- "remote-access with --project requires --local-only or both --encrypted-display-name and --encrypted-metadata"
70017
- );
70798
+ updated_at: timestamp,
70799
+ last_opened_at: timestamp
70800
+ };
70801
+ await client.createProject(payload);
70802
+ return {
70803
+ projectId,
70804
+ name,
70805
+ description: "",
70806
+ icon: "folder",
70807
+ color: "default",
70808
+ pinned: false,
70809
+ archived: false,
70810
+ version: 1,
70811
+ projectKey,
70812
+ encrypted: payload
70813
+ };
70018
70814
  }
70019
70815
  function shouldInitializeRedactor(command, subcommand) {
70020
70816
  return command === "chats" && ["new", "send", "answer-interactive", "incognito"].includes(subcommand ?? "");
@@ -76851,21 +77647,20 @@ Options:
76851
77647
  }
76852
77648
  function printRemoteAccessHelp() {
76853
77649
  console.log(`Remote access command:
76854
- openmates remote-access [--path <folder>] [--source-id <id>] [--project <id>] [--type <type>] [--local-only] [--json]
77650
+ openmates remote-access [--path <folder>]... [--json]
76855
77651
 
76856
77652
  Behavior:
76857
- Runs in the foreground and defaults to the current working directory when
76858
- --path is omitted. Keep the terminal open; use zellij, tmux, or screen for
76859
- long-running access sessions.
76860
-
76861
- Source types:
76862
- local_folder, local_git_repository
77653
+ Discovers repository Projects below the current working directory by default.
77654
+ Repeated --path values replace default discovery. Missing Projects are created
77655
+ only after interactive review. The command remains connected in the foreground;
77656
+ keep the terminal open or use zellij, tmux, or screen.
76863
77657
 
76864
77658
  Security:
76865
77659
  Source metadata is stored locally under ~/.openmates/remote-sources.json.
76866
77660
  Preview cache defaults to ~/.openmates/remote-cache/<source-id>.
76867
- Local searches are internal bridge capability only and run inside the approved
76868
- source root with high-risk, binary, and out-of-root paths excluded by default.`);
77661
+ Project metadata and bridge payloads are encrypted automatically. List, search,
77662
+ and text preview stay inside approved roots and exclude protected, ignored,
77663
+ binary, symlink-escaped, and out-of-root paths.`);
76869
77664
  }
76870
77665
  function printConnectedAccountsHelp() {
76871
77666
  console.log(`Connected account commands:
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  buildTravelConnectionsRequest,
4
4
  getExtForLang,
5
5
  serializeToYaml
6
- } from "./chunk-YRIKOMBJ.js";
6
+ } from "./chunk-3ZTRVFOG.js";
7
7
  import "./chunk-MLWBMJEN.js";
8
8
  export {
9
9
  buildTravelConnectionsRequest,
package/dist/index.d.ts CHANGED
@@ -69,6 +69,16 @@ interface ForceLogoutPayload {
69
69
  reason?: string;
70
70
  revoked_session_id?: string | null;
71
71
  }
72
+ interface ProjectRemoteAccessRequestFrame {
73
+ request_id: string;
74
+ project_id: string;
75
+ source_id: string;
76
+ source_session_id: string;
77
+ requesting_client_id: string;
78
+ operation: "list" | "search" | "read_text";
79
+ key_epoch: number;
80
+ encrypted_envelope: string;
81
+ }
72
82
  /** Streaming event dispatched for each chunk or lifecycle event. */
73
83
  interface StreamEvent {
74
84
  /** Event type. */
@@ -198,6 +208,11 @@ declare class OpenMatesWsClient {
198
208
  send(type: string, payload: unknown): void;
199
209
  sendAsync(type: string, payload: unknown): Promise<void>;
200
210
  onLocalConnectorRequest(handler: (payload: LocalConnectorRequestFrame) => void | Promise<void>): () => void;
211
+ onProjectRemoteAccessRequest(handler: (payload: ProjectRemoteAccessRequestFrame) => void | Promise<void>): () => void;
212
+ waitForClose(): Promise<{
213
+ code: number;
214
+ reason: string;
215
+ }>;
201
216
  private bufferPassiveTaskUpdateJobs;
202
217
  private handleForceLogout;
203
218
  drainPassiveTaskUpdateJobs(): PendingTaskUpdateJobFrame[];
@@ -2173,6 +2188,10 @@ declare class OpenMatesClient {
2173
2188
  }): Promise<Record<string, unknown>>;
2174
2189
  listProjectSources(projectId: string): Promise<ProjectSourceRecord[]>;
2175
2190
  createProjectSource(projectId: string, input: ProjectSourceCreateInput): Promise<ProjectSourceRecord>;
2191
+ openProjectRemoteAccessWebSocket(): Promise<{
2192
+ ws: OpenMatesWsClient;
2193
+ ownerId: string;
2194
+ }>;
2176
2195
  createProjectItem(projectId: string, input: ProjectItemCreateInput): Promise<ProjectItemRecord>;
2177
2196
  deleteProjectItemByTarget(projectId: string, itemType: "embed" | "chat" | "workflow", targetId: string): Promise<{
2178
2197
  deleted: boolean;
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  renderPlanList,
37
37
  renderSupportInfo,
38
38
  serializeToYaml
39
- } from "./chunk-YRIKOMBJ.js";
39
+ } from "./chunk-3ZTRVFOG.js";
40
40
  import "./chunk-MLWBMJEN.js";
41
41
  export {
42
42
  APP_SKILL_METADATA,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.15.0-alpha.97",
3
+ "version": "0.15.0-alpha.99",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "test:unit:workflows": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/workflows.test.ts",
31
31
  "test:unit:sdk-workflows": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/sdk-workflows.test.ts",
32
32
  "test:unit:remote-access": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/remoteSources.test.ts tests/remoteAccess.test.ts",
33
+ "test:unit:remote-access-crypto": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/remoteAccessCrypto.test.ts",
33
34
  "test:unit:teams": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/teams-permissions.test.ts",
34
35
  "test:unit:proton-bridge-connector": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/protonBridgeConnector.test.ts",
35
36
  "test:unit:proton-bridge-connector-integration": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/protonBridgeConnector.integration.test.ts",