@chance722/dsh-inbox 0.2.1 → 0.2.3

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
@@ -60,14 +60,14 @@ Two questions in the same conversation, on a real machine:
60
60
 
61
61
  ## Install
62
62
 
63
- Requires Node ≥ 22 and a working `dsh` (`@deepseek-ai/dsh`).
63
+ Needs Node ≥ 22 and a working `dsh`. `dsh plugin add` forwards to pnpm, so the installer brings pnpm along when your machine does not have it:
64
64
 
65
65
  **Platform**: fully accepted on **Windows** only so far; macOS and Linux are **not verified yet** (no platform-specific dependency in the code — try it and tell me how it goes).
66
66
 
67
67
  `dsh web` is just `dsh --profile web`, so install into the profile you already start:
68
68
 
69
69
  ```powershell
70
- npx @chance722/dsh-inbox init --profile web
70
+ npx @chance722/dsh-inbox init --profile web --install-pnpm
71
71
  ```
72
72
 
73
73
  Then start dsh the way you always do — `dsh web`. The **Inbox** entry is in the left rail, and a new session's assistant can look things up for you ("what links in my inbox haven't I read yet?").
@@ -75,7 +75,7 @@ Then start dsh the way you always do — `dsh web`. The **Inbox** entry is in th
75
75
  On a machine that has never run dsh there is no `web` profile yet; `--create-profile` makes it first:
76
76
 
77
77
  ```powershell
78
- npx @chance722/dsh-inbox init --profile web --create-profile
78
+ npx @chance722/dsh-inbox init --profile web --create-profile --install-pnpm
79
79
  ```
80
80
 
81
81
  `init` does three things, and running it twice is safe:
@@ -93,7 +93,7 @@ npx @chance722/dsh-inbox init --create-profile # an isolated `inbox` profile
93
93
  dsh --profile inbox --no-open --port 3102 # start it there
94
94
  ```
95
95
 
96
- Other flags: `--profile <name>` installs elsewhere, `--no-default` leaves the default preset alone, `--help` lists everything. Which profile you install into only decides **where the panel runs** — the agent preset is shared by every profile (`~/.dsh/.agent-presets/inbox/`), so installing into a second one just fills in the missing row.
96
+ Other flags: `--profile <name>` installs elsewhere, `--install-pnpm` installs pnpm first when it is missing (the two commands above already carry it), `--no-default` leaves the default preset alone, `--help` lists everything. Which profile you install into only decides **where the panel runs** — the agent preset is shared by every profile (`~/.dsh/.agent-presets/inbox/`), so installing into a second one just fills in the missing row.
97
97
 
98
98
  ### From a local checkout
99
99
 
package/README.zh.md CHANGED
@@ -60,14 +60,14 @@
60
60
 
61
61
  ## 安装
62
62
 
63
- 前置:Node ≥ 22,以及一个能用的 `dsh`(`@deepseek-ai/dsh`)。
63
+ 前置:Node ≥ 22,以及一个能用的 `dsh`。`dsh plugin add` 是转发给 pnpm 的,所以机器上没有 pnpm 时,安装命令会顺手替你装好:
64
64
 
65
65
  **平台**:目前只在 **Windows** 上做过完整验收;macOS / Linux **尚未验证**(代码里没有平台特定依赖,欢迎试用后反馈)。
66
66
 
67
67
  `dsh web` 就是 `dsh --profile web`,所以直接装进你日常启动的那个 profile:
68
68
 
69
69
  ```powershell
70
- npx @chance722/dsh-inbox init --profile web
70
+ npx @chance722/dsh-inbox init --profile web --install-pnpm
71
71
  ```
72
72
 
73
73
  然后照旧启动——`dsh web`。左栏出现 **Inbox**;新会话里问「我的收件箱里有哪些还没看的链接」,助手就会去查。
@@ -75,7 +75,7 @@ npx @chance722/dsh-inbox init --profile web
75
75
  全新机器(从没跑过 dsh,还没有 `web` 这个 profile)加 `--create-profile` 让它先建:
76
76
 
77
77
  ```powershell
78
- npx @chance722/dsh-inbox init --profile web --create-profile
78
+ npx @chance722/dsh-inbox init --profile web --create-profile --install-pnpm
79
79
  ```
80
80
 
81
81
  `init` 做三件事,重复运行是安全的:
@@ -93,7 +93,7 @@ npx @chance722/dsh-inbox init --create-profile # 建一个隔离的 inbox pr
93
93
  dsh --profile inbox --no-open --port 3102 # 在那个 profile 里起
94
94
  ```
95
95
 
