dsh-mnemon 0.5.32 → 0.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,7 @@ npx @deepseek-ai/dsh web
92
92
  4. For Memory Spaces, install the Mnemon CLI for Mnemon Native with `npm install --global @mnemon-dev/mnemon`, or enable another Provider on the Memory Spaces page.
93
93
  5. Choose the main strategy and enhancements under **Plugins → dsh-mnemon**.
94
94
 
95
- dsh-mnemon supports DSH `0.2.0-rc.2` (npm `latest`) and `0.2.1-rc.2` (npm `next`). The same package serves the desktop app's Plugins page, `dsh plugin --profile web add dsh-mnemon` on the command line, and Headless with `dsh plugin --profile headless add dsh-mnemon`; older hosts keep the release [compatibility and upgrades](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/reference/compatibility.md) names for them. [Getting started](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/guides/getting-started.md) continues from here, and [compatibility and upgrades](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/reference/compatibility.md) covers existing installations.
95
+ dsh-mnemon supports DSH `0.2.1-rc.2` (npm `latest` and `next`) and `0.2.0-rc.2`. The same package serves the desktop app's Plugins page, `dsh plugin --profile web add dsh-mnemon` on the command line, and Headless with `dsh plugin --profile headless add dsh-mnemon`; older hosts keep the release [compatibility and upgrades](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/reference/compatibility.md) names for them. [Getting started](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/guides/getting-started.md) continues from here, and [compatibility and upgrades](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/reference/compatibility.md) covers existing installations.
96
96
 
97
97
  ## How it works
98
98
 
@@ -146,3 +146,17 @@ pnpm verify:plugins
146
146
  ```
147
147
 
148
148
  Use Node.js `^22.19.0 || >=24.0.0` and pnpm. `node scripts/serve-e2e.mjs` starts a disposable real WebUI; add `--docs-demo` for the seeded project behind these screenshots, and `--live-model` to have the DeepSeek API answer, with the key read from `DEEPSEEK_API_KEY`. Mechanical tests are not claims about model accuracy or live cloud Providers. [Development and verification](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/development/README.md).
149
+
150
+ ## Star History
151
+
152
+ <a href="https://www.star-history.com/?repos=omdsh-dev%2Fdsh-mnemon&type=timeline&legend=bottom-right">
153
+ <picture>
154
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=omdsh-dev/dsh-mnemon&type=timeline&theme=dark&legend=bottom-right" />
155
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=omdsh-dev/dsh-mnemon&type=timeline&legend=bottom-right" />
156
+ <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=omdsh-dev/dsh-mnemon&type=timeline&legend=bottom-right" />
157
+ </picture>
158
+ </a>
159
+
160
+ ## License
161
+
162
+ [MIT](https://github.com/omdsh-dev/dsh-mnemon/blob/main/LICENSE). Third-party providers keep their own licenses and terms; see [Third-party notices](https://github.com/omdsh-dev/dsh-mnemon/blob/main/THIRD_PARTY_NOTICES.md).
package/README.zh-CN.md CHANGED
@@ -93,7 +93,7 @@ npx @deepseek-ai/dsh web
93
93
  4. 使用记忆空间时,可用 `npm install --global @mnemon-dev/mnemon` 安装 Mnemon Native 所需的 CLI,或在记忆空间页面启用其他 Provider。
94
94
  5. 在**插件 → 可组合记忆**中选择主策略与增强。
95
95
 
96
- dsh-mnemon 支持 DSH `0.2.0-rc.2`(npm `latest`)与 `0.2.1-rc.2`(npm `next`)。同一个包也用于桌面版的插件页、命令行 `dsh plugin --profile web add dsh-mnemon`,以及 Headless:`dsh plugin --profile headless add dsh-mnemon`;更早的宿主请继续使用[兼容性与升级](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/compatibility.md)为其列出的版本。接下来可以看[入门指南](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/guides/getting-started.md),已有安装的升级见[兼容性与升级](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/compatibility.md)。
96
+ dsh-mnemon 支持 DSH `0.2.1-rc.2`(npm `latest` 与 `next`)与 `0.2.0-rc.2`。同一个包也用于桌面版的插件页、命令行 `dsh plugin --profile web add dsh-mnemon`,以及 Headless:`dsh plugin --profile headless add dsh-mnemon`;更早的宿主请继续使用[兼容性与升级](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/compatibility.md)为其列出的版本。接下来可以看[入门指南](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/guides/getting-started.md),已有安装的升级见[兼容性与升级](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/reference/compatibility.md)。
97
97
 
98
98
  ## 工作原理
99
99
 
@@ -147,3 +147,17 @@ pnpm verify:plugins
147
147
  ```
148
148
 
149
149
  需要 Node.js `^22.19.0 || >=24.0.0` 与 pnpm。`node scripts/serve-e2e.mjs` 会启动一个用后即弃的真实 WebUI;加上 `--docs-demo` 即可得到这些截图背后的示例项目,再加 `--live-model` 则由 DeepSeek API(读取 `DEEPSEEK_API_KEY`)真实作答。机制测试不代表模型准确率,也不代表云端 Provider 的实际表现。详见[开发与验证](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/development/README.md)。
