cueme 0.1.2 → 0.1.3
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 +49 -13
- package/README.zh-CN.md +92 -0
- package/package.json +2 -1
- package/protocol.md +110 -0
- package/src/cli.js +78 -4
- package/src/db.js +52 -0
- package/src/handler.js +246 -20
- package/src/io.js +1 -0
package/README.md
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
# cueme
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<strong><a href="./README.md">English</a></strong>
|
|
6
|
+
·
|
|
7
|
+
<strong><a href="./README.zh-CN.md">中文</a></strong>
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/cueme)
|
|
14
|
+
[](https://www.npmjs.com/package/cueme)
|
|
15
|
+
|
|
16
|
+
[](https://github.com/nmhjklnm/cue-stack)
|
|
17
|
+
[](https://github.com/nmhjklnm/cue-console)
|
|
18
|
+
[](https://github.com/nmhjklnm/cue-command)
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
[Contributing](./CONTRIBUTING.md) · [Trademark](./TRADEMARK.md)
|
|
22
|
+
|
|
3
23
|
A command protocol adapter for Cue, compatible with the existing SQLite mailbox (`~/.cue/cue.db`).
|
|
4
24
|
|
|
25
|
+
Note: image sending is currently unavailable (WIP).
|
|
26
|
+
|
|
5
27
|
## Quick start (2 steps)
|
|
6
28
|
|
|
7
29
|
### Step 1: Install cueme
|
|
@@ -12,45 +34,59 @@ npm install -g cueme
|
|
|
12
34
|
|
|
13
35
|
### Step 2: Configure the protocol.md as your system prompt
|
|
14
36
|
|
|
15
|
-
|
|
37
|
+
Copy the contents of `protocol.md` into your runtime's system prompt / persistent rules:
|
|
38
|
+
|
|
39
|
+
- [`protocol.md`](https://github.com/nmhjklnm/cue-command/blob/main/protocol.md)
|
|
40
|
+
|
|
41
|
+
If you installed via npm, `protocol.md` is also included in the package.
|
|
16
42
|
|
|
17
43
|
This file defines the Human Agent Protocol (HAP) rules and the `cueme` command interface.
|
|
18
44
|
|
|
45
|
+
### Step 3: Run the UI and connect
|
|
46
|
+
|
|
47
|
+
`cueme` speaks to the same SQLite mailbox used by the UI (`~/.cue/cue.db`). Start the UI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g cue-console
|
|
51
|
+
cue-console start
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Open `http://localhost:3000`, then in your runtime chat type:
|
|
55
|
+
|
|
56
|
+
`cue`
|
|
57
|
+
|
|
19
58
|
## Usage
|
|
20
59
|
|
|
21
60
|
### join
|
|
22
61
|
|
|
23
62
|
```bash
|
|
24
|
-
cueme join
|
|
63
|
+
cueme join <agent_runtime>
|
|
25
64
|
```
|
|
26
65
|
|
|
27
66
|
### recall
|
|
28
67
|
|
|
29
68
|
```bash
|
|
30
|
-
cueme recall
|
|
69
|
+
cueme recall <hints>
|
|
31
70
|
```
|
|
32
71
|
|
|
33
72
|
### cue
|
|
34
73
|
|
|
35
74
|
```bash
|
|
36
|
-
cueme cue
|
|
75
|
+
cueme cue <agent_id> -
|
|
37
76
|
```
|
|
38
77
|
|
|
39
78
|
### pause
|
|
40
79
|
|
|
41
80
|
```bash
|
|
42
|
-
cueme pause
|
|
81
|
+
cueme pause <agent_id> -
|
|
43
82
|
```
|
|
44
83
|
|
|
45
84
|
All commands output plain text to stdout.
|
|
46
85
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Publishing is tag-driven (GitHub Actions). Create a tag `v<version>` that matches `package.json` `version`.
|
|
86
|
+
---
|
|
50
87
|
|
|
51
|
-
|
|
52
|
-
git tag v0.1.1
|
|
53
|
-
git push origin v0.1.1
|
|
54
|
-
```
|
|
88
|
+
## QQ Group
|
|
55
89
|
|
|
56
|
-
|
|
90
|
+
<p align="center">
|
|
91
|
+
<img src="./assets/qq.jpg" alt="QQ group QR code" width="25%" />
|
|
92
|
+
</p>
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# cueme
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<strong><a href="./README.md">English</a></strong>
|
|
6
|
+
·
|
|
7
|
+
<strong><a href="./README.zh-CN.md">中文</a></strong>
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/cueme)
|
|
14
|
+
[](https://www.npmjs.com/package/cueme)
|
|
15
|
+
|
|
16
|
+
[](https://github.com/nmhjklnm/cue-stack)
|
|
17
|
+
[](https://github.com/nmhjklnm/cue-console)
|
|
18
|
+
[](https://github.com/nmhjklnm/cue-command)
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
[Contributing](./CONTRIBUTING.md) · [Trademark](./TRADEMARK.md)
|
|
22
|
+
|
|
23
|
+
这是 Cue 的命令行协议适配器,兼容现有的 SQLite mailbox(`~/.cue/cue.db`)。
|
|
24
|
+
|
|
25
|
+
提示:发送图片功能暂不可用(开发中)。
|
|
26
|
+
|
|
27
|
+
## 快速开始(2 步)
|
|
28
|
+
|
|
29
|
+
### 第 1 步:安装 cueme
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g cueme
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 第 2 步:把 protocol.md 配置到你的系统提示词里
|
|
36
|
+
|
|
37
|
+
把 `protocol.md` 的内容复制到你正在使用的 runtime 的系统提示词 / 持久规则里:
|
|
38
|
+
|
|
39
|
+
- [`protocol.md`](https://github.com/nmhjklnm/cue-command/blob/main/protocol.md)
|
|
40
|
+
|
|
41
|
+
如果你是通过 npm 安装的,`protocol.md` 也会包含在安装包里。
|
|
42
|
+
|
|
43
|
+
该文件定义了 Human Agent Protocol(HAP)规则,以及 `cueme` 的命令接口。
|
|
44
|
+
|
|
45
|
+
### 第 3 步:启动 UI 并连接
|
|
46
|
+
|
|
47
|
+
`cueme` 会与 UI 共用同一个 SQLite mailbox(`~/.cue/cue.db`)。先启动 UI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g cue-console
|
|
51
|
+
cue-console start
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
打开 `http://localhost:3000`,然后在你的 runtime 聊天里输入:
|
|
55
|
+
|
|
56
|
+
`cue`
|
|
57
|
+
|
|
58
|
+
## 用法
|
|
59
|
+
|
|
60
|
+
### join
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cueme join <agent_runtime>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### recall
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cueme recall <hints>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### cue
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cueme cue <agent_id> -
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### pause
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cueme pause <agent_id> -
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
所有命令都以纯文本输出到 stdout。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## QQ 群
|
|
89
|
+
|
|
90
|
+
<p align="center">
|
|
91
|
+
<img src="./assets/qq.jpg" alt="QQ 群二维码" width="25%" />
|
|
92
|
+
</p>
|
package/package.json
CHANGED
package/protocol.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Human Agent Protocol (HAP)
|
|
2
|
+
|
|
3
|
+
你与用户的协作交互通过命令行工具 `cueme` 完成。
|
|
4
|
+
|
|
5
|
+
当用户接受 HAP:你必须在每次阶段性输出或准备结束回复前调用 `cueme cue`(或 `cueme pause`)等待用户继续。
|
|
6
|
+
|
|
7
|
+
## cueme (command interface)
|
|
8
|
+
|
|
9
|
+
### join() -> message
|
|
10
|
+
|
|
11
|
+
Command:
|
|
12
|
+
|
|
13
|
+
- `cueme join <agent_runtime>`
|
|
14
|
+
|
|
15
|
+
Returns (plain text):
|
|
16
|
+
|
|
17
|
+
- a short message (includes `agent_id=...`, `project_dir=...`, `agent_runtime=...`)
|
|
18
|
+
|
|
19
|
+
`agent_runtime`:
|
|
20
|
+
|
|
21
|
+
- Required positional tag for where the agent is running (IDE/runtime).
|
|
22
|
+
- Format: lowercase, `_` only.
|
|
23
|
+
- Examples: `windsurf`, `cursor`, `vscode`, `claude_code`, `terminal`, `codex`, `kiro`, `opencode`.
|
|
24
|
+
|
|
25
|
+
### recall(hints: str) -> message
|
|
26
|
+
|
|
27
|
+
Command:
|
|
28
|
+
|
|
29
|
+
- `cueme recall <hints>`
|
|
30
|
+
|
|
31
|
+
Returns (plain text):
|
|
32
|
+
|
|
33
|
+
- a short message (includes `agent_id=...`)
|
|
34
|
+
|
|
35
|
+
### cue(prompt: str, agent_id: str, payload?: str) -> text
|
|
36
|
+
|
|
37
|
+
Command:
|
|
38
|
+
|
|
39
|
+
- `cueme cue <agent_id> <prompt|-> [--payload "{...}"]`
|
|
40
|
+
|
|
41
|
+
`prompt`:
|
|
42
|
+
|
|
43
|
+
- Pass a prompt string as the positional `prompt` argument.
|
|
44
|
+
- If `-` is used, instructions are read from stdin.
|
|
45
|
+
|
|
46
|
+
Important:
|
|
47
|
+
|
|
48
|
+
- Do not pass rich prompt via positional `prompt` argument. Use stdin here-doc/here-string.
|
|
49
|
+
|
|
50
|
+
Examples (stdin):
|
|
51
|
+
|
|
52
|
+
- `bash/zsh` (here-doc)
|
|
53
|
+
|
|
54
|
+
`cueme cue <agent_id> - <<'EOF'
|
|
55
|
+
<your prompt here>
|
|
56
|
+
EOF`
|
|
57
|
+
|
|
58
|
+
- `PowerShell` (here-string)
|
|
59
|
+
|
|
60
|
+
`$prompt = @'
|
|
61
|
+
<your prompt here>
|
|
62
|
+
'@
|
|
63
|
+
$prompt | cueme cue <agent_id> -`
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
|
|
67
|
+
- plain text (stdout)
|
|
68
|
+
|
|
69
|
+
`payload`:
|
|
70
|
+
|
|
71
|
+
- Optional structured request, encoded as a JSON string.
|
|
72
|
+
- `cueme` does not validate payload; it passes it through.
|
|
73
|
+
|
|
74
|
+
Payload protocol (JSON string):
|
|
75
|
+
|
|
76
|
+
- required: {"type": "choice" | "confirm" | "form"}
|
|
77
|
+
- choice: {"type":"choice","options":["...",...],"allow_multiple":false}
|
|
78
|
+
- confirm: {"type":"confirm","text":"...","confirm_label":"Confirm","cancel_label":"Cancel"}
|
|
79
|
+
- form: {"type":"form","fields":[{"label":"...","kind":"text","options":["...",...],"allow_multiple":false}, ...]}
|
|
80
|
+
|
|
81
|
+
Minimal examples:
|
|
82
|
+
|
|
83
|
+
- choice: {"type":"choice","options":["Continue","Stop"]}
|
|
84
|
+
- confirm: {"type":"confirm","text":"Continue?"}
|
|
85
|
+
- form: {"type":"form","fields":[{"label":"Env","options":["prod","staging"]}]}
|
|
86
|
+
|
|
87
|
+
### pause(agent_id: str, prompt?: str) -> text
|
|
88
|
+
|
|
89
|
+
Command:
|
|
90
|
+
|
|
91
|
+
- `cueme pause <agent_id> <prompt|->`
|
|
92
|
+
|
|
93
|
+
`prompt`:
|
|
94
|
+
|
|
95
|
+
- Pass a prompt string as the positional `prompt` argument.
|
|
96
|
+
- If `-` is used, instructions are read from stdin.
|
|
97
|
+
- If stdin is empty, `pause` will use the default pause prompt.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
|
|
101
|
+
- plain text (stdout)
|
|
102
|
+
|
|
103
|
+
### Terminal
|
|
104
|
+
Terminal is non-interactive: MUST NOT rely on live local TTY input. Waiting for remote responses is OK (e.g., cueme cue/pause).
|
|
105
|
+
|
|
106
|
+
### Tool Use Instructions
|
|
107
|
+
IMPORTANT: When you need to perform actions like reading files, running commands,
|
|
108
|
+
or searching, you MUST use the available tools. Do NOT just describe what you would do -
|
|
109
|
+
actually call the tools to do it. Never respond with suggestions like 'check X' or
|
|
110
|
+
'look at Y' without actually using tools to perform those actions.
|
package/src/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { readAllStdin } = require('./io');
|
|
1
2
|
const { handleCommand } = require('./handler');
|
|
2
3
|
|
|
3
4
|
function parseArgs(argv) {
|
|
@@ -47,6 +48,7 @@ function extractTextFromResult(result) {
|
|
|
47
48
|
async function main() {
|
|
48
49
|
const parsed = parseArgs(process.argv);
|
|
49
50
|
const sub = parsed._[0];
|
|
51
|
+
const pos = parsed._.slice(1);
|
|
50
52
|
|
|
51
53
|
if (!sub || sub === 'help' || sub === '-h' || sub === '--help') {
|
|
52
54
|
process.stdout.write(
|
|
@@ -54,10 +56,11 @@ async function main() {
|
|
|
54
56
|
'cueme',
|
|
55
57
|
'',
|
|
56
58
|
'Usage:',
|
|
57
|
-
' cueme join',
|
|
58
|
-
' cueme recall
|
|
59
|
-
' cueme cue
|
|
60
|
-
' cueme pause
|
|
59
|
+
' cueme join <agent_runtime>',
|
|
60
|
+
' cueme recall <hints>',
|
|
61
|
+
' cueme cue <agent_id> [prompt|-] [--payload "{...}"]',
|
|
62
|
+
' cueme pause <agent_id> [prompt|-]',
|
|
63
|
+
' cueme migrate',
|
|
61
64
|
'',
|
|
62
65
|
'Output:',
|
|
63
66
|
' - join/recall/cue/pause: plain text (stdout)',
|
|
@@ -66,6 +69,77 @@ async function main() {
|
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
if (parsed.timeout != null) {
|
|
73
|
+
process.stderr.write('error: --timeout is not supported (fixed to 10 minutes)\n');
|
|
74
|
+
process.exitCode = 2;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (parsed.agent_id != null || parsed.prompt != null || parsed.hints != null) {
|
|
79
|
+
process.stderr.write('error: --agent_id/--prompt/--hints flags are not supported; use positional args\n');
|
|
80
|
+
process.exitCode = 2;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (sub === 'join') {
|
|
85
|
+
const agentRuntime = pos[0];
|
|
86
|
+
if (!agentRuntime) {
|
|
87
|
+
process.stderr.write('error: missing <agent_runtime>\n');
|
|
88
|
+
process.exitCode = 2;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
parsed.agent_runtime = String(agentRuntime);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (sub === 'recall') {
|
|
95
|
+
const hints = pos[0];
|
|
96
|
+
if (!hints) {
|
|
97
|
+
process.stderr.write('error: missing <hints>\n');
|
|
98
|
+
process.exitCode = 2;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
parsed.hints = String(hints);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (sub === 'cue') {
|
|
105
|
+
const agentId = pos[0];
|
|
106
|
+
if (!agentId) {
|
|
107
|
+
process.stderr.write('error: missing <agent_id>\n');
|
|
108
|
+
process.exitCode = 2;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
parsed.agent_id = String(agentId);
|
|
112
|
+
|
|
113
|
+
const promptPos = pos[1];
|
|
114
|
+
if (promptPos === '-') {
|
|
115
|
+
parsed.prompt = await readAllStdin();
|
|
116
|
+
} else if (promptPos != null) {
|
|
117
|
+
parsed.prompt = String(promptPos);
|
|
118
|
+
} else {
|
|
119
|
+
parsed.prompt = '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sub === 'pause') {
|
|
124
|
+
const agentId = pos[0];
|
|
125
|
+
if (!agentId) {
|
|
126
|
+
process.stderr.write('error: missing <agent_id>\n');
|
|
127
|
+
process.exitCode = 2;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
parsed.agent_id = String(agentId);
|
|
131
|
+
|
|
132
|
+
const promptPos = pos[1];
|
|
133
|
+
if (promptPos === '-') {
|
|
134
|
+
const stdinPrompt = await readAllStdin();
|
|
135
|
+
if (stdinPrompt && stdinPrompt.trim().length > 0) {
|
|
136
|
+
parsed.prompt = stdinPrompt;
|
|
137
|
+
}
|
|
138
|
+
} else if (promptPos != null) {
|
|
139
|
+
parsed.prompt = String(promptPos);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
69
143
|
const result = await handleCommand({ subcommand: sub, args: parsed });
|
|
70
144
|
process.stdout.write(extractTextFromResult(result) + '\n');
|
|
71
145
|
}
|
package/src/db.js
CHANGED
|
@@ -30,6 +30,16 @@ async function get(db, sql, params = []) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async function initSchema(db) {
|
|
33
|
+
await run(
|
|
34
|
+
db,
|
|
35
|
+
[
|
|
36
|
+
'CREATE TABLE IF NOT EXISTS schema_meta (',
|
|
37
|
+
' key TEXT PRIMARY KEY,',
|
|
38
|
+
' value TEXT NOT NULL',
|
|
39
|
+
')',
|
|
40
|
+
].join('\n')
|
|
41
|
+
);
|
|
42
|
+
|
|
33
43
|
await run(
|
|
34
44
|
db,
|
|
35
45
|
[
|
|
@@ -63,6 +73,48 @@ async function initSchema(db) {
|
|
|
63
73
|
);
|
|
64
74
|
|
|
65
75
|
await run(db, 'CREATE INDEX IF NOT EXISTS ix_cue_responses_request_id ON cue_responses(request_id)');
|
|
76
|
+
|
|
77
|
+
await run(
|
|
78
|
+
db,
|
|
79
|
+
[
|
|
80
|
+
'CREATE TABLE IF NOT EXISTS cue_files (',
|
|
81
|
+
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
|
|
82
|
+
' sha256 TEXT UNIQUE NOT NULL,',
|
|
83
|
+
' file TEXT NOT NULL,',
|
|
84
|
+
' mime_type TEXT NOT NULL,',
|
|
85
|
+
' size_bytes INTEGER NOT NULL,',
|
|
86
|
+
' created_at TEXT',
|
|
87
|
+
')',
|
|
88
|
+
].join('\n')
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await run(db, 'CREATE INDEX IF NOT EXISTS ix_cue_files_sha256 ON cue_files(sha256)');
|
|
92
|
+
|
|
93
|
+
await run(
|
|
94
|
+
db,
|
|
95
|
+
[
|
|
96
|
+
'CREATE TABLE IF NOT EXISTS cue_response_files (',
|
|
97
|
+
' response_id INTEGER NOT NULL,',
|
|
98
|
+
' file_id INTEGER NOT NULL,',
|
|
99
|
+
' idx INTEGER NOT NULL,',
|
|
100
|
+
' PRIMARY KEY (response_id, idx)',
|
|
101
|
+
')',
|
|
102
|
+
].join('\n')
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await run(db, 'CREATE INDEX IF NOT EXISTS ix_cue_response_files_response_id ON cue_response_files(response_id)');
|
|
106
|
+
await run(db, 'CREATE INDEX IF NOT EXISTS ix_cue_response_files_file_id ON cue_response_files(file_id)');
|
|
107
|
+
|
|
108
|
+
const versionRow = await get(db, 'SELECT value FROM schema_meta WHERE key = ?', ['schema_version']);
|
|
109
|
+
if (!versionRow) {
|
|
110
|
+
const reqCountRow = await get(db, 'SELECT COUNT(*) AS n FROM cue_requests');
|
|
111
|
+
const respCountRow = await get(db, 'SELECT COUNT(*) AS n FROM cue_responses');
|
|
112
|
+
const reqCount = reqCountRow ? Number(reqCountRow.n) : 0;
|
|
113
|
+
const respCount = respCountRow ? Number(respCountRow.n) : 0;
|
|
114
|
+
if (reqCount === 0 && respCount === 0) {
|
|
115
|
+
await run(db, 'INSERT INTO schema_meta (key, value) VALUES (?, ?)', ['schema_version', '2']);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
66
118
|
}
|
|
67
119
|
|
|
68
120
|
function nowIso() {
|
package/src/handler.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
2
5
|
const { sleep } = require('./io');
|
|
3
6
|
const { generateName } = require('./naming');
|
|
4
7
|
const { openDb, initSchema, run, get, nowIso, getDbPath } = require('./db');
|
|
@@ -33,34 +36,57 @@ async function waitForResponse(db, requestId, timeoutSeconds) {
|
|
|
33
36
|
function parseUserResponseJson(responseJson) {
|
|
34
37
|
try {
|
|
35
38
|
const obj = JSON.parse(responseJson);
|
|
36
|
-
if (!obj || typeof obj !== 'object') return { text: ''
|
|
39
|
+
if (!obj || typeof obj !== 'object') return { text: '' };
|
|
37
40
|
return {
|
|
38
41
|
text: typeof obj.text === 'string' ? obj.text : '',
|
|
39
|
-
images: Array.isArray(obj.images) ? obj.images : [],
|
|
40
42
|
};
|
|
41
43
|
} catch {
|
|
42
|
-
return { text: ''
|
|
44
|
+
return { text: '' };
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
async function getFilesByResponseId(db, responseId) {
|
|
49
|
+
if (!responseId) return [];
|
|
50
|
+
const rows = db
|
|
51
|
+
.prepare(
|
|
52
|
+
[
|
|
53
|
+
'SELECT f.file AS file, f.mime_type AS mime_type',
|
|
54
|
+
'FROM cue_response_files rf',
|
|
55
|
+
'JOIN cue_files f ON f.id = rf.file_id',
|
|
56
|
+
'WHERE rf.response_id = ?',
|
|
57
|
+
'ORDER BY rf.idx ASC',
|
|
58
|
+
].join('\n')
|
|
59
|
+
)
|
|
60
|
+
.all(responseId);
|
|
61
|
+
return Array.isArray(rows) ? rows : [];
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function buildToolContentsFromUserResponse(userResp) {
|
|
47
65
|
const contents = [];
|
|
48
66
|
|
|
49
67
|
const text = (userResp.text || '').trim();
|
|
50
|
-
const
|
|
68
|
+
const files = Array.isArray(userResp.files) ? userResp.files : [];
|
|
69
|
+
const fileLines = files
|
|
70
|
+
.map((f) => {
|
|
71
|
+
const file = f && typeof f === 'object' ? String(f.file || '') : '';
|
|
72
|
+
const mime = f && typeof f === 'object' ? String(f.mime_type || '') : '';
|
|
73
|
+
if (!file) return '';
|
|
74
|
+
const clean = file.replace(/^\/+/, '');
|
|
75
|
+
const pathForAgent = `~/.cue/${clean}`;
|
|
76
|
+
return `- ${pathForAgent}${mime ? ` (${mime})` : ''}`;
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean);
|
|
51
79
|
|
|
52
80
|
if (text) {
|
|
53
81
|
contents.push({ type: 'text', text: `用户希望继续,并提供了以下指令:\n\n${text}` });
|
|
54
|
-
} else if (
|
|
55
|
-
contents.push({ type: 'text', text: '
|
|
82
|
+
} else if (files.length > 0) {
|
|
83
|
+
contents.push({ type: 'text', text: '用户希望继续,并附加了文件:' });
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
if (!img) continue;
|
|
86
|
+
if (fileLines.length > 0) {
|
|
60
87
|
contents.push({
|
|
61
|
-
type: '
|
|
62
|
-
|
|
63
|
-
mimeType: img.mime_type,
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `\n\n附件文件路径如下(图片与其它文件统一为路径)。请你自行读取这些文件内容后再继续:\n${fileLines.join('\n')}`,
|
|
64
90
|
});
|
|
65
91
|
}
|
|
66
92
|
|
|
@@ -68,14 +94,23 @@ function buildToolContentsFromUserResponse(userResp) {
|
|
|
68
94
|
return contents;
|
|
69
95
|
}
|
|
70
96
|
|
|
71
|
-
|
|
97
|
+
function normalizeAgentRuntime(raw) {
|
|
98
|
+
const s = (raw == null ? '' : String(raw)).trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
99
|
+
return s ? s : 'unknown';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handleJoin(db, agent_runtime) {
|
|
72
103
|
const agent_id = generateName();
|
|
104
|
+
const project_dir = process.cwd();
|
|
105
|
+
const normalized_runtime = normalizeAgentRuntime(agent_runtime);
|
|
73
106
|
return {
|
|
74
107
|
ok: true,
|
|
75
108
|
data: {
|
|
76
109
|
agent_id,
|
|
77
110
|
message:
|
|
78
|
-
`agent_id=${agent_id}\n
|
|
111
|
+
`agent_id=${agent_id}\n` +
|
|
112
|
+
`project_dir=${project_dir}\n` +
|
|
113
|
+
`agent_runtime=${normalized_runtime}\n\n` +
|
|
79
114
|
'Use this agent_id when calling cue(prompt, agent_id).' +
|
|
80
115
|
' You must remember this agent_id: when calling cue(), pass it as agent_id so the system knows who you are.' +
|
|
81
116
|
' Before ending this session, call cue. Please use cue to provide a final summary, ask a question, or make a request.',
|
|
@@ -165,7 +200,7 @@ function timeoutContentsForMode(mode) {
|
|
|
165
200
|
{
|
|
166
201
|
type: 'text',
|
|
167
202
|
text:
|
|
168
|
-
'Timed out waiting for user response.
|
|
203
|
+
'Timed out waiting for user response. You MUST NOT continue or add any extra output. Immediately call pause(agent_id) and stop output until resumed.\n\n',
|
|
169
204
|
},
|
|
170
205
|
];
|
|
171
206
|
}
|
|
@@ -192,15 +227,16 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
192
227
|
data: {
|
|
193
228
|
request_id,
|
|
194
229
|
cancelled: true,
|
|
195
|
-
response: { text: ''
|
|
230
|
+
response: { text: '' },
|
|
196
231
|
contents: cancelledContentsForMode(mode),
|
|
197
232
|
},
|
|
198
233
|
};
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
const userResp = parseUserResponseJson(respRow.response_json);
|
|
237
|
+
userResp.files = await getFilesByResponseId(db, respRow.id);
|
|
202
238
|
|
|
203
|
-
if (!userResp.text.trim() && (!userResp.
|
|
239
|
+
if (!userResp.text.trim() && (!userResp.files || userResp.files.length === 0)) {
|
|
204
240
|
if (mode === 'cue') {
|
|
205
241
|
const updated_at = nowIso();
|
|
206
242
|
await run(
|
|
@@ -241,7 +277,7 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
241
277
|
|
|
242
278
|
const existing = await get(db, 'SELECT id FROM cue_responses WHERE request_id = ?', [request_id]);
|
|
243
279
|
if (!existing) {
|
|
244
|
-
const cancelledResponse = JSON.stringify({ text: ''
|
|
280
|
+
const cancelledResponse = JSON.stringify({ text: '' });
|
|
245
281
|
await run(
|
|
246
282
|
db,
|
|
247
283
|
'INSERT INTO cue_responses (request_id, response_json, cancelled, created_at) VALUES (?, ?, ?, ?)',
|
|
@@ -254,7 +290,7 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
254
290
|
data: {
|
|
255
291
|
request_id,
|
|
256
292
|
cancelled: true,
|
|
257
|
-
response: { text: ''
|
|
293
|
+
response: { text: '' },
|
|
258
294
|
contents: timeoutContentsForMode(mode),
|
|
259
295
|
},
|
|
260
296
|
};
|
|
@@ -265,7 +301,7 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
265
301
|
}
|
|
266
302
|
|
|
267
303
|
async function handlePause(db, { agent_id, prompt }) {
|
|
268
|
-
const pausePrompt = prompt || '
|
|
304
|
+
const pausePrompt = prompt || 'Paused. Click Continue when you are ready.';
|
|
269
305
|
const payload =
|
|
270
306
|
'{"type":"confirm","variant":"pause","text":"Paused. Click Continue when you are ready.","confirm_label":"Continue","cancel_label":""}';
|
|
271
307
|
|
|
@@ -278,12 +314,198 @@ async function handlePause(db, { agent_id, prompt }) {
|
|
|
278
314
|
});
|
|
279
315
|
}
|
|
280
316
|
|
|
317
|
+
async function ensureSchemaV2OrGuideMigrate(db) {
|
|
318
|
+
const versionRow = await get(db, 'SELECT value FROM schema_meta WHERE key = ?', ['schema_version']);
|
|
319
|
+
const version = versionRow && versionRow.value != null ? String(versionRow.value) : '';
|
|
320
|
+
if (version === '2') return { ok: true };
|
|
321
|
+
|
|
322
|
+
const reqCountRow = await get(db, 'SELECT COUNT(*) AS n FROM cue_requests');
|
|
323
|
+
const respCountRow = await get(db, 'SELECT COUNT(*) AS n FROM cue_responses');
|
|
324
|
+
const reqCount = reqCountRow ? Number(reqCountRow.n) : 0;
|
|
325
|
+
const respCount = respCountRow ? Number(respCountRow.n) : 0;
|
|
326
|
+
|
|
327
|
+
if (reqCount === 0 && respCount === 0) {
|
|
328
|
+
return { ok: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
error:
|
|
334
|
+
'Database schema is outdated (pre-file storage). Please migrate: cueme migrate\n' +
|
|
335
|
+
'数据库结构已过期(旧的 base64 存储)。请先执行:cueme migrate',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function filesRootDir() {
|
|
340
|
+
return path.join(os.homedir(), '.cue', 'files');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function extFromMime(mime) {
|
|
344
|
+
const m = (mime || '').toLowerCase().trim();
|
|
345
|
+
if (m === 'image/png') return 'png';
|
|
346
|
+
if (m === 'image/jpeg' || m === 'image/jpg') return 'jpg';
|
|
347
|
+
if (m === 'image/webp') return 'webp';
|
|
348
|
+
if (m === 'image/gif') return 'gif';
|
|
349
|
+
return 'bin';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function safeParseJson(s) {
|
|
353
|
+
try {
|
|
354
|
+
return { ok: true, value: JSON.parse(s) };
|
|
355
|
+
} catch (e) {
|
|
356
|
+
return { ok: false, error: e };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function decodeBase64(b64) {
|
|
361
|
+
try {
|
|
362
|
+
return { ok: true, value: Buffer.from(String(b64 || ''), 'base64') };
|
|
363
|
+
} catch (e) {
|
|
364
|
+
return { ok: false, error: e };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizeUserResponseForV2(parsed) {
|
|
369
|
+
const obj = parsed && typeof parsed === 'object' ? parsed : {};
|
|
370
|
+
const text = typeof obj.text === 'string' ? obj.text : '';
|
|
371
|
+
const mentions = Array.isArray(obj.mentions) ? obj.mentions : undefined;
|
|
372
|
+
return mentions ? { text, mentions } : { text };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function handleMigrate(db) {
|
|
376
|
+
const root = filesRootDir();
|
|
377
|
+
fs.mkdirSync(root, { recursive: true });
|
|
378
|
+
|
|
379
|
+
const versionRow = await get(db, 'SELECT value FROM schema_meta WHERE key = ?', ['schema_version']);
|
|
380
|
+
const version = versionRow && versionRow.value != null ? String(versionRow.value) : '';
|
|
381
|
+
if (version === '2') {
|
|
382
|
+
return { ok: true, data: { message: 'Already migrated (schema_version=2).' } };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const rows = db
|
|
386
|
+
.prepare('SELECT id, request_id, response_json, cancelled FROM cue_responses ORDER BY id ASC')
|
|
387
|
+
.all();
|
|
388
|
+
|
|
389
|
+
const total = Array.isArray(rows) ? rows.length : 0;
|
|
390
|
+
let processed = 0;
|
|
391
|
+
let migrated = 0;
|
|
392
|
+
let deleted = 0;
|
|
393
|
+
|
|
394
|
+
const deleteResponseStmt = db.prepare('DELETE FROM cue_responses WHERE id = ?');
|
|
395
|
+
const cancelRequestStmt = db.prepare('UPDATE cue_requests SET status = ? WHERE request_id = ?');
|
|
396
|
+
const upsertFileStmt = db.prepare(
|
|
397
|
+
[
|
|
398
|
+
'INSERT INTO cue_files (sha256, file, mime_type, size_bytes, created_at)',
|
|
399
|
+
'VALUES (@sha256, @file, @mime_type, @size_bytes, @created_at)',
|
|
400
|
+
'ON CONFLICT(sha256) DO UPDATE SET',
|
|
401
|
+
' file = excluded.file,',
|
|
402
|
+
' mime_type = excluded.mime_type,',
|
|
403
|
+
' size_bytes = excluded.size_bytes',
|
|
404
|
+
].join('\n')
|
|
405
|
+
);
|
|
406
|
+
const getFileIdStmt = db.prepare('SELECT id FROM cue_files WHERE sha256 = ?');
|
|
407
|
+
const deleteResponseFilesStmt = db.prepare('DELETE FROM cue_response_files WHERE response_id = ?');
|
|
408
|
+
const insertRespFileStmt = db.prepare(
|
|
409
|
+
'INSERT INTO cue_response_files (response_id, file_id, idx) VALUES (?, ?, ?)'
|
|
410
|
+
);
|
|
411
|
+
const updateResponseJsonStmt = db.prepare('UPDATE cue_responses SET response_json = ? WHERE id = ?');
|
|
412
|
+
|
|
413
|
+
const tx = db.transaction((row) => {
|
|
414
|
+
const parsed = safeParseJson(row.response_json);
|
|
415
|
+
if (!parsed.ok) {
|
|
416
|
+
deleteResponseStmt.run(row.id);
|
|
417
|
+
cancelRequestStmt.run('CANCELLED', row.request_id);
|
|
418
|
+
return { migrated: false, deleted: true };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const images = Array.isArray(parsed.value.images) ? parsed.value.images : [];
|
|
422
|
+
|
|
423
|
+
deleteResponseFilesStmt.run(row.id);
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < images.length; i += 1) {
|
|
426
|
+
const img = images[i];
|
|
427
|
+
const mime = img && typeof img === 'object' ? String(img.mime_type || '') : '';
|
|
428
|
+
const b64 = img && typeof img === 'object' ? img.base64_data : '';
|
|
429
|
+
|
|
430
|
+
const decoded = decodeBase64(b64);
|
|
431
|
+
if (!decoded.ok) {
|
|
432
|
+
deleteResponseStmt.run(row.id);
|
|
433
|
+
cancelRequestStmt.run('CANCELLED', row.request_id);
|
|
434
|
+
return { migrated: false, deleted: true };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const buf = decoded.value;
|
|
438
|
+
if (!buf || buf.length === 0) {
|
|
439
|
+
deleteResponseStmt.run(row.id);
|
|
440
|
+
cancelRequestStmt.run('CANCELLED', row.request_id);
|
|
441
|
+
return { migrated: false, deleted: true };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
|
445
|
+
const ext = extFromMime(mime);
|
|
446
|
+
const rel = path.join('files', `${sha256}.${ext}`);
|
|
447
|
+
const abs = path.join(os.homedir(), '.cue', rel);
|
|
448
|
+
|
|
449
|
+
if (!fs.existsSync(abs)) {
|
|
450
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
451
|
+
fs.writeFileSync(abs, buf);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const created_at = nowIso();
|
|
455
|
+
upsertFileStmt.run({
|
|
456
|
+
sha256,
|
|
457
|
+
file: rel,
|
|
458
|
+
mime_type: mime || 'application/octet-stream',
|
|
459
|
+
size_bytes: buf.length,
|
|
460
|
+
created_at,
|
|
461
|
+
});
|
|
462
|
+
const fileRow = getFileIdStmt.get(sha256);
|
|
463
|
+
const fileId = fileRow ? Number(fileRow.id) : null;
|
|
464
|
+
if (!fileId) {
|
|
465
|
+
deleteResponseStmt.run(row.id);
|
|
466
|
+
cancelRequestStmt.run('CANCELLED', row.request_id);
|
|
467
|
+
return { migrated: false, deleted: true };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
insertRespFileStmt.run(row.id, fileId, i);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const v2 = normalizeUserResponseForV2(parsed.value);
|
|
474
|
+
updateResponseJsonStmt.run(JSON.stringify(v2), row.id);
|
|
475
|
+
return { migrated: true, deleted: false };
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
for (const row of rows) {
|
|
479
|
+
processed += 1;
|
|
480
|
+
const res = tx(row);
|
|
481
|
+
if (res.deleted) deleted += 1;
|
|
482
|
+
if (res.migrated) migrated += 1;
|
|
483
|
+
if (processed % 50 === 0 || processed === total) {
|
|
484
|
+
process.stderr.write(`migrate: ${processed}/${total} (migrated=${migrated}, deleted=${deleted})\n`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await run(db, 'INSERT OR REPLACE INTO schema_meta (key, value) VALUES (?, ?)', ['schema_version', '2']);
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
ok: true,
|
|
492
|
+
data: {
|
|
493
|
+
message: `Migrate completed. total=${total} migrated=${migrated} deleted=${deleted}`,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
281
498
|
async function handleCommand({ subcommand, args }) {
|
|
282
499
|
const { db, dbPath } = openDb();
|
|
283
500
|
try {
|
|
284
501
|
await initSchema(db);
|
|
285
502
|
|
|
286
|
-
if (subcommand
|
|
503
|
+
if (subcommand !== 'join' && subcommand !== 'migrate') {
|
|
504
|
+
const schemaCheck = await ensureSchemaV2OrGuideMigrate(db);
|
|
505
|
+
if (!schemaCheck.ok) return { ok: false, error: schemaCheck.error, data: { db_path: dbPath } };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (subcommand === 'join') return await handleJoin(db, args.agent_runtime);
|
|
287
509
|
|
|
288
510
|
if (subcommand === 'recall') {
|
|
289
511
|
const hints = (args.hints ?? '').toString();
|
|
@@ -304,6 +526,10 @@ async function handleCommand({ subcommand, args }) {
|
|
|
304
526
|
return await handlePause(db, { agent_id, prompt });
|
|
305
527
|
}
|
|
306
528
|
|
|
529
|
+
if (subcommand === 'migrate') {
|
|
530
|
+
return await handleMigrate(db);
|
|
531
|
+
}
|
|
532
|
+
|
|
307
533
|
return { ok: false, error: `unknown subcommand: ${subcommand}`, data: { db_path: dbPath } };
|
|
308
534
|
} finally {
|
|
309
535
|
db.close();
|