cueme 0.1.1 → 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 +59 -21
- package/README.zh-CN.md +92 -0
- package/bin/cue-command.js +0 -0
- package/package.json +3 -2
- package/protocol.md +110 -0
- package/src/cli.js +99 -14
- package/src/db.js +76 -17
- package/src/handler.js +245 -53
- package/src/io.js +1 -0
package/README.md
CHANGED
|
@@ -1,54 +1,92 @@
|
|
|
1
1
|
# cueme
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
A command protocol adapter for Cue, compatible with the existing SQLite mailbox (`~/.cue/cue.db`).
|
|
24
|
+
|
|
25
|
+
Note: image sending is currently unavailable (WIP).
|
|
26
|
+
|
|
27
|
+
## Quick start (2 steps)
|
|
28
|
+
|
|
29
|
+
### Step 1: Install cueme
|
|
6
30
|
|
|
7
31
|
```bash
|
|
8
32
|
npm install -g cueme
|
|
9
33
|
```
|
|
10
34
|
|
|
35
|
+
### Step 2: Configure the protocol.md as your system prompt
|
|
36
|
+
|
|
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.
|
|
42
|
+
|
|
43
|
+
This file defines the Human Agent Protocol (HAP) rules and the `cueme` command interface.
|
|
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
|
+
|
|
11
58
|
## Usage
|
|
12
59
|
|
|
13
60
|
### join
|
|
14
61
|
|
|
15
62
|
```bash
|
|
16
|
-
cueme join
|
|
63
|
+
cueme join <agent_runtime>
|
|
17
64
|
```
|
|
18
65
|
|
|
19
66
|
### recall
|
|
20
67
|
|
|
21
68
|
```bash
|
|
22
|
-
cueme recall
|
|
69
|
+
cueme recall <hints>
|
|
23
70
|
```
|
|
24
71
|
|
|
25
72
|
### cue
|
|
26
73
|
|
|
27
74
|
```bash
|
|
28
|
-
cueme cue
|
|
75
|
+
cueme cue <agent_id> -
|
|
29
76
|
```
|
|
30
77
|
|
|
31
78
|
### pause
|
|
32
79
|
|
|
33
80
|
```bash
|
|
34
|
-
cueme pause
|
|
81
|
+
cueme pause <agent_id> -
|
|
35
82
|
```
|
|
36
83
|
|
|
37
|
-
|
|
84
|
+
All commands output plain text to stdout.
|
|
38
85
|
|
|
39
|
-
|
|
40
|
-
echo '{"cmd":"join"}' | cueme rpc
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
All commands output one JSON object to stdout.
|
|
44
|
-
|
|
45
|
-
## Release
|
|
86
|
+
---
|
|
46
87
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
git tag v0.1.1
|
|
51
|
-
git push origin v0.1.1
|
|
52
|
-
```
|
|
88
|
+
## QQ Group
|
|
53
89
|
|
|
54
|
-
|
|
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/bin/cue-command.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cueme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Cue command protocol adapter (stdin/stdout JSON)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin/",
|
|
8
8
|
"src/",
|
|
9
|
+
"protocol.md",
|
|
9
10
|
"README.md",
|
|
10
11
|
"LICENSE"
|
|
11
12
|
],
|
|
@@ -20,6 +21,6 @@
|
|
|
20
21
|
"prepare": "node -c src/cli.js && node -c src/handler.js"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"sqlite3": "^
|
|
24
|
+
"better-sqlite3": "^11.7.0"
|
|
24
25
|
}
|
|
25
26
|
}
|
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
|
@@ -25,38 +25,123 @@ function parseArgs(argv) {
|
|
|
25
25
|
return out;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function extractTextFromResult(result) {
|
|
29
|
+
if (!result || typeof result !== 'object') return '';
|
|
30
|
+
if (result.ok === false) return result.error ? String(result.error) : '';
|
|
31
|
+
|
|
32
|
+
const data = result.data;
|
|
33
|
+
if (!data || typeof data !== 'object') return '';
|
|
34
|
+
|
|
35
|
+
const contents = Array.isArray(data.contents) ? data.contents : [];
|
|
36
|
+
const textParts = [];
|
|
37
|
+
for (const c of contents) {
|
|
38
|
+
if (c && c.type === 'text' && typeof c.text === 'string' && c.text.length > 0) {
|
|
39
|
+
textParts.push(c.text);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (textParts.length > 0) return textParts.join('');
|
|
43
|
+
|
|
44
|
+
if (typeof data.message === 'string') return data.message;
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
async function main() {
|
|
29
49
|
const parsed = parseArgs(process.argv);
|
|
30
50
|
const sub = parsed._[0];
|
|
51
|
+
const pos = parsed._.slice(1);
|
|
31
52
|
|
|
32
53
|
if (!sub || sub === 'help' || sub === '-h' || sub === '--help') {
|
|
33
54
|
process.stdout.write(
|
|
34
55
|
[
|
|
35
|
-
'cueme
|
|
56
|
+
'cueme',
|
|
36
57
|
'',
|
|
37
58
|
'Usage:',
|
|
38
|
-
' cueme join',
|
|
39
|
-
' cueme recall
|
|
40
|
-
' cueme cue
|
|
41
|
-
' cueme pause
|
|
42
|
-
' cueme
|
|
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',
|
|
43
64
|
'',
|
|
44
|
-
'
|
|
65
|
+
'Output:',
|
|
66
|
+
' - join/recall/cue/pause: plain text (stdout)',
|
|
45
67
|
].join('\n') + '\n'
|
|
46
68
|
);
|
|
47
69
|
return;
|
|
48
70
|
}
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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);
|
|
55
140
|
}
|
|
56
141
|
}
|
|
57
142
|
|
|
58
|
-
const result = await handleCommand({ subcommand: sub, args: parsed
|
|
59
|
-
process.stdout.write(
|
|
143
|
+
const result = await handleCommand({ subcommand: sub, args: parsed });
|
|
144
|
+
process.stdout.write(extractTextFromResult(result) + '\n');
|
|
60
145
|
}
|
|
61
146
|
|
|
62
147
|
module.exports = { main };
|
package/src/db.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const
|
|
4
|
+
const Database = require('better-sqlite3');
|
|
5
5
|
|
|
6
6
|
function getDbPath() {
|
|
7
7
|
return path.join(os.homedir(), '.cue', 'cue.db');
|
|
@@ -15,29 +15,31 @@ function ensureDbDir() {
|
|
|
15
15
|
|
|
16
16
|
function openDb() {
|
|
17
17
|
const dbPath = ensureDbDir();
|
|
18
|
-
const db = new
|
|
18
|
+
const db = new Database(dbPath);
|
|
19
19
|
return { db, dbPath };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function run(db, sql, params = []) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (err) return reject(err);
|
|
26
|
-
resolve({ lastID: this.lastID, changes: this.changes });
|
|
27
|
-
});
|
|
28
|
-
});
|
|
22
|
+
async function run(db, sql, params = []) {
|
|
23
|
+
const info = db.prepare(sql).run(params);
|
|
24
|
+
return { lastID: info.lastInsertRowid, changes: info.changes };
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
function get(db, sql, params = []) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (err) return reject(err);
|
|
35
|
-
resolve(row || null);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
27
|
+
async function get(db, sql, params = []) {
|
|
28
|
+
const row = db.prepare(sql).get(params);
|
|
29
|
+
return row || null;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
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
|
+
|
|
41
43
|
await run(
|
|
42
44
|
db,
|
|
43
45
|
[
|
|
@@ -71,10 +73,67 @@ async function initSchema(db) {
|
|
|
71
73
|
);
|
|
72
74
|
|
|
73
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
|
+
}
|
|
74
118
|
}
|
|
75
119
|
|
|
76
120
|
function nowIso() {
|
|
77
|
-
|
|
121
|
+
const now = new Date();
|
|
122
|
+
const offset = -now.getTimezoneOffset();
|
|
123
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
124
|
+
const pad = (n) => String(Math.abs(n)).padStart(2, '0');
|
|
125
|
+
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
126
|
+
const offsetMinutes = pad(Math.abs(offset) % 60);
|
|
127
|
+
|
|
128
|
+
const year = now.getFullYear();
|
|
129
|
+
const month = pad(now.getMonth() + 1);
|
|
130
|
+
const day = pad(now.getDate());
|
|
131
|
+
const hours = pad(now.getHours());
|
|
132
|
+
const minutes = pad(now.getMinutes());
|
|
133
|
+
const seconds = pad(now.getSeconds());
|
|
134
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
135
|
+
|
|
136
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${sign}${offsetHours}:${offsetMinutes}`;
|
|
78
137
|
}
|
|
79
138
|
|
|
80
139
|
module.exports = { openDb, initSchema, run, get, nowIso, getDbPath };
|
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.',
|
|
@@ -86,7 +121,7 @@ async function handleJoin(db) {
|
|
|
86
121
|
async function handleRecall(db, hints) {
|
|
87
122
|
const row = await get(
|
|
88
123
|
db,
|
|
89
|
-
|
|
124
|
+
"SELECT agent_id FROM cue_requests WHERE agent_id != '' AND prompt LIKE ? ORDER BY created_at DESC LIMIT 1",
|
|
90
125
|
[`%${hints}%`]
|
|
91
126
|
);
|
|
92
127
|
|
|
@@ -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(
|
|
@@ -220,15 +256,6 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
220
256
|
};
|
|
221
257
|
}
|
|
222
258
|
|
|
223
|
-
if (mode === 'cue') {
|
|
224
|
-
const updated_at = nowIso();
|
|
225
|
-
await run(
|
|
226
|
-
db,
|
|
227
|
-
'UPDATE cue_requests SET status = ?, updated_at = ? WHERE request_id = ?',
|
|
228
|
-
['COMPLETED', updated_at, request_id]
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
259
|
return {
|
|
233
260
|
ok: true,
|
|
234
261
|
data: {
|
|
@@ -250,7 +277,7 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
250
277
|
|
|
251
278
|
const existing = await get(db, 'SELECT id FROM cue_responses WHERE request_id = ?', [request_id]);
|
|
252
279
|
if (!existing) {
|
|
253
|
-
const cancelledResponse = JSON.stringify({ text: ''
|
|
280
|
+
const cancelledResponse = JSON.stringify({ text: '' });
|
|
254
281
|
await run(
|
|
255
282
|
db,
|
|
256
283
|
'INSERT INTO cue_responses (request_id, response_json, cancelled, created_at) VALUES (?, ?, ?, ?)',
|
|
@@ -263,7 +290,7 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
263
290
|
data: {
|
|
264
291
|
request_id,
|
|
265
292
|
cancelled: true,
|
|
266
|
-
response: { text: ''
|
|
293
|
+
response: { text: '' },
|
|
267
294
|
contents: timeoutContentsForMode(mode),
|
|
268
295
|
},
|
|
269
296
|
};
|
|
@@ -274,7 +301,7 @@ async function handleCueLike(db, { mode, agent_id, prompt, payload, timeoutSecon
|
|
|
274
301
|
}
|
|
275
302
|
|
|
276
303
|
async function handlePause(db, { agent_id, prompt }) {
|
|
277
|
-
const pausePrompt = prompt || '
|
|
304
|
+
const pausePrompt = prompt || 'Paused. Click Continue when you are ready.';
|
|
278
305
|
const payload =
|
|
279
306
|
'{"type":"confirm","variant":"pause","text":"Paused. Click Continue when you are ready.","confirm_label":"Continue","cancel_label":""}';
|
|
280
307
|
|
|
@@ -287,37 +314,198 @@ async function handlePause(db, { agent_id, prompt }) {
|
|
|
287
314
|
});
|
|
288
315
|
}
|
|
289
316
|
|
|
290
|
-
async function
|
|
291
|
-
const
|
|
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) {
|
|
292
353
|
try {
|
|
293
|
-
|
|
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
|
+
}
|
|
294
384
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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 };
|
|
298
435
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
agent_id: String(input.agent_id || ''),
|
|
306
|
-
prompt: String(input.prompt || ''),
|
|
307
|
-
payload: input.payload == null ? null : String(input.payload),
|
|
308
|
-
timeoutSeconds: input.timeout == null ? 600 : Number(input.timeout),
|
|
309
|
-
});
|
|
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 };
|
|
310
442
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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 };
|
|
316
468
|
}
|
|
317
|
-
|
|
469
|
+
|
|
470
|
+
insertRespFileStmt.run(row.id, fileId, i);
|
|
318
471
|
}
|
|
319
472
|
|
|
320
|
-
|
|
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
|
+
|
|
498
|
+
async function handleCommand({ subcommand, args }) {
|
|
499
|
+
const { db, dbPath } = openDb();
|
|
500
|
+
try {
|
|
501
|
+
await initSchema(db);
|
|
502
|
+
|
|
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);
|
|
321
509
|
|
|
322
510
|
if (subcommand === 'recall') {
|
|
323
511
|
const hints = (args.hints ?? '').toString();
|
|
@@ -338,6 +526,10 @@ async function handleCommand({ subcommand, args, input }) {
|
|
|
338
526
|
return await handlePause(db, { agent_id, prompt });
|
|
339
527
|
}
|
|
340
528
|
|
|
529
|
+
if (subcommand === 'migrate') {
|
|
530
|
+
return await handleMigrate(db);
|
|
531
|
+
}
|
|
532
|
+
|
|
341
533
|
return { ok: false, error: `unknown subcommand: ${subcommand}`, data: { db_path: dbPath } };
|
|
342
534
|
} finally {
|
|
343
535
|
db.close();
|