@eduwork/dsh-mail 0.1.1 → 0.2.0

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/lib/index.js CHANGED
@@ -78,6 +78,43 @@ function pick(source, keys, forced) {
78
78
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
79
79
  return result;
80
80
  }
81
+ /** Shared config references used by schema validators and plugin runtimes. */
82
+ const write = Symbol.for("cosmokit.volatile.write");
83
+ function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
84
+ if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
85
+ if (value === null || typeof value !== "object") return value;
86
+ if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
87
+ ancestors.add(value);
88
+ try {
89
+ if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
90
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
91
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
92
+ } finally {
93
+ ancestors.delete(value);
94
+ }
95
+ }
96
+ /**
97
+ * Create a detached reference containing an immutable copy of the supplied data.
98
+ * @param value - validated config data; class instances and functions are unsupported.
99
+ * @returns a reference whose value is updated only by its owning runtime.
100
+ */
101
+ function createVolatile(value) {
102
+ let current = snapshot(value);
103
+ return Object.freeze({
104
+ get: () => current,
105
+ [write]: (value) => {
106
+ current = value;
107
+ }
108
+ });
109
+ }
110
+ /**
111
+ * Identify references across ESM/CJS copies of the shared library.
112
+ * @param value - a parsed config value.
113
+ * @returns whether the value implements the shared reference protocol.
114
+ */
115
+ function isVolatile(value) {
116
+ return typeof value === "object" && value !== null && write in value;
117
+ }
81
118
  /** Test values using `instanceof` with a `toStringTag` fallback. */
82
119
  function is(type, value) {
83
120
  if (arguments.length === 1) return (value) => is(type, value);
@@ -158,26 +195,47 @@ function clone(source, refs = /* @__PURE__ */ new Map()) {
158
195
  }
159
196
  return result;
160
197
  }
161
- /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
198
+ /**
199
+ * Compare values recursively, treating two volatile references as equal regardless of value.
200
+ * Strict comparison distinguishes null/undefined, treats opaque objects by identity,
201
+ * compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
202
+ * @param a - first value.
203
+ * @param b - second value.
204
+ * @param strict - whether to require strict data equality outside volatile references.
205
+ * @returns whether the values compare equal.
206
+ */
162
207
  function deepEqual(a, b, strict) {
163
- if (a === b) return true;
164
- if (!strict && isNullable(a) && isNullable(b)) return true;
165
- if (typeof a !== typeof b) return false;
166
- if (typeof a !== "object") return false;
167
- if (!a || !b) return false;
168
- function check(test, then) {
169
- return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
170
- }
171
- return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
172
- if (a.byteLength !== b.byteLength) return false;
173
- const viewA = new Uint8Array(a);
174
- const viewB = new Uint8Array(b);
175
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
176
- return true;
177
- }) ?? Object.keys({
178
- ...a,
179
- ...b
180
- }).every((key) => deepEqual(a[key], b[key], strict));
208
+ const ancestors = /* @__PURE__ */ new Set();
209
+ function compare(a, b) {
210
+ if (a === b) return true;
211
+ if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
212
+ if (!strict && isNullable(a) && isNullable(b)) return true;
213
+ if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
214
+ if (ancestors.has(a)) return false;
215
+ function check(test, then) {
216
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
217
+ }
218
+ ancestors.add(a);
219
+ try {
220
+ return check(Array.isArray, (a, b) => {
221
+ if (a.length !== b.length) return false;
222
+ for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
223
+ return true;
224
+ }) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
225
+ if (a.byteLength !== b.byteLength) return false;
226
+ const viewA = new Uint8Array(a);
227
+ const viewB = new Uint8Array(b);
228
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
229
+ return true;
230
+ }) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
231
+ ...a,
232
+ ...b
233
+ }).every((key) => compare(a[key], b[key])));
234
+ } finally {
235
+ ancestors.delete(a);
236
+ }
237
+ }
238
+ return compare(a, b);
181
239
  }
