@ahmednawaz/crank 0.1.2
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/.env.example +53 -0
- package/README.md +266 -0
- package/bin/crank.js +737 -0
- package/bookmarklet/crank-bookmarklet.js +2295 -0
- package/bookmarklet/crank-ui-helpers.js +355 -0
- package/bookmarklet/install-snippet.txt +5 -0
- package/bookmarklet/text-source.js +239 -0
- package/companion/agent-queue.js +485 -0
- package/companion/agent-runner.js +334 -0
- package/companion/bin/crank.js +69 -0
- package/companion/dev-loader.js +39 -0
- package/companion/glean-client.js +991 -0
- package/companion/package.json +18 -0
- package/companion/persist-apply.js +189 -0
- package/companion/selection-store.js +175 -0
- package/companion/server.js +1147 -0
- package/companion/text-dispatch.js +419 -0
- package/companion/vizpatch-bridge.js +49 -0
- package/lib/copy-labels.js +86 -0
- package/lib/detect.js +88 -0
- package/lib/env.js +23 -0
- package/lib/init.js +435 -0
- package/lib/load-team-env.js +43 -0
- package/lib/resolve-project.js +203 -0
- package/lib/team-auth.js +82 -0
- package/lib/urls.js +164 -0
- package/mcp-server/index.js +174 -0
- package/mcp-server/package.json +16 -0
- package/mcp-server/tools.js +480 -0
- package/package.json +57 -0
- package/panel/demo.html +62 -0
- package/skill/SKILL.md +60 -0
- package/skills/README.md +16 -0
- package/skills/copy-guidance.md +25 -0
- package/team.env +4 -0
- package/vite.js +75 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pending Crank copy changes queued from the panel "Agent" button.
|
|
5
|
+
* Retune-parity: store structured old→new mappings; MCP tools pull/format/watch/clear.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { unitDisplayLabel } = require('../lib/copy-labels');
|
|
10
|
+
|
|
11
|
+
/** @type {object|null} */
|
|
12
|
+
let pending = null;
|
|
13
|
+
|
|
14
|
+
/** @type {Array<{ sinceId: string|null, resolve: Function, timer: NodeJS.Timeout }>} */
|
|
15
|
+
const watchers = [];
|
|
16
|
+
|
|
17
|
+
function roleLabel(role, node, index, nodes) {
|
|
18
|
+
if (node && node.label) return node.label;
|
|
19
|
+
if (node && nodes) return unitDisplayLabel(node, index, nodes);
|
|
20
|
+
const r = String(role || 'text').toLowerCase();
|
|
21
|
+
if (r === 'heading') return 'Heading';
|
|
22
|
+
if (r === 'action' || r === 'button') return 'CTA';
|
|
23
|
+
if (r === 'label') return 'Label';
|
|
24
|
+
return 'Copy';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findSessionNode(sessionNodes, vn) {
|
|
28
|
+
if (!vn) return null;
|
|
29
|
+
if (vn.id) {
|
|
30
|
+
const byId = sessionNodes.find((n) => n && String(n.id) === String(vn.id));
|
|
31
|
+
if (byId) return byId;
|
|
32
|
+
}
|
|
33
|
+
if (vn.path != null) {
|
|
34
|
+
const byPath = sessionNodes.find(
|
|
35
|
+
(n) => n && String(n.path) === String(vn.path)
|
|
36
|
+
);
|
|
37
|
+
if (byPath) return byPath;
|
|
38
|
+
}
|
|
39
|
+
if (vn.selector) {
|
|
40
|
+
return (
|
|
41
|
+
sessionNodes.find((n) => n && n.selector && n.selector === vn.selector) ||
|
|
42
|
+
null
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build an executable prompt the Cursor agent can follow to update source.
|
|
50
|
+
* Always lists per-string old→new pairs (one row per copy unit).
|
|
51
|
+
* @param {{ session: object, variant: object }} opts
|
|
52
|
+
*/
|
|
53
|
+
function buildAgentPrompt({ session, variant }) {
|
|
54
|
+
const lines = [
|
|
55
|
+
'Apply this Crank copy change to the product source files.',
|
|
56
|
+
'',
|
|
57
|
+
'Context:',
|
|
58
|
+
`- Page: ${session?.pageUrl || '(unknown)'}`,
|
|
59
|
+
`- Selection selector: ${session?.selector || '(unknown)'}`,
|
|
60
|
+
`- Variant: ${variant?.label || variant?.id || '(unnamed)'}`,
|
|
61
|
+
'',
|
|
62
|
+
'Update each string below independently (old → new). Do not reuse one New value for every field.',
|
|
63
|
+
'Prefer hardcoded JSX/HTML string literals; do not invent i18n keys. Skip dynamic/bound/API-driven values.',
|
|
64
|
+
'',
|
|
65
|
+
'Per-string mapping:'
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const changes = [];
|
|
69
|
+
const sessionNodes = Array.isArray(session?.nodes) ? session.nodes : [];
|
|
70
|
+
const variantNodes = Array.isArray(variant?.nodes) ? variant.nodes : [];
|
|
71
|
+
|
|
72
|
+
// Primary: node-aligned mapping (id/path) — never collapse to a single blob.
|
|
73
|
+
if (variantNodes.length) {
|
|
74
|
+
for (let vi = 0; vi < variantNodes.length; vi++) {
|
|
75
|
+
const vn = variantNodes[vi];
|
|
76
|
+
const sn = findSessionNode(sessionNodes, vn);
|
|
77
|
+
const oldText =
|
|
78
|
+
sn?.originalText ?? sn?.previousText ?? sn?.text ?? '';
|
|
79
|
+
const newText = vn.text != null ? vn.text : '';
|
|
80
|
+
if (String(oldText) === String(newText)) continue;
|
|
81
|
+
const snIndex = sn
|
|
82
|
+
? sessionNodes.findIndex((n) => n && String(n.path) === String(sn.path))
|
|
83
|
+
: vi;
|
|
84
|
+
changes.push({
|
|
85
|
+
id: vn.id || sn?.id || `copy-${vn.path}`,
|
|
86
|
+
role: vn.role || sn?.role || 'text',
|
|
87
|
+
label: roleLabel(
|
|
88
|
+
vn.role || sn?.role,
|
|
89
|
+
sn || vn,
|
|
90
|
+
snIndex >= 0 ? snIndex : vi,
|
|
91
|
+
sessionNodes
|
|
92
|
+
),
|
|
93
|
+
selector: vn.selector || sn?.selector || session?.selector || '',
|
|
94
|
+
path: vn.path != null ? vn.path : sn?.path,
|
|
95
|
+
editable: sn?.editable !== false && vn.editable !== false,
|
|
96
|
+
oldText: String(oldText),
|
|
97
|
+
newText: String(newText)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Fallback: fields object with exclusive role/positional assignment
|
|
102
|
+
const fields = variant?.fields || {};
|
|
103
|
+
const bodies = Array.isArray(fields.bodies)
|
|
104
|
+
? fields.bodies
|
|
105
|
+
: fields.body
|
|
106
|
+
? [fields.body]
|
|
107
|
+
: [];
|
|
108
|
+
const slots = [
|
|
109
|
+
{ role: 'heading', value: fields.heading },
|
|
110
|
+
...bodies.map((value) => ({ role: 'text', value })),
|
|
111
|
+
{ role: 'action', value: fields.cta }
|
|
112
|
+
].filter((s) => s.value != null && s.value !== '');
|
|
113
|
+
|
|
114
|
+
const used = new Set();
|
|
115
|
+
for (const slot of slots) {
|
|
116
|
+
let match = sessionNodes.find(
|
|
117
|
+
(n, i) =>
|
|
118
|
+
!used.has(i) &&
|
|
119
|
+
String(n.role || '').toLowerCase() === slot.role
|
|
120
|
+
);
|
|
121
|
+
if (!match && slot.role === 'action') {
|
|
122
|
+
match = sessionNodes.find(
|
|
123
|
+
(n, i) =>
|
|
124
|
+
!used.has(i) &&
|
|
125
|
+
String(n.role || '').toLowerCase() === 'button'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!match) {
|
|
129
|
+
const idx = sessionNodes.findIndex((_, i) => !used.has(i));
|
|
130
|
+
match = idx >= 0 ? sessionNodes[idx] : null;
|
|
131
|
+
if (idx >= 0) used.add(idx);
|
|
132
|
+
} else {
|
|
133
|
+
used.add(sessionNodes.indexOf(match));
|
|
134
|
+
}
|
|
135
|
+
if (!match) continue;
|
|
136
|
+
const oldText =
|
|
137
|
+
match.originalText ?? match.previousText ?? match.text ?? '';
|
|
138
|
+
changes.push({
|
|
139
|
+
id: match.id || `copy-${match.path}`,
|
|
140
|
+
role: match.role || slot.role,
|
|
141
|
+
label: roleLabel(
|
|
142
|
+
match.role || slot.role,
|
|
143
|
+
match,
|
|
144
|
+
sessionNodes.indexOf(match),
|
|
145
|
+
sessionNodes
|
|
146
|
+
),
|
|
147
|
+
selector: match.selector || session?.selector || '',
|
|
148
|
+
path: match.path,
|
|
149
|
+
editable: match.editable !== false,
|
|
150
|
+
oldText: String(oldText),
|
|
151
|
+
newText: String(slot.value)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!changes.length) {
|
|
157
|
+
lines.push('(No text deltas found — inspect the variant nodes and selection.)');
|
|
158
|
+
} else {
|
|
159
|
+
lines.push('| Field | Selector | Old | New |');
|
|
160
|
+
lines.push('| --- | --- | --- | --- |');
|
|
161
|
+
for (const c of changes) {
|
|
162
|
+
lines.push(
|
|
163
|
+
`| ${c.label} | \`${c.selector || '(none)'}\` | ${JSON.stringify(c.oldText)} | ${JSON.stringify(c.newText)} |`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
lines.push('');
|
|
167
|
+
for (const c of changes) {
|
|
168
|
+
lines.push(`${c.label} (id=${c.id}, path=${c.path}):`);
|
|
169
|
+
lines.push(` Selector: ${c.selector || '(none)'}`);
|
|
170
|
+
lines.push(
|
|
171
|
+
` Editable: ${c.editable ? 'yes' : 'no (likely dynamic/bound — report if blocked)'}`
|
|
172
|
+
);
|
|
173
|
+
lines.push(` Old: ${JSON.stringify(c.oldText)}`);
|
|
174
|
+
lines.push(` New: ${JSON.stringify(c.newText)}`);
|
|
175
|
+
lines.push('');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
lines.push('Instructions:');
|
|
180
|
+
lines.push('1. Locate the source file(s) containing each Old string (HTML/JSX/TSX).');
|
|
181
|
+
lines.push('2. Replace each Old with its matching New only — keep mappings 1:1.');
|
|
182
|
+
lines.push('3. If a string is i18n/dynamic/bound, say so and do not invent translations.');
|
|
183
|
+
lines.push('4. After edits, briefly confirm which files changed.');
|
|
184
|
+
lines.push(
|
|
185
|
+
'5. Prefer MCP tools when available: crank_get_formatted_changes / get_pending_agent_prompt, then apply_copy_variant (persist) or edit source, then crank_clear_changes / ack_pending_agent_prompt.'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
prompt: lines.join('\n'),
|
|
190
|
+
changes
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function notifyWatchers() {
|
|
195
|
+
const id = pending?.id || null;
|
|
196
|
+
const ready = watchers.splice(0, watchers.length);
|
|
197
|
+
for (const w of ready) {
|
|
198
|
+
clearTimeout(w.timer);
|
|
199
|
+
w.resolve({
|
|
200
|
+
ok: true,
|
|
201
|
+
timedOut: false,
|
|
202
|
+
pending: pending
|
|
203
|
+
? publicPending(pending)
|
|
204
|
+
: null
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return id;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function publicPending(task) {
|
|
211
|
+
if (!task) return null;
|
|
212
|
+
return {
|
|
213
|
+
id: task.id,
|
|
214
|
+
createdAt: task.createdAt,
|
|
215
|
+
status: task.status,
|
|
216
|
+
source: task.source,
|
|
217
|
+
sessionId: task.sessionId,
|
|
218
|
+
variantId: task.variantId,
|
|
219
|
+
variantLabel: task.variantLabel,
|
|
220
|
+
pageUrl: task.pageUrl,
|
|
221
|
+
selector: task.selector,
|
|
222
|
+
prompt: task.prompt,
|
|
223
|
+
changes: task.changes,
|
|
224
|
+
changeCount: Array.isArray(task.changes) ? task.changes.length : 0
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function enqueueAgentTask({ session, variant, source = 'panel' }) {
|
|
229
|
+
if (!session || !variant) {
|
|
230
|
+
throw new Error('session and variant are required');
|
|
231
|
+
}
|
|
232
|
+
const built = buildAgentPrompt({ session, variant });
|
|
233
|
+
pending = {
|
|
234
|
+
id: crypto.randomBytes(6).toString('hex'),
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
status: 'pending',
|
|
237
|
+
source,
|
|
238
|
+
sessionId: session.id,
|
|
239
|
+
variantId: variant.id,
|
|
240
|
+
variantLabel: variant.label || variant.id,
|
|
241
|
+
pageUrl: session.pageUrl || '',
|
|
242
|
+
selector: session.selector || '',
|
|
243
|
+
prompt: built.prompt,
|
|
244
|
+
changes: built.changes,
|
|
245
|
+
session: {
|
|
246
|
+
id: session.id,
|
|
247
|
+
selector: session.selector,
|
|
248
|
+
pageUrl: session.pageUrl,
|
|
249
|
+
nodes: session.nodes
|
|
250
|
+
},
|
|
251
|
+
variant: {
|
|
252
|
+
id: variant.id,
|
|
253
|
+
label: variant.label,
|
|
254
|
+
fields: variant.fields || null,
|
|
255
|
+
nodes: variant.nodes || []
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
notifyWatchers();
|
|
259
|
+
return pending;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getPendingAgentTask() {
|
|
263
|
+
return pending;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getPendingChanges({ enriched = false } = {}) {
|
|
267
|
+
if (!pending) return null;
|
|
268
|
+
const base = {
|
|
269
|
+
id: pending.id,
|
|
270
|
+
createdAt: pending.createdAt,
|
|
271
|
+
status: pending.status,
|
|
272
|
+
variantId: pending.variantId,
|
|
273
|
+
variantLabel: pending.variantLabel,
|
|
274
|
+
pageUrl: pending.pageUrl,
|
|
275
|
+
selector: pending.selector,
|
|
276
|
+
changes: pending.changes || []
|
|
277
|
+
};
|
|
278
|
+
if (!enriched) return base;
|
|
279
|
+
return {
|
|
280
|
+
...base,
|
|
281
|
+
prompt: pending.prompt,
|
|
282
|
+
session: pending.session,
|
|
283
|
+
variant: pending.variant
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Retune-style markdown for the agent skill / paste path.
|
|
289
|
+
* @param {{ fidelity?: 'minimal'|'standard'|'full', clear?: boolean }} opts
|
|
290
|
+
*/
|
|
291
|
+
function getFormattedChanges(opts = {}) {
|
|
292
|
+
const fidelity = opts.fidelity || 'standard';
|
|
293
|
+
const shouldClear = opts.clear !== false;
|
|
294
|
+
if (!pending) {
|
|
295
|
+
return { ok: false, error: 'No pending Crank copy changes' };
|
|
296
|
+
}
|
|
297
|
+
const task = pending;
|
|
298
|
+
const changes = Array.isArray(task.changes) ? task.changes : [];
|
|
299
|
+
const lines = [
|
|
300
|
+
'# Copy Changes',
|
|
301
|
+
'',
|
|
302
|
+
`The user queued a Crank copy variant for source apply.`,
|
|
303
|
+
'',
|
|
304
|
+
`**Variant:** ${task.variantLabel || task.variantId}`,
|
|
305
|
+
`**Page:** ${task.pageUrl || '(unknown)'}`,
|
|
306
|
+
`**Selection:** \`${task.selector || '(none)'}\``,
|
|
307
|
+
`**Task id:** ${task.id}`,
|
|
308
|
+
`**Change count:** ${changes.length}`,
|
|
309
|
+
''
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
if (!changes.length) {
|
|
313
|
+
lines.push('_No text deltas._');
|
|
314
|
+
} else {
|
|
315
|
+
lines.push('## Per-string mapping');
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push('| Field | Selector | Path | Before | After | Editable |');
|
|
318
|
+
lines.push('| --- | --- | --- | --- | --- | --- |');
|
|
319
|
+
for (const c of changes) {
|
|
320
|
+
lines.push(
|
|
321
|
+
`| ${c.label || c.role || 'Body'} | \`${c.selector || '(none)'}\` | ${c.path != null ? c.path : '—'} | ${JSON.stringify(c.oldText)} | ${JSON.stringify(c.newText)} | ${c.editable === false ? 'no' : 'yes'} |`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
lines.push('');
|
|
325
|
+
if (fidelity !== 'minimal') {
|
|
326
|
+
lines.push('## Details');
|
|
327
|
+
lines.push('');
|
|
328
|
+
for (const c of changes) {
|
|
329
|
+
lines.push(`### ${c.label || 'Body'} (\`${c.id}\`)`);
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push(`- **Selector:** \`${c.selector || '(none)'}\``);
|
|
332
|
+
lines.push(`- **Path:** ${c.path != null ? c.path : '—'}`);
|
|
333
|
+
lines.push(
|
|
334
|
+
`- **Editable:** ${c.editable === false ? 'no (likely dynamic/bound)' : 'yes'}`
|
|
335
|
+
);
|
|
336
|
+
lines.push(`- **Before:** ${JSON.stringify(c.oldText)}`);
|
|
337
|
+
lines.push(`- **After:** ${JSON.stringify(c.newText)}`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (fidelity === 'full' && task.prompt) {
|
|
344
|
+
lines.push('## Full prompt');
|
|
345
|
+
lines.push('');
|
|
346
|
+
lines.push('```');
|
|
347
|
+
lines.push(task.prompt);
|
|
348
|
+
lines.push('```');
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
lines.push('## Workflow');
|
|
353
|
+
lines.push('');
|
|
354
|
+
lines.push(
|
|
355
|
+
'1. Locate each Before string in source (HTML/JSX/TSX) under the project root.'
|
|
356
|
+
);
|
|
357
|
+
lines.push('2. Replace with the matching After value only (1:1).');
|
|
358
|
+
lines.push(
|
|
359
|
+
'3. Prefer hardcoded literals; do not invent i18n keys for dynamic/bound strings.'
|
|
360
|
+
);
|
|
361
|
+
lines.push(
|
|
362
|
+
'4. Optionally call `apply_copy_variant` with persist for DOM + Vizpatch text-writer.'
|
|
363
|
+
);
|
|
364
|
+
lines.push(
|
|
365
|
+
'5. Always call `crank_clear_changes` (or `ack_pending_agent_prompt`) when done.'
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const markdown = lines.join('\n');
|
|
369
|
+
const snapshot = publicPending(task);
|
|
370
|
+
if (shouldClear) {
|
|
371
|
+
pending = null;
|
|
372
|
+
notifyWatchers();
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
ok: true,
|
|
376
|
+
markdown,
|
|
377
|
+
task: snapshot,
|
|
378
|
+
cleared: shouldClear
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Long-poll until a pending task exists (or id differs from sinceId).
|
|
384
|
+
* @param {{ timeoutMs?: number, sinceId?: string|null }} opts
|
|
385
|
+
*/
|
|
386
|
+
function watchPendingChanges(opts = {}) {
|
|
387
|
+
const timeoutMs = Math.min(
|
|
388
|
+
Math.max(Number(opts.timeoutMs) || 30000, 1000),
|
|
389
|
+
120000
|
|
390
|
+
);
|
|
391
|
+
const sinceId = opts.sinceId != null ? String(opts.sinceId) : null;
|
|
392
|
+
|
|
393
|
+
if (pending && pending.id !== sinceId) {
|
|
394
|
+
return Promise.resolve({
|
|
395
|
+
ok: true,
|
|
396
|
+
timedOut: false,
|
|
397
|
+
pending: publicPending(pending)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return new Promise((resolve) => {
|
|
402
|
+
const entry = {
|
|
403
|
+
sinceId,
|
|
404
|
+
resolve,
|
|
405
|
+
timer: setTimeout(() => {
|
|
406
|
+
const idx = watchers.indexOf(entry);
|
|
407
|
+
if (idx >= 0) watchers.splice(idx, 1);
|
|
408
|
+
resolve({
|
|
409
|
+
ok: true,
|
|
410
|
+
timedOut: true,
|
|
411
|
+
pending: pending ? publicPending(pending) : null
|
|
412
|
+
});
|
|
413
|
+
}, timeoutMs)
|
|
414
|
+
};
|
|
415
|
+
watchers.push(entry);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function ackPendingAgentTask(id) {
|
|
420
|
+
if (!pending) {
|
|
421
|
+
return { ok: false, error: 'No pending agent task' };
|
|
422
|
+
}
|
|
423
|
+
if (id && pending.id !== id) {
|
|
424
|
+
return { ok: false, error: `Pending id mismatch (have ${pending.id})` };
|
|
425
|
+
}
|
|
426
|
+
const done = { ...pending, status: 'acked', ackedAt: new Date().toISOString() };
|
|
427
|
+
pending = null;
|
|
428
|
+
notifyWatchers();
|
|
429
|
+
return { ok: true, task: done };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function clearPendingAgentTask() {
|
|
433
|
+
const had = pending;
|
|
434
|
+
pending = null;
|
|
435
|
+
notifyWatchers();
|
|
436
|
+
return { ok: true, cleared: !!had, task: had ? publicPending(had) : null };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function getAgentQueueStatus() {
|
|
440
|
+
let sdkAvailable = false;
|
|
441
|
+
try {
|
|
442
|
+
require.resolve('@cursor/sdk');
|
|
443
|
+
sdkAvailable = true;
|
|
444
|
+
} catch {
|
|
445
|
+
sdkAvailable = false;
|
|
446
|
+
}
|
|
447
|
+
const runtime = String(process.env.CURSOR_AGENT_RUNTIME || 'local')
|
|
448
|
+
.trim()
|
|
449
|
+
.toLowerCase();
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
hasPending: !!pending,
|
|
453
|
+
pending: pending
|
|
454
|
+
? {
|
|
455
|
+
id: pending.id,
|
|
456
|
+
variantId: pending.variantId,
|
|
457
|
+
variantLabel: pending.variantLabel,
|
|
458
|
+
createdAt: pending.createdAt,
|
|
459
|
+
changeCount: Array.isArray(pending.changes) ? pending.changes.length : 0,
|
|
460
|
+
status: pending.status
|
|
461
|
+
}
|
|
462
|
+
: null,
|
|
463
|
+
watchers: watchers.length,
|
|
464
|
+
apiKeyConfigured: Boolean(
|
|
465
|
+
process.env.CURSOR_API_KEY && String(process.env.CURSOR_API_KEY).trim()
|
|
466
|
+
),
|
|
467
|
+
sdkAvailable,
|
|
468
|
+
agentRuntime: runtime === 'cloud' ? 'cloud' : 'local',
|
|
469
|
+
howto:
|
|
470
|
+
'Paste “Apply the pending Crank change” in your current Cursor chat (or keep an agent with the crank-apply-changes skill). Tools: crank_get_formatted_changes → edit source → crank_clear_changes. Cursor has no public inject-into-open-chat API.'
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
module.exports = {
|
|
475
|
+
buildAgentPrompt,
|
|
476
|
+
enqueueAgentTask,
|
|
477
|
+
getPendingAgentTask,
|
|
478
|
+
getPendingChanges,
|
|
479
|
+
getFormattedChanges,
|
|
480
|
+
watchPendingChanges,
|
|
481
|
+
ackPendingAgentTask,
|
|
482
|
+
clearPendingAgentTask,
|
|
483
|
+
getAgentQueueStatus,
|
|
484
|
+
publicPending
|
|
485
|
+
};
|