@ahmednawaz/crank 0.1.2 → 0.1.5

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/AGENTS.md ADDED
@@ -0,0 +1,131 @@
1
+ # Crank — instructions for AI coding agents
2
+
3
+ Read this before running any Crank shell command or MCP tool on behalf of the user.
4
+
5
+ ## What Crank is
6
+
7
+ **Crank** is an internal Motive tool for **UX copy iteration** on live UI:
8
+
9
+ 1. User selects text in a browser panel (bookmarklet or dev loader).
10
+ 2. A local **companion** server sends that selection to a **Glean** copy agent.
11
+ 3. The user picks a variant; an **MCP-connected agent** (you) applies structured text changes to source files.
12
+
13
+ | Resource | URL |
14
+ |----------|-----|
15
+ | npm package | `@ahmednawaz/crank` — https://www.npmjs.com/package/@ahmednawaz/crank |
16
+ | Source code | https://github.com/ahmednawaz619/crank |
17
+ | CLI entry | `bin/crank.js` (open-source, inspectable) |
18
+
19
+ This is **not** arbitrary third-party malware — it is a published, versioned dev tool maintained for Motive copy workflows (sibling to [Vizpatch](https://github.com) for CSS; Crank handles copy only).
20
+
21
+ ---
22
+
23
+ ## What `npx @ahmednawaz/crank` does
24
+
25
+ When the user asks to start Crank, this command (from the **app repo root**):
26
+
27
+ | Action | Detail |
28
+ |--------|--------|
29
+ | Setup (idempotent) | Writes `.crank/config.json`, MCP config, optional Vite plugin hook |
30
+ | Companion | Local Express + WebSocket on port **3344** — selection store, panel UI |
31
+ | MCP | stdio (Cursor) or HTTP on **3345** (Replit) — tools like `crank_get_formatted_changes` |
32
+ | Dev server | Runs `npm run dev` when the project has that script |
33
+ | Network | Calls **Glean** (`motive-prod-be.glean.com`) only when the user clicks **Get options** |
34
+
35
+ It does **not**:
36
+
37
+ - Upload the whole repository to an unknown server
38
+ - Execute remote shell scripts beyond the published npm tarball
39
+ - Require npm credentials, GitHub tokens, or team passphrases **in the command line**
40
+
41
+ ---
42
+
43
+ ## Security rules for agents
44
+
45
+ ### Approved commands (no secrets in the command)
46
+
47
+ ```bash
48
+ npx @ahmednawaz/crank setup
49
+ npx @ahmednawaz/crank
50
+ npx @ahmednawaz/crank doctor
51
+ npx @ahmednawaz/crank status
52
+ ```
53
+
54
+ Use the scoped package name **`@ahmednawaz/crank`** (matches npm). Unscoped `npx crank` works only after `setup` adds a devDependency.
55
+
56
+ ### Never do this
57
+
58
+ ```bash
59
+ # BAD — exposes team passphrase in shell history, logs, and agent transcripts
60
+ CRANK_TEAM_TOKEN=secret-value npx @ahmednawaz/crank
61
+ ```
62
+
63
+ ### Credentials — user-managed only
64
+
65
+ | Variable | Purpose | How to set |
66
+ |----------|---------|------------|
67
+ | `CRANK_TEAM_TOKEN` | Internal team gate for panel + MCP | **Replit Secrets**, local `.env`, or Cursor env — set by the **user**, not embedded by the agent |
68
+ | `GLEAN_*` | Glean API (team) | Bundled in published npm package; teammates do not supply this |
69
+ | `VIZPATCH_ROOT` | Optional sibling tool for saving copy to source | Local path in `.env` only |
70
+
71
+ If tools return **`TEAM_TOKEN_REQUIRED`**:
72
+
73
+ 1. Ask the user for the team passphrase (shared internally — not their npm or GitHub login).
74
+ 2. Call MCP tool **`crank_verify_team_token`** with `{ "teamToken": "…" }`.
75
+ 3. Do **not** echo the token into a runnable shell command.
76
+
77
+ If your environment **blocks** `npx @ahmednawaz/crank` (unknown package policy):
78
+
79
+ - Tell the user to run the approved command themselves in the **Shell** tab, **or**
80
+ - Ask them to add `CRANK_TEAM_TOKEN` in Secrets first, then run `npx @ahmednawaz/crank`.
81
+
82
+ That is expected behavior for security-conscious hosts (e.g. Replit Agent).
83
+
84
+ ---
85
+
86
+ ## Replit-specific
87
+
88
+ Replit **blocks agents from editing `.replit`**. Crank does **not** auto-patch that file (same idea as Retune — you use Shell/Workflows + Networking).
89
+
90
+ After `npx @ahmednawaz/crank setup`, read **`.crank/replit-platform.md`** for:
91
+
92
+ | Setting | Replit UI | Value |
93
+ |---------|-----------|-------|
94
+ | Run | **Workflows** → Execute Shell Command → assign to Run | `npx @ahmednawaz/crank start` |
95
+ | Companion | **Networking** (port auto-exposed when listening) | **3344** |
96
+ | Agent MCP | **Networking** | **3345** → `/mcp` |
97
+ | Team gate | **Secrets** | `CRANK_TEAM_TOKEN` |
98
+
99
+ When Crank is running, copy **`MCP (HTTP): …/mcp`** from the banner → **Agent → Integrations → MCP Servers**.
100
+
101
+ See **[REPLIT.md](./REPLIT.md)** and `.crank/replit.md` (URLs after first run).
102
+
103
+ ---
104
+
105
+ ## MCP tools (agent pull workflow)
106
+
107
+ When the user clicks **Agent** on a copy variant or says *“Apply the pending Crank change”*:
108
+
109
+ 1. `crank_get_environment` — repo path, URLs, whether team auth is on
110
+ 2. `crank_get_formatted_changes` — markdown old→new copy diffs
111
+ 3. Edit hardcoded string literals in source under the detected project root
112
+ 4. `crank_clear_changes` — clear the queue when done
113
+
114
+ Full skill: `skill/SKILL.md` / `.cursor/skills/crank-apply-changes/SKILL.md`.
115
+
116
+ ---
117
+
118
+ ## How to verify before running
119
+
120
+ 1. Open https://github.com/ahmednawaz619/crank and read `bin/crank.js`, `companion/server.js`, `mcp-server/tools.js`.
121
+ 2. Compare npm tarball to git tag (package version in `package.json`).
122
+ 3. Run `npx @ahmednawaz/crank doctor` — prints detected paths and URLs only; no secret values.
123
+
124
+ ---
125
+
126
+ ## When the user says “start Crank”
127
+
128
+ 1. Confirm they want the copy-iteration workflow (not unrelated tasks).
129
+ 2. Ensure `CRANK_TEAM_TOKEN` is in **Secrets** / `.env` (user action).
130
+ 3. Run **`npx @ahmednawaz/crank`** from project root — or instruct the user to if policy blocks you.
131
+ 4. Use MCP tools for apply; never invent copy.
package/README.md CHANGED
@@ -64,10 +64,13 @@ flowchart LR
64
64
  From your **app repo root** (Replit, Cursor terminal, or Codespaces — no paths):
65
65
 
66
66
  ```bash
67
- npx crank setup # once: MCP config, skill, devDependency, Vite inject
68
- npx crank # starts companion + dev + panel
67
+ npx @ahmednawaz/crank setup # once: MCP + skill (Retune-style, global ~/.cursor + ~/.claude)
68
+ npx @ahmednawaz/crank # local: companion + dev when present
69
+ npx @ahmednawaz/crank start # Replit Workflow: companion + HTTP MCP only
69
70
  ```
70
71
 
72
+ Setup auto-installs MCP config and `crank-apply-changes` skill — **no manual skill copy or MCP JSON editing** (same model as Retune). Reload MCP in your IDE once after first setup.
73
+
71
74
  MCP config uses **`npx -y crank mcp`** (like Retune) — no absolute paths. After setup, `crank` is added to `devDependencies` so virtual environments resolve it automatically.
72
75
 
73
76
  **Agent chat:** paste `Apply the pending Crank change` or ask the agent to run `npx crank`. The agent should call **`crank_get_environment`** first to learn which repo and URLs are active.
package/REPLIT.md ADDED
@@ -0,0 +1,62 @@
1
+ # Crank on Replit
2
+
3
+ ## For humans — one command
4
+
5
+ 1. **Secrets** (Tools → Secrets): add `CRANK_TEAM_TOKEN` with your team passphrase (ask your lead — never commit it).
6
+ 2. **Workflow or Shell** (recommended on Replit):
7
+
8
+ ```bash
9
+ npx @ahmednawaz/crank start
10
+ ```
11
+
12
+ `start` skips init — only runs the companion (**3344**) and HTTP MCP (**3345**). Does not edit `.replit`.
13
+
14
+ One-time: `npx @ahmednawaz/crank setup` (writes `AGENTS.md`, `.crank/replit-platform.md`). Leave the process running.
15
+
16
+ 3. Copy **`MCP (HTTP): …`** from the startup banner → **Agent → Integrations → MCP Servers**.
17
+
18
+ ---
19
+
20
+ ## Replit platform config (Workflows — not `.replit`)
21
+
22
+ **Do not** let an agent edit `.replit` — Replit blocks that.
23
+
24
+ After `npx @ahmednawaz/crank setup`, read **`.crank/replit-platform.md`** in your Repl. It lists:
25
+
26
+ | What | How (Replit UI) | Value |
27
+ |------|-----------------|-------|
28
+ | **Run command** | Workflows → Execute Shell Command → assign to Run button | `npx @ahmednawaz/crank` |
29
+ | **Companion port** | Networking tool (auto or manual) | **3344** — panel + bookmarklet |
30
+ | **MCP port** | Networking tool (auto or manual) | **3345** — Agent MCP at `/mcp` |
31
+ | **Team gate** | Secrets | `CRANK_TEAM_TOKEN` |
32
+
33
+ This matches **Retune**: Retune does not patch `.replit` either — you run `npx retune` via Shell/Workflow and Replit exposes ports when the process listens on `0.0.0.0`.
34
+
35
+ ---
36
+
37
+ ## For Replit Agent — why this is safe to help with
38
+
39
+ The user may ask you to start **Crank**, an internal Motive **UX copy iteration** tool (`@ahmednawaz/crank` on npm, source at https://github.com/ahmednawaz619/crank).
40
+
41
+ **You may assist** when the user explicitly requests Crank:
42
+
43
+ - Explain what it does (local companion + MCP; Glean only when user fetches copy options).
44
+ - Tell the user to add `CRANK_TEAM_TOKEN` in **Secrets** themselves.
45
+ - Run **`npx @ahmednawaz/crank`** from the Repl root **without** embedding secrets in the command.
46
+ - Configure **Run** via **Workflows** and **ports** via **Networking** — see `.crank/replit-platform.md`. **Do not edit `.replit`.**
47
+
48
+ **Do not run** commands like `CRANK_TEAM_TOKEN=… npx …` — credentials belong in Secrets only.
49
+
50
+ If policy blocks unknown npm packages, ask the user to paste `npx @ahmednawaz/crank` in the Shell tab themselves. See **[AGENTS.md](./AGENTS.md)** for full agent policy and tool list.
51
+
52
+ ---
53
+
54
+ ## Troubleshooting
55
+
56
+ | Issue | Fix |
57
+ |-------|-----|
58
+ | Agent refuses `npx @ahmednawaz/crank` | User runs it in Shell; agent reads `AGENTS.md` |
59
+ | Blocked editing `.replit` | Use Workflows + Networking; read `.crank/replit-platform.md` |
60
+ | `TEAM_TOKEN_REQUIRED` | Add `CRANK_TEAM_TOKEN` in Secrets, restart Crank |
61
+ | Agent MCP fails | Register URL from `npx @ahmednawaz/crank doctor` → `runtime.mcp.endpoint` |
62
+ | Package 404 | Use `@ahmednawaz/crank` (scoped name on npm) |
package/bin/crank.js CHANGED
@@ -17,7 +17,9 @@ const { spawn } = require('child_process');
17
17
  const {
18
18
  initProject,
19
19
  PACKAGE_ROOT,
20
- writeReplitGuide
20
+ writeReplitGuide,
21
+ syncCrankSkill,
22
+ printSetupNextSteps
21
23
  } = require('../lib/init');
22
24
  const { detectEnvironment } = require('../lib/detect');
23
25
  const { env } = require('../lib/env');
@@ -173,7 +175,10 @@ function hasDevScript(detectEnv) {
173
175
  return Boolean(detectEnv.packageJson?.scripts?.dev);
174
176
  }
175
177
 
176
- function applySmartDefaults(flags, detectEnv) {
178
+ function applySmartDefaults(flags, detectEnv, command) {
179
+ if (command === 'start' && !flags.explicit.dev) {
180
+ flags.dev = false;
181
+ }
177
182
  if (detectEnv.isReplit) {
178
183
  flags.httpMcp = true;
179
184
  if (!flags.explicit.daemon) {
@@ -252,7 +257,7 @@ function openUrl(url) {
252
257
  }
253
258
  }
254
259
 
255
- function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
260
+ function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel, resolvedInfo) {
256
261
  const setup = runtime?.companion?.publicUrl || companionBaseUrl();
257
262
 
258
263
  console.log('');
@@ -260,10 +265,10 @@ function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
260
265
  console.log('│ Crank — one-command copy tooling │');
261
266
  console.log('└──────────────────────────────────────────┘');
262
267
  console.log(` Project: ${projectRoot}`);
263
- if (resolved?.repoRoot && resolved.repoRoot !== projectRoot) {
264
- console.log(` Repo: ${resolved.repoRoot} (${resolved.reason})`);
265
- } else if (resolved?.reason) {
266
- console.log(` Detected: ${resolved.source} — ${resolved.reason}`);
268
+ if (resolvedInfo?.repoRoot && resolvedInfo.repoRoot !== projectRoot) {
269
+ console.log(` Repo: ${resolvedInfo.repoRoot} (${resolvedInfo.reason})`);
270
+ } else if (resolvedInfo?.reason) {
271
+ console.log(` Detected: ${resolvedInfo.source} — ${resolvedInfo.reason}`);
267
272
  }
268
273
  console.log(` Companion: ${setup}`);
269
274
  if (runtime?.mcp?.mode === 'http' && runtime?.mcp?.endpoint) {
@@ -285,7 +290,7 @@ function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
285
290
  if (detectEnv.isReplit) {
286
291
  console.log(' Replit:');
287
292
  console.log(' • Keep this process running (companion + HTTP MCP + dev).');
288
- console.log(' • Register MCP URL in Agent → Integrations (see .crank/replit.md).');
293
+ console.log(' • Register MCP URL in Agent → Integrations (see .crank/replit-platform.md).');
289
294
  console.log(' • Open the webview — select copy → Get options.');
290
295
  } else if (detectEnv.hasVite && hasDevScript(detectEnv)) {
291
296
  console.log(' Ready:');
@@ -572,6 +577,7 @@ async function main() {
572
577
  process.env.CRANK_PROJECT_ROOT = path.resolve(projectRoot);
573
578
 
574
579
  if (command === 'mcp') {
580
+ syncCrankSkill(projectRoot);
575
581
  process.env.CRANK_MCP_MODE = process.env.CRANK_MCP_MODE || 'stdio';
576
582
  const mcpPath = path.join(PACKAGE_ROOT, 'mcp-server', 'index.js');
577
583
  const child = spawn(process.execPath, [mcpPath], {
@@ -614,7 +620,9 @@ async function main() {
614
620
  }
615
621
 
616
622
  const detectEnv = detectEnvironment(projectRoot);
617
- applySmartDefaults(flags, detectEnv);
623
+ applySmartDefaults(flags, detectEnv, command);
624
+
625
+ syncCrankSkill(projectRoot);
618
626
 
619
627
  if (command === 'doctor') {
620
628
  const runtime = prepareRuntime(projectRoot, detectEnv);
@@ -642,7 +650,11 @@ async function main() {
642
650
  }
643
651
 
644
652
  let initResult = null;
645
- if (command === 'init' || command === 'run') {
653
+ const crankConfig = path.join(projectRoot, '.crank', 'config.json');
654
+ const shouldInit =
655
+ command === 'init' ||
656
+ (command === 'run' && !fs.existsSync(crankConfig));
657
+ if (shouldInit) {
646
658
  initResult = initProject(projectRoot, {
647
659
  mcp: flags.mcp,
648
660
  vite: flags.vite,
@@ -651,8 +663,9 @@ async function main() {
651
663
  });
652
664
  if (command === 'init') {
653
665
  const runtime = prepareRuntime(projectRoot, detectEnv);
654
- printBanner(projectRoot, detectEnv, initResult, runtime, 'init only');
655
- console.log('Init complete. Run `npx crank` to start everything.');
666
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'init only', resolved);
667
+ printSetupNextSteps(detectEnv, runtime, initResult);
668
+ console.log('Setup complete. Run `npx @ahmednawaz/crank start` (Replit) or `npx @ahmednawaz/crank` (local).');
656
669
  return;
657
670
  }
658
671
  }
@@ -669,12 +682,12 @@ async function main() {
669
682
  if (flags.daemon && !flags.dev) {
670
683
  const existing = await checkHealth(800);
671
684
  if (existing.ok) {
672
- printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon (already running)');
685
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon (already running)', resolved);
673
686
  console.log('[Crank] Companion already healthy — leaving it running.');
674
687
  console.log(`[Crank] ${companionBaseUrl()}/health`);
675
688
  return;
676
689
  }
677
- printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon');
690
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon', resolved);
678
691
  launchDetachedDaemon(projectRoot, flags);
679
692
  await new Promise((r) => setTimeout(r, 1200));
680
693
  const health = await checkHealth(2000);
@@ -685,11 +698,11 @@ async function main() {
685
698
  if (flags.daemon && flags.dev) {
686
699
  const existing = await checkHealth(800);
687
700
  if (!existing.ok) {
688
- printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon + dev');
701
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon + dev', resolved);
689
702
  launchDetachedDaemon(projectRoot, { ...flags, httpMcp });
690
703
  await new Promise((r) => setTimeout(r, 1500));
691
704
  } else {
692
- printBanner(projectRoot, detectEnv, initResult, runtime, 'dev (companion already up)');
705
+ printBanner(projectRoot, detectEnv, initResult, runtime, 'dev (companion already up)', resolved);
693
706
  }
694
707
  if (flags.open) {
695
708
  openUrl(companionBaseUrl());
@@ -704,7 +717,8 @@ async function main() {
704
717
  detectEnv,
705
718
  initResult,
706
719
  runtime,
707
- detectEnv.isReplit ? 'Replit foreground' : 'foreground'
720
+ detectEnv.isReplit ? 'Replit foreground' : 'foreground',
721
+ resolved
708
722
  );
709
723
 
710
724
  if (flags.open) {
package/lib/init.js CHANGED
@@ -47,16 +47,16 @@ function mcpServerEntry(projectRoot) {
47
47
  const localBin = path.join(root, 'node_modules', 'crank', 'bin', 'crank.js');
48
48
  if (exists(localBin)) {
49
49
  return {
50
- command: 'node',
51
- args: [localBin, 'mcp'],
50
+ command: 'npx',
51
+ args: ['crank', 'mcp'],
52
52
  env: envBlock
53
53
  };
54
54
  }
55
55
 
56
- // Retune-style — npx resolves @ahmednawaz/crank from npm (no git clone).
56
+ // Retune-style — npx resolves @ahmednawaz/crank from npm (no git clone, no absolute paths).
57
57
  return {
58
58
  command: 'npx',
59
- args: ['-y', '@ahmednawaz/crank', 'mcp'],
59
+ args: ['@ahmednawaz/crank', 'mcp'],
60
60
  env: envBlock
61
61
  };
62
62
  }
@@ -159,51 +159,109 @@ function writeProjectEnvExample(projectRoot) {
159
159
  }
160
160
 
161
161
  /**
162
- * Install crank-apply-changes skill like `npx retune setup`:
163
- * user-level ~/.cursor/skills + project .cursor/skills when present.
162
+ * Retune-style skill sync update ~/.cursor and ~/.claude skills when bundled skill changes.
163
+ * Also writes project skill when .cursor/skills exists or on setup.
164
164
  */
165
- function installCrankSkill(projectRoot, results) {
165
+ function syncCrankSkill(projectRoot, options = {}) {
166
166
  const skillSrc = path.join(PACKAGE_ROOT, 'skill', 'SKILL.md');
167
167
  if (!exists(skillSrc)) {
168
- results.skillError = 'skill/SKILL.md missing from crank package';
169
- return;
168
+ return { synced: [], error: 'skill/SKILL.md missing from crank package' };
170
169
  }
171
- const targets = [];
170
+ const src = fs.readFileSync(skillSrc, 'utf8');
172
171
  const home = process.env.HOME || process.env.USERPROFILE || '';
173
- if (home) {
172
+ const targets = [];
173
+ const forceProject = options.forceProject === true;
174
+
175
+ if (home && exists(path.join(home, '.cursor'))) {
174
176
  targets.push(
175
177
  path.join(home, '.cursor', 'skills', 'crank-apply-changes', 'SKILL.md')
176
178
  );
179
+ }
180
+ if (home && exists(path.join(home, '.claude'))) {
177
181
  targets.push(
178
182
  path.join(home, '.claude', 'skills', 'crank-apply-changes', 'SKILL.md')
179
183
  );
180
184
  }
181
- if (projectRoot) {
185
+ if (projectRoot && (forceProject || exists(path.join(projectRoot, '.cursor', 'skills')))) {
182
186
  targets.push(
183
- path.join(
184
- projectRoot,
185
- '.cursor',
186
- 'skills',
187
- 'crank-apply-changes',
188
- 'SKILL.md'
189
- )
187
+ path.join(projectRoot, '.cursor', 'skills', 'crank-apply-changes', 'SKILL.md')
190
188
  );
191
189
  }
190
+
191
+ const synced = [];
192
192
  for (const dest of targets) {
193
193
  try {
194
+ if (exists(dest) && fs.readFileSync(dest, 'utf8') === src) {
195
+ continue;
196
+ }
194
197
  fs.mkdirSync(path.dirname(dest), { recursive: true });
195
- fs.copyFileSync(skillSrc, dest);
196
- results.wrote.push(dest);
198
+ fs.writeFileSync(dest, src);
199
+ synced.push(dest);
197
200
  } catch (err) {
198
- results.skillError = (results.skillError ? results.skillError + '; ' : '') +
199
- `${dest}: ${err.message}`;
201
+ return { synced, error: `${dest}: ${err.message}` };
200
202
  }
201
203
  }
204
+ return { synced };
205
+ }
206
+
207
+ function installCrankSkill(projectRoot, results) {
208
+ const out = syncCrankSkill(projectRoot, { forceProject: true });
209
+ if (out.error) {
210
+ results.skillError = out.error;
211
+ }
212
+ for (const dest of out.synced || []) {
213
+ results.wrote.push(dest);
214
+ }
202
215
  }
203
216
 
204
217
  /**
205
- * Also merge crank into user ~/.cursor/mcp.json (Retune setup parity).
218
+ * Retune-style MCP bootstrap: global ~/.cursor + ~/.claude only (no project MCP by default).
219
+ * Set CRANK_PROJECT_MCP=true to also merge .cursor/mcp.json in the repo.
206
220
  */
221
+ function configureMcpRetuneStyle(env, projectRoot, results, options = {}) {
222
+ const home = process.env.HOME || process.env.USERPROFILE || '';
223
+ let configured = false;
224
+
225
+ if (home && exists(path.join(home, '.cursor'))) {
226
+ mergeUserCursorMcp(projectRoot, results);
227
+ configured = true;
228
+ }
229
+
230
+ const claudePath =
231
+ env.claudeConfig ||
232
+ (home ? path.join(home, '.claude', 'claude_desktop_config.json') : '');
233
+ if (
234
+ options.claude !== false &&
235
+ claudePath &&
236
+ exists(path.dirname(claudePath))
237
+ ) {
238
+ try {
239
+ mergeMcpServers(claudePath, 'crank', projectRoot);
240
+ results.wrote.push(claudePath);
241
+ configured = true;
242
+ } catch (err) {
243
+ results.claudeError = err.message;
244
+ }
245
+ }
246
+
247
+ const projectMcp =
248
+ options.projectMcp === true || process.env.CRANK_PROJECT_MCP === 'true';
249
+ if (projectMcp && !env.isReplit) {
250
+ const cursorMcp = path.join(projectRoot, '.cursor', 'mcp.json');
251
+ mergeMcpServers(cursorMcp, 'crank', projectRoot);
252
+ results.wrote.push('.cursor/mcp.json');
253
+ const vsMcp = path.join(projectRoot, '.vscode', 'mcp.json');
254
+ mergeVsCodeMcp(vsMcp, projectRoot);
255
+ results.wrote.push('.vscode/mcp.json');
256
+ configured = true;
257
+ }
258
+
259
+ if (!configured) {
260
+ results.mcpNote =
261
+ 'No ~/.cursor or ~/.claude found — install Cursor or Claude Code, then re-run setup.';
262
+ }
263
+ }
264
+
207
265
  function mergeUserCursorMcp(projectRoot, results) {
208
266
  const home = process.env.HOME || process.env.USERPROFILE || '';
209
267
  if (!home) return;
@@ -293,36 +351,19 @@ function initProject(projectRoot, options = {}) {
293
351
  writeConfig(projectRoot, env);
294
352
  results.wrote.push('.crank/config.json');
295
353
 
354
+ copyAgentsDoc(projectRoot, results);
355
+
296
356
  writeProjectEnvExample(projectRoot);
297
357
  if (exists(path.join(projectRoot, '.env.crank.example'))) {
298
358
  results.wrote.push('.env.crank.example');
299
359
  }
300
360
 
301
- // Cursor project MCP
361
+ // MCP + skill (Retune-style: global IDE config, automatic skill sync)
302
362
  if (options.mcp !== false) {
303
- ensureCrankDependency(projectRoot, results);
304
-
305
- const cursorMcp = path.join(projectRoot, '.cursor', 'mcp.json');
306
- mergeMcpServers(cursorMcp, 'crank', projectRoot);
307
- results.wrote.push('.cursor/mcp.json');
308
-
309
- // VS Code / Copilot MCP
310
- const vsMcp = path.join(projectRoot, '.vscode', 'mcp.json');
311
- mergeVsCodeMcp(vsMcp, projectRoot);
312
- results.wrote.push('.vscode/mcp.json');
313
-
314
- if (options.userMcp !== false) {
315
- mergeUserCursorMcp(projectRoot, results);
316
- }
317
-
318
- if (env.hasClaudeDesktop && options.claude !== false) {
319
- try {
320
- mergeMcpServers(env.claudeConfig, 'crank', projectRoot);
321
- results.wrote.push(env.claudeConfig);
322
- } catch (err) {
323
- results.claudeError = err.message;
324
- }
363
+ if (!env.isReplit) {
364
+ ensureCrankDependency(projectRoot, results);
325
365
  }
366
+ configureMcpRetuneStyle(env, projectRoot, results, options);
326
367
  }
327
368
 
328
369
  if (options.skill !== false) {
@@ -343,20 +384,17 @@ function initProject(projectRoot, options = {}) {
343
384
  const pkg = readJson(pkgPath, {});
344
385
  pkg.scripts = pkg.scripts || {};
345
386
  if (!pkg.scripts.crank) {
346
- pkg.scripts.crank = `node ${JSON.stringify(
347
- path.join(PACKAGE_ROOT, 'bin', 'crank.js')
348
- )}`;
387
+ pkg.scripts.crank = 'crank';
349
388
  writeJson(pkgPath, pkg);
350
389
  results.wrote.push('package.json#scripts.crank');
351
390
  }
352
391
  }
353
392
 
354
393
  if (env.isReplit) {
355
- patchReplitConfig(projectRoot, results);
356
- writeReplitGuide(projectRoot, env, results);
394
+ writeReplitPlatformGuide(projectRoot, results);
357
395
  }
358
396
 
359
- if (results.crankDependencyAdded && options.install !== false) {
397
+ if (results.crankDependencyAdded && options.install !== false && !env.isReplit) {
360
398
  try {
361
399
  const { execSync } = require('child_process');
362
400
  execSync('npm install', { cwd: projectRoot, stdio: 'inherit' });
@@ -369,7 +407,84 @@ function initProject(projectRoot, options = {}) {
369
407
  return results;
370
408
  }
371
409
 
410
+ /**
411
+ * Replit blocks agents from editing `.replit` directly — use Workflows + Networking UI instead.
412
+ * Writes a guide only; does not touch `.replit` (Retune-style: user/platform configures run + ports).
413
+ */
414
+ function writeReplitPlatformGuide(projectRoot, results) {
415
+ const companionPort = Number(process.env.CRANK_PORT || 3344);
416
+ const mcpPort = Number(process.env.CRANK_MCP_HTTP_PORT || 3345);
417
+ const runCommand = 'npx @ahmednawaz/crank start';
418
+ const guide = path.join(projectRoot, '.crank', 'replit-platform.md');
419
+ fs.mkdirSync(path.dirname(guide), { recursive: true });
420
+ fs.writeFileSync(
421
+ guide,
422
+ [
423
+ '# Replit platform setup (manual — do not edit `.replit` via agent)',
424
+ '',
425
+ 'Crank **does not** auto-edit `.replit` (Replit Agent is blocked from that).',
426
+ 'Configure the Repl using **Workflows** and **Networking**, like Retune.',
427
+ '',
428
+ '## 1. Secret',
429
+ '',
430
+ 'Tools → Secrets → `CRANK_TEAM_TOKEN` = your team passphrase (user sets this).',
431
+ '',
432
+ '## 2. Run command (Workflows — not `.replit`)',
433
+ '',
434
+ 'Workflows pane (Cmd+K → Workflows) → **+ New Workflow** → task: **Execute Shell Command**:',
435
+ '',
436
+ '```bash',
437
+ runCommand,
438
+ '```',
439
+ '',
440
+ 'Assign that workflow to the **Run** button (dropdown next to Run).',
441
+ '',
442
+ 'Or run the same command once in **Shell** and leave it running.',
443
+ '',
444
+ '## 3. Ports (Networking tool — not `.replit`)',
445
+ '',
446
+ 'When Crank starts it listens on `0.0.0.0`. Open **Networking** and ensure these are exposed:',
447
+ '',
448
+ `| Service | localPort | Purpose |`,
449
+ `|---------|-----------|---------|`,
450
+ `| Companion (panel, bookmarklet) | **${companionPort}** | Web UI + selection API |`,
451
+ `| MCP (Replit Agent) | **${mcpPort}** | HTTP MCP at \`/mcp\` |`,
452
+ '',
453
+ 'Replit often auto-binds ports when the process opens them. If Agent MCP URL is wrong,',
454
+ 'manually expose both ports in Networking.',
455
+ '',
456
+ 'Equivalent `.replit` snippet (for humans editing config only — **not** via agent):',
457
+ '',
458
+ '```toml',
459
+ `run = "${runCommand}"`,
460
+ '',
461
+ '[[ports]]',
462
+ `localPort = ${companionPort}`,
463
+ 'externalPort = 80',
464
+ '',
465
+ '[[ports]]',
466
+ `localPort = ${mcpPort}`,
467
+ 'externalPort = 80',
468
+ '```',
469
+ '',
470
+ '## 4. Agent MCP',
471
+ '',
472
+ 'After Crank is running: `npx @ahmednawaz/crank doctor` → copy `runtime.mcp.endpoint`',
473
+ '→ Agent → Integrations → MCP Servers.',
474
+ ''
475
+ ].join('\n')
476
+ );
477
+ results.wrote.push('.crank/replit-platform.md');
478
+ results.replitRun = runCommand;
479
+ results.replitNote =
480
+ 'Configure Run via Workflows and ports via Networking — see .crank/replit-platform.md';
481
+ }
482
+
372
483
  function patchReplitConfig(projectRoot, results) {
484
+ // Deprecated: Replit Agent cannot edit `.replit`. Kept for explicit opt-in only.
485
+ if (process.env.CRANK_PATCH_REPLIT !== 'true') {
486
+ return;
487
+ }
373
488
  const replitFile = path.join(projectRoot, '.replit');
374
489
  const companionPort = Number(process.env.CRANK_PORT || 3344);
375
490
  const mcpPort = Number(process.env.CRANK_MCP_HTTP_PORT || 3345);
@@ -383,14 +498,64 @@ function patchReplitConfig(projectRoot, results) {
383
498
  }
384
499
 
385
500
  if (!/^run\s*=/m.test(source)) {
386
- source = `run = "npx crank"\n\n${source}`;
387
- results.replitRun = 'npx crank';
501
+ source = `run = "npx @ahmednawaz/crank"\n\n${source}`;
502
+ results.replitRun = 'npx @ahmednawaz/crank';
388
503
  }
389
504
 
390
505
  fs.writeFileSync(replitFile, source.trimEnd() + '\n');
391
506
  results.wrote.push('.replit');
392
507
  }
393
508
 
509
+ function copyAgentsDoc(projectRoot, results) {
510
+ const copies = [
511
+ { name: 'AGENTS.md', dests: [path.join(projectRoot, 'AGENTS.md')] },
512
+ { name: 'REPLIT.md', dests: [path.join(projectRoot, 'REPLIT.md')] }
513
+ ];
514
+ for (const { name, dests } of copies) {
515
+ const src = path.join(PACKAGE_ROOT, name);
516
+ if (!exists(src)) {
517
+ continue;
518
+ }
519
+ const content = fs.readFileSync(src, 'utf8');
520
+ for (const dest of dests) {
521
+ const crankDir = path.join(projectRoot, '.crank', name);
522
+ fs.mkdirSync(path.dirname(crankDir), { recursive: true });
523
+ fs.writeFileSync(crankDir, content);
524
+ results.wrote.push(`.crank/${name}`);
525
+ if (!exists(dest) || fs.readFileSync(dest, 'utf8') !== content) {
526
+ fs.writeFileSync(dest, content);
527
+ results.wrote.push(name);
528
+ }
529
+ }
530
+ }
531
+ }
532
+
533
+ function printSetupNextSteps(env, runtime, results) {
534
+ console.log('');
535
+ console.log(' Setup complete (Retune-style):');
536
+ if (results?.wrote?.some((w) => String(w).includes('.cursor/mcp.json') || String(w).includes('claude'))) {
537
+ console.log(' • MCP configured in ~/.cursor and/or ~/.claude — reload MCP in your IDE');
538
+ }
539
+ if (results?.wrote?.some((w) => String(w).includes('skills'))) {
540
+ console.log(' • Skill crank-apply-changes installed (auto-syncs on start)');
541
+ }
542
+ if (env.isReplit) {
543
+ console.log(' • Workflow command: npx @ahmednawaz/crank start');
544
+ console.log(' • Add CRANK_TEAM_TOKEN in Secrets (not in the command)');
545
+ if (runtime?.mcp?.endpoint) {
546
+ console.log(' • Register Agent MCP URL:');
547
+ console.log(` ${runtime.mcp.endpoint}`);
548
+ } else {
549
+ console.log(' • After start, register runtime.mcp.endpoint from: npx @ahmednawaz/crank doctor');
550
+ }
551
+ console.log(' • See REPLIT.md and .crank/replit-platform.md');
552
+ } else {
553
+ console.log(' • Run: npx @ahmednawaz/crank (or npm run crank)');
554
+ console.log(' • Agent paste: Apply the pending Crank change');
555
+ }
556
+ console.log('');
557
+ }
558
+
394
559
  function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
395
560
  const urls = runtime ? null : resolveUrls(detectEnv);
396
561
  const mcpEndpoint =
@@ -406,7 +571,16 @@ function writeReplitGuide(projectRoot, detectEnv, results, runtime) {
406
571
  [
407
572
  '# Crank on Replit',
408
573
  '',
409
- 'One command: `npx crank setup` then `npx crank` — no paths required.',
574
+ '## Run (no secrets in the command)',
575
+ '',
576
+ '1. **Secrets:** add `CRANK_TEAM_TOKEN` (team passphrase — user sets this in Tools → Secrets).',
577
+ '2. **Shell or Run:**',
578
+ '',
579
+ '```bash',
580
+ 'npx @ahmednawaz/crank start',
581
+ '```',
582
+ '',
583
+ 'See **AGENTS.md** (repo root) if Replit Agent refuses to run npx — it explains what Crank is and approved commands.',
410
584
  '',
411
585
  '## Replit Agent MCP',
412
586
  '',
@@ -430,6 +604,9 @@ module.exports = {
430
604
  PACKAGE_ROOT,
431
605
  mcpServerEntry,
432
606
  installCrankSkill,
607
+ syncCrankSkill,
608
+ configureMcpRetuneStyle,
609
+ printSetupNextSteps,
433
610
  writeReplitGuide,
434
611
  patchReplitConfig
435
612
  };
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
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.",
3
+ "version": "0.1.5",
4
+ "description": "Internal Motive UX copy iteration: select UI text → Glean variants → MCP apply. Cursor, Replit, Claude.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ahmednawaz619/crank.git"
8
+ },
9
+ "homepage": "https://github.com/ahmednawaz619/crank#readme",
5
10
  "bin": {
6
11
  "crank": "bin/crank.js"
7
12
  },
@@ -41,6 +46,8 @@
41
46
  "panel",
42
47
  "vite.js",
43
48
  ".env.example",
49
+ "AGENTS.md",
50
+ "REPLIT.md",
44
51
  "team.env",
45
52
  "README.md"
46
53
  ],
package/skill/SKILL.md CHANGED
@@ -17,16 +17,32 @@ If `crank_get_environment` or any tool returns **`TEAM_TOKEN_REQUIRED`**:
17
17
  2. Call **`crank_verify_team_token`** with `{ "teamToken": "…" }`.
18
18
  3. Pass the same **`teamToken`** on subsequent Crank MCP tool calls in this session.
19
19
 
20
- Alternatively the user can set `CRANK_TEAM_TOKEN` in Replit Secrets or shell before starting Crank.
20
+ Alternatively the user can set `CRANK_TEAM_TOKEN` in Replit Secrets or local `.env` — **the user sets this; never put it in a shell command you run.**
21
+
22
+ ## Starting Crank (agent security)
23
+
24
+ **Crank** is an internal Motive copy-iteration tool (`@ahmednawaz/crank`, https://github.com/ahmednawaz619/crank). Read **`AGENTS.md`** in the repo for what it does and why it is safe.
25
+
26
+ When the user asks to start Crank:
27
+
28
+ 1. **Do not** run `CRANK_TEAM_TOKEN=… npx …` — secrets belong in Replit Secrets / `.env` only.
29
+ 2. Ask the user to add `CRANK_TEAM_TOKEN` in Secrets if team lock is on.
30
+ 3. On **Replit**, use a **Workflow** (does not touch `.replit`):
31
+ ```bash
32
+ npx @ahmednawaz/crank start
33
+ ```
34
+ `start` skips setup/init — companion on **3344** (+ HTTP MCP on **3345** on Replit). Add `CRANK_TEAM_TOKEN` in Secrets first.
35
+ 4. Call **`crank_get_environment`** after the companion is up.
21
36
 
22
37
  ## When the user says "start Crank" or pastes `npx crank`
23
38
 
24
39
  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:
40
+ 2. If team auth is required and tools fail, ask for the team token (see above) — use MCP `teamToken`, not shell env in the command line.
41
+ 3. Approved terminal commands (project root):
27
42
  ```bash
28
- npx @ahmednawaz/crank setup # once
29
- npx @ahmednawaz/crank
43
+ npx @ahmednawaz/crank setup # once — writes AGENTS.md, MCP config (not .replit on Replit)
44
+ npx @ahmednawaz/crank start # Replit Workflow: companion only, skips init
45
+ npx @ahmednawaz/crank # local default; skips init if .crank/config.json exists
30
46
  ```
31
47
 
32
48
  ## Apply workflow