dsh-session-manager 0.7.1 → 0.7.3

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.
@@ -21,7 +21,7 @@ const MAX_HEADER_BYTES = 1024 * 1024;
21
21
 
22
22
  export function assertSessionId(id) {
23
23
  if (typeof id !== "string" || id.trim() === "" || id === "." || id === ".."
24
- || /[\\/\x00-\x1f<>:"|?*]/.test(id) || /[. ]$/.test(id)) {
24
+ || /[\\/\x00-\x1f<>"|?*]/.test(id) || /[. ]$/.test(id)) {
25
25
  throw Object.assign(new Error("sessionId 必须是合法的会话标识,不能包含路径或特殊目录名"), { code: "bad-request" });
26
26
  }
27
27
  return id;
@@ -46,12 +46,56 @@ export function parseSessionGenerationFromFilename(filename) {
46
46
  return match[1] !== undefined ? Number(match[1]) : 0;
47
47
  }
48
48
 
49
- export function encodeSessionSegment(id) {
49
+ /**
50
+ * Encode a session ID into its on-disk session directory name,
51
+ * matching DSH kernel's `encodeSegment` contract in
52
+ * packages/session/session-persistence-jsonl/src/format.ts:
53
+ * - "." -> "~002E"
54
+ * - ".." -> "~002E~002E"
55
+ * - Safe chars [A-Za-z0-9._-] (except "~") remain unchanged
56
+ * - Other characters (including ":", "~", non-ASCII) are escaped as ~XXXX (4-digit uppercase hex)
57
+ */
58
+ export function encodeFsSessionSegment(raw) {
59
+ if (typeof raw !== "string" || raw.length === 0) {
60
+ throw new Error("cannot encode an empty path segment");
61
+ }
62
+ if (raw === ".") return "~002E";
63
+ if (raw === "..") return "~002E~002E";
64
+ let out = "";
65
+ for (let i = 0; i < raw.length; i++) {
66
+ const code = raw.charCodeAt(i);
67
+ const ch = String.fromCharCode(code);
68
+ if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
69
+ out += ch;
70
+ } else {
71
+ out += "~" + code.toString(16).toUpperCase().padStart(4, "0");
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /**
78
+ * Decode an on-disk session directory name back to raw session ID.
79
+ */
80
+ export function decodeFsSessionSegment(encoded) {
81
+ if (typeof encoded !== "string" || encoded.length === 0) return encoded;
82
+ if (encoded === "~002E") return ".";
83
+ if (encoded === "~002E~002E") return "..";
84
+ return encoded.replace(/~([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
85
+ }
86
+
87
+ /**
88
+ * Encode a project directory path (cwd) into its on-disk project directory key,
89
+ * matching DSH kernel's `projectKey` contract:
90
+ * Wrapping "--...--" markers with path separators collapsed into "-".
91
+ */
92
+ export function encodeProjectSegment(path) {
93
+ if (typeof path !== "string") return "--root--";
50
94
  let readable = "";
51
95
  let separatorRun = false;
52
- for (let i = 0; i < id.length; i++) {
53
- const ch = id[i];
54
- const code = id.charCodeAt(i);
96
+ for (let i = 0; i < path.length; i++) {
97
+ const ch = path[i];
98
+ const code = path.charCodeAt(i);
55
99
  if (ch === "/" || ch === "\\" || ch === ":") {
56
100
  if (!separatorRun) readable += "-";
57
101
  separatorRun = true;
@@ -66,6 +110,28 @@ export function encodeSessionSegment(id) {
66
110
  return "--" + (readable.replace(/^-+/, "") || "root").slice(0, 251) + "--";
67
111
  }
68
112
 
113
+ /**
114
+ * Legacy alias for `encodeProjectSegment`.
115
+ * @deprecated Use `encodeProjectSegment` for project cwd keys, or `encodeFsSessionSegment` for session directories.
116
+ */
117
+ export const encodeSessionSegment = encodeProjectSegment;
118
+
119
+ /**
120
+ * Return strictly verified candidate directory names for a given session ID.
121
+ * - Primary: DSH's true `encodeFsSessionSegment(id)` (e.g. "im~003Afeishu...")
122
+ * - Raw `id`: matches when raw ID equals encoded ID (common UUIDs), or if raw dir was used
123
+ * - Legacy projectKey format: transitional compatibility for `--id--` created by older plugin versions
124
+ */
125
+ export function candidateSessionDirs(id) {
126
+ assertSessionId(id);
127
+ const primary = encodeFsSessionSegment(id);
128
+ const candidates = [primary];
129
+ if (id !== primary) candidates.push(id);
130
+ const legacyProject = encodeProjectSegment(id);
131
+ if (legacyProject !== primary && legacyProject !== id) candidates.push(legacyProject);
132
+ return candidates;
133
+ }
134
+
69
135
  /** Only an actual <sessions>/<project>/<session> directory may be mutated.
70
136
  * The root itself may be configured through a symlink, but neither child may
71
137
  * be a symlink/junction. Check again immediately before destructive cleanup.
@@ -79,8 +145,9 @@ export async function assertSessionDirectory(root, directory, id, { allowMissing
79
145
  const target = resolve(directory);
80
146
  const rel = relative(base, target);
81
147
  const parts = rel.split(sep);
148
+ const validDirs = candidateSessionDirs(id);
82
149
  if (isAbsolute(rel) || parts.length !== 2 || parts.some(p => p === ".." || p === "." || p === "")
83
- || ![id, encodeSessionSegment(id)].includes(parts[1])) {
150
+ || !validDirs.includes(parts[1])) {
84
151
  throw new Error("会话目录超出允许范围,已停止文件操作");
85
152
  }
86
153
  const canonicalRoot = await fs.realpath(base);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-manager",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "DeepSeek Harness session and workspace manager —— Workspaces: archive and visibility. Sessions: move, migrate presets, delete, favorite, review later, prioritize, tag, and annotate. Batch operations. | DSH 会话与工作区管理——工作区:归档与显示。会话:跨工作区移动、预设迁移、删除;收藏、待回看、优先级、标签、备注。批量操作。",
5
5
  "license": "MIT",
6
6
  "keywords": [