182
240
  /** Time constants plus parsing and formatting helpers. */
183
241
  var Time;
@@ -426,6 +484,7 @@ Schema.prototype.pattern = function pattern(regexp) {
426
484
  return schema;
427
485
  };
428
486
  Schema.prototype.simplify = function simplify(value) {
487
+ if (isVolatile(value)) value = value.get();
429
488
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
430
489
  if (isNullable(value)) return value;
431
490
  if (this.type === "object" || this.type === "dict") {
@@ -482,12 +541,49 @@ for (const key of [
482
541
  };
483
542
  return schema;
484
543
  } });
544
+ Schema.prototype.volatile = function volatile() {
545
+ if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
546
+ return this.extra("volatile", true);
547
+ };
485
548
  const resolvers = {};
549
+ const checkedVolatile = Symbol("checked-volatile-schema");
550
+ function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
551
+ const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
552
+ if (states.has(blocked)) return;
553
+ states.add(blocked);
554
+ seen.set(schema, states);
555
+ if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
556
+ const nested = blocked || !!schema.meta?.volatile;
557
+ if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
558
+ if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
559
+ if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
560
+ if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
561
+ }
486
562
  Schema.extend = function extend(type, resolve) {
487
563
  resolvers[type] = resolve;
488
564
  };
489
565
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
490
566
  if (!schema) return [data];
567
+ if (!options[checkedVolatile]) {
568
+ validateVolatileSchema(schema, options.path);
569
+ options = {
570
+ ...options,
571
+ [checkedVolatile]: true
572
+ };
573
+ }
574
+ if (schema.meta?.volatile) {
575
+ const inner = Schema(schema);
576
+ inner.meta = {
577
+ ...schema.meta,
578
+ volatile: false
579
+ };
580
+ const [value, adapted] = Schema.resolve(data, inner, options, strict);
581
+ try {
582
+ return [createVolatile(value), adapted];
583
+ } catch (error) {
584
+ throw new ValidationError(error instanceof Error ? error.message : String(error), options);
585
+ }
586
+ }
491
587
  if (options.ignore?.(data, schema)) return [data];
492
588
  if (isNullable(data) && schema.type !== "lazy") {
493
589
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -590,6 +686,7 @@ Schema.extend("lazy", (data, schema, options, strict) => {
590
686
  ...schema.meta,
591
687
  ...schema.inner.meta
592
688
  };
689
+ validateVolatileSchema(schema.inner, options.path, true);
593
690
  }
594
691
  return Schema.resolve(data, schema.inner, options, strict);
595
692
  });
@@ -689,7 +786,7 @@ function property(data, key, schema, options) {
689
786
  } catch (e) {
690
787
  if (!options?.autofix) throw e;
691
788
  delete data[key];
692
- return schema.meta.default;
789
+ return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
693
790
  }
694
791
  }
695
792
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -848,20 +945,21 @@ defineMethod("transform", [
848
945
  const MailSettingsSchema = Schema.object({
849
946
  readEnabled: Schema.boolean().default(DEFAULTS.readEnabled),
850
947
  sendEnabled: Schema.boolean().default(DEFAULTS.sendEnabled),
851
- email: Schema.string().default(DEFAULTS.email),
852
- username: Schema.string().default(DEFAULTS.username),
853
- fromName: Schema.string().default(DEFAULTS.fromName),
854
- inboxFolder: Schema.string().default(DEFAULTS.inboxFolder),
855
- imapHost: Schema.string().default(DEFAULTS.imapHost),
856
- imapPort: Schema.number().default(DEFAULTS.imapPort),
948
+ email: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.email),
949
+ username: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.username),
950
+ fromName: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.fromName),
951
+ inboxFolder: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.inboxFolder),
952
+ imapHost: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.imapHost),
953
+ imapPort: Schema.number().step(1).min(1).max(65535).default(DEFAULTS.imapPort),
857
954
  imapTls: Schema.union(["implicit", "starttls"]).default(DEFAULTS.imapTls),
