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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # ufoo
2
2
 
3
+ [中文文档](README.zh-CN.md)
4
+
3
5
  🤖 Multi-agent AI collaboration framework for orchestrating Claude Code, OpenAI Codex, and custom AI agents.
4
6
 
5
7
  📦 **npm**: [https://www.npmjs.com/package/u-foo](https://www.npmjs.com/package/u-foo)
package/README.zh-CN.md CHANGED
@@ -1,35 +1,168 @@
1
1
  # ufoo
2
2
 
3
- 多Agent AI 协作工具包,支持 Claude Code 和 OpenAI Codex。
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
+ [![npm version](https://img.shields.io/npm/v/u-foo.svg)](https://www.npmjs.com/package/u-foo)
10
+ [![npm downloads](https://img.shields.io/npm/dm/u-foo.svg)](https://www.npmjs.com/package/u-foo)
11
+ [![License](https://img.shields.io/badge/license-UNLICENSED-red.svg)](LICENSE)
12
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
13
+ [![Platform](https://img.shields.io/badge/platform-macOS-blue.svg)](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
- - **事件总线** - Agent间实时消息通信 (`ufoo bus`)
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`) 和 Codex (`ucodex`) 自动初始化
10
- - **技能系统** - 可扩展的Agent能力 (`ufoo skills`)
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
- git clone <repo> ~/.ufoo
17
- cd ~/.ufoo && npm link
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
- # 或使用Agent包装器(自动初始化 + 加入总线)
24
- uclaude # 代替 'claude'
25
- ucodex # 代替 'codex'
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 daemon --start|--stop|--status` | 管理 ufoo 守护进程 |
57
- | `ufoo chat` | 启动 ufoo 交互界面(无参数默认进入) |
58
- | `ufoo resume [nickname]` | 恢复 agent 会话(可选昵称) |
59
- | `ufoo bus join` | 加入事件总线(uclaude/ucodex 自动完成)|
60
- | `ufoo bus send <id> <msg>` | 发送消息给Agent |
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 doctor` | 检查安装状态 |
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
- └── ucodex # Codex 包装器
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
- └── agent/ # Agent 启动/运行
85
- ├── scripts/ # 历史遗留(bash,已弃用)
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
- └── DECISIONS/ # 决策记录
107
- ├── scripts/ # 软链接(历史遗留,可选)
262
+ ├── decisions/ # 决策记录
263
+ │ └── decisions.jsonl # 决策索引
108
264
  ├── AGENTS.md # 注入的协议块
109
265
  └── CLAUDE.md # → AGENTS.md
110
266
  ```
111
267
 
112
- ## Agent通信
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(用于 Terminal.app/iTerm2 注入功能)
139
- - Node.js >= 18(可选,用于 npm 全局安装)
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
- 如果 Codex CLI 在 `~/.codex` 下报权限错误(例如 sessions 目录),请在启动 daemon/chat 前设置可写的 `CODEX_HOME`:
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 daemon start
149
- ufoo chat
338
+ ufoo chat # 守护进程自动启动
150
339
  ```
151
340
 
152
341
  ## 开发
153
342
 
343
+ ### 环境搭建
154
344
  ```bash
155
- # 本地开发
156
- ./bin/ufoo --help
345
+ # 克隆仓库
346
+ git clone https://github.com/Icyoung/ufoo.git
347
+ cd ufoo
348
+
349
+ # 安装依赖
350
+ npm install
157
351
 
158
- # 或通过 Node
352
+ # 本地开发链接
159
353
  npm link
160
- ufoo --help
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.6",
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 line = `${prefixLabel}${escapeBlessed(displayMessage)}`;
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}${escapeBlessed(line)}`;
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}${escapeBlessed(line)}`);
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
- logBox.setLine(state.lineIndex, `${prefix}${escapeBlessed(state.buffer)}`);
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 renderState = state && typeof state === "object" ? state : {};
247
- if (typeof renderState.inCodeBlock !== "boolean") {
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!"