dsh-cursor-dev 0.0.0-stage → 0.11.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 +263 -2
- package/cordis.patch.yml +12 -0
- package/docs/INSTALL.md +219 -0
- package/lib/changes.js +192 -0
- package/lib/cli.js +237 -0
- package/lib/client.js +626 -0
- package/lib/command.js +271 -0
- package/lib/conversations.js +101 -0
- package/lib/cursor-session.js +871 -0
- package/lib/fence.js +123 -0
- package/lib/index.js +477 -0
- package/lib/journal.js +276 -0
- package/lib/mirror.js +1293 -0
- package/lib/models.js +314 -0
- package/lib/native-provider.js +224 -0
- package/lib/prompt.js +59 -0
- package/lib/proxy.js +414 -0
- package/lib/resolve-agent.js +251 -0
- package/lib/session-pool.js +927 -0
- package/lib/settings.js +642 -0
- package/lib/tool.js +747 -0
- package/lib/usage.js +2262 -0
- package/package.json +60 -4
- package/screenshots/panel-zh-settings.png +0 -0
- package/screenshots/panel-zh.png +0 -0
package/lib/journal.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run event journal.
|
|
3
|
+
*
|
|
4
|
+
* The ACP stream is rich (reasoning, tool calls, diffs, permissions) but the
|
|
5
|
+
* parent model must not pay for all of it: the tool result stays compact while
|
|
6
|
+
* the full journal stays available for inspection. Each key keeps a bounded
|
|
7
|
+
* ring so a long session cannot grow without limit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Events kept per key. */
|
|
11
|
+
const CAPACITY = 600;
|
|
12
|
+
|
|
13
|
+
/** Floor for the retention window, so a typo cannot evict a running session. */
|
|
14
|
+
const MIN_TTL_MS = 10 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
/** Shortest gap between retention sweeps. */
|
|
17
|
+
const SWEEP_INTERVAL_MS = 60 * 1000;
|
|
18
|
+
|
|
19
|
+
/** One journal entry. */
|
|
20
|
+
let sequence = 0;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bounded, queryable record of everything a Cursor session did.
|
|
24
|
+
*/
|
|
25
|
+
export class RunJournal {
|
|
26
|
+
#entries = new Map();
|
|
27
|
+
|
|
28
|
+
#ttlMs;
|
|
29
|
+
|
|
30
|
+
#lastSweep = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param options - `ttlMs` retention window (default 60 min, min 10 min).
|
|
34
|
+
*/
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.#ttlMs = Math.max(MIN_TTL_MS, options.ttlMs ?? 60 * 60 * 1000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Retention window in milliseconds. */
|
|
40
|
+
get ttlMs() {
|
|
41
|
+
return this.#ttlMs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Append one normalized event.
|
|
46
|
+
*
|
|
47
|
+
* @param key - pool key identifying the conversation.
|
|
48
|
+
* @param event - normalized event from `CursorSession`.
|
|
49
|
+
*/
|
|
50
|
+
append(key, event) {
|
|
51
|
+
const bucket = this.#bucket(key);
|
|
52
|
+
bucket.updatedAt = Date.now();
|
|
53
|
+
bucket.events.push({ seq: ++sequence, time: Date.now(), ...event });
|
|
54
|
+
if (bucket.events.length > CAPACITY)
|
|
55
|
+
bucket.events.splice(0, bucket.events.length - CAPACITY);
|
|
56
|
+
this.#sweep();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Record run-level facts for a key.
|
|
61
|
+
*
|
|
62
|
+
* @param key - pool key.
|
|
63
|
+
* @param patch - fields to merge.
|
|
64
|
+
*/
|
|
65
|
+
annotate(key, patch) {
|
|
66
|
+
const bucket = this.#bucket(key);
|
|
67
|
+
bucket.updatedAt = Date.now();
|
|
68
|
+
Object.assign(bucket, patch);
|
|
69
|
+
this.#sweep();
|
|
70
|
+
return bucket;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read one key's journal.
|
|
75
|
+
*
|
|
76
|
+
* @param key - pool key.
|
|
77
|
+
*/
|
|
78
|
+
read(key) {
|
|
79
|
+
return this.#entries.get(key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Drop one key's journal.
|
|
84
|
+
*
|
|
85
|
+
* @param key - pool key.
|
|
86
|
+
*/
|
|
87
|
+
clear(key) {
|
|
88
|
+
this.#entries.delete(key);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read the newest journal belonging to one conversation.
|
|
93
|
+
*
|
|
94
|
+
* Buckets are keyed by the generation-scoped pool key (`parent::label#2`),
|
|
95
|
+
* but the durable conversation index — and therefore the dashboard's
|
|
96
|
+
* 历史会话 table — only knows the conversation key (`parent::label`). A row
|
|
97
|
+
* that links by the durable key would ask for a bucket that never existed,
|
|
98
|
+
* so the lookup walks the generations and answers with the most recent one
|
|
99
|
+
* that still has a journal.
|
|
100
|
+
*
|
|
101
|
+
* @param conversationKey - `parent::label`.
|
|
102
|
+
* @returns `{key, bucket}` for the newest matching generation, else undefined.
|
|
103
|
+
*/
|
|
104
|
+
readConversation(conversationKey) {
|
|
105
|
+
const prefix = `${conversationKey}#`;
|
|
106
|
+
let found;
|
|
107
|
+
for (const [key, bucket] of this.#entries) {
|
|
108
|
+
if (!key.startsWith(prefix))
|
|
109
|
+
continue;
|
|
110
|
+
if (found === undefined || (bucket.updatedAt ?? bucket.startedAt) > (found.bucket.updatedAt ?? found.bucket.startedAt))
|
|
111
|
+
found = { key, bucket };
|
|
112
|
+
}
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Counts for inspection surfaces (the dashboard, the status route).
|
|
118
|
+
*/
|
|
119
|
+
stats() {
|
|
120
|
+
let events = 0;
|
|
121
|
+
for (const bucket of this.#entries.values())
|
|
122
|
+
events += bucket.events.length;
|
|
123
|
+
return { keys: this.#entries.size, events, ttlMs: this.#ttlMs };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Get or create a key's bucket. */
|
|
127
|
+
#bucket(key) {
|
|
128
|
+
let bucket = this.#entries.get(key);
|
|
129
|
+
if (bucket === undefined) {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
bucket = { events: [], startedAt: now, updatedAt: now, task: undefined, cursorSessionId: undefined };
|
|
132
|
+
this.#entries.set(key, bucket);
|
|
133
|
+
}
|
|
134
|
+
return bucket;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Drop journals nobody has touched inside the retention window. Runs at most
|
|
139
|
+
* once a minute and is driven by writes, so it costs nothing when idle.
|
|
140
|
+
*/
|
|
141
|
+
#sweep() {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
if (now - this.#lastSweep < SWEEP_INTERVAL_MS)
|
|
144
|
+
return;
|
|
145
|
+
this.#lastSweep = now;
|
|
146
|
+
const cutoff = now - this.#ttlMs;
|
|
147
|
+
for (const [key, bucket] of this.#entries) {
|
|
148
|
+
if ((bucket.updatedAt ?? bucket.startedAt) < cutoff)
|
|
149
|
+
this.#entries.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reduce a key's journal to the token-cheap signals a parent model benefits
|
|
155
|
+
* from: what tools ran, in what order, and how they ended.
|
|
156
|
+
*
|
|
157
|
+
* @param key - pool key.
|
|
158
|
+
* @param limit - maximum steps reported.
|
|
159
|
+
*/
|
|
160
|
+
timeline(key, limit = 40) {
|
|
161
|
+
const bucket = this.#entries.get(key);
|
|
162
|
+
if (bucket === undefined)
|
|
163
|
+
return [];
|
|
164
|
+
const steps = [];
|
|
165
|
+
const index = new Map();
|
|
166
|
+
for (const event of bucket.events) {
|
|
167
|
+
if (event.kind === 'tool-call') {
|
|
168
|
+
const call = event.call;
|
|
169
|
+
// A malformed event must not take down the caller: this runs
|
|
170
|
+
// inside an HTTP handler, where a throw is a process-level
|
|
171
|
+
// unhandled exception rather than a 500.
|
|
172
|
+
if (call === undefined || call.id === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
index.set(call.id, steps.length);
|
|
175
|
+
steps.push({
|
|
176
|
+
id: call.id,
|
|
177
|
+
name: call.name,
|
|
178
|
+
kind: call.kind,
|
|
179
|
+
path: call.path,
|
|
180
|
+
status: call.status,
|
|
181
|
+
diffCount: 0,
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (event.kind === 'tool-update') {
|
|
186
|
+
const call = event.call;
|
|
187
|
+
if (call === undefined || call.id === undefined)
|
|
188
|
+
continue;
|
|
189
|
+
const position = index.get(call.id);
|
|
190
|
+
if (position === undefined)
|
|
191
|
+
continue;
|
|
192
|
+
const step = steps[position];
|
|
193
|
+
step.status = call.status;
|
|
194
|
+
step.name = call.name ?? step.name;
|
|
195
|
+
if (call.path !== undefined)
|
|
196
|
+
step.path = call.path;
|
|
197
|
+
step.diffCount += typeof event.diffCount === 'number' ? event.diffCount : 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return steps.slice(0, limit);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether a key has recorded anything.
|
|
205
|
+
*
|
|
206
|
+
* @param key - pool key.
|
|
207
|
+
*/
|
|
208
|
+
has(key) {
|
|
209
|
+
return this.#entries.has(key);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The newest thing a run did, in one line.
|
|
214
|
+
*
|
|
215
|
+
* This is the answer to "what is the child doing right now" for a table row,
|
|
216
|
+
* where fetching the whole journal per row would be wasteful. The newest
|
|
217
|
+
* event that carries meaning for a reader is used: a tool call, a tool
|
|
218
|
+
* result, a message, a thought. Bookkeeping events (turn-start, mode) are
|
|
219
|
+
* skipped rather than reported as activity.
|
|
220
|
+
*
|
|
221
|
+
* @param key - pool key.
|
|
222
|
+
* @param maxChars - cap for the returned text.
|
|
223
|
+
*/
|
|
224
|
+
lastActivity(key, maxChars = 160) {
|
|
225
|
+
const bucket = this.#entries.get(key);
|
|
226
|
+
if (bucket === undefined)
|
|
227
|
+
return undefined;
|
|
228
|
+
for (let position = bucket.events.length - 1; position >= 0; position -= 1) {
|
|
229
|
+
const event = bucket.events[position];
|
|
230
|
+
const text = describeEvent(event);
|
|
231
|
+
if (text === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
return {
|
|
234
|
+
seq: event.seq,
|
|
235
|
+
time: event.time,
|
|
236
|
+
kind: event.kind,
|
|
237
|
+
text: text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* One line of human-readable activity for an event, or `undefined` when the
|
|
246
|
+
* event says nothing about what the run is doing.
|
|
247
|
+
*
|
|
248
|
+
* @param event - normalized journal event.
|
|
249
|
+
*/
|
|
250
|
+
function describeEvent(event) {
|
|
251
|
+
const call = event?.call;
|
|
252
|
+
switch (event?.kind) {
|
|
253
|
+
case 'tool-call':
|
|
254
|
+
return call === undefined
|
|
255
|
+
? undefined
|
|
256
|
+
: `${call.name ?? 'tool'} ${call.path ?? call.arguments ?? ''}`.trim();
|
|
257
|
+
case 'tool-update':
|
|
258
|
+
return call === undefined
|
|
259
|
+
? undefined
|
|
260
|
+
: `${call.name ?? 'tool'} → ${call.status ?? 'running'}${call.path === undefined ? '' : ` ${call.path}`}`;
|
|
261
|
+
case 'text':
|
|
262
|
+
return typeof event.text === 'string' ? event.text : undefined;
|
|
263
|
+
case 'thought':
|
|
264
|
+
return typeof event.text === 'string' ? `💭 ${event.text}` : undefined;
|
|
265
|
+
case 'diff':
|
|
266
|
+
return `diff ${event.diff?.path ?? ''}`.trim();
|
|
267
|
+
case 'permission':
|
|
268
|
+
return `permission ${event.kindOf ?? ''}`.trim();
|
|
269
|
+
case 'plan':
|
|
270
|
+
return 'plan update';
|
|
271
|
+
case 'turn-end':
|
|
272
|
+
return `turn ${event.index ?? ''} ${event.stopReason ?? ''}`.trim();
|
|
273
|
+
default:
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|