96
- 其它选项:`--profile <名字>` 装到别处,`--no-default` 不动默认 preset,`--help` 列全。装进哪个 profile 只决定**面板跑在哪儿**——agent preset 是所有 profile 共享的(`~/.dsh/.agent-presets/inbox/`),装第二个 profile 只会补上缺的那行。
96
+ 其它选项:`--profile <名字>` 装到别处,`--install-pnpm` 在没有 pnpm 时先替你装(上面两条命令已经带上它),`--no-default` 不动默认 preset,`--help` 列全。装进哪个 profile 只决定**面板跑在哪儿**——agent preset 是所有 profile 共享的(`~/.dsh/.agent-presets/inbox/`),装第二个 profile 只会补上缺的那行。
97
97
 
98
98
  ### 从本地仓库装
99
99
 
package/lib/cli.js CHANGED
@@ -36,6 +36,7 @@ function usage() {
36
36
  --create-profile profile \u4E0D\u5B58\u5728\u65F6\u7528 dsh \u81EA\u5E26\u7684 web \u6A21\u677F\u5EFA\u4E00\u4E2A\uFF08\u9ED8\u8BA4\u4E0D\u5F00\uFF09
37
37
  --preset <id> agent preset \u7684 id\uFF0C\u9ED8\u8BA4 inbox
38
38
  --package <\u6765\u6E90> \u63D2\u4EF6\u6765\u6E90\uFF0C\u9ED8\u8BA4 ${PACKAGE_NAME}\uFF08\u672C\u5730\u5F00\u53D1\u4F20\u4ED3\u5E93\u8DEF\u5F84\uFF09
39
+ --install-pnpm PATH \u4E0A\u6CA1\u6709 pnpm \u65F6\u66FF\u4F60\u88C5\uFF08\u5148 npm i -g pnpm\uFF0C\u4E0D\u884C\u518D\u8BD5 corepack\uFF09
39
40
  --no-default \u4E0D\u628A\u9ED8\u8BA4 preset \u6307\u8FC7\u53BB\uFF08\u53EA\u88C5\uFF0C\u4E0D\u6539 dsh \u7684\u9ED8\u8BA4\u9009\u62E9\uFF09
40
41
  --help \u8FD9\u4EFD\u8BF4\u660E
41
42
 
@@ -49,7 +50,8 @@ function parse(argv) {
49
50
  preset: "",
50
51
  source: PACKAGE_NAME,
51
52
  defaultPreset: true,
52
- createProfile: false
53
+ createProfile: false,
54
+ installPnpm: false
53
55
  };
54
56
  const rest = argv[0] === "init" ? argv.slice(1) : argv;
55
57
  for (let index = 0; index < rest.length; index += 1) {
@@ -68,6 +70,7 @@ function parse(argv) {
68
70
  else if (flag === "--package") options.source = value();
69
71
  else if (flag === "--no-default") options.defaultPreset = false;
70
72
  else if (flag === "--create-profile") options.createProfile = true;
73
+ else if (flag === "--install-pnpm") options.installPnpm = true;
71
74
  else throw new Error(`\u770B\u4E0D\u61C2\u7684\u9009\u9879\uFF1A${String(flag)}`);
72
75
  }
73
76
  if (options.preset.length === 0) options.preset = "inbox";
@@ -222,6 +225,18 @@ function main(argv) {
222
225
  const profile = choice.profile;
223
226
  const chosen = choice.label.length === 0 ? "" : `\uFF08${choice.label}\uFF09`;
224
227
  const profileDir = join(home, "profiles", profile);
228
+ if (!hasPnpm()) {
229
+ if (!options.installPnpm) {
230
+ process.stderr.write(missingPnpmMessage(profile, options.source));
231
+ return 1;
232
+ }
233
+ process.stdout.write("\xB7 PATH \u4E0A\u6CA1\u6709 pnpm\uFF0C\u5148\u88C5\u4E00\u4E2A\uFF08--install-pnpm\uFF09\u2026\n");
234
+ if (!installPnpm()) {
235
+ process.stderr.write(missingPnpmMessage(profile, options.source));
236
+ return 1;
237
+ }
238
+ process.stdout.write(" pnpm \u88C5\u597D\u4E86\n");
239
+ }
225
240
  if (!existsSync(join(profileDir, "package.json"))) {
226
241
  const hint = `\u5148\u5EFA\u4E00\u4E2A\uFF1Adsh --profile ${profile} --from-default-profile web --dump-config
227
242
  \uFF08profile \u540D\u662F dsh \u81EA\u5E26\u6A21\u677F\u65F6\u7528\uFF1Adsh --profile ${profile} --dump-config\uFF09
@@ -322,6 +337,39 @@ function profileCreationAttempts(profile) {
322
337
  ["--profile", profile, "--dump-config"]
323
338
  ];
324
339
  }
340
+ function hasPnpm() {
341
+ const probe = spawnSync("pnpm", ["--version"], {
342
+ shell: process.platform === "win32",
343
+ stdio: "ignore"
344
+ });
345
+ return probe.error === void 0 && probe.status === 0;
346
+ }
347
+ function pnpmInstallAttempts() {
348
+ return [
349
+ ["npm", "i", "-g", "pnpm"],
350
+ ["corepack", "enable", "pnpm"]
351
+ ];
352
+ }
353
+ function installPnpm() {
354
+ for (const args of pnpmInstallAttempts()) {
355
+ const command = args.join(" ");
356
+ process.stdout.write(` ${command} \u2026
357
+ `);
358
+ const run = spawnSync(args[0], [...args].slice(1), {
359
+ stdio: "inherit",
360
+ shell: process.platform === "win32"
361
+ });
362
+ if (run.error === void 0 && run.status === 0 && hasPnpm()) return true;
363
+ }
364
+ return false;
365
+ }
366
+ function missingPnpmMessage(profile, source) {
367
+ return `\u88C5\u63D2\u4EF6\u9700\u8981 pnpm\uFF1Adsh \u7684 \`plugin add\` \u662F\u8F6C\u53D1\u7ED9 pnpm \u7684\uFF08\u4E0D\u662F\u672C\u547D\u4EE4\u7684\u9009\u62E9\uFF09\u3002
368
+ \u8981\u4E48\u81EA\u5DF1\u88C5\uFF1Anpm i -g pnpm \uFF08\u6216\u8005\uFF1Acorepack enable pnpm\uFF09
369
+ \u8981\u4E48\u91CD\u8DD1\u65F6\u52A0\u4E0A --install-pnpm\uFF0C\u8BA9\u672C\u547D\u4EE4\u66FF\u4F60\u88C5\u3002
370
+ \u624B\u52A8\u7B49\u4EF7\u547D\u4EE4\uFF1Adsh plugin --profile ${profile} add ${source}
371
+ `;
372
+ }
325
373
  function createProfile(profile) {
326
374
  let output = "";
327
375
  for (const args of profileCreationAttempts(profile)) {
@@ -342,6 +390,8 @@ export {
342
390
  ensurePresetRow,
343
391
  isEntryPoint,
344
392
  listProfiles,
393
+ missingPnpmMessage,
345
394
  parse,
395
+ pnpmInstallAttempts,
346
396
  profileCreationAttempts
347
397
  };
package/lib/client.js CHANGED
@@ -624,6 +624,14 @@ window.__ModuleLoader__.load({
624
624
  var INBOX_ENDPOINT_TAGS = "tags";
625
625
  var INBOX_ENDPOINT_SECRET = "secret";
626
626
  var INBOX_ENDPOINT_PUSH = "push";
627
+ var DEFAULT_SYNC_DIRECTORY = "inbox";
628
+ function syncDirectory(raw) {
629
+ const trimmed = (raw ?? "").trim().replace(/^\/+|\/+$/g, "");
630
+ return trimmed.length === 0 ? DEFAULT_SYNC_DIRECTORY : trimmed;
631
+ }
632
+ function syncRootFor(raw) {
633
+ return `${syncDirectory(raw)}/sync`;
634
+ }
627
635
  function isSuccess(status) {
628
636
  return status >= 200 && status < 300;
629
637
  }
@@ -816,6 +824,7 @@ window.__ModuleLoader__.load({
816
824
  "settings.save": "\u4FDD\u5B58",
817
825
  "settings.probe": "\u81EA\u68C0",
818
826
  "settings.details": "\u8BE6\u60C5\uFF08{count} \u6B21\u8BF7\u6C42\uFF09",
827
+ "settings.syncRootNow": "\u540C\u6B65\u6839\uFF1A{root}",
819
828
  "settings.noServiceBody": "\u8FD9\u4E2A\u7EC4\u5408\u91CC\u6CA1\u6709\u8BBE\u7F6E\u670D\u52A1\uFF0C\u5730\u5740\u6539\u4E0D\u4E86\u2014\u2014\u4F60\u591A\u534A\u5728\u7528 headless \u5F62\u6001\u5F00\u53D1\u3002",
820
829
  "settings.bucketHint": "\u522B\u7684\u8BBE\u5907\u5F80\u8FD9\u91CC\u6254\u4E1C\u897F",
821
830
  // Notices (the one-line status under the toolbar).
@@ -858,7 +867,9 @@ window.__ModuleLoader__.load({
858
867
  "sync.pullMerged": "\u4ECE\u4E91\u7AEF\u5408\u5E76 {count} \u6761{attachments}",
859
868
  "sync.pullAttachments": " / {count} \u4E2A\u9644\u4EF6",
860
869
  "sync.pullDone": "\u62C9\u53D6\u5B8C\u6210\uFF1A\u8FDC\u7AEF\u5217\u51FA {listed} \u9879\uFF0C\u65B0\u5165\u5E93 {pulled} \u6761\uFF0C\u8DF3\u8FC7 {skipped} \u6761{tail}",
861
- "sync.pullFailedCount": " \xB7 \u5931\u8D25 {count} \u6761",
870
+ "sync.pullSkipWhy": "\uFF08\u81EA\u5DF1\u7684\u540C\u6B65\u5BF9\u8C61 {sync} \u9879\u3001\u6BD4\u4E0A\u6B21\u62C9\u53D6\u66F4\u65E7 {older} \u9879\uFF09",
871
+ "sync.pullForeignSync": " \xB7 \u26A0\uFE0F \u8FD8\u53D1\u73B0\u53E6\u4E00\u5957\u540C\u6B65\u524D\u7F00 {roots}\uFF08\u672C\u673A\u662F {ours}\uFF09\uFF1A\u4E24\u53F0\u673A\u5668\u7684\u300C\u76EE\u5F55\u300D\u8BBE\u7F6E\u4E0D\u4E00\u81F4\uFF0C\u5148\u628A\u4E24\u8FB9\u6539\u6210\u540C\u4E00\u4E2A",
872
+ "sync.pullFailures": " \xB7 \u5931\u8D25 {count}\uFF1A{reason}",
862
873
  "sync.thisPage": " \xB7 \u672C\u9875 {count} \u6761",
863
874
  "settings.status": "{settings} \xB7 {webdav} \xB7 {s3}",
864
875
  "settings.statusOn": "\u8BBE\u7F6E\u670D\u52A1\u5728",
@@ -1072,6 +1083,7 @@ window.__ModuleLoader__.load({
1072
1083
  "settings.save": "Save",
1073
1084
  "settings.probe": "Test",
1074
1085
  "settings.details": "Details ({count} requests)",
1086
+ "settings.syncRootNow": "sync root: {root}",
1075
1087
  "settings.noServiceBody": "this composition has no settings service, so the address cannot be changed \u2014 you are most likely running the headless shape.",
1076
1088
  "settings.bucketHint": "other devices drop things here",
1077
1089
  "notice.listFailed": "Could not read the list: {reason}",
@@ -1112,7 +1124,9 @@ window.__ModuleLoader__.load({
1112
1124
  "sync.pullMerged": "Merged {count} from the cloud{attachments}",
1113
1125
  "sync.pullAttachments": " / {count} attachment(s)",
1114
1126
  "sync.pullDone": "Pull done: {listed} listed remotely, {pulled} filed, {skipped} skipped{tail}",
1115
- "sync.pullFailedCount": " \xB7 {count} failed",
1127
+ "sync.pullSkipWhy": " ({sync} of our own sync objects, {older} older than the last pull)",
1128
+ "sync.pullForeignSync": " \xB7 \u26A0\uFE0F another sync prefix exists: {roots} (this machine: {ours}) \u2014 two machines have different directory settings; make them match",
1129
+ "sync.pullFailures": " \xB7 {count} failed: {reason}",
1116
1130
  "sync.thisPage": " \xB7 {count} on this page",
1117
1131
  "settings.status": "{settings} \xB7 {webdav} \xB7 {s3}",
1118
1132
  "settings.statusOn": "settings service present",
@@ -3217,11 +3231,20 @@ window.__ModuleLoader__.load({
3217
3231
  attachments: attachments === 0 ? "" : t("sync.pullAttachments", { count: attachments })
3218
3232
  });
3219
3233
  if (result.pulled === 0 && result.skipped === 0 && result.failed === 0) return syncPart;
3234
+ const skippedWhy = result.skippedSync === void 0 ? "" : t("sync.pullSkipWhy", { sync: result.skippedSync, older: result.skippedOlder ?? 0 });
3235
+ const foreignWhy = result.foreignSyncRoots === void 0 || result.foreignSyncRoots.length === 0 ? "" : t("sync.pullForeignSync", {
3236
+ roots: result.foreignSyncRoots.join("\u3001"),
3237
+ ours: result.syncRoot ?? ""
3238
+ });
3239
+ const failedWhy = result.failed > 0 ? t("sync.pullFailures", {
3240
+ count: result.failed,
3241
+ reason: result.reason ?? t("sync.pushUnknown")
3242
+ }) : "";
3220
3243
  return t("sync.pullDone", {
3221
3244
  listed: result.listed,
3222
3245
  pulled: result.pulled,
3223
3246
  skipped: result.skipped,
3224
- tail: `${syncPart}${result.failed > 0 ? t("sync.pullFailedCount", { count: result.failed }) : ""}`
3247
+ tail: `${skippedWhy}${syncPart}${foreignWhy}${failedWhy}`
3225
3248
  });
3226
3249
  }
3227
3250
  function EncryptionSettings({ call }) {
@@ -3581,12 +3604,19 @@ window.__ModuleLoader__.load({
3581
3604
  }
3582
3605
  )
3583
3606
  ] }),
3584
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: { margin: 0, opacity: 0.6 }, children: [
3585
- t("settings.dirS3.lead"),
3586
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { children: "/inbox" }),
3587
- t("settings.dirS3.mid"),
3588
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { children: "inbox/" }),
3589
- t("settings.dirS3.tail")
3607
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
3608
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.7, minWidth: 64 }, children: t("settings.bucketDir") }),
3609
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3610
+ "input",
3611
+ {
3612
+ value: directory,
3613
+ disabled: busy,
3614
+ onChange: (event) => setDirectory(event.target.value),
3615
+ placeholder: "/inbox",
3616
+ style: { ...inputStyle, flex: 1 }
3617
+ }
3618
+ ),
3619
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.syncRootNow", { root: syncRootFor(directory) }) })
3590
3620
  ] })
3591
3621
  ] }),
3592
3622
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
3
3
 
4
4
  // src/shared/constants.ts
5
5
  var PACKAGE_NAME = "@chance722/dsh-inbox";
6
- var VERSION = true ? "0.2.1" : "0.0.0-dev";
6
+ var VERSION = true ? "0.2.3" : "0.0.0-dev";
7
7
  var DEFAULT_USER_AGENT = "dsh-inbox";
8
8
 
9
9
  // src/shared/vocabulary.ts
@@ -933,6 +933,42 @@ async function readFile(path, deps, absoluteUrl) {
933
933
  return { bytes, contentType: declared.split(";")[0] ?? "application/octet-stream" };
934
934
  }
935
935
 
936
+ // src/shared/panel-wire.ts
937
+ var INBOX_API_PREFIX = "/api/inbox";
938
+ var INBOX_ENDPOINT_CAPTURE = "capture";
939
+ var INBOX_ENDPOINT_LIST = "list";
940
+ var INBOX_ENDPOINT_DETAIL = "detail";
941
+ var INBOX_ENDPOINT_UPDATE = "update";
942
+ var INBOX_ENDPOINT_DELETE = "delete";
943
+ var INBOX_ENDPOINT_RESTORE = "restore";
944
+ var INBOX_ENDPOINT_PURGE = "purge";
945
+ var INBOX_ENDPOINT_ATTACHMENT = "attachment";
946
+ var INBOX_ENDPOINT_WEBDAV = "webdav";
947
+ var INBOX_ENDPOINT_PULL = "pull";
948
+ var INBOX_ENDPOINT_PROBE = "probe";
949
+ var INBOX_ENDPOINT_UI = "ui";
950
+ var INBOX_ENDPOINT_TAGS = "tags";
951
+ var INBOX_ENDPOINT_SECRET = "secret";
952
+ var INBOX_ENDPOINT_PUSH = "push";
953
+ var DEFAULT_SYNC_DIRECTORY = "inbox";
954
+ function syncDirectory(raw) {
955
+ const trimmed = (raw ?? "").trim().replace(/^\/+|\/+$/g, "");
956
+ return trimmed.length === 0 ? DEFAULT_SYNC_DIRECTORY : trimmed;
957
+ }
958
+ function syncRootFor(raw) {
959
+ return `${syncDirectory(raw)}/sync`;
960
+ }
961
+ var INBOX_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
962
+ var LIST_LIMIT = 50;
963
+ var UI_LIST_MODES = ["grid", "compact"];
964
+ var PREVIEW_CHARS = 140;
965
+ var MAX_ATTACHMENTS_PER_SUBMISSION = 20;
966
+ var MAX_NOTE_CHARS = 2e3;
967
+ var MAX_TAGS = 20;
968
+ var MAX_TAG_CHARS = 40;
969
+ var MAX_FILTER_CHARS = 200;
970
+ var MAX_TITLE_CHARS = 300;
971
+
936
972
  // src/host/webdav/config.ts
937
973
  import z from "@deepseek-ai/schemastery";
938
974
  var SETTINGS_NAMESPACE = "dsh-inbox-webdav";
@@ -1070,36 +1106,6 @@ async function saveWebdav(ctx, vault, base, patch) {
1070
1106
 
1071
1107
  // src/host/remote/pull.ts
1072
1108
  import { admitEncodedFile, admitEncodedImages } from "@deepseek-ai/dsh-attachment";
1073
-
1074
- // src/shared/panel-wire.ts
1075
- var INBOX_API_PREFIX = "/api/inbox";
1076
- var INBOX_ENDPOINT_CAPTURE = "capture";
1077
- var INBOX_ENDPOINT_LIST = "list";
1078
- var INBOX_ENDPOINT_DETAIL = "detail";
1079
- var INBOX_ENDPOINT_UPDATE = "update";
1080
- var INBOX_ENDPOINT_DELETE = "delete";
1081
- var INBOX_ENDPOINT_RESTORE = "restore";
1082
- var INBOX_ENDPOINT_PURGE = "purge";
1083
- var INBOX_ENDPOINT_ATTACHMENT = "attachment";
1084
- var INBOX_ENDPOINT_WEBDAV = "webdav";
1085
- var INBOX_ENDPOINT_PULL = "pull";
1086
- var INBOX_ENDPOINT_PROBE = "probe";
1087
- var INBOX_ENDPOINT_UI = "ui";
1088
- var INBOX_ENDPOINT_TAGS = "tags";
1089
- var INBOX_ENDPOINT_SECRET = "secret";
1090
- var INBOX_ENDPOINT_PUSH = "push";
1091
- var INBOX_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
1092
- var LIST_LIMIT = 50;
1093
- var UI_LIST_MODES = ["grid", "compact"];
1094
- var PREVIEW_CHARS = 140;
1095
- var MAX_ATTACHMENTS_PER_SUBMISSION = 20;
1096
- var MAX_NOTE_CHARS = 2e3;
1097
- var MAX_TAGS = 20;
1098
- var MAX_TAG_CHARS = 40;
1099
- var MAX_FILTER_CHARS = 200;
1100
- var MAX_TITLE_CHARS = 300;
1101
-
1102
- // src/host/remote/pull.ts
1103
1109
  var DEFAULT_DIRECTORY = "/inbox";
1104
1110
  var TEXT_TYPES = ["text/", "application/json"];
1105
1111
  var TEXT_SUFFIXES = [".txt", ".md", ".url", ".json"];
@@ -1108,8 +1114,11 @@ function looksTextual(name2, contentType) {
1108
1114
  if (TEXT_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return true;
1109
1115
  return contentType !== void 0 && TEXT_TYPES.some((prefix) => contentType.startsWith(prefix));
1110
1116
  }
1111
- function isSyncObject(path) {
1112
- return path.split("/").some((part) => part === "sync");
1117
+ function syncPrefixOf(path) {
1118
+ const parts = path.split("/").filter((part) => part.length > 0);
1119
+ const at = parts.lastIndexOf("sync");
1120
+ if (at === -1) return void 0;
1121
+ return parts.slice(0, at + 1).slice(-2).join("/");
1113
1122
  }
1114
1123
  function laterOf(current, candidate) {
1115
1124
  if (candidate === void 0) return current;
@@ -1128,7 +1137,7 @@ function isNewer(entry, lastPullAt) {
1128
1137
  if (Number.isNaN(seen) || Number.isNaN(remote)) return true;
1129
1138
  return remote > seen;
1130
1139
  }
1131
- async function ingestFrom(vault, source, attachments) {
1140
+ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(void 0)) {
1132
1141
  const lastPullAt = vault.global.sync.lastPullAt;
1133
1142
  let entries;
1134
1143
  try {
@@ -1141,15 +1150,28 @@ async function ingestFrom(vault, source, attachments) {
1141
1150
  let pulled = 0;
1142
1151
  let failed2 = 0;
1143
1152
  let skipped = 0;
1153
+ let skippedSync = 0;
1154
+ let skippedOlder = 0;
1155
+ let skippedForeign = 0;
1156
+ const foreignSyncRoots = /* @__PURE__ */ new Set();
1144
1157
  let newestSuccess;
1145
1158
  const failures = [];
1146
1159
  for (const entry of entries) {
1147
- if (isSyncObject(entry.path)) {
1160
+ const prefix = syncPrefixOf(entry.path);
1161
+ if (prefix !== void 0 && prefix !== syncRoot3) {
1162
+ skipped += 1;
1163
+ skippedForeign += 1;
1164
+ foreignSyncRoots.add(prefix);
1165
+ continue;
1166
+ }
1167
+ if (prefix !== void 0) {
1148
1168
  skipped += 1;
1169
+ skippedSync += 1;
1149
1170
  continue;
1150
1171
  }
1151
1172
  if (!isNewer(entry, lastPullAt)) {
1152
1173
  skipped += 1;
1174
+ skippedOlder += 1;
1153
1175
  continue;
1154
1176
  }
1155
1177
  const name2 = nameOf(entry.path);
@@ -1207,6 +1229,11 @@ async function ingestFrom(vault, source, attachments) {
1207
1229
  pulled,
1208
1230
  failed: failed2,
1209
1231
  skipped,
1232
+ skippedSync,
1233
+ skippedOlder,
1234
+ skippedForeign,
1235
+ syncRoot: syncRoot3,
1236
+ ...foreignSyncRoots.size === 0 ? {} : { foreignSyncRoots: [...foreignSyncRoots] },
1210
1237
  listed,
1211
1238
  lastPullAt: cursor,
1212
1239
  ...failures.length === 0 ? {} : { reason: failures.slice(0, 3).join("\uFF1B") }
@@ -1224,7 +1251,7 @@ async function pullRemote(vault, config, deps) {
1224
1251
  listed: 0
1225
1252
  };
1226
1253
  }
1227
- const directory = config.directory ?? DEFAULT_DIRECTORY;
1254
+ const directory = `/${syncDirectory(config.directory ?? DEFAULT_DIRECTORY)}`;
1228
1255
  return ingestFrom(
1229
1256
  vault,
1230
1257
  {
@@ -1235,7 +1262,9 @@ async function pullRemote(vault, config, deps) {
1235
1262
  })),
1236
1263
  read: async (entry) => readFile(entry.path, deps, entry.path.startsWith("http") ? entry.path : void 0)
1237
1264
  },
1238
- deps.attachments
1265
+ deps.attachments,
1266
+ // WebDAV resolves the sync root from the same directory rule the writer uses.
1267
+ syncRootFor(config.directory)
1239
1268
  );
1240
1269
  }
1241
1270
  async function pullS3(vault, config, prefix, deps) {
@@ -1258,7 +1287,9 @@ async function pullS3(vault, config, prefix, deps) {
1258
1287
  })),
1259
1288
  read: async (entry) => readObject(config, entry.path, deps)
1260
1289
  },
1261
- deps.attachments
1290
+ deps.attachments,
1291
+ // S3 is handed the prefix by its caller — that prefix *is* the sync root.
1292
+ prefix
1262
1293
  );
1263
1294
  }
1264
1295
 
@@ -1570,8 +1601,7 @@ async function pullDropFolder(ctx, vault, attachments) {
1570
1601
 
1571
1602
  // src/host/remote/writer.ts
1572
1603
  function syncRoot(settings) {
1573
- const prefix = settings.directory.replace(/^\/+|\/+$/g, "");
1574
- return prefix.length === 0 ? "sync" : `${prefix}/sync`;
1604
+ return syncRootFor(settings.directory);
1575
1605
  }
1576
1606
  async function remoteWriter(ctx) {
1577
1607
  const settings = readSettings(ctx);
@@ -48,6 +48,8 @@ export interface Options {
48
48
  defaultPreset: boolean;
49
49
  /** Create the profile when it is missing, instead of refusing. */
50
50
  createProfile: boolean;
51
+ /** Install pnpm when it is missing, instead of stopping to explain it. */
52
+ installPnpm: boolean;
51
53
  }
52
54
  export declare function parse(argv: readonly string[]): Options | undefined;
53
55
  /**
@@ -118,3 +120,25 @@ export declare function isEntryPoint(moduleUrl: string, entry: string | undefine
118
120
  * @returns argv lists for `dsh`, most specific first.
119
121
  */
120
122
  export declare function profileCreationAttempts(profile: string): readonly (readonly string[])[];
123
+ /**
124
+ * The commands that can put pnpm on PATH, in the order to try them.
125
+ *
126
+ * `npm i -g pnpm` first because whoever ran `npx` has npm by definition and
127
+ * knows where its global shims land. `corepack enable pnpm` is the fallback:
128
+ * no download, but it needs a Node that still ships corepack (Node ≥ 25 does
129
+ * not), so it cannot be the first choice.
130
+ *
131
+ * @returns argv lists, most likely to work first.
132
+ */
133
+ export declare function pnpmInstallAttempts(): readonly (readonly string[])[];
134
+ /**
135
+ * What to say when there is no pnpm to hand.
136
+ *
137
+ * Exported because the wording is the whole value here: the point is that the
138
+ * reader can act on it without knowing which part of the chain wanted pnpm.
139
+ *
140
+ * @param profile - the profile that was about to receive the plugin.
141
+ * @param source - the package (or path) that was about to be installed.
142
+ * @returns the message, ending in the command to run by hand.
143
+ */
144
+ export declare function missingPnpmMessage(profile: string, source: string): string;
@@ -144,6 +144,7 @@ export declare const zh: {
144
144
  'settings.save': string;
145
145
  'settings.probe': string;
146
146
  'settings.details': string;
147
+ 'settings.syncRootNow': string;
147
148
  'settings.noServiceBody': string;
148
149
  'settings.bucketHint': string;
149
150
  'notice.listFailed': string;
@@ -184,7 +185,9 @@ export declare const zh: {
184
185
  'sync.pullMerged': string;
185
186
  'sync.pullAttachments': string;
186
187
  'sync.pullDone': string;
187
- 'sync.pullFailedCount': string;
188
+ 'sync.pullSkipWhy': string;
189
+ 'sync.pullForeignSync': string;
190
+ 'sync.pullFailures': string;
188
191
  'sync.thisPage': string;
189
192
  'settings.status': string;
190
193
  'settings.statusOn': string;
@@ -401,6 +404,7 @@ export declare const MESSAGES: {
401
404
  'settings.save': string;
402
405
  'settings.probe': string;
403
406
  'settings.details': string;
407
+ 'settings.syncRootNow': string;
404
408
  'settings.noServiceBody': string;
405
409
  'settings.bucketHint': string;
406
410
  'notice.listFailed': string;
@@ -441,7 +445,9 @@ export declare const MESSAGES: {
441
445
  'sync.pullMerged': string;
442
446
  'sync.pullAttachments': string;
443
447
  'sync.pullDone': string;
444
- 'sync.pullFailedCount': string;
448
+ 'sync.pullSkipWhy': string;
449
+ 'sync.pullForeignSync': string;
450
+ 'sync.pullFailures': string;
445
451
  'sync.thisPage': string;
446
452
  'settings.status': string;
447
453
  'settings.statusOn': string;
@@ -43,7 +43,9 @@ export interface RemoteSource {
43
43
  * @param attachments - the store that owns pulled bytes.
44
44
  * @returns what happened.
45
45
  */
46
- export declare function ingestFrom(vault: Vault, source: RemoteSource, attachments: AttachmentStore): Promise<PullResult>;
46
+ export declare function ingestFrom(vault: Vault, source: RemoteSource, attachments: AttachmentStore,
47
+ /** This machine's own sync root; anything else under `sync/` is a stranger. */
48
+ syncRoot?: string): Promise<PullResult>;
47
49
  /** What the user configures for WebDAV. */
48
50
  export interface WebdavConfig {
49
51
  baseUrl: string;
@@ -123,6 +123,33 @@ export interface WebdavStatus {
123
123
  credentialsAvailable: boolean;
124
124
  }
125
125
  /** What one pull did, for the panel and for the log. */
126
+ /** The directory a sync uses when the user never named one. */
127
+ export declare const DEFAULT_SYNC_DIRECTORY = "inbox";
128
+ /**
129
+ * The directory a sync actually uses, decided in one place.
130
+ *
131
+ * `/`, an empty string and "never named one" all mean the default. They used to
132
+ * mean different things depending on which path read them: `syncRoot()` turned a
133
+ * stripped-empty directory into the *bucket root* (`sync/`) while the
134
+ * drop-folder ingest defaulted to `/inbox`, so two machines set up through the
135
+ * same dialog could write to `sync/` and `inbox/sync/` respectively — and the
136
+ * merge, which reads its own prefix, then found nothing and reported nothing new
137
+ * (measured 2026-09-20).
138
+ *
139
+ * @param raw - whatever the settings hold, if anything.
140
+ * @returns the directory without leading or trailing slashes; never empty.
141
+ */
142
+ export declare function syncDirectory(raw: string | undefined): string;
143
+ /**
144
+ * Where the vault's own objects live: `<directory>/sync`.
145
+ *
146
+ * Shared by the writer, the drop-folder ingest and the display, so "which
147
+ * prefix is mine" has one answer on both sides of the wire.
148
+ *
149
+ * @param raw - the configured directory, if any.
150
+ * @returns the sync root, e.g. `inbox/sync`.
151
+ */
152
+ export declare function syncRootFor(raw: string | undefined): string;
126
153
  export interface PullResult {
127
154
  status: 'ok' | 'unconfigured' | 'failed';
128
155
  reason?: string;
@@ -137,6 +164,24 @@ export interface PullResult {
137
164
  failed: number;
138
165
  /** Files the server listed but we skipped as already-seen. */
139
166
  skipped: number;
167
+ /**
168
+ * How many of {@link skipped} were the vault's own upload queue (`sync/…`).
169
+ *
170
+ * Optional because the older shape of this result (and every test that builds
171
+ * one by hand) predates the split.
172
+ */
173
+ skippedSync?: number;
174
+ /** How many of {@link skipped} were older than the last pull's cursor. */
175
+ skippedOlder?: number;
176
+ /**
177
+ * How many of {@link skipped} sat under a `…/sync/` prefix that is **not**
178
+ * ours — another machine syncing under a different directory.
179
+ */
180
+ skippedForeign?: number;
181
+ /** Those other prefixes, e.g. `['inbox/sync']`; the panel warns about them. */
182
+ foreignSyncRoots?: string[];
183
+ /** This machine's own sync root, so the warning can name both sides. */
184
+ syncRoot?: string;
140
185
  /** How many entries the remote listed at all: distinguishes "empty folder"
141
186
  * from "everything already ingested". */
142
187
  listed: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chance722/dsh-inbox",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Personal paste inbox for DeepSeek Harness (dsh): capture, classify, browse and retrieve.",
6
6
  "keywords": [