dsh-auto-memory 0.3.0 → 0.4.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/README.md CHANGED
@@ -21,8 +21,24 @@ Say *"Remember: I'm a Python backend engineer preparing for interviews"* today
21
21
  open a brand-new session tomorrow, ask *"what do you know about me?"*, and it
22
22
  **remembers**.
23
23
 
24
+ ![Memory management panel in the dsh Web UI](imgs/img-1.png)
25
+
24
26
  ---
25
27
 
28
+ ## What's new in 0.4.0 (P2)
29
+
30
+ - **Web UI memory panel** (the screenshot above): a dual-half plugin — the host
31
+ registers authenticated `/api/auto-memory/*` routes over the existing locked
32
+ store, the browser half registers a sidebar entry and a management page
33
+ (grouped list with types/pins/read counts, per-scope capacity bars with
34
+ budget warnings, inline editing, pin toggle, delete-with-confirm). Works in
35
+ Web and desktop clients from one bundle; non-Web compositions stay no-op.
36
+ - Panel bugfixes: routes now register via a connection-ready callback (host
37
+ load order used to skip them), and the editor uses official theme variables
38
+ (`--dsw-alias-*`) instead of a hardcoded white — readable in dark mode.
39
+ - Package hygiene: the browser half ships as `lib/client.js` (17 kB) behind a
40
+ `dsh.client` manifest; total install stays ~32 kB with zero runtime deps.
41
+
26
42
  ## What's new in 0.3.0 (P2)
27
43
 
28
44
  - **Pinned memories** (`pinned: true` on memory_write): pinned entries lead
package/README.zh.md CHANGED
@@ -20,8 +20,21 @@ dsh plugin --profile demo add dsh-auto-memory
20
20
  今天对它说 *"记住:我是准备面试的 Python 后端工程师"*——
21
21
  明天开一个全新会话,问 *"你对我有什么了解?"*,它**记得**。
22
22
 
23
+ ![dsh Web UI 中的记忆管理面板](imgs/img-1-zh.png)
24
+
23
25
  ---
24
26
 
27
+ ## 0.4.0 新增(P2)
28
+
29
+ - **Web UI 记忆面板**(上方截图):双半插件——宿主半在认证围栏内注册
30
+ `/api/auto-memory/*` 路由(复用带锁 store),浏览器半注册侧栏入口与管理页
31
+ (按作用域分组的列表,含类型/置顶/读取计数;容量条与预算预警;行内编辑、
32
+ 置顶切换、确认删除)。一套 bundle 同时服务 Web 与桌面端;非 Web 组合自动 no-op。
33
+ - 面板修复:路由改经 connection 就绪回调注册(此前被宿主加载顺序跳过);
34
+ 编辑器改用官方主题变量(`--dsw-alias-*`)——深色模式可读。
35
+ - 包卫生:浏览器半以 `lib/client.js`(17 kB)随 `dsh.client` manifest 分发;
36
+ 总安装体积约 32 kB,运行时零依赖。
37
+
25
38
  ## 0.3.0 新增(P2)
26
39
 
27
40
  - **置顶记忆**(memory_write 传 `pinned: true`):置顶条目排在索引最前、
