@gatherthread/dsh-host 0.1.0-alpha.8 → 0.1.0-beta.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.
@@ -823,10 +823,10 @@ function mergeDefs(...defs) {
823
823
  function cloneDef(schema) {
824
824
  return mergeDefs(schema._zod.def);
825
825
  }
826
- function getElementAtPath(obj, path10) {
827
- if (!path10)
826
+ function getElementAtPath(obj, path11) {
827
+ if (!path11)
828
828
  return obj;
829
- return path10.reduce((acc, key) => acc?.[key], obj);
829
+ return path11.reduce((acc, key) => acc?.[key], obj);
830
830
  }
831
831
  function promiseAllObject(promisesObj) {
832
832
  const keys = Object.keys(promisesObj);
@@ -1166,11 +1166,11 @@ function explicitlyAborted(x, startIndex = 0) {
1166
1166
  }
1167
1167
  return false;
1168
1168
  }
1169
- function prefixIssues(path10, issues) {
1169
+ function prefixIssues(path11, issues) {
1170
1170
  return issues.map((iss) => {
1171
1171
  var _a3;
1172
1172
  (_a3 = iss).path ?? (_a3.path = []);
1173
- iss.path.unshift(path10);
1173
+ iss.path.unshift(path11);
1174
1174
  return iss;
1175
1175
  });
1176
1176
  }
@@ -1277,16 +1277,16 @@ function cleanEnum(obj) {
1277
1277
  }
1278
1278
  function base64ToUint8Array(base643) {
1279
1279
  const binaryString = atob(base643);
1280
- const bytes = new Uint8Array(binaryString.length);
1280
+ const bytes2 = new Uint8Array(binaryString.length);
1281
1281
  for (let i = 0; i < binaryString.length; i++) {
1282
- bytes[i] = binaryString.charCodeAt(i);
1282
+ bytes2[i] = binaryString.charCodeAt(i);
1283
1283
  }
1284
- return bytes;
1284
+ return bytes2;
1285
1285
  }
1286
- function uint8ArrayToBase64(bytes) {
1286
+ function uint8ArrayToBase64(bytes2) {
1287
1287
  let binaryString = "";
1288
- for (let i = 0; i < bytes.length; i++) {
1289
- binaryString += String.fromCharCode(bytes[i]);
1288
+ for (let i = 0; i < bytes2.length; i++) {
1289
+ binaryString += String.fromCharCode(bytes2[i]);
1290
1290
  }
1291
1291
  return btoa(binaryString);
1292
1292
  }
@@ -1295,22 +1295,22 @@ function base64urlToUint8Array(base64url3) {
1295
1295
  const padding = "=".repeat((4 - base643.length % 4) % 4);
1296
1296
  return base64ToUint8Array(base643 + padding);
1297
1297
  }
1298
- function uint8ArrayToBase64url(bytes) {
1299
- return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1298
+ function uint8ArrayToBase64url(bytes2) {
1299
+ return uint8ArrayToBase64(bytes2).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1300
1300
  }
1301
1301
  function hexToUint8Array(hex3) {
1302
1302
  const cleanHex = hex3.replace(/^0x/, "");
1303
1303
  if (cleanHex.length % 2 !== 0) {
1304
1304
  throw new Error("Invalid hex string length");
1305
1305
  }
1306
- const bytes = new Uint8Array(cleanHex.length / 2);
1306
+ const bytes2 = new Uint8Array(cleanHex.length / 2);
1307
1307
  for (let i = 0; i < cleanHex.length; i += 2) {
1308
- bytes[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16);
1308
+ bytes2[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16);
1309
1309
  }
1310
- return bytes;
1310
+ return bytes2;
1311
1311
  }
1312
- function uint8ArrayToHex(bytes) {
1313
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1312
+ function uint8ArrayToHex(bytes2) {
1313
+ return Array.from(bytes2).map((b) => b.toString(16).padStart(2, "0")).join("");
1314
1314
  }
1315
1315
  var Class = class {
1316
1316
  constructor(..._args) {
@@ -1620,16 +1620,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
1620
1620
  }
1621
1621
  function formatError(error62, mapper = (issue2) => issue2.message) {
1622
1622
  const fieldErrors = { _errors: [] };
1623
- const processError = (error63, path10 = []) => {
1623
+ const processError = (error63, path11 = []) => {
1624
1624
  for (const issue2 of error63.issues) {
1625
1625
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1626
- issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
1626
+ issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
1627
1627
  } else if (issue2.code === "invalid_key") {
1628
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
1628
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
1629
1629
  } else if (issue2.code === "invalid_element") {
1630
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
1630
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
1631
1631
  } else {
1632
- const fullpath = [...path10, ...issue2.path];
1632
+ const fullpath = [...path11, ...issue2.path];
1633
1633
  if (fullpath.length === 0) {
1634
1634
  fieldErrors._errors.push(mapper(issue2));
1635
1635
  } else {
@@ -1668,17 +1668,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
1668
1668
  }
1669
1669
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
1670
1670
  const result = { errors: [] };
1671
- const processError = (error63, path10 = []) => {
1671
+ const processError = (error63, path11 = []) => {
1672
1672
  var _a3;
1673
1673
  for (const issue2 of error63.issues) {
1674
1674
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1675
- issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
1675
+ issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
1676
1676
  } else if (issue2.code === "invalid_key") {
1677
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
1677
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
1678
1678
  } else if (issue2.code === "invalid_element") {
1679
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
1679
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
1680
1680
  } else {
1681
- const fullpath = [...path10, ...issue2.path];
1681
+ const fullpath = [...path11, ...issue2.path];
1682
1682
  if (fullpath.length === 0) {
1683
1683
  result.errors.push(mapper(issue2));
1684
1684
  continue;
@@ -1717,8 +1717,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
1717
1717
  }
1718
1718
  function toDotPath(_path) {
1719
1719
  const segs = [];
1720
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1721
- for (const seg of path10) {
1720
+ const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1721
+ for (const seg of path11) {
1722
1722
  if (typeof seg === "number")
1723
1723
  segs.push(`[${seg}]`);
1724
1724
  else if (typeof seg === "symbol")
@@ -18820,13 +18820,13 @@ function resolveRef(ref, ctx) {
18820
18820
  if (!ref.startsWith("#")) {
18821
18821
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
18822
18822
  }
18823
- const path10 = ref.slice(1).split("/").filter(Boolean);
18824
- if (path10.length === 0) {
18823
+ const path11 = ref.slice(1).split("/").filter(Boolean);
18824
+ if (path11.length === 0) {
18825
18825
  return ctx.rootSchema;
18826
18826
  }
18827
18827
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
18828
- if (path10[0] === defsKey) {
18829
- const key = path10[1] === void 0 ? void 0 : decodeJSONPointerSegment(path10[1]);
18828
+ if (path11[0] === defsKey) {
18829
+ const key = path11[1] === void 0 ? void 0 : decodeJSONPointerSegment(path11[1]);
18830
18830
  if (!key || !ctx.defs[key]) {
18831
18831
  throw new Error(`Reference not found: ${ref}`);
18832
18832
  }
@@ -19685,16 +19685,16 @@ var Commit = external_exports.string().regex(/^[a-f0-9]{40}$/);
19685
19685
  var BranchId = external_exports.string().regex(/^branch-[a-f0-9]{24}$/);
19686
19686
  var pathEncoder = new TextEncoder();
19687
19687
  var hfsIgnoredCharacters = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
19688
- function portablePathKey(path10) {
19689
- return path10.normalize("NFC").replace(hfsIgnoredCharacters, "").toLowerCase();
19688
+ function portablePathKey(path11) {
19689
+ return path11.normalize("NFC").replace(hfsIgnoredCharacters, "").toLowerCase();
19690
19690
  }
19691
19691
  function containsCodeSyncSecret(text) {
19692
- return /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----|\b(?:gh[pousr]|github_pat|glpat|sk|xox[baprs])[-_][A-Za-z0-9_-]{16,}|\b(?:gt[abidp]|acp(?:i|d)?)_[A-Za-z0-9_-]{20,}|\bAKIA[A-Z0-9]{16}\b/u.test(text);
19692
+ return /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----|\b(?:gh[pousr]|github_pat|glpat|sk|xox[baprs])[-_][A-Za-z0-9_-]{16,}|\b(?:(?:gt[abidp]|gteg?)|acp(?:i|d)?)_[A-Za-z0-9_-]{20,}|\bAKIA[A-Z0-9]{16}\b/u.test(text);
19693
19693
  }
19694
- function isCodeSyncPathAllowed(path10) {
19695
- if (!path10 || path10.length > 500 || path10.startsWith("/") || /[\\\u0000-\u001f\u007f\ud800-\udfff:<>"|?*]/u.test(path10))
19694
+ function isCodeSyncPathAllowed(path11) {
19695
+ if (!path11 || path11.length > 500 || path11.startsWith("/") || /[\\\u0000-\u001f\u007f\ud800-\udfff:<>"|?*]/u.test(path11))
19696
19696
  return false;
19697
- const components = path10.split("/");
19697
+ const components = path11.split("/");
19698
19698
  return components.every((part) => {
19699
19699
  const lower = portablePathKey(part);
19700
19700
  if (!part || pathEncoder.encode(part).byteLength > 255 || part === "." || part === ".." || part.endsWith(".") || part.endsWith(" "))
@@ -19714,34 +19714,42 @@ function isCodeSyncPathAllowed(path10) {
19714
19714
  return !/\.(?:pem|p12|pfx|key)$/iu.test(lower) && !/^(?:id_rsa|id_ed25519)(?:\.|$)/iu.test(lower);
19715
19715
  });
19716
19716
  }
19717
- var CodeFileSchema = external_exports.object({
19718
- path: external_exports.string().refine(isCodeSyncPathAllowed, "Unsafe or private code path"),
19719
- content_base64: external_exports.string().max(4 * Math.ceil(CODE_SYNC_MAX_FILE_BYTES / 3)).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
19720
- executable: external_exports.boolean()
19721
- }).strict();
19722
- var CodeFilesSchema = external_exports.array(CodeFileSchema).max(CODE_SYNC_MAX_FILES).superRefine((files, context) => {
19723
- let size = 0;
19724
- const names = /* @__PURE__ */ new Set();
19725
- for (const [index, file2] of files.entries()) {
19726
- const bytes = file2.content_base64.length * 3 / 4 - (file2.content_base64.endsWith("==") ? 2 : file2.content_base64.endsWith("=") ? 1 : 0);
19727
- size += bytes;
19728
- const normalized = portablePathKey(file2.path);
19729
- if (bytes > CODE_SYNC_MAX_FILE_BYTES || names.has(normalized))
19730
- context.addIssue({ code: "custom", path: [index], message: "File limit or duplicate portable path" });
19731
- names.add(normalized);
19732
- }
19733
- if (size > CODE_SYNC_MAX_SNAPSHOT_BYTES)
19734
- context.addIssue({ code: "custom", message: "Code snapshot exceeds 8 MiB" });
19735
- for (const name2 of names) {
19736
- const parts = name2.split("/");
19737
- parts.pop();
19738
- while (parts.length) {
19739
- if (names.has(parts.join("/")))
19740
- context.addIssue({ code: "custom", message: "File and directory paths overlap" });
19717
+ var CLOUD_CODE_SYNC_LIMITS = { maxFiles: CODE_SYNC_MAX_FILES, maxFileBytes: CODE_SYNC_MAX_FILE_BYTES, maxSnapshotBytes: CODE_SYNC_MAX_SNAPSHOT_BYTES };
19718
+ var GITHUB_CODE_SYNC_LIMITS = { maxFiles: 1e4, maxFileBytes: 20 * 1024 * 1024, maxSnapshotBytes: 128 * 1024 * 1024 };
19719
+ function createCodeFileSchema(limits) {
19720
+ return external_exports.object({
19721
+ path: external_exports.string().refine(isCodeSyncPathAllowed, "Unsafe or private code path"),
19722
+ content_base64: external_exports.string().max(4 * Math.ceil(limits.maxFileBytes / 3)).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
19723
+ executable: external_exports.boolean()
19724
+ }).strict();
19725
+ }
19726
+ var CodeFileSchema = createCodeFileSchema(CLOUD_CODE_SYNC_LIMITS);
19727
+ function createCodeFilesSchema(limits) {
19728
+ return external_exports.array(createCodeFileSchema(limits)).max(limits.maxFiles).superRefine((files, context) => {
19729
+ let size = 0;
19730
+ const names = /* @__PURE__ */ new Set();
19731
+ for (const [index, file2] of files.entries()) {
19732
+ const bytes2 = file2.content_base64.length * 3 / 4 - (file2.content_base64.endsWith("==") ? 2 : file2.content_base64.endsWith("=") ? 1 : 0);
19733
+ size += bytes2;
19734
+ const normalized = portablePathKey(file2.path);
19735
+ if (bytes2 > limits.maxFileBytes || names.has(normalized))
19736
+ context.addIssue({ code: "custom", path: [index], message: "File limit or duplicate portable path" });
19737
+ names.add(normalized);
19738
+ }
19739
+ if (size > limits.maxSnapshotBytes)
19740
+ context.addIssue({ code: "custom", message: "Code snapshot exceeds provider limit" });
19741
+ for (const name2 of names) {
19742
+ const parts = name2.split("/");
19741
19743
  parts.pop();
19744
+ while (parts.length) {
19745
+ if (names.has(parts.join("/")))
19746
+ context.addIssue({ code: "custom", message: "File and directory paths overlap" });
19747
+ parts.pop();
19748
+ }
19742
19749
  }
19743
- }
19744
- });
19750
+ });
19751
+ }
19752
+ var CodeFilesSchema = createCodeFilesSchema(CLOUD_CODE_SYNC_LIMITS);
19745
19753
  var CodeEnableInputSchema = external_exports.object({ idempotency_key: Key }).strict();
19746
19754
  var CodeDisableInputSchema = external_exports.object({ idempotency_key: Key }).strict();
19747
19755
  var CodeCheckpointInputSchema = external_exports.object({
@@ -19803,6 +19811,47 @@ var CodeSnapshotResultSchema = external_exports.object({ snapshot: external_expo
19803
19811
  }).strict() }).strict();
19804
19812
  var codeSyncRequestKinds = ["code_sync_status", "code_upload", "code_download", "code_recover", "code_auto_upload_enable", "code_auto_upload_disable"];
19805
19813
 
19814
+ // ../protocol/dist/src/github-sync.js
19815
+ var GitHubRepositorySchema = external_exports.string().max(140).regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?\/[A-Za-z0-9_.-]{1,100}$/).refine((value) => {
19816
+ const repository = value.split("/")[1];
19817
+ return repository !== "." && repository !== ".." && !repository.toLowerCase().endsWith(".git");
19818
+ }, "Use owner/repository without a .git suffix");
19819
+ function isGitHubBranchAllowed(value) {
19820
+ return value.length > 0 && value.length <= 200 && /^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(value) && !value.includes("..") && value.split("/").every((part) => !!part && !part.startsWith(".") && !part.endsWith(".") && !part.toLowerCase().endsWith(".lock")) && value !== "HEAD";
19821
+ }
19822
+ var GitHubBranchSchema = external_exports.string().refine(isGitHubBranchAllowed, "Unsafe GitHub branch name");
19823
+ var GitHubBaseBranchSchema = GitHubBranchSchema.refine((value) => !value.startsWith("gatherthread/"), "The shared base cannot use the GatherThread personal branch namespace");
19824
+ var GitHubPersonalBranchSchema = external_exports.string().regex(/^gatherthread\/[a-f0-9]{24}\/[a-f0-9]{24}$/);
19825
+ var GitHubConnectionSchema = external_exports.object({
19826
+ repository: GitHubRepositorySchema,
19827
+ base_branch: GitHubBaseBranchSchema,
19828
+ enabled: external_exports.boolean(),
19829
+ revision: external_exports.string().uuid()
19830
+ }).strict();
19831
+ var GitHubProjectStatusSchema = external_exports.object({
19832
+ connection: GitHubConnectionSchema.nullable(),
19833
+ branch: GitHubPersonalBranchSchema,
19834
+ can_configure: external_exports.boolean(),
19835
+ // GatherThread membership only. GitHub permissions must be checked locally.
19836
+ can_write: external_exports.boolean()
19837
+ }).strict();
19838
+ var GitHubConnectionInputSchema = external_exports.object({
19839
+ repository: GitHubRepositorySchema,
19840
+ base_branch: GitHubBaseBranchSchema,
19841
+ enabled: external_exports.boolean(),
19842
+ expected_revision: external_exports.string().uuid().nullable()
19843
+ }).strict();
19844
+ var githubCodeSyncRequestKinds = [
19845
+ "github_auth_connect",
19846
+ "github_code_sync_status",
19847
+ "github_code_upload",
19848
+ "github_code_download",
19849
+ "github_code_recover",
19850
+ "github_code_auto_upload_enable",
19851
+ "github_code_auto_upload_disable",
19852
+ "github_code_update"
19853
+ ];
19854
+
19806
19855
  // ../protocol/dist/src/history-summary.js
19807
19856
  var HISTORY_SUMMARY_MAX_SOURCE_BYTES = 20 * 1024;
19808
19857
  var HISTORY_SUMMARY_MAX_PROMPT_BYTES = 30 * 1024;
@@ -19827,6 +19876,155 @@ function historySummaryMarker(event) {
19827
19876
  return { version: 1, source_event_ids: ids, source_digest: marker.source_digest };
19828
19877
  }
19829
19878
 
19879
+ // ../protocol/dist/src/test-gate.js
19880
+ var TestGateExchangeInputSchema = external_exports.object({
19881
+ admission_code: external_exports.string().regex(/^gte_[A-Za-z0-9_-]{43}$/)
19882
+ }).strict();
19883
+ var TestGateStatusSchema = external_exports.object({
19884
+ enabled: external_exports.boolean(),
19885
+ admitted: external_exports.boolean(),
19886
+ environment: external_exports.enum(["production", "test"])
19887
+ }).strict();
19888
+
19889
+ // ../protocol/dist/src/hosted-github.js
19890
+ var Key2 = external_exports.string().min(8).max(200);
19891
+ var HostedGithubRepositoryInputSchema = external_exports.object({
19892
+ repository: external_exports.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u).max(140),
19893
+ base_branch: external_exports.string().min(1).max(200).refine((v) => !/[\s~^:?*\[\\]/u.test(v) && !v.includes("..") && !v.startsWith("-") && !v.endsWith("/") && !v.endsWith(".lock"))
19894
+ }).strict();
19895
+ var HostedGithubTaskInputSchema = external_exports.object({
19896
+ content: external_exports.string().trim().min(1).max(6e3),
19897
+ profile_id: external_exports.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/u),
19898
+ idempotency_key: Key2,
19899
+ continue_task_id: external_exports.string().regex(/^gh-task-[a-f0-9]{32}$/u).optional(),
19900
+ reply_to_event_id: external_exports.string().min(1).max(128).nullable().optional()
19901
+ }).strict();
19902
+ var HostedGithubCompleteInputSchema = external_exports.object({
19903
+ state: external_exports.string().regex(/^[A-Za-z0-9_-]{43}$/u),
19904
+ code: external_exports.string().min(1).max(500)
19905
+ }).strict();
19906
+ var HostedGithubPrInputSchema = external_exports.object({
19907
+ title: external_exports.string().trim().min(1).max(200),
19908
+ body: external_exports.string().max(6e3),
19909
+ expected_revision: external_exports.string().regex(/^[a-f0-9]{64}$/u)
19910
+ }).strict();
19911
+ var Login = external_exports.string().regex(/^[A-Za-z0-9-]{1,39}$/u);
19912
+ var TaskId = external_exports.string().regex(/^gh-task-[a-f0-9]{32}$/u);
19913
+ var Sha = external_exports.string().regex(/^[a-f0-9]{40}$/u).nullable();
19914
+ var Revision = external_exports.string().regex(/^[a-f0-9]{64}$/u).nullable();
19915
+ var PullUrl = external_exports.string().max(2048).regex(/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/u).nullable();
19916
+ var HostedGithubBindingSchema = HostedGithubRepositoryInputSchema;
19917
+ var HostedGithubStatusSchema = external_exports.union([
19918
+ external_exports.object({ enabled: external_exports.literal(false) }).strict(),
19919
+ external_exports.object({
19920
+ enabled: external_exports.literal(true),
19921
+ connected: external_exports.boolean(),
19922
+ login: Login.nullable(),
19923
+ binding: HostedGithubBindingSchema.nullable(),
19924
+ installation_url: external_exports.string().max(2048).regex(/^https:\/\/github\.com\/apps\/[A-Za-z0-9_.-]+\/installations\/new$/u)
19925
+ }).strict().refine((v) => v.connected ? v.login !== null : v.login === null && v.binding === null)
19926
+ ]);
19927
+ var HostedGithubAuthorizationSchema = external_exports.object({ authorization_url: external_exports.string().max(4096).refine((value) => {
19928
+ try {
19929
+ const url2 = new URL(value);
19930
+ return url2.origin === "https://github.com" && url2.pathname === "/login/oauth/authorize" && !url2.username && !url2.password && !url2.hash && /^[A-Za-z0-9_-]{43}$/u.test(url2.searchParams.get("state") ?? "") && [...url2.searchParams.keys()].every((key) => ["client_id", "redirect_uri", "state", "code_challenge", "code_challenge_method", "prompt"].includes(key));
19931
+ } catch {
19932
+ return false;
19933
+ }
19934
+ }) }).strict();
19935
+ var HostedGithubConnectionSchema = external_exports.object({ connected: external_exports.literal(true), login: Login }).strict();
19936
+ var HostedGithubDisconnectionSchema = external_exports.object({ connected: external_exports.literal(false) }).strict();
19937
+ var HostedGithubTaskSummarySchema = external_exports.object({
19938
+ id: TaskId,
19939
+ session_id: external_exports.string().min(1).max(128),
19940
+ profile_id: HostedGithubTaskInputSchema.shape.profile_id,
19941
+ resumable: external_exports.boolean(),
19942
+ state: external_exports.enum(["running", "completed", "failed", "interrupted"]),
19943
+ repository: HostedGithubRepositoryInputSchema.shape.repository,
19944
+ base_branch: HostedGithubRepositoryInputSchema.shape.base_branch,
19945
+ base_sha: Sha,
19946
+ revision: Revision,
19947
+ error_code: external_exports.string().regex(/^[a-z][a-z0-9_]{0,63}$/u).nullable(),
19948
+ pull_request_url: PullUrl,
19949
+ expires_at: external_exports.number().int().positive().max(Number.MAX_SAFE_INTEGER)
19950
+ }).strict();
19951
+ var bytes = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
19952
+ var Source = external_exports.string().max(2796204).regex(/^[A-Za-z0-9+/]*={0,2}$/u).refine((value) => value.length % 4 === 0 && bytes(value) <= 2 * 1024 * 1024).nullable();
19953
+ var HostedGithubChangeSchema = external_exports.object({
19954
+ path: external_exports.string().max(4096).refine(isCodeSyncPathAllowed),
19955
+ before_base64: Source,
19956
+ after_base64: Source,
19957
+ before_executable: external_exports.boolean().nullable(),
19958
+ after_executable: external_exports.boolean().nullable()
19959
+ }).strict().refine((v) => v.before_base64 === null === (v.before_executable === null) && v.after_base64 === null === (v.after_executable === null) && (v.before_base64 !== null || v.after_base64 !== null));
19960
+ var HostedGithubTaskSchema = HostedGithubTaskSummarySchema.extend({
19961
+ answer: external_exports.string().max(14e3).nullable(),
19962
+ changes: external_exports.array(HostedGithubChangeSchema).max(2e3)
19963
+ }).strict().refine((v) => {
19964
+ if (new Set(v.changes.map((c) => c.path)).size !== v.changes.length)
19965
+ return false;
19966
+ for (const side of ["before_base64", "after_base64"]) {
19967
+ const values = v.changes.map((c) => c[side]).filter((s) => s !== null);
19968
+ if (values.length > 1e3 || values.reduce((sum, s) => sum + bytes(s), 0) > 8 * 1024 * 1024)
19969
+ return false;
19970
+ }
19971
+ return !v.resumable || v.state !== "running" && v.pull_request_url === null;
19972
+ });
19973
+ var HostedGithubTaskListSchema = external_exports.object({ tasks: external_exports.array(HostedGithubTaskSummarySchema).max(10) }).strict();
19974
+
19975
+ // ../protocol/dist/src/registration.js
19976
+ var RegistrationEmailSchema = external_exports.string().max(254).trim().toLowerCase().regex(/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/).refine((value) => value.split("@")[0].length <= 64 && value.split("@")[1].split(".").every((part) => part.length <= 63));
19977
+ var Label = external_exports.string().regex(/^[^\u0000-\u001f\u007f-\u009f]*$/u).trim().min(1).max(120);
19978
+ var SendRegistrationInputSchema = external_exports.object({
19979
+ email: RegistrationEmailSchema,
19980
+ challenge_token: external_exports.string().min(1).max(2048),
19981
+ idempotency_key: external_exports.string().uuid(),
19982
+ locale: external_exports.enum(["en", "zh-CN"])
19983
+ }).strict();
19984
+ var AccountPasswordSchema = external_exports.string().min(12).max(128).refine((value) => new TextEncoder().encode(value).byteLength <= 512);
19985
+ var EmailLoginInputSchema = external_exports.object({
19986
+ email: RegistrationEmailSchema,
19987
+ password: AccountPasswordSchema,
19988
+ device_name: Label,
19989
+ remember_device: external_exports.boolean().default(false)
19990
+ }).strict();
19991
+ var VerifyRegistrationInputSchema = external_exports.object({
19992
+ registration_id: external_exports.string().uuid(),
19993
+ code: external_exports.string().regex(/^\d{8}$/),
19994
+ display_name: Label,
19995
+ device_name: Label,
19996
+ remember_device: external_exports.boolean().default(false),
19997
+ privacy_acknowledged: external_exports.literal(true),
19998
+ password: AccountPasswordSchema
19999
+ }).strict();
20000
+ var RegistrationStatusSchema = external_exports.object({
20001
+ enabled: external_exports.boolean(),
20002
+ site_key: external_exports.string().nullable(),
20003
+ challenge_binding: external_exports.string().nullable()
20004
+ }).strict();
20005
+ var RegistrationSentSchema = external_exports.object({
20006
+ registration_id: external_exports.string().uuid(),
20007
+ expires_in_seconds: external_exports.literal(600),
20008
+ resend_after_seconds: external_exports.literal(60)
20009
+ }).strict();
20010
+ var PasswordResetSentSchema = external_exports.object({
20011
+ reset_id: external_exports.string().uuid(),
20012
+ expires_in_seconds: external_exports.literal(600),
20013
+ resend_after_seconds: external_exports.literal(60)
20014
+ }).strict();
20015
+ var VerifyPasswordResetInputSchema = external_exports.object({
20016
+ reset_id: external_exports.string().uuid(),
20017
+ email: RegistrationEmailSchema,
20018
+ code: external_exports.string().regex(/^\d{8}$/),
20019
+ password: AccountPasswordSchema,
20020
+ password_confirmation: AccountPasswordSchema,
20021
+ locale: external_exports.enum(["en", "zh-CN"])
20022
+ }).strict().refine((value) => value.password === value.password_confirmation);
20023
+ var EmailAccountSessionSchema = external_exports.object({
20024
+ actor: external_exports.object({ user_id: external_exports.string(), display_name: external_exports.string(), device_id: external_exports.string(), can_create_projects: external_exports.literal(true) }).strict(),
20025
+ expires_at: external_exports.string().datetime()
20026
+ }).strict();
20027
+
19830
20028
  // ../protocol/dist/src/index.js
19831
20029
  var sessionModes = ["solo", "multi"];
19832
20030
  var membershipRoles = ["owner", "participant", "viewer"];
@@ -19971,6 +20169,43 @@ var AppendEventInputSchema = external_exports.object({
19971
20169
  observed_model: external_exports.string().trim().min(1).max(160).regex(/^[^\u0000-\u001f\u007f-\u009f]+$/u).optional(),
19972
20170
  observed_reasoning_effort: external_exports.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f-\u009f]+$/u).optional()
19973
20171
  });
20172
+ var HostedAgentRequestInputSchema = external_exports.object({
20173
+ profile_id: external_exports.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/u).default("default"),
20174
+ content: external_exports.string().trim().min(1).max(6e3),
20175
+ include_code: external_exports.boolean().default(false),
20176
+ idempotency_key: IdempotencyKeySchema,
20177
+ reply_to_event_id: IdSchema.nullable().optional(),
20178
+ github_task_id: external_exports.string().regex(/^gh-task-[a-f0-9]{32}$/u).optional()
20179
+ }).strict();
20180
+ var HostedAgentStatusSchema = external_exports.discriminatedUnion("enabled", [
20181
+ external_exports.object({ enabled: external_exports.literal(false) }).strict(),
20182
+ external_exports.object({
20183
+ enabled: external_exports.literal(true),
20184
+ harness: external_exports.literal("opencode"),
20185
+ utc_day: external_exports.string(),
20186
+ user_limit_runs: external_exports.number().int().nonnegative().nullable(),
20187
+ user_used_runs: external_exports.number().int().nonnegative(),
20188
+ global_limit_runs: external_exports.number().int().nonnegative().nullable(),
20189
+ global_used_runs: external_exports.number().int().nonnegative(),
20190
+ rate_limits: external_exports.object({
20191
+ user_min_interval_seconds: external_exports.number().int().min(1).max(3600),
20192
+ user_max_concurrent: external_exports.number().int().min(1).max(8),
20193
+ user_active_runs: external_exports.number().int().nonnegative(),
20194
+ retry_after_seconds: external_exports.number().int().nonnegative()
20195
+ }).strict().optional(),
20196
+ profiles: external_exports.array(external_exports.object({
20197
+ id: external_exports.string(),
20198
+ label: external_exports.string(),
20199
+ provider: external_exports.string(),
20200
+ model: external_exports.string(),
20201
+ available: external_exports.boolean(),
20202
+ capacity: external_exports.number().int().nonnegative(),
20203
+ status: external_exports.enum(["available", "busy", "daily_limit", "cooldown", "user_busy", "rate_limit"])
20204
+ }).strict()),
20205
+ capabilities: external_exports.array(external_exports.string()),
20206
+ privacy: external_exports.string()
20207
+ }).strict()
20208
+ ]);
19974
20209
  var SingleLineTitleSchema = external_exports.string().regex(/^[^\u0000-\u001f\u007f-\u009f]*$/u, "Title must not contain control characters").trim().min(1).max(200);
19975
20210
  var SessionTitleSchema = SingleLineTitleSchema;
19976
20211
  var ProjectTitleSchema = SingleLineTitleSchema;
@@ -20017,7 +20252,12 @@ var ProjectListItemSchema = ProjectRecordSchema.omit({ owner_user_id: true }).ex
20017
20252
  role: MembershipRoleSchema,
20018
20253
  session_count: external_exports.number().int().nonnegative()
20019
20254
  });
20255
+ var AvatarIdSchema = external_exports.enum(["cat", "fox", "bear", "rabbit", "penguin", "owl"]);
20256
+ var SetAccountAvatarInputSchema = external_exports.object({ avatar_id: AvatarIdSchema.nullable() }).strict();
20257
+ var AccountAvatarProfileSchema = external_exports.object({ user_id: IdSchema, avatar_id: AvatarIdSchema.nullable() });
20258
+ var SessionAvatarProfilesSchema = external_exports.object({ profiles: external_exports.array(AccountAvatarProfileSchema) });
20020
20259
  var ProjectMemberRecordSchema = external_exports.object({
20260
+ avatar_id: AvatarIdSchema.nullable().optional(),
20021
20261
  user_id: IdSchema,
20022
20262
  display_name: external_exports.string().trim().min(1).max(120),
20023
20263
  role: MembershipRoleSchema
@@ -20232,6 +20472,7 @@ var snapshotRequestStatuses = ["pending", "claimed", "completed", "failed"];
20232
20472
  var SnapshotRequestStatusSchema = external_exports.enum(snapshotRequestStatuses);
20233
20473
  var snapshotRequestKinds = [
20234
20474
  ...codeSyncRequestKinds,
20475
+ ...githubCodeSyncRequestKinds,
20235
20476
  "immutable",
20236
20477
  "visible_history_replace",
20237
20478
  "local_sync_status",
@@ -20553,9 +20794,9 @@ var HttpCollaborationClient = class {
20553
20794
  }));
20554
20795
  return fromWireSnapshotRequest(body.snapshot_request ?? body);
20555
20796
  }
20556
- async #request(path10, init = {}) {
20797
+ async #request(path11, init = {}) {
20557
20798
  const signal = combineSignals(init.signal ?? void 0, this.#signal, AbortSignal.timeout(this.#requestTimeoutMs));
20558
- const response = await this.#fetch(`${this.#baseUrl}${path10}`, {
20799
+ const response = await this.#fetch(`${this.#baseUrl}${path11}`, {
20559
20800
  ...init,
20560
20801
  redirect: "error",
20561
20802
  signal,
@@ -21228,10 +21469,14 @@ var ProjectCodeSync = class _ProjectCodeSync {
21228
21469
  #apiUrl;
21229
21470
  #root = "";
21230
21471
  #stateRoot = "";
21472
+ #lockRoot = "";
21231
21473
  #initialized;
21232
21474
  #stableDigest = "";
21233
21475
  #stableSince = 0;
21234
21476
  #lastAutomaticAttempt = 0;
21477
+ get #limits() {
21478
+ return this.#options.storage?.limits ?? CLOUD_CODE_SYNC_LIMITS;
21479
+ }
21235
21480
  constructor(options) {
21236
21481
  const url2 = new URL(options.apiUrl);
21237
21482
  if (url2.username || url2.password || url2.search || url2.hash || url2.protocol !== "https:" && !(url2.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname))) {
@@ -21265,6 +21510,9 @@ var ProjectCodeSync = class _ProjectCodeSync {
21265
21510
  this.#root = path2.join(await realpath(path2.dirname(requestedRoot)), path2.basename(requestedRoot));
21266
21511
  }
21267
21512
  this.#stateRoot = this.#options.stateRoot ?? path2.join(homedir2(), ".gatherthread", "code-sync", digest(this.#root));
21513
+ const sharedRoot = this.#stateRoot;
21514
+ if (this.#options.storage)
21515
+ this.#stateRoot = path2.join(this.#stateRoot, digest(this.#options.storage.key));
21268
21516
  if (isWithin(this.#root, path2.resolve(this.#stateRoot)))
21269
21517
  throw new CodeSyncError("code_sync_binding", "Code sync state must stay outside source files.");
21270
21518
  await mkdir2(this.#stateRoot, { recursive: true, mode: 448 });
@@ -21274,6 +21522,7 @@ var ProjectCodeSync = class _ProjectCodeSync {
21274
21522
  if (isWithin(this.#root, this.#stateRoot))
21275
21523
  throw new CodeSyncError("code_sync_binding", "Private code sync state resolves inside source files.");
21276
21524
  await chmod2(this.#stateRoot, 448);
21525
+ this.#lockRoot = await realpath(sharedRoot);
21277
21526
  await this.#locked(async () => {
21278
21527
  const inventoryPath = path2.join(this.#stateRoot, "inventory.git");
21279
21528
  try {
@@ -21284,6 +21533,7 @@ var ProjectCodeSync = class _ProjectCodeSync {
21284
21533
  throw error62;
21285
21534
  }
21286
21535
  await git(["init", "--bare", "--quiet", inventoryPath], this.#stateRoot);
21536
+ await this.#options.storage?.initialize(this.#stateRoot);
21287
21537
  const filename = path2.join(this.#stateRoot, "binding.json");
21288
21538
  try {
21289
21539
  await this.#readBinding();
@@ -21297,7 +21547,9 @@ var ProjectCodeSync = class _ProjectCodeSync {
21297
21547
  actor_id: this.#options.actorId,
21298
21548
  workspace_path: this.#root,
21299
21549
  base_commit: null,
21300
- automatic_upload: false,
21550
+ // GitHub authorization is an explicit local action. New GitHub
21551
+ // bindings follow settled source edits automatically thereafter.
21552
+ automatic_upload: this.#options.storage?.metadata.provider === "github",
21301
21553
  baseline: {}
21302
21554
  }));
21303
21555
  }
@@ -21348,7 +21600,7 @@ var ProjectCodeSync = class _ProjectCodeSync {
21348
21600
  idempotency_key: `code-upload-${digest(JSON.stringify([this.#options.projectId, this.#options.actorId, uploadBase, inventory.hashes]))}`
21349
21601
  };
21350
21602
  await this.#requireWorkspace();
21351
- const parsed = CodeMutationResultSchema.safeParse(await this.#request("/checkpoints", body));
21603
+ const parsed = CodeMutationResultSchema.safeParse(this.#options.storage ? await this.#options.storage.checkpoint(body) : await this.#request("/checkpoints", body));
21352
21604
  if (!parsed.success)
21353
21605
  throw invalidResponse();
21354
21606
  const verified = await this.#snapshot(parsed.data.status);
@@ -21389,7 +21641,7 @@ var ProjectCodeSync = class _ProjectCodeSync {
21389
21641
  await this.#save(binding);
21390
21642
  },
21391
21643
  baselineExecutables: new Set(binding.executable_paths ?? [])
21392
- });
21644
+ }, this.#limits);
21393
21645
  const verified = await this.#inventory(new Set(snapshot.files.filter((file2) => file2.executable).map((file2) => file2.path)));
21394
21646
  if (!sameHashes(verified.hashes, nextHashes))
21395
21647
  throw new CodeSyncError("code_sync_conflict", "Local files changed during download. Check the workspace before retrying.");
@@ -21430,11 +21682,25 @@ var ProjectCodeSync = class _ProjectCodeSync {
21430
21682
  const recoveryName = `${recoveryStem.join("")}-recovered-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/gu, "").slice(0, 14)}-${randomUUID().slice(0, 8)}`;
21431
21683
  const recoveryPath = path2.join(path2.dirname(this.#root), recoveryName);
21432
21684
  await mkdir2(recoveryPath, { mode: 448 });
21433
- await applySnapshot(recoveryPath, snapshot.files, {});
21685
+ await applySnapshot(recoveryPath, snapshot.files, {}, void 0, this.#limits);
21434
21686
  const restored = new _ProjectCodeSync({
21435
21687
  ...this.#options,
21436
21688
  workspacePath: recoveryPath,
21437
- stateRoot: path2.join(path2.dirname(this.#stateRoot), digest(recoveryPath))
21689
+ ...this.#options.stateRoot ? { stateRoot: path2.join(path2.dirname(this.#options.stateRoot), digest(recoveryPath)) } : {},
21690
+ // The transport has private mutable Git state; recovery only seeds the
21691
+ // baseline and does not need another transport initialization here.
21692
+ ...this.#options.storage ? { storage: {
21693
+ ...this.#options.storage,
21694
+ key: this.#options.storage.key,
21695
+ limits: this.#limits,
21696
+ metadata: this.#options.storage.metadata,
21697
+ initialize: async () => {
21698
+ },
21699
+ status: () => this.#cloudStatus(),
21700
+ snapshot: (status) => this.#snapshot(status),
21701
+ checkpoint: (input2) => this.#options.storage.checkpoint(input2),
21702
+ update: () => this.#options.storage.update()
21703
+ } } : {}
21438
21704
  });
21439
21705
  await restored.initialize();
21440
21706
  await restored.#locked(async () => {
@@ -21460,7 +21726,18 @@ var ProjectCodeSync = class _ProjectCodeSync {
21460
21726
  await this.initialize();
21461
21727
  return this.#locked(async () => {
21462
21728
  const binding = await this.#readBinding();
21463
- const cloud = await this.#cloudStatus();
21729
+ if (!enabled) {
21730
+ binding.automatic_upload = false;
21731
+ await this.#save(binding);
21732
+ }
21733
+ let cloud;
21734
+ try {
21735
+ cloud = await this.#cloudStatus();
21736
+ } catch (error62) {
21737
+ if (enabled)
21738
+ throw error62;
21739
+ return { ...this.#status(binding, { repository: { enabled: false, main_commit: null }, branches: [], own_branch_id: null }, { files: [], hashes: binding.baseline, excluded: 0 }), local_status_unknown: true };
21740
+ }
21464
21741
  if (enabled)
21465
21742
  assertEnabled(cloud);
21466
21743
  binding.automatic_upload = enabled;
@@ -21505,7 +21782,12 @@ var ProjectCodeSync = class _ProjectCodeSync {
21505
21782
  return this.#upload(inventory.hashes);
21506
21783
  }
21507
21784
  async execute(kind, { busy = false, operationId } = {}) {
21508
- if (busy && ["code_upload", "code_download", "code_recover", "code_auto_upload_enable"].includes(kind)) {
21785
+ if (kind.startsWith("github_") !== (this.#options.storage?.metadata.provider === "github")) {
21786
+ throw new CodeSyncError("code_sync_unsupported", "The requested action belongs to a different code provider.");
21787
+ }
21788
+ if (kind.startsWith("github_"))
21789
+ kind = kind.slice(7);
21790
+ if (busy && ["code_upload", "code_download", "code_recover", "code_auto_upload_enable", "code_update"].includes(kind)) {
21509
21791
  throw new CodeSyncError("code_sync_busy", "An Agent is working in this workspace. Wait until it finishes before syncing code.");
21510
21792
  }
21511
21793
  switch (kind) {
@@ -21521,11 +21803,26 @@ var ProjectCodeSync = class _ProjectCodeSync {
21521
21803
  return this.setAutomaticUpload(true);
21522
21804
  case "code_auto_upload_disable":
21523
21805
  return this.setAutomaticUpload(false);
21806
+ case "code_update": {
21807
+ await this.initialize();
21808
+ if (!this.#options.storage)
21809
+ throw new CodeSyncError("code_sync_unsupported", "Use the cloud review controls to update this branch.");
21810
+ return this.#locked(async () => {
21811
+ const binding = await this.#readBinding();
21812
+ const inventory = await this.#inventory();
21813
+ if (!sameHashes(binding.baseline, inventory.hashes)) {
21814
+ throw new CodeSyncError("code_sync_dirty", "Local source files have unuploaded changes. Upload them or recover the remote version before updating your GitHub branch.");
21815
+ }
21816
+ return this.#status(binding, await this.#options.storage.update(), inventory);
21817
+ });
21818
+ }
21524
21819
  default:
21525
21820
  throw new CodeSyncError("code_sync_unsupported", "Unsupported code sync action.");
21526
21821
  }
21527
21822
  }
21528
21823
  async #cloudStatus() {
21824
+ if (this.#options.storage)
21825
+ return this.#options.storage.status();
21529
21826
  const parsed = CodeStatusSchema.safeParse(await this.#request(""));
21530
21827
  if (!parsed.success)
21531
21828
  throw invalidResponse();
@@ -21534,6 +21831,8 @@ var ProjectCodeSync = class _ProjectCodeSync {
21534
21831
  async #snapshot(cloud) {
21535
21832
  if (!cloud.repository.main_commit && !cloud.own_branch_id)
21536
21833
  throw new CodeSyncError("code_sync_empty", "No code has been uploaded yet.");
21834
+ if (this.#options.storage)
21835
+ return this.#options.storage.snapshot(cloud);
21537
21836
  const parsed = CodeSnapshotResultSchema.safeParse(await this.#request(`/snapshot?branch_id=${encodeURIComponent(cloud.own_branch_id ?? "main")}`));
21538
21837
  if (!parsed.success)
21539
21838
  throw invalidResponse();
@@ -21542,6 +21841,7 @@ var ProjectCodeSync = class _ProjectCodeSync {
21542
21841
  #status(binding, cloud, inventory) {
21543
21842
  const cloudCommit = cloud.branches.find((branch) => branch.id === cloud.own_branch_id)?.head_commit ?? cloud.repository.main_commit;
21544
21843
  return {
21844
+ ...this.#options.storage?.metadata,
21545
21845
  enabled: cloud.repository.enabled,
21546
21846
  automatic_upload: binding.automatic_upload,
21547
21847
  local_changes: changedCount(binding.baseline, inventory.hashes),
@@ -21609,7 +21909,7 @@ var ProjectCodeSync = class _ProjectCodeSync {
21609
21909
  async #readBinding() {
21610
21910
  const file2 = path2.join(this.#stateRoot, "binding.json");
21611
21911
  const info = await lstat2(file2);
21612
- if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > MAX_BINDING_BYTES)
21912
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > MAX_BINDING_BYTES * (this.#options.storage ? 10 : 1))
21613
21913
  throw new CodeSyncError("code_sync_binding", "Private code sync state is not a regular bounded file.");
21614
21914
  let binding;
21615
21915
  try {
@@ -21624,13 +21924,13 @@ var ProjectCodeSync = class _ProjectCodeSync {
21624
21924
  }
21625
21925
  if (binding.interrupted_download !== void 0 && typeof binding.interrupted_download !== "boolean")
21626
21926
  throw new CodeSyncError("code_sync_binding", "Private download recovery state is invalid.");
21627
- if (binding.executable_paths !== void 0 && (!Array.isArray(binding.executable_paths) || binding.executable_paths.length > CODE_SYNC_MAX_FILES || binding.executable_paths.some((name2) => typeof name2 !== "string" || !isCodeSyncPathAllowed(name2))))
21927
+ if (binding.executable_paths !== void 0 && (!Array.isArray(binding.executable_paths) || binding.executable_paths.length > this.#limits.maxFiles || binding.executable_paths.some((name2) => typeof name2 !== "string" || !isCodeSyncPathAllowed(name2))))
21628
21928
  throw new CodeSyncError("code_sync_binding", "Private code file-mode state is invalid.");
21629
21929
  const baseline = Object.entries(binding.baseline);
21630
- if (baseline.length > CODE_SYNC_MAX_FILES)
21930
+ if (baseline.length > this.#limits.maxFiles)
21631
21931
  throw new CodeSyncError("code_sync_binding", "Private code sync baseline exceeds the file limit.");
21632
- for (const [name2, hash2] of baseline) {
21633
- if (!isCodeSyncPathAllowed(name2) || typeof hash2 !== "string" || !/^[a-f0-9]{64}$/u.test(hash2))
21932
+ for (const [name2, hash3] of baseline) {
21933
+ if (!isCodeSyncPathAllowed(name2) || typeof hash3 !== "string" || !/^[a-f0-9]{64}$/u.test(hash3))
21634
21934
  throw new CodeSyncError("code_sync_binding", "Private code sync baseline is invalid.");
21635
21935
  }
21636
21936
  return binding;
@@ -21663,19 +21963,23 @@ var ProjectCodeSync = class _ProjectCodeSync {
21663
21963
  const nativeExecutables = /* @__PURE__ */ new Set();
21664
21964
  let candidates;
21665
21965
  let nativeRoot = "";
21966
+ const inventoryOutputLimit = this.#options.storage ? this.#limits.maxFiles * 2100 : 2 * 1024 * 1024;
21666
21967
  try {
21667
21968
  nativeRoot = (await git(["rev-parse", "--show-toplevel"], this.#root)).trim();
21668
21969
  } catch {
21669
21970
  }
21670
21971
  if (nativeRoot && await realpath(nativeRoot) === this.#root) {
21671
- const trackedEntries = (await git(["ls-files", "--stage", "-z"], this.#root)).split("\0").filter(Boolean);
21972
+ const trackedEntries = (await git(["ls-files", "--stage", "-z"], this.#root, inventoryOutputLimit)).split("\0").filter(Boolean);
21672
21973
  tracked = new Set(trackedEntries.map((entry) => entry.slice(entry.indexOf(" ") + 1)));
21974
+ if (this.#options.storage?.metadata.provider === "github" && trackedEntries.some((entry) => !/^(?:100644|100755) /u.test(entry))) {
21975
+ throw new CodeSyncError("code_sync_unsupported", "Tracked links or submodules cannot be synchronized to GitHub. Remove them from the native Git index or use native Git.");
21976
+ }
21673
21977
  for (const entry of trackedEntries)
21674
21978
  if (entry.startsWith("100755 "))
21675
21979
  nativeExecutables.add(entry.slice(entry.indexOf(" ") + 1));
21676
- candidates = [...tracked, ...(await git(["ls-files", "--others", "--exclude-standard", "-z"], this.#root)).split("\0").filter(Boolean)];
21980
+ candidates = [...tracked, ...(await git(["ls-files", "--others", "--exclude-standard", "-z"], this.#root, inventoryOutputLimit)).split("\0").filter(Boolean)];
21677
21981
  } else {
21678
- candidates = (await git([`--git-dir=${path2.join(this.#stateRoot, "inventory.git")}`, `--work-tree=${this.#root}`, "ls-files", "--others", "--exclude-standard", "-z"], this.#root)).split("\0").filter(Boolean);
21982
+ candidates = (await git([`--git-dir=${path2.join(this.#stateRoot, "inventory.git")}`, `--work-tree=${this.#root}`, "ls-files", "--others", "--exclude-standard", "-z"], this.#root, inventoryOutputLimit)).split("\0").filter(Boolean);
21679
21983
  }
21680
21984
  const binding = await this.#readBinding();
21681
21985
  const executablePaths = executableHints ?? new Set(binding.executable_paths ?? []);
@@ -21688,6 +21992,9 @@ var ProjectCodeSync = class _ProjectCodeSync {
21688
21992
  let totalBytes = 0;
21689
21993
  for (const name2 of [...new Set(candidates)].sort()) {
21690
21994
  if (!isCodeSyncPathAllowed(name2) || BUILD_PATH.test(name2)) {
21995
+ if (this.#options.storage?.metadata.provider === "github" && tracked.has(name2)) {
21996
+ throw new CodeSyncError("code_sync_unsupported", "A tracked private, unsafe or generated path cannot be synchronized to GitHub. Remove it from the native Git index or use native Git.");
21997
+ }
21691
21998
  if (tracked.has(name2) && !BUILD_PATH.test(name2))
21692
21999
  throw new CodeSyncError("code_sync_secret", "A tracked private or unsafe file is excluded from code sync. Remove it from version control before uploading.");
21693
22000
  excluded += 1;
@@ -21695,6 +22002,8 @@ var ProjectCodeSync = class _ProjectCodeSync {
21695
22002
  }
21696
22003
  const target = path2.join(this.#root, ...name2.split("/"));
21697
22004
  if (!await safeAncestors(this.#root, name2)) {
22005
+ if (this.#options.storage?.metadata.provider === "github" && tracked.has(name2))
22006
+ throw new CodeSyncError("code_sync_unsupported", "A tracked path crosses a link or unsafe directory. Use native Git after reviewing it.");
21698
22007
  excluded += 1;
21699
22008
  continue;
21700
22009
  }
@@ -21707,26 +22016,28 @@ var ProjectCodeSync = class _ProjectCodeSync {
21707
22016
  throw error62;
21708
22017
  }
21709
22018
  if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
22019
+ if (this.#options.storage?.metadata.provider === "github" && tracked.has(name2))
22020
+ throw new CodeSyncError("code_sync_unsupported", "A tracked link or non-regular file cannot be synchronized to GitHub. Use native Git after reviewing it.");
21710
22021
  excluded += 1;
21711
22022
  continue;
21712
22023
  }
21713
- if (info.size > CODE_SYNC_MAX_FILE_BYTES)
21714
- throw new CodeSyncError("code_sync_limit", "A source file exceeds the 2 MiB code sync limit.");
21715
- const source = await readBoundedSource(target);
21716
- const bytes = source.bytes;
21717
- if (containsCodeSyncSecret(bytes.toString("utf8")))
22024
+ if (info.size > this.#limits.maxFileBytes)
22025
+ throw new CodeSyncError("code_sync_limit", "A source file exceeds the selected provider's file limit.");
22026
+ const source = await readBoundedSource(target, this.#limits.maxFileBytes);
22027
+ const bytes2 = source.bytes;
22028
+ if (containsCodeSyncSecret(bytes2.toString("utf8")))
21718
22029
  throw new CodeSyncError("code_sync_secret", "A source file contains a recognizable credential or private key. Remove it before uploading.");
21719
- totalBytes += bytes.byteLength;
21720
- if (totalBytes > CODE_SYNC_MAX_SNAPSHOT_BYTES || files.length >= CODE_SYNC_MAX_FILES)
21721
- throw new CodeSyncError("code_sync_limit", "Code sync is limited to 1,000 files and 8 MiB per checkpoint.");
21722
- files.push({ path: name2, content_base64: bytes.toString("base64"), executable: process.platform === "win32" ? executablePaths.has(name2) || nativeExecutables.has(name2) : source.executable });
22030
+ totalBytes += bytes2.byteLength;
22031
+ if (totalBytes > this.#limits.maxSnapshotBytes || files.length >= this.#limits.maxFiles)
22032
+ throw new CodeSyncError("code_sync_limit", "Source files exceed the selected provider's checkpoint limit.");
22033
+ files.push({ path: name2, content_base64: bytes2.toString("base64"), executable: process.platform === "win32" ? executablePaths.has(name2) || nativeExecutables.has(name2) : source.executable });
21723
22034
  }
21724
- if (!CodeFilesSchema.safeParse(files).success)
22035
+ if (!createCodeFilesSchema(this.#limits).safeParse(files).success)
21725
22036
  throw new CodeSyncError("code_sync_unsafe_path", "Source files contain non-portable or conflicting paths.");
21726
22037
  return { files, hashes: fileHashes(files), excluded };
21727
22038
  }
21728
22039
  async #locked(operation) {
21729
- const lockPath = path2.join(this.#stateRoot, "operation.lock");
22040
+ const lockPath = path2.join(this.#lockRoot, "operation.lock");
21730
22041
  let handle;
21731
22042
  try {
21732
22043
  handle = await open3(lockPath, "wx", 384);
@@ -21798,14 +22109,14 @@ function isWithin(root, target) {
21798
22109
  const relative = path2.relative(root, target);
21799
22110
  return !relative || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative);
21800
22111
  }
21801
- async function git(args, cwd) {
22112
+ async function git(args, cwd, maxBuffer = 2 * 1024 * 1024) {
21802
22113
  const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_") && !key.startsWith("GATHERTHREAD_")));
21803
22114
  try {
21804
22115
  const result = await execFileAsync("git", ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args], {
21805
22116
  cwd,
21806
22117
  env: { ...env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" },
21807
22118
  timeout: 15e3,
21808
- maxBuffer: 2 * 1024 * 1024,
22119
+ maxBuffer,
21809
22120
  encoding: "utf8",
21810
22121
  windowsHide: true
21811
22122
  });
@@ -21844,7 +22155,7 @@ async function safeAncestors(root, relative, create = false) {
21844
22155
  }
21845
22156
  return true;
21846
22157
  }
21847
- async function fileHash(root, name2, executableHint) {
22158
+ async function fileHash(root, name2, executableHint, maxBytes = CODE_SYNC_MAX_FILE_BYTES) {
21848
22159
  if (!await safeAncestors(root, name2))
21849
22160
  throw new CodeSyncError("code_sync_unsafe_path", "A destination directory is a symbolic link or not a directory.");
21850
22161
  const target = path2.join(root, ...name2.split("/"));
@@ -21856,17 +22167,17 @@ async function fileHash(root, name2, executableHint) {
21856
22167
  return;
21857
22168
  throw error62;
21858
22169
  }
21859
- if (!info.isFile() || info.isSymbolicLink() || info.size > CODE_SYNC_MAX_FILE_BYTES)
22170
+ if (!info.isFile() || info.isSymbolicLink() || info.size > maxBytes)
21860
22171
  throw new CodeSyncError("code_sync_unsafe_path", "A destination is not a bounded regular source file.");
21861
- const source = await readBoundedSource(target);
22172
+ const source = await readBoundedSource(target, maxBytes);
21862
22173
  const executable = process.platform === "win32" && executableHint !== void 0 ? executableHint : source.executable;
21863
22174
  return digest(`${executable ? "1" : "0"}\0${source.bytes.toString("base64")}`);
21864
22175
  }
21865
- async function readBoundedSource(filename) {
22176
+ async function readBoundedSource(filename, maxBytes = CODE_SYNC_MAX_FILE_BYTES) {
21866
22177
  const handle = await open3(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
21867
22178
  try {
21868
22179
  const before = await handle.stat();
21869
- if (!before.isFile() || before.nlink !== 1 || before.size > CODE_SYNC_MAX_FILE_BYTES)
22180
+ if (!before.isFile() || before.nlink !== 1 || before.size > maxBytes)
21870
22181
  throw new CodeSyncError("code_sync_unsafe_path", "A source path is not a bounded regular file.");
21871
22182
  const buffer = Buffer.alloc(before.size + 1);
21872
22183
  const result = await handle.read(buffer, 0, buffer.length, 0);
@@ -21878,18 +22189,18 @@ async function readBoundedSource(filename) {
21878
22189
  await handle.close();
21879
22190
  }
21880
22191
  }
21881
- async function applySnapshot(root, files, baseline, options) {
21882
- if (!CodeFilesSchema.safeParse(files).success)
22192
+ async function applySnapshot(root, files, baseline, options, limits = CLOUD_CODE_SYNC_LIMITS) {
22193
+ if (!createCodeFilesSchema(limits).safeParse(files).success)
21883
22194
  throw new CodeSyncError("code_sync_unsafe_path", "Cloud code contains unsafe paths.");
21884
22195
  const names = /* @__PURE__ */ new Set([...Object.keys(baseline), ...files.map((file2) => file2.path)]);
21885
22196
  for (const name2 of names) {
21886
- if (await fileHash(root, name2, options?.baselineExecutables?.has(name2)) !== baseline[name2])
22197
+ if (await fileHash(root, name2, options?.baselineExecutables?.has(name2), limits.maxFileBytes) !== baseline[name2])
21887
22198
  throw new CodeSyncError("code_sync_dirty", "Cloud code would overwrite local or ignored files. Recover into a new folder instead.");
21888
22199
  }
21889
22200
  if (options) {
21890
22201
  await mkdir2(options.backupRoot, { recursive: true, mode: 448 });
21891
22202
  for (const name2 of Object.keys(baseline)) {
21892
- const source = await readBoundedSource(path2.join(root, ...name2.split("/")));
22203
+ const source = await readBoundedSource(path2.join(root, ...name2.split("/")), limits.maxFileBytes);
21893
22204
  if (process.platform === "win32" && options.baselineExecutables)
21894
22205
  source.executable = options.baselineExecutables.has(name2);
21895
22206
  if (digest(`${source.executable ? "1" : "0"}\0${source.bytes.toString("base64")}`) !== baseline[name2])
@@ -21908,7 +22219,7 @@ async function applySnapshot(root, files, baseline, options) {
21908
22219
  }
21909
22220
  const targetNames = new Set(files.map((file2) => file2.path));
21910
22221
  for (const file2 of files) {
21911
- if (!await safeAncestors(root, file2.path, true) || await fileHash(root, file2.path, options?.baselineExecutables?.has(file2.path)) !== baseline[file2.path])
22222
+ if (!await safeAncestors(root, file2.path, true) || await fileHash(root, file2.path, options?.baselineExecutables?.has(file2.path), limits.maxFileBytes) !== baseline[file2.path])
21912
22223
  throw new CodeSyncError("code_sync_busy", "Local files changed during download. No further files were applied.");
21913
22224
  const filename = path2.join(root, ...file2.path.split("/"));
21914
22225
  const temporary = path2.join(path2.dirname(filename), `.gatherthread-${randomUUID()}.tmp`);
@@ -21919,7 +22230,7 @@ async function applySnapshot(root, files, baseline, options) {
21919
22230
  } finally {
21920
22231
  await handle.close();
21921
22232
  }
21922
- if (!await safeAncestors(root, file2.path) || await fileHash(root, file2.path, options?.baselineExecutables?.has(file2.path)) !== baseline[file2.path]) {
22233
+ if (!await safeAncestors(root, file2.path) || await fileHash(root, file2.path, options?.baselineExecutables?.has(file2.path), limits.maxFileBytes) !== baseline[file2.path]) {
21923
22234
  await unlink(temporary);
21924
22235
  throw new CodeSyncError("code_sync_busy", "Source files changed while preparing a download. Original files were backed up; no further files were applied.");
21925
22236
  }
@@ -21927,55 +22238,442 @@ async function applySnapshot(root, files, baseline, options) {
21927
22238
  }
21928
22239
  for (const name2 of Object.keys(baseline)) {
21929
22240
  if (!targetNames.has(name2)) {
21930
- if (await fileHash(root, name2, options?.baselineExecutables?.has(name2)) !== baseline[name2])
22241
+ if (await fileHash(root, name2, options?.baselineExecutables?.has(name2), limits.maxFileBytes) !== baseline[name2])
21931
22242
  throw new CodeSyncError("code_sync_busy", "Local files changed during download. No further files were applied.");
21932
22243
  await unlink(path2.join(root, ...name2.split("/")));
21933
22244
  }
21934
22245
  }
21935
22246
  }
21936
22247
 
22248
+ // ../bridge/dist/src/github-code-sync.js
22249
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
22250
+ import { spawn } from "node:child_process";
22251
+ import { constants as constants2 } from "node:fs";
22252
+ import { lstat as lstat3, mkdir as mkdir3, open as open4, unlink as unlink2 } from "node:fs/promises";
22253
+ import path3 from "node:path";
22254
+ var hash2 = (text) => createHash3("sha256").update(text).digest("hex");
22255
+ var Commit2 = /^[a-f0-9]{40}$/u;
22256
+ var filesSchema = createCodeFilesSchema(GITHUB_CODE_SYNC_LIMITS);
22257
+ var generatedPath = /(?:^|\/)(?:node_modules|dist|build|coverage|\.next|\.cache|\.turbo|\.venv|venv|__pycache__)(?:\/|$)/u;
22258
+ function createGitHubCodeSync(options, runner = runGitHubGit) {
22259
+ return new ProjectCodeSync({ ...options, storage: new GitHubCodeStorage(options, runner) });
22260
+ }
22261
+ var GitHubCodeStorage = class {
22262
+ limits = GITHUB_CODE_SYNC_LIMITS;
22263
+ key;
22264
+ metadata;
22265
+ #options;
22266
+ #run;
22267
+ #remote;
22268
+ #branchId;
22269
+ #directory = "";
22270
+ #revision;
22271
+ constructor(options, runner) {
22272
+ if (!GitHubConnectionSchema.safeParse({ repository: options.repository, base_branch: options.baseBranch, revision: options.revision, enabled: true }).success) {
22273
+ throw new CodeSyncError("code_sync_binding", "GitHub requires an owner/repository and a valid base branch.");
22274
+ }
22275
+ const branch = `gatherthread/${hash2(options.projectId).slice(0, 24)}/${hash2(options.actorId).slice(0, 24)}`;
22276
+ if (options.baseBranch.startsWith("gatherthread/"))
22277
+ throw new CodeSyncError("code_sync_binding", "Choose a base branch outside GatherThread personal branches.");
22278
+ this.#options = options;
22279
+ this.#run = runner;
22280
+ this.#revision = options.revision;
22281
+ this.#remote = `https://github.com/${options.repository}.git`;
22282
+ this.#branchId = `branch-${hash2(branch).slice(0, 24)}`;
22283
+ this.key = JSON.stringify(["github", options.apiUrl.replace(/\/$/u, "").replace(/\/v1$/u, ""), options.projectId, options.actorId, options.repository.toLowerCase(), options.baseBranch, options.revision]);
22284
+ this.metadata = { provider: "github", repository: options.repository, branch, base_branch: options.baseBranch };
22285
+ }
22286
+ async initialize(privateDirectory) {
22287
+ this.#directory = path3.join(privateDirectory, "github.git");
22288
+ try {
22289
+ if ((await lstat3(this.#directory)).isSymbolicLink())
22290
+ throw new CodeSyncError("code_sync_binding", "Private GitHub state cannot be a symbolic link.");
22291
+ } catch (error62) {
22292
+ if (error62.code !== "ENOENT")
22293
+ throw error62;
22294
+ }
22295
+ await mkdir3(this.#directory, { recursive: true, mode: 448 });
22296
+ await this.#git(["init", "--bare", "--quiet", "."]);
22297
+ const info = path3.join(this.#directory, "info");
22298
+ await mkdir3(info, { recursive: true, mode: 448 });
22299
+ if ((await lstat3(info)).isSymbolicLink())
22300
+ throw new CodeSyncError("code_sync_binding", "Private GitHub attributes directory is unsafe.");
22301
+ const attributes = await open4(path3.join(info, "attributes"), constants2.O_WRONLY | constants2.O_CREAT | (constants2.O_NOFOLLOW ?? 0), 384);
22302
+ try {
22303
+ if (!(await attributes.stat()).isFile() || (await attributes.stat()).nlink !== 1)
22304
+ throw new CodeSyncError("code_sync_binding", "Private GitHub attributes must be a regular file.");
22305
+ await attributes.truncate(0);
22306
+ await attributes.writeFile("* conflict-marker-size=7\n");
22307
+ } finally {
22308
+ await attributes.close();
22309
+ }
22310
+ }
22311
+ async #authorize() {
22312
+ const url2 = new URL(this.#options.apiUrl);
22313
+ url2.pathname = url2.pathname.replace(/\/$/u, "").replace(/\/v1$/u, "") + `/v1/projects/${encodeURIComponent(this.#options.projectId)}/github`;
22314
+ let response;
22315
+ try {
22316
+ response = await (this.#options.fetch ?? fetch)(url2.toString(), {
22317
+ headers: { authorization: `Bearer ${this.#options.token}` },
22318
+ redirect: "error",
22319
+ signal: AbortSignal.timeout(15e3)
22320
+ });
22321
+ } catch {
22322
+ throw new CodeSyncError("code_sync_unavailable", "GatherThread could not verify current GitHub project access.");
22323
+ }
22324
+ if (!response.ok) {
22325
+ await response.body?.cancel();
22326
+ throw new CodeSyncError("code_sync_forbidden", "Current GatherThread project access is required for GitHub synchronization.");
22327
+ }
22328
+ if (!response.body)
22329
+ throw new CodeSyncError("code_sync_unavailable", "Invalid GitHub configuration response.");
22330
+ const reader = response.body.getReader();
22331
+ const chunks = [];
22332
+ let size = 0;
22333
+ for (; ; ) {
22334
+ const part = await reader.read();
22335
+ if (part.done)
22336
+ break;
22337
+ size += part.value.length;
22338
+ if (size > 16384) {
22339
+ await reader.cancel();
22340
+ throw new CodeSyncError("code_sync_limit", "GitHub configuration response is too large.");
22341
+ }
22342
+ chunks.push(part.value);
22343
+ }
22344
+ let data;
22345
+ try {
22346
+ data = GitHubProjectStatusSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")).data);
22347
+ } catch {
22348
+ throw new CodeSyncError("code_sync_unavailable", "Invalid GitHub configuration response.");
22349
+ }
22350
+ const connection = data.connection;
22351
+ if (!data.can_write || data.branch !== this.metadata.branch || !connection || connection.repository.toLowerCase() !== this.#options.repository.toLowerCase() || connection.base_branch !== this.#options.baseBranch) {
22352
+ throw new CodeSyncError("code_sync_binding", "GitHub target or project permissions changed. Review the connection and authorize it locally again.");
22353
+ }
22354
+ if (!connection.enabled)
22355
+ return false;
22356
+ if (this.#revision !== connection.revision)
22357
+ throw new CodeSyncError("code_sync_binding", "GitHub configuration changed. Reconnect and authorize the current target locally again.");
22358
+ return true;
22359
+ }
22360
+ async #enabled() {
22361
+ if (!await this.#authorize())
22362
+ throw new CodeSyncError("code_sync_disabled", "GitHub synchronization is paused for this project.");
22363
+ }
22364
+ async status() {
22365
+ if (!await this.#authorize())
22366
+ return { repository: { enabled: false, main_commit: null }, branches: [], own_branch_id: null };
22367
+ const refs = (await this.#git(["ls-remote", "--refs", this.#remote, `refs/heads/${this.#options.baseBranch}`, `refs/heads/${this.metadata.branch}`])).toString("utf8");
22368
+ let main = null;
22369
+ let own2 = null;
22370
+ for (const line of refs.trim().split("\n")) {
22371
+ const [oid, ref] = line.split(" ");
22372
+ if (!oid || !Commit2.test(oid))
22373
+ continue;
22374
+ if (ref === `refs/heads/${this.#options.baseBranch}`)
22375
+ main = oid;
22376
+ if (ref === `refs/heads/${this.metadata.branch}`)
22377
+ own2 = oid;
22378
+ }
22379
+ return {
22380
+ repository: { enabled: true, main_commit: main },
22381
+ own_branch_id: own2 ? this.#branchId : null,
22382
+ branches: own2 ? [{ id: this.#branchId, name: "My GitHub branch", user_id: this.#options.actorId, head_commit: own2, review_status: "draft" }] : []
22383
+ };
22384
+ }
22385
+ #head(status) {
22386
+ return status.branches[0]?.head_commit ?? status.repository.main_commit;
22387
+ }
22388
+ async #fetch(status) {
22389
+ await this.#enabled();
22390
+ const refs = [];
22391
+ if (status.repository.main_commit)
22392
+ refs.push(`+refs/heads/${this.#options.baseBranch}:refs/gt/base`);
22393
+ if (status.own_branch_id)
22394
+ refs.push(`+refs/heads/${this.metadata.branch}:refs/gt/own`);
22395
+ if (refs.length)
22396
+ await this.#git(["fetch", "--quiet", "--no-tags", "--no-recurse-submodules", this.#remote, ...refs]);
22397
+ for (const [ref, expected] of [["refs/gt/base", status.repository.main_commit], ["refs/gt/own", status.branches[0]?.head_commit]]) {
22398
+ if (expected && (await this.#git(["rev-parse", "--verify", ref])).toString().trim() !== expected)
22399
+ throw conflict();
22400
+ }
22401
+ }
22402
+ async snapshot(status) {
22403
+ const commit = this.#head(status);
22404
+ if (!commit)
22405
+ throw new CodeSyncError("code_sync_empty", "No GitHub files have been uploaded yet.");
22406
+ await this.#fetch(status);
22407
+ return { commit, files: await this.#files(commit) };
22408
+ }
22409
+ async #files(commit) {
22410
+ if (!Commit2.test(commit))
22411
+ throw conflict();
22412
+ const listing = await this.#git(["ls-tree", "-r", "-z", "-l", commit]);
22413
+ if (!Buffer.from(listing.toString("utf8"), "utf8").equals(listing))
22414
+ throw new CodeSyncError("code_sync_unsafe_path", "The GitHub tree contains non-UTF-8 filenames. Use native Git for this repository.");
22415
+ const entries = listing.toString("utf8").split("\0").filter(Boolean).map((entry) => {
22416
+ const match = /^(100644|100755) blob ([a-f0-9]{40})\s+(\d+)\t(.+)$/u.exec(entry);
22417
+ if (!match)
22418
+ throw new CodeSyncError("code_sync_unsupported", "This GitHub tree contains unsupported links, submodules or paths. Use native Git for this repository.");
22419
+ return { mode: match[1], oid: match[2], size: Number(match[3]), path: match[4] };
22420
+ });
22421
+ if (entries.length > this.limits.maxFiles || entries.some((entry) => entry.size > this.limits.maxFileBytes) || entries.reduce((sum, entry) => sum + entry.size, 0) > this.limits.maxSnapshotBytes)
22422
+ throw new CodeSyncError("code_sync_limit", "GitHub source exceeds the supported checkpoint limit. Use native Git for larger trees.");
22423
+ const output2 = entries.length ? await this.#git(["cat-file", "--batch"], Buffer.from(entries.map((entry) => entry.oid).join("\n") + "\n")) : Buffer.alloc(0);
22424
+ let offset = 0;
22425
+ const files = [];
22426
+ for (const entry of entries) {
22427
+ const end = output2.indexOf(10, offset);
22428
+ if (end < 0 || output2.subarray(offset, end).toString() !== `${entry.oid} blob ${entry.size}`)
22429
+ throw conflict();
22430
+ const content = output2.subarray(end + 1, end + 1 + entry.size);
22431
+ offset = end + 2 + entry.size;
22432
+ if (content.length !== entry.size || output2[offset - 1] !== 10)
22433
+ throw conflict();
22434
+ if (generatedPath.test(entry.path) || containsCodeSyncSecret(content.toString("utf8")) || content.subarray(0, 80).toString().startsWith("version https://git-lfs.github.com/spec/v1")) {
22435
+ throw new CodeSyncError("code_sync_unsupported", "This GitHub tree contains excluded generated files, credentials or Git LFS pointers. Nothing was applied; use native Git or a source-only repository.");
22436
+ }
22437
+ files.push({ path: entry.path, content_base64: content.toString("base64"), executable: entry.mode === "100755" });
22438
+ }
22439
+ if (!filesSchema.safeParse(files).success)
22440
+ throw new CodeSyncError("code_sync_unsafe_path", "The GitHub tree contains non-portable or private paths. Nothing was applied.");
22441
+ return files;
22442
+ }
22443
+ async #tree(files) {
22444
+ if (!filesSchema.safeParse(files).success)
22445
+ throw new CodeSyncError("code_sync_limit", "Source tree exceeds GitHub checkpoint limits.");
22446
+ const index = path3.join(this.#directory, `index-${randomUUID2()}`);
22447
+ try {
22448
+ await this.#git(["read-tree", "--empty"], void 0, index);
22449
+ const records = [];
22450
+ for (const file2 of files) {
22451
+ const bytes2 = Buffer.from(file2.content_base64, "base64");
22452
+ if (containsCodeSyncSecret(bytes2.toString("utf8")))
22453
+ throw new CodeSyncError("code_sync_secret", "A recognizable credential was detected in source.");
22454
+ if (generatedPath.test(file2.path) || bytes2.subarray(0, 80).toString().startsWith("version https://git-lfs.github.com/spec/v1"))
22455
+ throw new CodeSyncError("code_sync_unsupported", "Use native Git for Git LFS or generated-file repositories.");
22456
+ const oid = (await this.#git(["hash-object", "-w", "--stdin", "--no-filters"], bytes2)).toString().trim();
22457
+ if (!Commit2.test(oid))
22458
+ throw conflict();
22459
+ records.push(`${file2.executable ? "100755" : "100644"} ${oid} ${file2.path}\0`);
22460
+ }
22461
+ await this.#git(["update-index", "-z", "--index-info"], Buffer.from(records.join("")), index);
22462
+ return (await this.#git(["write-tree"], void 0, index)).toString().trim();
22463
+ } finally {
22464
+ await unlink2(index).catch(() => {
22465
+ });
22466
+ }
22467
+ }
22468
+ async checkpoint(input2) {
22469
+ const status = await this.status();
22470
+ await this.#enabled();
22471
+ const current = this.#head(status);
22472
+ await this.#fetch(status);
22473
+ if (current)
22474
+ await this.#files(current);
22475
+ const tree = await this.#tree(input2.files);
22476
+ if (current && (await this.#git(["rev-parse", `${current}^{tree}`])).toString().trim() === tree)
22477
+ return { status, commit: current };
22478
+ if (input2.base_commit !== current)
22479
+ throw conflict();
22480
+ const commit = (await this.#git(["commit-tree", tree, ...current ? ["-p", current] : [], "-m", "GatherThread workspace checkpoint"])).toString().trim();
22481
+ await this.#push(commit, status);
22482
+ const next = await this.status();
22483
+ if (this.#head(next) !== commit)
22484
+ throw conflict();
22485
+ return { status: next, commit };
22486
+ }
22487
+ async #push(commit, expected) {
22488
+ await this.#enabled();
22489
+ const latest = await this.status();
22490
+ if (this.#head(latest) !== this.#head(expected) || latest.own_branch_id !== expected.own_branch_id)
22491
+ throw conflict();
22492
+ await this.#git(["push", "--porcelain", "--no-verify", this.#remote, `${commit}:refs/heads/${this.metadata.branch}`]);
22493
+ }
22494
+ async update() {
22495
+ const status = await this.status();
22496
+ await this.#enabled();
22497
+ if (!status.own_branch_id || !status.repository.main_commit)
22498
+ return status;
22499
+ await this.#fetch(status);
22500
+ const own2 = this.#head(status);
22501
+ const base = status.repository.main_commit;
22502
+ await this.#files(own2);
22503
+ await this.#files(base);
22504
+ if (own2 === base)
22505
+ return status;
22506
+ try {
22507
+ await this.#git(["merge-base", "--is-ancestor", base, own2]);
22508
+ return status;
22509
+ } catch {
22510
+ }
22511
+ let tree;
22512
+ try {
22513
+ tree = (await this.#git(["merge-tree", "--write-tree", own2, base])).toString().split("\n")[0].trim();
22514
+ } catch {
22515
+ throw new CodeSyncError("code_sync_conflict", "The base branch could not be merged automatically. Resolve conflicts using native Git or GitHub, then download again. Git 2.38 or newer is required.");
22516
+ }
22517
+ if (!Commit2.test(tree))
22518
+ throw conflict();
22519
+ await this.#files(tree);
22520
+ const commit = (await this.#git(["commit-tree", tree, "-p", own2, "-p", base, "-m", "Merge shared GitHub base into my GatherThread branch"])).toString().trim();
22521
+ await this.#push(commit, status);
22522
+ return this.status();
22523
+ }
22524
+ #git(args, input2, index) {
22525
+ return this.#run(args, { cwd: this.#directory, ...input2 ? { input: input2 } : {}, ...index ? { index } : {} });
22526
+ }
22527
+ };
22528
+ function conflict() {
22529
+ return new CodeSyncError("code_sync_conflict", "GitHub progress changed. Refresh, then download or recover before uploading again.");
22530
+ }
22531
+ async function runGitHubGit(args, options) {
22532
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_") && !key.startsWith("GATHERTHREAD_") && !["GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GH_HOST", "GH_PROMPT_DISABLED"].includes(key)));
22533
+ return new Promise((resolve, reject) => {
22534
+ const child = spawn("git", [
22535
+ "-c",
22536
+ "core.hooksPath=/dev/null",
22537
+ "-c",
22538
+ "core.fsmonitor=false",
22539
+ "-c",
22540
+ "commit.gpgSign=false",
22541
+ "-c",
22542
+ "protocol.allow=never",
22543
+ "-c",
22544
+ "protocol.https.allow=always",
22545
+ "-c",
22546
+ "http.followRedirects=false",
22547
+ "-c",
22548
+ "credential.helper=",
22549
+ "-c",
22550
+ "credential.helper=!gh auth git-credential",
22551
+ "-c",
22552
+ "credential.interactive=false",
22553
+ "-c",
22554
+ "user.name=GatherThread",
22555
+ "-c",
22556
+ "user.email=checkpoint@users.noreply.github.com",
22557
+ ...args
22558
+ ], {
22559
+ cwd: options.cwd,
22560
+ windowsHide: true,
22561
+ stdio: ["pipe", "pipe", "pipe"],
22562
+ env: { ...env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null", GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", ...options.index ? { GIT_INDEX_FILE: options.index } : {} }
22563
+ });
22564
+ const chunks = [];
22565
+ let size = 0;
22566
+ let failed = false;
22567
+ const fail = () => {
22568
+ if (failed)
22569
+ return;
22570
+ failed = true;
22571
+ child.kill();
22572
+ reject(new CodeSyncError("code_sync_unavailable", "GitHub Git operation failed. Check Git 2.38+, GitHub CLI login, repository access and network. No credentials are sent through GatherThread."));
22573
+ };
22574
+ const timer = setTimeout(fail, 12e4);
22575
+ timer.unref();
22576
+ child.stdout.on("data", (chunk) => {
22577
+ size += chunk.length;
22578
+ if (size > GITHUB_CODE_SYNC_LIMITS.maxSnapshotBytes + 8 * 1024 * 1024)
22579
+ fail();
22580
+ else
22581
+ chunks.push(chunk);
22582
+ });
22583
+ child.stderr.resume();
22584
+ child.stdin.on("error", () => {
22585
+ });
22586
+ child.on("error", fail);
22587
+ child.on("close", (code) => {
22588
+ clearTimeout(timer);
22589
+ if (code !== 0)
22590
+ fail();
22591
+ else if (!failed)
22592
+ resolve(Buffer.concat(chunks));
22593
+ });
22594
+ child.stdin.end(options.input);
22595
+ });
22596
+ }
22597
+
22598
+ // ../bridge/dist/src/github-auth.js
22599
+ import { spawn as spawn2 } from "node:child_process";
22600
+ async function connectGitHubAccount(command = runGitHubAuthCommand) {
22601
+ if (await command(["auth", "status", "--hostname", "github.com"], 1e4))
22602
+ return;
22603
+ const signedIn = await command(["auth", "login", "--hostname", "github.com", "--git-protocol", "https", "--web", "--clipboard"], 24e4);
22604
+ if (!signedIn || !await command(["auth", "status", "--hostname", "github.com"], 1e4)) {
22605
+ throw new CodeSyncError("github_auth_failed", "GitHub sign-in was not completed on this device. Reopen the connection and finish in your browser.");
22606
+ }
22607
+ }
22608
+ function runGitHubAuthCommand(args, timeoutMs) {
22609
+ return new Promise((resolve, reject) => {
22610
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GH_HOST", "GH_PROMPT_DISABLED"].includes(key)));
22611
+ const child = spawn2("gh", [...args], { env, windowsHide: true, stdio: ["ignore", "ignore", "ignore"] });
22612
+ let settled = false;
22613
+ const finish = (value) => {
22614
+ if (settled)
22615
+ return;
22616
+ settled = true;
22617
+ clearTimeout(timer);
22618
+ resolve(value);
22619
+ };
22620
+ const timer = setTimeout(() => {
22621
+ child.kill();
22622
+ finish(false);
22623
+ }, timeoutMs);
22624
+ child.on("error", (error62) => {
22625
+ if (settled)
22626
+ return;
22627
+ settled = true;
22628
+ clearTimeout(timer);
22629
+ reject(new CodeSyncError(error62.code === "ENOENT" ? "github_cli_unavailable" : "github_auth_failed", error62.code === "ENOENT" ? "Install GitHub CLI on this device before connecting." : "GitHub CLI could not start on this device."));
22630
+ });
22631
+ child.on("close", (code) => finish(code === 0));
22632
+ });
22633
+ }
22634
+
21937
22635
  // dist/src/native-plugin.js
21938
- import { createHash as createHash8 } from "node:crypto";
21939
- import path9 from "node:path";
22636
+ import { createHash as createHash10 } from "node:crypto";
22637
+ import path10 from "node:path";
21940
22638
 
21941
22639
  // dist/src/code-sync-controller.js
21942
- import { constants as constants2 } from "node:fs";
21943
- import { lstat as lstat4, mkdir as mkdir3, open as open4, rename as rename2, unlink as unlink2 } from "node:fs/promises";
21944
- import { randomUUID as randomUUID2 } from "node:crypto";
21945
- import path4 from "node:path";
22640
+ import { constants as constants3 } from "node:fs";
22641
+ import { lstat as lstat5, mkdir as mkdir4, open as open5, rename as rename2, unlink as unlink3 } from "node:fs/promises";
22642
+ import { randomUUID as randomUUID3 } from "node:crypto";
22643
+ import path5 from "node:path";
21946
22644
 
21947
22645
  // dist/src/path-security.js
21948
- import { lstat as lstat3, realpath as realpath2 } from "node:fs/promises";
21949
- import path3 from "node:path";
22646
+ import { lstat as lstat4, realpath as realpath2 } from "node:fs/promises";
22647
+ import path4 from "node:path";
21950
22648
  async function prospectiveRealPath(candidate) {
21951
- let current = path3.resolve(candidate);
22649
+ let current = path4.resolve(candidate);
21952
22650
  const missing = [];
21953
22651
  while (true) {
21954
22652
  try {
21955
- return path3.join(await realpath2(current), ...missing);
22653
+ return path4.join(await realpath2(current), ...missing);
21956
22654
  } catch (error62) {
21957
22655
  if (!isNodeError2(error62) || error62.code !== "ENOENT")
21958
22656
  throw error62;
21959
- const parent = path3.dirname(current);
22657
+ const parent = path4.dirname(current);
21960
22658
  if (parent === current)
21961
22659
  throw new Error(`No existing ancestor for path: ${candidate}`);
21962
- missing.unshift(path3.basename(current));
22660
+ missing.unshift(path4.basename(current));
21963
22661
  current = parent;
21964
22662
  }
21965
22663
  }
21966
22664
  }
21967
22665
  async function secureDirectoryChain(directory) {
21968
- const absolute = path3.resolve(directory);
21969
- const root = path3.parse(absolute).root;
21970
- const relative = path3.relative(root, absolute);
21971
- const components = relative ? relative.split(path3.sep) : [];
22666
+ const absolute = path4.resolve(directory);
22667
+ const root = path4.parse(absolute).root;
22668
+ const relative = path4.relative(root, absolute);
22669
+ const components = relative ? relative.split(path4.sep) : [];
21972
22670
  const identities = [];
21973
22671
  let current = root;
21974
22672
  for (const component of components) {
21975
- current = path3.join(current, component);
22673
+ current = path4.join(current, component);
21976
22674
  let metadata;
21977
22675
  try {
21978
- metadata = await lstat3(current);
22676
+ metadata = await lstat4(current);
21979
22677
  } catch (error62) {
21980
22678
  if (isNodeError2(error62) && error62.code === "ENOENT")
21981
22679
  break;
@@ -22012,6 +22710,7 @@ var ACTIONS = /* @__PURE__ */ new Set([
22012
22710
  "code_auto_upload_enable",
22013
22711
  "code_auto_upload_disable"
22014
22712
  ]);
22713
+ var GITHUB_CODE_ACTIONS = githubCodeSyncRequestKinds.filter((kind) => kind !== "github_auth_connect");
22015
22714
  var DshCodeSyncController = class {
22016
22715
  #authorized = false;
22017
22716
  #status;
@@ -22021,8 +22720,10 @@ var DshCodeSyncController = class {
22021
22720
  #poll;
22022
22721
  #stopped = false;
22023
22722
  #options;
22723
+ #actions;
22024
22724
  constructor(options) {
22025
22725
  this.#options = options;
22726
+ this.#actions = options.actionKinds ? new Set(options.actionKinds) : ACTIONS;
22026
22727
  }
22027
22728
  view() {
22028
22729
  return {
@@ -22091,7 +22792,7 @@ var DshCodeSyncController = class {
22091
22792
  if (this.#stopped)
22092
22793
  return;
22093
22794
  const runtime = runtimes.get(job.sessionId);
22094
- if (!runtime || job.targetRuntimeId !== runtime || !ACTIONS.has(job.kind))
22795
+ if (!runtime || job.targetRuntimeId !== runtime || !this.#actions.has(job.kind))
22095
22796
  continue;
22096
22797
  let claimed;
22097
22798
  try {
@@ -22134,11 +22835,11 @@ var DshCodeSyncController = class {
22134
22835
  await this.#tail;
22135
22836
  }
22136
22837
  async #execute(kind, operationId) {
22137
- if (!ACTIONS.has(kind))
22838
+ if (!this.#actions.has(kind))
22138
22839
  throw new CodeSyncLocalError("code_action_invalid");
22139
22840
  if (!this.#authorized)
22140
22841
  throw new CodeSyncLocalError("code_sync_disabled");
22141
- if (this.#options.isBusy() && kind !== "code_sync_status" && kind !== "code_auto_upload_disable") {
22842
+ if (this.#options.isBusy() && !["code_sync_status", "code_auto_upload_disable", "github_code_sync_status", "github_code_auto_upload_disable"].includes(kind)) {
22142
22843
  throw new CodeSyncLocalError("code_sync_busy");
22143
22844
  }
22144
22845
  return this.#requireEngine().execute(kind, {
@@ -22169,13 +22870,13 @@ var CodeSyncLocalError = class extends Error {
22169
22870
  };
22170
22871
  function codeSyncErrorCode(error62) {
22171
22872
  const code = typeof error62 === "object" && error62 !== null && "code" in error62 ? error62.code : void 0;
22172
- return typeof code === "string" && /^code_[a-z_]{1,60}$/u.test(code) ? code : "code_sync_failed";
22873
+ return typeof code === "string" && /^(?:github_)?code_[a-z_]{1,60}$/u.test(code) ? code : "code_sync_failed";
22173
22874
  }
22174
22875
  async function loadConsent(file2, binding) {
22175
- const ancestors = await secureDirectoryChain(path4.dirname(file2));
22876
+ const ancestors = await secureDirectoryChain(path5.dirname(file2));
22176
22877
  let before;
22177
22878
  try {
22178
- before = await lstat4(file2);
22879
+ before = await lstat5(file2);
22179
22880
  } catch (error62) {
22180
22881
  if (error62.code === "ENOENT")
22181
22882
  return false;
@@ -22184,25 +22885,25 @@ async function loadConsent(file2, binding) {
22184
22885
  if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > 4096 || process.platform !== "win32" && (before.mode & 63) !== 0) {
22185
22886
  throw new CodeSyncLocalError("code_consent_invalid");
22186
22887
  }
22187
- const handle = await open4(file2, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
22888
+ const handle = await open5(file2, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
22188
22889
  try {
22189
22890
  const opened = await handle.stat();
22190
22891
  if (before.dev !== opened.dev || before.ino !== opened.ino)
22191
22892
  throw new CodeSyncLocalError("code_consent_invalid");
22192
22893
  const value = JSON.parse(await handle.readFile("utf8"));
22193
- assertSameDirectoryChain(ancestors, await secureDirectoryChain(path4.dirname(file2)));
22894
+ assertSameDirectoryChain(ancestors, await secureDirectoryChain(path5.dirname(file2)));
22194
22895
  return value.version === 1 && value.binding === binding && value.enabled === true;
22195
22896
  } finally {
22196
22897
  await handle.close();
22197
22898
  }
22198
22899
  }
22199
22900
  async function saveConsent(file2, binding, enabled) {
22200
- await secureDirectoryChain(path4.dirname(file2));
22201
- await mkdir3(path4.dirname(file2), { recursive: true, mode: 448 });
22202
- const ancestors = await secureDirectoryChain(path4.dirname(file2));
22901
+ await secureDirectoryChain(path5.dirname(file2));
22902
+ await mkdir4(path5.dirname(file2), { recursive: true, mode: 448 });
22903
+ const ancestors = await secureDirectoryChain(path5.dirname(file2));
22203
22904
  await loadConsent(file2, binding);
22204
- const temporary = `${file2}.${randomUUID2()}.tmp`;
22205
- const handle = await open4(temporary, "wx", 384);
22905
+ const temporary = `${file2}.${randomUUID3()}.tmp`;
22906
+ const handle = await open5(temporary, "wx", 384);
22206
22907
  try {
22207
22908
  await handle.writeFile(JSON.stringify({ version: 1, binding, enabled }));
22208
22909
  await handle.sync();
@@ -22210,14 +22911,136 @@ async function saveConsent(file2, binding, enabled) {
22210
22911
  await handle.close();
22211
22912
  }
22212
22913
  try {
22213
- assertSameDirectoryChain(ancestors, await secureDirectoryChain(path4.dirname(file2)));
22914
+ assertSameDirectoryChain(ancestors, await secureDirectoryChain(path5.dirname(file2)));
22214
22915
  await rename2(temporary, file2);
22215
22916
  } catch (error62) {
22216
- await unlink2(temporary).catch(() => void 0);
22917
+ await unlink3(temporary).catch(() => void 0);
22217
22918
  throw error62;
22218
22919
  }
22219
22920
  }
22220
22921
 
22922
+ // dist/src/github-code-sync-controller.js
22923
+ import { createHash as createHash4 } from "node:crypto";
22924
+ var DshGitHubCodeSyncController = class {
22925
+ options;
22926
+ #controller;
22927
+ #metadata;
22928
+ #key;
22929
+ #error;
22930
+ #tail = Promise.resolve();
22931
+ #stopped = false;
22932
+ #poll;
22933
+ constructor(options) {
22934
+ this.options = options;
22935
+ }
22936
+ view() {
22937
+ return {
22938
+ ...this.#controller?.view() ?? { authorized: false },
22939
+ connection: this.#metadata?.connection ? { ...this.#metadata.connection } : null,
22940
+ branch: this.#metadata?.branch ?? null,
22941
+ ...this.#error ? { error: this.#error } : {}
22942
+ };
22943
+ }
22944
+ async start() {
22945
+ await this.#serialize(async () => {
22946
+ await this.#refresh();
22947
+ });
22948
+ }
22949
+ authorize(enabled, expected) {
22950
+ return this.#serialize(async () => {
22951
+ if (!enabled) {
22952
+ await this.#controller?.authorize(false);
22953
+ return this.view();
22954
+ }
22955
+ await this.#refresh();
22956
+ const target = this.#metadata?.connection;
22957
+ if (!target || !expected || targetKey(target) !== targetKey(expected))
22958
+ throw coded("code_github_binding_changed");
22959
+ if (!this.#metadata?.can_write || !target.enabled)
22960
+ throw coded("code_sync_forbidden");
22961
+ await this.#controller.authorize(true);
22962
+ return this.view();
22963
+ });
22964
+ }
22965
+ execute(kind) {
22966
+ return this.#serialize(async () => {
22967
+ if (kind === "github_code_auto_upload_disable" && this.#controller) {
22968
+ await this.#controller.execute(kind);
22969
+ return this.view();
22970
+ }
22971
+ await this.#refresh();
22972
+ if (kind === "github_code_sync_status" && !this.#controller?.view().authorized)
22973
+ return this.view();
22974
+ if (!this.#controller)
22975
+ throw coded("code_github_not_configured");
22976
+ if (!this.#metadata?.connection?.enabled && kind !== "github_code_sync_status")
22977
+ throw coded("code_not_enabled");
22978
+ await this.#controller.execute(kind);
22979
+ return this.view();
22980
+ });
22981
+ }
22982
+ poll() {
22983
+ if (this.#stopped)
22984
+ return Promise.resolve();
22985
+ this.#poll ??= this.#serialize(async () => {
22986
+ await this.#refresh();
22987
+ await this.#controller?.poll();
22988
+ }).finally(() => {
22989
+ this.#poll = void 0;
22990
+ });
22991
+ return this.#poll;
22992
+ }
22993
+ async stop() {
22994
+ this.#stopped = true;
22995
+ await this.#tail;
22996
+ await this.#controller?.stop();
22997
+ }
22998
+ async #refresh() {
22999
+ const metadata = await this.options.readMetadata();
23000
+ const target = metadata.connection;
23001
+ const key = target && metadata.can_write ? targetKey(target) : void 0;
23002
+ if (key !== this.#key) {
23003
+ await this.#controller?.authorize(false);
23004
+ await this.#controller?.stop();
23005
+ this.#controller = void 0;
23006
+ this.#key = void 0;
23007
+ if (target && key) {
23008
+ const capturedTarget = { ...target };
23009
+ this.#controller = new DshCodeSyncController({
23010
+ ...this.options,
23011
+ binding: createHash4("sha256").update(JSON.stringify([this.options.binding, key])).digest("hex"),
23012
+ actionKinds: GITHUB_CODE_ACTIONS,
23013
+ createEngine: () => this.options.createEngine(capturedTarget)
23014
+ });
23015
+ await this.#controller.start();
23016
+ this.#key = key;
23017
+ }
23018
+ }
23019
+ this.#metadata = metadata;
23020
+ }
23021
+ #serialize(operation) {
23022
+ const pending = this.#tail.then(async () => {
23023
+ if (this.#stopped)
23024
+ throw coded("code_sync_stopped");
23025
+ this.#error = void 0;
23026
+ try {
23027
+ return await operation();
23028
+ } catch (error62) {
23029
+ this.#error = codeSyncErrorCode(error62);
23030
+ throw coded(this.#error);
23031
+ }
23032
+ });
23033
+ this.#tail = pending.catch(() => void 0);
23034
+ return pending;
23035
+ }
23036
+ };
23037
+ function targetKey(target) {
23038
+ return JSON.stringify([target.repository, target.base_branch, target.revision]);
23039
+ }
23040
+ function coded(code) {
23041
+ return Object.assign(new Error(code), { code });
23042
+ }
23043
+
22221
23044
  // dist/src/collaboration-api.js
22222
23045
  function createHttpDshCollaborationApi(options) {
22223
23046
  const client = new HttpCollaborationClient({
@@ -22293,9 +23116,9 @@ function clipToUtf8Bytes(value, maxBytes) {
22293
23116
  }
22294
23117
 
22295
23118
  // dist/src/config.js
22296
- import { createHash as createHash3 } from "node:crypto";
22297
- import { lstat as lstat5 } from "node:fs/promises";
22298
- import path5 from "node:path";
23119
+ import { createHash as createHash5 } from "node:crypto";
23120
+ import { lstat as lstat6 } from "node:fs/promises";
23121
+ import path6 from "node:path";
22299
23122
  var CONFIG_KEYS = /* @__PURE__ */ new Set([
22300
23123
  "enabled",
22301
23124
  "bindingMode",
@@ -22408,7 +23231,7 @@ async function assertSafeDshStateRoot(config2) {
22408
23231
  await assertRealStateSeparation(config2.workspacePath, config2.stateRoot, "config.stateRoot");
22409
23232
  await secureDirectoryChain(config2.stateRoot);
22410
23233
  try {
22411
- const target = await lstat5(config2.stateRoot);
23234
+ const target = await lstat6(config2.stateRoot);
22412
23235
  if (target.isSymbolicLink() || !target.isDirectory()) {
22413
23236
  throw new Error("config.stateRoot must be a real directory, never a symbolic link");
22414
23237
  }
@@ -22437,11 +23260,11 @@ function rejectModeKeys(value, forbidden, bindingMode) {
22437
23260
  }
22438
23261
  }
22439
23262
  function deriveDshSessionId(projectId, sessionId, workspacePath) {
22440
- const digest3 = createHash3("sha256").update(JSON.stringify([projectId, sessionId, path5.resolve(workspacePath)])).digest("hex").slice(0, 32);
23263
+ const digest3 = createHash5("sha256").update(JSON.stringify([projectId, sessionId, path6.resolve(workspacePath)])).digest("hex").slice(0, 32);
22441
23264
  return `gatherthread-${digest3}`;
22442
23265
  }
22443
23266
  function deriveNativeDshSessionId(projectId, sessionId, workspacePath) {
22444
- const digest3 = createHash3("sha256").update(JSON.stringify(["native-web-v2", projectId, sessionId, path5.resolve(workspacePath)])).digest("hex").slice(0, 32);
23267
+ const digest3 = createHash5("sha256").update(JSON.stringify(["native-web-v2", projectId, sessionId, path6.resolve(workspacePath)])).digest("hex").slice(0, 32);
22445
23268
  return `gatherthread-${digest3}`;
22446
23269
  }
22447
23270
  function normalizeApiUrl(value) {
@@ -22479,13 +23302,13 @@ function safeText(value, label, maximum) {
22479
23302
  }
22480
23303
  function absolutePath(value, label) {
22481
23304
  const text = requiredString2(value, label, 4096);
22482
- if (!path5.isAbsolute(text))
23305
+ if (!path6.isAbsolute(text))
22483
23306
  throw new Error(`${label} must be absolute`);
22484
- return path5.resolve(text);
23307
+ return path6.resolve(text);
22485
23308
  }
22486
23309
  function isWithin2(parent, candidate) {
22487
- const relative = path5.relative(parent, candidate);
22488
- return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== "..";
23310
+ const relative = path6.relative(parent, candidate);
23311
+ return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== "..";
22489
23312
  }
22490
23313
  function isNodeError3(error62) {
22491
23314
  return error62 instanceof Error && "code" in error62;
@@ -22524,7 +23347,7 @@ function rejectUnknownKeys(value, allowed, label) {
22524
23347
  }
22525
23348
 
22526
23349
  // dist/src/connector.js
22527
- import { createHash as createHash4 } from "node:crypto";
23350
+ import { createHash as createHash6 } from "node:crypto";
22528
23351
 
22529
23352
  // dist/src/canonical-prompt.js
22530
23353
  var MAX_EVENT_TEXT_BYTES = 8 * 1024;
@@ -22597,12 +23420,12 @@ ${footer}`, "utf8");
22597
23420
  const retained = [];
22598
23421
  let used = 0;
22599
23422
  for (const line of contextLines.toReversed()) {
22600
- const bytes = Buffer.byteLength(`${line}
23423
+ const bytes2 = Buffer.byteLength(`${line}
22601
23424
  `, "utf8");
22602
- if (used + bytes > contextBudget)
23425
+ if (used + bytes2 > contextBudget)
22603
23426
  continue;
22604
23427
  retained.push(line);
22605
- used += bytes;
23428
+ used += bytes2;
22606
23429
  }
22607
23430
  retained.reverse();
22608
23431
  const omitted = retained.length < contextLines.length ? [`[${contextLines.length - retained.length} older public context update(s) omitted by size limit]`] : [];
@@ -22916,6 +23739,15 @@ var DshHostConnector = class {
22916
23739
  get stopped() {
22917
23740
  return this.#stopped;
22918
23741
  }
23742
+ /** Project refresh is authoritative for the mode of future DSH bubbles. */
23743
+ updateSessionSummary(session) {
23744
+ if (!this.#started || this.#stopped)
23745
+ throw new Error("DSH connector is not running");
23746
+ if (session.id !== this.#config.sessionId || session.projectId !== this.#config.projectId || session.state !== "active" || !isSessionWritableBy(session, this.#actorUserId)) {
23747
+ throw new Error("Refreshed GatherThread Session is not this writable binding");
23748
+ }
23749
+ this.#session = session;
23750
+ }
22919
23751
  /**
22920
23752
  * Adopt a refreshed execution-profile catalog on a running connection.
22921
23753
  *
@@ -23577,7 +24409,7 @@ var DshHostConnector = class {
23577
24409
  role: "user",
23578
24410
  content,
23579
24411
  occurredAt: event.timestamp,
23580
- ...event.actorDisplayName === void 0 ? {} : {
24412
+ ...this.#session?.mode !== "multi" || event.actorDisplayName === void 0 ? {} : {
23581
24413
  actorDisplayName: event.actorDisplayName
23582
24414
  }
23583
24415
  });
@@ -24011,7 +24843,7 @@ function turnSettlement(events) {
24011
24843
  return typeof reason?.kind === "string" ? reason.kind : void 0;
24012
24844
  }
24013
24845
  function digest2(value) {
24014
- return createHash4("sha256").update(value).digest("hex");
24846
+ return createHash6("sha256").update(value).digest("hex");
24015
24847
  }
24016
24848
  function cloneExecutionProfiles(profiles) {
24017
24849
  return profiles.map((profile) => ({
@@ -24147,7 +24979,7 @@ function asObject3(value) {
24147
24979
  }
24148
24980
 
24149
24981
  // dist/src/dsh-compat.js
24150
- import { createHash as createHash5 } from "node:crypto";
24982
+ import { createHash as createHash7 } from "node:crypto";
24151
24983
  var DSH_COMPATIBILITY = Object.freeze({
24152
24984
  tag: "dsh-v0.1.3-alpha.1",
24153
24985
  version: "0.1.3-alpha.1",
@@ -24193,8 +25025,8 @@ function createDshHostFacade(options) {
24193
25025
  let executionHandle;
24194
25026
  let contextPreparationPromise;
24195
25027
  const executionBinding = JSON.stringify({ nativeSessionId: options.sessionId, ...options.contextBinding });
24196
- const executionSessionId = `gatherthread-execution-${createHash5("sha256").update(executionBinding).digest("hex").slice(0, 32)}`;
24197
- const executionOwnerId = `gatherthread-execution-owner:${createHash5("sha256").update(executionBinding).digest("hex")}`;
25028
+ const executionSessionId = `gatherthread-execution-${createHash7("sha256").update(executionBinding).digest("hex").slice(0, 32)}`;
25029
+ const executionOwnerId = `gatherthread-execution-owner:${createHash7("sha256").update(executionBinding).digest("hex")}`;
24198
25030
  const executionOwnerText = JSON.stringify({
24199
25031
  version: 1,
24200
25032
  owner: "gatherthread",
@@ -24302,7 +25134,7 @@ function createDshHostFacade(options) {
24302
25134
  if (input2.selectedOnly && historyContext.items.length !== 0) {
24303
25135
  throw new Error("Selected-only summary execution cannot include other shared history");
24304
25136
  }
24305
- await open6();
25137
+ await open7();
24306
25138
  if (disposed || promptActive || projectionActive)
24307
25139
  throw new Error("DSH host facade permits only one active write");
24308
25140
  const parent = requireAgent(handle);
@@ -24323,7 +25155,7 @@ function createDshHostFacade(options) {
24323
25155
  throw new Error("DSH isolated Agent did not inherit its native Agent's exact tool composition");
24324
25156
  }
24325
25157
  };
24326
- const markerId = `gatherthread-context:${createHash5("sha256").update(JSON.stringify({
25158
+ const markerId = `gatherthread-context:${createHash7("sha256").update(JSON.stringify({
24327
25159
  requestId: input2.requestId,
24328
25160
  requestSequence: input2.requestSequence,
24329
25161
  selectedOnly: input2.selectedOnly,
@@ -24422,7 +25254,7 @@ ${JSON.stringify(historyContext.items)}`;
24422
25254
  }
24423
25255
  await presets.mount(agentContext, recorded ?? options.agentPreset);
24424
25256
  };
24425
- const open6 = () => {
25257
+ const open7 = () => {
24426
25258
  if (disposed)
24427
25259
  return Promise.reject(new Error("DSH host facade is disposed"));
24428
25260
  if (openPromise !== void 0)
@@ -24531,7 +25363,7 @@ ${JSON.stringify(historyContext.items)}`;
24531
25363
  let selectionRef;
24532
25364
  try {
24533
25365
  if (requestedExecutionId === void 0)
24534
- await open6();
25366
+ await open7();
24535
25367
  throwIfDisposed(disposed, lifecycleAbort.signal, "prompt");
24536
25368
  const agent = operationAgent(requestedExecutionId);
24537
25369
  if (requestedExecutionId !== void 0) {
@@ -24551,7 +25383,7 @@ ${JSON.stringify(historyContext.items)}`;
24551
25383
  }
24552
25384
  }
24553
25385
  const fromSequence = agent.session.seq;
24554
- const message = requestedExecutionId === void 0 ? await createUserMessage(text, options) : await createIsolatedMessage(`gatherthread-request:${createHash5("sha256").update(`${requestedExecutionId}:${fromSequence}:${text}`).digest("hex")}`, text, options);
25386
+ const message = requestedExecutionId === void 0 ? await createUserMessage(text, options) : await createIsolatedMessage(`gatherthread-request:${createHash7("sha256").update(`${requestedExecutionId}:${fromSequence}:${text}`).digest("hex")}`, text, options);
24555
25387
  throwIfDisposed(disposed, lifecycleAbort.signal, "prompt");
24556
25388
  if (agent.status !== "idle" || agent.session.seq !== fromSequence)
24557
25389
  throw new Error("DSH Agent changed while preparing a GatherThread prompt");
@@ -24582,7 +25414,7 @@ ${JSON.stringify(historyContext.items)}`;
24582
25414
  const flush = async () => {
24583
25415
  if (disposed)
24584
25416
  throw new Error("DSH host facade is disposed");
24585
- await open6();
25417
+ await open7();
24586
25418
  throwIfDisposed(disposed, lifecycleAbort.signal, "projection");
24587
25419
  await sessions.flush(requireAgent(handle).session);
24588
25420
  throwIfDisposed(disposed, lifecycleAbort.signal, "projection");
@@ -24596,7 +25428,7 @@ ${JSON.stringify(historyContext.items)}`;
24596
25428
  return;
24597
25429
  projectionActive = true;
24598
25430
  try {
24599
- await open6();
25431
+ await open7();
24600
25432
  throwIfDisposed(disposed, lifecycleAbort.signal, "projection");
24601
25433
  const agent = requireAgent(handle);
24602
25434
  if (agent.status !== "idle") {
@@ -24655,7 +25487,7 @@ ${JSON.stringify(historyContext.items)}`;
24655
25487
  };
24656
25488
  return {
24657
25489
  sessionId: options.sessionId,
24658
- open: open6,
25490
+ open: open7,
24659
25491
  currentSequence(requestedExecutionId) {
24660
25492
  return operationAgent(requestedExecutionId).session.seq;
24661
25493
  },
@@ -24903,8 +25735,11 @@ async function createProjectionMessage(projection, options) {
24903
25735
  });
24904
25736
  }
24905
25737
  function formatProjectionText(projection) {
24906
- if (projection.role === "user")
24907
- return projection.content;
25738
+ if (projection.role === "user") {
25739
+ const sender = projection.actorDisplayName?.replace(/[\p{Cc}\p{Cf}【】]+/gu, " ").replace(/\s+/gu, " ").trim();
25740
+ return sender ? `\u3010${sender} \xB7 GatherThread\u3011
25741
+ ${projection.content}` : projection.content;
25742
+ }
24908
25743
  const route = [projection.provider, projection.model].filter(Boolean).join(" / ");
24909
25744
  const attribution = [projection.actorDisplayName, route].filter(Boolean).join(" \xB7 ");
24910
25745
  return `[GatherThread remote Agent reply${attribution ? ` \xB7 ${attribution}` : ""}]
@@ -25069,9 +25904,9 @@ function asObject4(value) {
25069
25904
  }
25070
25905
 
25071
25906
  // dist/src/native-connection.js
25072
- import { createHash as createHash6 } from "node:crypto";
25907
+ import { createHash as createHash8 } from "node:crypto";
25073
25908
  import { homedir as homedir3 } from "node:os";
25074
- import path6 from "node:path";
25909
+ import path7 from "node:path";
25075
25910
  var DSH_NATIVE_CREDENTIAL_KEY = "gatherthread-dsh-host/default";
25076
25911
  var DSH_NATIVE_GRANT_SCHEMA_VERSION = 2;
25077
25912
  var DSH_NATIVE_PAIRING_CHANNEL = "/gatherthread";
@@ -25204,7 +26039,7 @@ function createNativeProjectConfig(options) {
25204
26039
  projectId: options.binding.projectId,
25205
26040
  projectName: options.binding.projectName,
25206
26041
  deviceId: options.grant.deviceId,
25207
- workspacePath: path6.resolve(options.workspacePath),
26042
+ workspacePath: path7.resolve(options.workspacePath),
25208
26043
  stateRoot,
25209
26044
  provider: options.binding.provider,
25210
26045
  model: options.binding.model,
@@ -25293,12 +26128,12 @@ function normalizeDshServerUrl(value) {
25293
26128
  return { serverUrl, apiUrl: `${serverUrl}/v1` };
25294
26129
  }
25295
26130
  function nativeStateRoot(dshHome, serverUrl, projectId, workspacePath) {
25296
- const digest3 = createHash6("sha256").update(JSON.stringify([serverUrl, projectId, path6.resolve(workspacePath)])).digest("hex").slice(0, 32);
25297
- return path6.join(path6.resolve(dshHome), "gatherthread", "state", digest3);
26131
+ const digest3 = createHash8("sha256").update(JSON.stringify([serverUrl, projectId, path7.resolve(workspacePath)])).digest("hex").slice(0, 32);
26132
+ return path7.join(path7.resolve(dshHome), "gatherthread", "state", digest3);
25298
26133
  }
25299
26134
  function resolveDshHome(env) {
25300
26135
  const configured = env.DSH_HOME?.trim();
25301
- return path6.resolve(configured ? configured : path6.join(homedir3(), ".dsh"));
26136
+ return path7.resolve(configured ? configured : path7.join(homedir3(), ".dsh"));
25302
26137
  }
25303
26138
  function parseBinding(value) {
25304
26139
  const input2 = exactObject(value, /* @__PURE__ */ new Set([
@@ -25442,8 +26277,8 @@ function isLoopback2(hostname3) {
25442
26277
  }
25443
26278
 
25444
26279
  // dist/src/project-manager.js
25445
- import path7 from "node:path";
25446
- import { createHash as createHash7 } from "node:crypto";
26280
+ import path8 from "node:path";
26281
+ import { createHash as createHash9 } from "node:crypto";
25447
26282
 
25448
26283
  // dist/src/execution-gate.js
25449
26284
  var BoundedDshExecutionGate = class {
@@ -25722,6 +26557,27 @@ var DshProjectManager = class {
25722
26557
  for (const error62 of permissions.errors)
25723
26558
  this.#reportError(error62);
25724
26559
  const eligibleSessions = [...permissions.eligibleSessions];
26560
+ const sessionUpdateErrors = [];
26561
+ for (const session of eligibleSessions) {
26562
+ const binding = this.#managed.get(session.id);
26563
+ if (binding?.phase === "active" && !binding.connector.stopped) {
26564
+ try {
26565
+ binding.connector.updateSessionSummary(session);
26566
+ } catch (error62) {
26567
+ this.#managed.delete(session.id);
26568
+ try {
26569
+ await binding.connector.stop();
26570
+ } catch (stopError) {
26571
+ this.#reportError(publicError2(stopError, "DSH Session connector cleanup failed"));
26572
+ }
26573
+ if (!this.#stopped)
26574
+ this.#recordFailure(session.id);
26575
+ const failure2 = publicError2(error62, `DSH Session ${session.id} mode refresh failed`);
26576
+ sessionUpdateErrors.push(failure2);
26577
+ this.#reportError(failure2);
26578
+ }
26579
+ }
26580
+ }
25725
26581
  if (this.#discoverLocalSessions !== void 0) {
25726
26582
  try {
25727
26583
  const candidates2 = await this.#discoverLocalSessions();
@@ -25765,7 +26621,7 @@ var DshProjectManager = class {
25765
26621
  status: "updated",
25766
26622
  eligibleSessionIds: [...eligibleIds].sort(),
25767
26623
  activeSessionIds: this.activeSessionIds(),
25768
- errors: [...permissions.errors, ...attachErrors]
26624
+ errors: [...permissions.errors, ...sessionUpdateErrors, ...attachErrors]
25769
26625
  };
25770
26626
  this.#notifyStatus({
25771
26627
  state: "connected",
@@ -25880,7 +26736,7 @@ function projectSessionConfig(config2, sessionId, nativeSessionId) {
25880
26736
  sessionId,
25881
26737
  deviceId: config2.deviceId,
25882
26738
  workspacePath: config2.workspacePath,
25883
- statePath: path7.join(config2.stateRoot, `${dshSessionId}.json`),
26739
+ statePath: path8.join(config2.stateRoot, `${dshSessionId}.json`),
25884
26740
  provider: config2.provider,
25885
26741
  model: config2.model,
25886
26742
  ...config2.executionProfiles === void 0 ? {} : {
@@ -25898,7 +26754,7 @@ function projectSessionConfig(config2, sessionId, nativeSessionId) {
25898
26754
  };
25899
26755
  }
25900
26756
  function nativeSessionCreationKey(projectId, localSessionId) {
25901
- const identity = createHash7("sha256").update(`${projectId}\0${localSessionId}`).digest("hex");
26757
+ const identity = createHash9("sha256").update(`${projectId}\0${localSessionId}`).digest("hex");
25902
26758
  return `dsh-native:${identity}`;
25903
26759
  }
25904
26760
  function retryDelay(config2, attempts) {
@@ -25928,31 +26784,31 @@ function publicError2(error62, fallback) {
25928
26784
  }
25929
26785
 
25930
26786
  // dist/src/state-store.js
25931
- import { randomUUID as randomUUID3 } from "node:crypto";
25932
- import { constants as constants3 } from "node:fs";
25933
- import { lstat as lstat6, mkdir as mkdir4, open as open5, rename as rename3, unlink as unlink3 } from "node:fs/promises";
25934
- import path8 from "node:path";
26787
+ import { randomUUID as randomUUID4 } from "node:crypto";
26788
+ import { constants as constants4 } from "node:fs";
26789
+ import { lstat as lstat7, mkdir as mkdir5, open as open6, rename as rename3, unlink as unlink4 } from "node:fs/promises";
26790
+ import path9 from "node:path";
25935
26791
  var FileConnectorStateStore = class {
25936
26792
  #path;
25937
26793
  constructor(statePath) {
25938
- if (!path8.isAbsolute(statePath))
26794
+ if (!path9.isAbsolute(statePath))
25939
26795
  throw new Error("DSH connector state path must be absolute");
25940
- this.#path = path8.resolve(statePath);
26796
+ this.#path = path9.resolve(statePath);
25941
26797
  }
25942
26798
  async load() {
25943
- const directory = path8.dirname(this.#path);
26799
+ const directory = path9.dirname(this.#path);
25944
26800
  const ancestorsBefore = await secureDirectoryChain(directory);
25945
26801
  let metadata;
25946
26802
  try {
25947
- metadata = await lstat6(this.#path);
26803
+ metadata = await lstat7(this.#path);
25948
26804
  } catch (error62) {
25949
26805
  if (isNodeError4(error62) && error62.code === "ENOENT")
25950
26806
  return void 0;
25951
26807
  throw error62;
25952
26808
  }
25953
26809
  assertPrivateRegularFile(metadata, "DSH connector state");
25954
- const noFollow = "O_NOFOLLOW" in constants3 ? constants3.O_NOFOLLOW : 0;
25955
- const handle = await open5(this.#path, constants3.O_RDONLY | noFollow);
26810
+ const noFollow = "O_NOFOLLOW" in constants4 ? constants4.O_NOFOLLOW : 0;
26811
+ const handle = await open6(this.#path, constants4.O_RDONLY | noFollow);
25956
26812
  let raw;
25957
26813
  try {
25958
26814
  const opened = await handle.stat();
@@ -25974,17 +26830,17 @@ var FileConnectorStateStore = class {
25974
26830
  }
25975
26831
  async save(state) {
25976
26832
  const validated = validateConnectorState(structuredClone(state));
25977
- const directory = path8.dirname(this.#path);
26833
+ const directory = path9.dirname(this.#path);
25978
26834
  await secureDirectoryChain(directory);
25979
- await mkdir4(directory, { recursive: true, mode: 448 });
26835
+ await mkdir5(directory, { recursive: true, mode: 448 });
25980
26836
  const ancestorsBefore = await secureDirectoryChain(directory);
25981
- const directoryBefore = await lstat6(directory);
26837
+ const directoryBefore = await lstat7(directory);
25982
26838
  assertSecureDirectory(directoryBefore);
25983
26839
  const targetBefore = await optionalLstat(this.#path);
25984
26840
  if (targetBefore !== void 0)
25985
26841
  assertPrivateRegularFile(targetBefore, "DSH connector state");
25986
- const temporaryPath = `${this.#path}.tmp-${process.pid}-${randomUUID3()}`;
25987
- const handle = await open5(temporaryPath, "wx", 384);
26842
+ const temporaryPath = `${this.#path}.tmp-${process.pid}-${randomUUID4()}`;
26843
+ const handle = await open6(temporaryPath, "wx", 384);
25988
26844
  try {
25989
26845
  await handle.writeFile(`${JSON.stringify(validated, null, 2)}
25990
26846
  `, { encoding: "utf8" });
@@ -25993,7 +26849,7 @@ var FileConnectorStateStore = class {
25993
26849
  await handle.close();
25994
26850
  }
25995
26851
  try {
25996
- const directoryAfter = await lstat6(directory);
26852
+ const directoryAfter = await lstat7(directory);
25997
26853
  if (directoryAfter.isSymbolicLink() || directoryAfter.dev !== directoryBefore.dev || directoryAfter.ino !== directoryBefore.ino) {
25998
26854
  throw new Error("DSH connector state directory changed during atomic save");
25999
26855
  }
@@ -26004,9 +26860,9 @@ var FileConnectorStateStore = class {
26004
26860
  }
26005
26861
  await rename3(temporaryPath, this.#path);
26006
26862
  assertSameDirectoryChain(ancestorsBefore, await secureDirectoryChain(directory));
26007
- assertPrivateRegularFile(await lstat6(this.#path), "DSH connector state");
26863
+ assertPrivateRegularFile(await lstat7(this.#path), "DSH connector state");
26008
26864
  } catch (error62) {
26009
- await unlink3(temporaryPath).catch(() => void 0);
26865
+ await unlink4(temporaryPath).catch(() => void 0);
26010
26866
  throw error62;
26011
26867
  }
26012
26868
  }
@@ -26269,7 +27125,7 @@ function isNodeError4(error62) {
26269
27125
  }
26270
27126
  async function optionalLstat(target) {
26271
27127
  try {
26272
- return await lstat6(target);
27128
+ return await lstat7(target);
26273
27129
  } catch (error62) {
26274
27130
  if (isNodeError4(error62) && error62.code === "ENOENT")
26275
27131
  return void 0;
@@ -26526,7 +27382,10 @@ var RPC_ENDPOINTS = /* @__PURE__ */ new Set([
26526
27382
  "sync/set-auto-upload",
26527
27383
  "sync/upload",
26528
27384
  "code/authorize",
26529
- "code/action"
27385
+ "code/action",
27386
+ "github/authorize",
27387
+ "github/action",
27388
+ "github/connect"
26530
27389
  ]);
26531
27390
  var MAX_CATALOG_PROVIDERS = 64;
26532
27391
  var MAX_CATALOG_MODELS = 256;
@@ -26628,6 +27487,12 @@ var DshNativeHostController = class {
26628
27487
  projectName: this.#projects.get(projectId)?.name ?? projectId,
26629
27488
  ...view
26630
27489
  }];
27490
+ }).slice(0, 100),
27491
+ githubSync: [...this.#owners].flatMap(([projectId, owner]) => {
27492
+ if (this.#projects.get(projectId)?.role === "viewer")
27493
+ return [];
27494
+ const view = owner.githubSyncView?.();
27495
+ return view === void 0 ? [] : [{ projectId, projectName: this.#projects.get(projectId)?.name ?? projectId, ...view }];
26631
27496
  }).slice(0, 100)
26632
27497
  }
26633
27498
  },
@@ -26889,6 +27754,40 @@ var DshNativeHostController = class {
26889
27754
  }
26890
27755
  return this.publicState();
26891
27756
  }
27757
+ async githubSyncAction(inputValue, authorize = false) {
27758
+ this.#assertAvailable();
27759
+ const input2 = exactInput(inputValue, new Set(authorize ? ["projectId", "enabled", "repository", "base_branch", "revision"] : ["projectId", "action"]));
27760
+ const projectId = safeInputIdentifier(input2.projectId, "projectId");
27761
+ const owner = this.#owners.get(projectId);
27762
+ if (!owner || this.#projects.get(projectId)?.role === "viewer")
27763
+ throw new Error("This Project is not writable on this DSH device");
27764
+ if (authorize) {
27765
+ if (typeof input2.enabled !== "boolean" || !owner.authorizeGitHubSync)
27766
+ throw new Error("Invalid GitHub authorization");
27767
+ let expected;
27768
+ if (input2.enabled) {
27769
+ if (typeof input2.repository !== "string" || typeof input2.base_branch !== "string" || typeof input2.revision !== "string" || input2.repository.length > 160 || input2.base_branch.length > 255 || input2.revision.length > 128)
27770
+ throw new Error("Invalid GitHub target");
27771
+ expected = { repository: input2.repository, base_branch: input2.base_branch, revision: input2.revision };
27772
+ }
27773
+ await owner.authorizeGitHubSync(input2.enabled, expected);
27774
+ } else {
27775
+ if (typeof input2.action !== "string" || !GITHUB_CODE_ACTIONS.includes(input2.action) || !owner.executeGitHubSync)
27776
+ throw new Error("Invalid GitHub action");
27777
+ await owner.executeGitHubSync(input2.action);
27778
+ }
27779
+ return this.publicState();
27780
+ }
27781
+ async connectGitHub(inputValue) {
27782
+ this.#assertAvailable();
27783
+ const input2 = exactInput(inputValue, /* @__PURE__ */ new Set(["projectId"]));
27784
+ const projectId = safeInputIdentifier(input2.projectId, "projectId");
27785
+ if (!this.#owners.has(projectId) || this.#projects.get(projectId)?.role === "viewer") {
27786
+ throw new Error("This Project is not writable on this DSH device");
27787
+ }
27788
+ await connectGitHubAccount();
27789
+ return this.publicState();
27790
+ }
26892
27791
  dispose() {
26893
27792
  this.#disposePromise ??= (async () => {
26894
27793
  this.#disposed = true;
@@ -27192,7 +28091,7 @@ async function apply(context, rawConfig) {
27192
28091
  workspacePath: config2.workspacePath,
27193
28092
  ...dshHome === void 0 ? {} : {
27194
28093
  dshHome,
27195
- projectWorkspaceHomeDirectory: path9.dirname(dshHome)
28094
+ projectWorkspaceHomeDirectory: path10.dirname(dshHome)
27196
28095
  },
27197
28096
  ...config2.officialServerUrl === void 0 ? {} : { officialServerUrl: config2.officialServerUrl }
27198
28097
  });
@@ -27250,6 +28149,12 @@ function registerNativeDshRpc(contextValue, controller) {
27250
28149
  return rpcSuccess(await controller.codeSyncAction(payload, true));
27251
28150
  case "code/action":
27252
28151
  return rpcSuccess(await controller.codeSyncAction(payload));
28152
+ case "github/authorize":
28153
+ return rpcSuccess(await controller.githubSyncAction(payload, true));
28154
+ case "github/action":
28155
+ return rpcSuccess(await controller.githubSyncAction(payload));
28156
+ case "github/connect":
28157
+ return rpcSuccess(await controller.connectGitHub(payload));
27253
28158
  default:
27254
28159
  return rpcFailure("gatherthread/not-found", "GatherThread DSH action is unavailable");
27255
28160
  }
@@ -27344,8 +28249,8 @@ async function createProductionOwner(options) {
27344
28249
  throw error62;
27345
28250
  }
27346
28251
  const codeSync = new DshCodeSyncController({
27347
- permissionPath: path9.join(config2.stateRoot, `code-consent-${createHash8("sha256").update(actor.id).digest("hex")}.json`),
27348
- binding: createHash8("sha256").update(JSON.stringify([
28252
+ permissionPath: path10.join(config2.stateRoot, `code-consent-${createHash10("sha256").update(actor.id).digest("hex")}.json`),
28253
+ binding: createHash10("sha256").update(JSON.stringify([
27349
28254
  config2.apiUrl,
27350
28255
  config2.projectId,
27351
28256
  actor.id,
@@ -27360,7 +28265,7 @@ async function createProductionOwner(options) {
27360
28265
  // A custom DSH home is an isolated profile, including its code-sync state.
27361
28266
  // Default profiles keep the harness-neutral state shared with Codex.
27362
28267
  ...options.dshHome === void 0 ? {} : {
27363
- stateRoot: path9.join(options.dshHome, "gatherthread-code-sync", createHash8("sha256").update(config2.workspacePath).digest("hex"))
28268
+ stateRoot: path10.join(options.dshHome, "gatherthread-code-sync", createHash10("sha256").update(config2.workspacePath).digest("hex"))
27364
28269
  }
27365
28270
  }),
27366
28271
  api: new HttpCollaborationClient({
@@ -27372,8 +28277,77 @@ async function createProductionOwner(options) {
27372
28277
  isBusy: () => nativeWorkspace.isBusy()
27373
28278
  });
27374
28279
  await codeSync.start().catch(() => void 0);
28280
+ const githubSync = new DshGitHubCodeSyncController({
28281
+ permissionPath: path10.join(config2.stateRoot, `github-consent-${createHash10("sha256").update(actor.id).digest("hex")}.json`),
28282
+ binding: createHash10("sha256").update(JSON.stringify([
28283
+ "github",
28284
+ config2.apiUrl,
28285
+ config2.projectId,
28286
+ actor.id,
28287
+ config2.workspacePath
28288
+ ])).digest("hex"),
28289
+ readMetadata: async () => {
28290
+ const response = await fetch(`${config2.apiUrl}/projects/${encodeURIComponent(config2.projectId)}/github`, {
28291
+ headers: { authorization: `Bearer ${options.grant.token}` },
28292
+ redirect: "error",
28293
+ signal: AbortSignal.any([ownerAbort.signal, AbortSignal.timeout(15e3)])
28294
+ });
28295
+ if (!response.ok || !response.body)
28296
+ throw Object.assign(new Error("GitHub metadata unavailable"), { code: "code_github_unavailable" });
28297
+ const reader = response.body.getReader();
28298
+ const chunks = [];
28299
+ let size = 0;
28300
+ try {
28301
+ for (; ; ) {
28302
+ const next = await reader.read();
28303
+ if (next.done)
28304
+ break;
28305
+ size += next.value.byteLength;
28306
+ if (size > 16 * 1024)
28307
+ throw new Error("GitHub metadata too large");
28308
+ chunks.push(next.value);
28309
+ }
28310
+ } finally {
28311
+ await reader.cancel().catch(() => void 0);
28312
+ }
28313
+ const envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
28314
+ return GitHubProjectStatusSchema.parse(envelope.data);
28315
+ },
28316
+ createEngine: (target) => createGitHubCodeSync({
28317
+ apiUrl: config2.apiUrl,
28318
+ token: options.grant.token,
28319
+ projectId: config2.projectId,
28320
+ actorId: actor.id,
28321
+ workspacePath: config2.workspacePath,
28322
+ repository: target.repository,
28323
+ baseBranch: target.base_branch,
28324
+ revision: target.revision,
28325
+ ...options.dshHome === void 0 ? {} : {
28326
+ stateRoot: path10.join(options.dshHome, "gatherthread-code-sync", createHash10("sha256").update(config2.workspacePath).digest("hex"))
28327
+ }
28328
+ }),
28329
+ api: new HttpCollaborationClient({ baseUrl: config2.apiUrl, bearerToken: options.grant.token, signal: ownerAbort.signal }),
28330
+ runtimes: () => new Map([...codeRuntimes].flatMap(([id, connector]) => connector.stopped || !connector.executionRuntimeId ? [] : [[id, connector.executionRuntimeId]])),
28331
+ isBusy: () => nativeWorkspace.isBusy()
28332
+ });
28333
+ await githubSync.start().catch(() => void 0);
28334
+ let localCodeTail = Promise.resolve();
28335
+ function scheduleCode(operation) {
28336
+ const task = localCodeTail.then(operation);
28337
+ localCodeTail = task.catch(() => void 0);
28338
+ return task;
28339
+ }
28340
+ let codePolling = false;
27375
28341
  const codeTimer = setInterval(() => {
27376
- void codeSync.poll();
28342
+ if (codePolling)
28343
+ return;
28344
+ codePolling = true;
28345
+ void scheduleCode(async () => {
28346
+ await codeSync.poll();
28347
+ await githubSync.poll();
28348
+ }).catch(() => void 0).finally(() => {
28349
+ codePolling = false;
28350
+ });
27377
28351
  }, 5e3);
27378
28352
  codeTimer.unref();
27379
28353
  let stopPromise;
@@ -27383,14 +28357,19 @@ async function createProductionOwner(options) {
27383
28357
  setLocalAutoUpload: (sessionId, enabled) => manager.setLocalAutoUpload(sessionId, enabled),
27384
28358
  uploadLocalTurns: (sessionId) => manager.uploadLocalTurns(sessionId),
27385
28359
  codeSyncView: () => codeSync.view(),
27386
- authorizeCodeSync: (enabled) => codeSync.authorize(enabled),
27387
- executeCodeSync: (action) => codeSync.execute(action),
28360
+ authorizeCodeSync: (enabled) => scheduleCode(() => codeSync.authorize(enabled)),
28361
+ executeCodeSync: (action) => scheduleCode(() => codeSync.execute(action)),
28362
+ githubSyncView: () => githubSync.view(),
28363
+ authorizeGitHubSync: (enabled, expected) => scheduleCode(() => githubSync.authorize(enabled, expected)),
28364
+ executeGitHubSync: (action) => scheduleCode(() => githubSync.execute(action)),
27388
28365
  stop() {
27389
28366
  stopPromise ??= (async () => {
27390
28367
  options.signal.removeEventListener("abort", abortFromRoot);
27391
28368
  clearInterval(codeTimer);
27392
28369
  ownerAbort.abort(new Error("GatherThread native DSH manager stopped"));
28370
+ await localCodeTail;
27393
28371
  await codeSync.stop();
28372
+ await githubSync.stop();
27394
28373
  await manager.stop();
27395
28374
  nativeWorkspace.dispose();
27396
28375
  })();
@@ -27460,6 +28439,7 @@ function createNativeManagedConnector(options) {
27460
28439
  await connector.start();
27461
28440
  },
27462
28441
  updateExecutionProfiles: (profiles) => connector.updateExecutionProfiles(profiles),
28442
+ updateSessionSummary: (session) => connector.updateSessionSummary(session),
27463
28443
  localSyncStatus: () => connector.localSyncStatus(),
27464
28444
  setLocalAutoUpload: (enabled) => connector.setLocalAutoUpload(enabled),
27465
28445
  uploadLocalTurns: () => connector.uploadLocalTurns(),
@@ -27486,7 +28466,7 @@ async function resolveNativeProjectWorkspace(grant, binding, signal, homeDirecto
27486
28466
  }
27487
28467
  function nativeDshHomeFromEnvironment() {
27488
28468
  const value = process.env.DSH_HOME?.trim();
27489
- return value ? path9.resolve(value) : void 0;
28469
+ return value ? path10.resolve(value) : void 0;
27490
28470
  }
27491
28471
  async function listNativeLlmCatalog(contextValue, signal) {
27492
28472
  const llm = requireLlm(contextValue);