@l1vide/dsh-session-delete 0.0.0-stage → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -2
- package/client.js +277 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +6 -0
- package/lib/classify.js +267 -0
- package/lib/index.js +367 -0
- package/lib/transcript.js +217 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +61 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
4
|
+
|
|
5
|
+
/** Zstandard frame magic, little-endian on disk. */
|
|
6
|
+
const ZSTD_MAGIC = 0xfd2fb528;
|
|
7
|
+
|
|
8
|
+
/** Tools whose call arguments name a file the call is about to mutate. */
|
|
9
|
+
const MUTATING_TOOLS = new Map([
|
|
10
|
+
["write", "create"],
|
|
11
|
+
["create_file", "create"],
|
|
12
|
+
["edit", "modify"],
|
|
13
|
+
["multi_edit", "modify"],
|
|
14
|
+
["str_replace_editor", "modify"],
|
|
15
|
+
["ops_write", "create"],
|
|
16
|
+
["ops_edit", "modify"],
|
|
17
|
+
["apply_patch", "patch"]
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/** Argument keys a mutating tool may use for its target path. */
|
|
21
|
+
const PATH_KEYS = ["path", "file_path", "filePath", "target_file", "target"];
|
|
22
|
+
|
|
23
|
+
/** Tools whose result proves the model had already seen a file's content. */
|
|
24
|
+
const READING_TOOLS = new Set(["read", "read_image", "view", "ops_read_files"]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Locate complete Zstandard frames without decompressing their blocks.
|
|
28
|
+
*
|
|
29
|
+
* Session artifacts are append-only: every durable batch is its own frame, so
|
|
30
|
+
* a plain one-shot decompress of the file stops after the first one and would
|
|
31
|
+
* read only the session header. This walks the frame structure — the same
|
|
32
|
+
* algorithm the harness uses to recover a torn tail — and returns the byte
|
|
33
|
+
* range of every complete frame.
|
|
34
|
+
*/
|
|
35
|
+
function scanFrames(buffer) {
|
|
36
|
+
const frames = [];
|
|
37
|
+
let offset = 0;
|
|
38
|
+
while (offset < buffer.length) {
|
|
39
|
+
const start = offset;
|
|
40
|
+
if (buffer.length - offset < 4) break;
|
|
41
|
+
if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) break;
|
|
42
|
+
offset += 4;
|
|
43
|
+
if (offset >= buffer.length) break;
|
|
44
|
+
const descriptor = buffer.readUInt8(offset);
|
|
45
|
+
offset += 1;
|
|
46
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
47
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
48
|
+
const checksum = (descriptor & 4) !== 0;
|
|
49
|
+
const dictionaryFlag = descriptor & 3;
|
|
50
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
51
|
+
const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag;
|
|
52
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
53
|
+
if (buffer.length - offset < remainingHeaderBytes) break;
|
|
54
|
+
offset += remainingHeaderBytes;
|
|
55
|
+
for (;;) {
|
|
56
|
+
if (buffer.length - offset < 3) return frames;
|
|
57
|
+
const blockHeader = buffer.readUIntLE(offset, 3);
|
|
58
|
+
offset += 3;
|
|
59
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
60
|
+
const blockType = (blockHeader >>> 1) & 3;
|
|
61
|
+
const blockSize = blockHeader >>> 3;
|
|
62
|
+
const payloadBytes = blockType === 1 ? 1 : blockSize;
|
|
63
|
+
if (buffer.length - offset < payloadBytes) return frames;
|
|
64
|
+
offset += payloadBytes;
|
|
65
|
+
if (lastBlock) break;
|
|
66
|
+
}
|
|
67
|
+
if (checksum) {
|
|
68
|
+
if (buffer.length - offset < 4) return frames;
|
|
69
|
+
offset += 4;
|
|
70
|
+
}
|
|
71
|
+
frames.push({ start, end: offset });
|
|
72
|
+
}
|
|
73
|
+
return frames;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Decode a stored session artifact into its raw event list.
|
|
78
|
+
* @param path - the artifact path from `sessionPersistence.locate()`.
|
|
79
|
+
*/
|
|
80
|
+
export async function readSessionEvents(path) {
|
|
81
|
+
const bytes = await readFile(path);
|
|
82
|
+
const text = path.endsWith(".zstd")
|
|
83
|
+
? scanFrames(bytes)
|
|
84
|
+
.map((frame) => {
|
|
85
|
+
try {
|
|
86
|
+
return zstdDecompressSync(bytes.subarray(frame.start, frame.end)).toString("utf8");
|
|
87
|
+
} catch {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.join("")
|
|
92
|
+
: bytes.toString("utf8");
|
|
93
|
+
const events = [];
|
|
94
|
+
for (const line of text.split("\n")) {
|
|
95
|
+
if (line === "") continue;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(line);
|
|
98
|
+
if (parsed && typeof parsed.type === "string") events.push(parsed);
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
return events;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseArguments(raw) {
|
|
105
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(raw);
|
|
108
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function patchTargets(args) {
|
|
115
|
+
const text = [args.patch, args.input, args.diff, args.text].find(
|
|
116
|
+
(value) => typeof value === "string"
|
|
117
|
+
);
|
|
118
|
+
if (text === void 0) return [];
|
|
119
|
+
const targets = [];
|
|
120
|
+
for (const line of text.split("\n")) {
|
|
121
|
+
const match = /^\*\*\* (?:Add|Update|Delete) File: (.+?)\s*$/.exec(line);
|
|
122
|
+
if (match) targets.push({ path: match[1], kind: match[0].includes("Add") ? "create" : "modify" });
|
|
123
|
+
}
|
|
124
|
+
return targets;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function targetsOf(name, args) {
|
|
128
|
+
if (args === null) return [];
|
|
129
|
+
if (name === "apply_patch") return patchTargets(args);
|
|
130
|
+
if (name === "ops_read_files" || name === "ops_write") {
|
|
131
|
+
const list = Array.isArray(args.files) ? args.files : null;
|
|
132
|
+
if (list !== null) {
|
|
133
|
+
return list
|
|
134
|
+
.map((entry) => (typeof entry?.path === "string" ? entry.path : null))
|
|
135
|
+
.filter((value) => value !== null)
|
|
136
|
+
.map((path) => ({ path, kind: MUTATING_TOOLS.get(name) ?? "modify" }));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const key of PATH_KEYS) {
|
|
140
|
+
const value = args[key];
|
|
141
|
+
if (typeof value === "string" && value !== "") {
|
|
142
|
+
return [{ path: value, kind: MUTATING_TOOLS.get(name) ?? "modify" }];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve one argument path against the session cwd, refusing anything that
|
|
150
|
+
* escapes it: a rollback may only ever touch the workspace it belongs to.
|
|
151
|
+
*/
|
|
152
|
+
function resolveInside(cwd, raw) {
|
|
153
|
+
const trimmed = raw.trim().replace(/^['"]|['"]$/g, "");
|
|
154
|
+
if (trimmed === "") return null;
|
|
155
|
+
const absolute = isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed);
|
|
156
|
+
const relation = relative(cwd, absolute);
|
|
157
|
+
if (relation === "" || relation.startsWith("..") || isAbsolute(relation)) return null;
|
|
158
|
+
if (relation.split(sep).includes(".git")) return null;
|
|
159
|
+
return absolute;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Derive which workspace files a session touched, and whether it created them.
|
|
164
|
+
*
|
|
165
|
+
* A path counts as created when the session's first mutation of it is a plain
|
|
166
|
+
* write with no earlier read in the same session — the harness requires a read
|
|
167
|
+
* before overwriting an existing file, so a write with no prior read means the
|
|
168
|
+
* file did not exist yet.
|
|
169
|
+
*
|
|
170
|
+
* @returns `{ cwd, items, skipped }` where every item carries the workspace
|
|
171
|
+
* relative path, the classification, and how many mutations hit it.
|
|
172
|
+
*/
|
|
173
|
+
export function collectTouchedFiles(events, cwd) {
|
|
174
|
+
const read = new Set();
|
|
175
|
+
const order = [];
|
|
176
|
+
const byPath = new Map();
|
|
177
|
+
let skipped = 0;
|
|
178
|
+
|
|
179
|
+
for (const event of events) {
|
|
180
|
+
if (event.type !== "tool/call") continue;
|
|
181
|
+
const data = event.data;
|
|
182
|
+
if (!data || typeof data.name !== "string") continue;
|
|
183
|
+
const tool = data.name;
|
|
184
|
+
const args = parseArguments(data.arguments);
|
|
185
|
+
const isReader = READING_TOOLS.has(tool);
|
|
186
|
+
const isMutator = MUTATING_TOOLS.has(tool);
|
|
187
|
+
if (!isReader && !isMutator) continue;
|
|
188
|
+
|
|
189
|
+
for (const target of targetsOf(tool, args)) {
|
|
190
|
+
const absolute = resolveInside(cwd, target.path);
|
|
191
|
+
if (absolute === null) {
|
|
192
|
+
skipped += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (isReader) {
|
|
196
|
+
read.add(absolute);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const existing = byPath.get(absolute);
|
|
200
|
+
if (existing !== void 0) {
|
|
201
|
+
existing.count += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const entry = {
|
|
205
|
+
absolute,
|
|
206
|
+
path: relative(cwd, absolute).split(sep).join("/"),
|
|
207
|
+
kind: target.kind === "create" && !read.has(absolute) ? "created" : "modified",
|
|
208
|
+
count: 1,
|
|
209
|
+
tool
|
|
210
|
+
};
|
|
211
|
+
byPath.set(absolute, entry);
|
|
212
|
+
order.push(entry);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { cwd, items: order, skipped };
|
|
217
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@l1vide/dsh-session-delete",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Delete a DSH session from the session menu: conversation gone, leftover scaffolding files cleaned up, results kept",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./client.js",
|
|
11
|
+
"./locale/*.json": "./locale/*.json",
|
|
12
|
+
"./icon": "./icon.svg",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib/index.js",
|
|
17
|
+
"lib/transcript.js",
|
|
18
|
+
"lib/classify.js",
|
|
19
|
+
"client.js",
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"icon.svg",
|
|
22
|
+
"locale/en.json",
|
|
23
|
+
"locale/zh.json"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"dsh",
|
|
27
|
+
"dsh-plugin",
|
|
28
|
+
"deepseek-harness",
|
|
29
|
+
"session",
|
|
30
|
+
"session-management",
|
|
31
|
+
"delete"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://github.com/Dubai1210/dsh-session-delete#readme",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/Dubai1210/dsh-session-delete.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/Dubai1210/dsh-session-delete/issues"
|
|
40
|
+
},
|
|
41
|
+
"author": "Dubai1210",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"immediately": true,
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
55
|
+
"@deepseek-ai/dsh-client-locale",
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
62
|
+
}
|
|
63
|
+
}
|