package/lib/client.js ADDED
@@ -0,0 +1,539 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-auto-memory",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/MemoryPage.tsx
10
+ /**
11
+ * 记忆管理面板(主列全局面板):
12
+ * - 分组列表:用户级 + 各项目组(当前工作区组标记可编辑)
13
+ * - 容量条:每组索引字节 vs 注入预算(超 80% 预警)
14
+ * - 逐条操作:编辑(标题/摘要/正文)、置顶/取消置顶、删除(带确认)
15
+ * - 写入即生效:下次请求注入自动更新(纯文件派生索引,无需重启)
16
+ *
17
+ * 样式内联(官方 CSS Modules 管线未随 preset 发布);控件不依赖
18
+ * ui-primitives 具体导出面(降低跨版本脆弱性),仅用原生元素。
19
+ */
20
+ const GROUP_STYLE = {
21
+ margin: "16px 0",
22
+ padding: "12px",
23
+ border: "1px solid rgba(128,128,128,.35)",
24
+ borderRadius: "8px"
25
+ };
26
+ const BAR_STYLE = {
27
+ height: "6px",
28
+ borderRadius: "3px",
29
+ background: "rgba(128,128,128,.3)",
30
+ overflow: "hidden",
31
+ margin: "6px 0"
32
+ };
33
+ const ROW_STYLE = {
34
+ display: "flex",
35
+ gap: "8px",
36
+ alignItems: "baseline",
37
+ padding: "6px 0",
38
+ borderBottom: "1px solid rgba(128,128,128,.15)"
39
+ };
40
+ /** 悬浮编辑器容器:官方主题变量(--dsw-alias-*)+ 半透明灰回退,深浅主题均可读。 */
41
+ const EDITOR_STYLE = {
42
+ ...GROUP_STYLE,
43
+ position: "sticky",
44
+ bottom: "12px",
45
+ background: "var(--dsw-alias-bg-overlay, rgba(127,127,127,0.65))",
46
+ backdropFilter: "blur(6px)",
47
+ color: "var(--dsw-alias-label-primary, inherit)"
48
+ };
49
+ /** 记忆管理面板(face 方法经直通交叉成为顶层 props)。 */
50
+ function MemoryPage({ t, list, read, write, del, currentCwd }) {
51
+ const [groups, setGroups] = (0, react.useState)(null);
52
+ const [maxBytes, setMaxBytes] = (0, react.useState)(4096);
53
+ const [error, setError] = (0, react.useState)(false);
54
+ const [editing, setEditing] = (0, react.useState)(null);
55
+ const [busy, setBusy] = (0, react.useState)(false);
56
+ const load = (0, react.useCallback)(async () => {
57
+ setError(false);
58
+ try {
59
+ const data = await list();
60
+ setGroups(data.groups);
61
+ setMaxBytes(data.maxBytes);
62
+ } catch {
63
+ setError(true);
64
+ }
65
+ }, [list]);
66
+ (0, react.useEffect)(() => {
67
+ load();
68
+ }, [load]);
69
+ const act = async (fn) => {
70
+ setBusy(true);
71
+ try {
72
+ const response = await fn();
73
+ if (!response.ok) console.warn("[auto-memory] action failed", response.status);
74
+ await load();
75
+ } finally {
76
+ setBusy(false);
77
+ }
78
+ };
79
+ const cwd = (group) => group.scope === "project" && group.current === true ? currentCwd() : void 0;
80
+ /** 打开编辑器:先拉取正文(列表响应不含 body)。 */
81
+ const openEditor = async (group, memory) => {
82
+ const detail = await read({
83
+ name: memory.name,
84
+ scope: group.scope,
85
+ cwd: cwd(group)
86
+ });
87
+ setEditing({
88
+ group,
89
+ memory,
90
+ body: "body" in detail ? detail.body : ""
91
+ });
92
+ };
93
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
94
+ style: { padding: "24px" },
95
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("error") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
96
+ onClick: () => {
97
+ load();
98
+ },
99
+ children: t("retry")
100
+ })]
101
+ });
102
+ if (groups === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
103
+ style: { padding: "24px" },
104
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("loading") })
105
+ });
106
+ const total = groups.reduce((sum, group) => sum + group.memories.length, 0);
107
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
108
+ style: {
109
+ padding: "20px",
110
+ maxWidth: "860px"
111
+ },
112
+ children: [
113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("title") }),
114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
115
+ style: { opacity: .75 },
116
+ children: t("intro")
117
+ }),
118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
119
+ onClick: () => {
120
+ load();
121
+ },
122
+ disabled: busy,
123
+ children: t("refresh")
124
+ }) }),
125
+ total === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("empty") }),
126
+ groups.map((group) => {
127
+ const usage = group.indexBytes / maxBytes;
128
+ const hot = usage > .8;
129
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
130
+ style: GROUP_STYLE,
131
+ children: [
132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
133
+ style: { margin: "0 0 4px" },
134
+ children: [group.scope === "user" ? t("userScope") : `${t("projectScope")} · ${group.slug}`, group.scope === "project" && group.current !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
135
+ style: {
136
+ marginLeft: "8px",
137
+ opacity: .65
138
+ },
139
+ children: t("projectNeedsSession")
140
+ })]
141
+ }),
142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
143
+ style: {
144
+ fontSize: "12px",
145
+ color: hot ? "#d97706" : "inherit"
146
+ },
147
+ children: [
148
+ t("indexUsage"),
149
+ ": ",
150
+ group.indexBytes,
151
+ t("bytes"),
152
+ " ",
153
+ t("of"),
154
+ " ",
155
+ t("budget"),
156
+ " ",
157
+ maxBytes,
158
+ t("bytes"),
159
+ " (",
160
+ Math.round(usage * 100),
161
+ "%)"
162
+ ]
163
+ }),
164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
165
+ style: BAR_STYLE,
166
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
167
+ width: `${Math.min(100, usage * 100)}%`,
168
+ height: "100%",
169
+ background: hot ? "#d97706" : "rgba(96,165,250,.9)"
170
+ } })
171
+ }),
172
+ group.memories.map((memory) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
173
+ style: ROW_STYLE,
174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
175
+ style: {
176
+ minWidth: 0,
177
+ flex: 1
178
+ },
179
+ children: [
180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: memory.title ?? memory.name }),
181
+ memory.pinned && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
182
+ title: t("pinned"),
183
+ children: " 📌"
184
+ }),
185
+ " ",
186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
187
+ style: {
188
+ fontSize: "11px",
189
+ opacity: .7
190
+ },
191
+ children: [
192
+ memory.type,
193
+ "/",
194
+ memory.name
195
+ ]
196
+ }),
197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
199
+ style: {
200
+ fontSize: "13px",
201
+ opacity: .85
202
+ },
203
+ children: memory.description
204
+ }),
205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
206
+ style: {
207
+ fontSize: "11px",
208
+ opacity: .55
209
+ },
210
+ children: [
211
+ " · ",
212
+ memory.reads,
213
+ " ",
214
+ t("reads")
215
+ ]
216
+ })
217
+ ]
218
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
219
+ style: { whiteSpace: "nowrap" },
220
+ children: [
221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
222
+ disabled: busy,
223
+ onClick: () => {
224
+ openEditor(group, memory);
225
+ },
226
+ children: t("edit")
227
+ }),
228
+ " ",
229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
230
+ disabled: busy,
231
+ onClick: () => {
232
+ (async () => {
233
+ const detail = await read({
234
+ name: memory.name,
235
+ scope: group.scope,
236
+ cwd: cwd(group)
237
+ });
238
+ const bodyText = "body" in detail ? detail.body : "";
239
+ await act(() => write({
240
+ cwd: cwd(group),
241
+ scope: group.scope,
242
+ name: memory.name,
243
+ title: memory.title,
244
+ description: memory.description,
245
+ type: memory.type,
246
+ body: bodyText,
247
+ pinned: !memory.pinned
248
+ }));
249
+ })();
250
+ },
251
+ children: memory.pinned ? t("unpinAction") : t("pinAction")
252
+ }),
253
+ " ",
254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
255
+ disabled: busy,
256
+ onClick: () => {
257
+ if (window.confirm(t("confirmDelete"))) act(() => del({
258
+ cwd: cwd(group),
259
+ scope: group.scope,
260
+ name: memory.name
261
+ }));
262
+ },
263
+ children: t("delete")
264
+ })
265
+ ]
266
+ })]
267
+ }, memory.name))
268
+ ]
269
+ }, group.key);
270
+ }),
271
+ editing !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoryEditor, {
272
+ t,
273
+ initial: editing.memory,
274
+ initialBody: editing.body,
275
+ onCancel: () => {
276
+ setEditing(null);
277
+ },
278
+ onSave: async (payload) => {
279
+ await act(() => write({
280
+ cwd: cwd(editing.group),
281
+ scope: editing.group.scope,
282
+ ...payload
283
+ }));
284
+ setEditing(null);
285
+ }
286
+ })
287
+ ]
288
+ });
289
+ }
290
+ /** 单条记忆编辑器(标题/摘要/正文,保存即重建索引、下次注入生效)。 */
291
+ function MemoryEditor({ t, initial, initialBody, onSave, onCancel }) {
292
+ const [title, setTitle] = (0, react.useState)(initial.title ?? "");
293
+ const [description, setDescription] = (0, react.useState)(initial.description);
294
+ const [body, setBody] = (0, react.useState)(initialBody);
295
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
296
+ style: EDITOR_STYLE,
297
+ children: [
298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
299
+ style: { margin: 0 },
300
+ children: [
301
+ t("edit"),
302
+ ": ",
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: initial.name })
304
+ ]
305
+ }),
306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
307
+ style: { margin: "8px 0" },
308
+ children: [
309
+ t("titleField"),
310
+ ":",
311
+ " ",
312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
313
+ style: { width: "60%" },
314
+ value: title,
315
+ onChange: (event) => {
316
+ setTitle(event.target.value);
317
+ }
318
+ })
319
+ ]
320
+ }),
321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
322
+ style: { margin: "8px 0" },
323
+ children: [
324
+ t("descriptionField"),
325
+ ":",
326
+ " ",
327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
328
+ style: { width: "80%" },
329
+ value: description,
330
+ onChange: (event) => {
331
+ setDescription(event.target.value);
332
+ }
333
+ })
334
+ ]
335
+ }),
336
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
337
+ style: { margin: "8px 0" },
338
+ children: [t("bodyField"), ":"]
339
+ }),
340
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
341
+ style: {
342
+ width: "100%",
343
+ minHeight: "120px",
344
+ boxSizing: "border-box"
345
+ },
346
+ value: body,
347
+ onChange: (event) => {
348
+ setBody(event.target.value);
349
+ }
350
+ }),
351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
352
+ style: { margin: "8px 0 0" },
353
+ children: [
354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
355
+ onClick: () => {
356
+ onSave({
357
+ name: initial.name,
358
+ title: title.trim().length > 0 ? title.trim() : void 0,
359
+ description,
360
+ type: initial.type,
361
+ body,
362
+ pinned: initial.pinned
363
+ });
364
+ },
365
+ children: t("save")
366
+ }),
367
+ " ",
368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
369
+ onClick: onCancel,
370
+ children: t("cancel")
371
+ })
372
+ ]
373
+ })
374
+ ]
375
+ });
376
+ }
377
+ //#endregion
378
+ //#region src/client/MemoryPanelIcon.tsx
379
+ /** 按侧栏请求的边长渲染图标(使用基线 ui-primitives 的通用图标)。 */
380
+ function MemoryPanelIcon(_props) {
381
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
382
+ "aria-hidden": true,
383
+ style: {
384
+ fontSize: "15px",
385
+ lineHeight: 1
386
+ },
387
+ children: "🗂"
388
+ });
389
+ }
390
+ //#endregion
391
+ //#region src/client/locales.ts
392
+ /**
393
+ * 面板词典(双语强制:locale.register 要求 zh/en 齐全)。
394
+ * key 集合即 PluginMemoryLocaleKey 的真值源。
395
+ */
396
+ const zh = {
397
+ panel: "记忆",
398
+ title: "持久记忆",
399
+ intro: "管理跨会话保留的记忆文件",
400
+ loading: "正在读取记忆…",
401
+ error: "暂时无法读取记忆。",
402
+ retry: "重试",
403
+ refresh: "刷新",
404
+ empty: "还没有任何记忆。对智能体说\"记住…\"即可写入第一条。",
405
+ userScope: "用户级(所有工作区)",
406
+ projectScope: "项目级",
407
+ pinned: "置顶",
408
+ reads: "次读取",
409
+ updated: "更新于",
410
+ indexUsage: "索引占用",
411
+ budget: "注入预算",
412
+ of: "/",
413
+ bytes: "B",
414
+ edit: "编辑",
415
+ save: "保存",
416
+ cancel: "取消",
417
+ delete: "删除",
418
+ confirmDelete: "删除这条记忆?",
419
+ deleteFailed: "删除失败",
420
+ saveFailed: "保存失败",
421
+ pinAction: "置顶",
422
+ unpinAction: "取消置顶",
423
+ projectNeedsSession: "打开该工作区的一个会话后可编辑此组",
424
+ name: "标识",
425
+ descriptionField: "摘要",
426
+ bodyField: "正文",
427
+ titleField: "标题(可选)"
428
+ };
429
+ const en = {
430
+ panel: "Memory",
431
+ title: "Persistent memory",
432
+ intro: "Manage the memory files kept across sessions",
433
+ loading: "Loading memories…",
434
+ error: "Could not load memories right now.",
435
+ retry: "Retry",
436
+ refresh: "Refresh",
437
+ empty: "No memories yet. Tell the agent \"remember…\" to write the first one.",
438
+ userScope: "User scope (all workspaces)",
439
+ projectScope: "Project scope",
440
+ pinned: "pinned",
441
+ reads: "reads",
442
+ updated: "updated",
443
+ indexUsage: "Index usage",
444
+ budget: "injection budget",
445
+ of: "/",
446
+ bytes: "B",
447
+ edit: "Edit",
448
+ save: "Save",
449
+ cancel: "Cancel",
450
+ delete: "Delete",
451
+ confirmDelete: "Delete this memory?",
452
+ deleteFailed: "Delete failed",
453
+ saveFailed: "Save failed",
454
+ pinAction: "Pin",
455
+ unpinAction: "Unpin",
456
+ projectNeedsSession: "Open a session in this workspace to edit this group",
457
+ name: "Name",
458
+ descriptionField: "Description",
459
+ bodyField: "Body",
460
+ titleField: "Title (optional)"
461
+ };
462
+ //#endregion
463
+ //#region src/client/index.ts
464
+ /** 词典命名空间。 */
465
+ const NS = "autoMemory";
466
+ /** 侧栏入口与主面板共用的面板 id。 */
467
+ const PANEL_ID = "auto-memory";
468
+ /** 浏览器半依赖的服务(slots/locale 为 web 组合必有;sessions 来自基线 client-store)。 */
469
+ const inject = [
470
+ "slots",
471
+ "locale",
472
+ "sessions"
473
+ ];
474
+ const BASE = "/api/auto-memory";
475
+ function memoryFace(ctx) {
476
+ return {
477
+ list: async () => {
478
+ const cwd = currentCwd(ctx);
479
+ const query = cwd === void 0 ? "" : `?cwd=${encodeURIComponent(cwd)}`;
480
+ const response = await fetch(`${BASE}/groups.list${query}`, { headers: { "cache-control": "no-store" } });
481
+ if (!response.ok) throw new Error(`groups.list HTTP ${String(response.status)}`);
482
+ return await response.json();
483
+ },
484
+ read: async ({ name, scope, cwd }) => {
485
+ const params = new URLSearchParams({
486
+ name,
487
+ scope
488
+ });
489
+ if (cwd !== void 0) params.set("cwd", cwd);
490
+ return (await fetch(`${BASE}/memory.read?${params}`)).json();
491
+ },
492
+ write: (payload) => fetch(`${BASE}/memory.write`, {
493
+ method: "POST",
494
+ headers: { "content-type": "application/json" },
495
+ body: JSON.stringify(payload)
496
+ }),
497
+ del: (payload) => fetch(`${BASE}/memory.delete`, {
498
+ method: "POST",
499
+ headers: { "content-type": "application/json" },
500
+ body: JSON.stringify(payload)
501
+ }),
502
+ currentCwd: () => currentCwd(ctx)
503
+ };
504
+ }
505
+ /** 从 sessions 快照取一个可用的工作区 cwd(官方 ui-reference 同款读取面)。 */
506
+ function currentCwd(ctx) {
507
+ const byId = ctx.get("sessions")?.list?.getSnapshot?.()?.byId;
508
+ if (byId === void 0) return void 0;
509
+ for (const session of Object.values(byId)) if (session.cwd !== void 0 && session.cwd.length > 0) return session.cwd;
510
+ }
511
+ /** 注册侧栏入口与记忆管理面板。 */
512
+ function apply(ctx) {
513
+ ctx.effect(() => ctx.locale.register(NS, {
514
+ zh,
515
+ en
516
+ }), "dsh-auto-memory: dictionaries");
517
+ const t = ctx.locale.bind(NS);
518
+ ctx.slots.inject("main", () => ctx.slots.register({
519
+ name: "main",
520
+ key: PANEL_ID,
521
+ locale: NS,
522
+ inject: () => memoryFace(ctx)
523
+ }, MemoryPage));
524
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
525
+ name: "sidebar.panellist",
526
+ id: PANEL_ID,
527
+ order: 50,
528
+ label: () => t("panel"),
529
+ locale: NS
530
+ }, MemoryPanelIcon));
531
+ }
532
+ //#endregion
533
+ exports.NS = NS;
534
+ exports.PANEL_ID = PANEL_ID;
535
+ exports.apply = apply;
536
+ exports.inject = inject;
537
+ return module.exports;
538
+ }
539
+ });
package/lib/index.js CHANGED
@@ -1233,6 +1233,164 @@ function registerConsolidation(ctx, store, options) {
1233
1233
  }, "dsh-auto-memory consolidation");
