rush-ai 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/README.md +206 -7
- package/dist/{chunk-UJLVFMWM.js → chunk-SZO6LEQX.js} +285 -101
- package/dist/chunk-SZO6LEQX.js.map +1 -0
- package/dist/index.js +7701 -1334
- package/dist/index.js.map +1 -1
- package/dist/plugin-assets/SKILL.md +66 -59
- package/dist/{server-MRWAFK5G.js → server-KFDYSF4T.js} +3 -5
- package/dist/{server-MRWAFK5G.js.map → server-KFDYSF4T.js.map} +1 -1
- package/dist/skills/README.md +48 -0
- package/dist/skills/agent-shelf.md +102 -0
- package/dist/skills/hand-off.md +163 -0
- package/package.json +4 -1
- package/skills/README.md +48 -0
- package/skills/agent-shelf.md +102 -0
- package/skills/hand-off.md +163 -0
- package/dist/chunk-MG4HY2PD.js +0 -136
- package/dist/chunk-MG4HY2PD.js.map +0 -1
- package/dist/chunk-UJLVFMWM.js.map +0 -1
- package/dist/client-NTECGN3O.js +0 -10
- package/dist/client-NTECGN3O.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Skill: Hand off the task to Rush
|
|
2
|
+
|
|
3
|
+
Use this skill when the user is talking to you inside an IDE (Cursor, Claude Code, or similar) and the next step belongs on the Rush platform. You already have full context from the conversation — `rush-ai` exists so you can relay that context to Rush's cloud agents without making the user summarize it again.
|
|
4
|
+
|
|
5
|
+
## When this skill applies
|
|
6
|
+
|
|
7
|
+
Triggers (not exhaustive — use judgement):
|
|
8
|
+
|
|
9
|
+
- "Build me a landing page / product site / marketing page."
|
|
10
|
+
- "Turn this spec / sketch / Figma link into a site."
|
|
11
|
+
- "Spin up a demo site for X."
|
|
12
|
+
- Any time the conversation points at producing a running web artifact the user will want to **share a link** for.
|
|
13
|
+
|
|
14
|
+
Do NOT use this skill for:
|
|
15
|
+
|
|
16
|
+
- Small local edits the user wants you to apply directly in the current repo.
|
|
17
|
+
- Questions about Rush itself ("what is rush-ai", "how do I log in") — answer those from docs instead of creating a task.
|
|
18
|
+
|
|
19
|
+
## Decision: which agent?
|
|
20
|
+
|
|
21
|
+
| Situation | Agent to pass to `-a` |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Build a website, landing page, marketing page, or anything that should render as a live preview URL | `web-builder` |
|
|
24
|
+
| General-purpose conversation / analysis / generation the user would have asked a chat agent for | `rush` (the platform default) |
|
|
25
|
+
| User named a specific specialist agent (e.g. "use the HR analytics agent") | that agent name verbatim |
|
|
26
|
+
|
|
27
|
+
If you are unsure, start with `npx rush-ai agent list --default` — that shows just the built-in defaults (`web-builder`, `rush`, `skill-publisher`, ...), which is usually all you need for a hand-off. Widen with `--search <keyword>` or `--all` only when the user clearly asked for a specialist. When in doubt between `rush` and `web-builder`, ask the user — but if the output is going to be a URL, default to `web-builder`.
|
|
28
|
+
|
|
29
|
+
## Playbook
|
|
30
|
+
|
|
31
|
+
### 1. Verify auth (once per session)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx rush-ai auth status --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The response includes a boolean `authenticated` (or equivalent) plus an auth method. **Read it before you do anything else.** There are only two cases — no third case, no "maybe":
|
|
38
|
+
|
|
39
|
+
- **Authenticated** → proceed to step 2.
|
|
40
|
+
- **Not authenticated** → STOP. Print this exact message to the user and wait:
|
|
41
|
+
|
|
42
|
+
> Rush 还没登录。请在终端里跑 `npx rush-ai auth login`(会打开浏览器完成 CAS 登录),登录成功后告诉我一声。
|
|
43
|
+
|
|
44
|
+
Do NOT run `auth login` yourself — it opens a browser and waits for human interaction.
|
|
45
|
+
Do NOT invent workarounds (don't try API keys, don't edit `~/.rush/auth.json`).
|
|
46
|
+
Do NOT fall through to `task create` anyway — it will return 401 and confuse the user.
|
|
47
|
+
|
|
48
|
+
When the user returns, re-run `auth status --json` to confirm before continuing.
|
|
49
|
+
|
|
50
|
+
### 2. Create the task, carrying the context you already have
|
|
51
|
+
|
|
52
|
+
Write the prompt from the conversation. Do **not** ask the user to restate requirements you already know. Include any constraints they have already mentioned (brand, style, pages, copy).
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx rush-ai task create \
|
|
56
|
+
-a web-builder \
|
|
57
|
+
-p "<prompt synthesized from the conversation>" \
|
|
58
|
+
--json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The JSON response contains `id`. Keep it; you'll need it for every follow-up.
|
|
62
|
+
|
|
63
|
+
### 3. Report the handoff to the user
|
|
64
|
+
|
|
65
|
+
Tell the user, in one line:
|
|
66
|
+
|
|
67
|
+
- the task id
|
|
68
|
+
- that it's running on Rush
|
|
69
|
+
- how they can watch (either "I'll poll for you" or "open the Rush web UI")
|
|
70
|
+
|
|
71
|
+
Don't dump the full JSON unless they ask.
|
|
72
|
+
|
|
73
|
+
### 4. Poll status until ready
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx rush-ai task status <id> --json | jq -r '.status'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Terminal states: `completed`, `failed`, `cancelled`. Poll every ~15–30s; don't hammer it.
|
|
80
|
+
|
|
81
|
+
**Pipe directly** — don't round-trip the JSON through a shell variable:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# good — JSON stays as raw bytes, no shell re-interpretation
|
|
85
|
+
npx rush-ai task status <id> --json | jq -r '.status'
|
|
86
|
+
|
|
87
|
+
# bad — zsh's builtin `echo` re-interprets \n as a real newline;
|
|
88
|
+
# jq then rejects the result with "control characters must be escaped".
|
|
89
|
+
# (bash's echo usually doesn't, but behavior is shell-dependent.)
|
|
90
|
+
STATUS=$(npx rush-ai task status <id> --json)
|
|
91
|
+
echo "$STATUS" | jq . # ← breaks in zsh once the JSON contains \n
|
|
92
|
+
|
|
93
|
+
# acceptable alternatives when you really need the variable:
|
|
94
|
+
printf '%s' "$STATUS" | jq . # portable — printf never re-interprets escapes
|
|
95
|
+
jq . <<< "$STATUS" # here-string — does not re-interpret backslash escapes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When `status == completed`, the response contains (for `web-builder`):
|
|
99
|
+
|
|
100
|
+
- `previewUrl` — shareable live preview (e.g. `https://<id>-preview.rush.zhenguanyu.com/`)
|
|
101
|
+
- `gitRepoUrl` — cloneable repo (e.g. `https://gitlab-ee.zhenguanyu.com/rush/online/<id>`)
|
|
102
|
+
|
|
103
|
+
Present both to the user. The preview URL is the primary artifact they care about.
|
|
104
|
+
|
|
105
|
+
### 5. Iterate via `task send`, not by creating new tasks
|
|
106
|
+
|
|
107
|
+
If the user says "change the button color" / "add a testimonials section" / "make the hero bigger", **do not create a new task** — send a follow-up to the existing one:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx rush-ai task send <id> -p "<the change they asked for>" --json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The preview URL stays the same; it will refresh after the send completes. Poll `task status <id>` again.
|
|
114
|
+
|
|
115
|
+
Only create a fresh task if the user explicitly starts a new, unrelated project.
|
|
116
|
+
|
|
117
|
+
### 6. Failure handling
|
|
118
|
+
|
|
119
|
+
If `status == failed`, the response has an `error` field. Surface it to the user verbatim and ask whether to:
|
|
120
|
+
|
|
121
|
+
1. retry with a revised prompt via `task send`,
|
|
122
|
+
2. cancel and start over, or
|
|
123
|
+
3. dig in themselves (give them the task id so they can look it up).
|
|
124
|
+
|
|
125
|
+
Do not silently retry.
|
|
126
|
+
|
|
127
|
+
## Example end-to-end
|
|
128
|
+
|
|
129
|
+
User (in Cursor): "帮我做个公司官网首页,风格大气专业。"
|
|
130
|
+
|
|
131
|
+
Your actions:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 1. check auth (assume ok)
|
|
135
|
+
npx rush-ai auth status --json
|
|
136
|
+
|
|
137
|
+
# 2. create (prompt synthesized from the prior conversation — brand, tone, pages)
|
|
138
|
+
npx rush-ai task create -a web-builder \
|
|
139
|
+
-p "做一个公司官网首页,包含导航、Hero、核心特性、客户评价和底部联系信息,风格大气专业。" \
|
|
140
|
+
--json
|
|
141
|
+
# → {"id": "lodig8oknq0r", "status": "pending", ...}
|
|
142
|
+
|
|
143
|
+
# 3. tell the user
|
|
144
|
+
# "已经派到 Rush 的 web-builder,任务 id lodig8oknq0r,我帮你盯着。"
|
|
145
|
+
|
|
146
|
+
# 4. poll
|
|
147
|
+
npx rush-ai task status lodig8oknq0r --json
|
|
148
|
+
# ...wait...
|
|
149
|
+
# → {"status": "completed", "previewUrl": "https://lodig8oknq0r-preview.rush.zhenguanyu.com/", "gitRepoUrl": "..."}
|
|
150
|
+
|
|
151
|
+
# 5. present
|
|
152
|
+
# "跑完了:
|
|
153
|
+
# 预览: https://lodig8oknq0r-preview.rush.zhenguanyu.com/
|
|
154
|
+
# 代码: https://gitlab-ee.zhenguanyu.com/rush/online/lodig8oknq0r
|
|
155
|
+
# 要改哪儿继续说,我走 task send 不新起任务。"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Anti-patterns
|
|
159
|
+
|
|
160
|
+
- ❌ Asking the user to re-describe requirements they already told you in-thread.
|
|
161
|
+
- ❌ Creating a new task for every tweak — use `task send`.
|
|
162
|
+
- ❌ Hiding the task id from the user (they may want to reopen the Rush UI later).
|
|
163
|
+
- ❌ Running `task watch` when all you need is final status — it streams verbosely, use `task status` for quick polls.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rush-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Rush CLI - Command-line interface for the Rush AI platform",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
+
"skills",
|
|
18
19
|
"README.md",
|
|
20
|
+
"AGENTS.md",
|
|
19
21
|
"LICENSE"
|
|
20
22
|
],
|
|
21
23
|
"publishConfig": {
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"mcp"
|
|
36
38
|
],
|
|
37
39
|
"dependencies": {
|
|
40
|
+
"@iarna/toml": "^2.2.5",
|
|
38
41
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
39
42
|
"chalk": "^5.4.1",
|
|
40
43
|
"commander": "^13.1.0",
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Rush-ai skills for AI agents
|
|
2
|
+
|
|
3
|
+
`rush-ai` is a CLI for the Rush platform. If you are an AI agent (Cursor, Claude Code, a local sub-agent) reading this, these guides tell you when and how to invoke `rush-ai` on behalf of the user.
|
|
4
|
+
|
|
5
|
+
## When to use rush-ai
|
|
6
|
+
|
|
7
|
+
Pick the guide that matches the situation:
|
|
8
|
+
|
|
9
|
+
- **You are in the middle of a conversation with the user (inside an IDE) and part of the work belongs on Rush.**
|
|
10
|
+
→ Read [`hand-off.md`](./hand-off.md). Typical trigger: "build me a landing page", "turn this sketch into a site", "generate the docs site".
|
|
11
|
+
|
|
12
|
+
- **You are composing a workflow and need a specialist agent as one of the steps.**
|
|
13
|
+
→ Read [`agent-shelf.md`](./agent-shelf.md). Typical trigger: a workflow node that needs domain expertise you don't have (HR analytics, observability Q&A, physics reasoning, etc).
|
|
14
|
+
|
|
15
|
+
Still unsure? Run `npx rush-ai agent list` first — seeing the available agents usually makes the decision obvious.
|
|
16
|
+
|
|
17
|
+
## Cheat sheet
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Discover
|
|
21
|
+
npx rush-ai agent list --default # just the built-in defaults (web-builder, rush, skill-publisher, ...)
|
|
22
|
+
npx rush-ai agent list --search "人力" # fuzzy-filter the full shelf
|
|
23
|
+
npx rush-ai agent list --all # everything (can be long)
|
|
24
|
+
npx rush-ai agent info <agent> # inspect one agent's skills / MCP servers
|
|
25
|
+
|
|
26
|
+
# Hand off
|
|
27
|
+
npx rush-ai task create -a <agent> -p "<prompt>" # create a task
|
|
28
|
+
npx rush-ai task status <task-id> # check status (includes preview / git URLs when ready)
|
|
29
|
+
npx rush-ai task send <task-id> -p "<follow-up>" # iterate on a running task
|
|
30
|
+
npx rush-ai task watch <task-id> # stream execution (SSE)
|
|
31
|
+
|
|
32
|
+
# Defaults
|
|
33
|
+
-a rush # general-purpose agent; this is the CLI default, so `-a` can be omitted when you want `rush`
|
|
34
|
+
-a web-builder # site/landing-page builder; returns previewUrl + gitRepoUrl
|
|
35
|
+
-a <other> # any other agent name from `agent list` — confirm it exists first
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`--json` on any command gives machine-readable output; use it when you need to parse the result.
|
|
39
|
+
|
|
40
|
+
## Prerequisites the agent should enforce
|
|
41
|
+
|
|
42
|
+
Before creating a task, make sure the user is authenticated:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx rush-ai auth status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If not authenticated, tell the user to run `npx rush-ai auth login` in their terminal (the command is interactive and requires a browser — you cannot complete it on their behalf).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Skill: Call a Rush agent as your sub-agent
|
|
2
|
+
|
|
3
|
+
Use this skill when you are composing a workflow — your own, or one the user described — and a step calls for domain expertise you don't have. Rush hosts a shelf of specialist agents. You can invoke any of them with one `rush-ai task create` call and feed the result back into your workflow.
|
|
4
|
+
|
|
5
|
+
## When this skill applies
|
|
6
|
+
|
|
7
|
+
Triggers:
|
|
8
|
+
|
|
9
|
+
- The user's instructions name a specialist by role ("use the HR analytics expert", "ask the physics reasoning agent", "let the observability agent check it").
|
|
10
|
+
- You're drafting a multi-step plan and one node is outside your competence (e.g. "analyze RUM data", "write a physics problem set", "summarize an HR report").
|
|
11
|
+
- The user asks "what can Rush do that I can't?" — that's a prompt to show the shelf.
|
|
12
|
+
|
|
13
|
+
Do NOT use this skill when:
|
|
14
|
+
|
|
15
|
+
- The task fits the default general-purpose agent (`rush`) — that's a hand-off, not a sub-agent call. See `hand-off.md`.
|
|
16
|
+
- The task needs a live web artifact — use `web-builder` via `hand-off.md`.
|
|
17
|
+
|
|
18
|
+
## Playbook
|
|
19
|
+
|
|
20
|
+
### 1. Discover — don't guess
|
|
21
|
+
|
|
22
|
+
Always start by listing the shelf, unless the user already named a specific agent. Use the right scope for the question at hand:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Just the built-in defaults — useful when you're looking for the general fallback
|
|
26
|
+
npx rush-ai agent list --default --json
|
|
27
|
+
|
|
28
|
+
# Fuzzy-filter when the user hinted at a domain
|
|
29
|
+
npx rush-ai agent list --search "人力" --json
|
|
30
|
+
|
|
31
|
+
# Full catalog — use sparingly, can be hundreds of entries
|
|
32
|
+
npx rush-ai agent list --all --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Pick the best match by name + description. If multiple are plausible, inspect one:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx rush-ai agent info <agent-name> --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If nothing fits, tell the user what's available and ask how to proceed — don't force-fit a wrong agent.
|
|
42
|
+
|
|
43
|
+
### 2. Call it as a node in your workflow
|
|
44
|
+
|
|
45
|
+
Treat the agent call like a pure function: input prompt, output task result. Compose your own prompt with the context the agent needs (not the whole conversation — just the inputs for this step).
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx rush-ai task create -a <agent-name> \
|
|
49
|
+
-p "<focused prompt — inputs + expected output format>" \
|
|
50
|
+
--json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For a workflow node you need synchronously, poll:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx rush-ai task status <id> --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When `status == completed`, the response has the output. For agents that produce structured results, request `--json` and parse it.
|
|
60
|
+
|
|
61
|
+
### 3. Feed the result back into your workflow
|
|
62
|
+
|
|
63
|
+
The sub-agent's output is just another piece of context for the next step of your plan. Incorporate it into the next action the way you would any tool result.
|
|
64
|
+
|
|
65
|
+
If the output is unsatisfactory, either:
|
|
66
|
+
|
|
67
|
+
- refine via `task send <id> -p "<clarification>"` (same sub-agent, same context), or
|
|
68
|
+
- fall back to your plan B (try a different agent, or escalate to the user).
|
|
69
|
+
|
|
70
|
+
## Example
|
|
71
|
+
|
|
72
|
+
User: "帮我分析一下 Q1 的部门人效,然后根据结论写一段给 CEO 的周会汇报。"
|
|
73
|
+
|
|
74
|
+
Your plan:
|
|
75
|
+
|
|
76
|
+
1. Call a data-analysis sub-agent on Rush to do the number-crunching.
|
|
77
|
+
2. Use the analysis output to draft the exec-summary locally.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Step 1 — find the HR analytics specialist (--json returns { agents, pagination })
|
|
81
|
+
npx rush-ai agent list --search "人力" --json | jq '.agents[] | {name, description}'
|
|
82
|
+
# → matches "人力资源分析专家"
|
|
83
|
+
|
|
84
|
+
# Step 2 — call it as a sub-agent node
|
|
85
|
+
npx rush-ai task create -a 人力资源分析专家 \
|
|
86
|
+
-p "分析 Q1 各部门人效趋势,输出 3 条关键结论。" \
|
|
87
|
+
--json
|
|
88
|
+
# → {"id": "t_abc", "status": "pending"}
|
|
89
|
+
|
|
90
|
+
# Step 3 — wait for the node to finish
|
|
91
|
+
npx rush-ai task status t_abc --json
|
|
92
|
+
# → {"status": "completed", "result": "..."}
|
|
93
|
+
|
|
94
|
+
# Step 4 — you take the result and draft the exec summary locally.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Anti-patterns
|
|
98
|
+
|
|
99
|
+
- ❌ Inventing an agent name. If `agent list` doesn't show it, it doesn't exist — tell the user.
|
|
100
|
+
- ❌ Passing the entire original conversation as the sub-agent's prompt. Give it only the inputs it needs.
|
|
101
|
+
- ❌ Using `web-builder` for non-website work. It's specialized for producing deployable sites.
|
|
102
|
+
- ❌ Abandoning the workflow if the sub-agent call fails. Surface the error, offer a fallback.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Skill: Hand off the task to Rush
|
|
2
|
+
|
|
3
|
+
Use this skill when the user is talking to you inside an IDE (Cursor, Claude Code, or similar) and the next step belongs on the Rush platform. You already have full context from the conversation — `rush-ai` exists so you can relay that context to Rush's cloud agents without making the user summarize it again.
|
|
4
|
+
|
|
5
|
+
## When this skill applies
|
|
6
|
+
|
|
7
|
+
Triggers (not exhaustive — use judgement):
|
|
8
|
+
|
|
9
|
+
- "Build me a landing page / product site / marketing page."
|
|
10
|
+
- "Turn this spec / sketch / Figma link into a site."
|
|
11
|
+
- "Spin up a demo site for X."
|
|
12
|
+
- Any time the conversation points at producing a running web artifact the user will want to **share a link** for.
|
|
13
|
+
|
|
14
|
+
Do NOT use this skill for:
|
|
15
|
+
|
|
16
|
+
- Small local edits the user wants you to apply directly in the current repo.
|
|
17
|
+
- Questions about Rush itself ("what is rush-ai", "how do I log in") — answer those from docs instead of creating a task.
|
|
18
|
+
|
|
19
|
+
## Decision: which agent?
|
|
20
|
+
|
|
21
|
+
| Situation | Agent to pass to `-a` |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Build a website, landing page, marketing page, or anything that should render as a live preview URL | `web-builder` |
|
|
24
|
+
| General-purpose conversation / analysis / generation the user would have asked a chat agent for | `rush` (the platform default) |
|
|
25
|
+
| User named a specific specialist agent (e.g. "use the HR analytics agent") | that agent name verbatim |
|
|
26
|
+
|
|
27
|
+
If you are unsure, start with `npx rush-ai agent list --default` — that shows just the built-in defaults (`web-builder`, `rush`, `skill-publisher`, ...), which is usually all you need for a hand-off. Widen with `--search <keyword>` or `--all` only when the user clearly asked for a specialist. When in doubt between `rush` and `web-builder`, ask the user — but if the output is going to be a URL, default to `web-builder`.
|
|
28
|
+
|
|
29
|
+
## Playbook
|
|
30
|
+
|
|
31
|
+
### 1. Verify auth (once per session)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx rush-ai auth status --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The response includes a boolean `authenticated` (or equivalent) plus an auth method. **Read it before you do anything else.** There are only two cases — no third case, no "maybe":
|
|
38
|
+
|
|
39
|
+
- **Authenticated** → proceed to step 2.
|
|
40
|
+
- **Not authenticated** → STOP. Print this exact message to the user and wait:
|
|
41
|
+
|
|
42
|
+
> Rush 还没登录。请在终端里跑 `npx rush-ai auth login`(会打开浏览器完成 CAS 登录),登录成功后告诉我一声。
|
|
43
|
+
|
|
44
|
+
Do NOT run `auth login` yourself — it opens a browser and waits for human interaction.
|
|
45
|
+
Do NOT invent workarounds (don't try API keys, don't edit `~/.rush/auth.json`).
|
|
46
|
+
Do NOT fall through to `task create` anyway — it will return 401 and confuse the user.
|
|
47
|
+
|
|
48
|
+
When the user returns, re-run `auth status --json` to confirm before continuing.
|
|
49
|
+
|
|
50
|
+
### 2. Create the task, carrying the context you already have
|
|
51
|
+
|
|
52
|
+
Write the prompt from the conversation. Do **not** ask the user to restate requirements you already know. Include any constraints they have already mentioned (brand, style, pages, copy).
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx rush-ai task create \
|
|
56
|
+
-a web-builder \
|
|
57
|
+
-p "<prompt synthesized from the conversation>" \
|
|
58
|
+
--json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The JSON response contains `id`. Keep it; you'll need it for every follow-up.
|
|
62
|
+
|
|
63
|
+
### 3. Report the handoff to the user
|
|
64
|
+
|
|
65
|
+
Tell the user, in one line:
|
|
66
|
+
|
|
67
|
+
- the task id
|
|
68
|
+
- that it's running on Rush
|
|
69
|
+
- how they can watch (either "I'll poll for you" or "open the Rush web UI")
|
|
70
|
+
|
|
71
|
+
Don't dump the full JSON unless they ask.
|
|
72
|
+
|
|
73
|
+
### 4. Poll status until ready
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx rush-ai task status <id> --json | jq -r '.status'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Terminal states: `completed`, `failed`, `cancelled`. Poll every ~15–30s; don't hammer it.
|
|
80
|
+
|
|
81
|
+
**Pipe directly** — don't round-trip the JSON through a shell variable:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# good — JSON stays as raw bytes, no shell re-interpretation
|
|
85
|
+
npx rush-ai task status <id> --json | jq -r '.status'
|
|
86
|
+
|
|
87
|
+
# bad — zsh's builtin `echo` re-interprets \n as a real newline;
|
|
88
|
+
# jq then rejects the result with "control characters must be escaped".
|
|
89
|
+
# (bash's echo usually doesn't, but behavior is shell-dependent.)
|
|
90
|
+
STATUS=$(npx rush-ai task status <id> --json)
|
|
91
|
+
echo "$STATUS" | jq . # ← breaks in zsh once the JSON contains \n
|
|
92
|
+
|
|
93
|
+
# acceptable alternatives when you really need the variable:
|
|
94
|
+
printf '%s' "$STATUS" | jq . # portable — printf never re-interprets escapes
|
|
95
|
+
jq . <<< "$STATUS" # here-string — does not re-interpret backslash escapes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When `status == completed`, the response contains (for `web-builder`):
|
|
99
|
+
|
|
100
|
+
- `previewUrl` — shareable live preview (e.g. `https://<id>-preview.rush.zhenguanyu.com/`)
|
|
101
|
+
- `gitRepoUrl` — cloneable repo (e.g. `https://gitlab-ee.zhenguanyu.com/rush/online/<id>`)
|
|
102
|
+
|
|
103
|
+
Present both to the user. The preview URL is the primary artifact they care about.
|
|
104
|
+
|
|
105
|
+
### 5. Iterate via `task send`, not by creating new tasks
|
|
106
|
+
|
|
107
|
+
If the user says "change the button color" / "add a testimonials section" / "make the hero bigger", **do not create a new task** — send a follow-up to the existing one:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx rush-ai task send <id> -p "<the change they asked for>" --json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The preview URL stays the same; it will refresh after the send completes. Poll `task status <id>` again.
|
|
114
|
+
|
|
115
|
+
Only create a fresh task if the user explicitly starts a new, unrelated project.
|
|
116
|
+
|
|
117
|
+
### 6. Failure handling
|
|
118
|
+
|
|
119
|
+
If `status == failed`, the response has an `error` field. Surface it to the user verbatim and ask whether to:
|
|
120
|
+
|
|
121
|
+
1. retry with a revised prompt via `task send`,
|
|
122
|
+
2. cancel and start over, or
|
|
123
|
+
3. dig in themselves (give them the task id so they can look it up).
|
|
124
|
+
|
|
125
|
+
Do not silently retry.
|
|
126
|
+
|
|
127
|
+
## Example end-to-end
|
|
128
|
+
|
|
129
|
+
User (in Cursor): "帮我做个公司官网首页,风格大气专业。"
|
|
130
|
+
|
|
131
|
+
Your actions:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 1. check auth (assume ok)
|
|
135
|
+
npx rush-ai auth status --json
|
|
136
|
+
|
|
137
|
+
# 2. create (prompt synthesized from the prior conversation — brand, tone, pages)
|
|
138
|
+
npx rush-ai task create -a web-builder \
|
|
139
|
+
-p "做一个公司官网首页,包含导航、Hero、核心特性、客户评价和底部联系信息,风格大气专业。" \
|
|
140
|
+
--json
|
|
141
|
+
# → {"id": "lodig8oknq0r", "status": "pending", ...}
|
|
142
|
+
|
|
143
|
+
# 3. tell the user
|
|
144
|
+
# "已经派到 Rush 的 web-builder,任务 id lodig8oknq0r,我帮你盯着。"
|
|
145
|
+
|
|
146
|
+
# 4. poll
|
|
147
|
+
npx rush-ai task status lodig8oknq0r --json
|
|
148
|
+
# ...wait...
|
|
149
|
+
# → {"status": "completed", "previewUrl": "https://lodig8oknq0r-preview.rush.zhenguanyu.com/", "gitRepoUrl": "..."}
|
|
150
|
+
|
|
151
|
+
# 5. present
|
|
152
|
+
# "跑完了:
|
|
153
|
+
# 预览: https://lodig8oknq0r-preview.rush.zhenguanyu.com/
|
|
154
|
+
# 代码: https://gitlab-ee.zhenguanyu.com/rush/online/lodig8oknq0r
|
|
155
|
+
# 要改哪儿继续说,我走 task send 不新起任务。"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Anti-patterns
|
|
159
|
+
|
|
160
|
+
- ❌ Asking the user to re-describe requirements they already told you in-thread.
|
|
161
|
+
- ❌ Creating a new task for every tweak — use `task send`.
|
|
162
|
+
- ❌ Hiding the task id from the user (they may want to reopen the Rush UI later).
|
|
163
|
+
- ❌ Running `task watch` when all you need is final status — it streams verbosely, use `task status` for quick polls.
|
package/dist/chunk-MG4HY2PD.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/util/sse.ts
|
|
4
|
-
function createSSEParser(onEvent, onDone) {
|
|
5
|
-
let buffer = "";
|
|
6
|
-
return {
|
|
7
|
-
feed(chunk) {
|
|
8
|
-
buffer += chunk;
|
|
9
|
-
const lines = buffer.split("\n");
|
|
10
|
-
buffer = lines.pop() ?? "";
|
|
11
|
-
for (const line of lines) {
|
|
12
|
-
if (line.startsWith("data: ")) {
|
|
13
|
-
const data = line.slice(6);
|
|
14
|
-
if (data === "[DONE]") {
|
|
15
|
-
onDone?.();
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
try {
|
|
19
|
-
const parsed = JSON.parse(data);
|
|
20
|
-
onEvent({
|
|
21
|
-
type: parsed.type ?? "unknown",
|
|
22
|
-
data: typeof parsed.data === "string" ? parsed.data : JSON.stringify(parsed)
|
|
23
|
-
});
|
|
24
|
-
} catch {
|
|
25
|
-
onEvent({ type: "raw", data });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
flush() {
|
|
31
|
-
if (buffer.startsWith("data: ")) {
|
|
32
|
-
const data = buffer.slice(6);
|
|
33
|
-
if (data !== "[DONE]") {
|
|
34
|
-
try {
|
|
35
|
-
const parsed = JSON.parse(data);
|
|
36
|
-
onEvent({
|
|
37
|
-
type: parsed.type ?? "unknown",
|
|
38
|
-
data: typeof parsed.data === "string" ? parsed.data : JSON.stringify(parsed)
|
|
39
|
-
});
|
|
40
|
-
} catch {
|
|
41
|
-
onEvent({ type: "raw", data });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
buffer = "";
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
async function consumeSSEStreamWithReconnect(connectFn, onEvent, options = {}) {
|
|
50
|
-
const {
|
|
51
|
-
maxReconnects = 5,
|
|
52
|
-
reconnectDelay = 1e3,
|
|
53
|
-
signal,
|
|
54
|
-
onReconnect,
|
|
55
|
-
isTerminal
|
|
56
|
-
} = options;
|
|
57
|
-
let emittedEventCount = 0;
|
|
58
|
-
let reconnectAttempts = 0;
|
|
59
|
-
let terminalReached = false;
|
|
60
|
-
while (!terminalReached) {
|
|
61
|
-
if (signal?.aborted) return;
|
|
62
|
-
let receivedDone = false;
|
|
63
|
-
let currentEventIndex = 0;
|
|
64
|
-
try {
|
|
65
|
-
const stream = await connectFn();
|
|
66
|
-
const reader = stream.getReader();
|
|
67
|
-
const decoder = new TextDecoder();
|
|
68
|
-
const parser = createSSEParser(
|
|
69
|
-
(event) => {
|
|
70
|
-
currentEventIndex++;
|
|
71
|
-
if (currentEventIndex <= emittedEventCount) {
|
|
72
|
-
if (isTerminal?.(event)) {
|
|
73
|
-
terminalReached = true;
|
|
74
|
-
}
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
emittedEventCount++;
|
|
78
|
-
onEvent(event);
|
|
79
|
-
if (isTerminal?.(event)) {
|
|
80
|
-
terminalReached = true;
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
() => {
|
|
84
|
-
receivedDone = true;
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
for (; ; ) {
|
|
88
|
-
if (signal?.aborted || terminalReached) {
|
|
89
|
-
reader.cancel().catch(() => {
|
|
90
|
-
});
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const { done, value } = await reader.read();
|
|
94
|
-
if (done) break;
|
|
95
|
-
parser.feed(decoder.decode(value, { stream: true }));
|
|
96
|
-
if (terminalReached) {
|
|
97
|
-
reader.cancel().catch(() => {
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
parser.flush();
|
|
103
|
-
} catch {
|
|
104
|
-
if (signal?.aborted || terminalReached) return;
|
|
105
|
-
}
|
|
106
|
-
if (terminalReached) return;
|
|
107
|
-
if (receivedDone && terminalReached) return;
|
|
108
|
-
reconnectAttempts++;
|
|
109
|
-
if (reconnectAttempts > maxReconnects) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`SSE reconnection failed after ${maxReconnects} attempts`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
if (signal?.aborted) return;
|
|
115
|
-
onReconnect?.(reconnectAttempts);
|
|
116
|
-
const delay = Math.min(
|
|
117
|
-
reconnectDelay * 2 ** (reconnectAttempts - 1),
|
|
118
|
-
3e4
|
|
119
|
-
);
|
|
120
|
-
await new Promise((resolve) => {
|
|
121
|
-
const timer = setTimeout(resolve, delay);
|
|
122
|
-
if (signal) {
|
|
123
|
-
const onAbort = () => {
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
resolve();
|
|
126
|
-
};
|
|
127
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export {
|
|
134
|
-
consumeSSEStreamWithReconnect
|
|
135
|
-
};
|
|
136
|
-
//# sourceMappingURL=chunk-MG4HY2PD.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/util/sse.ts"],"sourcesContent":["/**\n * SSE (Server-Sent Events) line-buffered parser.\n * Handles events split across multiple chunks.\n */\nexport interface SSEEvent {\n type: string;\n data: string;\n}\n\nexport function createSSEParser(\n onEvent: (event: SSEEvent) => void,\n onDone?: () => void\n) {\n let buffer = '';\n\n return {\n feed(chunk: string): void {\n buffer += chunk;\n const lines = buffer.split('\\n');\n // Keep the last (possibly incomplete) line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') {\n onDone?.();\n return;\n }\n try {\n const parsed = JSON.parse(data);\n onEvent({\n type: parsed.type ?? 'unknown',\n data:\n typeof parsed.data === 'string'\n ? parsed.data\n : JSON.stringify(parsed),\n });\n } catch {\n // Non-JSON SSE data\n onEvent({ type: 'raw', data });\n }\n }\n }\n },\n flush(): void {\n // Process any remaining buffered data\n if (buffer.startsWith('data: ')) {\n const data = buffer.slice(6);\n if (data !== '[DONE]') {\n try {\n const parsed = JSON.parse(data);\n onEvent({\n type: parsed.type ?? 'unknown',\n data:\n typeof parsed.data === 'string'\n ? parsed.data\n : JSON.stringify(parsed),\n });\n } catch {\n onEvent({ type: 'raw', data });\n }\n }\n }\n buffer = '';\n },\n };\n}\n\n/**\n * Consume an SSE ReadableStream with proper cross-chunk buffering.\n */\nexport async function consumeSSEStream(\n body: ReadableStream<Uint8Array>,\n onEvent: (event: SSEEvent) => void,\n onDone?: () => void\n): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n const parser = createSSEParser(onEvent, onDone);\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n parser.feed(decoder.decode(value, { stream: true }));\n }\n parser.flush();\n}\n\nexport interface SSEReconnectOptions {\n /** Maximum number of reconnection attempts (default: 5) */\n maxReconnects?: number;\n /** Initial reconnection delay in ms (default: 1000) */\n reconnectDelay?: number;\n /** AbortSignal to cancel the stream and reconnections */\n signal?: AbortSignal;\n /** Called before each reconnection attempt */\n onReconnect?: (attempt: number) => void;\n /**\n * Determines if an event represents a terminal state.\n * When this returns true, the stream stops and no reconnection occurs.\n */\n isTerminal?: (event: SSEEvent) => boolean;\n}\n\n/**\n * Consume an SSE stream with automatic reconnection and event deduplication.\n *\n * Key behaviors:\n * - [DONE] does NOT automatically stop the stream. Only `isTerminal(event)` stops it.\n * - If [DONE] is received without a terminal event, reconnection is triggered\n * (e.g., server 5-minute SSE connection lifetime timeout).\n * - On reconnection, the server replays all historical events from epoch.\n * Client-side deduplication skips already-seen events using an event counter.\n *\n * @param connectFn - Factory that creates a new SSE stream (called on each reconnect)\n * @param onEvent - Event handler (only called for new, non-duplicate events)\n * @param options - Reconnection options\n */\nexport async function consumeSSEStreamWithReconnect(\n connectFn: () => Promise<ReadableStream<Uint8Array>>,\n onEvent: (event: SSEEvent) => void,\n options: SSEReconnectOptions = {}\n): Promise<void> {\n const {\n maxReconnects = 5,\n reconnectDelay = 1000,\n signal,\n onReconnect,\n isTerminal,\n } = options;\n\n let emittedEventCount = 0;\n let reconnectAttempts = 0;\n let terminalReached = false;\n\n while (!terminalReached) {\n if (signal?.aborted) return;\n\n let receivedDone = false;\n let currentEventIndex = 0;\n\n try {\n const stream = await connectFn();\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n\n // Note: We do NOT reset reconnectAttempts here.\n // For [DONE]+running scenarios (server 5-min timeout), each reconnect\n // should count toward the limit. The counter only resets after the\n // function is called fresh.\n\n const parser = createSSEParser(\n (event) => {\n currentEventIndex++;\n\n // Dedup: skip events we've already emitted in previous connections\n if (currentEventIndex <= emittedEventCount) {\n // Still check for terminal even on dedup'd events\n if (isTerminal?.(event)) {\n terminalReached = true;\n }\n return;\n }\n\n emittedEventCount++;\n onEvent(event);\n\n if (isTerminal?.(event)) {\n terminalReached = true;\n }\n },\n () => {\n receivedDone = true;\n }\n );\n\n for (;;) {\n if (signal?.aborted || terminalReached) {\n reader.cancel().catch(() => {});\n return;\n }\n\n const { done, value } = await reader.read();\n if (done) break;\n parser.feed(decoder.decode(value, { stream: true }));\n\n if (terminalReached) {\n reader.cancel().catch(() => {});\n return;\n }\n }\n parser.flush();\n } catch {\n // Stream error — will attempt reconnect below\n if (signal?.aborted || terminalReached) return;\n }\n\n // If terminal was reached, we're done\n if (terminalReached) return;\n\n // If [DONE] was received AND we already got a terminal event, stop\n // (This shouldn't happen because terminalReached would be true, but safety check)\n if (receivedDone && terminalReached) return;\n\n // Otherwise, reconnect: [DONE] without terminal = server timeout, or stream error\n reconnectAttempts++;\n if (reconnectAttempts > maxReconnects) {\n throw new Error(\n `SSE reconnection failed after ${maxReconnects} attempts`\n );\n }\n\n if (signal?.aborted) return;\n\n onReconnect?.(reconnectAttempts);\n\n // Exponential backoff for reconnection\n const delay = Math.min(\n reconnectDelay * 2 ** (reconnectAttempts - 1),\n 30_000\n );\n\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, delay);\n if (signal) {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n }\n}\n"],"mappings":"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","names":[]}
|