u-foo 1.2.6 → 1.2.8
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 +2 -0
- package/README.zh-CN.md +249 -39
- package/modules/online/README.md +74 -0
- package/package.json +1 -2
- package/src/chat/daemonMessageRouter.js +7 -1
- package/src/chat/streamTracker.js +15 -3
- package/src/code/tui.js +2 -100
- package/src/shared/markdownRenderer.js +121 -0
- package/scripts/import-pi-mono.js +0 -124
- package/scripts/sync-claude-skills.sh +0 -21
package/README.md
CHANGED
package/README.zh-CN.md
CHANGED
|
@@ -1,35 +1,168 @@
|
|
|
1
1
|
# ufoo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
🤖 多Agent AI 协作框架,支持 Claude Code、OpenAI Codex 和自定义 AI Agent 的编排协作。
|
|
6
|
+
|
|
7
|
+
📦 **npm**: [https://www.npmjs.com/package/u-foo](https://www.npmjs.com/package/u-foo)
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/u-foo)
|
|
10
|
+
[](https://www.npmjs.com/package/u-foo)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](https://www.apple.com/macos)
|
|
14
|
+
|
|
15
|
+
## 为什么选择 ufoo?
|
|
16
|
+
|
|
17
|
+
ufoo 解决多 AI 编程 Agent 协同工作的难题:
|
|
18
|
+
|
|
19
|
+
- **🔗 统一界面** - 一个聊天 UI 管理所有 AI Agent
|
|
20
|
+
- **📬 消息路由** - Agent 之间通过事件总线通信协作
|
|
21
|
+
- **🧠 上下文共享** - 跨 Agent 共享决策和知识
|
|
22
|
+
- **🚀 自动初始化** - Agent 包装器自动完成配置
|
|
23
|
+
- **📝 决策追踪** - 记录架构决策和权衡取舍
|
|
24
|
+
- **⚡ 实时更新** - 即时查看 Agent 状态和消息
|
|
4
25
|
|
|
5
26
|
## 功能特性
|
|
6
27
|
|
|
7
|
-
-
|
|
28
|
+
- **聊天界面** - 交互式多 Agent 聊天 UI (`ufoo chat`)
|
|
29
|
+
- 实时 Agent 通信和状态监控
|
|
30
|
+
- 仪表盘展示 Agent 列表、在线状态和快捷操作
|
|
31
|
+
- 使用 `@agent-name` 向特定 Agent 发送消息
|
|
32
|
+
- **事件总线** - Agent 间实时消息通信 (`ufoo bus`)
|
|
8
33
|
- **上下文共享** - 共享决策和项目上下文 (`ufoo ctx`)
|
|
9
|
-
- **Agent包装器** - Claude Code (`uclaude`) 和
|
|
10
|
-
-
|
|
34
|
+
- **Agent 包装器** - Claude Code (`uclaude`)、Codex (`ucodex`) 和 ucode 助手 (`ucode`) 自动初始化
|
|
35
|
+
- **PTY 包装器** - 智能终端模拟与就绪检测
|
|
36
|
+
- **智能探针注入** - 等待 Agent 初始化完成后再注入命令
|
|
37
|
+
- **统一命名** - 一致的 Agent 命名规范(如 ucode-1、claude-1、codex-1)
|
|
38
|
+
- **技能系统** - 可扩展的 Agent 能力 (`ufoo skills`)
|
|
11
39
|
|
|
12
|
-
##
|
|
40
|
+
## 安装
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 从 npm 全局安装(推荐)
|
|
44
|
+
npm install -g u-foo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
或从源码安装:
|
|
13
48
|
|
|
14
49
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
50
|
+
git clone https://github.com/Icyoung/ufoo.git ~/.ufoo
|
|
51
|
+
cd ~/.ufoo && npm install && npm link
|
|
52
|
+
```
|
|
18
53
|
|
|
54
|
+
安装后可使用以下全局命令:`ufoo`、`uclaude`、`ucodex`、`ucode`。
|
|
55
|
+
|
|
56
|
+
## 快速开始
|
|
57
|
+
|
|
58
|
+
```bash
|
|
19
59
|
# 初始化项目
|
|
20
60
|
cd your-project
|
|
21
61
|
ufoo init
|
|
22
62
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
63
|
+
# 启动聊天界面(默认命令)
|
|
64
|
+
ufoo chat
|
|
65
|
+
# 或直接
|
|
66
|
+
ufoo
|
|
67
|
+
|
|
68
|
+
# 使用 Agent 包装器(自动初始化 + 加入总线)
|
|
69
|
+
uclaude # Claude Code 包装器
|
|
70
|
+
ucodex # Codex 包装器
|
|
71
|
+
ucode # ucode 助手(自研 AI 编程 Agent)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 示例工作流
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 1. 启动聊天界面
|
|
78
|
+
$ ufoo
|
|
79
|
+
|
|
80
|
+
# 2. 从聊天中启动 Agent
|
|
81
|
+
> /launch claude
|
|
82
|
+
> /launch ucode
|
|
83
|
+
|
|
84
|
+
# 3. 向 Agent 发送任务
|
|
85
|
+
> @claude-1 请分析当前代码库结构
|
|
86
|
+
> @ucode-1 修复认证模块的 bug
|
|
87
|
+
|
|
88
|
+
# 4. Agent 通过总线通信
|
|
89
|
+
claude-1: 分析完成,发现 3 处需要重构...
|
|
90
|
+
ucode-1: Bug 已修复,正在运行测试...
|
|
91
|
+
|
|
92
|
+
# 5. 查看已做的决策
|
|
93
|
+
> /decisions
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
原生自研实现位于 `src/code` 目录。
|
|
97
|
+
|
|
98
|
+
准备和验证 `ucode` 运行时:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
ufoo ucode doctor
|
|
102
|
+
ufoo ucode prepare
|
|
103
|
+
ufoo ucode build
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
尝试原生核心队列运行时(开发中):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
ucode-core submit --tool read --args-json '{"path":"README.md"}'
|
|
110
|
+
ucode-core run-once --json
|
|
111
|
+
ucode-core list --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Agent 配置
|
|
115
|
+
|
|
116
|
+
在 `.ufoo/config.json` 中配置 AI 提供商:
|
|
117
|
+
|
|
118
|
+
### ucode 配置(自研助手)
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"ucodeProvider": "openai",
|
|
122
|
+
"ucodeModel": "gpt-4-turbo-preview",
|
|
123
|
+
"ucodeBaseUrl": "https://api.openai.com/v1",
|
|
124
|
+
"ucodeApiKey": "sk-***"
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Claude 配置
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"claudeProvider": "claude-cli",
|
|
132
|
+
"claudeModel": "claude-3-opus"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Codex 配置
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"codexProvider": "codex-cli",
|
|
140
|
+
"codexModel": "gpt-4"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 完整示例
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"launchMode": "internal",
|
|
148
|
+
"ucodeProvider": "openai",
|
|
149
|
+
"ucodeModel": "gpt-4-turbo-preview",
|
|
150
|
+
"ucodeBaseUrl": "https://api.openai.com/v1",
|
|
151
|
+
"ucodeApiKey": "sk-***",
|
|
152
|
+
"claudeProvider": "claude-cli",
|
|
153
|
+
"claudeModel": "claude-3-opus",
|
|
154
|
+
"codexProvider": "codex-cli",
|
|
155
|
+
"codexModel": "gpt-4"
|
|
156
|
+
}
|
|
26
157
|
```
|
|
27
158
|
|
|
159
|
+
`ucode` 会将配置写入专用运行时目录(`.ufoo/agent/ucode/pi-agent`),用于原生 planner/engine 调用。
|
|
160
|
+
|
|
28
161
|
## 架构
|
|
29
162
|
|
|
30
163
|
```
|
|
31
164
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
32
|
-
│ uclaude │ │ ucodex │ │
|
|
165
|
+
│ uclaude │ │ ucodex │ │ ucode │
|
|
33
166
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
|
34
167
|
│ │ │
|
|
35
168
|
└───────────────────┼───────────────────┘
|
|
@@ -49,21 +182,43 @@ Bus 状态存放于 `.ufoo/agent/all-agents.json`(元数据)、`.ufoo/bus/*`
|
|
|
49
182
|
|
|
50
183
|
## 命令列表
|
|
51
184
|
|
|
185
|
+
### 核心命令
|
|
52
186
|
| 命令 | 说明 |
|
|
53
187
|
|------|------|
|
|
188
|
+
| `ufoo` | 启动聊天界面(默认) |
|
|
189
|
+
| `ufoo chat` | 启动交互式多 Agent 聊天 UI |
|
|
54
190
|
| `ufoo init` | 在当前项目初始化 .ufoo |
|
|
55
191
|
| `ufoo status` | 显示 banner、未读消息和未处理决策 |
|
|
56
|
-
| `ufoo
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
|
60
|
-
|
|
192
|
+
| `ufoo doctor` | 检查安装状态 |
|
|
193
|
+
|
|
194
|
+
### Agent 管理
|
|
195
|
+
| 命令 | 说明 |
|
|
196
|
+
|------|------|
|
|
197
|
+
| `ufoo daemon start` | 启动 ufoo 守护进程 |
|
|
198
|
+
| `ufoo daemon stop` | 停止 ufoo 守护进程 |
|
|
199
|
+
| `ufoo daemon status` | 查看守护进程状态 |
|
|
200
|
+
| `ufoo resume [nickname]` | 恢复 Agent 会话 |
|
|
201
|
+
|
|
202
|
+
### 事件总线
|
|
203
|
+
| 命令 | 说明 |
|
|
204
|
+
|------|------|
|
|
205
|
+
| `ufoo bus join` | 加入事件总线(Agent 包装器自动完成) |
|
|
206
|
+
| `ufoo bus send <id> <msg>` | 发送消息给 Agent |
|
|
61
207
|
| `ufoo bus check <id>` | 检查待处理消息 |
|
|
62
|
-
| `ufoo bus status` |
|
|
208
|
+
| `ufoo bus status` | 查看总线状态和在线 Agent |
|
|
209
|
+
|
|
210
|
+
### 上下文与决策
|
|
211
|
+
| 命令 | 说明 |
|
|
212
|
+
|------|------|
|
|
63
213
|
| `ufoo ctx decisions -l` | 列出所有决策 |
|
|
64
214
|
| `ufoo ctx decisions -n 1` | 显示最新决策 |
|
|
215
|
+
| `ufoo ctx decisions new <title>` | 创建新决策 |
|
|
216
|
+
|
|
217
|
+
### 技能
|
|
218
|
+
| 命令 | 说明 |
|
|
219
|
+
|------|------|
|
|
65
220
|
| `ufoo skills list` | 列出可用技能 |
|
|
66
|
-
| `ufoo
|
|
221
|
+
| `ufoo skills show <skill>` | 显示技能详情 |
|
|
67
222
|
|
|
68
223
|
备注:
|
|
69
224
|
- Claude CLI 的 headless agent 使用 `--dangerously-skip-permissions`。
|
|
@@ -76,13 +231,14 @@ ufoo/
|
|
|
76
231
|
│ ├── ufoo # 主 CLI 入口 (bash)
|
|
77
232
|
│ ├── ufoo.js # Node 包装器
|
|
78
233
|
│ ├── uclaude # Claude Code 包装器
|
|
79
|
-
│
|
|
234
|
+
│ ├── ucodex # Codex 包装器
|
|
235
|
+
│ └── ucode # ucode 助手包装器
|
|
80
236
|
├── SKILLS/ # 全局技能(uinit, ustatus)
|
|
81
237
|
├── src/
|
|
82
238
|
│ ├── bus/ # 事件总线实现(JS)
|
|
83
239
|
│ ├── daemon/ # Daemon + chat bridge
|
|
84
|
-
│
|
|
85
|
-
|
|
240
|
+
│ ├── agent/ # Agent 启动/运行
|
|
241
|
+
│ └── code/ # 原生 ucode 核心实现
|
|
86
242
|
├── modules/
|
|
87
243
|
│ ├── context/ # 决策/上下文协议
|
|
88
244
|
│ ├── bus/ # 总线模块资源
|
|
@@ -100,21 +256,52 @@ your-project/
|
|
|
100
256
|
├── .ufoo/
|
|
101
257
|
│ ├── bus/
|
|
102
258
|
│ │ ├── events/ # 事件日志(只追加)
|
|
103
|
-
│ │ ├── queues/ # 每个Agent的消息队列
|
|
259
|
+
│ │ ├── queues/ # 每个 Agent 的消息队列
|
|
104
260
|
│ │ └── offsets/ # 读取位置跟踪
|
|
105
261
|
│ └── context/
|
|
106
|
-
│
|
|
107
|
-
|
|
262
|
+
│ ├── decisions/ # 决策记录
|
|
263
|
+
│ └── decisions.jsonl # 决策索引
|
|
108
264
|
├── AGENTS.md # 注入的协议块
|
|
109
265
|
└── CLAUDE.md # → AGENTS.md
|
|
110
266
|
```
|
|
111
267
|
|
|
112
|
-
##
|
|
268
|
+
## 聊天界面
|
|
269
|
+
|
|
270
|
+
交互式聊天 UI 提供集中化的 Agent 管理中心:
|
|
271
|
+
|
|
272
|
+
### 功能
|
|
273
|
+
- **实时通信** - 在一个界面查看所有 Agent 消息
|
|
274
|
+
- **Agent 仪表盘** - 监控在线状态、会话 ID 和昵称
|
|
275
|
+
- **定向消息** - 使用 `@agent-name` 向特定 Agent 发送消息
|
|
276
|
+
- **命令补全** - Tab 键补全命令和 Agent 名称
|
|
277
|
+
- **鼠标支持** - `Ctrl+M` 切换鼠标模式(滚动 vs 文本选择)
|
|
278
|
+
- **会话历史** - 跨会话持久化消息记录
|
|
279
|
+
|
|
280
|
+
### 快捷键
|
|
281
|
+
| 按键 | 操作 |
|
|
282
|
+
|------|------|
|
|
283
|
+
| `Tab` | 自动补全命令/Agent |
|
|
284
|
+
| `Ctrl+C` | 退出聊天 |
|
|
285
|
+
| `Ctrl+M` | 切换鼠标模式 |
|
|
286
|
+
| `Ctrl+L` | 清屏 |
|
|
287
|
+
| `Ctrl+R` | 刷新 Agent 列表 |
|
|
288
|
+
| `↑/↓` | 浏览命令历史 |
|
|
289
|
+
|
|
290
|
+
### 聊天命令
|
|
291
|
+
| 命令 | 说明 |
|
|
292
|
+
|------|------|
|
|
293
|
+
| `/help` | 显示可用命令 |
|
|
294
|
+
| `/agents` | 列出在线 Agent |
|
|
295
|
+
| `/clear` | 清除聊天记录 |
|
|
296
|
+
| `/settings` | 配置聊天偏好 |
|
|
297
|
+
| `@agent-name <message>` | 向特定 Agent 发送消息 |
|
|
113
298
|
|
|
114
|
-
Agent
|
|
299
|
+
## Agent 通信
|
|
300
|
+
|
|
301
|
+
Agent 通过事件总线通信:
|
|
115
302
|
|
|
116
303
|
```bash
|
|
117
|
-
# Agent A 向Agent B 发送任务
|
|
304
|
+
# Agent A 向 Agent B 发送任务
|
|
118
305
|
ufoo bus send "codex:abc123" "请分析项目结构"
|
|
119
306
|
|
|
120
307
|
# Agent B 检查并执行
|
|
@@ -124,7 +311,7 @@ ufoo bus check "codex:abc123"
|
|
|
124
311
|
ufoo bus send "claude-code:xyz789" "分析完成:..."
|
|
125
312
|
```
|
|
126
313
|
|
|
127
|
-
## 技能(供Agent使用)
|
|
314
|
+
## 技能(供 Agent 使用)
|
|
128
315
|
|
|
129
316
|
内置技能通过斜杠命令触发:
|
|
130
317
|
|
|
@@ -135,31 +322,54 @@ ufoo bus send "claude-code:xyz789" "分析完成:..."
|
|
|
135
322
|
|
|
136
323
|
## 系统要求
|
|
137
324
|
|
|
138
|
-
- macOS
|
|
139
|
-
- Node.js >= 18
|
|
140
|
-
- Bash 4
|
|
325
|
+
- **macOS** - 用于 Terminal.app/iTerm2 集成
|
|
326
|
+
- **Node.js >= 18** - npm 安装和 JavaScript 运行时
|
|
327
|
+
- **Bash 4+** - Shell 脚本和命令执行
|
|
328
|
+
- **终端** - iTerm2 或 Terminal.app 用于启动 Agent
|
|
141
329
|
|
|
142
330
|
## Codex CLI 说明
|
|
143
331
|
|
|
144
|
-
|
|
332
|
+
`ufoo chat` 会自动启动守护进程(无需单独运行 `ufoo daemon start`)。
|
|
333
|
+
|
|
334
|
+
如果 Codex CLI 在 `~/.codex` 下报权限错误(例如 sessions 目录),请设置可写的 `CODEX_HOME`:
|
|
145
335
|
|
|
146
336
|
```bash
|
|
147
337
|
export CODEX_HOME="$PWD/.ufoo/codex"
|
|
148
|
-
ufoo
|
|
149
|
-
ufoo chat
|
|
338
|
+
ufoo chat # 守护进程自动启动
|
|
150
339
|
```
|
|
151
340
|
|
|
152
341
|
## 开发
|
|
153
342
|
|
|
343
|
+
### 环境搭建
|
|
154
344
|
```bash
|
|
155
|
-
#
|
|
156
|
-
|
|
345
|
+
# 克隆仓库
|
|
346
|
+
git clone https://github.com/Icyoung/ufoo.git
|
|
347
|
+
cd ufoo
|
|
348
|
+
|
|
349
|
+
# 安装依赖
|
|
350
|
+
npm install
|
|
157
351
|
|
|
158
|
-
#
|
|
352
|
+
# 本地开发链接
|
|
159
353
|
npm link
|
|
160
|
-
|
|
354
|
+
|
|
355
|
+
# 运行测试
|
|
356
|
+
npm test
|
|
161
357
|
```
|
|
162
358
|
|
|
359
|
+
### 参与贡献
|
|
360
|
+
- Fork 本仓库
|
|
361
|
+
- 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
|
362
|
+
- 提交更改 (`git commit -m 'Add amazing feature'`)
|
|
363
|
+
- 推送分支 (`git push origin feature/amazing-feature`)
|
|
364
|
+
- 发起 Pull Request
|
|
365
|
+
|
|
366
|
+
### 项目结构
|
|
367
|
+
- `src/` - 核心 JavaScript 实现
|
|
368
|
+
- `bin/` - CLI 入口
|
|
369
|
+
- `modules/` - 模块化功能(bus、context 等)
|
|
370
|
+
- `test/` - 单元测试和集成测试
|
|
371
|
+
- `SKILLS/` - Agent 技能定义
|
|
372
|
+
|
|
163
373
|
## 许可证
|
|
164
374
|
|
|
165
375
|
UNLICENSED(私有)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# online
|
|
2
|
+
|
|
3
|
+
WebSocket relay module for cross-machine agent collaboration. Extends the local ufoo bus to work over the network.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
online enables agents on different machines to collaborate:
|
|
8
|
+
|
|
9
|
+
- Public channel chat (broadcast to all connected agents)
|
|
10
|
+
- Private room collaboration (bus/decisions/wake sync)
|
|
11
|
+
- Token-based authentication
|
|
12
|
+
- Auto-reconnect with exponential backoff
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Start a relay server
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ufoo online server --port 8787
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Connect an agent
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Join a public channel
|
|
26
|
+
ufoo online connect --nickname my-agent --join lobby
|
|
27
|
+
|
|
28
|
+
# Join a private room
|
|
29
|
+
ufoo online connect --nickname my-agent --room room_001 --room-password secret
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 3. Send messages
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# To a channel
|
|
36
|
+
ufoo online send --nickname my-agent --channel lobby --text "hello everyone"
|
|
37
|
+
|
|
38
|
+
# To a room
|
|
39
|
+
ufoo online send --nickname my-agent --room room_001 --text "hello team"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 4. Check inbox
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ufoo online inbox my-agent # All messages
|
|
46
|
+
ufoo online inbox my-agent --unread # Unread only
|
|
47
|
+
ufoo online inbox my-agent --clear # Clear inbox
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Private Room Sync
|
|
51
|
+
|
|
52
|
+
In private room mode, agents automatically sync:
|
|
53
|
+
|
|
54
|
+
- **Bus messages** — local bus ↔ online relay, bidirectional
|
|
55
|
+
- **Decisions** — new `.md` files synced across team
|
|
56
|
+
- **Wake events** — remote agent can wake local agent via bus
|
|
57
|
+
|
|
58
|
+
## Storage
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
~/.ufoo/online/
|
|
62
|
+
├── tokens.json # Auth tokens
|
|
63
|
+
├── inbox/<nickname>.jsonl # Incoming messages
|
|
64
|
+
└── outbox/<nickname>.jsonl # Queued outgoing messages
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Relationship with bus
|
|
68
|
+
|
|
69
|
+
| Module | Scope |
|
|
70
|
+
|--------|-------|
|
|
71
|
+
| bus | Local file-system based messaging within a single machine |
|
|
72
|
+
| online | Network relay extending bus across machines via WebSocket |
|
|
73
|
+
|
|
74
|
+
online builds on top of bus — local agents still communicate via the file-system bus, while online bridges messages to remote agents.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"postinstall": "node scripts/postinstall.js",
|
|
43
|
-
"import:pi-mono": "node scripts/import-pi-mono.js",
|
|
44
43
|
"test": "jest",
|
|
45
44
|
"test:watch": "jest --watch",
|
|
46
45
|
"test:coverage": "jest --coverage"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
2
|
+
const { renderMarkdownLines } = require("../shared/markdownRenderer");
|
|
2
3
|
|
|
3
4
|
function createDaemonMessageRouter(options = {}) {
|
|
4
5
|
const {
|
|
@@ -215,7 +216,12 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
215
216
|
if (hasStream(publisher)) {
|
|
216
217
|
finalizeStream(publisher, data, "interrupted");
|
|
217
218
|
}
|
|
218
|
-
const
|
|
219
|
+
const mdState = {};
|
|
220
|
+
const renderedLines = renderMarkdownLines(displayMessage, mdState, escapeBlessed);
|
|
221
|
+
const line = renderedLines.map((l, i) => {
|
|
222
|
+
const p = i === 0 ? prefixLabel : continuationPrefix;
|
|
223
|
+
return `${p}${l}`;
|
|
224
|
+
}).join("\n");
|
|
219
225
|
logMessage("bus", line, data);
|
|
220
226
|
if (data.event === "message" && pendingBeforeMessage) {
|
|
221
227
|
consumePendingDelivery(publisher, displayName);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { renderMarkdownLines } = require("../shared/markdownRenderer");
|
|
2
|
+
|
|
1
3
|
function createStreamTracker(options = {}) {
|
|
2
4
|
const {
|
|
3
5
|
logBox,
|
|
@@ -11,11 +13,17 @@ function createStreamTracker(options = {}) {
|
|
|
11
13
|
const streamStates = new Map();
|
|
12
14
|
const pendingDeliveries = new Map();
|
|
13
15
|
|
|
16
|
+
function renderLine(line, mdState) {
|
|
17
|
+
const rendered = renderMarkdownLines(line, mdState, escapeBlessed);
|
|
18
|
+
return rendered.length > 0 ? rendered[0] : escapeBlessed(line);
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
function buildStreamDisplayText(fullText, prefix, continuationPrefix) {
|
|
22
|
+
const mdState = {};
|
|
15
23
|
const lines = String(fullText || "").split("\n");
|
|
16
24
|
return lines.map((line, i) => {
|
|
17
25
|
const p = i === 0 ? prefix : continuationPrefix;
|
|
18
|
-
return `${p}${
|
|
26
|
+
return `${p}${renderLine(line, mdState)}`;
|
|
19
27
|
}).join("\n");
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -36,6 +44,7 @@ function createStreamTracker(options = {}) {
|
|
|
36
44
|
full: "",
|
|
37
45
|
linesEmitted: 0,
|
|
38
46
|
meta,
|
|
47
|
+
mdState: {},
|
|
39
48
|
};
|
|
40
49
|
streamStates.set(publisher, state);
|
|
41
50
|
if (typeof onStreamStart === "function") {
|
|
@@ -53,7 +62,7 @@ function createStreamTracker(options = {}) {
|
|
|
53
62
|
const completed = parts.slice(0, -1);
|
|
54
63
|
for (const line of completed) {
|
|
55
64
|
const prefix = state.linesEmitted === 0 ? state.prefix : state.continuationPrefix;
|
|
56
|
-
logBox.setLine(state.lineIndex, `${prefix}${
|
|
65
|
+
logBox.setLine(state.lineIndex, `${prefix}${renderLine(line, state.mdState)}`);
|
|
57
66
|
state.linesEmitted += 1;
|
|
58
67
|
logBox.pushLine(state.continuationPrefix);
|
|
59
68
|
state.lineIndex = logBox.getLines().length - 1;
|
|
@@ -61,7 +70,10 @@ function createStreamTracker(options = {}) {
|
|
|
61
70
|
state.buffer = parts[parts.length - 1];
|
|
62
71
|
}
|
|
63
72
|
const prefix = state.linesEmitted === 0 ? state.prefix : state.continuationPrefix;
|
|
64
|
-
|
|
73
|
+
// For the current incomplete line, render with a snapshot of mdState
|
|
74
|
+
// to avoid mutating state on partial lines
|
|
75
|
+
const snapState = { inCodeBlock: state.mdState.inCodeBlock };
|
|
76
|
+
logBox.setLine(state.lineIndex, `${prefix}${renderLine(state.buffer, snapState)}`);
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
function finalizeStream(publisher, meta, reason = "") {
|
package/src/code/tui.js
CHANGED
|
@@ -243,106 +243,8 @@ function loadActiveAgents(workspaceRoot) {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
function renderLogLinesWithMarkdown(text = "", state = {}, escapeFn = (value) => String(value || "")) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
renderState.inCodeBlock = false;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const renderInlineCode = (input = "") => {
|
|
252
|
-
const source = String(input || "");
|
|
253
|
-
if (!source) return "";
|
|
254
|
-
if (!source.includes("`")) return escapeFn(source);
|
|
255
|
-
|
|
256
|
-
let out = "";
|
|
257
|
-
let cursor = 0;
|
|
258
|
-
const pattern = /`([^`\n]+)`/g;
|
|
259
|
-
let match = pattern.exec(source);
|
|
260
|
-
while (match) {
|
|
261
|
-
const index = Number(match.index) || 0;
|
|
262
|
-
if (index > cursor) {
|
|
263
|
-
out += escapeFn(source.slice(cursor, index));
|
|
264
|
-
}
|
|
265
|
-
out += `{yellow-fg}${escapeFn(match[1])}{/yellow-fg}`;
|
|
266
|
-
cursor = index + match[0].length;
|
|
267
|
-
match = pattern.exec(source);
|
|
268
|
-
}
|
|
269
|
-
if (cursor < source.length) {
|
|
270
|
-
out += escapeFn(source.slice(cursor));
|
|
271
|
-
}
|
|
272
|
-
return out;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const lines = String(text || "").split(/\r?\n/);
|
|
276
|
-
const out = [];
|
|
277
|
-
|
|
278
|
-
for (const line of lines) {
|
|
279
|
-
const raw = stripLeakedEscapeTags(String(line || ""));
|
|
280
|
-
const fenceMatch = raw.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
|
281
|
-
if (fenceMatch) {
|
|
282
|
-
if (!renderState.inCodeBlock) {
|
|
283
|
-
const language = String(fenceMatch[3] || "").trim();
|
|
284
|
-
const label = language
|
|
285
|
-
? `┌ code:${escapeFn(language)}`
|
|
286
|
-
: "┌ code";
|
|
287
|
-
out.push(`{gray-fg}${label}{/gray-fg}`);
|
|
288
|
-
renderState.inCodeBlock = true;
|
|
289
|
-
} else {
|
|
290
|
-
out.push("{gray-fg}└{/gray-fg}");
|
|
291
|
-
renderState.inCodeBlock = false;
|
|
292
|
-
}
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (renderState.inCodeBlock) {
|
|
297
|
-
out.push(`{gray-fg}│{/gray-fg} {white-fg}${escapeFn(raw)}{/white-fg}`);
|
|
298
|
-
} else {
|
|
299
|
-
const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
|
|
300
|
-
if (headingMatch) {
|
|
301
|
-
const indent = escapeFn(headingMatch[1] || "");
|
|
302
|
-
const marks = escapeFn(headingMatch[2] || "");
|
|
303
|
-
const content = renderInlineCode(headingMatch[3] || "");
|
|
304
|
-
out.push(`${indent}{cyan-fg}${marks}{/cyan-fg} {bold}${content}{/bold}`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
|
|
309
|
-
if (quoteMatch) {
|
|
310
|
-
const indent = escapeFn(quoteMatch[1] || "");
|
|
311
|
-
const content = renderInlineCode(quoteMatch[2] || "");
|
|
312
|
-
out.push(`${indent}{gray-fg}▍{/gray-fg} ${content}`);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
317
|
-
if (bulletMatch) {
|
|
318
|
-
const indent = escapeFn(bulletMatch[1] || "");
|
|
319
|
-
const content = renderInlineCode(bulletMatch[3] || "");
|
|
320
|
-
out.push(`${indent}{gray-fg}•{/gray-fg} ${content}`);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
|
325
|
-
if (orderedMatch) {
|
|
326
|
-
const indent = escapeFn(orderedMatch[1] || "");
|
|
327
|
-
const order = escapeFn(orderedMatch[2] || "");
|
|
328
|
-
const content = renderInlineCode(orderedMatch[3] || "");
|
|
329
|
-
out.push(`${indent}{gray-fg}${order}.{/gray-fg} ${content}`);
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
|
|
334
|
-
if (errorMatch) {
|
|
335
|
-
const indent = escapeFn(errorMatch[1] || "");
|
|
336
|
-
const content = renderInlineCode(errorMatch[2] || "");
|
|
337
|
-
out.push(`${indent}{red-fg}${content}{/red-fg}`);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
out.push(renderInlineCode(raw));
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return out;
|
|
246
|
+
const { renderMarkdownLines } = require("../shared/markdownRenderer");
|
|
247
|
+
return renderMarkdownLines(text, state, escapeFn);
|
|
346
248
|
}
|
|
347
249
|
|
|
348
250
|
function shouldEnterAgentSelection(inputValue = "") {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared blessed-compatible markdown renderer for TUI output.
|
|
3
|
+
*
|
|
4
|
+
* Used by both ucode TUI and ufoo chat to render agent responses
|
|
5
|
+
* with fenced code blocks, headings, quotes, bullets, inline code, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function stripLeakedEscapeTags(text = "") {
|
|
9
|
+
const source = String(text == null ? "" : text);
|
|
10
|
+
const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
|
|
11
|
+
const withoutDanglingEscape = withoutClosedTags.replace(/\{\s*\/?\s*escape[\s\S]*$/gi, "");
|
|
12
|
+
return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String(value || "")) {
|
|
16
|
+
const renderState = state && typeof state === "object" ? state : {};
|
|
17
|
+
if (typeof renderState.inCodeBlock !== "boolean") {
|
|
18
|
+
renderState.inCodeBlock = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const renderInlineCode = (input = "") => {
|
|
22
|
+
const source = String(input || "");
|
|
23
|
+
if (!source) return "";
|
|
24
|
+
if (!source.includes("`")) return escapeFn(source);
|
|
25
|
+
|
|
26
|
+
let out = "";
|
|
27
|
+
let cursor = 0;
|
|
28
|
+
const pattern = /`([^`\n]+)`/g;
|
|
29
|
+
let match = pattern.exec(source);
|
|
30
|
+
while (match) {
|
|
31
|
+
const index = Number(match.index) || 0;
|
|
32
|
+
if (index > cursor) {
|
|
33
|
+
out += escapeFn(source.slice(cursor, index));
|
|
34
|
+
}
|
|
35
|
+
out += `{yellow-fg}${escapeFn(match[1])}{/yellow-fg}`;
|
|
36
|
+
cursor = index + match[0].length;
|
|
37
|
+
match = pattern.exec(source);
|
|
38
|
+
}
|
|
39
|
+
if (cursor < source.length) {
|
|
40
|
+
out += escapeFn(source.slice(cursor));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
46
|
+
const out = [];
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const raw = stripLeakedEscapeTags(String(line || ""));
|
|
50
|
+
const fenceMatch = raw.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
|
51
|
+
if (fenceMatch) {
|
|
52
|
+
if (!renderState.inCodeBlock) {
|
|
53
|
+
const language = String(fenceMatch[3] || "").trim();
|
|
54
|
+
const label = language
|
|
55
|
+
? `┌ code:${escapeFn(language)}`
|
|
56
|
+
: "┌ code";
|
|
57
|
+
out.push(`{gray-fg}${label}{/gray-fg}`);
|
|
58
|
+
renderState.inCodeBlock = true;
|
|
59
|
+
} else {
|
|
60
|
+
out.push("{gray-fg}└{/gray-fg}");
|
|
61
|
+
renderState.inCodeBlock = false;
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (renderState.inCodeBlock) {
|
|
67
|
+
out.push(`{gray-fg}│{/gray-fg} {white-fg}${escapeFn(raw)}{/white-fg}`);
|
|
68
|
+
} else {
|
|
69
|
+
const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
|
|
70
|
+
if (headingMatch) {
|
|
71
|
+
const indent = escapeFn(headingMatch[1] || "");
|
|
72
|
+
const marks = escapeFn(headingMatch[2] || "");
|
|
73
|
+
const content = renderInlineCode(headingMatch[3] || "");
|
|
74
|
+
out.push(`${indent}{cyan-fg}${marks}{/cyan-fg} {bold}${content}{/bold}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
|
|
79
|
+
if (quoteMatch) {
|
|
80
|
+
const indent = escapeFn(quoteMatch[1] || "");
|
|
81
|
+
const content = renderInlineCode(quoteMatch[2] || "");
|
|
82
|
+
out.push(`${indent}{gray-fg}▍{/gray-fg} ${content}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
87
|
+
if (bulletMatch) {
|
|
88
|
+
const indent = escapeFn(bulletMatch[1] || "");
|
|
89
|
+
const content = renderInlineCode(bulletMatch[3] || "");
|
|
90
|
+
out.push(`${indent}{gray-fg}•{/gray-fg} ${content}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
|
95
|
+
if (orderedMatch) {
|
|
96
|
+
const indent = escapeFn(orderedMatch[1] || "");
|
|
97
|
+
const order = escapeFn(orderedMatch[2] || "");
|
|
98
|
+
const content = renderInlineCode(orderedMatch[3] || "");
|
|
99
|
+
out.push(`${indent}{gray-fg}${order}.{/gray-fg} ${content}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
|
|
104
|
+
if (errorMatch) {
|
|
105
|
+
const indent = escapeFn(errorMatch[1] || "");
|
|
106
|
+
const content = renderInlineCode(errorMatch[2] || "");
|
|
107
|
+
out.push(`${indent}{red-fg}${content}{/red-fg}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
out.push(renderInlineCode(raw));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
stripLeakedEscapeTags,
|
|
120
|
+
renderMarkdownLines,
|
|
121
|
+
};
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console */
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { spawnSync } = require("child_process");
|
|
6
|
-
|
|
7
|
-
function usage() {
|
|
8
|
-
console.log("Usage: node scripts/import-pi-mono.js <pi-mono-source-path> [--target <target-path>]");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function shouldSkip(name = "") {
|
|
12
|
-
return name === ".git" || name === "node_modules";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function copyRecursive(source, target) {
|
|
16
|
-
const stat = fs.statSync(source);
|
|
17
|
-
if (stat.isDirectory()) {
|
|
18
|
-
fs.mkdirSync(target, { recursive: true });
|
|
19
|
-
const entries = fs.readdirSync(source);
|
|
20
|
-
for (const entry of entries) {
|
|
21
|
-
if (shouldSkip(entry)) continue;
|
|
22
|
-
copyRecursive(path.join(source, entry), path.join(target, entry));
|
|
23
|
-
}
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
27
|
-
fs.copyFileSync(source, target);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function parseArgs(argv = []) {
|
|
31
|
-
const args = Array.isArray(argv) ? argv : [];
|
|
32
|
-
const parsed = {
|
|
33
|
-
source: "",
|
|
34
|
-
target: "",
|
|
35
|
-
help: false,
|
|
36
|
-
};
|
|
37
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
38
|
-
const item = String(args[i] || "").trim();
|
|
39
|
-
if (!item) continue;
|
|
40
|
-
if (item === "--help" || item === "-h") {
|
|
41
|
-
parsed.help = true;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (item === "--target" || item === "-t") {
|
|
45
|
-
const next = String(args[i + 1] || "").trim();
|
|
46
|
-
if (next) parsed.target = next;
|
|
47
|
-
i += 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (!parsed.source) parsed.source = item;
|
|
51
|
-
}
|
|
52
|
-
return parsed;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readGitValue(sourceRoot = "", gitArgs = []) {
|
|
56
|
-
try {
|
|
57
|
-
const res = spawnSync("git", ["-C", sourceRoot, ...gitArgs], {
|
|
58
|
-
encoding: "utf8",
|
|
59
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
60
|
-
});
|
|
61
|
-
if (res.error || res.status !== 0) return "";
|
|
62
|
-
return String(res.stdout || "").trim();
|
|
63
|
-
} catch {
|
|
64
|
-
return "";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function readGitMetadata(sourceRoot = "") {
|
|
69
|
-
return {
|
|
70
|
-
commit: readGitValue(sourceRoot, ["rev-parse", "HEAD"]),
|
|
71
|
-
branch: readGitValue(sourceRoot, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
72
|
-
remote: readGitValue(sourceRoot, ["config", "--get", "remote.origin.url"]),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function main() {
|
|
77
|
-
const parsedArgs = parseArgs(process.argv.slice(2));
|
|
78
|
-
if (parsedArgs.help) {
|
|
79
|
-
usage();
|
|
80
|
-
process.exit(0);
|
|
81
|
-
}
|
|
82
|
-
const sourceArg = parsedArgs.source || "";
|
|
83
|
-
if (!sourceArg) {
|
|
84
|
-
usage();
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const sourceRoot = path.resolve(sourceArg);
|
|
89
|
-
const sourcePackage = path.join(sourceRoot, "package.json");
|
|
90
|
-
if (!fs.existsSync(sourcePackage)) {
|
|
91
|
-
console.error(`Invalid source: missing package.json at ${sourceRoot}`);
|
|
92
|
-
process.exit(2);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const repoRoot = path.resolve(__dirname, "..");
|
|
96
|
-
const targetRoot = path.resolve(parsedArgs.target || path.join(repoRoot, "src", "code", "pi-mono"));
|
|
97
|
-
const backupRoot = `${targetRoot}.backup-${Date.now()}`;
|
|
98
|
-
|
|
99
|
-
if (fs.existsSync(targetRoot)) {
|
|
100
|
-
fs.renameSync(targetRoot, backupRoot);
|
|
101
|
-
console.log(`Backed up existing fork to ${backupRoot}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
copyRecursive(sourceRoot, targetRoot);
|
|
105
|
-
const upstream = readGitMetadata(sourceRoot);
|
|
106
|
-
|
|
107
|
-
const metadata = {
|
|
108
|
-
imported_at: new Date().toISOString(),
|
|
109
|
-
source_root: sourceRoot,
|
|
110
|
-
target: targetRoot,
|
|
111
|
-
upstream_commit: upstream.commit,
|
|
112
|
-
upstream_branch: upstream.branch,
|
|
113
|
-
upstream_remote: upstream.remote,
|
|
114
|
-
};
|
|
115
|
-
fs.writeFileSync(
|
|
116
|
-
path.join(targetRoot, ".ufoo-import.json"),
|
|
117
|
-
`${JSON.stringify(metadata, null, 2)}\n`,
|
|
118
|
-
"utf8",
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
console.log(`Imported pi-mono into ${targetRoot}${upstream.commit ? ` @ ${upstream.commit}` : ""}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
main();
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Sync SKILLS to .claude/commands for Claude Code slash commands
|
|
3
|
-
|
|
4
|
-
set -e
|
|
5
|
-
|
|
6
|
-
SKILLS_DIR="SKILLS"
|
|
7
|
-
CLAUDE_COMMANDS_DIR=".claude/commands"
|
|
8
|
-
|
|
9
|
-
# Create commands directory if it doesn't exist
|
|
10
|
-
mkdir -p "$CLAUDE_COMMANDS_DIR"
|
|
11
|
-
|
|
12
|
-
# Copy all SKILL.md files to .claude/commands/
|
|
13
|
-
for skill_dir in "$SKILLS_DIR"/*; do
|
|
14
|
-
if [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ]; then
|
|
15
|
-
skill_name=$(basename "$skill_dir")
|
|
16
|
-
echo "Syncing $skill_name..."
|
|
17
|
-
cp "$skill_dir/SKILL.md" "$CLAUDE_COMMANDS_DIR/$skill_name.md"
|
|
18
|
-
fi
|
|
19
|
-
done
|
|
20
|
-
|
|
21
|
-
echo "Skills synced to Claude Code commands directory!"
|