1234
1234
  }
1235
1235
  //#endregion
1236
+ //#region src/memory-api.ts
1237
+ const BASE = "/api/auto-memory";
1238
+ /** 列出一个目录组(读文件,畸形/符号链接跳过——与 store.list 同纪律)。 */
1239
+ async function readGroup(dir, scope, slug) {
1240
+ const group = {
1241
+ key: slug,
1242
+ scope,
1243
+ slug,
1244
+ memories: [],
1245
+ indexBytes: 0,
1246
+ indexText: null
1247
+ };
1248
+ let entries;
1249
+ try {
1250
+ entries = await promises.readdir(dir);
1251
+ } catch {
1252
+ return group;
1253
+ }
1254
+ for (const entry of entries) {
1255
+ if (!entry.endsWith(".md")) continue;
1256
+ const file = join(dir, entry);
1257
+ const raw = await promises.readFile(file, "utf8").catch(() => null);
1258
+ if (raw === null) continue;
1259
+ if (entry.toLowerCase() === "MEMORY.md".toLowerCase()) {
1260
+ group.indexBytes = Buffer.byteLength(raw, "utf8");
1261
+ group.indexText = raw.trim().length > 0 ? raw : null;
1262
+ continue;
1263
+ }
1264
+ const record = parseMemory(raw, scope);
1265
+ if (record === null) continue;
1266
+ group.memories.push({
1267
+ name: record.name,
1268
+ ...record.title !== void 0 ? { title: record.title } : {},
1269
+ description: record.description,
1270
+ type: record.type,
1271
+ pinned: record.pinned === true,
1272
+ reads: record.reads ?? 0,
1273
+ ...record.updatedMs !== void 0 ? { updatedMs: record.updatedMs } : {},
1274
+ bytes: Buffer.byteLength(raw, "utf8")
1275
+ });
1276
+ }
1277
+ group.memories.sort((a, b) => a.pinned === b.pinned ? a.name < b.name ? -1 : 1 : a.pinned ? -1 : 1);
1278
+ return group;
1279
+ }
1280
+ /** 扫描记忆根目录的全部组(用户级在前,项目组按目录名序)。 */
1281
+ async function scanGroups(rootDir) {
1282
+ const groups = [await readGroup(join(rootDir, USER_SCOPE_DIR), "user", USER_SCOPE_DIR)];
1283
+ let entries;
1284
+ try {
1285
+ entries = await promises.readdir(rootDir);
1286
+ } catch {
1287
+ return groups;
1288
+ }
1289
+ for (const entry of entries.sort()) {
1290
+ if (!entry.startsWith("--") || !entry.endsWith("--")) continue;
1291
+ if (entry === "_user") continue;
1292
+ groups.push(await readGroup(join(rootDir, entry), "project", entry));
1293
+ }
1294
+ return groups.filter((group) => group.memories.length > 0 || group.indexText !== null);
1295
+ }
1296
+ /** 注册面板 API(宿主半入口;无 connection 服务时 no-op)。 */
1297
+ function registerMemoryApi(ctx, store, rootDir, config) {
1298
+ const connection = ctx.get("connection");
1299
+ if (connection === void 0) return;
1300
+ const json = (data, status = 200) => Promise.resolve(new Response(JSON.stringify(data), {
1301
+ status,
1302
+ headers: {
1303
+ "content-type": "application/json",
1304
+ "cache-control": "no-store"
1305
+ }
1306
+ }));
1307
+ connection.fetch.register({
1308
+ path: `${BASE}/groups.list`,
1309
+ methods: ["GET"],
1310
+ requestBody: "buffered",
1311
+ fetch: async (request) => {
1312
+ const cwd = new URL(request.url).searchParams.get("cwd") ?? void 0;
1313
+ const groups = await scanGroups(rootDir);
1314
+ if (cwd !== void 0 && cwd.length > 0) {
1315
+ const currentSlug = projectKey(cwd);
1316
+ for (const group of groups) if (group.scope === "project" && group.slug === currentSlug) group.current = true;
1317
+ }
1318
+ return json({
1319
+ groups,
1320
+ maxBytes: config.maxBytes
1321
+ });
1322
+ }
1323
+ });
1324
+ connection.fetch.register({
1325
+ path: `${BASE}/memory.read`,
1326
+ methods: ["GET"],
1327
+ requestBody: "buffered",
1328
+ fetch: async (request) => {
1329
+ const url = new URL(request.url);
1330
+ const name = url.searchParams.get("name") ?? "";
1331
+ const scope = url.searchParams.get("scope") ?? "user";
1332
+ const cwd = scope === "project" ? url.searchParams.get("cwd") ?? void 0 : void 0;
1333
+ if (scope === "project" && cwd === void 0) return json({ error: "project scope requires cwd" }, 400);
1334
+ try {
1335
+ const record = await store.read(name, scope, cwd);
1336
+ if (record === null) return json({ error: "not found" }, 404);
1337
+ return json({
1338
+ name: record.name,
1339
+ title: record.title,
1340
+ description: record.description,
1341
+ type: record.type,
1342
+ body: record.body,
1343
+ pinned: record.pinned === true
1344
+ });
1345
+ } catch (error) {
1346
+ return json({ error: String(error) }, 400);
1347
+ }
1348
+ }
1349
+ });
1350
+ connection.fetch.register({
1351
+ path: `${BASE}/memory.write`,
1352
+ methods: ["POST"],
1353
+ requestBody: "buffered",
1354
+ fetch: async (request) => {
1355
+ try {
1356
+ const body = await request.json();
1357
+ const cwd = body.scope === "project" ? body.cwd : void 0;
1358
+ if (body.scope === "project" && (cwd === void 0 || cwd.length === 0)) return json({ error: "project scope requires cwd" }, 400);
1359
+ const name = normalizeName(body.name);
1360
+ await store.write({
1361
+ name,
1362
+ ...body.title !== void 0 && body.title.trim().length > 0 ? { title: body.title.trim() } : {},
1363
+ ...body.pinned !== void 0 ? { pinned: body.pinned } : {},
1364
+ description: body.description,
1365
+ type: body.type,
1366
+ body: body.body
1367
+ }, body.scope, cwd);
1368
+ return json({
1369
+ ok: true,
1370
+ name
1371
+ });
1372
+ } catch (error) {
1373
+ return json({ error: String(error) }, 400);
1374
+ }
1375
+ }
1376
+ });
1377
+ connection.fetch.register({
1378
+ path: `${BASE}/memory.delete`,
1379
+ methods: ["POST"],
1380
+ requestBody: "buffered",
1381
+ fetch: async (request) => {
1382
+ try {
1383
+ const body = await request.json();
1384
+ const cwd = body.scope === "project" ? body.cwd : void 0;
1385
+ if (body.scope === "project" && (cwd === void 0 || cwd.length === 0)) return json({ error: "project scope requires cwd" }, 400);
1386
+ return await store.delete(body.name, body.scope, cwd) ? json({ ok: true }) : json({ error: "not found" }, 404);
1387
+ } catch (error) {
1388
+ return json({ error: String(error) }, 400);
1389
+ }
1390
+ }
1391
+ });
1392
+ }
1393
+ //#endregion
1236
1394
  //#region src/index.ts
