agentworkshop 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,58 +1,62 @@
1
- // ============================================================
2
- // TUI 认证/连接配置 —— 落配置根 .AgentWorkShop/tui-auth.json(0600)。
3
- // 解析口径与服务端一致:cwd 向上找 config.yml → <repo>/.AgentWorkShop,
4
- // 找不到(全局安装任意 cwd)→ ~/.AgentWorkShop。
5
- // ============================================================
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs'
7
- import { homedir } from 'node:os'
8
- import { join, resolve, dirname } from 'node:path'
9
-
10
- /** 向上查找文件(repo 模式判定) */
11
- function findUp(startDir, filename) {
12
- let dir = resolve(startDir)
13
- for (;;) {
14
- const candidate = join(dir, filename)
15
- if (existsSync(candidate)) return candidate
16
- const parent = dirname(dir)
17
- if (parent === dir) return null
18
- dir = parent
19
- }
20
- }
21
-
22
- /** 配置根(repo 内 cwd → <repo>/.AgentWorkShop;否则 ~/.AgentWorkShop) */
23
- export function configRoot(cwd = process.cwd()) {
24
- const marker = findUp(cwd, 'config.yml')
25
- if (marker) return join(dirname(marker), '.AgentWorkShop')
26
- return join(homedir(), '.AgentWorkShop')
27
- }
28
-
29
- const authPath = cwd => join(configRoot(cwd), 'tui-auth.json')
30
-
31
- /** 读取已存凭据({ baseUrl, token, email? } 或 null) */
32
- export function loadAuth(cwd = process.cwd()) {
33
- try {
34
- const raw = JSON.parse(readFileSync(authPath(cwd), 'utf8'))
35
- if (raw && typeof raw.token === 'string' && typeof raw.baseUrl === 'string') return raw
36
- }
37
- catch { /* 无凭据/损坏 → 重新登录 */ }
38
- return null
39
- }
40
-
41
- /** 持久化凭据(0600;路径加入 .gitignore) */
42
- export function saveAuth(auth, cwd = process.cwd()) {
43
- const file = authPath(cwd)
44
- mkdirSync(dirname(file), { recursive: true })
45
- writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, 'utf8')
46
- try {
47
- chmodSync(file, 0o600)
48
- }
49
- catch { /* Windows FAT 卷 chmod 无效,忽略 */ }
50
- }
51
-
52
- /** 清除凭据(登出) */
53
- export function clearAuth(cwd = process.cwd()) {
54
- try {
55
- existsSync(authPath(cwd)) && (writeFileSync(authPath(cwd), '{}\n', 'utf8'), true)
56
- }
57
- catch { /* 尽力而为 */ }
58
- }
1
+ // ============================================================
2
+ // TUI 认证/连接配置 —— 落配置根 .AgentWorkShop/tui-auth.json(0600)。
3
+ // 解析口径与服务端一致:cwd 向上找 config.yml → <repo>/.AgentWorkShop,
4
+ // 找不到(全局安装任意 cwd)→ ~/.AgentWorkShop。
5
+ // ============================================================
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs'
7
+ import { homedir } from 'node:os'
8
+ import { join, resolve, dirname } from 'node:path'
9
+
10
+ /** 向上查找文件(repo 模式判定) */
11
+ function findUp(startDir, filename) {
12
+ let dir = resolve(startDir)
13
+ for (;;) {
14
+ const candidate = join(dir, filename)
15
+ if (existsSync(candidate)) return candidate
16
+ const parent = dirname(dir)
17
+ if (parent === dir) return null
18
+ dir = parent
19
+ }
20
+ }
21
+
22
+ /** 配置根(与服务端同策略):仅当用户自建了 <检出根>/.AgentWorkShop 时优先使用;
23
+ * 否则一律回落 ~/.AgentWorkShop(绝不在 pwd 下自动创建)。 */
24
+ export function configRoot(cwd = process.cwd()) {
25
+ const marker = findUp(cwd, 'config.yml')
26
+ if (marker) {
27
+ const candidate = join(dirname(marker), '.AgentWorkShop')
28
+ if (existsSync(candidate)) return candidate
29
+ }
30
+ return join(homedir(), '.AgentWorkShop')
31
+ }
32
+
33
+ const authPath = cwd => join(configRoot(cwd), 'tui-auth.json')
34
+
35
+ /** 读取已存凭据({ baseUrl, token, email? } 或 null) */
36
+ export function loadAuth(cwd = process.cwd()) {
37
+ try {
38
+ const raw = JSON.parse(readFileSync(authPath(cwd), 'utf8'))
39
+ if (raw && typeof raw.token === 'string' && typeof raw.baseUrl === 'string') return raw
40
+ }
41
+ catch { /* 无凭据/损坏 → 重新登录 */ }
42
+ return null
43
+ }
44
+
45
+ /** 持久化凭据(0600;路径加入 .gitignore) */
46
+ export function saveAuth(auth, cwd = process.cwd()) {
47
+ const file = authPath(cwd)
48
+ mkdirSync(dirname(file), { recursive: true })
49
+ writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, 'utf8')
50
+ try {
51
+ chmodSync(file, 0o600)
52
+ }
53
+ catch { /* Windows FAT 卷 chmod 无效,忽略 */ }
54
+ }
55
+
56
+ /** 清除凭据(登出) */
57
+ export function clearAuth(cwd = process.cwd()) {
58
+ try {
59
+ existsSync(authPath(cwd)) && (writeFileSync(authPath(cwd), '{}\n', 'utf8'), true)
60
+ }
61
+ catch { /* 尽力而为 */ }
62
+ }
@@ -0,0 +1,102 @@
1
+ // ============================================================
2
+ // 交互选择器(浮层)—— 频道选择 / Agent 选择。
3
+ // 基于 pi-tui showOverlay + SelectList:方向键移动、Enter 选择、Esc 取消。
4
+ // ============================================================
5
+ import { SelectList } from '@earendil-works/pi-tui'
6
+ import { theme } from '../theme.mjs'
7
+
8
+ /** 组合"标题 + SelectList"的浮层组件。返回 handle { hide, list, component, __bindHandle } */
9
+ function pickerComponent(title, items, maxVisible, onSelect, onCancel) {
10
+ const list = new SelectList(items, maxVisible, theme.editor.selectList)
11
+ const wrap = {
12
+ render(width) {
13
+ return [theme.accent(`┌─ ${title}`), ...list.render(Math.max(width - 2, 24))]
14
+ },
15
+ }
16
+ const handle = { hide() {}, list, component: wrap }
17
+ list.onSelect = (item) => {
18
+ handle.hide()
19
+ onSelect(item)
20
+ }
21
+ list.onCancel = () => {
22
+ handle.hide()
23
+ onCancel?.()
24
+ }
25
+ // 二次装配:showOverlay 返回真实 handle 后回填 hide(调用方调 picker.__bindHandle(real))
26
+ handle.__bindHandle = (h) => {
27
+ handle.hide = () => h.hide()
28
+ }
29
+ return handle
30
+ }
31
+
32
+ /**
33
+ * 频道选择浮层。Enter 选择并切换;Esc 取消(fallback 频道由调用方决定)。
34
+ * @returns handle { close }
35
+ */
36
+ export function openChannelPicker(tui, state, { onSelect, onCancel } = {}) {
37
+ const items = state.channels.map(c => ({
38
+ value: c.id,
39
+ label: c.name,
40
+ description: c.leadAgentId ? 'lead 已配置' : '无 lead(仅收发消息)',
41
+ }))
42
+ const picker = pickerComponent('选择要进入的频道(↑↓ 移动 · Enter 进入 · Esc 取消)', items, Math.min(10, items.length),
43
+ (item) => {
44
+ state.channelPickerOpen = false
45
+ onSelect?.(item.value)
46
+ },
47
+ () => {
48
+ state.channelPickerOpen = false
49
+ onCancel?.()
50
+ })
51
+ state.channelPickerOpen = true
52
+ const handle = tui.showOverlay(picker.component, { anchor: 'center' })
53
+ picker.__bindHandle(handle)
54
+ tui.setFocus(picker.list)
55
+ return handle
56
+ }
57
+
58
+ /**
59
+ * Agent/对话目标选择浮层(Tab 触发)。首项 = 频道(发布任务模式);
60
+ * 选具体成员 = 切换通信目标并自动打开其独立监控。
61
+ */
62
+ export function openTargetPicker(tui, state, { onPick, onCancel } = {}) {
63
+ const s = state.agents
64
+ .filter(a => a.enabled !== 0)
65
+ .map(a => ({
66
+ value: a.id,
67
+ label: `${a.name}${a.role === 'lead' ? '(lead)' : ''}`,
68
+ description: `通信对话 + 独立监控 · ${state.agentStates[a.id]?.state ?? 'idle'}`,
69
+ }))
70
+ const items = [{ value: '__channel__', label: '频道(发布任务)', description: '普通文本 = 向频道发布正式任务' }, ...s]
71
+ if (items.length === 1 && s.length === 0) {
72
+ onCancel?.()
73
+ return null
74
+ }
75
+ const picker = pickerComponent('选择对话目标(Tab 呼出 · ↑↓ 移动 · Enter 确认 · Esc 取消)', items, Math.min(10, items.length),
76
+ (item) => {
77
+ if (process.env.AW_TUI_DEBUG) console.error('[picker-debug] item=', JSON.stringify(item), 'agentIds=', JSON.stringify(s.map(x => x.value)))
78
+ state.targetPickerOpen = false
79
+ if (item.value === '__channel__') {
80
+ state.target = null
81
+ onPick?.(null)
82
+ }
83
+ else {
84
+ const a = state.agents.find(x => x.id === item.value)
85
+ if (!a) {
86
+ onCancel?.()
87
+ return
88
+ }
89
+ state.target = { agentId: a.id, name: a.name }
90
+ onPick?.(a)
91
+ }
92
+ },
93
+ () => {
94
+ state.targetPickerOpen = false
95
+ onCancel?.()
96
+ })
97
+ state.targetPickerOpen = true
98
+ const handle = tui.showOverlay(picker.component, { anchor: 'center' })
99
+ picker.__bindHandle(handle)
100
+ tui.setFocus(picker.list)
101
+ return handle
102
+ }
package/tui/lib/state.mjs CHANGED
@@ -28,6 +28,12 @@ export function createState() {
28
28
  /** 正在作答的条目(index → /hitl <n>;null = 常规输入) */
29
29
  hitlAnswering: null,
30
30
 
31
+ /** 对话目标:null = 频道(普通文本=发布任务);{agentId,name} = 成员(普通文本=通信消息) */
32
+ target: null,
33
+ /** 浮层开关(按键路由用:Tab 呼出目标选择;启动/`/channel` 呼出频道选择) */
34
+ channelPickerOpen: false,
35
+ targetPickerOpen: false,
36
+
31
37
  monitor: {
32
38
  agentId: null,
33
39
  name: null,