awel 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/README.zh-CN.md +115 -0
- package/dist/cli/agent.js +11 -0
- package/dist/cli/awel-config.d.ts +6 -0
- package/dist/cli/awel-config.js +20 -0
- package/dist/cli/babel-setup.js +2 -19
- package/dist/cli/comment-popup.js +50 -26
- package/dist/cli/index.js +2 -0
- package/dist/cli/onboarding.d.ts +1 -0
- package/dist/cli/onboarding.js +58 -0
- package/dist/cli/providers/registry.d.ts +12 -0
- package/dist/cli/providers/registry.js +41 -0
- package/dist/cli/providers/vercel.js +18 -4
- package/dist/cli/subprocess.js +76 -14
- package/dist/dashboard/assets/index-B374_cjZ.css +1 -0
- package/dist/dashboard/assets/{index-Bk--q3wu.js → index-BJ6wUfxa.js} +89 -84
- package/dist/dashboard/index.html +2 -2
- package/dist/host/host.js +61 -52
- package/package.json +1 -2
- package/dist/dashboard/assets/index-DkWV03So.css +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Awel
|
|
2
2
|
|
|
3
|
+
English | [中文](./README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
AI-powered development overlay for Next.js. Awel runs a proxy in front of your dev server, injects a floating chat button into your app, and lets you talk to an AI agent that can read, write, and edit files in your project — all from an embedded dashboard.
|
|
4
6
|
|
|
5
7
|

|
|
@@ -10,9 +12,17 @@ AI-powered development overlay for Next.js. Awel runs a proxy in front of your d
|
|
|
10
12
|
# Skip if you're already in a Next.js app
|
|
11
13
|
npx create-next-app@latest my-app && cd my-app
|
|
12
14
|
|
|
15
|
+
# Set up at least one AI provider (pick one):
|
|
16
|
+
export ANTHROPIC_API_KEY="sk-ant-..." # Anthropic API
|
|
17
|
+
export OPENAI_API_KEY="sk-..." # OpenAI
|
|
18
|
+
export GOOGLE_GENERATIVE_AI_API_KEY="..." # Google AI
|
|
19
|
+
# Or install the Claude CLI: https://docs.anthropic.com/en/docs/claude-code
|
|
20
|
+
|
|
13
21
|
npx awel dev
|
|
14
22
|
```
|
|
15
23
|
|
|
24
|
+
Awel needs at least one configured provider to function. See [Supported Models](#supported-models) for the full list.
|
|
25
|
+
|
|
16
26
|
This starts Awel on port 3001 and proxies your Next.js dev server on port 3000. Open `http://localhost:3001` to see your app with the Awel overlay.
|
|
17
27
|
|
|
18
28
|
### Options
|
|
@@ -71,6 +81,13 @@ The AI agent has access to:
|
|
|
71
81
|
## Features
|
|
72
82
|
|
|
73
83
|
- **Element inspector** — click the crosshair icon to select an element in your app and attach it as context to your prompt
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
- **Screenshot annotator** — annotate screenshots with shapes, arrows, and text before sending to the agent
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
74
91
|
- **Image attachments** — attach screenshots or reference images
|
|
75
92
|
- **Plan approval** — the agent can propose plans for you to review before making changes
|
|
76
93
|
- **Undo** — roll back all file changes from an agent session in one click
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Awel
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 中文
|
|
4
|
+
|
|
5
|
+
为 Next.js 打造的 AI 开发助手。Awel 在你的开发服务器前运行一个代理,向页面注入一个悬浮聊天按钮,让你通过内嵌的面板与 AI 智能体对话——它可以读取、编写和编辑项目中的文件。
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## 快速开始
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 如果你已经在一个 Next.js 项目中,可以跳过这一步
|
|
13
|
+
npx create-next-app@latest my-app && cd my-app
|
|
14
|
+
|
|
15
|
+
# 至少配置一个 AI 服务商(任选其一):
|
|
16
|
+
export ANTHROPIC_API_KEY="sk-ant-..." # Anthropic API
|
|
17
|
+
export OPENAI_API_KEY="sk-..." # OpenAI
|
|
18
|
+
export GOOGLE_GENERATIVE_AI_API_KEY="..." # Google AI
|
|
19
|
+
# 或安装 Claude CLI:https://docs.anthropic.com/en/docs/claude-code
|
|
20
|
+
|
|
21
|
+
npx awel dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Awel 需要至少一个已配置的服务商才能运行。完整列表见[支持的模型](#支持的模型)。
|
|
25
|
+
|
|
26
|
+
Awel 会在端口 3001 启动,并代理运行在端口 3000 的 Next.js 开发服务器。打开 `http://localhost:3001` 即可看到带有 Awel 浮层的应用。
|
|
27
|
+
|
|
28
|
+
### 选项
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
awel dev [options]
|
|
32
|
+
|
|
33
|
+
-p, --port <port> 目标应用端口(默认:3000)
|
|
34
|
+
-v, --verbose 将 LLM 流式事件输出到 stderr
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 工作原理
|
|
38
|
+
|
|
39
|
+
Awel 位于浏览器和开发服务器之间:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
浏览器 → Awel(代理 :3001)→ 你的应用(开发服务器 :3000)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
1. Awel 拦截 HTML 响应,注入一段脚本
|
|
46
|
+
2. 脚本在页面右下角渲染一个悬浮按钮(通过 Shadow DOM 隔离)
|
|
47
|
+
3. 点击按钮会打开一个全屏聊天面板(iframe)
|
|
48
|
+
4. 描述你的需求——AI 智能体会读取代码、进行编辑、执行命令,并实时流式返回结果
|
|
49
|
+
|
|
50
|
+
HMR / WebSocket 流量会透明代理,在智能体编辑文件期间暂停,以防止热重载干扰。
|
|
51
|
+
|
|
52
|
+
## 支持的模型
|
|
53
|
+
|
|
54
|
+
Awel 使用 [Vercel AI SDK](https://sdk.vercel.ai),支持多个服务商。设置对应的环境变量即可启用:
|
|
55
|
+
|
|
56
|
+
| 服务商 | 环境变量 | 示例模型 |
|
|
57
|
+
|--------|----------|----------|
|
|
58
|
+
| Claude Code | PATH 中有 Claude CLI | sonnet, opus, haiku |
|
|
59
|
+
| Anthropic API | `ANTHROPIC_API_KEY` | claude-sonnet-4-5, claude-opus-4-5 |
|
|
60
|
+
| OpenAI | `OPENAI_API_KEY` | gpt-5.2-codex, gpt-5.1-codex |
|
|
61
|
+
| Google AI | `GOOGLE_GENERATIVE_AI_API_KEY` | gemini-3-pro-preview, gemini-2.5-pro |
|
|
62
|
+
| 通义千问 | `DASHSCOPE_API_KEY` | qwen-max, qwen-plus-latest |
|
|
63
|
+
| MiniMax | `MINIMAX_API_KEY` | MiniMax-M2 |
|
|
64
|
+
| Vercel Gateway | `AI_GATEWAY_API_KEY` | 通过网关访问多种模型 |
|
|
65
|
+
|
|
66
|
+
可随时在面板顶部的下拉菜单中切换模型。
|
|
67
|
+
|
|
68
|
+
## 智能体工具
|
|
69
|
+
|
|
70
|
+
AI 智能体可使用以下工具:
|
|
71
|
+
|
|
72
|
+
- **Read** / **Write** / **Edit** / **MultiEdit** — 文件操作
|
|
73
|
+
- **Bash** — 执行 Shell 命令
|
|
74
|
+
- **Glob** / **Grep** / **CodeSearch** — 查找文件和搜索代码
|
|
75
|
+
- **WebSearch** / **WebFetch** — 网络搜索
|
|
76
|
+
- **ProposePlan** — 提出多步骤实施计划,等待你审批后再执行
|
|
77
|
+
- **AskUser** — 在执行过程中向你提问
|
|
78
|
+
- **RestartDevServer** — 配置变更后重启开发服务器
|
|
79
|
+
- **TodoRead** / **TodoWrite** — 跨对话的任务管理
|
|
80
|
+
|
|
81
|
+
## 功能特性
|
|
82
|
+
|
|
83
|
+
- **元素检查器** — 点击十字准星图标,在应用中选择一个元素,自动作为上下文附加到提示中
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
- **截图标注** — 用图形、箭头和文字标注截图后发送给智能体
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
- **图片附件** — 附加截图或参考图片
|
|
92
|
+
- **计划审批** — 智能体可以提出计划,由你审核后再执行变更
|
|
93
|
+
- **撤销** — 一键回滚整个智能体会话的所有文件变更
|
|
94
|
+
- **Diff 审查** — 在接受变更前查看所有文件修改的摘要
|
|
95
|
+
- **深色模式** — 跟随系统偏好
|
|
96
|
+
- **国际化** — 支持英文和中文
|
|
97
|
+
|
|
98
|
+
## 开发
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run build # 构建所有模块
|
|
102
|
+
npm run dev # 监听模式(仅 CLI)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
单独构建:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run build:cli # TypeScript → dist/cli/
|
|
109
|
+
npm run build:dashboard # Vite → dist/dashboard/
|
|
110
|
+
npm run build:host # esbuild → dist/host/host.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 许可证
|
|
114
|
+
|
|
115
|
+
MIT
|
package/dist/cli/agent.js
CHANGED
|
@@ -164,6 +164,13 @@ export function createAgentRoute(projectCwd, targetPort) {
|
|
|
164
164
|
appendUserMessage(userContent);
|
|
165
165
|
appendResponseMessages(responseMessages);
|
|
166
166
|
}
|
|
167
|
+
})
|
|
168
|
+
.catch((err) => {
|
|
169
|
+
// Swallow provider-level rejections (e.g. API 400 from tool-use
|
|
170
|
+
// concurrency in Claude Code). The error has already been surfaced
|
|
171
|
+
// as an SSE error event inside streamResponse.
|
|
172
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
+
console.error(`[awel] streamResponse rejected: ${msg}`);
|
|
167
174
|
})
|
|
168
175
|
.finally(() => {
|
|
169
176
|
if (!signal.aborted) {
|
|
@@ -262,5 +269,9 @@ export function createAgentRoute(projectCwd, targetPort) {
|
|
|
262
269
|
agent.get('/api/dev-server/status', (c) => {
|
|
263
270
|
return c.json(getDevServerStatus());
|
|
264
271
|
});
|
|
272
|
+
// ─── Project Info ───────────────────────────────────────
|
|
273
|
+
agent.get('/api/project-info', (c) => {
|
|
274
|
+
return c.json({ projectCwd });
|
|
275
|
+
});
|
|
265
276
|
return agent;
|
|
266
277
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
export function readAwelConfig(projectCwd) {
|
|
4
|
+
const configPath = join(projectCwd, '.awel', 'config.json');
|
|
5
|
+
if (!existsSync(configPath))
|
|
6
|
+
return {};
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function writeAwelConfig(projectCwd, config) {
|
|
15
|
+
const dir = join(projectCwd, '.awel');
|
|
16
|
+
if (!existsSync(dir)) {
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
20
|
+
}
|
package/dist/cli/babel-setup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import { readAwelConfig, writeAwelConfig } from './awel-config.js';
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
6
|
const BABEL_CONFIG_FILES = [
|
|
6
7
|
'babel.config.js',
|
|
@@ -37,24 +38,6 @@ function findExistingBabelConfig(projectCwd) {
|
|
|
37
38
|
return 'package.json';
|
|
38
39
|
return null;
|
|
39
40
|
}
|
|
40
|
-
function readAwelConfig(projectCwd) {
|
|
41
|
-
const configPath = join(projectCwd, '.awel', 'config.json');
|
|
42
|
-
if (!existsSync(configPath))
|
|
43
|
-
return {};
|
|
44
|
-
try {
|
|
45
|
-
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function writeAwelConfig(projectCwd, config) {
|
|
52
|
-
const dir = join(projectCwd, '.awel');
|
|
53
|
-
if (!existsSync(dir)) {
|
|
54
|
-
mkdirSync(dir, { recursive: true });
|
|
55
|
-
}
|
|
56
|
-
writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
57
|
-
}
|
|
58
41
|
// ANSI 256-color helpers — darker shades that stay visible on light backgrounds
|
|
59
42
|
const bold = (s) => `\x1b[1m${s}\x1b[22m`;
|
|
60
43
|
const dim = (s) => `\x1b[2m${s}\x1b[22m`;
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
const
|
|
3
|
-
|
|
2
|
+
const i18n = {
|
|
3
|
+
en: { placeholder: 'Describe what you want to change...', cancel: 'Cancel', send: 'Send' },
|
|
4
|
+
zh: { placeholder: '描述您想进行的更改...', cancel: '取消', send: '发送' },
|
|
5
|
+
};
|
|
6
|
+
function getCommentPopupHtml(lang, theme) {
|
|
7
|
+
const t = i18n[lang] ?? i18n.en;
|
|
8
|
+
const isDark = theme !== 'light';
|
|
9
|
+
return `<!DOCTYPE html>
|
|
10
|
+
<html lang="en" data-theme="${isDark ? 'dark' : 'light'}">
|
|
4
11
|
<head>
|
|
5
12
|
<meta charset="UTF-8">
|
|
6
13
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
14
|
<style>
|
|
15
|
+
:root[data-theme="dark"] {
|
|
16
|
+
--bg: #18181b; --fg: #fafafa;
|
|
17
|
+
--title: #e4e4e7; --muted: #a1a1aa; --dim: #71717a; --sep: #52525b;
|
|
18
|
+
--input-bg: #09090b; --input-border: #27272a; --input-focus: #a1a1aa;
|
|
19
|
+
--btn-bg: #27272a; --btn-fg: #a1a1aa; --btn-hover-bg: #3f3f46; --btn-hover-fg: #fafafa;
|
|
20
|
+
--primary-bg: #fafafa; --primary-border: #fafafa; --primary-fg: #18181b; --primary-hover: #e4e4e7;
|
|
21
|
+
}
|
|
22
|
+
:root[data-theme="light"] {
|
|
23
|
+
--bg: #ffffff; --fg: #18181b;
|
|
24
|
+
--title: #27272a; --muted: #71717a; --dim: #a1a1aa; --sep: #d4d4d8;
|
|
25
|
+
--input-bg: #f4f4f5; --input-border: #d4d4d8; --input-focus: #71717a;
|
|
26
|
+
--btn-bg: #e4e4e7; --btn-fg: #71717a; --btn-hover-bg: #d4d4d8; --btn-hover-fg: #18181b;
|
|
27
|
+
--primary-bg: #18181b; --primary-border: #18181b; --primary-fg: #fafafa; --primary-hover: #27272a;
|
|
28
|
+
}
|
|
8
29
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
30
|
body {
|
|
10
31
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
-
background:
|
|
12
|
-
color:
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--fg);
|
|
13
34
|
padding: 0 12px 12px;
|
|
14
35
|
height: 100vh;
|
|
15
36
|
display: flex;
|
|
@@ -30,7 +51,7 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
30
51
|
.header-title {
|
|
31
52
|
font-size: 12px;
|
|
32
53
|
font-weight: 600;
|
|
33
|
-
color:
|
|
54
|
+
color: var(--title);
|
|
34
55
|
}
|
|
35
56
|
.element-info {
|
|
36
57
|
margin-left: auto;
|
|
@@ -38,7 +59,7 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
38
59
|
align-items: center;
|
|
39
60
|
gap: 4px;
|
|
40
61
|
font-size: 11px;
|
|
41
|
-
color:
|
|
62
|
+
color: var(--muted);
|
|
42
63
|
overflow: hidden;
|
|
43
64
|
}
|
|
44
65
|
.element-name {
|
|
@@ -49,10 +70,10 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
49
70
|
max-width: 100px;
|
|
50
71
|
}
|
|
51
72
|
.element-sep {
|
|
52
|
-
color:
|
|
73
|
+
color: var(--sep);
|
|
53
74
|
}
|
|
54
75
|
.element-file {
|
|
55
|
-
color:
|
|
76
|
+
color: var(--dim);
|
|
56
77
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
57
78
|
font-size: 10px;
|
|
58
79
|
white-space: nowrap;
|
|
@@ -63,10 +84,10 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
63
84
|
textarea {
|
|
64
85
|
flex: 1;
|
|
65
86
|
width: 100%;
|
|
66
|
-
background:
|
|
67
|
-
border: 1px solid
|
|
87
|
+
background: var(--input-bg);
|
|
88
|
+
border: 1px solid var(--input-border);
|
|
68
89
|
border-radius: 6px;
|
|
69
|
-
color:
|
|
90
|
+
color: var(--fg);
|
|
70
91
|
font-family: inherit;
|
|
71
92
|
font-size: 13px;
|
|
72
93
|
padding: 8px 10px;
|
|
@@ -75,10 +96,10 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
75
96
|
transition: border-color 0.15s ease;
|
|
76
97
|
}
|
|
77
98
|
textarea:focus {
|
|
78
|
-
border-color:
|
|
99
|
+
border-color: var(--input-focus);
|
|
79
100
|
}
|
|
80
101
|
textarea::placeholder {
|
|
81
|
-
color:
|
|
102
|
+
color: var(--dim);
|
|
82
103
|
}
|
|
83
104
|
.buttons {
|
|
84
105
|
display: flex;
|
|
@@ -92,26 +113,26 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
92
113
|
font-weight: 500;
|
|
93
114
|
padding: 6px 14px;
|
|
94
115
|
border-radius: 6px;
|
|
95
|
-
border: 1px solid
|
|
116
|
+
border: 1px solid var(--input-border);
|
|
96
117
|
cursor: pointer;
|
|
97
118
|
transition: all 0.15s ease;
|
|
98
119
|
}
|
|
99
120
|
button:active { transform: scale(0.97); }
|
|
100
121
|
.btn-close {
|
|
101
|
-
background:
|
|
102
|
-
color:
|
|
122
|
+
background: var(--btn-bg);
|
|
123
|
+
color: var(--btn-fg);
|
|
103
124
|
}
|
|
104
125
|
.btn-close:hover {
|
|
105
|
-
background:
|
|
106
|
-
color:
|
|
126
|
+
background: var(--btn-hover-bg);
|
|
127
|
+
color: var(--btn-hover-fg);
|
|
107
128
|
}
|
|
108
129
|
.btn-submit {
|
|
109
|
-
background:
|
|
110
|
-
border-color:
|
|
111
|
-
color:
|
|
130
|
+
background: var(--primary-bg);
|
|
131
|
+
border-color: var(--primary-border);
|
|
132
|
+
color: var(--primary-fg);
|
|
112
133
|
}
|
|
113
134
|
.btn-submit:hover {
|
|
114
|
-
background:
|
|
135
|
+
background: var(--primary-hover);
|
|
115
136
|
}
|
|
116
137
|
.btn-submit:disabled {
|
|
117
138
|
opacity: 0.4;
|
|
@@ -135,10 +156,10 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
135
156
|
</div>
|
|
136
157
|
<div class="element-info" id="elementInfo"></div>
|
|
137
158
|
</div>
|
|
138
|
-
<textarea id="comment" placeholder="
|
|
159
|
+
<textarea id="comment" placeholder="${t.placeholder}" autofocus></textarea>
|
|
139
160
|
<div class="buttons">
|
|
140
|
-
<button class="btn-close" id="closeBtn"
|
|
141
|
-
<button class="btn-submit" id="submitBtn" disabled
|
|
161
|
+
<button class="btn-close" id="closeBtn">${t.cancel}</button>
|
|
162
|
+
<button class="btn-submit" id="submitBtn" disabled>${t.send}<kbd id="shortcutHint"></kbd></button>
|
|
142
163
|
</div>
|
|
143
164
|
<script>
|
|
144
165
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -197,10 +218,13 @@ const commentPopupHtml = `<!DOCTYPE html>
|
|
|
197
218
|
</script>
|
|
198
219
|
</body>
|
|
199
220
|
</html>`;
|
|
221
|
+
}
|
|
200
222
|
export function createCommentPopupRoute() {
|
|
201
223
|
const app = new Hono();
|
|
202
224
|
app.get('/_awel/comment-popup', (c) => {
|
|
203
|
-
|
|
225
|
+
const lang = c.req.query('lang') ?? 'en';
|
|
226
|
+
const theme = c.req.query('theme') ?? 'dark';
|
|
227
|
+
return c.html(getCommentPopupHtml(lang, theme));
|
|
204
228
|
});
|
|
205
229
|
return app;
|
|
206
230
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { startServer } from './server.js';
|
|
|
3
3
|
import { AWEL_PORT, USER_APP_PORT } from './config.js';
|
|
4
4
|
import { setVerbose } from './verbose.js';
|
|
5
5
|
import { ensureBabelPlugin } from './babel-setup.js';
|
|
6
|
+
import { ensureProvider } from './onboarding.js';
|
|
6
7
|
import { awel } from './logger.js';
|
|
7
8
|
import { spawnDevServer } from './subprocess.js';
|
|
8
9
|
program
|
|
@@ -18,6 +19,7 @@ program
|
|
|
18
19
|
const targetPort = parseInt(options.port, 10);
|
|
19
20
|
if (options.verbose)
|
|
20
21
|
setVerbose(true);
|
|
22
|
+
await ensureProvider(process.cwd());
|
|
21
23
|
await ensureBabelPlugin(process.cwd());
|
|
22
24
|
awel.log('🌟 Starting Awel...');
|
|
23
25
|
awel.log(` Target app port: ${targetPort}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureProvider(projectCwd: string): Promise<void>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readAwelConfig } from './awel-config.js';
|
|
2
|
+
import { getAvailableProviders, PROVIDER_ENV_KEYS, PROVIDER_LABELS } from './providers/registry.js';
|
|
3
|
+
import { awel } from './logger.js';
|
|
4
|
+
function printSetupInstructions() {
|
|
5
|
+
awel.log('No LLM providers are configured.');
|
|
6
|
+
awel.log('Awel needs at least one AI provider to function.');
|
|
7
|
+
awel.log('');
|
|
8
|
+
awel.log('Set up a provider by exporting its API key:');
|
|
9
|
+
awel.log('');
|
|
10
|
+
// List all providers with their setup commands
|
|
11
|
+
for (const [provider, envKey] of Object.entries(PROVIDER_ENV_KEYS)) {
|
|
12
|
+
const label = PROVIDER_LABELS[provider] ?? provider;
|
|
13
|
+
awel.log(` ${label}`);
|
|
14
|
+
awel.log(` export ${envKey}="..."`);
|
|
15
|
+
awel.log('');
|
|
16
|
+
}
|
|
17
|
+
// Claude Code is special — no env var, needs CLI install
|
|
18
|
+
const label = PROVIDER_LABELS['claude-code'] ?? 'Claude Code';
|
|
19
|
+
awel.log(` ${label}`);
|
|
20
|
+
awel.log(' Install the Claude CLI: https://docs.anthropic.com/en/docs/claude-code');
|
|
21
|
+
awel.log('');
|
|
22
|
+
awel.log('Then run `awel dev` again.');
|
|
23
|
+
}
|
|
24
|
+
export async function ensureProvider(projectCwd) {
|
|
25
|
+
const config = readAwelConfig(projectCwd);
|
|
26
|
+
const providers = getAvailableProviders();
|
|
27
|
+
const available = providers.filter(p => p.available);
|
|
28
|
+
const isFirstRun = !config.onboarded;
|
|
29
|
+
if (isFirstRun && available.length > 0) {
|
|
30
|
+
// First run with providers available — show welcome
|
|
31
|
+
awel.log('');
|
|
32
|
+
awel.log('Welcome to Awel!');
|
|
33
|
+
awel.log('AI-powered development overlay for Next.js');
|
|
34
|
+
awel.log('');
|
|
35
|
+
awel.log(`\u2714 ${available.length} provider${available.length === 1 ? '' : 's'} available:`);
|
|
36
|
+
for (const p of available) {
|
|
37
|
+
awel.log(` \u25CF ${p.label}`);
|
|
38
|
+
}
|
|
39
|
+
awel.log('');
|
|
40
|
+
printSetupInstructions();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (isFirstRun && available.length === 0) {
|
|
44
|
+
// First run with NO providers — show welcome + instructions, exit
|
|
45
|
+
awel.log('');
|
|
46
|
+
awel.log('Welcome to Awel!');
|
|
47
|
+
awel.log('AI-powered development overlay for Next.js');
|
|
48
|
+
awel.log('');
|
|
49
|
+
printSetupInstructions();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (!isFirstRun && available.length === 0) {
|
|
53
|
+
// Subsequent run with NO providers — instructions only, exit
|
|
54
|
+
printSetupInstructions();
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// Subsequent run with providers available — silent pass-through
|
|
58
|
+
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { StreamProvider, ModelDefinition } from './types.js';
|
|
2
2
|
export declare const PROVIDER_ENV_KEYS: Record<string, string>;
|
|
3
|
+
export declare const PROVIDER_LABELS: Record<string, string>;
|
|
4
|
+
export interface ProviderAvailability {
|
|
5
|
+
provider: string;
|
|
6
|
+
label: string;
|
|
7
|
+
available: boolean;
|
|
8
|
+
envVar: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns deduplicated provider availability info.
|
|
12
|
+
* Checks each provider once (claude-code via binary check, others via env var).
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAvailableProviders(): ProviderAvailability[];
|
|
3
15
|
export declare function resolveProvider(modelId: string): {
|
|
4
16
|
provider: StreamProvider;
|
|
5
17
|
modelProvider: string;
|
|
@@ -59,6 +59,47 @@ function isClaudeBinaryAvailable() {
|
|
|
59
59
|
}
|
|
60
60
|
return _claudeBinaryAvailable;
|
|
61
61
|
}
|
|
62
|
+
// ─── Provider Availability ────────────────────────────────────
|
|
63
|
+
export const PROVIDER_LABELS = {
|
|
64
|
+
'claude-code': 'Claude Code',
|
|
65
|
+
anthropic: 'Anthropic API',
|
|
66
|
+
openai: 'OpenAI',
|
|
67
|
+
'google-ai': 'Google AI',
|
|
68
|
+
'vercel-gateway': 'Vercel AI Gateway',
|
|
69
|
+
qwen: 'Qwen',
|
|
70
|
+
minimax: 'MiniMax',
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Returns deduplicated provider availability info.
|
|
74
|
+
* Checks each provider once (claude-code via binary check, others via env var).
|
|
75
|
+
*/
|
|
76
|
+
export function getAvailableProviders() {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const result = [];
|
|
79
|
+
for (const model of MODEL_CATALOG) {
|
|
80
|
+
if (seen.has(model.provider))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(model.provider);
|
|
83
|
+
const label = PROVIDER_LABELS[model.provider] ?? model.provider;
|
|
84
|
+
if (model.provider === 'claude-code') {
|
|
85
|
+
result.push({
|
|
86
|
+
provider: model.provider,
|
|
87
|
+
label,
|
|
88
|
+
available: isClaudeBinaryAvailable(),
|
|
89
|
+
envVar: null,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const envKey = PROVIDER_ENV_KEYS[model.provider];
|
|
94
|
+
result.push({
|
|
95
|
+
provider: model.provider,
|
|
96
|
+
label,
|
|
97
|
+
available: envKey ? !!process.env[envKey] : true,
|
|
98
|
+
envVar: envKey ?? null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
62
103
|
// ─── Provider Resolution ─────────────────────────────────────
|
|
63
104
|
export function resolveProvider(modelId) {
|
|
64
105
|
const model = MODEL_CATALOG.find(m => m.id === modelId);
|
|
@@ -63,7 +63,10 @@ Inspector Context:
|
|
|
63
63
|
- CRITICAL: Focus your changes on the specific selected tag, NOT the entire parent component. The rendered HTML attributes help you locate the exact JSX element in the source code.
|
|
64
64
|
- Use the parent component source code only as context to find and modify the specific tag.
|
|
65
65
|
- Prioritize addressing what the user sees: if props are undefined/null, investigate why.
|
|
66
|
-
- Reference the specific line numbers from the context when making edits
|
|
66
|
+
- Reference the specific line numbers from the context when making edits.
|
|
67
|
+
|
|
68
|
+
Language:
|
|
69
|
+
- IMPORTANT: Always respond in the same language the user writes in. If the user writes in Chinese, respond in Chinese. If the user writes in English, respond in English. Match the user's language throughout the conversation.`;
|
|
67
70
|
/** Detects files Claude Code uses for plan output (.claude/plans/*.md, plan.md) */
|
|
68
71
|
function isPlanFile(filePath) {
|
|
69
72
|
const normalized = filePath.replace(/\\/g, '/');
|
|
@@ -96,6 +99,7 @@ function createModel(modelId, providerType, cwd) {
|
|
|
96
99
|
permissionMode: 'acceptEdits',
|
|
97
100
|
streamingInput: 'always',
|
|
98
101
|
maxTurns: 25,
|
|
102
|
+
appendSystemPrompt: 'IMPORTANT: Always respond in the same language the user writes in. If the user writes in Chinese, respond in Chinese. If the user writes in English, respond in English. Match the user\'s language throughout the conversation.',
|
|
99
103
|
});
|
|
100
104
|
}
|
|
101
105
|
else if (providerType === 'anthropic') {
|
|
@@ -427,10 +431,20 @@ export function createVercelProvider(modelId, providerType) {
|
|
|
427
431
|
}
|
|
428
432
|
}
|
|
429
433
|
catch (err) {
|
|
430
|
-
// Ignore abort errors from user-input pauses or external cancellation
|
|
434
|
+
// Ignore abort errors from user-input pauses or external cancellation.
|
|
435
|
+
// For other errors (e.g. transient API 400s from tool-use concurrency),
|
|
436
|
+
// log and surface them as SSE error events instead of killing the stream.
|
|
431
437
|
const externallyAborted = config.signal?.aborted;
|
|
432
|
-
if (!waitingForUserInput && !externallyAborted)
|
|
433
|
-
|
|
438
|
+
if (!waitingForUserInput && !externallyAborted) {
|
|
439
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
440
|
+
logEvent('error', `stream error (non-fatal): ${errorMsg}`);
|
|
441
|
+
const errorData = JSON.stringify({
|
|
442
|
+
type: 'error',
|
|
443
|
+
message: errorMsg
|
|
444
|
+
});
|
|
445
|
+
addToHistory('error', errorData);
|
|
446
|
+
await stream.writeSSE({ event: 'error', data: errorData });
|
|
447
|
+
}
|
|
434
448
|
}
|
|
435
449
|
// Capture response messages for multi-turn accumulation.
|
|
436
450
|
// Skip only when externally aborted (new request cancelled this one).
|