858
- smtpHost: Schema.string().default(DEFAULTS.smtpHost),
859
- smtpPort: Schema.number().default(DEFAULTS.smtpPort),
955
+ smtpHost: Schema.string().pattern(/^[^\r\n\u0000]*$/u).default(DEFAULTS.smtpHost),
956
+ smtpPort: Schema.number().step(1).min(1).max(65535).default(DEFAULTS.smtpPort),
860
957
  smtpTls: Schema.union(["implicit", "starttls"]).default(DEFAULTS.smtpTls),
861
- maxBodyChars: Schema.number().default(DEFAULTS.maxBodyChars),
862
- maxMessageBytes: Schema.number().default(DEFAULTS.maxMessageBytes),
863
- maxAttachmentBytes: Schema.number().default(DEFAULTS.maxAttachmentBytes)
958
+ maxBodyChars: Schema.number().step(1).min(1e3).max(2e5).default(DEFAULTS.maxBodyChars),
959
+ maxMessageBytes: Schema.number().step(1).min(1048576).max(104857600).default(DEFAULTS.maxMessageBytes),
960
+ maxAttachmentBytes: Schema.number().step(1).min(1024).max(104857600).default(DEFAULTS.maxAttachmentBytes)
864
961
  });
962
+ const Config = typeof Schema.string().volatile === "function" ? Schema.object(Object.fromEntries(Object.entries(MailSettingsSchema.dict).map(([key, field]) => [key, field.volatile()]))) : void 0;
865
963
  function settingsBase(config = {}) {
866
964
  const base = { ...DEFAULTS };
867
965
  for (const key of Object.keys(DEFAULTS)) if (config[key] !== void 0) base[key] = config[key];
@@ -910,6 +1008,7 @@ const SEND_APPROVAL_REASON = "Send this email through the configured SMTP accoun
910
1008
  * after the user has explicitly enabled sending in Mail assistant settings.
911
1009
  */
912
1010
  function decideMailPermission(ctx, exec, next) {
1011
+ if (exec.name === TOOL_NAMES.attachment) return decideAttachmentWrite(ctx, exec, next);
913
1012
  if (exec.name !== TOOL_NAMES.send) return next();
914
1013
  const agent = exec.agent;
915
1014
  if (agent === void 0) return Promise.resolve({
@@ -922,6 +1021,38 @@ function decideMailPermission(ctx, exec, next) {
922
1021
  reason: SEND_APPROVAL_REASON
923
1022
  });
924
1023
  }
1024
+ async function decideAttachmentWrite(ctx, exec, next) {
1025
+ if (!exec.agent) return {
1026
+ kind: "deny",
1027
+ reason: "Attachment downloads require an Agent-backed session"
1028
+ };
1029
+ if (!exec.agent.session.header?.cwd) return {
1030
+ kind: "deny",
1031
+ reason: "Attachment downloads require a session workspace"
1032
+ };
1033
+ const resolver = ctx.get?.("sandboxPolicy");
1034
+ const policy = resolver ? resolver.resolve({ session: exec.agent.session }) : { mode: ctx.permissionPresets.current(exec.agent.session) };
1035
+ if (policy?.mode === "danger-full-access") return next();
1036
+ if (policy?.mode === "workspace-write") {
1037
+ if (policy.workspaceRoot !== void 0) {
1038
+ const options = {
1039
+ cwd: exec.agent.session.header.cwd,
1040
+ signal: exec.signal
1041
+ };
1042
+ const root = await ctx.fs.resolve(".", options);
1043
+ const allowed = await ctx.fs.resolve(policy.workspaceRoot, options);
1044
+ if (!ctx.fs.contains(allowed, root)) return {
1045
+ kind: "deny",
1046
+ reason: "Attachment workspace is outside the allowed write root"
1047
+ };
1048
+ }
1049
+ return next();
1050
+ }
1051
+ return {
1052
+ kind: "ask",
1053
+ reason: "Save this mail attachment in the current workspace"
1054
+ };
1055
+ }
925
1056
  Object.freeze({ SEND_APPROVAL_REASON });
926
1057
  //#endregion
927
1058
  //#region node_modules/pino-std-serializers/lib/err-helpers.js
@@ -101893,11 +102024,12 @@ async function readWorkspaceAttachments(ctx, paths, exec, maxBytes) {
101893
102024
  if (!Array.isArray(paths) || paths.length > 10 || paths.some((path) => typeof path !== "string" || path.trim() === "")) throw new Error("attachments must be an array of at most 10 non-empty workspace paths");
101894
102025
  const workspace = workspaceOf(exec);
101895
102026
  const workspaceTarget = await ctx.fs.resolve(workspace, { signal: exec.signal });
102027
+ const cwd = ctx.get?.("workingDirectory")?.get(exec.agent.session) ?? workspace;
101896
102028
  const attachments = [];
101897
102029
  let total = 0;
101898
102030
  for (const raw of paths) {
101899
102031
  const target = await ctx.fs.resolve(raw.trim(), {
101900
- cwd: workspace,
102032
+ cwd,
101901
102033
  signal: exec.signal
101902
102034
  });
101903
102035
  if (!ctx.fs.contains(workspaceTarget, target)) throw new Error(`Attachment is outside the session workspace: ${raw}`);
@@ -102412,14 +102544,16 @@ const inject = [
102412
102544
  "permissionPresets"
102413
102545
  ];
102414
102546
  function apply(ctx, config = {}) {
102415
- const base = settingsBase(config);
102547
+ const native = typeof ctx.settings.register !== "function";
102548
+ const readLive = () => Object.fromEntries(Object.keys(DEFAULTS).map((key) => [key, config[key].get()]));
102549
+ const base = native ? readLive() : settingsBase(config);
102416
102550
  validateSettings(base);
102417
- const scope = ctx.settings.register(SETTINGS_NAMESPACE, MailSettingsSchema, {
102551
+ const scope = native ? { get: readLive } : ctx.settings.register(SETTINGS_NAMESPACE, MailSettingsSchema, {
102418
102552
  base,
102419
102553
  applies: "live",
102420
102554
  validate: validateSettings
102421
102555
  });
102422
- const getSettings = () => currentSettings(ctx, scope, config);
102556
+ const getSettings = () => currentSettings(ctx, scope, native ? {} : config);
102423
102557
  ctx.on("tools/pre-execute", (exec, next) => decideMailPermission(ctx, exec, next));
102424
102558
  installTools(ctx, getSettings);
102425
102559
  ctx.tools.guard((exec) => {
@@ -102453,9 +102587,10 @@ function apply(ctx, config = {}) {
102453
102587
  var host_default = {
102454
102588
  name,
102455
102589
  inject,
102590
+ Config,
102456
102591
  apply
102457
102592
  };
102458
102593
  //#endregion
102459
- export { DEFAULTS, MailSettingsSchema, PASSWORD_REF_NAME, SETTINGS_NAMESPACE, TOOL_NAMES, apply, host_default as default, inject, name, validateSettings };
102594
+ export { Config, DEFAULTS, MailSettingsSchema, PASSWORD_REF_NAME, SETTINGS_NAMESPACE, TOOL_NAMES, apply, host_default as default, inject, name, validateSettings };
102460
102595
 
102461
102596
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduwork/dsh-mail",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Standards-based IMAP/SMTP mail assistant for DeepSeek Harness with explicit read/send controls and approval-aware delivery",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -23,7 +23,7 @@
23
23
  "lib/**/*.js",
24
24
  "docs/**/*.md",
25
25
  "README.md",
26
- "README.en.md",
26
+ "README_EN.md",
27
27
  "CHANGELOG.md",
28
28
  "CHANGELOG.en.md",
29
29
  "CODE_OF_CONDUCT.md",
@@ -50,7 +50,8 @@
50
50
  "inject": [
51
51
  "@deepseek-ai/dsh-api-remotes",
52
52
  "@deepseek-ai/dsh-client-ui-renderer",
53
- "@deepseek-ai/dsh-client-ui-settings"
53
+ "@deepseek-ai/dsh-client-ui-settings",
54
+ "@deepseek-ai/dsh-client-locale"
54
55
  ],
55
56
  "platform": "web",
56
57
  "immediately": true
@@ -78,7 +79,7 @@
78
79
  "prepare": "npm run build"
79
80
  },
80
81
  "engines": {
81
- "node": ">=22.0.0"
82
+ "node": ">=24.18.0"
82
83
  },
83
84
  "license": "MIT",
84
85
  "publishConfig": {
@@ -86,21 +87,23 @@
86
87
  },
87
88
  "repository": {
88
89
  "type": "git",
89
- "url": "git+https://github.com/freedomkk-qfeng/dsh-mail-assistant.git"
90
+ "url": "git+https://github.com/ECNU/EduWork.git",
91
+ "directory": "packages/dsh-mail"
90
92
  },
91
93
  "bugs": {
92
- "url": "https://github.com/freedomkk-qfeng/dsh-mail-assistant/issues"
94
+ "url": "https://github.com/ECNU/EduWork/issues"
93
95
  },
94
- "homepage": "https://github.com/freedomkk-qfeng/dsh-mail-assistant#readme",
96
+ "homepage": "https://github.com/ECNU/EduWork/tree/main/packages/dsh-mail#readme",
95
97
  "peerDependencies": {
96
- "@deepseek-ai/cordis": "4.0.2",
97
- "@deepseek-ai/dsh-api-remotes": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
98
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
99
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
100
- "@deepseek-ai/dsh-credentials": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
101
- "@deepseek-ai/dsh-permission-presets": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
102
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-rc.1",
103
- "react": "^18.2.0"
98
+ "@deepseek-ai/cordis": ">=4.0.5 <4.1.0",
99
+ "@deepseek-ai/dsh-api-remotes": ">=0.2.1-rc.2 <0.3.0-0",
100
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.2 <0.3.0-0",
101
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.2.1-rc.2 <0.3.0-0",
102
+ "@deepseek-ai/dsh-credentials": ">=0.2.1-rc.2 <0.3.0-0",
103
+ "@deepseek-ai/dsh-permission-presets": ">=0.2.1-rc.2 <0.3.0-0",
104
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.2 <0.3.0-0",
105
+ "react": "^18.2.0",
106
+ "@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.2 <0.3.0-0"
104
107
  },
105
108
  "peerDependenciesMeta": {
106
109
  "@deepseek-ai/cordis": {
@@ -126,10 +129,13 @@
126
129
  },
127
130
  "react": {
128
131
  "optional": true
132
+ },
133
+ "@deepseek-ai/dsh-client-locale": {
134
+ "optional": true
129
135
  }
130
136
  },
131
137
  "devDependencies": {
132
- "@deepseek-ai/schemastery": "3.18.2",
138
+ "@deepseek-ai/schemastery": "3.18.5",
133
139
  "@types/node": "^26.0.0",
134
140
  "@types/react": "^18.3.28",
135
141
  "imapflow": "^1.7.0",
@@ -137,7 +143,10 @@
137
143
  "nodemailer": "^9.1.0",
138
144
  "react": "^18.3.1",
139
145
  "tsdown": "0.22.14",
140
- "typescript": "^5.9.3"
146
+ "typescript": "^5.9.3",
147
+ "@deepseek-ai/dsh": "0.2.1-rc.2",
148
+ "@deepseek-ai/cordis": "4.0.5",
149
+ "semver": "7.7.4"
141
150
  },
142
151
  "directories": {
143
152
  "doc": "docs",