dsh-continual-evolve 0.13.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 +10 -8
- package/README.zh.md +10 -8
- package/client/client.js +656 -162
- package/lib/card-routes.d.ts +3 -2
- package/lib/card-routes.js +87 -11
- package/lib/index.d.ts +7 -6
- package/lib/index.js +41 -9
- package/lib/memory-snapshot.d.ts +28 -0
- package/lib/memory-snapshot.js +57 -3
- package/lib/workspace-catalog.d.ts +30 -0
- package/lib/workspace-catalog.js +149 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +6 -4
package/lib/card-routes.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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";
|
|
5
|
+
export declare const CARD_API_MEMORY_FILE_PATH = "/dsh-continual-evolve/api/v1/memory/file";
|
|
5
6
|
/** Minimal request shape the handlers touch (Node IncomingMessage subset). */
|
|
6
7
|
export interface CardRequest {
|
|
7
8
|
method?: string;
|
|
@@ -28,5 +29,5 @@ export interface CardWebServer {
|
|
|
28
29
|
* shape the host hands back (or none, as on some host versions), the
|
|
29
30
|
* disposer calls only the functions among them.
|
|
30
31
|
*/
|
|
31
|
-
export declare function mountCardRoutes(webServer: CardWebServer,
|
|
32
|
+
export declare function mountCardRoutes(webServer: CardWebServer, catalog: WorkspaceCatalog): () => void;
|
|
32
33
|
//# sourceMappingURL=card-routes.d.ts.map
|
package/lib/card-routes.js
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
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
|
-
*
|
|
7
|
-
* known root,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* three endpoints are GET-only projections: the workspace catalogue, the
|
|
7
|
+
* memory snapshot for a known root, and one memory file's body for the card's
|
|
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.
|
|
11
14
|
*/
|
|
12
15
|
import { resolve } from "node:path";
|
|
13
|
-
import { memorySnapshot } from "./memory-snapshot.js";
|
|
16
|
+
import { memoryFileContent, memorySnapshot } from "./memory-snapshot.js";
|
|
14
17
|
/** URL prefix of the card API (kind-exact routes registered under it). */
|
|
15
18
|
export const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
|
|
16
19
|
export const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
|
|
20
|
+
export const CARD_API_MEMORY_FILE_PATH = "/dsh-continual-evolve/api/v1/memory/file";
|
|
17
21
|
function sendJson(response, status, body) {
|
|
18
22
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
19
23
|
response.end(JSON.stringify(body));
|
|
@@ -27,7 +31,7 @@ function isGet(request) {
|
|
|
27
31
|
* shape the host hands back (or none, as on some host versions), the
|
|
28
32
|
* disposer calls only the functions among them.
|
|
29
33
|
*/
|
|
30
|
-
export function mountCardRoutes(webServer,
|
|
34
|
+
export function mountCardRoutes(webServer, catalog) {
|
|
31
35
|
const disposers = [];
|
|
32
36
|
disposers.push(webServer.register({
|
|
33
37
|
kind: "exact",
|
|
@@ -38,7 +42,9 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
38
42
|
response.end();
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
|
-
|
|
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() });
|
|
42
48
|
},
|
|
43
49
|
}));
|
|
44
50
|
disposers.push(webServer.register({
|
|
@@ -55,16 +61,66 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
55
61
|
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
56
62
|
return;
|
|
57
63
|
}
|
|
58
|
-
const known =
|
|
64
|
+
const known = knownEntryOf(catalog, root);
|
|
59
65
|
if (known === undefined) {
|
|
60
|
-
// The
|
|
61
|
-
//
|
|
66
|
+
// The catalogue answers 404 without distinguishing "not a
|
|
67
|
+
// workspace here" from "wrong path": no filesystem oracle.
|
|
62
68
|
sendJson(response, 404, { error: "unknown workspace root" });
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
65
71
|
sendJson(response, 200, memorySnapshot(known.root));
|
|
66
72
|
},
|
|
67
73
|
}));
|
|
74
|
+
disposers.push(webServer.register({
|
|
75
|
+
kind: "exact",
|
|
76
|
+
path: CARD_API_MEMORY_FILE_PATH,
|
|
77
|
+
handler(request, response) {
|
|
78
|
+
if (!isGet(request)) {
|
|
79
|
+
response.writeHead(405, { allow: "GET" });
|
|
80
|
+
response.end();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const root = rootParamOf(request);
|
|
84
|
+
if (root === null) {
|
|
85
|
+
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const file = fileParamOf(request);
|
|
89
|
+
if (file === null) {
|
|
90
|
+
sendJson(response, 400, { error: "file query parameter is required" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const known = knownEntryOf(catalog, root);
|
|
94
|
+
if (known === undefined) {
|
|
95
|
+
// Same fence as the snapshot route: 404 without distinguishing
|
|
96
|
+
// "not a workspace here" from "wrong path".
|
|
97
|
+
sendJson(response, 404, { error: "unknown workspace root" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const content = memoryFileContent(known.root, file);
|
|
101
|
+
if (!content.ok) {
|
|
102
|
+
// The fence reasons stay indistinguishable from absence in status
|
|
103
|
+
// terms only where safe: "outside" answers 400 (a client bug),
|
|
104
|
+
// absence 404, oversized 413 — never a body leak either way.
|
|
105
|
+
if (content.reason === "outside") {
|
|
106
|
+
sendJson(response, 400, { error: "file must be a plain .md entry of the memory directory" });
|
|
107
|
+
}
|
|
108
|
+
else if (content.reason === "too-large") {
|
|
109
|
+
sendJson(response, 413, { error: "memory file exceeds the 5 MiB preview limit" });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
sendJson(response, 404, { error: "memory file not found" });
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
sendJson(response, 200, {
|
|
117
|
+
file: content.file,
|
|
118
|
+
content: content.content,
|
|
119
|
+
mtimeMs: content.mtimeMs,
|
|
120
|
+
changed: content.changed,
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
68
124
|
return () => {
|
|
69
125
|
for (const disposer of disposers) {
|
|
70
126
|
if (typeof disposer === "function") {
|
|
@@ -73,6 +129,11 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
73
129
|
}
|
|
74
130
|
};
|
|
75
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
|
+
}
|
|
76
137
|
/** The decoded `root` query parameter, or null when absent/relative. */
|
|
77
138
|
function rootParamOf(request) {
|
|
78
139
|
try {
|
|
@@ -89,4 +150,19 @@ function rootParamOf(request) {
|
|
|
89
150
|
return null;
|
|
90
151
|
}
|
|
91
152
|
}
|
|
153
|
+
/** The decoded `file` query parameter, or null when absent. */
|
|
154
|
+
function fileParamOf(request) {
|
|
155
|
+
try {
|
|
156
|
+
const url = new URL(request.url ?? "/", "http://dsh-card.invalid");
|
|
157
|
+
const file = url.searchParams.get("file");
|
|
158
|
+
if (file === null || file.trim().length === 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return file;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
void error;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
92
168
|
//# sourceMappingURL=card-routes.js.map
|
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;
|
package/lib/memory-snapshot.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/** The index file name (same store contract as the injection path). */
|
|
2
2
|
export declare const MEMORY_SNAPSHOT_INDEX_FILE = "MEMORY.md";
|
|
3
|
+
/**
|
|
4
|
+
* Preview size cap for the file content endpoint — files above it answer
|
|
5
|
+
* `too-large` instead of their body (ZCode's memory viewer uses the same 5 MiB).
|
|
6
|
+
*/
|
|
7
|
+
export declare const MEMORY_FILE_PREVIEW_LIMIT: number;
|
|
3
8
|
/** One memory file with its frontmatter fields ("" when absent). */
|
|
4
9
|
export interface MemoryFileInfo {
|
|
5
10
|
file: string;
|
|
6
11
|
name: string;
|
|
7
12
|
description: string;
|
|
8
13
|
type: string;
|
|
14
|
+
/** File modification time in epoch milliseconds (the card's "updated" line). */
|
|
15
|
+
updatedAt: number;
|
|
9
16
|
}
|
|
10
17
|
/** One parsed index row: `- [title](file.md) — hook`. */
|
|
11
18
|
export interface MemoryIndexRow {
|
|
@@ -51,4 +58,25 @@ export declare function parseMemoryFrontmatter(text: string): MemoryFrontmatter;
|
|
|
51
58
|
* card must observe, never break.
|
|
52
59
|
*/
|
|
53
60
|
export declare function memorySnapshot(root: string): MemorySnapshot;
|
|
61
|
+
/** The content endpoint's outcomes: the body, or why there is none. */
|
|
62
|
+
export type MemoryFileContent = {
|
|
63
|
+
ok: true;
|
|
64
|
+
file: string;
|
|
65
|
+
content: string;
|
|
66
|
+
mtimeMs: number;
|
|
67
|
+
changed: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: "outside" | "absent" | "too-large";
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Read one memory file's body for the card's content preview — the viewer
|
|
74
|
+
* side of the same read-only fence as `memorySnapshot`. The requested file
|
|
75
|
+
* name must be a plain entry of the workspace's memory directory (no path
|
|
76
|
+
* separators, no traversal, resolved location stays inside the directory);
|
|
77
|
+
* anything else answers `outside` without touching the filesystem, so the
|
|
78
|
+
* endpoint cannot become a path oracle. Oversized files answer `too-large`
|
|
79
|
+
* with the body unread.
|
|
80
|
+
*/
|
|
81
|
+
export declare function memoryFileContent(root: string, file: string): MemoryFileContent;
|
|
54
82
|
//# sourceMappingURL=memory-snapshot.d.ts.map
|
package/lib/memory-snapshot.js
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* card is a viewer, never a second store: no caching, no writes, no bootstrap
|
|
5
5
|
* (unlike the injection path, a missing store is reported, not created).
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import { join, resolve } from "node:path";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
9
9
|
import { memoryDirFor } from "./memory-section.js";
|
|
10
10
|
/** The index file name (same store contract as the injection path). */
|
|
11
11
|
export const MEMORY_SNAPSHOT_INDEX_FILE = "MEMORY.md";
|
|
12
|
+
/**
|
|
13
|
+
* Preview size cap for the file content endpoint — files above it answer
|
|
14
|
+
* `too-large` instead of their body (ZCode's memory viewer uses the same 5 MiB).
|
|
15
|
+
*/
|
|
16
|
+
export const MEMORY_FILE_PREVIEW_LIMIT = 5 * 1024 * 1024;
|
|
12
17
|
/**
|
|
13
18
|
* Parse `- [title](file.md) …` rows out of an index text. Anything else
|
|
14
19
|
* (headings, the bootstrap hint comment, blank lines) is ignored.
|
|
@@ -67,7 +72,8 @@ function readStoreState(memoryDir) {
|
|
|
67
72
|
const diskFiles = listStoreFiles(memoryDir);
|
|
68
73
|
const files = diskFiles.map((file) => {
|
|
69
74
|
const frontmatter = parseMemoryFrontmatter(readFileSync(join(memoryDir, file), "utf8"));
|
|
70
|
-
|
|
75
|
+
const updatedAt = statSync(join(memoryDir, file)).mtimeMs;
|
|
76
|
+
return { file, name: frontmatter.name, description: frontmatter.description, type: frontmatter.type, updatedAt };
|
|
71
77
|
});
|
|
72
78
|
return { indexRows: parseMemoryIndexRows(indexText), diskFiles, files };
|
|
73
79
|
}
|
|
@@ -122,4 +128,52 @@ export function memorySnapshot(root) {
|
|
|
122
128
|
readError: null,
|
|
123
129
|
};
|
|
124
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Read one memory file's body for the card's content preview — the viewer
|
|
133
|
+
* side of the same read-only fence as `memorySnapshot`. The requested file
|
|
134
|
+
* name must be a plain entry of the workspace's memory directory (no path
|
|
135
|
+
* separators, no traversal, resolved location stays inside the directory);
|
|
136
|
+
* anything else answers `outside` without touching the filesystem, so the
|
|
137
|
+
* endpoint cannot become a path oracle. Oversized files answer `too-large`
|
|
138
|
+
* with the body unread.
|
|
139
|
+
*/
|
|
140
|
+
export function memoryFileContent(root, file) {
|
|
141
|
+
const memoryDir = memoryDirFor(resolve(root));
|
|
142
|
+
if (file.length === 0 ||
|
|
143
|
+
!file.endsWith(".md") ||
|
|
144
|
+
file.includes("/") ||
|
|
145
|
+
file.includes("\\") ||
|
|
146
|
+
basename(file) !== file) {
|
|
147
|
+
return { ok: false, reason: "outside" };
|
|
148
|
+
}
|
|
149
|
+
const target = resolve(memoryDir, file);
|
|
150
|
+
if (!target.startsWith(memoryDir + "/") && target !== memoryDir) {
|
|
151
|
+
return { ok: false, reason: "outside" };
|
|
152
|
+
}
|
|
153
|
+
let before;
|
|
154
|
+
try {
|
|
155
|
+
before = statSync(target);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { ok: false, reason: "absent" };
|
|
159
|
+
}
|
|
160
|
+
if (!before.isFile()) {
|
|
161
|
+
return { ok: false, reason: "absent" };
|
|
162
|
+
}
|
|
163
|
+
if (before.size > MEMORY_FILE_PREVIEW_LIMIT) {
|
|
164
|
+
return { ok: false, reason: "too-large" };
|
|
165
|
+
}
|
|
166
|
+
let content;
|
|
167
|
+
try {
|
|
168
|
+
content = readFileSync(target, "utf8");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return { ok: false, reason: "absent" };
|
|
172
|
+
}
|
|
173
|
+
// The file may have been rewritten between stat and read — surface that
|
|
174
|
+
// instead of silently showing stale bytes (same guard shape as ZCode's
|
|
175
|
+
// memory viewer's "changed during read" state).
|
|
176
|
+
const after = statSync(target);
|
|
177
|
+
return { ok: true, file, content, mtimeMs: after.mtimeMs, changed: after.mtimeMs !== before.mtimeMs };
|
|
178
|
+
}
|
|
125
179
|
//# sourceMappingURL=memory-snapshot.js.map
|
|
@@ -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/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.14.1",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"default": "./lib/index.js"
|
|
21
21
|
},
|
|
22
22
|
"./client": "./client/client.js",
|
|
23
|
-
"./package.json": "./package.json"
|
|
23
|
+
"./package.json": "./package.json",
|
|
24
|
+
"./locale/*.json": "./locale/*.json"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"lib/**/*.js",
|
|
27
28
|
"lib/**/*.d.ts",
|
|
28
29
|
"client/client.js",
|
|
29
30
|
"cordis.patch.yml",
|
|
31
|
+
"locale/*.json",
|
|
30
32
|
"README.md",
|
|
31
33
|
"LICENSE"
|
|
32
34
|
],
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
"test": "vitest run",
|
|
59
61
|
"test:coverage": "vitest run --coverage",
|
|
60
62
|
"test:watch": "vitest",
|
|
61
|
-
"lint": "oxlint src test",
|
|
63
|
+
"lint": "oxlint src test client",
|
|
62
64
|
"clean": "rm -rf lib",
|
|
63
65
|
"check:docs": "tsx scripts/run-gates.ts docs",
|
|
64
66
|
"check:handoff": "tsx scripts/verify-handoff-structure.ts --self-test && tsx scripts/verify-handoff-structure.ts",
|