@dsh-cc/memory 0.5.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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +158 -0
  4. package/README.zh.md +125 -0
  5. package/lib/index.d.ts +75 -0
  6. package/lib/index.d.ts.map +1 -0
  7. package/lib/index.js +91 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/invariant.d.ts +16 -0
  10. package/lib/invariant.d.ts.map +1 -0
  11. package/lib/invariant.js +22 -0
  12. package/lib/invariant.js.map +1 -0
  13. package/lib/parser.d.ts +30 -0
  14. package/lib/parser.d.ts.map +1 -0
  15. package/lib/parser.js +96 -0
  16. package/lib/parser.js.map +1 -0
  17. package/lib/paths.d.ts +98 -0
  18. package/lib/paths.d.ts.map +1 -0
  19. package/lib/paths.js +236 -0
  20. package/lib/paths.js.map +1 -0
  21. package/lib/recall.d.ts +91 -0
  22. package/lib/recall.d.ts.map +1 -0
  23. package/lib/recall.js +253 -0
  24. package/lib/recall.js.map +1 -0
  25. package/lib/save.d.ts +53 -0
  26. package/lib/save.d.ts.map +1 -0
  27. package/lib/save.js +180 -0
  28. package/lib/save.js.map +1 -0
  29. package/lib/scan.d.ts +29 -0
  30. package/lib/scan.d.ts.map +1 -0
  31. package/lib/scan.js +70 -0
  32. package/lib/scan.js.map +1 -0
  33. package/lib/section.d.ts +129 -0
  34. package/lib/section.d.ts.map +1 -0
  35. package/lib/section.js +353 -0
  36. package/lib/section.js.map +1 -0
  37. package/lib/team.d.ts +90 -0
  38. package/lib/team.d.ts.map +1 -0
  39. package/lib/team.js +167 -0
  40. package/lib/team.js.map +1 -0
  41. package/lib/truncate.d.ts +35 -0
  42. package/lib/truncate.d.ts.map +1 -0
  43. package/lib/truncate.js +52 -0
  44. package/lib/truncate.js.map +1 -0
  45. package/lib/types.d.ts +34 -0
  46. package/lib/types.d.ts.map +1 -0
  47. package/lib/types.js +18 -0
  48. package/lib/types.js.map +1 -0
  49. package/lib/writeback.d.ts +85 -0
  50. package/lib/writeback.d.ts.map +1 -0
  51. package/lib/writeback.js +121 -0
  52. package/lib/writeback.js.map +1 -0
  53. package/package.json +65 -0