1237
1395
  /**
1238
1396
  * dsh-auto-memory — 把 Claude Code 的 auto-memory 机制移植为 DeepSeek Harness 原生插件。
@@ -1255,8 +1413,12 @@ const Config = z.object({
1255
1413
  staleAfterDays: z.number().default(0)
1256
1414
  });
1257
1415
  function apply(ctx, config) {
1258
- const store = new MemoryStore(config.memoryDir !== void 0 && config.memoryDir.length > 0 ? resolve(config.memoryDir) : join(resolveDshHome(), "memory"), { staleAfterDays: config.staleAfterDays });
1416
+ const rootDir = config.memoryDir !== void 0 && config.memoryDir.length > 0 ? resolve(config.memoryDir) : join(resolveDshHome(), "memory");
1417
+ const store = new MemoryStore(rootDir, { staleAfterDays: config.staleAfterDays });
1259
1418
  registerMemoryTools(ctx, store, config.enableUserScope);
1419
+ ctx.inject(["connection"], () => {
1420
+ registerMemoryApi(ctx, store, rootDir, { maxBytes: config.maxBytes });
1421
+ });
1260
1422
  registerConsolidation(ctx, store, {
1261
1423
  autoSummarize: config.autoSummarize,
1262
1424
  autoSummarizeMaxMemories: config.autoSummarizeMaxMemories,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-memory",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code style auto-memory plugin for DeepSeek Harness: typed memory files + MEMORY.md index auto-injected into the system prompt",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,6 +11,9 @@
11
11
  "types": "./lib/index.d.ts",
12
12
  "default": "./lib/index.js"
13
13
  },
14
+ "./client": {
15
+ "default": "./lib/client.js"
16
+ },
14
17
  "./package.json": "./package.json"
15
18
  },
16
19
  "files": [
@@ -37,7 +40,17 @@
37
40
  "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2 <0.2.0"
38
41
  },
39
42
  "devDependencies": {
43
+ "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
44
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
45
+ "@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
46
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2",
47
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
48
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
49
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.2",
50
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
40
51
  "@types/node": "^24.0.0",
52
+ "@types/react": "^19.3.0",
53
+ "react": "^19.3.0",
41
54
  "tsdown": "^0.22.2",
42
55
  "typescript": "^5.9.0",
43
56
  "vitest": "^4.1.8"
@@ -49,6 +62,14 @@
49
62
  "manifestVersion": 1,
50
63
  "bundle": {
51
64
  "patch": "./cordis.patch.yml"
65
+ },
66
+ "client": {
67
+ "platform": "web",
68
+ "inject": [
69
+ "@deepseek-ai/dsh-client-locale",
70
+ "@deepseek-ai/dsh-client-ui-layout",
71
+ "@deepseek-ai/dsh-client-ui-sidebar"
72
+ ]
52
73
  }
53
74
  },
54
75
  "keywords": [