@ahmednawaz/crank 0.1.2 → 0.1.6
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 +131 -0
- package/README.md +5 -2
- package/REPLIT.md +62 -0
- package/bin/crank.js +35 -18
- package/lib/init.js +233 -56
- package/package.json +19 -3
- package/skill/SKILL.md +21 -5
- package/team.env +1 -1
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
|
|
68
|
-
npx crank #
|
|
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,13 @@ 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.daemon) {
|
|
180
|
+
flags.daemon = false;
|
|
181
|
+
if (!flags.explicit.dev) {
|
|
182
|
+
flags.dev = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
177
185
|
if (detectEnv.isReplit) {
|
|
178
186
|
flags.httpMcp = true;
|
|
179
187
|
if (!flags.explicit.daemon) {
|
|
@@ -252,7 +260,7 @@ function openUrl(url) {
|
|
|
252
260
|
}
|
|
253
261
|
}
|
|
254
262
|
|
|
255
|
-
function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
|
|
263
|
+
function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel, resolvedInfo) {
|
|
256
264
|
const setup = runtime?.companion?.publicUrl || companionBaseUrl();
|
|
257
265
|
|
|
258
266
|
console.log('');
|
|
@@ -260,10 +268,10 @@ function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
|
|
|
260
268
|
console.log('│ Crank — one-command copy tooling │');
|
|
261
269
|
console.log('└──────────────────────────────────────────┘');
|
|
262
270
|
console.log(` Project: ${projectRoot}`);
|
|
263
|
-
if (
|
|
264
|
-
console.log(` Repo: ${
|
|
265
|
-
} else if (
|
|
266
|
-
console.log(` Detected: ${
|
|
271
|
+
if (resolvedInfo?.repoRoot && resolvedInfo.repoRoot !== projectRoot) {
|
|
272
|
+
console.log(` Repo: ${resolvedInfo.repoRoot} (${resolvedInfo.reason})`);
|
|
273
|
+
} else if (resolvedInfo?.reason) {
|
|
274
|
+
console.log(` Detected: ${resolvedInfo.source} — ${resolvedInfo.reason}`);
|
|
267
275
|
}
|
|
268
276
|
console.log(` Companion: ${setup}`);
|
|
269
277
|
if (runtime?.mcp?.mode === 'http' && runtime?.mcp?.endpoint) {
|
|
@@ -285,7 +293,7 @@ function printBanner(projectRoot, detectEnv, initResult, runtime, modeLabel) {
|
|
|
285
293
|
if (detectEnv.isReplit) {
|
|
286
294
|
console.log(' Replit:');
|
|
287
295
|
console.log(' • Keep this process running (companion + HTTP MCP + dev).');
|
|
288
|
-
console.log(' • Register MCP URL in Agent → Integrations (see .crank/replit.md).');
|
|
296
|
+
console.log(' • Register MCP URL in Agent → Integrations (see .crank/replit-platform.md).');
|
|
289
297
|
console.log(' • Open the webview — select copy → Get options.');
|
|
290
298
|
} else if (detectEnv.hasVite && hasDevScript(detectEnv)) {
|
|
291
299
|
console.log(' Ready:');
|
|
@@ -494,7 +502,7 @@ function runCompanionSupervisor(projectRoot, { foreground, httpMcp }) {
|
|
|
494
502
|
}
|
|
495
503
|
|
|
496
504
|
function launchDetachedDaemon(projectRoot, argvFlags) {
|
|
497
|
-
const args = [__filename, '
|
|
505
|
+
const args = [__filename, 'run', '--daemon', '--no-open', projectRoot];
|
|
498
506
|
if (argvFlags.httpMcp) {
|
|
499
507
|
args.push('--http-mcp');
|
|
500
508
|
}
|
|
@@ -572,6 +580,7 @@ async function main() {
|
|
|
572
580
|
process.env.CRANK_PROJECT_ROOT = path.resolve(projectRoot);
|
|
573
581
|
|
|
574
582
|
if (command === 'mcp') {
|
|
583
|
+
syncCrankSkill(projectRoot);
|
|
575
584
|
process.env.CRANK_MCP_MODE = process.env.CRANK_MCP_MODE || 'stdio';
|
|
576
585
|
const mcpPath = path.join(PACKAGE_ROOT, 'mcp-server', 'index.js');
|
|
577
586
|
const child = spawn(process.execPath, [mcpPath], {
|
|
@@ -614,7 +623,9 @@ async function main() {
|
|
|
614
623
|
}
|
|
615
624
|
|
|
616
625
|
const detectEnv = detectEnvironment(projectRoot);
|
|
617
|
-
applySmartDefaults(flags, detectEnv);
|
|
626
|
+
applySmartDefaults(flags, detectEnv, command);
|
|
627
|
+
|
|
628
|
+
syncCrankSkill(projectRoot);
|
|
618
629
|
|
|
619
630
|
if (command === 'doctor') {
|
|
620
631
|
const runtime = prepareRuntime(projectRoot, detectEnv);
|
|
@@ -642,7 +653,11 @@ async function main() {
|
|
|
642
653
|
}
|
|
643
654
|
|
|
644
655
|
let initResult = null;
|
|
645
|
-
|
|
656
|
+
const crankConfig = path.join(projectRoot, '.crank', 'config.json');
|
|
657
|
+
const shouldInit =
|
|
658
|
+
command === 'init' ||
|
|
659
|
+
(command === 'run' && !fs.existsSync(crankConfig));
|
|
660
|
+
if (shouldInit) {
|
|
646
661
|
initResult = initProject(projectRoot, {
|
|
647
662
|
mcp: flags.mcp,
|
|
648
663
|
vite: flags.vite,
|
|
@@ -651,8 +666,9 @@ async function main() {
|
|
|
651
666
|
});
|
|
652
667
|
if (command === 'init') {
|
|
653
668
|
const runtime = prepareRuntime(projectRoot, detectEnv);
|
|
654
|
-
printBanner(projectRoot, detectEnv, initResult, runtime, 'init only');
|
|
655
|
-
|
|
669
|
+
printBanner(projectRoot, detectEnv, initResult, runtime, 'init only', resolved);
|
|
670
|
+
printSetupNextSteps(detectEnv, runtime, initResult);
|
|
671
|
+
console.log('Setup complete. Run `npx @ahmednawaz/crank start` (Replit) or `npx @ahmednawaz/crank` (local).');
|
|
656
672
|
return;
|
|
657
673
|
}
|
|
658
674
|
}
|
|
@@ -669,12 +685,12 @@ async function main() {
|
|
|
669
685
|
if (flags.daemon && !flags.dev) {
|
|
670
686
|
const existing = await checkHealth(800);
|
|
671
687
|
if (existing.ok) {
|
|
672
|
-
printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon (already running)');
|
|
688
|
+
printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon (already running)', resolved);
|
|
673
689
|
console.log('[Crank] Companion already healthy — leaving it running.');
|
|
674
690
|
console.log(`[Crank] ${companionBaseUrl()}/health`);
|
|
675
691
|
return;
|
|
676
692
|
}
|
|
677
|
-
printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon');
|
|
693
|
+
printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon', resolved);
|
|
678
694
|
launchDetachedDaemon(projectRoot, flags);
|
|
679
695
|
await new Promise((r) => setTimeout(r, 1200));
|
|
680
696
|
const health = await checkHealth(2000);
|
|
@@ -685,11 +701,11 @@ async function main() {
|
|
|
685
701
|
if (flags.daemon && flags.dev) {
|
|
686
702
|
const existing = await checkHealth(800);
|
|
687
703
|
if (!existing.ok) {
|
|
688
|
-
printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon + dev');
|
|
704
|
+
printBanner(projectRoot, detectEnv, initResult, runtime, 'daemon + dev', resolved);
|
|
689
705
|
launchDetachedDaemon(projectRoot, { ...flags, httpMcp });
|
|
690
706
|
await new Promise((r) => setTimeout(r, 1500));
|
|
691
707
|
} else {
|
|
692
|
-
printBanner(projectRoot, detectEnv, initResult, runtime, 'dev (companion already up)');
|
|
708
|
+
printBanner(projectRoot, detectEnv, initResult, runtime, 'dev (companion already up)', resolved);
|
|
693
709
|
}
|
|
694
710
|
if (flags.open) {
|
|
695
711
|
openUrl(companionBaseUrl());
|
|
@@ -704,7 +720,8 @@ async function main() {
|
|
|
704
720
|
detectEnv,
|
|
705
721
|
initResult,
|
|
706
722
|
runtime,
|
|
707
|
-
detectEnv.isReplit ? 'Replit foreground' : 'foreground'
|
|
723
|
+
detectEnv.isReplit ? 'Replit foreground' : 'foreground',
|
|
724
|
+
resolved
|
|
708
725
|
);
|
|
709
726
|
|
|
710
727
|
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: '
|
|
51
|
-
args: [
|
|
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: ['
|
|
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
|
-
*
|
|
163
|
-
*
|
|
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
|
|
165
|
+
function syncCrankSkill(projectRoot, options = {}) {
|
|
166
166
|
const skillSrc = path.join(PACKAGE_ROOT, 'skill', 'SKILL.md');
|
|
167
167
|
if (!exists(skillSrc)) {
|
|
168
|
-
|
|
169
|
-
return;
|
|
168
|
+
return { synced: [], error: 'skill/SKILL.md missing from crank package' };
|
|
170
169
|
}
|
|
171
|
-
const
|
|
170
|
+
const src = fs.readFileSync(skillSrc, 'utf8');
|
|
172
171
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
173
|
-
|
|
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.
|
|
196
|
-
|
|
198
|
+
fs.writeFileSync(dest, src);
|
|
199
|
+
synced.push(dest);
|
|
197
200
|
} catch (err) {
|
|
198
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
361
|
+
// MCP + skill (Retune-style: global IDE config, automatic skill sync)
|
|
302
362
|
if (options.mcp !== false) {
|
|
303
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
'
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.6",
|
|
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
|
},
|
|
@@ -25,11 +30,20 @@
|
|
|
25
30
|
"test:parse": "node scripts/test-parse-options.js",
|
|
26
31
|
"test:resolve": "node scripts/test-resolve-project.js",
|
|
27
32
|
"prepare:publish": "node scripts/prepare-publish.js",
|
|
28
|
-
"
|
|
33
|
+
"verify:deps": "node scripts/verify-deps.js",
|
|
34
|
+
"publish:team": "node scripts/prepare-publish.js && node scripts/verify-deps.js && npm publish --access public"
|
|
29
35
|
},
|
|
30
36
|
"engines": {
|
|
31
37
|
"node": ">=18"
|
|
32
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@cursor/sdk": "^1.0.23",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
42
|
+
"cors": "^2.8.5",
|
|
43
|
+
"express": "^4.21.2",
|
|
44
|
+
"ws": "^8.18.0",
|
|
45
|
+
"zod": "^3.24.2"
|
|
46
|
+
},
|
|
33
47
|
"files": [
|
|
34
48
|
"bin",
|
|
35
49
|
"lib",
|
|
@@ -41,6 +55,8 @@
|
|
|
41
55
|
"panel",
|
|
42
56
|
"vite.js",
|
|
43
57
|
".env.example",
|
|
58
|
+
"AGENTS.md",
|
|
59
|
+
"REPLIT.md",
|
|
44
60
|
"team.env",
|
|
45
61
|
"README.md"
|
|
46
62
|
],
|
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
|
|
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.
|
|
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
|