package/lib/team.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Team memory: an opt-in shared per-project memory directory layered on the
3
+ * private memdir. Provides the team path resolution, the dual-directory
4
+ * (private + team) merged prompt, and a seam-native read/write path validation
5
+ * chain so a relative key cannot escape the team directory.
6
+ *
7
+ * Security model (three hard prerequisites, all enforced before any read or
8
+ * write):
9
+ * 1. **Key sanitization** — a pure string pass (`sanitizePathKey`) run before
10
+ * any filesystem operation rejects null bytes, URL-encoded traversal, NFKC
11
+ * normalization differences, backslashes, and absolute keys.
12
+ * 2. **Seam-native validation chain** — for the sanitized key: `fs.lstat`
13
+ * rejects a final-segment symlink, then `fs.resolve` + `fs.contains`
14
+ * enforce prefix containment against the resolved team root.
15
+ * 3. **Containment on every access** — reads and writes go through the chain,
16
+ * so a symlinked intermediate component that points outside is caught by
17
+ * the resolve + contains step.
18
+ *
19
+ * Residual gap (documented): this closes per-access traversal, but a
20
+ * mutate-between-check TOCTOU window on *intermediate* components is not fully
21
+ * closed — only the final segment is lstat-checked, and the resolve/contains
22
+ * check and the read are not atomic. Do not enable `teamEnabled` in multi-tenant
23
+ * or untrusted-writer deployments.
24
+ * @module @dsh-cc/memory/team
25
+ */
26
+ import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs';
27
+ /** Error thrown when a team-memory key/path fails security validation. */
28
+ export declare class TeamMemoryError extends Error {
29
+ constructor(message: string);
30
+ }
31
+ /** Default subdirectory of the memory home holding team memory. */
32
+ export declare const TEAM_MEMORY_DIR = "team";
33
+ /**
34
+ * The team memory entrypoint filename, matching the private entrypoint.
35
+ * Each directory (private and team) keeps its own MEMORY.md index.
36
+ */
37
+ export declare const TEAM_ENTRYPOINT_NAME = "MEMORY.md";
38
+ /**
39
+ * Sanitize a relative path key by rejecting dangerous patterns, mirroring the
40
+ * Claude Code `sanitizePathKey` semantics. Pure string — run this before any
41
+ * filesystem operation. Returns the sanitized key unchanged or throws
42
+ * {@link TeamMemoryError}. Rejects:
43
+ * - null bytes (can truncate paths in C-based syscalls)
44
+ * - URL-encoded traversal (`%2e%2e%2f` → `../`)
45
+ * - NFKC normalization that collapses to `..` / separators (fullwidth `../`)
46
+ * - backslashes (Windows separator used as a traversal vector)
47
+ * - absolute paths
48
+ * @param key - the relative path key under the team directory.
49
+ * @returns the sanitized key (unchanged).
50
+ */
51
+ export declare function sanitizePathKey(key: string): string;
52
+ /**
53
+ * Resolve the team memory directory for one workspace. Team memory is shared
54
+ * by all users of THE PROJECT, so it lives inside the workspace's private
55
+ * memory directory: `<workspaceDir>/team`. (Before per-workspace isolation it
56
+ * sat at the global `<memoryHome>/team`; that directory is now inert.)
57
+ * @param workspaceDir - the workspace's private memory directory.
58
+ * @returns the team directory path (`<workspaceDir>/team`).
59
+ */
60
+ export declare function resolveTeamMemoryRoot(workspaceDir: string): string;
61
+ /**
62
+ * The seam-native team path validation chain. For a relative key under a team
63
+ * directory, enforce, in order:
64
+ * 1. `sanitizePathKey` — pure-string rejection (no fs access).
65
+ * 2. `fs.lstat` — reject a final-segment symlink before following it.
66
+ * 3. `fs.resolve` of both the team root and the candidate, then `fs.contains`
67
+ * — reject when the resolved candidate escapes the resolved team root.
68
+ *
69
+ * Throws {@link TeamMemoryError} when any step fails. A missing file is not an
70
+ * error here: the resolve+contains step still spans the nearest existing
71
+ * ancestor (realpath), so a missing child under an escaping symlinked ancestor
72
+ * is still caught.
73
+ * @param fs - the contiguous filesystem seam.
74
+ * @param teamDir - the resolved team directory path.
75
+ * @param relativeKey - the sanitizable relative key to validate.
76
+ * @returns the resolved, contained {@link FsTarget} for the candidate.
77
+ */
78
+ export declare function validateTeamMemKey(fs: FileSystem, teamDir: string, relativeKey: string): Promise<FsTarget>;
79
+ /**
80
+ * Validate and read a team memory file body, or `undefined` when the file is
81
+ * absent. The read only proceeds when {@link validateTeamMemKey} passes, so a
82
+ * traversal or symlink-escape key never reaches the read.
83
+ * @param fs - the contiguous filesystem seam.
84
+ * @param teamDir - the resolved team directory path.
85
+ * @param relativeKey - the sanitizable relative key to read.
86
+ * @param signal - optional cancellation.
87
+ * @returns the file body, or `undefined` if absent.
88
+ */
89
+ export declare function readTeamMemFile(fs: FileSystem, teamDir: string, relativeKey: string, signal?: AbortSignal): Promise<string | undefined>;
90
+ //# sourceMappingURL=team.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../src/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE/D,0EAA0E;AAC1E,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,mEAAmE;AACnE,eAAO,MAAM,eAAe,SAAS,CAAA;AAErC;;;GAGG;AACH,eAAO,MAAM,oBAAoB,cAAc,CAAA;AAE/C;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA0CnD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAiBnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
package/lib/team.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Team memory: an opt-in shared per-project memory directory layered on the
3
+ * private memdir. Provides the team path resolution, the dual-directory
4
+ * (private + team) merged prompt, and a seam-native read/write path validation
5
+ * chain so a relative key cannot escape the team directory.
6
+ *
7
+ * Security model (three hard prerequisites, all enforced before any read or
8
+ * write):
9
+ * 1. **Key sanitization** — a pure string pass (`sanitizePathKey`) run before
10
+ * any filesystem operation rejects null bytes, URL-encoded traversal, NFKC
11
+ * normalization differences, backslashes, and absolute keys.
12
+ * 2. **Seam-native validation chain** — for the sanitized key: `fs.lstat`
13
+ * rejects a final-segment symlink, then `fs.resolve` + `fs.contains`
14
+ * enforce prefix containment against the resolved team root.
15
+ * 3. **Containment on every access** — reads and writes go through the chain,
16
+ * so a symlinked intermediate component that points outside is caught by
17
+ * the resolve + contains step.
18
+ *
19
+ * Residual gap (documented): this closes per-access traversal, but a
20
+ * mutate-between-check TOCTOU window on *intermediate* components is not fully
21
+ * closed — only the final segment is lstat-checked, and the resolve/contains
22
+ * check and the read are not atomic. Do not enable `teamEnabled` in multi-tenant
23
+ * or untrusted-writer deployments.
24
+ * @module @dsh-cc/memory/team
25
+ */
26
+ import { join } from 'node:path';
27
+ /** Error thrown when a team-memory key/path fails security validation. */
28
+ export class TeamMemoryError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = 'TeamMemoryError';
32
+ }
33
+ }
34
+ /** Default subdirectory of the memory home holding team memory. */
35
+ export const TEAM_MEMORY_DIR = 'team';
36
+ /**
37
+ * The team memory entrypoint filename, matching the private entrypoint.
38
+ * Each directory (private and team) keeps its own MEMORY.md index.
39
+ */
40
+ export const TEAM_ENTRYPOINT_NAME = 'MEMORY.md';
41
+ /**
42
+ * Sanitize a relative path key by rejecting dangerous patterns, mirroring the
43
+ * Claude Code `sanitizePathKey` semantics. Pure string — run this before any
44
+ * filesystem operation. Returns the sanitized key unchanged or throws
45
+ * {@link TeamMemoryError}. Rejects:
46
+ * - null bytes (can truncate paths in C-based syscalls)
47
+ * - URL-encoded traversal (`%2e%2e%2f` → `../`)
48
+ * - NFKC normalization that collapses to `..` / separators (fullwidth `../`)
49
+ * - backslashes (Windows separator used as a traversal vector)
50
+ * - absolute paths
51
+ * @param key - the relative path key under the team directory.
52
+ * @returns the sanitized key (unchanged).
53
+ */
54
+ export function sanitizePathKey(key) {
55
+ if (key.length === 0) {
56
+ throw new TeamMemoryError('Empty path key');
57
+ }
58
+ // Null bytes can truncate paths in C-based syscalls.
59
+ if (key.includes('\0')) {
60
+ throw new TeamMemoryError(`Null byte in path key: "${key}"`);
61
+ }
62
+ // URL-encoded traversals (e.g. %2e%2e%2f = ../).
63
+ let decoded;
64
+ try {
65
+ decoded = decodeURIComponent(key);
66
+ }
67
+ catch {
68
+ // Malformed percent-encoding (e.g. %ZZ, lone %) — not valid URL-encoding,
69
+ // so no URL-encoded traversal is possible.
70
+ decoded = key;
71
+ }
72
+ if (decoded !== key && (decoded.includes('..') || decoded.includes('/') || decoded.includes('\\'))) {
73
+ throw new TeamMemoryError(`URL-encoded traversal in path key: "${key}"`);
74
+ }
75
+ // Unicode normalization attacks: fullwidth ../ normalize to ASCII ../ under
76
+ // NFKC. Reject for defense-in-depth even though a literal-fs backend treats
77
+ // these as bytes, not separators.
78
+ const normalized = key.normalize('NFKC');
79
+ if (normalized !== key &&
80
+ (normalized.includes('..') ||
81
+ normalized.includes('/') ||
82
+ normalized.includes('\\') ||
83
+ normalized.includes('\0'))) {
84
+ throw new TeamMemoryError(`Unicode-normalized traversal in path key: "${key}"`);
85
+ }
86
+ // Reject backslashes (Windows path separator used as a traversal vector).
87
+ if (key.includes('\\')) {
88
+ throw new TeamMemoryError(`Backslash in path key: "${key}"`);
89
+ }
90
+ // Reject absolute paths.
91
+ if (key.startsWith('/')) {
92
+ throw new TeamMemoryError(`Absolute path key: "${key}"`);
93
+ }
94
+ return key;
95
+ }
96
+ /**
97
+ * Resolve the team memory directory for one workspace. Team memory is shared
98
+ * by all users of THE PROJECT, so it lives inside the workspace's private
99
+ * memory directory: `<workspaceDir>/team`. (Before per-workspace isolation it
100
+ * sat at the global `<memoryHome>/team`; that directory is now inert.)
101
+ * @param workspaceDir - the workspace's private memory directory.
102
+ * @returns the team directory path (`<workspaceDir>/team`).
103
+ */
104
+ export function resolveTeamMemoryRoot(workspaceDir) {
105
+ return join(workspaceDir, TEAM_MEMORY_DIR);
106
+ }
107
+ /**
108
+ * The seam-native team path validation chain. For a relative key under a team
109
+ * directory, enforce, in order:
110
+ * 1. `sanitizePathKey` — pure-string rejection (no fs access).
111
+ * 2. `fs.lstat` — reject a final-segment symlink before following it.
112
+ * 3. `fs.resolve` of both the team root and the candidate, then `fs.contains`
113
+ * — reject when the resolved candidate escapes the resolved team root.
114
+ *
115
+ * Throws {@link TeamMemoryError} when any step fails. A missing file is not an
116
+ * error here: the resolve+contains step still spans the nearest existing
117
+ * ancestor (realpath), so a missing child under an escaping symlinked ancestor
118
+ * is still caught.
119
+ * @param fs - the contiguous filesystem seam.
120
+ * @param teamDir - the resolved team directory path.
121
+ * @param relativeKey - the sanitizable relative key to validate.
122
+ * @returns the resolved, contained {@link FsTarget} for the candidate.
123
+ */
124
+ export async function validateTeamMemKey(fs, teamDir, relativeKey) {
125
+ sanitizePathKey(relativeKey);
126
+ const fullPath = join(teamDir, relativeKey);
127
+ // Step 2: reject a final-segment symlink.
128
+ const pathInfo = await fs.lstat(fullPath);
129
+ if (pathInfo !== undefined && pathInfo.type === 'symlink') {
130
+ throw new TeamMemoryError(`Symlink at final path segment: "${relativeKey}"`);
131
+ }
132
+ // Step 3: resolve + prefix containment.
133
+ const candidate = await fs.resolve(fullPath);
134
+ const teamRoot = await fs.resolve(teamDir);
135
+ if (!fs.contains(teamRoot, candidate)) {
136
+ throw new TeamMemoryError(`Path escapes team memory directory: "${relativeKey}"`);
137
+ }
138
+ return candidate;
139
+ }
140
+ /**
141
+ * Validate and read a team memory file body, or `undefined` when the file is
142
+ * absent. The read only proceeds when {@link validateTeamMemKey} passes, so a
143
+ * traversal or symlink-escape key never reaches the read.
144
+ * @param fs - the contiguous filesystem seam.
145
+ * @param teamDir - the resolved team directory path.
146
+ * @param relativeKey - the sanitizable relative key to read.
147
+ * @param signal - optional cancellation.
148
+ * @returns the file body, or `undefined` if absent.
149
+ */
150
+ export async function readTeamMemFile(fs, teamDir, relativeKey, signal) {
151
+ const target = await validateTeamMemKey(fs, teamDir, relativeKey);
152
+ try {
153
+ return await fs.readText(target, signal);
154
+ }
155
+ catch (error) {
156
+ if (isNotFound(error))
157
+ return undefined;
158
+ throw error;
159
+ }
160
+ }
161
+ function isNotFound(error) {
162
+ return typeof error === 'object' && error !== null
163
+ && 'code' in error
164
+ && (error.code === 'FS_NOT_FOUND'
165
+ || error.code === 'ENOENT');
166
+ }
167
+ //# sourceMappingURL=team.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.js","sourceRoot":"","sources":["../src/team.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * MEMORY.md entrypoint bounds and truncation.
3
+ * The entrypoint is the always-loaded index; its content is capped so a
4
+ * runaway index cannot flood the system prompt.
5
+ * @module @dsh-cc/memory/truncate
6
+ */
7
+ /** The always-loaded index filename inside a memory directory. */
8
+ export declare const ENTRYPOINT_NAME = "MEMORY.md";
9
+ /** Line cap for the entrypoint index. */
10
+ export declare const MAX_ENTRYPOINT_LINES = 200;
11
+ /** Byte cap for the entrypoint index (long lines are the failure mode). */
12
+ export declare const MAX_ENTRYPOINT_BYTES = 25000;
13
+ /** Result of applying the entrypoint caps. */
14
+ export interface EntrypointTruncation {
15
+ /** Truncated content, plus a trailing warning line when a cap fired. */
16
+ content: string;
17
+ /** Original trimmed line count (before any truncation). */
18
+ lineCount: number;
19
+ /** Original trimmed byte count (before any truncation). */
20
+ byteCount: number;
21
+ /** Whether the line cap fired. */
22
+ wasLineTruncated: boolean;
23
+ /** Whether the byte cap fired (against the original, pre-line-cap size). */
24
+ wasByteTruncated: boolean;
25
+ }
26
+ /**
27
+ * Truncate MEMORY.md content to the line AND byte caps. Line-truncates first
28
+ * (a natural boundary), then byte-truncates at the last newline before the cap
29
+ * so a line is never cut mid-way, then appends a warning that names which cap
30
+ * fired. Unmodified content is returned unchanged.
31
+ * @param raw - the raw entrypoint index text.
32
+ * @returns the capped content and the cap flags.
33
+ */
34
+ export declare function truncateEntrypointContent(raw: string): EntrypointTruncation;
35
+ //# sourceMappingURL=truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kEAAkE;AAClE,eAAO,MAAM,eAAe,cAAc,CAAA;AAE1C,yCAAyC;AACzC,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAE1C,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;IACjB,kCAAkC;IAClC,gBAAgB,EAAE,OAAO,CAAA;IACzB,4EAA4E;IAC5E,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAoC3E"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * MEMORY.md entrypoint bounds and truncation.
3
+ * The entrypoint is the always-loaded index; its content is capped so a
4
+ * runaway index cannot flood the system prompt.
5
+ * @module @dsh-cc/memory/truncate
6
+ */
7
+ /** The always-loaded index filename inside a memory directory. */
8
+ export const ENTRYPOINT_NAME = 'MEMORY.md';
9
+ /** Line cap for the entrypoint index. */
10
+ export const MAX_ENTRYPOINT_LINES = 200;
11
+ /** Byte cap for the entrypoint index (long lines are the failure mode). */
12
+ export const MAX_ENTRYPOINT_BYTES = 25_000;
13
+ /**
14
+ * Truncate MEMORY.md content to the line AND byte caps. Line-truncates first
15
+ * (a natural boundary), then byte-truncates at the last newline before the cap
16
+ * so a line is never cut mid-way, then appends a warning that names which cap
17
+ * fired. Unmodified content is returned unchanged.
18
+ * @param raw - the raw entrypoint index text.
19
+ * @returns the capped content and the cap flags.
20
+ */
21
+ export function truncateEntrypointContent(raw) {
22
+ const trimmed = raw.trim();
23
+ const contentLines = trimmed.split('\n');
24
+ const lineCount = contentLines.length;
25
+ const byteCount = trimmed.length;
26
+ const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES;
27
+ const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES;
28
+ if (!wasLineTruncated && !wasByteTruncated) {
29
+ return { content: trimmed, lineCount, byteCount, wasLineTruncated, wasByteTruncated };
30
+ }
31
+ let truncated = wasLineTruncated
32
+ ? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')
33
+ : trimmed;
34
+ if (truncated.length > MAX_ENTRYPOINT_BYTES) {
35
+ const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES);
36
+ truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES);
37
+ }
38
+ const size = byteCount >= 1024 ? `${(byteCount / 1024).toFixed(1)}KB` : `${byteCount}B`;
39
+ const reason = wasByteTruncated && !wasLineTruncated
40
+ ? `${size} (limit: 25KB) — index entries are too long`
41
+ : wasLineTruncated && !wasByteTruncated
42
+ ? `${lineCount} lines (limit: ${MAX_ENTRYPOINT_LINES})`
43
+ : `${lineCount} lines and ${size}`;
44
+ return {
45
+ content: `${truncated}\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.`,
46
+ lineCount,
47
+ byteCount,
48
+ wasLineTruncated,
49
+ wasByteTruncated,
50
+ };
51
+ }
52
+ //# sourceMappingURL=truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kEAAkE;AAClE,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAA;AAE1C,yCAAyC;AACzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAA;AAEvC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAA;AAgB1C;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAA;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAA;IAEhC,MAAM,gBAAgB,GAAG,SAAS,GAAG,oBAAoB,CAAA;IACzD,MAAM,gBAAgB,GAAG,SAAS,GAAG,oBAAoB,CAAA;IAEzD,IAAI,CAAC,gBAAgB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC3C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAA;IACvF,CAAC;IAED,IAAI,SAAS,GAAG,gBAAgB;QAC9B,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACxD,CAAC,CAAC,OAAO,CAAA;IAEX,IAAI,SAAS,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;QAC/D,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAA;IAC1E,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,CAAA;IACvF,MAAM,MAAM,GAAG,gBAAgB,IAAI,CAAC,gBAAgB;QAClD,CAAC,CAAC,GAAG,IAAI,6CAA6C;QACtD,CAAC,CAAC,gBAAgB,IAAI,CAAC,gBAAgB;YACrC,CAAC,CAAC,GAAG,SAAS,kBAAkB,oBAAoB,GAAG;YACvD,CAAC,CAAC,GAAG,SAAS,cAAc,IAAI,EAAE,CAAA;IAEtC,OAAO;QACL,OAAO,EAAE,GAAG,SAAS,kBAAkB,eAAe,OAAO,MAAM,8GAA8G;QACjL,SAAS;QACT,SAAS;QACT,gBAAgB;QAChB,gBAAgB;KACjB,CAAA;AACH,CAAC"}
package/lib/types.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Closed memory-type taxonomy and rationalized frontmatter for `dsh-memory`
3
+ * topic files.
4
+ * @module @dsh-cc/memory/types
5
+ */
6
+ /** The four memory types a topic file may declare. */
7
+ export declare const MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
8
+ /** One valid memory type; unknown or absent values degrade gracefully. */
9
+ export type MemoryType = (typeof MEMORY_TYPES)[number];
10
+ /**
11
+ * Parse a raw frontmatter `type` value into a {@link MemoryType}.
12
+ * @param raw - the value read from a topic file's frontmatter.
13
+ * @returns the matching type, or `undefined` for missing or unknown values.
14
+ */
15
+ export declare function parseMemoryType(raw: unknown): MemoryType | undefined;
16
+ /** One topic file's rationalized frontmatter header. */
17
+ export interface MemoryFrontmatter {
18
+ /** Topic name used as the MEMORY.md index title. */
19
+ name: string;
20
+ /** One-line relevance description presented to the recall selector. */
21
+ description: string;
22
+ /** Topic type, or `undefined` for legacy files without a `type` field. */
23
+ type?: MemoryType;
24
+ }
25
+ /** A topic file discovered in a memory directory, with its rationalized header. */
26
+ export interface MemoryIndexEntry {
27
+ /** Absolute path of the topic file. */
28
+ path: string;
29
+ /** Basename used to join a MEMORY.md pointer and to dedupe recall. */
30
+ filename: string;
31
+ /** Rationalized frontmatter (name/description/type). */
32
+ frontmatter: MemoryFrontmatter;
33
+ }
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,sDAAsD;AACtD,eAAO,MAAM,YAAY,uDAAwD,CAAA;AAEjF,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAIpE;AAED,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAA;IAChB,wDAAwD;IACxD,WAAW,EAAE,iBAAiB,CAAA;CAC/B"}
package/lib/types.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Closed memory-type taxonomy and rationalized frontmatter for `dsh-memory`
3
+ * topic files.
4
+ * @module @dsh-cc/memory/types
5
+ */
6
+ /** The four memory types a topic file may declare. */
7
+ export const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'];
8
+ /**
9
+ * Parse a raw frontmatter `type` value into a {@link MemoryType}.
10
+ * @param raw - the value read from a topic file's frontmatter.
11
+ * @returns the matching type, or `undefined` for missing or unknown values.
12
+ */
13
+ export function parseMemoryType(raw) {
14
+ return typeof raw === 'string'
15
+ ? MEMORY_TYPES.find(type => type === raw)
16
+ : undefined;
17
+ }
18
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,sDAAsD;AACtD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,CAAU,CAAA;AAKjF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ;QAC5B,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC;QACzC,CAAC,CAAC,SAAS,CAAA;AACf,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Host-side write-back for memory fork output.
3
+ *
4
+ * The extraction/dream forks inherit the parent session's sandbox policy,
5
+ * under which the memory directory (the harness home) is OUTSIDE the session
6
+ * workspace: every model-side `write` is fenced, and a background job cannot
7
+ * escalate (escalation prompts, and its approval policy is `never`). So the
8
+ * forks return their file set as structured output and the plugin — trusted
9
+ * host code — performs the writes itself, stamping a per-call policy whose
10
+ * writable root IS the memory directory. The sandbox still confines each
11
+ * write behind the filename validation below (defense in depth); nothing
12
+ * outside the memory directory becomes writable.
13
+ *
14
+ * Validation rejects the WHOLE batch on any violation — a partial write would
15
+ * leave the index and its topic files inconsistent with no signal back to the
16
+ * fork that produced them.
17
+ * @module @dsh-cc/memory-consolidation/writeback
18
+ */
19
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
20
+ /** One file the fork asks the plugin to write: a flat filename plus full body. */
21
+ export interface MemoryWrite {
22
+ readonly path: string;
23
+ readonly content: string;
24
+ }
25
+ /** The structured-output contract every memory fork reports. */
26
+ export declare const MEMORY_WRITES_SCHEMA: {
27
+ readonly type: "object";
28
+ readonly properties: {
29
+ readonly writes: {
30
+ readonly type: "array";
31
+ readonly items: {
32
+ readonly type: "object";
33
+ readonly properties: {
34
+ readonly path: {
35
+ readonly type: "string";
36
+ };
37
+ readonly content: {
38
+ readonly type: "string";
39
+ };
40
+ };
41
+ readonly required: readonly ["path", "content"];
42
+ readonly additionalProperties: false;
43
+ };
44
+ };
45
+ };
46
+ readonly required: readonly ["writes"];
47
+ readonly additionalProperties: false;
48
+ };
49
+ /** At most this many files per fork report. */
50
+ export declare const WRITEBACK_MAX_FILES = 32;
51
+ /** Per-file body cap, in UTF-8 bytes. */
52
+ export declare const WRITEBACK_MAX_FILE_BYTES: number;
53
+ /** Whole-batch body cap, in UTF-8 bytes. */
54
+ export declare const WRITEBACK_MAX_TOTAL_BYTES: number;
55
+ /**
56
+ * The per-call policy stamped on host-side memory writes: confinement is kept
57
+ * (`workspace-write`), but the writable root is the memory directory itself.
58
+ * Mirrors the upstream "an approved escalation stamps a wider mode for one
59
+ * call" mechanism — here the plugin is the approver, and the name validation
60
+ * above has already narrowed the file set.
61
+ */
62
+ export interface MemoryWritePolicy {
63
+ readonly mode: 'workspace-write';
64
+ readonly workspaceRoot: string;
65
+ }
66
+ /** The policy for host-side writes inside `dir` (also used for the dream lock). */
67
+ export declare function memoryWritePolicy(dir: string): MemoryWritePolicy;
68
+ /**
69
+ * Validate a fork's structured payload into a writable batch. Throws with a
70
+ * precise reason on ANY violation; an empty `writes` array is a valid no-op.
71
+ * @param input - the raw `structured` value captured from the fork.
72
+ * @returns the validated writes.
73
+ */
74
+ export declare function validateMemoryWrites(input: unknown): MemoryWrite[];
75
+ /**
76
+ * Validate, then write the batch inside the memory directory under the
77
+ * confined per-call policy. Writes are sequential so a mid-batch failure
78
+ * leaves a deterministic prefix; the caller maps any throw to a failed job.
79
+ * @param fs - the filesystem seam.
80
+ * @param dir - the memory directory root.
81
+ * @param writes - an already-validated batch (see {@link validateMemoryWrites}).
82
+ * @returns the filenames written, in batch order.
83
+ */
84
+ export declare function writeMemoryFiles(fs: FileSystem, dir: string, writes: readonly MemoryWrite[]): Promise<string[]>;
85
+ //# sourceMappingURL=writeback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../src/writeback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,gEAAgE;AAChE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;CAkBvB,CAAA;AAEV,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB,KAAK,CAAA;AACrC,yCAAyC;AACzC,eAAO,MAAM,wBAAwB,QAAY,CAAA;AACjD,4CAA4C;AAC5C,eAAO,MAAM,yBAAyB,QAAa,CAAA;AASnD;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,EAAE,CAuClE;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,WAAW,EAAE,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CASnB"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Host-side write-back for memory fork output.
3
+ *
4
+ * The extraction/dream forks inherit the parent session's sandbox policy,
5
+ * under which the memory directory (the harness home) is OUTSIDE the session
6
+ * workspace: every model-side `write` is fenced, and a background job cannot
7
+ * escalate (escalation prompts, and its approval policy is `never`). So the
8
+ * forks return their file set as structured output and the plugin — trusted
9
+ * host code — performs the writes itself, stamping a per-call policy whose
10
+ * writable root IS the memory directory. The sandbox still confines each
11
+ * write behind the filename validation below (defense in depth); nothing
12
+ * outside the memory directory becomes writable.
13
+ *
14
+ * Validation rejects the WHOLE batch on any violation — a partial write would
15
+ * leave the index and its topic files inconsistent with no signal back to the
16
+ * fork that produced them.
17
+ * @module @dsh-cc/memory-consolidation/writeback
18
+ */
19
+ import { join } from 'node:path';
20
+ /** The structured-output contract every memory fork reports. */
21
+ export const MEMORY_WRITES_SCHEMA = {
22
+ type: 'object',
23
+ properties: {
24
+ writes: {
25
+ type: 'array',
26
+ items: {
27
+ type: 'object',
28
+ properties: {
29
+ path: { type: 'string' },
30
+ content: { type: 'string' },
31
+ },
32
+ required: ['path', 'content'],
33
+ additionalProperties: false,
34
+ },
35
+ },
36
+ },
37
+ required: ['writes'],
38
+ additionalProperties: false,
39
+ };
40
+ /** At most this many files per fork report. */
41
+ export const WRITEBACK_MAX_FILES = 32;
42
+ /** Per-file body cap, in UTF-8 bytes. */
43
+ export const WRITEBACK_MAX_FILE_BYTES = 64 * 1024;
44
+ /** Whole-batch body cap, in UTF-8 bytes. */
45
+ export const WRITEBACK_MAX_TOTAL_BYTES = 256 * 1024;
46
+ /**
47
+ * Flat `.md` filenames only: a leading alphanumeric, then alphanumerics /
48
+ * dots / dashes / underscores. This forbids separators, `..` escapes,
49
+ * absolute paths, and dotfiles (including the consolidation lock) in one rule.
50
+ */
51
+ const FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/;
52
+ /** The policy for host-side writes inside `dir` (also used for the dream lock). */
53
+ export function memoryWritePolicy(dir) {
54
+ return { mode: 'workspace-write', workspaceRoot: dir };
55
+ }
56
+ /**
57
+ * Validate a fork's structured payload into a writable batch. Throws with a
58
+ * precise reason on ANY violation; an empty `writes` array is a valid no-op.
59
+ * @param input - the raw `structured` value captured from the fork.
60
+ * @returns the validated writes.
61
+ */
62
+ export function validateMemoryWrites(input) {
63
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
64
+ throw new Error('memory fork output must be an object with a "writes" array');
65
+ }
66
+ const raw = input.writes;
67
+ if (!Array.isArray(raw)) {
68
+ throw new Error('memory fork output must be an object with a "writes" array');
69
+ }
70
+ if (raw.length > WRITEBACK_MAX_FILES) {
71
+ throw new Error(`memory fork reported ${raw.length} files, over the ${WRITEBACK_MAX_FILES} cap`);
72
+ }
73
+ const seen = new Set();
74
+ let total = 0;
75
+ const writes = raw.map((entry, i) => {
76
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
77
+ throw new Error(`memory write #${i} must be an object with path/content strings`);
78
+ }
79
+ const { path, content } = entry;
80
+ if (typeof path !== 'string' || typeof content !== 'string') {
81
+ throw new Error(`memory write #${i} must be an object with path/content strings`);
82
+ }
83
+ if (!FILE_NAME.test(path)) {
84
+ throw new Error(`invalid memory filename "${path}": expected a flat .md name`);
85
+ }
86
+ if (seen.has(path)) {
87
+ throw new Error(`duplicate memory filename "${path}" in one batch`);
88
+ }
89
+ seen.add(path);
90
+ const bytes = Buffer.byteLength(content, 'utf8');
91
+ if (bytes > WRITEBACK_MAX_FILE_BYTES) {
92
+ throw new Error(`memory file "${path}" is ${bytes} bytes, over the ${WRITEBACK_MAX_FILE_BYTES} cap`);
93
+ }
94
+ total += bytes;
95
+ return { path, content };
96
+ });
97
+ if (total > WRITEBACK_MAX_TOTAL_BYTES) {
98
+ throw new Error(`memory batch is ${total} bytes, over the ${WRITEBACK_MAX_TOTAL_BYTES} cap`);
99
+ }
100
+ return writes;
101
+ }
102
+ /**
103
+ * Validate, then write the batch inside the memory directory under the
104
+ * confined per-call policy. Writes are sequential so a mid-batch failure
105
+ * leaves a deterministic prefix; the caller maps any throw to a failed job.
106
+ * @param fs - the filesystem seam.
107
+ * @param dir - the memory directory root.
108
+ * @param writes - an already-validated batch (see {@link validateMemoryWrites}).
109
+ * @returns the filenames written, in batch order.
110
+ */
111
+ export async function writeMemoryFiles(fs, dir, writes) {
112
+ const policy = memoryWritePolicy(dir);
113
+ const written = [];
114
+ for (const { path, content } of writes) {
115
+ const target = await fs.resolve(join(dir, path));
116
+ await fs.writeText(target, content, undefined, undefined, policy);
117
+ written.push(path);
118
+ }
119
+ return written;
120
+ }
121
+ //# sourceMappingURL=writeback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeback.js","sourceRoot":"","sources":["../src/writeback.ts"],"names":[],"mappings":"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"}