@michengai/dsh-archive-manager 0.1.44 → 1.0.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.
@@ -0,0 +1,235 @@
1
+ const id = (value) => {
2
+ if (typeof value !== "string" || !value.length || value.length > 1024) throw new TypeError("\u4F1A\u8BDD ID \u65E0\u6548");
3
+ return value;
4
+ };
5
+ const queryText = (value, required = false) => {
6
+ if (typeof value !== "string" || value.length > 200 || required && !value.trim()) throw new TypeError("\u5173\u952E\u8BCD\u987B\u4E3A 1\uFF5E200 \u4E2A\u5B57\u7B26");
7
+ return value.trim();
8
+ };
9
+ const searchInputSchema = { parse(value) {
10
+ if (!value || !Array.isArray(value.sessionIds) || !value.sessionIds.length || value.sessionIds.length > 20) throw new TypeError("\u6BCF\u6B21\u68C0\u7D22\u987B\u5305\u542B 1\uFF5E20 \u4E2A\u4F1A\u8BDD");
11
+ return { sessionIds: [...new Set(value.sessionIds.map(id))], query: queryText(value.query, true) };
12
+ } };
13
+ const previewInputSchema = { parse(value) {
14
+ if (!value) throw new TypeError("\u9884\u89C8\u53C2\u6570\u65E0\u6548");
15
+ return { sessionId: id(value.sessionId), query: queryText(value.query ?? "") };
16
+ } };
17
+ const boundedString = (value, max) => {
18
+ if (typeof value !== "string" || value.length > max) throw new TypeError("\u8FD4\u56DE\u6587\u672C\u65E0\u6548");
19
+ return value;
20
+ };
21
+ const sequence = (value) => {
22
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("\u6D88\u606F\u5E8F\u53F7\u65E0\u6548");
23
+ return value;
24
+ };
25
+ const searchResultSchema = { parse(value) {
26
+ if (!value || !Array.isArray(value.items) || !Array.isArray(value.failures) || value.items.length > 20 || value.failures.length > 20) throw new TypeError("\u68C0\u7D22\u7ED3\u679C\u65E0\u6548");
27
+ return {
28
+ items: value.items.map((row) => ({ sessionId: id(row.sessionId), seq: sequence(row.seq), snippet: boundedString(row.snippet, 400) })),
29
+ failures: value.failures.map((row) => ({ sessionId: id(row.sessionId), message: boundedString(row.message, 500) }))
30
+ };
31
+ } };
32
+ const previewResultSchema = { parse(value) {
33
+ if (!value || !Array.isArray(value.messages) || value.messages.length > 8 || typeof value.hasEarlier !== "boolean" || typeof value.hasLater !== "boolean") throw new TypeError("\u9884\u89C8\u7ED3\u679C\u65E0\u6548");
34
+ return {
35
+ sessionId: id(value.sessionId),
36
+ hasEarlier: value.hasEarlier,
37
+ hasLater: value.hasLater,
38
+ messages: value.messages.map((row) => {
39
+ if (!["user", "assistant"].includes(row.role) || typeof row.truncated !== "boolean") throw new TypeError("\u6D88\u606F\u89D2\u8272\u6216\u622A\u65AD\u72B6\u6001\u65E0\u6548");
40
+ return { seq: sequence(row.seq), role: row.role, text: boundedString(row.text, 2e3), truncated: row.truncated };
41
+ })
42
+ };
43
+ } };
44
+ const detailsInputSchema = { parse(value) {
45
+ if (!value || !Array.isArray(value.sessionIds) || !value.sessionIds.length || value.sessionIds.length > 20) throw new TypeError("\u6BCF\u6B21\u987B\u5305\u542B 1\uFF5E20 \u4E2A\u4F1A\u8BDD");
46
+ return { sessionIds: [...new Set(value.sessionIds.map(id))] };
47
+ } };
48
+ const detailsResultSchema = { parse(value) {
49
+ if (!value || !Array.isArray(value.items) || value.items.length > 20) throw new TypeError("\u4F1A\u8BDD\u8BE6\u60C5\u65E0\u6548");
50
+ return { items: value.items.map((row) => ({ sessionId: id(row.sessionId), ...row.createdAt !== void 0 ? { createdAt: sequence(row.createdAt) } : {}, turnCount: row.turnCount === null ? null : sequence(row.turnCount), path: row.path === null ? null : boundedString(row.path, 32768), error: boundedString(row.error, 500) })) };
51
+ } };
52
+ function countConversationTurns(events) {
53
+ if (!Array.isArray(events)) throw new TypeError("\u4F1A\u8BDD\u65E5\u5FD7\u65E0\u6548");
54
+ return events.filter((event) => {
55
+ if (event.type !== "user/message") return false;
56
+ const message = event.data?.message ?? event.data;
57
+ return message && (!message.source?.kind || message.source.kind === "user");
58
+ }).length;
59
+ }
60
+ function compareTurnCounts(left, right, order) {
61
+ const a = Number.isSafeInteger(left) && left >= 0;
62
+ const b = Number.isSafeInteger(right) && right >= 0;
63
+ if (a !== b) return a ? -1 : 1;
64
+ return a ? order === "turnsAsc" ? left - right : right - left : 0;
65
+ }
66
+ async function copySessionText(text, clipboard = globalThis.navigator?.clipboard) {
67
+ if (typeof text !== "string" || !text) throw Object.assign(new Error("\u6CA1\u6709\u53EF\u590D\u5236\u7684\u5185\u5BB9"), { code: "copy.empty" });
68
+ if (!clipboard?.writeText) throw Object.assign(new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u526A\u8D34\u677F\uFF0C\u8BF7\u5728 HTTPS \u6216\u672C\u673A\u5730\u5740\u4E2D\u6253\u5F00\u9875\u9762"), { code: "copy.unavailable" });
69
+ try {
70
+ await clipboard.writeText(text);
71
+ } catch {
72
+ throw Object.assign(new Error("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u5141\u8BB8\u6D4F\u89C8\u5668\u8BBF\u95EE\u526A\u8D34\u677F\u540E\u91CD\u8BD5"), { code: "copy.denied" });
73
+ }
74
+ }
75
+ function discoveryInvocations() {
76
+ const codec = (name, schema) => ({ mode: "strict", typeSymbol: `@michengai/dsh-archive-manager/types#${name}`, create: () => schema, schema });
77
+ return [["searchSessionContent", searchInputSchema, searchResultSchema], ["diagnoseSession", repairInputSchema, repairResultSchema], ["repairSession", repairInputSchema, repairResultSchema], ["sessionDetails", detailsInputSchema, detailsResultSchema], ["searchArchivedContent", searchInputSchema, searchResultSchema], ["previewArchivedSession", previewInputSchema, previewResultSchema]].map(([method, input, output]) => ({
78
+ id: `@michengai/dsh-archive-manager#workspaceRegistry/${method}`,
79
+ service: "workspaceRegistry",
80
+ namespace: "workspaceRegistry",
81
+ method,
82
+ invocation: { kind: "direct" },
83
+ parameters: [{ name: "input", wire: "input", source: "json", codec: codec(method + "Input", input) }],
84
+ result: codec(method + "Result", output),
85
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
86
+ }));
87
+ }
88
+ function extractConversation(events) {
89
+ if (!Array.isArray(events)) throw new TypeError("\u4F1A\u8BDD\u65E5\u5FD7\u65E0\u6548");
90
+ const messages = [];
91
+ for (const event of events) {
92
+ if (event.type !== "user/message" && event.type !== "assistant/message") continue;
93
+ const message = event.data?.message ?? event.data;
94
+ const role = event.type === "user/message" ? "user" : "assistant";
95
+ if (role === "user" && message?.source?.kind && message.source.kind !== "user") continue;
96
+ const content = message?.content;
97
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n") : "";
98
+ if (text.trim()) messages.push({ seq: sequence(event.seq), role, text });
99
+ }
100
+ return messages;
101
+ }
102
+ function findContentMatch(messages, query) {
103
+ const needle = query.trim();
104
+ if (!needle) return null;
105
+ for (const message of messages) {
106
+ const match = findTextRange(message.text, needle);
107
+ if (!match) continue;
108
+ const start = Math.max(0, match.start - 70);
109
+ return { seq: message.seq, snippet: (start ? "\u2026" : "") + sliceWholeCharacters(message.text, start, start + 300) + (message.text.length > start + 300 ? "\u2026" : "") };
110
+ }
111
+ return null;
112
+ }
113
+ function previewConversation(messages, query) {
114
+ const needle = query.trim();
115
+ const hit = needle ? messages.findIndex((row) => findTextRange(row.text, needle)) : -1;
116
+ const start = hit >= 0 ? Math.max(0, hit - 2) : Math.max(0, messages.length - 8);
117
+ return { hasEarlier: start > 0, hasLater: start + 8 < messages.length, messages: messages.slice(start, start + 8).map((row) => {
118
+ const match = findTextRange(row.text, needle)?.start ?? -1;
119
+ const offset = match >= 2e3 ? Math.max(0, match - 300) : 0;
120
+ return { ...row, text: sliceWholeCharacters(row.text, offset, offset + 2e3), truncated: row.text.length > 2e3 };
121
+ }) };
122
+ }
123
+ function findTextRange(text, query) {
124
+ const needle = query?.trim().toLowerCase();
125
+ if (!needle) return null;
126
+ const position = text.toLowerCase().indexOf(needle);
127
+ if (position < 0) return null;
128
+ let original = 0, folded = 0, start;
129
+ for (const character of text) {
130
+ const next = folded + character.toLowerCase().length;
131
+ if (start === void 0 && next > position) start = original;
132
+ original += character.length;
133
+ if (next >= position + needle.length) return { start, end: original };
134
+ folded = next;
135
+ }
136
+ return null;
137
+ }
138
+ function sliceWholeCharacters(text, start, end) {
139
+ const splitsPair = (index) => index > 0 && /[\uD800-\uDBFF]/.test(text[index - 1]) && /[\uDC00-\uDFFF]/.test(text[index] ?? "");
140
+ if (splitsPair(start)) start++;
141
+ if (splitsPair(end)) end--;
142
+ return text.slice(start, end);
143
+ }
144
+ function sessionDetailCandidates(ids, byId) {
145
+ return [...new Set(ids)].map((id2) => byId[id2] ?? { id: id2 });
146
+ }
147
+ function dayBoundary(value, nextDay = false) {
148
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return NaN;
149
+ const [year, month, day] = value.split("-").map(Number);
150
+ const date = new Date(year, month - 1, day);
151
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return NaN;
152
+ if (nextDay) date.setDate(date.getDate() + 1);
153
+ return date.getTime();
154
+ }
155
+ function validUpdatedRange(from, to) {
156
+ return (!from || Number.isFinite(dayBoundary(from))) && (!to || Number.isFinite(dayBoundary(to))) && (!from || !to || from <= to);
157
+ }
158
+ function matchesUpdatedRange(value, from, to) {
159
+ if (!validUpdatedRange(from, to)) return false;
160
+ if (!from && !to) return true;
161
+ const time = typeof value === "number" ? value : Date.parse(value);
162
+ return Number.isFinite(time) && (!from || time >= dayBoundary(from)) && (!to || time < dayBoundary(to, true));
163
+ }
164
+ async function searchArchiveBatches(ids, query, call, { signal, onProgress } = {}) {
165
+ const items = [], failures = [];
166
+ const unique = [...new Set(ids)];
167
+ const check = () => {
168
+ if (signal?.aborted) throw new Error("\u641C\u7D22\u5DF2\u53D6\u6D88");
169
+ };
170
+ for (let start = 0; start < unique.length; start += 20) {
171
+ check();
172
+ const result = searchResultSchema.parse(await call({ sessionIds: unique.slice(start, start + 20), query }));
173
+ check();
174
+ items.push(...result.items);
175
+ failures.push(...result.failures);
176
+ onProgress?.({ done: Math.min(start + 20, unique.length), total: unique.length, items: [...items], failures: [...failures] });
177
+ }
178
+ check();
179
+ return { items, failures };
180
+ }
181
+ function classifySessionError(error) {
182
+ const message = String(error?.message ?? error);
183
+ const descriptions = {
184
+ "repair-frame": ["\u65E7\u7248\u4FEE\u590D\u751F\u6210\u7684\u538B\u7F29\u683C\u5F0F\u4E0D\u7B26\u5408\u5BBF\u4E3B\u8981\u6C42", "\u91CD\u65B0\u8BCA\u65AD\u53EF\u7EA0\u6B63\u538B\u7F29\u5E27\u683C\u5F0F\uFF0C\u539F\u59CB\u65E5\u5FD7\u4FDD\u6301\u4E0D\u53D8\u3002"],
185
+ "legacy-source": ["\u65E7\u7248\u6D88\u606F\u6765\u6E90\u4E0D\u53D7\u65B0\u7248 DSH \u652F\u6301", "\u8FD0\u884C\u8BCA\u65AD\uFF0C\u68C0\u67E5\u662F\u5426\u4E3A\u53EF\u8F6C\u6362\u7684\u65E7\u81EA\u52A8\u5316\u6765\u6E90\u3002"],
186
+ missing: ["\u4F1A\u8BDD\u65E5\u5FD7\u6216\u9644\u4EF6\u8DEF\u5F84\u4E0D\u5B58\u5728", "\u68C0\u67E5\u539F\u76EE\u5F55\u6216\u78C1\u76D8\u662F\u5426\u53EF\u7528\uFF1B\u7F3A\u5931\u6B63\u6587\u4E0D\u80FD\u901A\u8FC7\u91CD\u5EFA\u7F13\u5B58\u6062\u590D\u3002"],
187
+ permission: ["\u6CA1\u6709\u6743\u9650\u8BFB\u53D6\u4F1A\u8BDD\u6587\u4EF6", "\u68C0\u67E5\u76EE\u5F55\u8BBF\u95EE\u6743\u9650\u3001\u6587\u4EF6\u5360\u7528\u4E0E\u5B89\u5168\u8F6F\u4EF6\u9650\u5236\uFF0C\u518D\u91CD\u8BD5\u3002"],
188
+ corrupt: ["\u4F1A\u8BDD\u65E5\u5FD7\u53EF\u80FD\u635F\u574F\u6216\u672A\u5B8C\u6574\u5199\u5165", "\u4FDD\u7559\u73B0\u6709\u65E5\u5FD7\uFF0C\u68C0\u67E5\u78C1\u76D8\u548C\u539F\u59CB\u6587\u4EF6\uFF1B\u4E0D\u81EA\u52A8\u622A\u65AD\u6216\u5220\u9664\u6D88\u606F\u3002"],
189
+ unknown: ["\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6", "\u5148\u91CD\u8BD5\uFF1B\u4ECD\u5931\u8D25\u65F6\u5C55\u5F00\u6280\u672F\u8BE6\u60C5\uFF0C\u6309\u5177\u4F53\u9519\u8BEF\u68C0\u67E5\u5BBF\u4E3B\u517C\u5BB9\u6027\u3002"]
190
+ };
191
+ const codes = { ENOENT: "missing", EACCES: "permission", EPERM: "permission" };
192
+ const patterns = [
193
+ ["repair-frame", /first frame is not exactly one header line/],
194
+ ["legacy-source", /unclassified message source/],
195
+ ["missing", /ENOENT|不存在|not found/i],
196
+ ["permission", /EACCES|EPERM|permission/i],
197
+ ["corrupt", /corrupt|JSON|incomplete|truncat|损坏/i]
198
+ ];
199
+ const explicit = Object.hasOwn(codes, error?.code) ? codes[error.code] : error?.code;
200
+ const code = Object.hasOwn(descriptions, explicit) ? explicit : patterns.find(([, pattern]) => pattern.test(message))?.[0] ?? "unknown";
201
+ const [reason, advice] = descriptions[code];
202
+ return { code, reason, advice };
203
+ }
204
+ const repairInputSchema = { parse(value) {
205
+ const sessionId = id(value?.sessionId);
206
+ if (value?.token !== void 0 && !/^[a-f0-9]{64}$/.test(value.token)) throw new TypeError("\u4FEE\u590D\u51ED\u636E\u65E0\u6548");
207
+ return { sessionId, ...value.token !== void 0 ? { token: value.token } : {} };
208
+ } };
209
+ const repairResultSchema = { parse(value) {
210
+ if (!value || typeof value.repairable !== "boolean" || typeof value.repaired !== "boolean") throw new TypeError("\u8BCA\u65AD\u7ED3\u679C\u65E0\u6548");
211
+ return { ...value.code !== void 0 ? { code: boundedString(value.code, 64) } : {}, sessionId: id(value.sessionId), repairable: value.repairable, repaired: value.repaired, reason: boundedString(value.reason, 1e3), advice: boundedString(value.advice, 1e3), token: boundedString(value.token ?? "", 64), count: sequence(value.count ?? 0) };
212
+ } };
213
+ export {
214
+ classifySessionError,
215
+ compareTurnCounts,
216
+ copySessionText,
217
+ countConversationTurns,
218
+ detailsInputSchema,
219
+ detailsResultSchema,
220
+ discoveryInvocations,
221
+ extractConversation,
222
+ findContentMatch,
223
+ findTextRange,
224
+ matchesUpdatedRange,
225
+ previewConversation,
226
+ previewInputSchema,
227
+ previewResultSchema,
228
+ repairInputSchema,
229
+ repairResultSchema,
230
+ searchArchiveBatches,
231
+ searchInputSchema,
232
+ searchResultSchema,
233
+ sessionDetailCandidates,
234
+ validUpdatedRange
235
+ };
@@ -0,0 +1,105 @@
1
+ function checkedId(value) {
2
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) throw new TypeError("\u4F1A\u8BDD ID \u957F\u5EA6\u987B\u4E3A 1\uFF5E1024 \u4E2A\u5B57\u7B26");
3
+ return value;
4
+ }
5
+ const favoriteInputSchema = {
6
+ parse(value) {
7
+ if (!value || typeof value !== "object" || typeof value.favorite !== "boolean") throw new TypeError("\u6536\u85CF\u72B6\u6001\u5FC5\u987B\u4E3A\u5E03\u5C14\u503C");
8
+ return { sessionId: checkedId(value.sessionId), favorite: value.favorite };
9
+ }
10
+ };
11
+ const favoriteStateSchema = {
12
+ parse(value) {
13
+ if (!value || !Array.isArray(value.favoriteSessionIds) || value.favoriteSessionIds.length > 1e5) throw new TypeError("\u6536\u85CF\u96C6\u5408\u65E0\u6548");
14
+ return { favoriteSessionIds: [...new Set(value.favoriteSessionIds.map(checkedId))] };
15
+ },
16
+ safeParse(value) {
17
+ try {
18
+ return { success: true, data: this.parse(value) };
19
+ } catch (error) {
20
+ return { success: false, error };
21
+ }
22
+ }
23
+ };
24
+ function favoriteInvocations() {
25
+ const codec = (name, schema) => ({ mode: "strict", typeSymbol: `@michengai/dsh-archive-manager/types#${name}`, create: () => schema, schema });
26
+ return ["favoriteSessions", "setSessionFavorite"].map((method) => ({
27
+ id: `@michengai/dsh-archive-manager#workspaceRegistry/${method}`,
28
+ service: "workspaceRegistry",
29
+ namespace: "workspaceRegistry",
30
+ method,
31
+ invocation: { kind: "direct" },
32
+ parameters: method === "favoriteSessions" ? [] : [{ name: "input", wire: "input", source: "json", codec: codec("FavoriteInput", favoriteInputSchema) }],
33
+ result: codec("FavoriteState", favoriteStateSchema),
34
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
35
+ }));
36
+ }
37
+ function idleArchiveCandidate(session, { days, now = Date.now(), favorites = /* @__PURE__ */ new Set(), currentId, pending = /* @__PURE__ */ new Map() }) {
38
+ if (!session || !Number.isInteger(days) || days < 1 || days > 36500) return false;
39
+ if (favorites.has(session.id) || session.id === currentId || session.retainedBy?.mainView > 0) return false;
40
+ if (session.running || session.runningSubagentCount > 0 || session.pendingInteraction != null || pending.has(session.id)) return false;
41
+ const updated = typeof session.updatedAt === "number" ? session.updatedAt : Date.parse(session.updatedAt);
42
+ return Number.isFinite(updated) && updated <= now - days * 864e5;
43
+ }
44
+ async function runSessionBatch(ids, operate, { onProgress, refresh } = {}) {
45
+ const requested = [...new Set(ids.map(checkedId))];
46
+ const result = { requested, succeeded: [], skipped: [], failures: [], unprocessed: [], remaining: [], refreshError: null };
47
+ const report = () => onProgress?.({ total: requested.length, done: result.succeeded.length + result.skipped.length + result.failures.length, succeeded: result.succeeded.length, skipped: result.skipped.length, failed: result.failures.length });
48
+ report();
49
+ for (const [index, id] of requested.entries()) {
50
+ try {
51
+ (await operate(id) ? result.succeeded : result.skipped).push(id);
52
+ } catch (error) {
53
+ result.failures.push({ sessionId: id, message: error instanceof Error ? error.message : String(error) });
54
+ result.unprocessed = requested.slice(index + 1);
55
+ result.remaining = requested.slice(index);
56
+ report();
57
+ break;
58
+ }
59
+ report();
60
+ }
61
+ try {
62
+ await refresh?.();
63
+ } catch (error) {
64
+ result.refreshError = error instanceof Error ? error.message : String(error);
65
+ }
66
+ return result;
67
+ }
68
+ function createSessionOrganizer({ workspaces, sessions, archive, restore, deleteOne, getFavorites, refresh, currentSessionId }) {
69
+ return async (kind, ids, options = {}) => {
70
+ if (!["archive", "restore", "undo", "delete"].includes(kind)) throw new TypeError("\u672A\u77E5\u6279\u91CF\u64CD\u4F5C");
71
+ return runSessionBatch(ids, async (id) => {
72
+ const before = workspaces.getSnapshot();
73
+ const archived = before.archivedSessionIds.includes(id);
74
+ if (kind === "archive") {
75
+ if (archived) return false;
76
+ const snapshot = sessions.getSnapshot();
77
+ const session = snapshot.byId[id];
78
+ if (!session) return false;
79
+ if (options.idleDays !== void 0) {
80
+ const favorites = new Set((await getFavorites()).favoriteSessionIds);
81
+ const latest = sessions.getSnapshot();
82
+ if (!idleArchiveCandidate(latest.byId[id], { days: options.idleDays, favorites, currentId: currentSessionId(latest), pending: options.getPending?.() ?? options.pending })) return false;
83
+ }
84
+ await archive(id);
85
+ return workspaces.getSnapshot().archivedSessionIds.includes(id);
86
+ }
87
+ if (!archived) return false;
88
+ if (kind === "delete") {
89
+ await deleteOne(id);
90
+ return true;
91
+ }
92
+ const value = await restore(id);
93
+ const remaining = value?.archivedSessionIds ?? workspaces.getSnapshot().archivedSessionIds;
94
+ return !remaining.includes(id);
95
+ }, { onProgress: options.onProgress, refresh });
96
+ };
97
+ }
98
+ export {
99
+ createSessionOrganizer,
100
+ favoriteInputSchema,
101
+ favoriteInvocations,
102
+ favoriteStateSchema,
103
+ idleArchiveCandidate,
104
+ runSessionBatch
105
+ };