dsh-continual-evolve 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/README.zh.md +7 -6
- package/client/client.js +183 -37
- package/lib/card-routes.d.ts +2 -2
- package/lib/card-routes.js +21 -13
- package/lib/index.d.ts +7 -6
- package/lib/index.js +41 -9
- package/lib/workspace-catalog.d.ts +30 -0
- package/lib/workspace-catalog.js +149 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -23,7 +23,7 @@ Reusable experience gathered in one session (user preferences, pitfalls, project
|
|
|
23
23
|
1. **Session-start injection** — the plugin registers a single system-prompt section: the `MEMORY.md` index content (whole-line truncated over budget, with a read-the-directory hint), the absolute store path, and the when_to_save guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
|
|
24
24
|
2. **Native read/write** — DSH fully permits file access inside the workspace (reads are never fenced; the write fence only covers paths outside the workspace, see [`docs/FAQ.md`](docs/FAQ.md) #6). The model reads, writes, and edits memory files and maintains the index directly; the store is bootstrapped on first use.
|
|
25
25
|
3. **Governance is the file** — no versions, no snapshots, no approvals, no background extraction: a bad memory is a visible file in the workspace, and deleting it is the retirement path. Memories are personal context — on bootstrap in a git workspace the store appends `.evolve/` to the workspace `.gitignore` (non-git workspaces are left untouched).
|
|
26
|
-
4. **Read-only card** — the plugin mounts a read-only memory card on its own page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card (collapsed by default, header toggle with a rotating chevron, hover/open frame states) and aligned with ZCode's Settings → Memory viewer: one workspace at a time
|
|
26
|
+
4. **Read-only card** — the plugin mounts a read-only memory card on its own page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card (collapsed by default, header toggle with a rotating chevron, hover/open frame states) and aligned with ZCode's Settings → Memory viewer: one workspace at a time behind a dropdown scope selector fed by the host workspace registry, a file search box, per-file relative updated times, click-to-preview file bodies (5 MiB cap), plus index-drift warnings (unindexed files, index rows whose file is gone). The plugin's display name and description localize through the host's package `locale/*.json` metadata channel. The card follows the market's look — host theme tokens, the `Button` primitive for refresh, and zh/en copy that tracks the DSH language (plain themed elements on old hosts). The card is a request-time projection of the files on disk — the single source of truth remains `.evolve/memory/`; the card has no edit path, edit the files directly.
|
|
27
27
|
|
|
28
28
|
Memory file format (same as ZCode): frontmatter with `name` / `description` (the hook that decides whether a future session recalls it) / `metadata.type` (`user | feedback | project | reference`); `feedback` bodies must carry **Why:** and **How to apply:** lines.
|
|
29
29
|
|
|
@@ -66,9 +66,9 @@ Profile patch example:
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
69
|
-
pnpm test # vitest (
|
|
69
|
+
pnpm test # vitest (91 tests)
|
|
70
70
|
pnpm test:coverage # v8 coverage, CI-enforced thresholds
|
|
71
|
-
pnpm lint # oxlint src test
|
|
71
|
+
pnpm lint # oxlint src test client
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Layout:
|
|
@@ -79,12 +79,13 @@ Layout:
|
|
|
79
79
|
│ ├── memory-section.ts # injection: read index / bootstrap / truncate / session freeze
|
|
80
80
|
│ ├── memory-guide.ts # guide: when_to_save and upkeep (adapted from ZCode)
|
|
81
81
|
│ ├── memory-snapshot.ts # card: read-only store projection (entries / drift / error degrade)
|
|
82
|
-
│ ├──
|
|
82
|
+
│ ├── workspace-catalog.ts# card: workspace list from the host registry (the API read fence)
|
|
83
|
+
│ ├── known-workspaces.ts# card: in-process LRU fallback source for the catalogue
|
|
83
84
|
│ └── card-routes.ts # card: GET-only read API on the host webServer
|
|
84
85
|
├── client/
|
|
85
86
|
│ └── client.js # hand-written client bundle: read-only card in the plugin manager
|
|
86
87
|
├── locale/ # host package metadata: localized display name + description (zh/en)
|
|
87
|
-
├── test/ # vitest suite (
|
|
88
|
+
├── test/ # vitest suite (8 files)
|
|
88
89
|
├── lib/ # build output (tsc)
|
|
89
90
|
├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
|
|
90
91
|
└── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -23,7 +23,7 @@ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项
|
|
|
23
23
|
1. **开局注入** —— 插件只注册一个 system-prompt section:注入 `MEMORY.md` 索引正文(超预算按整行截断并提示读目录)、记忆目录绝对路径、when_to_save 指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
|
|
24
24
|
2. **原生读写** —— DSH 对工作区内文件读写全放行(读路径从不设防,写围栏只围工作区外,见 [`docs/FAQ.md`](docs/FAQ.md) #6)。模型直接 Read/Write/Edit 记忆文件并维护索引;store 不存在时自动 bootstrap。
|
|
25
25
|
3. **治理即文件** —— 无版本、无快照、无审批、无后台提取:一条坏记忆就是工作区里一个可见的文件,删掉它就是退役。记忆是个人上下文——bootstrap 时若工作区是 git 仓库会自动把 `.evolve/` 追加进 `.gitignore`(非 git 工作区不碰任何文件)。
|
|
26
|
-
4. **只读卡片** —— 插件在官方插件管理的本 bundle 页面挂一张只读记忆卡片(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡(默认收起、头部整行可点 + 旋转 chevron、hover/展开框态),内容对齐 ZCode
|
|
26
|
+
4. **只读卡片** —— 插件在官方插件管理的本 bundle 页面挂一张只读记忆卡片(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡(默认收起、头部整行可点 + 旋转 chevron、hover/展开框态),内容对齐 ZCode 设置的记忆选项卡——工作区一次选一个(下拉作用域选择器,目录取宿主工作区注册表)、文件搜索框、每个文件的相对更新时间、点击行内展开文件内容预览(5 MiB 上限),外加索引漂移警告(未索引文件与索引失联文件)。插件的显示名与描述走宿主包元数据 `locale/*.json` 通道双语切换。卡片与市场同款——宿主主题 token、`Button` 原语刷新按钮、中英文案跟随 DSH 语言(老宿主降级为同主题裸元素)。卡片是磁盘文件的请求时投影——单一事实源永远是 `.evolve/memory/` 里的文件,卡片没有编辑路径,编辑请直接改文件。
|
|
27
27
|
|
|
28
28
|
记忆文件格式(与 ZCode 一致):frontmatter 带 `name` / `description`(决定未来会话会不会想起它)/ `metadata.type`(`user | feedback | project | reference`);`feedback` 正文必带 **Why:** 与 **How to apply:**。
|
|
29
29
|
|
|
@@ -66,9 +66,9 @@ profile patch 示例:
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
69
|
-
pnpm test # vitest(
|
|
69
|
+
pnpm test # vitest(91 例)
|
|
70
70
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
71
|
-
pnpm lint # oxlint src test
|
|
71
|
+
pnpm lint # oxlint src test client
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
目录结构:
|
|
@@ -79,12 +79,13 @@ pnpm lint # oxlint src test
|
|
|
79
79
|
│ ├── memory-section.ts # 注入:读索引 / bootstrap / 截断 / 会话冻结
|
|
80
80
|
│ ├── memory-guide.ts # 指南:when_to_save 与维护纪律(改编自 ZCode)
|
|
81
81
|
│ ├── memory-snapshot.ts # 卡片:记忆库只读投影(条目 / 漂移 / 错误降级)
|
|
82
|
-
│ ├──
|
|
82
|
+
│ ├── workspace-catalog.ts# 卡片:工作区目录(宿主注册表;API 读围栏)
|
|
83
|
+
│ ├── known-workspaces.ts# 卡片:目录的进程内 LRU 回退源
|
|
83
84
|
│ └── card-routes.ts # 卡片:webServer 上的 GET-only 只读 API
|
|
84
85
|
├── client/
|
|
85
86
|
│ └── client.js # 手写 client bundle:官方插件管理内只读卡片
|
|
86
87
|
├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
|
|
87
|
-
├── test/ # vitest 测试(
|
|
88
|
+
├── test/ # vitest 测试(8 个文件)
|
|
88
89
|
├── lib/ # 构建产物(tsc)
|
|
89
90
|
├── docs/ # design.md(设计)· FAQ.md(踩坑)
|
|
90
91
|
└── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
|
package/client/client.js
CHANGED
|
@@ -25,13 +25,15 @@
|
|
|
25
25
|
* reads as another product). The stylesheet is injected once, like the
|
|
26
26
|
* host's own CSS-module tags.
|
|
27
27
|
*
|
|
28
|
-
* Content (v0.14): aligned with ZCode's Settings → Memory
|
|
29
|
-
* .evolve store shape was itself aligned with — one workspace
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* Content (v0.14, scope selector v0.15): aligned with ZCode's Settings → Memory
|
|
29
|
+
* viewer, which the .evolve store shape was itself aligned with — one workspace
|
|
30
|
+
* selected at a time behind a dropdown scope selector (workspaces come from the
|
|
31
|
+
* host registry, so the selector lists the same workspaces the workspace picker
|
|
32
|
+
* does), a file search box, per-file relative updated times, and a
|
|
33
|
+
* click-to-preview body served by the host's content endpoint (5 MiB cap,
|
|
34
|
+
* deleted/changed guards). The drift warnings (dangling index rows / unindexed
|
|
35
|
+
* files) stay: they are this plugin's OBSERVATION surface and a deliberate
|
|
36
|
+
* superset of ZCode's viewer.
|
|
35
37
|
* Locale/theme/primitives wiring unchanged from v0.13: `ctx.locale.register`
|
|
36
38
|
* + `bind`, `var(--dsw-alias-*)` tokens, primitives with plain-element
|
|
37
39
|
* fallbacks so an old host never blanks the settings dialog. Read-only
|
|
@@ -87,11 +89,12 @@ window.__ModuleLoader__.load({
|
|
|
87
89
|
hint: "事实源是各工作区 .evolve/memory/ 下的 markdown 文件——模型原生读写;本卡片只展示,编辑请直接改文件。",
|
|
88
90
|
loading: "读取中…",
|
|
89
91
|
loadFailed: "卡片数据加载失败:",
|
|
90
|
-
empty: "
|
|
92
|
+
empty: "还没有任何工作区有记忆库——在某个工作区开一次会话后回来刷新。",
|
|
91
93
|
absent: "该工作区尚无记忆库(开一次会话自动创建)。",
|
|
92
94
|
readFailed: "记忆库读取失败:",
|
|
93
95
|
stats: "{files} 条记忆 · 索引 {index} 行",
|
|
94
96
|
filesHeading: "文件",
|
|
97
|
+
scopeLabel: "选择工作区",
|
|
95
98
|
searchPlaceholder: "搜索记忆文件…",
|
|
96
99
|
searchEmpty: "没有匹配的记忆文件。",
|
|
97
100
|
missing: "索引失联(索引引用但文件已删):",
|
|
@@ -114,11 +117,12 @@ window.__ModuleLoader__.load({
|
|
|
114
117
|
hint: "The source of truth is the markdown files under each workspace's .evolve/memory/ — read and written by the model; this card only displays them, edit the files directly.",
|
|
115
118
|
loading: "Loading…",
|
|
116
119
|
loadFailed: "Failed to load card data: ",
|
|
117
|
-
empty: "
|
|
120
|
+
empty: "No workspace has a memory store yet — open a session in a workspace, then come back and refresh.",
|
|
118
121
|
absent: "No memory store in this workspace yet (one is created when you open a session).",
|
|
119
122
|
readFailed: "Failed to read the memory store: ",
|
|
120
123
|
stats: "{files} memories · {index} index rows",
|
|
121
124
|
filesHeading: "Files",
|
|
125
|
+
scopeLabel: "Choose workspace",
|
|
122
126
|
searchPlaceholder: "Search memory files…",
|
|
123
127
|
searchEmpty: "No matching memory files.",
|
|
124
128
|
missing: "Dangling index (indexed but deleted): ",
|
|
@@ -178,7 +182,7 @@ window.__ModuleLoader__.load({
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
/**
|
|
181
|
-
* Assemble the card's view model: the workspace
|
|
185
|
+
* Assemble the card's view model: the workspace catalogue, then one
|
|
182
186
|
* snapshot per workspace. Network or API failure per workspace degrades
|
|
183
187
|
* to an `error` string on that workspace's entry — one bad store never
|
|
184
188
|
* blanks the whole card.
|
|
@@ -196,17 +200,20 @@ window.__ModuleLoader__.load({
|
|
|
196
200
|
var entries = await Promise.all(
|
|
197
201
|
workspaces.map(async function (workspace) {
|
|
198
202
|
var root = typeof workspace.root === "string" ? workspace.root : "";
|
|
199
|
-
var
|
|
203
|
+
var label =
|
|
204
|
+
typeof workspace.label === "string" && workspace.label.trim().length > 0
|
|
205
|
+
? workspace.label
|
|
206
|
+
: workspaceLabel(root);
|
|
200
207
|
try {
|
|
201
208
|
var snapshotResponse = await fetchImpl(API_ROOT + "/memory?root=" + encodeURIComponent(root));
|
|
202
209
|
if (!snapshotResponse.ok) {
|
|
203
|
-
return { root: root,
|
|
210
|
+
return { root: root, label: label, snapshot: null, error: "HTTP " + snapshotResponse.status };
|
|
204
211
|
}
|
|
205
|
-
return { root: root,
|
|
212
|
+
return { root: root, label: label, snapshot: await snapshotResponse.json(), error: null };
|
|
206
213
|
} catch (error) {
|
|
207
214
|
return {
|
|
208
215
|
root: root,
|
|
209
|
-
|
|
216
|
+
label: label,
|
|
210
217
|
snapshot: null,
|
|
211
218
|
error: error instanceof Error ? error.message : String(error),
|
|
212
219
|
};
|
|
@@ -290,9 +297,18 @@ window.__ModuleLoader__.load({
|
|
|
290
297
|
".dce-body{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);margin:0 16px;padding:12px 0 14px;display:flex;flex-direction:column;gap:10px}",
|
|
291
298
|
".dce-hint{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}",
|
|
292
299
|
".dce-empty{border:1px dashed var(--dsw-alias-border-l3,#d1d9e0);border-radius:10px;padding:16px;text-align:center;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:13px;line-height:20px}",
|
|
293
|
-
".dce-
|
|
294
|
-
".dce-
|
|
295
|
-
|
|
300
|
+
".dce-scope-wrap{position:relative;display:inline-flex;min-width:0;max-width:100%;align-self:flex-start}",
|
|
301
|
+
".dce-scope{display:inline-flex;align-items:center;gap:8px;max-width:100%;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);font:inherit;font-size:12px;line-height:18px;cursor:pointer}",
|
|
302
|
+
".dce-scope:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}",
|
|
303
|
+
".dce-scope:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:1px}",
|
|
304
|
+
".dce-scope-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
305
|
+
".dce-scope-caret{display:inline-flex;color:var(--dsw-alias-label-tertiary,#8b93a1)}",
|
|
306
|
+
".dce-menu{position:absolute;top:calc(100% + 6px);left:0;z-index:20;min-width:240px;max-width:min(420px,80vw);max-height:320px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 8px 24px rgba(0,0,0,.12)}",
|
|
307
|
+
".dce-menu-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:6px 10px;border:0;border-radius:8px;background:0 0;font:inherit;color:inherit;text-align:left;cursor:pointer}",
|
|
308
|
+
".dce-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.03))}",
|
|
309
|
+
'.dce-menu-item[data-active="true"]{background:var(--dsw-alias-bg-module-platform,#eef1f4)}',
|
|
310
|
+
".dce-menu-label{color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
311
|
+
".dce-menu-path{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
296
312
|
".dce-search{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l4,#d8dee4);border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-layer-1,#fff);height:32px;color:var(--dsw-alias-label-primary,#1f2328);font:inherit;font-size:12px;padding:0 10px;outline:none}",
|
|
297
313
|
".dce-search::placeholder{color:var(--dsw-alias-label-tertiary,#8b93a1)}",
|
|
298
314
|
".dce-search:focus-visible{border-color:var(--dsw-alias-brand-primary,#4f6ef7)}",
|
|
@@ -393,47 +409,176 @@ window.__ModuleLoader__.load({
|
|
|
393
409
|
return createElement("li", { className: "dce-item" }, children);
|
|
394
410
|
}
|
|
395
411
|
|
|
396
|
-
/**
|
|
397
|
-
function
|
|
398
|
-
|
|
412
|
+
/** Leading "switch scope" marker from the host primitives, or nothing. */
|
|
413
|
+
function ScopeMark() {
|
|
414
|
+
if (
|
|
415
|
+
primitives !== undefined &&
|
|
416
|
+
primitives !== null &&
|
|
417
|
+
typeof primitives.IconChevronsUpDownOutlineRegular !== "undefined" &&
|
|
418
|
+
primitives.IconChevronsUpDownOutlineRegular !== null
|
|
419
|
+
) {
|
|
420
|
+
return createElement(primitives.IconChevronsUpDownOutlineRegular, { size: 14, "aria-hidden": "true" });
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The workspace scope selector, ZCode's Settings → Memory shape: one
|
|
427
|
+
* scope at a time behind a dropdown (leading marker + current workspace
|
|
428
|
+
* label + chevron), never a row of chips. A chip row can only render
|
|
429
|
+
* when several workspaces are known, so in the common single-workspace
|
|
430
|
+
* case it renders nothing at all and the capability reads as missing.
|
|
431
|
+
* The menu closes on select, on Escape and on a click outside.
|
|
432
|
+
*/
|
|
433
|
+
function WorkspaceScopeMenu(props) {
|
|
434
|
+
var workspaces = props.workspaces;
|
|
399
435
|
var selected = props.selected;
|
|
400
436
|
var onSelect = props.onSelect;
|
|
401
437
|
var t = props.t;
|
|
402
438
|
var UI = props.UI;
|
|
403
|
-
var
|
|
404
|
-
var
|
|
405
|
-
var
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
439
|
+
var openState = useState(false);
|
|
440
|
+
var open = openState[0];
|
|
441
|
+
var setOpen = openState[1];
|
|
442
|
+
useEffect(
|
|
443
|
+
function () {
|
|
444
|
+
if (!open || typeof document === "undefined") {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
var close = function () {
|
|
448
|
+
setOpen(false);
|
|
449
|
+
};
|
|
450
|
+
var onKeyDown = function (event) {
|
|
451
|
+
if (event.key === "Escape") {
|
|
452
|
+
close();
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
document.addEventListener("click", close);
|
|
456
|
+
document.addEventListener("keydown", onKeyDown);
|
|
457
|
+
return function () {
|
|
458
|
+
document.removeEventListener("click", close);
|
|
459
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
[open],
|
|
463
|
+
);
|
|
464
|
+
var current = null;
|
|
465
|
+
for (var index = 0; index < workspaces.length; index++) {
|
|
466
|
+
if (workspaces[index].root === selected) {
|
|
467
|
+
current = workspaces[index];
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (current === null && workspaces.length > 0) {
|
|
472
|
+
current = workspaces[0];
|
|
473
|
+
}
|
|
474
|
+
var label = current === null ? "" : current.label || workspaceLabel(current.root);
|
|
475
|
+
var scopeChildren = [
|
|
476
|
+
createElement(ScopeMark, { key: "mark" }),
|
|
477
|
+
createElement("span", { key: "label", className: "dce-scope-label" }, label),
|
|
478
|
+
createElement("span", { key: "caret", className: "dce-scope-caret" }, createElement(Chevron, null)),
|
|
479
|
+
];
|
|
480
|
+
var triggerProps = {
|
|
481
|
+
type: "button",
|
|
482
|
+
"aria-haspopup": "menu",
|
|
483
|
+
"aria-expanded": open ? "true" : "false",
|
|
484
|
+
"aria-label": t("scopeLabel"),
|
|
485
|
+
onClick: function () {
|
|
486
|
+
setOpen(!open);
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
var trigger = null;
|
|
490
|
+
if (UI !== null && UI !== undefined && typeof UI.Button !== "undefined" && UI.Button !== null) {
|
|
491
|
+
trigger = createElement(
|
|
492
|
+
UI.Button,
|
|
493
|
+
{
|
|
494
|
+
key: "trigger",
|
|
495
|
+
variant: "outline",
|
|
496
|
+
size: "sm",
|
|
497
|
+
type: triggerProps.type,
|
|
498
|
+
"aria-haspopup": triggerProps["aria-haspopup"],
|
|
499
|
+
"aria-expanded": triggerProps["aria-expanded"],
|
|
500
|
+
"aria-label": triggerProps["aria-label"],
|
|
501
|
+
onClick: triggerProps.onClick,
|
|
502
|
+
},
|
|
503
|
+
scopeChildren,
|
|
504
|
+
);
|
|
505
|
+
} else {
|
|
506
|
+
trigger = createElement(
|
|
507
|
+
"button",
|
|
508
|
+
{
|
|
509
|
+
key: "trigger",
|
|
510
|
+
type: triggerProps.type,
|
|
511
|
+
className: "dce-scope",
|
|
512
|
+
"aria-haspopup": triggerProps["aria-haspopup"],
|
|
513
|
+
"aria-expanded": triggerProps["aria-expanded"],
|
|
514
|
+
"aria-label": triggerProps["aria-label"],
|
|
515
|
+
onClick: triggerProps.onClick,
|
|
516
|
+
},
|
|
517
|
+
scopeChildren,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
var menu = null;
|
|
521
|
+
if (open) {
|
|
522
|
+
menu = createElement(
|
|
413
523
|
"div",
|
|
414
|
-
{ key: "
|
|
415
|
-
|
|
524
|
+
{ key: "menu", className: "dce-menu", role: "menu", "aria-label": t("scopeLabel") },
|
|
525
|
+
workspaces.map(function (workspace) {
|
|
416
526
|
return createElement(
|
|
417
527
|
"button",
|
|
418
528
|
{
|
|
419
529
|
key: workspace.root,
|
|
420
530
|
type: "button",
|
|
421
|
-
|
|
531
|
+
role: "menuitemradio",
|
|
532
|
+
"aria-checked": workspace.root === selected ? "true" : "false",
|
|
533
|
+
className: "dce-menu-item",
|
|
422
534
|
"data-active": workspace.root === selected ? "true" : "false",
|
|
423
535
|
onClick: function () {
|
|
424
|
-
|
|
536
|
+
setOpen(false);
|
|
537
|
+
if (workspace.root !== selected) {
|
|
538
|
+
onSelect(workspace.root);
|
|
539
|
+
}
|
|
425
540
|
},
|
|
426
541
|
},
|
|
427
|
-
|
|
542
|
+
// The registry allows duplicate titles, so the root
|
|
543
|
+
// disambiguates two workspaces that read the same.
|
|
544
|
+
createElement("span", { key: "label", className: "dce-menu-label" }, workspace.label || workspaceLabel(workspace.root)),
|
|
545
|
+
createElement("span", { key: "root", className: "dce-menu-path" }, workspace.root),
|
|
428
546
|
);
|
|
429
547
|
}),
|
|
430
548
|
);
|
|
431
549
|
}
|
|
550
|
+
return createElement("div", { key: "scope", className: "dce-scope-wrap" }, trigger, menu);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** One workspace's memory store: stats, drift warnings, searchable file list with previews. */
|
|
554
|
+
function WorkspaceBlock(props) {
|
|
555
|
+
var entry = props.entry;
|
|
556
|
+
var selected = props.selected;
|
|
557
|
+
var onSelect = props.onSelect;
|
|
558
|
+
var t = props.t;
|
|
559
|
+
var UI = props.UI;
|
|
560
|
+
var now = props.now;
|
|
561
|
+
var expandedFile = props.expandedFile;
|
|
562
|
+
var preview = props.preview;
|
|
563
|
+
var onToggleFile = props.onToggleFile;
|
|
564
|
+
var onRefresh = props.onRefresh;
|
|
565
|
+
var query = props.query;
|
|
566
|
+
var onQueryChange = props.onQueryChange;
|
|
567
|
+
var scope = createElement(WorkspaceScopeMenu, {
|
|
568
|
+
key: "scope",
|
|
569
|
+
workspaces: entry.workspaces.map(function (workspace) {
|
|
570
|
+
return { root: workspace.root, label: workspace.label };
|
|
571
|
+
}),
|
|
572
|
+
selected: selected,
|
|
573
|
+
onSelect: onSelect,
|
|
574
|
+
t: t,
|
|
575
|
+
UI: UI,
|
|
576
|
+
});
|
|
432
577
|
var active = entry.workspaces.find(function (workspace) {
|
|
433
578
|
return workspace.root === selected;
|
|
434
579
|
});
|
|
435
580
|
if (active === undefined) {
|
|
436
|
-
return createElement("div", { key: "block" },
|
|
581
|
+
return createElement("div", { key: "block", className: "dce-workspace" }, scope);
|
|
437
582
|
}
|
|
438
583
|
var body = null;
|
|
439
584
|
if (active.error !== null && active.error !== undefined) {
|
|
@@ -536,7 +681,7 @@ window.__ModuleLoader__.load({
|
|
|
536
681
|
}
|
|
537
682
|
body = parts;
|
|
538
683
|
}
|
|
539
|
-
return createElement("div", { key: "block", className: "dce-workspace" },
|
|
684
|
+
return createElement("div", { key: "block", className: "dce-workspace" }, scope, body);
|
|
540
685
|
}
|
|
541
686
|
|
|
542
687
|
/**
|
|
@@ -651,6 +796,7 @@ window.__ModuleLoader__.load({
|
|
|
651
796
|
setSelection({ root: root, query: "", expandedFile: null });
|
|
652
797
|
setPreview({ file: null, status: "loading", content: "", message: "" });
|
|
653
798
|
},
|
|
799
|
+
onRefresh: refresh,
|
|
654
800
|
t: t,
|
|
655
801
|
UI: UI,
|
|
656
802
|
now: now,
|
package/lib/card-routes.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkspaceCatalog } from "./workspace-catalog.js";
|
|
2
2
|
/** URL prefix of the card API (kind-exact routes registered under it). */
|
|
3
3
|
export declare const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
|
|
4
4
|
export declare const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
|
|
@@ -29,5 +29,5 @@ export interface CardWebServer {
|
|
|
29
29
|
* shape the host hands back (or none, as on some host versions), the
|
|
30
30
|
* disposer calls only the functions among them.
|
|
31
31
|
*/
|
|
32
|
-
export declare function mountCardRoutes(webServer: CardWebServer,
|
|
32
|
+
export declare function mountCardRoutes(webServer: CardWebServer, catalog: WorkspaceCatalog): () => void;
|
|
33
33
|
//# sourceMappingURL=card-routes.d.ts.map
|
package/lib/card-routes.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mounted through the host's `webServer` service (same registration shape
|
|
5
5
|
* dshmarket uses: `kind: "exact"` routes on the profile's web server), the
|
|
6
|
-
* three endpoints are GET-only projections: the workspace
|
|
6
|
+
* three endpoints are GET-only projections: the workspace catalogue, the
|
|
7
7
|
* memory snapshot for a known root, and one memory file's body for the card's
|
|
8
|
-
* content preview. The
|
|
9
|
-
* resolve their `root` parameter only against
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* content preview. The catalogue IS the security fence — the memory endpoints
|
|
9
|
+
* resolve their `root` parameter only against workspaces the host's registry
|
|
10
|
+
* (or, on hosts without one, a root this process has served) knows about, and
|
|
11
|
+
* the file endpoint additionally accepts only plain .md entries of that
|
|
12
|
+
* workspace's memory directory, so the API cannot read arbitrary paths.
|
|
13
|
+
* Handlers are synchronous and never throw past the response.
|
|
13
14
|
*/
|
|
14
15
|
import { resolve } from "node:path";
|
|
15
16
|
import { memoryFileContent, memorySnapshot } from "./memory-snapshot.js";
|
|
@@ -30,7 +31,7 @@ function isGet(request) {
|
|
|
30
31
|
* shape the host hands back (or none, as on some host versions), the
|
|
31
32
|
* disposer calls only the functions among them.
|
|
32
33
|
*/
|
|
33
|
-
export function mountCardRoutes(webServer,
|
|
34
|
+
export function mountCardRoutes(webServer, catalog) {
|
|
34
35
|
const disposers = [];
|
|
35
36
|
disposers.push(webServer.register({
|
|
36
37
|
kind: "exact",
|
|
@@ -41,7 +42,9 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
41
42
|
response.end();
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
// The listed set is the display projection (workspaces with a
|
|
46
|
+
// memory directory); the read fence is `known` below.
|
|
47
|
+
sendJson(response, 200, { workspaces: catalog.list() });
|
|
45
48
|
},
|
|
46
49
|
}));
|
|
47
50
|
disposers.push(webServer.register({
|
|
@@ -58,10 +61,10 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
58
61
|
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
61
|
-
const known =
|
|
64
|
+
const known = knownEntryOf(catalog, root);
|
|
62
65
|
if (known === undefined) {
|
|
63
|
-
// The
|
|
64
|
-
//
|
|
66
|
+
// The catalogue answers 404 without distinguishing "not a
|
|
67
|
+
// workspace here" from "wrong path": no filesystem oracle.
|
|
65
68
|
sendJson(response, 404, { error: "unknown workspace root" });
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
@@ -87,10 +90,10 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
87
90
|
sendJson(response, 400, { error: "file query parameter is required" });
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
90
|
-
const known =
|
|
93
|
+
const known = knownEntryOf(catalog, root);
|
|
91
94
|
if (known === undefined) {
|
|
92
95
|
// Same fence as the snapshot route: 404 without distinguishing
|
|
93
|
-
// "
|
|
96
|
+
// "not a workspace here" from "wrong path".
|
|
94
97
|
sendJson(response, 404, { error: "unknown workspace root" });
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
@@ -126,6 +129,11 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
126
129
|
}
|
|
127
130
|
};
|
|
128
131
|
}
|
|
132
|
+
/** The catalogued workspace a `root` query parameter resolves to, if any. */
|
|
133
|
+
function knownEntryOf(catalog, root) {
|
|
134
|
+
const target = resolve(root);
|
|
135
|
+
return catalog.known().find((workspace) => workspace.root === target);
|
|
136
|
+
}
|
|
129
137
|
/** The decoded `root` query parameter, or null when absent/relative. */
|
|
130
138
|
function rootParamOf(request) {
|
|
131
139
|
try {
|
package/lib/index.d.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* background extraction, and no governance. See
|
|
10
10
|
* .agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md.
|
|
11
11
|
*
|
|
12
|
-
* The 2026-10-04 plugin-management card adds a read-only surface: the
|
|
13
|
-
* workspace
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md
|
|
12
|
+
* The 2026-10-04 plugin-management card adds a read-only surface: the host's
|
|
13
|
+
* workspace registry feeds a catalogue, and a `webServer` route triple projects
|
|
14
|
+
* any catalogued workspace's store for the card in the official plugin manager
|
|
15
|
+
* (`plugins.bundle.config`, client bundle in client/client.js). The card is a
|
|
16
|
+
* viewer — see
|
|
17
|
+
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md and
|
|
18
|
+
* .agents/notes/implemented/feature/2026-10-05-zcode-workspace-switcher.md.
|
|
18
19
|
*
|
|
19
20
|
* The host API surface is declared locally (the minimal shape this plugin
|
|
20
21
|
* touches) instead of importing the DSH type packages — the section call is
|
package/lib/index.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* background extraction, and no governance. See
|
|
10
10
|
* .agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md.
|
|
11
11
|
*
|
|
12
|
-
* The 2026-10-04 plugin-management card adds a read-only surface: the
|
|
13
|
-
* workspace
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md
|
|
12
|
+
* The 2026-10-04 plugin-management card adds a read-only surface: the host's
|
|
13
|
+
* workspace registry feeds a catalogue, and a `webServer` route triple projects
|
|
14
|
+
* any catalogued workspace's store for the card in the official plugin manager
|
|
15
|
+
* (`plugins.bundle.config`, client bundle in client/client.js). The card is a
|
|
16
|
+
* viewer — see
|
|
17
|
+
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md and
|
|
18
|
+
* .agents/notes/implemented/feature/2026-10-05-zcode-workspace-switcher.md.
|
|
18
19
|
*
|
|
19
20
|
* The host API surface is declared locally (the minimal shape this plugin
|
|
20
21
|
* touches) instead of importing the DSH type packages — the section call is
|
|
@@ -24,6 +25,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
24
25
|
import { mountCardRoutes } from "./card-routes.js";
|
|
25
26
|
import { createKnownWorkspaces } from "./known-workspaces.js";
|
|
26
27
|
import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
|
|
28
|
+
import { createWorkspaceCatalog } from "./workspace-catalog.js";
|
|
27
29
|
export const name = "continual-evolve";
|
|
28
30
|
export const inject = ["systemPrompt"];
|
|
29
31
|
export const Config = z.object({
|
|
@@ -53,9 +55,12 @@ export const Config = z.object({
|
|
|
53
55
|
/**
|
|
54
56
|
* Mount the card routes on the host web server through a nested inject, so
|
|
55
57
|
* the plugin stays mountable on hosts without that service. Called only when
|
|
56
|
-
* the card is enabled.
|
|
58
|
+
* the card is enabled. The workspace registry is looked up inside the scoped
|
|
59
|
+
* context (nothing else injects it: a host without the service must still get
|
|
60
|
+
* the card) and read per request, so a workspace created while the card is
|
|
61
|
+
* open appears on the next refresh.
|
|
57
62
|
*/
|
|
58
|
-
function mountCardWhenAvailable(host,
|
|
63
|
+
function mountCardWhenAvailable(host, served) {
|
|
59
64
|
const nested = host;
|
|
60
65
|
if (typeof nested.inject !== "function") {
|
|
61
66
|
host.logger("continual-evolve").warn("host context exposes no inject — memory card routes not mounted (memory section unaffected)");
|
|
@@ -67,9 +72,36 @@ function mountCardWhenAvailable(host, workspaces) {
|
|
|
67
72
|
host.logger("continual-evolve").warn("webServer service present but empty — memory card routes not mounted (memory section unaffected)");
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
|
|
75
|
+
const catalog = createWorkspaceCatalog({
|
|
76
|
+
registry: registryLookup(scoped),
|
|
77
|
+
served: served,
|
|
78
|
+
homeDir: process.env.DSH_HOME,
|
|
79
|
+
});
|
|
80
|
+
mountCardRoutes(webServer, catalog);
|
|
71
81
|
});
|
|
72
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* A live `ctx.workspaceRegistry` getter over the scoped context. The service
|
|
85
|
+
* is a sibling, so it is reached with `ctx.get` (property access only walks
|
|
86
|
+
* fiber ancestors) and treated as optional: an absent, foreign or throwing
|
|
87
|
+
* service degrades the catalogue to its fallback sources instead of taking the
|
|
88
|
+
* card down.
|
|
89
|
+
*/
|
|
90
|
+
function registryLookup(scoped) {
|
|
91
|
+
const context = scoped;
|
|
92
|
+
return () => {
|
|
93
|
+
if (typeof context.get !== "function") {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const service = context.get("workspaceRegistry");
|
|
98
|
+
return service === undefined || service === null ? undefined : service;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
73
105
|
export function apply(host, config) {
|
|
74
106
|
if (config.memoryIndex?.enabled === false) {
|
|
75
107
|
return;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { KnownWorkspaces } from "./known-workspaces.js";
|
|
2
|
+
/** One workspace the card knows: canonical root plus its display label. */
|
|
3
|
+
export interface CatalogEntry {
|
|
4
|
+
root: string;
|
|
5
|
+
/** Display title from the registry, else the directory name. */
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
/** The read fence (`known`) and the listed set (`list`). */
|
|
9
|
+
export interface WorkspaceCatalog {
|
|
10
|
+
/** Every workspace the card may read, in registry order. */
|
|
11
|
+
known(): CatalogEntry[];
|
|
12
|
+
/** The known workspaces that currently have a memory directory. */
|
|
13
|
+
list(): CatalogEntry[];
|
|
14
|
+
}
|
|
15
|
+
/** The slice of `ctx.workspaceRegistry` this plugin consumes. */
|
|
16
|
+
export interface WorkspaceRegistryLike {
|
|
17
|
+
list(): unknown;
|
|
18
|
+
}
|
|
19
|
+
/** Where the catalog gets its workspace records, in precedence order. */
|
|
20
|
+
export interface WorkspaceCatalogOptions {
|
|
21
|
+
/** Live registry lookup; undefined when the host exposes no registry. */
|
|
22
|
+
registry?: (() => WorkspaceRegistryLike | undefined) | undefined;
|
|
23
|
+
/** The roots this process served — the last-resort source. */
|
|
24
|
+
served: KnownWorkspaces;
|
|
25
|
+
/** Harness home; enables the `storages/workspace.json` fallback when set. */
|
|
26
|
+
homeDir?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Create the catalog over the given sources. */
|
|
29
|
+
export declare function createWorkspaceCatalog(options: WorkspaceCatalogOptions): WorkspaceCatalog;
|
|
30
|
+
//# sourceMappingURL=workspace-catalog.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The card's workspace catalog — which workspaces the card may read, and which
|
|
3
|
+
* of them it lists.
|
|
4
|
+
*
|
|
5
|
+
* The list comes from the host's workspace registry (`ctx.workspaceRegistry`,
|
|
6
|
+
* the durable records the workspace picker itself renders), not from the
|
|
7
|
+
* sessions this process happened to serve: a card that only knows its own
|
|
8
|
+
* session history can never offer ZCode's workspace switching — with one
|
|
9
|
+
* served root the selector has nothing to switch to. The served-roots map
|
|
10
|
+
* stays as a source (a session in a workspace the registry does not know about
|
|
11
|
+
* still shows up), and `storages/workspace.json` is read only when the
|
|
12
|
+
* registry service yields nothing: that file is an internal format, so it is a
|
|
13
|
+
* fallback, never the primary.
|
|
14
|
+
*
|
|
15
|
+
* Two questions, deliberately answered by two methods. `known()` is the read
|
|
16
|
+
* fence: can this root be read at all? `list()` is the display projection:
|
|
17
|
+
* which workspaces have a memory directory right now? ZCode's viewer lists
|
|
18
|
+
* only workspaces that have one, but the fence cannot be that filter — a store
|
|
19
|
+
* deleted between listing and read must degrade to the empty state on its own
|
|
20
|
+
* entry, not turn the read into a 404.
|
|
21
|
+
*/
|
|
22
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
23
|
+
import { basename, join, resolve } from "node:path";
|
|
24
|
+
import { memoryDirFor } from "./memory-section.js";
|
|
25
|
+
/** The registry stores `fs.realpath` already; resolve the rest the same way. */
|
|
26
|
+
function canonicalPath(path) {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(path);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// A vanished or unreadable directory keeps its resolved spelling: the
|
|
32
|
+
// registry itself never rewrites a record when its directory disappears.
|
|
33
|
+
return resolve(path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** One catalogue entry from a path/title pair, or undefined when unusable. */
|
|
37
|
+
function entryOf(path, title) {
|
|
38
|
+
if (typeof path !== "string" || path.trim().length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const root = canonicalPath(path);
|
|
42
|
+
const label = typeof title === "string" && title.trim().length > 0 ? title.trim() : basename(root);
|
|
43
|
+
return { root, label };
|
|
44
|
+
}
|
|
45
|
+
/** Records from the live registry service. */
|
|
46
|
+
function registryEntries(registry) {
|
|
47
|
+
if (registry === undefined) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
let records;
|
|
51
|
+
try {
|
|
52
|
+
records = registry.list();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// A registry that throws is a host fault the card must not inherit:
|
|
56
|
+
// the next source still fills the list.
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(records)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const record of records) {
|
|
64
|
+
if (typeof record !== "object" || record === null) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const entry = entryOf(record.path, record.title);
|
|
68
|
+
if (entry !== undefined) {
|
|
69
|
+
entries.push(entry);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
/** Records from the registry's own durable file, for hosts without the service. */
|
|
75
|
+
function fileEntries(homeDir) {
|
|
76
|
+
if (homeDir === undefined || homeDir.trim().length === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(readFileSync(join(homeDir, "storages", "workspace.json"), "utf8"));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Missing or unreadable file: no workspaces from this source, no throw.
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const table = parsed?.tables?.workspaces;
|
|
88
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const entries = [];
|
|
92
|
+
for (const record of Object.values(table)) {
|
|
93
|
+
if (typeof record !== "object" || record === null) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const entry = entryOf(record.path, record.title);
|
|
97
|
+
if (entry !== undefined) {
|
|
98
|
+
entries.push(entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return entries;
|
|
102
|
+
}
|
|
103
|
+
/** Records from the roots this process served, most recent first. */
|
|
104
|
+
function servedEntries(served) {
|
|
105
|
+
return served.list().map(({ root }) => {
|
|
106
|
+
const canonical = canonicalPath(root);
|
|
107
|
+
return { root: canonical, label: basename(canonical) };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** ZCode's memory-directory rule: a plain directory, never a symlink. */
|
|
111
|
+
function hasMemoryDirectory(root) {
|
|
112
|
+
try {
|
|
113
|
+
const stat = lstatSync(memoryDirFor(root));
|
|
114
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Deduplicate by canonical root, keeping the first (highest-precedence) entry. */
|
|
121
|
+
function dedupe(entries) {
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const unique = [];
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (seen.has(entry.root)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
seen.add(entry.root);
|
|
129
|
+
unique.push(entry);
|
|
130
|
+
}
|
|
131
|
+
return unique;
|
|
132
|
+
}
|
|
133
|
+
/** Create the catalog over the given sources. */
|
|
134
|
+
export function createWorkspaceCatalog(options) {
|
|
135
|
+
const known = () => {
|
|
136
|
+
const live = registryEntries(options.registry?.());
|
|
137
|
+
if (live.length > 0) {
|
|
138
|
+
return dedupe([...live, ...servedEntries(options.served)]);
|
|
139
|
+
}
|
|
140
|
+
return dedupe([...fileEntries(options.homeDir), ...servedEntries(options.served)]);
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
known,
|
|
144
|
+
list() {
|
|
145
|
+
return known().filter((entry) => hasMemoryDirectory(entry.root));
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=workspace-catalog.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "ZCode-style workspace memory for DeepSeek Harness: a per-workspace markdown store with session-start index injection.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test": "vitest run",
|
|
61
61
|
"test:coverage": "vitest run --coverage",
|
|
62
62
|
"test:watch": "vitest",
|
|
63
|
-
"lint": "oxlint src test",
|
|
63
|
+
"lint": "oxlint src test client",
|
|
64
64
|
"clean": "rm -rf lib",
|
|
65
65
|
"check:docs": "tsx scripts/run-gates.ts docs",
|
|
66
66
|
"check:handoff": "tsx scripts/verify-handoff-structure.ts --self-test && tsx scripts/verify-handoff-structure.ts",
|