dsh-plugin-tool-management 0.1.2 → 0.1.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.
- package/README.md +43 -13
- package/README_EN.md +43 -13
- package/cordis.patch.yml +18 -0
- package/lib/agents-md/service.js +240 -0
- package/lib/client.js +637 -30
- package/lib/history/projcache.js +335 -0
- package/lib/history/tombstone.js +32 -0
- package/lib/history/workspace.js +892 -0
- package/lib/imports/parsers.js +181 -0
- package/lib/index.js +651 -1
- package/package.json +14 -3
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// src/imports/parsers.js — transcript parsing for conversation import.
|
|
2
|
+
// Pure Node, no DSH dependencies: detect format, then convert a Claude Code /
|
|
3
|
+
// Cursor JSONL, a Codex-style Markdown transcript, or generic prefixed text
|
|
4
|
+
// into an ordered list of `{ role: 'user' | 'assistant', text }` turns.
|
|
5
|
+
/** Heading lines that begin a Markdown turn (`## User` / `### Assistant` …). */
|
|
6
|
+
const HEADING_RE = /^#{1,3}\s+(User|Human|Assistant|AI|Bot)\s*:?\s*$/i;
|
|
7
|
+
/** Bold-prefix lines (`**User:** …` / `**User**: …`). */
|
|
8
|
+
const BOLD_PREFIX_RE = /^\*\*(User|Human|Assistant|AI|Bot):?\*\*\s*:?\s*(.*)$/i;
|
|
9
|
+
/** Generic prefixed lines (`User: …` / `Assistant: …` / `用户:…`). */
|
|
10
|
+
const GENERIC_PREFIX_RE = /^(User|Human|用户|Assistant|AI|Bot|助手)\s*[::]\s*(.*)$/i;
|
|
11
|
+
/** Flatten a message content value (string, text-block array, or nested object) to text. */
|
|
12
|
+
export function extractText(content) {
|
|
13
|
+
if (typeof content === 'string')
|
|
14
|
+
return content;
|
|
15
|
+
if (Array.isArray(content)) {
|
|
16
|
+
let out = '';
|
|
17
|
+
for (const part of content) {
|
|
18
|
+
if (typeof part === 'string')
|
|
19
|
+
out += part;
|
|
20
|
+
else if (part && typeof part === 'object') {
|
|
21
|
+
if (typeof part.text === 'string')
|
|
22
|
+
out += part.text;
|
|
23
|
+
else if (typeof part.content === 'string')
|
|
24
|
+
out += part.content;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
if (content && typeof content === 'object') {
|
|
30
|
+
if (typeof content.text === 'string')
|
|
31
|
+
return content.text;
|
|
32
|
+
if (typeof content.content === 'string')
|
|
33
|
+
return content.content;
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
/** Push a finished turn, trimming empty bodies. */
|
|
38
|
+
function flushTurn(current, turns) {
|
|
39
|
+
if (!current)
|
|
40
|
+
return;
|
|
41
|
+
const text = String(current.text || '').trim();
|
|
42
|
+
if (text)
|
|
43
|
+
turns.push({ role: current.role, text });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Detect the transcript format. Extension wins; content sniffing only applies
|
|
47
|
+
* when the name carries no recognized extension.
|
|
48
|
+
* @returns {'jsonl' | 'markdown' | 'generic'}
|
|
49
|
+
*/
|
|
50
|
+
export function detectFormat(fileName, content) {
|
|
51
|
+
const name = String(fileName || '').toLowerCase();
|
|
52
|
+
if (/\.jsonl$/.test(name))
|
|
53
|
+
return 'jsonl';
|
|
54
|
+
if (/\.(md|markdown)$/.test(name))
|
|
55
|
+
return 'markdown';
|
|
56
|
+
if (/\.txt$/.test(name))
|
|
57
|
+
return 'generic';
|
|
58
|
+
const first = String(content || '').split(/\r?\n/).map((s) => s.trim()).find((s) => s) || '';
|
|
59
|
+
if (first.startsWith('{')) {
|
|
60
|
+
try {
|
|
61
|
+
const obj = JSON.parse(first);
|
|
62
|
+
if (obj && typeof obj === 'object' && (obj.type === 'user' || obj.type === 'assistant' || obj.role === 'user' || obj.role === 'assistant'))
|
|
63
|
+
return 'jsonl';
|
|
64
|
+
}
|
|
65
|
+
catch (e) { /* not JSONL after all */ }
|
|
66
|
+
}
|
|
67
|
+
if (HEADING_RE.test(first) || BOLD_PREFIX_RE.test(first))
|
|
68
|
+
return 'markdown';
|
|
69
|
+
return 'generic';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse a Claude Code / Cursor style JSONL transcript.
|
|
73
|
+
* Recognizes `{type:'user'|'assistant', message:{role,content}}` (Claude Code)
|
|
74
|
+
* and `{role, content}` (Cursor-like). System/tool lines are skipped; adjacent
|
|
75
|
+
* turns with the same role are merged.
|
|
76
|
+
* @returns {Array<{role:'user'|'assistant', text:string}>}
|
|
77
|
+
*/
|
|
78
|
+
export function parseJsonlTranscript(text) {
|
|
79
|
+
const turns = [];
|
|
80
|
+
let lastRole = null;
|
|
81
|
+
let lastText = '';
|
|
82
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
83
|
+
const line = raw.trim();
|
|
84
|
+
if (!line)
|
|
85
|
+
continue;
|
|
86
|
+
let obj;
|
|
87
|
+
try {
|
|
88
|
+
obj = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
94
|
+
continue;
|
|
95
|
+
let role = null;
|
|
96
|
+
let content;
|
|
97
|
+
if (obj.type === 'user' || obj.type === 'assistant') {
|
|
98
|
+
const message = obj.message && typeof obj.message === 'object' && !Array.isArray(obj.message) ? obj.message : null;
|
|
99
|
+
role = message && (message.role === 'user' || message.role === 'assistant') ? message.role : (obj.type === 'user' ? 'user' : 'assistant');
|
|
100
|
+
content = message ? message.content : undefined;
|
|
101
|
+
}
|
|
102
|
+
else if (obj.role === 'user' || obj.role === 'assistant') {
|
|
103
|
+
role = obj.role;
|
|
104
|
+
content = obj.content;
|
|
105
|
+
}
|
|
106
|
+
if (!role)
|
|
107
|
+
continue;
|
|
108
|
+
const piece = extractText(content).trim();
|
|
109
|
+
if (!piece)
|
|
110
|
+
continue;
|
|
111
|
+
if (role === lastRole) {
|
|
112
|
+
lastText += '\n' + piece;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (lastText)
|
|
116
|
+
turns.push({ role: lastRole, text: lastText });
|
|
117
|
+
lastRole = role;
|
|
118
|
+
lastText = piece;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (lastText)
|
|
122
|
+
turns.push({ role: lastRole, text: lastText });
|
|
123
|
+
return turns;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse a Markdown transcript into turns by heading or bold-prefix markers
|
|
127
|
+
* (`## User`, `### Assistant`, `**User:**`, …). Content lines accumulate under
|
|
128
|
+
* the current turn until the next marker.
|
|
129
|
+
*/
|
|
130
|
+
export function parseMarkdownTranscript(text) {
|
|
131
|
+
const turns = [];
|
|
132
|
+
let current = null;
|
|
133
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
134
|
+
const heading = line.match(HEADING_RE);
|
|
135
|
+
if (heading) {
|
|
136
|
+
flushTurn(current, turns);
|
|
137
|
+
current = { role: isAssistantLabel(heading[1]) ? 'assistant' : 'user', text: '' };
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const bold = line.match(BOLD_PREFIX_RE);
|
|
141
|
+
if (bold) {
|
|
142
|
+
flushTurn(current, turns);
|
|
143
|
+
current = { role: isAssistantLabel(bold[1]) ? 'assistant' : 'user', text: String(bold[2] || '').trim() };
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (current)
|
|
147
|
+
current.text += (current.text ? '\n' : '') + line;
|
|
148
|
+
}
|
|
149
|
+
flushTurn(current, turns);
|
|
150
|
+
return turns;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parse generic prefixed text. Lines starting with `User:` / `Assistant:` /
|
|
154
|
+
* `用户:` / `助手:` begin a new turn; everything else appends to the current
|
|
155
|
+
* one. When no marker is present the whole text becomes one user turn.
|
|
156
|
+
*/
|
|
157
|
+
export function parseGenericText(text) {
|
|
158
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
159
|
+
if (!lines.some((line) => GENERIC_PREFIX_RE.test(line.trim()))) {
|
|
160
|
+
const whole = String(text || '').trim();
|
|
161
|
+
return whole ? [{ role: 'user', text: whole }] : [];
|
|
162
|
+
}
|
|
163
|
+
const turns = [];
|
|
164
|
+
let current = null;
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
const m = line.match(GENERIC_PREFIX_RE);
|
|
167
|
+
if (m) {
|
|
168
|
+
flushTurn(current, turns);
|
|
169
|
+
current = { role: isAssistantLabel(m[1]) ? 'assistant' : 'user', text: String(m[2] || '').trim() };
|
|
170
|
+
}
|
|
171
|
+
else if (current) {
|
|
172
|
+
current.text += (current.text ? '\n' : '') + line;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
flushTurn(current, turns);
|
|
176
|
+
return turns;
|
|
177
|
+
}
|
|
178
|
+
/** Map a speaker label to the assistant role (AI / Bot / Assistant / 助手). */
|
|
179
|
+
function isAssistantLabel(label) {
|
|
180
|
+
return /^(assistant|ai|bot|助手)$/i.test(label);
|
|
181
|
+
}
|