150
+
151
+ ## Star 历史
152
+
153
+ <a href="https://www.star-history.com/?repos=omdsh-dev%2Fdsh-mnemon&type=timeline&legend=bottom-right">
154
+ <picture>
155
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=omdsh-dev/dsh-mnemon&type=timeline&theme=dark&legend=bottom-right" />
156
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=omdsh-dev/dsh-mnemon&type=timeline&legend=bottom-right" />
157
+ <img alt="Star 历史图表" src="https://api.star-history.com/chart?repos=omdsh-dev/dsh-mnemon&type=timeline&legend=bottom-right" />
158
+ </picture>
159
+ </a>
160
+
161
+ ## 许可证
162
+
163
+ [MIT](https://github.com/omdsh-dev/dsh-mnemon/blob/main/LICENSE)。第三方 Provider 适用各自的许可证与服务条款,见[第三方声明](https://github.com/omdsh-dev/dsh-mnemon/blob/main/THIRD_PARTY_NOTICES.md)。
package/lib/client.js CHANGED
@@ -131,7 +131,11 @@ window.__ModuleLoader__.load({
131
131
  const SSH = /^ssh:\/\/[^\s/]+(?::\d+)?\/[^\s]+$/u;
132
132
  const SCP = /^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:[^\s]+$/u;
133
133
  const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u;
134
- const EMBEDDED_CREDENTIAL = /^https:\/\/[^/]*@/u;
134
+ const HTTPS_USER = /^https:\/\/([^/]*)@/u;
135
+ const USER_TEXT = /^(?:[A-Za-z0-9._~!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+$/u;
136
+ const NOT_A_NAME = /[:@\s\p{Cc}]/u;
137
+ const TOKEN_SHAPE = /^(?:gh[opsru]_|github_pat_|glpat-|gldt-|glcbt-|hf_|ATBB|ATCTT|ATATT)|^[0-9A-Fa-f]{32,}$|^[A-Za-z0-9]{52,}$/u;
138
+ const USER_NAME_MAX = 100;
135
139
  const EMAIL = /^[^\s<>@]+@[^\s<>@]+$/u;
136
140
  const WINDOWS_DRIVE = /^[A-Za-z]:[\\/]/u;
137
141
  const CONTROL = /[\p{Cc}\u2028\u2029]/u;
@@ -149,17 +153,35 @@ window.__ModuleLoader__.load({
149
153
  }
150
154
  /**
151
155
  * What is wrong with a repository address, or nothing for an https:// or ssh:// URL without
152
- * credentials in it, a git@host:path address, or a local path that `localPath` takes.
156
+ * credentials in it, a git@host:path address, or a local path that `localPath` takes. An HTTPS
157
+ * address may name the user a host wants its token offered under, as Bitbucket's do, but never a
158
+ * password, nor a token written where that name goes.
153
159
  */
154
160
  function syncRepositoryProblem(url, localPath) {
155
161
  if (url === "" || url.length > 2e3 || /\s/u.test(url)) return "format";
156
162
  if (SCHEME.test(url)) {
157
163
  if (!HTTPS.test(url) && !SSH.test(url)) return "format";
158
- return EMBEDDED_CREDENTIAL.test(url) ? "credentials" : void 0;
164
+ const user = HTTPS_USER.exec(url)?.[1];
165
+ return user === void 0 || syncUserName(user) ? void 0 : "credentials";
159
166
  }
160
167
  return SCP.test(url) || localPath(url) ? void 0 : "format";
161
168
  }
162
169
  /**
170
+ * Whether the user part of an HTTPS address is a user name. It is judged as Git reads it, with its
171
+ * escapes decoded, so an escaped colon, an escaped token prefix or an escaped line break counts as what
172
+ * it decodes to; one that does not decode is no name at all.
173
+ */
174
+ function syncUserName(written) {
175
+ if (!USER_TEXT.test(written)) return false;
176
+ let name;
177
+ try {
178
+ name = decodeURIComponent(written);
179
+ } catch {
180
+ return false;
181
+ }
182
+ return name.length <= USER_NAME_MAX && !NOT_A_NAME.test(name) && !TOKEN_SHAPE.test(name);
183
+ }
184
+ /**
163
185
  * A local path that is absolute on POSIX or on Windows, for the browser, which cannot tell which
164
186
  * platform the Host runs on: a POSIX path, a Windows path on a drive, or a UNC share. The Host
165
187
  * checks a path by its own platform's rule instead.
@@ -2832,6 +2854,11 @@ window.__ModuleLoader__.load({
2832
2854
  "config.syncNoCredentialToken": "推送需要访问令牌",
2833
2855
  "config.syncIssueAuthHttps": "需要凭据,请用 GitHub 登录或填写访问令牌",
2834
2856
  "config.syncIssueAuthToken": "需要凭据,请填写访问令牌",
2857
+ "config.syncIssueRejectedGitHub": "GitHub 拒绝了登录凭据,请重新登录,或检查它能否访问这个仓库",
2858
+ "config.syncIssueRejectedGitHubToken": "GitHub 拒绝了保存的访问令牌,请更换令牌",
2859
+ "config.syncIssueRejectedGitHubEnvironment": "GitHub 拒绝了 {env} 中的令牌,请更换它",
2860
+ "config.syncIssueRejected": "仓库主机拒绝了保存的访问令牌,请更换令牌,或在地址中写上它要求的用户名",
2861
+ "config.syncIssueRejectedEnvironment": "仓库主机拒绝了 {env} 中的令牌,请更换它",
2835
2862
  "config.syncIssueAuthSsh": "SSH 认证失败,请检查这台电脑的 SSH 密钥",
2836
2863
  "config.syncIssueAuth": "没有访问这个仓库的权限",
2837
2864
  "config.syncIssueNotFound": "找不到这个仓库,请检查地址和访问权限",
@@ -2850,7 +2877,7 @@ window.__ModuleLoader__.load({
2850
2877
  "config.syncRepoUrl": "仓库地址",
2851
2878
  "config.syncRepoUrlPlaceholder": "https://…、git@主机:路径 或本机绝对路径",
2852
2879
  "config.syncRepoUrlInvalid": "地址需要是 https://、ssh://、git@主机:路径 或本机绝对路径",
2853
- "config.syncRepoUrlCredentials": "不要把令牌写进地址,请填在“访问令牌”里",
2880
+ "config.syncRepoUrlCredentials": "地址里只能写用户名,令牌请填在“访问令牌”里",
2854
2881
  "config.syncToken": "访问令牌",
2855
2882
  "config.syncTokenOptional": "可选",
2856
2883
  "config.syncTokenSaved": "已保存;输入新令牌可替换",
@@ -3585,6 +3612,11 @@ window.__ModuleLoader__.load({
3585
3612
  "config.syncNoCredentialToken": "pushing needs an access token",
3586
3613
  "config.syncIssueAuthHttps": "Needs credentials; sign in with GitHub or add an access token",
3587
3614
  "config.syncIssueAuthToken": "Needs credentials; add an access token",
3615
+ "config.syncIssueRejectedGitHub": "GitHub rejected the sign-in; sign in again or check its access to this repository",
3616
+ "config.syncIssueRejectedGitHubToken": "GitHub rejected the saved access token; replace it",
3617
+ "config.syncIssueRejectedGitHubEnvironment": "GitHub rejected the token in {env}; replace it",
3618
+ "config.syncIssueRejected": "The host rejected the saved token; replace it, or name the user it expects in the address",
3619
+ "config.syncIssueRejectedEnvironment": "The host rejected the token in {env}; replace it",
3588
3620
  "config.syncIssueAuthSsh": "SSH authentication failed; check this computer's SSH key",
3589
3621
  "config.syncIssueAuth": "Access to this repository was refused",
3590
3622
  "config.syncIssueNotFound": "Repository not found; check the address and your access",
@@ -3603,7 +3635,7 @@ window.__ModuleLoader__.load({
3603
3635
  "config.syncRepoUrl": "Repository address",
3604
3636
  "config.syncRepoUrlPlaceholder": "https://…, git@host:path, or an absolute local path",
3605
3637
  "config.syncRepoUrlInvalid": "Use an https:// or ssh:// URL, git@host:path, or an absolute local path",
3606
- "config.syncRepoUrlCredentials": "Keep the token out of the address; enter it under Access token",
3638
+ "config.syncRepoUrlCredentials": "Only a user name may go in the address; enter the token under Access token",
3607
3639
  "config.syncToken": "Access token",
3608
3640
  "config.syncTokenOptional": "Optional",
3609
3641
  "config.syncTokenSaved": "Saved; type a new one to replace it",
@@ -7460,11 +7492,15 @@ window.__ModuleLoader__.load({
7460
7492
  minute: "2-digit"
7461
7493
  });
7462
7494
  }
7463
- /** Why a remote could not be read, said for the transport the repository uses. */
7464
- function issueText(t, issue, url, error) {
7495
+ /**
7496
+ * Why a remote could not be read, said for the transport the repository uses and for the credential
7497
+ * the Host offered it, which `credential` names as the status does.
7498
+ */
7499
+ function issueText(t, issue, url, error, credential = "none") {
7465
7500
  const transport = transportOf(url);
7466
7501
  switch (issue) {
7467
7502
  case "auth":
7503
+ if (transport === "https" && credential !== "none") return rejectedText(t, url, credential);
7468
7504
  if (transport === "https") return onGitHub(url) ? t("config.syncIssueAuthHttps") : t("config.syncIssueAuthToken");
7469
7505
  return transport === "ssh" ? t("config.syncIssueAuthSsh") : t("config.syncIssueAuth");
7470
7506
  case "not-found": return onGitHub(url) ? t("config.syncIssueNotFoundGitHub") : t("config.syncIssueNotFound");
@@ -7473,6 +7509,16 @@ window.__ModuleLoader__.load({
7473
7509
  default: return t("config.syncIssueUnknown", { error: error ?? "" });
7474
7510
  }
7475
7511
  }
7512
+ /**
7513
+ * A credential the host refused, said for where it came from: a saved token or the environment variable
7514
+ * comes before the GitHub sign-in, so only replacing that one changes what Git is given.
7515
+ */
7516
+ function rejectedText(t, url, credential) {
7517
+ const github = onGitHub(url);
7518
+ if (credential === "environment") return t(github ? "config.syncIssueRejectedGitHubEnvironment" : "config.syncIssueRejectedEnvironment", { env: MNEMON_SYNC_TOKEN_ENV });
7519
+ if (credential === "github") return t("config.syncIssueRejectedGitHub");
7520
+ return t(github ? "config.syncIssueRejectedGitHubToken" : "config.syncIssueRejected");
7521
+ }
7476
7522
  const REMOTE_ISSUE = {
7477
7523
  "remote-auth": "auth",
7478
7524
  "remote-not-found": "not-found",
@@ -7480,9 +7526,9 @@ window.__ModuleLoader__.load({
7480
7526
  "remote-timeout": "timeout"
7481
7527
  };
7482
7528
  /** A failure the Host named, in this page's language; anything it could not name keeps its own words. */
7483
- function failureText(t, reason, message, url) {
7529
+ function failureText(t, reason, message, url, credential = "none") {
7484
7530
  const issue = reason === void 0 ? void 0 : REMOTE_ISSUE[reason];
7485
- if (issue !== void 0) return issueText(t, issue, url, message);
7531
+ if (issue !== void 0) return issueText(t, issue, url, message, credential);
7486
7532
  switch (reason) {
7487
7533
  case "busy": return t("config.syncReasonBusy");
7488
7534
  case "no-repository": return t("config.syncReasonNoRepository");
@@ -7541,7 +7587,7 @@ window.__ModuleLoader__.load({
7541
7587
  * which computer, and what it held. A page is read at a time; the commit is a
7542
7588
  * quiet tag, because the time and the computer are what a reader looks for.
7543
7589
  */
7544
- function SyncHistoryDialog({ client, repoUrl, t, onClose }) {
7590
+ function SyncHistoryDialog({ client, repoUrl, credential, t, onClose }) {
7545
7591
  const [history, setHistory] = (0, react.useState)(null);
7546
7592
  const [busy, setBusy] = (0, react.useState)(true);
7547
7593
  const [failed, setFailed] = (0, react.useState)(null);
@@ -7561,13 +7607,14 @@ window.__ModuleLoader__.load({
7561
7607
  };
7562
7608
  });
7563
7609
  } catch (reason) {
7564
- setFailed(failureText(t, reasonOf(reason), message(reason), repoUrl));
7610
+ setFailed(failureText(t, reasonOf(reason), message(reason), repoUrl, credential));
7565
7611
  } finally {
7566
7612
  setBusy(false);
7567
7613
  }
7568
7614
  }, [
7569
7615
  client,
7570
7616
  repoUrl,
7617
+ credential,
7571
7618
  t
7572
7619
  ]);
7573
7620
  (0, react.useEffect)(() => {
@@ -7647,7 +7694,7 @@ window.__ModuleLoader__.load({
7647
7694
  * deleted, and which removals the branch recorded. Reading writes nothing; the
7648
7695
  * footer offers the sync itself and a merge that publishes nothing.
7649
7696
  */
7650
- function SyncCompareDialog({ client, repoUrl, writable, t, onClose, onSync, onChanged }) {
7697
+ function SyncCompareDialog({ client, repoUrl, credential, writable, t, onClose, onSync, onChanged }) {
7651
7698
  const [preview, setPreview] = (0, react.useState)(null);
7652
7699
  const [difference, setDifference] = (0, react.useState)(null);
7653
7700
  const [busy, setBusy] = (0, react.useState)("read");
@@ -7668,13 +7715,14 @@ window.__ModuleLoader__.load({
7668
7715
  setPreview(null);
7669
7716
  setDifference(null);
7670
7717
  setEmpty(reasonOf(reason) === "remote-empty");
7671
- setFailed(failureText(t, reasonOf(reason), message(reason), repoUrl));
7718
+ setFailed(failureText(t, reasonOf(reason), message(reason), repoUrl, credential));
7672
7719
  } finally {
7673
7720
  setBusy(null);
7674
7721
  }
7675
7722
  }, [
7676
7723
  client,
7677
7724
  repoUrl,
7725
+ credential,
7678
7726
  t
7679
7727
  ]);
7680
7728
  (0, react.useEffect)(() => {
@@ -7691,7 +7739,7 @@ window.__ModuleLoader__.load({
7691
7739
  const added = result.runtime?.added ?? 0;
7692
7740
  setNotice(revive ? t("config.syncCompareRevived", { count: added }) : added > 0 ? t("config.syncMergedCount", { count: added }) : t("config.syncMerged", { commit: result.commit.slice(0, 8) }));
7693
7741
  } catch (reason) {
7694
- failure = failureText(t, reasonOf(reason), message(reason), repoUrl);
7742
+ failure = failureText(t, reasonOf(reason), message(reason), repoUrl, credential);
7695
7743
  } finally {
7696
7744
  setBusy(null);
7697
7745
  }
@@ -8659,6 +8707,7 @@ window.__ModuleLoader__.load({
8659
8707
  const connected = status !== null && status.enabled !== false && repoUrl !== void 0;
8660
8708
  const editable = !disabled && client !== null;
8661
8709
  const gitReady = status?.git.available === true;
8710
+ const credential = status?.config.credentialSource ?? "none";
8662
8711
  /**
8663
8712
  * The one primary action: merge the repository here, then publish the result.
8664
8713
  * Answers why it did not finish, or null when it did, for a caller that shows it elsewhere.
@@ -8669,11 +8718,11 @@ window.__ModuleLoader__.load({
8669
8718
  setSyncFeedback(null);
8670
8719
  let failure = null;
8671
8720
  try {
8672
- const feedback = syncResult(t, await client.pushSync(), repoUrl);
8721
+ const feedback = syncResult(t, await client.pushSync(), repoUrl, credential);
8673
8722
  setSyncFeedback(feedback);
8674
8723
  if (feedback.tone === "error") failure = feedback.text;
8675
8724
  } catch (reason) {
8676
- failure = t("config.syncFailed", { error: failureText(t, reasonOf(reason), message(reason), repoUrl) });
8725
+ failure = t("config.syncFailed", { error: failureText(t, reasonOf(reason), message(reason), repoUrl, credential) });
8677
8726
  setSyncFeedback({
8678
8727
  tone: "error",
8679
8728
  text: failure
@@ -8755,7 +8804,7 @@ window.__ModuleLoader__.load({
8755
8804
  }));
8756
8805
  const autoHint = autoBackup?.lastError !== void 0 && minutes > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8757
8806
  className: MnemonSettingsCard_module_css_default.error,
8758
- children: t("config.syncAutoFailed", { error: failureText(t, autoBackup.lastFailure, autoBackup.lastError, repoUrl ?? "") })
8807
+ children: t("config.syncAutoFailed", { error: failureText(t, autoBackup.lastFailure, autoBackup.lastError, repoUrl ?? "", credential) })
8759
8808
  }) : minutes <= 0 ? t("config.syncAutoHintOff") : [autoBackup?.nextAt === void 0 ? "" : t("config.syncAutoNext", { time: when(autoBackup.nextAt) }), autoBackup?.lastAt === void 0 ? "" : t("config.syncAutoLast", { time: when(autoBackup.lastAt) })].filter(Boolean).join(" · ") || t("config.syncAutoHintOn");
8760
8809
  const lastRun = status?.lastRun;
8761
8810
  const syncHint = lastRun === void 0 ? t("config.syncNever") : lastRun.kind === "merge" ? t("config.syncLastMerge", { time: when(lastRun.at) }) : t("config.syncLast", { time: when(lastRun.at) });
@@ -8913,6 +8962,7 @@ window.__ModuleLoader__.load({
8913
8962
  client !== null && repoUrl !== void 0 && dialog === "compare" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncCompareDialog, {
8914
8963
  client,
8915
8964
  repoUrl,
8965
+ credential,
8916
8966
  writable: editable,
8917
8967
  t,
8918
8968
  onClose: () => setDialog(null),
@@ -8925,6 +8975,7 @@ window.__ModuleLoader__.load({
8925
8975
  client !== null && repoUrl !== void 0 && dialog === "history" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncHistoryDialog, {
8926
8976
  client,
8927
8977
  repoUrl,
8978
+ credential,
8928
8979
  t,
8929
8980
  onClose: () => setDialog(null)
8930
8981
  }),
@@ -8945,7 +8996,7 @@ window.__ModuleLoader__.load({
8945
8996
  });
8946
8997
  }
8947
8998
  /** What one finished sync did, said as one line. */
8948
- function syncResult(t, result, repoUrl) {
8999
+ function syncResult(t, result, repoUrl, credential) {
8949
9000
  const added = result.merged?.runtime?.added ?? 0;
8950
9001
  const commit = result.commit.slice(0, 8);
8951
9002
  if (result.pushed) return {
@@ -8961,7 +9012,7 @@ window.__ModuleLoader__.load({
8961
9012
  };
8962
9013
  return {
8963
9014
  tone: "error",
8964
- text: t("config.syncPushFailed", { reason: failureText(t, result.failure, result.reason ?? "", repoUrl) })
9015
+ text: t("config.syncPushFailed", { reason: failureText(t, result.failure, result.reason ?? "", repoUrl, credential) })
8965
9016
  };
8966
9017
  }
8967
9018
  /** Where the repository is, whether it answers, and how it is authenticated. */
@@ -8977,7 +9028,7 @@ window.__ModuleLoader__.load({
8977
9028
  text: t("config.syncReasonGitMissing")
8978
9029
  } : !remote.reachable ? {
8979
9030
  dot: "error",
8980
- text: issueText(t, remote.issue, url, remote.error)
9031
+ text: issueText(t, remote.issue, url, remote.error, status.config.credentialSource)
8981
9032
  } : {
8982
9033
  dot: credential === void 0 && transportOf(url) === "https" ? "warning" : "done",
8983
9034
  text: [remote.branchExists ? t("config.syncStateConnected") : t("config.syncStateNewBranch", { branch: status.config.branch }), ...credential !== void 0 ? [credential] : transportOf(url) === "https" ? [onGitHub(url) ? t("config.syncNoCredential") : t("config.syncNoCredentialToken")] : []].join(" · ")
package/lib/index.js CHANGED
@@ -155,7 +155,11 @@ const HTTPS = /^https:\/\/[^\s/]+(?::\d+)?\/[^\s]+$/u;
155
155
  const SSH = /^ssh:\/\/[^\s/]+(?::\d+)?\/[^\s]+$/u;
156
156
  const SCP = /^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:[^\s]+$/u;
157
157
  const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u;
158
- const EMBEDDED_CREDENTIAL = /^https:\/\/[^/]*@/u;
158
+ const HTTPS_USER = /^https:\/\/([^/]*)@/u;
159
+ const USER_TEXT = /^(?:[A-Za-z0-9._~!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+$/u;
160
+ const NOT_A_NAME = /[:@\s\p{Cc}]/u;
161
+ const TOKEN_SHAPE = /^(?:gh[opsru]_|github_pat_|glpat-|gldt-|glcbt-|hf_|ATBB|ATCTT|ATATT)|^[0-9A-Fa-f]{32,}$|^[A-Za-z0-9]{52,}$/u;
162
+ const USER_NAME_MAX = 100;
159
163
  const EMAIL = /^[^\s<>@]+@[^\s<>@]+$/u;
160
164
  const CONTROL = /[\p{Cc}\u2028\u2029]/u;
161
165
  const GITHUB_REPOSITORY = /^(?!\.\.?$)[A-Za-z0-9._-]{1,100}$/u;
@@ -172,16 +176,34 @@ function syncDirectoryParts(value) {
172
176
  }
173
177
  /**
174
178
  * What is wrong with a repository address, or nothing for an https:// or ssh:// URL without
175
- * credentials in it, a git@host:path address, or a local path that `localPath` takes.
179
+ * credentials in it, a git@host:path address, or a local path that `localPath` takes. An HTTPS
180
+ * address may name the user a host wants its token offered under, as Bitbucket's do, but never a
181
+ * password, nor a token written where that name goes.
176
182
  */
177
183
  function syncRepositoryProblem(url, localPath) {
178
184
  if (url === "" || url.length > 2e3 || /\s/u.test(url)) return "format";
179
185
  if (SCHEME.test(url)) {
180
186
  if (!HTTPS.test(url) && !SSH.test(url)) return "format";
181
- return EMBEDDED_CREDENTIAL.test(url) ? "credentials" : void 0;
187
+ const user = HTTPS_USER.exec(url)?.[1];
188
+ return user === void 0 || syncUserName(user) ? void 0 : "credentials";
182
189
  }
183
190
  return SCP.test(url) || localPath(url) ? void 0 : "format";
184
191
  }
192
+ /**
193
+ * Whether the user part of an HTTPS address is a user name. It is judged as Git reads it, with its
194
+ * escapes decoded, so an escaped colon, an escaped token prefix or an escaped line break counts as what
195
+ * it decodes to; one that does not decode is no name at all.
196
+ */
197
+ function syncUserName(written) {
198
+ if (!USER_TEXT.test(written)) return false;
199
+ let name;
200
+ try {
201
+ name = decodeURIComponent(written);
202
+ } catch {
203
+ return false;
204
+ }
205
+ return name.length <= USER_NAME_MAX && !NOT_A_NAME.test(name) && !TOKEN_SHAPE.test(name);
206
+ }
185
207
  /** A commit author's address. */
186
208
  function syncEmailValid(email) {
187
209
  return email.length <= 320 && EMAIL.test(email);
@@ -4597,6 +4619,47 @@ function isGitHubHttps(repoUrl) {
4597
4619
  }
4598
4620
  }
4599
4621
  /**
4622
+ * The user name a token goes to Git under: the one GitHub documents for its tokens and its sign-in's
4623
+ * grant, which every host has been offered, and which stays for every host not known to refuse it.
4624
+ */
4625
+ const TOKEN_USER = "x-access-token";
4626
+ /**
4627
+ * The hosts known to refuse `x-access-token` with a token, and the name each takes instead: Gitee
4628
+ * answers it with HTTP 403 and takes `oauth2` or the account name (#408). Any other host that wants
4629
+ * a name of its own, a self-hosted Gitee included, is given it in the address.
4630
+ */
4631
+ const TOKEN_USER_BY_HOST = /* @__PURE__ */ new Map([["gitee.com", "oauth2"]]);
4632
+ /**
4633
+ * From this version on, the last `credential.username` Git reads wins, which is the command line's.
4634
+ * Before it, the first one configured did, and only a name in the address came before that.
4635
+ */
4636
+ const LAST_USERNAME_WINS_GIT = "2.26";
4637
+ /**
4638
+ * Who Git is to look the token up as for one HTTPS address, and for which host: the user name the
4639
+ * address names, else the one its host takes a token under; the host exactly as the address writes
4640
+ * it, since Git compares a stored credential's host as text, its case and port included; and the
4641
+ * address without its user name. `user` is written as an address writes it, `name` as Git reads it.
4642
+ */
4643
+ function credentialTarget(url) {
4644
+ const [, written, host = "", rest = ""] = /^https:\/\/(?:([^/@]*)@)?([^/]*)(.*)$/u.exec(url) ?? [];
4645
+ let hostname = "";
4646
+ try {
4647
+ hostname = new URL(url).hostname.toLowerCase().replace(/\.$/u, "");
4648
+ } catch {}
4649
+ const user = written ?? TOKEN_USER_BY_HOST.get(hostname) ?? TOKEN_USER;
4650
+ let name = user;
4651
+ try {
4652
+ name = decodeURIComponent(user);
4653
+ } catch {}
4654
+ return {
4655
+ user,
4656
+ name,
4657
+ named: written !== void 0,
4658
+ host,
4659
+ address: "https://" + host + rest
4660
+ };
4661
+ }
4662
+ /**
4600
4663
  * What kind of failure one Git error describes. Git words these differently per
4601
4664
  * transport and per host, so the patterns are the phrases each transport uses:
4602
4665
  * a refused or missing credential, an address that leads nowhere (GitHub answers
@@ -4644,6 +4707,14 @@ function versionAtLeast(version, minimum) {
4644
4707
  return true;
4645
4708
  }
4646
4709
  /**
4710
+ * A boolean as Git reads one: `true`, `yes`, `on` or a number other than 0 is true, and
4711
+ * `false`, `no`, `off`, 0 or an empty value is false.
4712
+ */
4713
+ function gitBoolean(value) {
4714
+ const text = value.trim().toLowerCase();
4715
+ return text === "true" || text === "yes" || text === "on" || /^[-+]?\d+$/u.test(text) && Number(text) !== 0;
4716
+ }
4717
+ /**
4647
4718
  * The deletions one side of the channel recorded. They live in the sync extension, never
4648
4719
  * in the payload, so a Pack stays readable by any version; a branch that predates the
4649
4720
  * extension recorded none, and an unreadable list is treated the same way rather than
@@ -4932,6 +5003,8 @@ var MnemonGitSync = class {
4932
5003
  store;
4933
5004
  machine;
4934
5005
  sequence = 0;
5006
+ /** The Git version the last check found, which decides how a token's user name is given to Git. */
5007
+ gitVersion;
4935
5008
  auth;
4936
5009
  autoBackupView;
4937
5010
  constructor(runner, config, packs, run = runProcess, now = () => /* @__PURE__ */ new Date()) {
@@ -5579,12 +5652,14 @@ var MnemonGitSync = class {
5579
5652
  const result = await this.git(["--version"], { signal });
5580
5653
  version = /(\d+\.\d+(?:\.\d+)?)/u.exec(result.stdout)?.[1];
5581
5654
  } catch (error) {
5655
+ this.gitVersion = void 0;
5582
5656
  return {
5583
5657
  available: false,
5584
5658
  required,
5585
5659
  issue: error instanceof Error ? error.message : String(error)
5586
5660
  };
5587
5661
  }
5662
+ this.gitVersion = version;
5588
5663
  if (version === void 0) return {
5589
5664
  available: false,
5590
5665
  required,
@@ -6116,7 +6191,7 @@ var MnemonGitSync = class {
6116
6191
  }
6117
6192
  }
6118
6193
  async git(args, options) {
6119
- const prepared = this.credentials(args, options);
6194
+ const prepared = await this.credentials(args, options);
6120
6195
  try {
6121
6196
  return await this.run(GIT_LABEL, prepared.args, {
6122
6197
  timeoutMs: options.timeoutMs ?? 12e4,
@@ -6134,22 +6209,43 @@ var MnemonGitSync = class {
6134
6209
  * Hand the token to Git through a `0600` credential store that lives for one
6135
6210
  * command. The token never reaches an argument, a URL, a log, or the mirror,
6136
6211
  * and it is removed before the caller sees the result.
6212
+ *
6213
+ * Git must look the token up under the name the store holds, not under one this
6214
+ * machine configures, as Git Credential Manager setups do, nor by a path the store
6215
+ * does not hold (#408). Git 2.26 and later are told so on the command line, which
6216
+ * they read last and let win, and are given the address without a user name, as
6217
+ * Git offers a name it finds there once without the token. An older Git kept the
6218
+ * first name it read, and only a name in the address comes before that: the name
6219
+ * goes there only where this machine's configuration names another, and the path
6220
+ * leaves the lookup only where it puts the path in, so every other command, one
6221
+ * that configures the store's own name included, stays the one that has always run.
6137
6222
  */
6138
- credentials(args, options) {
6223
+ async credentials(args, options) {
6139
6224
  const token = options.token;
6140
6225
  if (token === void 0 || token === "" || options.authenticated !== true) return {
6141
6226
  args: [...NO_REWRITE, ...args],
6142
6227
  cleanup: () => {}
6143
6228
  };
6144
- const url = args.find((argument) => argument.startsWith("https://"));
6145
- if (url === void 0) return {
6229
+ const index = args.findIndex((argument) => argument.startsWith("https://"));
6230
+ if (index === -1) return {
6146
6231
  args: [...NO_REWRITE, ...args],
6147
6232
  cleanup: () => {}
6148
6233
  };
6234
+ const url = args[index];
6235
+ const target = credentialTarget(url);
6236
+ const command = [...args];
6237
+ const lookup = [];
6238
+ if (this.gitVersion !== void 0 && versionAtLeast(this.gitVersion, LAST_USERNAME_WINS_GIT)) {
6239
+ lookup.push("-c", "credential.useHttpPath=false", "-c", "credential.username=" + target.name);
6240
+ command[index] = target.address;
6241
+ } else {
6242
+ const configured = await this.configuredLookup(url, options);
6243
+ if (configured.path) lookup.push("-c", "credential.useHttpPath=false");
6244
+ if (configured.user !== void 0 && configured.user !== target.name && !target.named) command[index] = "https://" + target.user + "@" + target.address.slice(8);
6245
+ }
6149
6246
  this.sequence += 1;
6150
- const host = new URL(url).host;
6151
6247
  const path = join(this.store.directory(), ".credentials." + branchSlug(String(process.pid)) + "." + String(this.sequence) + ".tmp");
6152
- writeFileSync(path, "https://x-access-token:" + token + "@" + host + "\n", {
6248
+ writeFileSync(path, "https://" + target.user + ":" + encodeURIComponent(token) + "@" + target.host + "\n", {
6153
6249
  encoding: "utf8",
6154
6250
  mode: 384
6155
6251
  });
@@ -6160,13 +6256,53 @@ var MnemonGitSync = class {
6160
6256
  "credential.helper=",
6161
6257
  "-c",
6162
6258
  "credential.helper=store --file=" + shellQuote(path),
6163
- ...args
6259
+ ...lookup,
6260
+ ...command
6164
6261
  ],
6165
6262
  cleanup: () => {
6166
6263
  rmSync(path, { force: true });
6167
6264
  }
6168
6265
  };
6169
6266
  }
6267
+ /**
6268
+ * The user name this machine's configuration gives an address, if any, and whether
6269
+ * it adds the address's path to the lookup, read in the environment and directory
6270
+ * the command runs in. A configuration Git cannot read leaves the command as it
6271
+ * was, which fails on it the same way.
6272
+ */
6273
+ async configuredLookup(url, options) {
6274
+ let result;
6275
+ try {
6276
+ result = await this.run(GIT_LABEL, [
6277
+ "config",
6278
+ "--get-urlmatch",
6279
+ "credential",
6280
+ url
6281
+ ], {
6282
+ timeoutMs: options.timeoutMs ?? 12e4,
6283
+ env: gitEnvironment(),
6284
+ ...options.cwd === void 0 ? {} : { cwd: options.cwd },
6285
+ ...options.signal === void 0 ? {} : { signal: options.signal },
6286
+ label: GIT_LABEL
6287
+ });
6288
+ } catch (error) {
6289
+ if (options.signal?.aborted === true) throw error;
6290
+ return {
6291
+ user: void 0,
6292
+ path: false
6293
+ };
6294
+ }
6295
+ const settings = /* @__PURE__ */ new Map();
6296
+ if (result.exitCode === 0) for (const line of result.stdout.split(/\r?\n/u)) {
6297
+ const space = line.indexOf(" ");
6298
+ if (space > 0) settings.set(line.slice(0, space).toLowerCase(), line.slice(space + 1));
6299
+ }
6300
+ const path = settings.get("credential.usehttppath");
6301
+ return {
6302
+ user: settings.get("credential.username"),
6303
+ path: path !== void 0 && gitBoolean(path)
6304
+ };
6305
+ }
6170
6306
  };
6171
6307
  /**
6172
6308
  * How the payload files differ. The manifest and the checksum inventory describe a
@@ -12313,7 +12449,17 @@ var MnemonLifecycle = class {
12313
12449
  this.taskAgentIds.delete(sessionId);
12314
12450
  }
12315
12451
  }
12316
- /** Resolve the same model route and preset composition as an ordinary fresh DSH Agent. */
12452
+ /**
12453
+ * Resolve the same model route and preset composition as an ordinary fresh DSH Agent.
12454
+ *
12455
+ * DSH persists a task Agent's session like any other. Its sidebar and ACP session
12456
+ * list leave out a session whose header has `origin: 'subagent'`, the origin DSH
12457
+ * gives its own subagents. Without it the sidebar hides a session that never started
12458
+ * a turn only while DSH's projection cache holds a record of it, so a finished task
12459
+ * could stay listed as an untitled Ungrouped conversation (#435). No `parentSession`
12460
+ * or `delegationDepth`: the task belongs to no conversation, and its children stay
12461
+ * one level deep.
12462
+ */
12317
12463
  async taskAgentCreation(fallbackSessionId, workspaceRoot, signal) {
12318
12464
  const agentOptions = this.taskAgentModelOptions(fallbackSessionId, workspaceRoot);
12319
12465
  if (agentOptions === void 0) throw new Error("no default provider/model is available for a clean task Agent");
@@ -12321,15 +12467,17 @@ var MnemonLifecycle = class {
12321
12467
  const presets = presetService(this.ctx.get("agentPresets"));
12322
12468
  const presetId = presets === void 0 ? void 0 : (await presets.resolve()).id;
12323
12469
  const workingDirectory = workFrom === void 0 ? void 0 : this.ctx.get("workingDirectory");
12470
+ const meta = {
12471
+ cwd,
12472
+ origin: "subagent",
12473
+ ...presetId === void 0 ? {} : { agentPreset: presetId }
12474
+ };
12324
12475
  if (presets === void 0 && typeof workingDirectory?.set !== "function") return {
12325
- meta: { cwd },
12476
+ meta,
12326
12477
  agentOptions
12327
12478
  };
12328
12479
  return {
12329
- meta: {
12330
- cwd,
12331
- ...presetId === void 0 ? {} : { agentPreset: presetId }
12332
- },
12480
+ meta,
12333
12481
  agentOptions,
12334
12482
  setup: async (agentCtx, agent) => {
12335
12483
  if (presets !== void 0 && presetId !== void 0) await presets.mount(agentCtx, presetId);
@@ -537,6 +537,11 @@ export declare const zh: {
537
537
  'config.syncNoCredentialToken': string;
538
538
  'config.syncIssueAuthHttps': string;
539
539
  'config.syncIssueAuthToken': string;
540
+ 'config.syncIssueRejectedGitHub': string;
541
+ 'config.syncIssueRejectedGitHubToken': string;
542
+ 'config.syncIssueRejectedGitHubEnvironment': string;
543
+ 'config.syncIssueRejected': string;
544
+ 'config.syncIssueRejectedEnvironment': string;
540
545
  'config.syncIssueAuthSsh': string;
541
546
  'config.syncIssueAuth': string;
542
547
  'config.syncIssueNotFound': string;
@@ -13,7 +13,9 @@ export declare function syncDirectoryParts(value: string): string[] | undefined;
13
13
  export type MnemonSyncRepositoryProblem = 'format' | 'credentials';
14
14
  /**
15
15
  * What is wrong with a repository address, or nothing for an https:// or ssh:// URL without
16
- * credentials in it, a git@host:path address, or a local path that `localPath` takes.
16
+ * credentials in it, a git@host:path address, or a local path that `localPath` takes. An HTTPS
17
+ * address may name the user a host wants its token offered under, as Bitbucket's do, but never a
18
+ * password, nor a token written where that name goes.
17
19
  */
18
20
  export declare function syncRepositoryProblem(url: string, localPath: (path: string) => boolean): MnemonSyncRepositoryProblem | undefined;
19
21
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mnemon",
3
- "version": "0.5.32",
3
+ "version": "0.5.33",
4
4
  "description": "Composable, view-based memory for DeepSeek Harness. Pluggable sources and strategies, with layered memory out of the box.",
5
5
  "keywords": [
6
6
  "deepseek-harness",