@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,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool handlers — talk to the Crank companion HTTP API.
|
|
3
|
+
* Used by Cursor / Claude Desktop (stdio) and Replit Agent (remote HTTP MCP).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const COMPANION_URL = (process.env.CRANK_COMPANION_URL || 'http://127.0.0.1:3344').replace(
|
|
7
|
+
/\/$/,
|
|
8
|
+
''
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const TEAM_TOKEN = process.env.CRANK_TEAM_TOKEN || '';
|
|
12
|
+
|
|
13
|
+
const teamTokenSchema = {
|
|
14
|
+
teamToken: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description:
|
|
17
|
+
'Shared Crank team passphrase. Ask the user if tools return TEAM_TOKEN_REQUIRED. Optional when CRANK_TEAM_TOKEN is set in MCP env.'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Glean Robot Content Designer often takes 90–140s; allow up to 5 min. */
|
|
22
|
+
const GLEAN_VARIANTS_TIMEOUT_MS = Number(process.env.CRANK_VARIANTS_TIMEOUT_MS || 300000);
|
|
23
|
+
|
|
24
|
+
async function companionFetch(pathname, options = {}) {
|
|
25
|
+
const { timeoutMs, teamToken, ...fetchOpts } = options;
|
|
26
|
+
const token = teamToken || TEAM_TOKEN || '';
|
|
27
|
+
const controller =
|
|
28
|
+
timeoutMs > 0 && typeof AbortController !== 'undefined'
|
|
29
|
+
? new AbortController()
|
|
30
|
+
: null;
|
|
31
|
+
let timer = null;
|
|
32
|
+
if (controller) {
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
try {
|
|
35
|
+
controller.abort();
|
|
36
|
+
} catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
}, timeoutMs);
|
|
40
|
+
}
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(`${COMPANION_URL}${pathname}`, {
|
|
44
|
+
...fetchOpts,
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
...(token ? { 'X-Crank-Team-Token': token } : {}),
|
|
48
|
+
...(fetchOpts.headers || {})
|
|
49
|
+
},
|
|
50
|
+
signal: controller ? controller.signal : fetchOpts.signal
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (timer) clearTimeout(timer);
|
|
54
|
+
if (err && err.name === 'AbortError') {
|
|
55
|
+
throw new Error(`Companion request timed out after ${timeoutMs}ms on ${pathname}`);
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
if (timer) clearTimeout(timer);
|
|
60
|
+
const data = await res.json().catch(() => ({}));
|
|
61
|
+
if (!res.ok || data.ok === false) {
|
|
62
|
+
if (res.status === 401 && data.code === 'TEAM_TOKEN_REQUIRED') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'TEAM_TOKEN_REQUIRED: Ask the user for the Crank team token, then retry with teamToken on this tool (or set CRANK_TEAM_TOKEN in MCP env).'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(data.error || `Companion ${res.status} on ${pathname}`);
|
|
68
|
+
}
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const toolDefinitions = [
|
|
73
|
+
{
|
|
74
|
+
name: 'get_selected_copy',
|
|
75
|
+
description:
|
|
76
|
+
'Return the currently selected page region and its text nodes from the Crank companion (set by the browser bookmarklet/panel).',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
sessionId: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Optional session id; defaults to the active selection'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'propose_copy_variants',
|
|
89
|
+
description:
|
|
90
|
+
'Send the selected copy payload to the Glean agent (skills/rules aware) and return 3–4 copy iteration options for all text nodes in the selection.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
sessionId: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'Optional session id; defaults to active selection'
|
|
97
|
+
},
|
|
98
|
+
variantCount: {
|
|
99
|
+
type: 'number',
|
|
100
|
+
description: 'Preferred number of variants (default 4)'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'apply_copy_variant',
|
|
107
|
+
description:
|
|
108
|
+
'Apply a chosen Glean copy variant: update live DOM in connected browsers AND persist editable hardcoded strings to source via Vizpatch text-writer. Set persist=false for DOM-only preview.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
sessionId: { type: 'string' },
|
|
113
|
+
variantId: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Variant id from propose_copy_variants'
|
|
116
|
+
},
|
|
117
|
+
persist: {
|
|
118
|
+
type: 'boolean',
|
|
119
|
+
description:
|
|
120
|
+
'When true (default), also write hardcoded literals to source. false = DOM preview only.'
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
required: ['variantId']
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'apply_custom_copy',
|
|
128
|
+
description:
|
|
129
|
+
'Apply agent-edited node text updates to the live DOM (and optionally label them). Prefer apply_copy_variant for full variant apply + persist.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
sessionId: { type: 'string' },
|
|
134
|
+
nodes: {
|
|
135
|
+
type: 'array',
|
|
136
|
+
description: 'Array of { path, text, ... } node updates'
|
|
137
|
+
},
|
|
138
|
+
variantId: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Optional label for this custom apply'
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
required: ['nodes']
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'crank_get_pending_changes',
|
|
148
|
+
description:
|
|
149
|
+
'Retune-parity: return structured pending copy changes queued by the panel Agent button (per-field old→new, selectors, paths). Use when applying Crank source edits.',
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
enriched: {
|
|
154
|
+
type: 'boolean',
|
|
155
|
+
description: 'Include full prompt + session/variant payloads'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'crank_get_formatted_changes',
|
|
162
|
+
description:
|
|
163
|
+
'Retune-parity: return agent-ready markdown for pending Crank copy changes. By default clears pending after read (pass clear:false to keep). Prefer this in the crank-apply-changes skill workflow.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
fidelity: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
enum: ['minimal', 'standard', 'full'],
|
|
170
|
+
description: 'Amount of detail in the markdown (default standard)'
|
|
171
|
+
},
|
|
172
|
+
clear: {
|
|
173
|
+
type: 'boolean',
|
|
174
|
+
description:
|
|
175
|
+
'Clear pending after returning markdown (default true, like Retune)'
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'crank_watch_changes',
|
|
182
|
+
description:
|
|
183
|
+
'Long-poll the companion until pending Crank copy changes appear or update (up to ~30s). Useful when waiting for the user to click Agent.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
timeoutMs: {
|
|
188
|
+
type: 'number',
|
|
189
|
+
description: 'Max wait in ms (default 30000, max 120000)'
|
|
190
|
+
},
|
|
191
|
+
sinceId: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'Return when pending id differs from this value'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'crank_clear_changes',
|
|
200
|
+
description:
|
|
201
|
+
'Clear pending Crank copy changes after they have been applied to source (Retune-parity). Always call this when done.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
id: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'Optional pending task id to clear'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'crank_verify_team_token',
|
|
214
|
+
description:
|
|
215
|
+
'Verify the shared Crank team passphrase after the user provides it. Call when crank_get_environment reports teamAuthRequired or other tools return TEAM_TOKEN_REQUIRED.',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
teamToken: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'Team passphrase from the user'
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
required: ['teamToken']
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'crank_get_environment',
|
|
229
|
+
description:
|
|
230
|
+
'Retune-style: return which repo/app directory Crank is bound to, virtual vs local environment (Replit, Codespaces), companion URLs, MCP endpoint, auth requirements, and suggested commands. Call first when unsure where code lives or how to start Crank.',
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
...teamTokenSchema
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'crank_status',
|
|
240
|
+
description:
|
|
241
|
+
'Return whether Crank has pending copy changes queued, watcher count, and whether CURSOR_API_KEY is configured for optional headless auto-run.',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'get_pending_agent_prompt',
|
|
249
|
+
description:
|
|
250
|
+
'Legacy alias: read the pending Crank agent task (full executable prompt + structured changes). Prefer crank_get_formatted_changes for skill workflows.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'ack_pending_agent_prompt',
|
|
258
|
+
description:
|
|
259
|
+
'Legacy alias for crank_clear_changes — acknowledge and clear the pending task after applying.',
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
id: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Optional pending task id from get_pending_agent_prompt'
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'queue_agent_prompt',
|
|
272
|
+
description:
|
|
273
|
+
'Queue a Crank variant as pending copy changes (same as the panel Agent button). Does not save files.',
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
sessionId: { type: 'string' },
|
|
278
|
+
variantId: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'Variant id to hand off to the Cursor agent'
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
required: ['variantId']
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
function formatJson(data) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function callTool(name, args = {}) {
|
|
295
|
+
const teamToken = args.teamToken || TEAM_TOKEN || '';
|
|
296
|
+
const withToken = (opts = {}) => ({ ...opts, teamToken: opts.teamToken || teamToken });
|
|
297
|
+
switch (name) {
|
|
298
|
+
case 'crank_verify_team_token': {
|
|
299
|
+
const data = await companionFetch('/api/auth/verify', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify({ token: args.teamToken }),
|
|
302
|
+
teamToken: args.teamToken
|
|
303
|
+
});
|
|
304
|
+
return formatJson({
|
|
305
|
+
...data,
|
|
306
|
+
note: 'Token accepted — pass the same teamToken on other Crank MCP tools in this session.'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
case 'crank_get_environment': {
|
|
310
|
+
const data = await companionFetch('/api/environment', withToken());
|
|
311
|
+
return formatJson(data);
|
|
312
|
+
}
|
|
313
|
+
case 'get_selected_copy': {
|
|
314
|
+
const qs = args.sessionId
|
|
315
|
+
? `?sessionId=${encodeURIComponent(args.sessionId)}`
|
|
316
|
+
: '';
|
|
317
|
+
const data = await companionFetch(`/api/selection${qs}`, withToken());
|
|
318
|
+
return formatJson(data.session);
|
|
319
|
+
}
|
|
320
|
+
case 'propose_copy_variants': {
|
|
321
|
+
const data = await companionFetch('/api/variants', withToken({
|
|
322
|
+
method: 'POST',
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
sessionId: args.sessionId,
|
|
325
|
+
options: { variantCount: args.variantCount || 4 }
|
|
326
|
+
}),
|
|
327
|
+
timeoutMs: GLEAN_VARIANTS_TIMEOUT_MS
|
|
328
|
+
}));
|
|
329
|
+
return formatJson({
|
|
330
|
+
sessionId: data.session.id,
|
|
331
|
+
variants: data.variants
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
case 'apply_copy_variant': {
|
|
335
|
+
const persist = args.persist !== false;
|
|
336
|
+
const data = await companionFetch('/api/apply', withToken({
|
|
337
|
+
method: 'POST',
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
sessionId: args.sessionId,
|
|
340
|
+
variantId: args.variantId,
|
|
341
|
+
persist
|
|
342
|
+
})
|
|
343
|
+
}));
|
|
344
|
+
return formatJson({
|
|
345
|
+
applied: true,
|
|
346
|
+
persisted: !!(data.persistResult && data.persistResult.persisted),
|
|
347
|
+
sessionId: data.session.id,
|
|
348
|
+
variantId: data.variant.id,
|
|
349
|
+
label: data.variant.label,
|
|
350
|
+
nodes: data.variant.nodes,
|
|
351
|
+
persistResult: data.persistResult || null,
|
|
352
|
+
note: persist
|
|
353
|
+
? 'DOM updated; source save attempted for editable hardcoded literals (see persistResult).'
|
|
354
|
+
: 'DOM preview only — source not written.'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
case 'apply_custom_copy': {
|
|
358
|
+
const data = await companionFetch('/api/apply-nodes', withToken({
|
|
359
|
+
method: 'POST',
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
sessionId: args.sessionId,
|
|
362
|
+
nodes: args.nodes,
|
|
363
|
+
variantId: args.variantId || 'custom'
|
|
364
|
+
})
|
|
365
|
+
}));
|
|
366
|
+
return formatJson({
|
|
367
|
+
applied: true,
|
|
368
|
+
session: data.session,
|
|
369
|
+
note: 'DOM updated only. Edit source files or use apply_copy_variant with persist for hardcoded saves.'
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
case 'crank_get_pending_changes': {
|
|
373
|
+
const qs =
|
|
374
|
+
args.enriched === true || args.enriched === 'true'
|
|
375
|
+
? '?enriched=true'
|
|
376
|
+
: '';
|
|
377
|
+
const data = await companionFetch(`/api/pending-changes${qs}`, withToken());
|
|
378
|
+
return formatJson(data.pending);
|
|
379
|
+
}
|
|
380
|
+
case 'crank_get_formatted_changes': {
|
|
381
|
+
const params = new URLSearchParams();
|
|
382
|
+
if (args.fidelity) params.set('fidelity', String(args.fidelity));
|
|
383
|
+
if (args.clear === false || args.clear === 'false') {
|
|
384
|
+
params.set('clear', 'false');
|
|
385
|
+
}
|
|
386
|
+
const qs = params.toString() ? `?${params}` : '';
|
|
387
|
+
const data = await companionFetch(`/api/pending-changes/formatted${qs}`, withToken());
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{
|
|
391
|
+
type: 'text',
|
|
392
|
+
text: [
|
|
393
|
+
data.markdown,
|
|
394
|
+
'',
|
|
395
|
+
data.cleared
|
|
396
|
+
? '(Pending cleared after read — call tools again only if the user re-queues.)'
|
|
397
|
+
: '(Pending kept — call crank_clear_changes after applying.)'
|
|
398
|
+
].join('\n')
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
case 'crank_watch_changes': {
|
|
404
|
+
const params = new URLSearchParams();
|
|
405
|
+
if (args.timeoutMs) params.set('timeoutMs', String(args.timeoutMs));
|
|
406
|
+
if (args.sinceId) params.set('sinceId', String(args.sinceId));
|
|
407
|
+
const qs = params.toString() ? `?${params}` : '';
|
|
408
|
+
const watchTimeout = Math.min(
|
|
409
|
+
Math.max(Number(args.timeoutMs) || 30000, 1000) + 5000,
|
|
410
|
+
130000
|
|
411
|
+
);
|
|
412
|
+
const data = await companionFetch(`/api/pending-changes/watch${qs}`, withToken({
|
|
413
|
+
timeoutMs: watchTimeout
|
|
414
|
+
}));
|
|
415
|
+
return formatJson(data);
|
|
416
|
+
}
|
|
417
|
+
case 'crank_clear_changes':
|
|
418
|
+
case 'ack_pending_agent_prompt': {
|
|
419
|
+
if (args.id) {
|
|
420
|
+
const data = await companionFetch('/api/agent-prompt/ack', withToken({
|
|
421
|
+
method: 'POST',
|
|
422
|
+
body: JSON.stringify({ id: args.id })
|
|
423
|
+
}));
|
|
424
|
+
return formatJson({ cleared: true, task: data.task });
|
|
425
|
+
}
|
|
426
|
+
const data = await companionFetch('/api/pending-changes', withToken({
|
|
427
|
+
method: 'DELETE'
|
|
428
|
+
}));
|
|
429
|
+
return formatJson({ cleared: true, task: data.task });
|
|
430
|
+
}
|
|
431
|
+
case 'crank_status': {
|
|
432
|
+
const data = await companionFetch('/api/agent-status', withToken());
|
|
433
|
+
return formatJson(data);
|
|
434
|
+
}
|
|
435
|
+
case 'get_pending_agent_prompt': {
|
|
436
|
+
const data = await companionFetch('/api/agent-prompt', withToken());
|
|
437
|
+
const task = data.task;
|
|
438
|
+
return {
|
|
439
|
+
content: [
|
|
440
|
+
{
|
|
441
|
+
type: 'text',
|
|
442
|
+
text: [
|
|
443
|
+
'Pending Crank agent task — execute the prompt below, then call crank_clear_changes / ack_pending_agent_prompt.',
|
|
444
|
+
'',
|
|
445
|
+
`Task id: ${task.id}`,
|
|
446
|
+
`Variant: ${task.variantLabel || task.variantId}`,
|
|
447
|
+
`Page: ${task.pageUrl || '(unknown)'}`,
|
|
448
|
+
'',
|
|
449
|
+
'--- PROMPT ---',
|
|
450
|
+
task.prompt,
|
|
451
|
+
'--- END PROMPT ---',
|
|
452
|
+
'',
|
|
453
|
+
'Structured changes:',
|
|
454
|
+
JSON.stringify(task.changes || [], null, 2)
|
|
455
|
+
].join('\n')
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
case 'queue_agent_prompt': {
|
|
461
|
+
const data = await companionFetch('/api/agent-prompt', withToken({
|
|
462
|
+
method: 'POST',
|
|
463
|
+
body: JSON.stringify({
|
|
464
|
+
sessionId: args.sessionId,
|
|
465
|
+
variantId: args.variantId,
|
|
466
|
+
source: 'mcp'
|
|
467
|
+
})
|
|
468
|
+
}));
|
|
469
|
+
return formatJson({
|
|
470
|
+
queued: true,
|
|
471
|
+
taskId: data.task?.id,
|
|
472
|
+
howto: data.howto,
|
|
473
|
+
changeCount: data.task?.changeCount,
|
|
474
|
+
promptPreview: String(data.task?.prompt || '').slice(0, 500)
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
default:
|
|
478
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ahmednawaz/crank",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "One-command copy iteration: select UI text → Glean variants → MCP apply. Works in Cursor, VS Code, Claude, and Replit.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"crank": "bin/crank.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "vite.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./vite.js",
|
|
11
|
+
"./vite": "./vite.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"workspaces": [
|
|
15
|
+
"companion",
|
|
16
|
+
"mcp-server"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node bin/crank.js",
|
|
20
|
+
"crank": "node bin/crank.js",
|
|
21
|
+
"companion": "npm run start --workspace=companion",
|
|
22
|
+
"mcp": "npm run start --workspace=mcp-server",
|
|
23
|
+
"mcp:http": "npm run start:http --workspace=mcp-server",
|
|
24
|
+
"demo": "node scripts/demo-flow.js",
|
|
25
|
+
"test:parse": "node scripts/test-parse-options.js",
|
|
26
|
+
"test:resolve": "node scripts/test-resolve-project.js",
|
|
27
|
+
"prepare:publish": "node scripts/prepare-publish.js",
|
|
28
|
+
"publish:team": "node scripts/prepare-publish.js && npm publish --access public"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin",
|
|
35
|
+
"lib",
|
|
36
|
+
"companion",
|
|
37
|
+
"mcp-server",
|
|
38
|
+
"bookmarklet",
|
|
39
|
+
"skills",
|
|
40
|
+
"skill",
|
|
41
|
+
"panel",
|
|
42
|
+
"vite.js",
|
|
43
|
+
".env.example",
|
|
44
|
+
"team.env",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"crank",
|
|
52
|
+
"glean",
|
|
53
|
+
"mcp",
|
|
54
|
+
"copy",
|
|
55
|
+
"ux-writing"
|
|
56
|
+
]
|
|
57
|
+
}
|
package/panel/demo.html
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Crank panel demo</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
--ink: #1a1a1a;
|
|
9
|
+
--muted: #555;
|
|
10
|
+
--accent: #0f766e;
|
|
11
|
+
--bg: #f4f6f5;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font: 16px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
16
|
+
color: var(--ink);
|
|
17
|
+
background:
|
|
18
|
+
radial-gradient(circle at 10% 0%, #d9eee9 0%, transparent 45%),
|
|
19
|
+
radial-gradient(circle at 90% 10%, #e8eef2 0%, transparent 40%),
|
|
20
|
+
var(--bg);
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
}
|
|
23
|
+
main {
|
|
24
|
+
max-width: 42rem;
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
padding: 3rem 1.25rem;
|
|
27
|
+
}
|
|
28
|
+
.card {
|
|
29
|
+
background: #fff;
|
|
30
|
+
border: 1px solid #d8d8d8;
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
padding: 1.5rem;
|
|
33
|
+
}
|
|
34
|
+
h1 { margin: 0 0 0.5rem; font-size: 1.75rem; }
|
|
35
|
+
p { margin: 0 0 1rem; color: var(--muted); }
|
|
36
|
+
button {
|
|
37
|
+
border: 0;
|
|
38
|
+
background: var(--accent);
|
|
39
|
+
color: #fff;
|
|
40
|
+
padding: 0.55rem 0.95rem;
|
|
41
|
+
border-radius: 8px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
.hint { font-size: 0.9rem; margin-top: 1.25rem; }
|
|
46
|
+
code { background: #eee; padding: 0.1em 0.35em; border-radius: 4px; }
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<main>
|
|
51
|
+
<div id="hero" class="card">
|
|
52
|
+
<h1>Utilize our robust platform today</h1>
|
|
53
|
+
<p>Seamless fleet insights for every operator.</p>
|
|
54
|
+
<button type="button">Get started</button>
|
|
55
|
+
</div>
|
|
56
|
+
<p class="hint">
|
|
57
|
+
Start the companion (<code>npm run companion</code>), load the Crank bookmarklet,
|
|
58
|
+
click <strong>Select</strong>, then click this card’s outer <code>div</code>.
|
|
59
|
+
</p>
|
|
60
|
+
</main>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crank-apply-changes
|
|
3
|
+
description: Apply copy changes from Crank MCP tools (crank_get_formatted_changes, crank_get_pending_changes, get_pending_agent_prompt) OR when the user pastes structured "# Copy Changes" output / old→new Heading/Body/CTA tables. Triggers on: crank, "Apply the pending Crank change", "Start Crank", "npx crank", team token, Copy Changes, pending Crank, apply copy variant, Agent button handoff.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Applying Crank Copy Changes
|
|
7
|
+
|
|
8
|
+
Crank queues structured copy updates from the browser panel **Agent** button. Your job is to translate each old→new mapping into precise source edits (hardcoded JSX/HTML string literals).
|
|
9
|
+
|
|
10
|
+
**Same-chat workflow:** The panel does not inject into Composer (Cursor has no public API for that). It queues pending changes and the user pastes `Apply the pending Crank change` into the **current** chat — or you are already active with this skill. You must **pull** with MCP tools.
|
|
11
|
+
|
|
12
|
+
## Team token (when enabled)
|
|
13
|
+
|
|
14
|
+
If `crank_get_environment` or any tool returns **`TEAM_TOKEN_REQUIRED`**:
|
|
15
|
+
|
|
16
|
+
1. Ask the user: *“What is the Crank team token?”* (shared internal passphrase — not their Glean or npm login).
|
|
17
|
+
2. Call **`crank_verify_team_token`** with `{ "teamToken": "…" }`.
|
|
18
|
+
3. Pass the same **`teamToken`** on subsequent Crank MCP tool calls in this session.
|
|
19
|
+
|
|
20
|
+
Alternatively the user can set `CRANK_TEAM_TOKEN` in Replit Secrets or shell before starting Crank.
|
|
21
|
+
|
|
22
|
+
## When the user says "start Crank" or pastes `npx crank`
|
|
23
|
+
|
|
24
|
+
1. Call **`crank_get_environment`** first — repo path, Replit vs local, companion URL, auth requirements.
|
|
25
|
+
2. If team auth is required and tools fail, ask for the team token (see above).
|
|
26
|
+
3. Run in the terminal from project root:
|
|
27
|
+
```bash
|
|
28
|
+
npx @ahmednawaz/crank setup # once
|
|
29
|
+
npx @ahmednawaz/crank
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Apply workflow
|
|
33
|
+
|
|
34
|
+
1. Call **`crank_get_formatted_changes`** (preferred) or **`crank_get_pending_changes`** / **`get_pending_agent_prompt`**.
|
|
35
|
+
- Pass **`teamToken`** if required.
|
|
36
|
+
- If nothing is pending, tell the user to click **Agent** on a Crank variant first — or use **`crank_watch_changes`** once if they are about to queue.
|
|
37
|
+
2. For each change row, locate the source file under the **project root** from `crank_get_environment` and edit the hardcoded string literal (JSX/HTML/template).
|
|
38
|
+
3. Preserve structure — only change copy text, not layout or component hierarchy.
|
|
39
|
+
4. Always call **`crank_clear_changes`** (or **`ack_pending_agent_prompt`**) after applying (or deciding it cannot be applied).
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
|
|
43
|
+
- Match selectors/paths when provided; fall back to grep for the old string under the detected project root.
|
|
44
|
+
- Do not invent copy — use the queued new values exactly.
|
|
45
|
+
- If `crank_status` reports no pending changes, stop — do not invent copy.
|
|
46
|
+
- Never ask for Glean API keys — those are bundled server-side. Only ask for the **Crank team token** when auth is enabled.
|
|
47
|
+
|
|
48
|
+
## MCP tools
|
|
49
|
+
|
|
50
|
+
| Tool | Purpose |
|
|
51
|
+
|------|---------|
|
|
52
|
+
| `crank_verify_team_token` | Validate passphrase after user provides it |
|
|
53
|
+
| `crank_get_environment` | Repo path, auth, Replit vs local, URLs |
|
|
54
|
+
| `crank_get_formatted_changes` | Agent-ready markdown |
|
|
55
|
+
| `crank_get_pending_changes` | Structured JSON diffs |
|
|
56
|
+
| `crank_watch_changes` | Long-poll until pending appears |
|
|
57
|
+
| `crank_clear_changes` | Clear after apply |
|
|
58
|
+
| `crank_status` | Pending queue status |
|
|
59
|
+
|
|
60
|
+
Pass optional **`teamToken`** on any tool when `teamAuthRequired` is true.
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Skills & rules (copy guidance)
|
|
2
|
+
|
|
3
|
+
This directory is loaded by the companion Glean client and attached to variant requests.
|
|
4
|
+
|
|
5
|
+
When the real Glean agent is wired, either:
|
|
6
|
+
|
|
7
|
+
1. Point Glean at these files as installed skills/rules, or
|
|
8
|
+
2. Keep sending their contents in the `skills[]` field of the Glean request body (current stub behavior).
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | Purpose |
|
|
13
|
+
|------|---------|
|
|
14
|
+
| `copy-guidance.md` | Default product-copy rules for Motive internal UIs |
|
|
15
|
+
|
|
16
|
+
Add more `.md` / `.txt` files as needed — they are picked up automatically.
|