cueme 0.1.2 → 0.1.4

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,8 +1,30 @@
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
+ [![npm](https://img.shields.io/npm/v/cueme?label=cueme&color=0B7285)](https://www.npmjs.com/package/cueme)
14
+ [![npm downloads](https://img.shields.io/npm/dm/cueme?color=0B7285)](https://www.npmjs.com/package/cueme)
15
+
16
+ [![Repo: cue-stack](https://img.shields.io/badge/repo-cue--stack-111827)](https://github.com/nmhjklnm/cue-stack)
17
+ [![Repo: cue-console](https://img.shields.io/badge/repo-cue--console-111827)](https://github.com/nmhjklnm/cue-console)
18
+ [![Repo: cue-command](https://img.shields.io/badge/repo-cue--command-111827)](https://github.com/nmhjklnm/cue-command)
19
+ ![License](https://img.shields.io/badge/license-Apache--2.0-1E40AF)
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
 
5
- ## Quick start (2 steps)
25
+ Note: image sending is currently unavailable (WIP).
26
+
27
+ ## Quick start (3 steps)
6
28
 
7
29
  ### Step 1: Install cueme
8
30
 
@@ -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
- Add the contents of `cue-command/protocol.md` to your tool's system prompt / rules.
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 --hints "refactored login"
69
+ cueme recall <hints>
31
70
  ```
32
71
 
33
72
  ### cue
34
73
 
35
74
  ```bash
36
- cueme cue --agent_id "tavilron" --prompt "What should I do next?" --timeout 600
75
+ cueme cue <agent_id> -
37
76
  ```
38
77
 
39
78
  ### pause
40
79
 
41
80
  ```bash
42
- cueme pause --agent_id "tavilron" --prompt "Waiting..."
81
+ cueme pause <agent_id> -
43
82
  ```
44
83
 
45
84
  All commands output plain text to stdout.
46
85
 
47
- ## Release
48
-
49
- Publishing is tag-driven (GitHub Actions). Create a tag `v<version>` that matches `package.json` `version`.
86
+ ---
50
87
 
51
- ```bash
52
- git tag v0.1.1
53
- git push origin v0.1.1
54
- ```
88
+ ## QQ Group
55
89
 
56
- The workflow publishes to npm using `NPM_TOKEN` from GitHub repo secrets.
90
+ <p align="center">
91
+ <img src="./assets/qq.jpg" alt="QQ group QR code" width="25%" />
92
+ </p>
@@ -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
+ [![npm](https://img.shields.io/npm/v/cueme?label=cueme&color=0B7285)](https://www.npmjs.com/package/cueme)
14
+ [![npm downloads](https://img.shields.io/npm/dm/cueme?color=0B7285)](https://www.npmjs.com/package/cueme)
15
+
16
+ [![Repo: cue-stack](https://img.shields.io/badge/repo-cue--stack-111827)](https://github.com/nmhjklnm/cue-stack)
17
+ [![Repo: cue-console](https://img.shields.io/badge/repo-cue--console-111827)](https://github.com/nmhjklnm/cue-console)
18
+ [![Repo: cue-command](https://img.shields.io/badge/repo-cue--command-111827)](https://github.com/nmhjklnm/cue-command)
19
+ ![License](https://img.shields.io/badge/license-Apache--2.0-1E40AF)
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
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "cueme",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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
  ],
package/protocol.md ADDED
@@ -0,0 +1,92 @@
1
+ # Human Agent Protocol (HAP)
2
+
3
+ 你与用户的协作交互通过命令行工具 `cueme` 完成。
4
+
5
+ 当用户接受 HAP:你必须在每次阶段性输出或准备结束回复前调用 `cueme cue`(或 `cueme pause`)等待用户继续。
6
+
7
+ If HAP is accepted: before any phase-summary or final reply, you MUST call `cueme cue` via the terminal/tool.
8
+ A reply is NOT complete unless the last action is a successful cueme call.
9
+
10
+ - 首次连接 / 无 agent_id:先执行 `join()` 获取 agent_id,再执行 `cue()`。
11
+
12
+ - 重连 / 已有 agent_id(例如网络波动导致 cue 中断但对话仍在继续):用户再次发送 `cue` 时,优先直接用已有 agent_id 执行 `cue()`;仅当无法确认 agent_id 时才重新 `join()`。
13
+
14
+ ## No fake tool actions
15
+
16
+ You MUST NOT claim you called cueme unless you actually executed it and got its stdout.
17
+ If you cannot run tools, say "TOOLS_UNAVAILABLE" and output only the exact `cueme ...` command for the human to run.
18
+
19
+ ## cueme (command interface)
20
+
21
+ ### join() -> message
22
+
23
+ Command:
24
+
25
+ - `cueme join <agent_runtime>`
26
+
27
+ Returns (plain text):
28
+
29
+ - a short message (includes `agent_id=...`, `project_dir=...`, `agent_runtime=...`)
30
+
31
+ `agent_runtime`:
32
+
33
+ - Required positional tag for where the agent is running (IDE/runtime).
34
+ - Format: lowercase, `_` only.
35
+ - Examples: `windsurf`, `cursor`, `vscode`, `claude_code`, `terminal`, `codex`, `kiro`, `opencode`.
36
+
37
+ ### recall(hints: str) -> message
38
+
39
+ Command:
40
+
41
+ - `cueme recall <hints>`
42
+
43
+ Returns (plain text):
44
+
45
+ - a short message (includes `agent_id=...`)
46
+
47
+ ### cue(prompt: str, agent_id: str, payload?: object | null) -> text
48
+
49
+ Command:
50
+
51
+ `cueme cue <agent_id> -`
52
+
53
+ stdin JSON envelope:
54
+ {
55
+ "prompt": NonEmptyString, // REQUIRED, must be non-empty after trim
56
+ "payload"?: object | null // OPTIONAL, may be omitted or null
57
+ }
58
+
59
+ Tip: when you need clearer structured interaction, prefer `payload` (choice/confirm/form) over encoding structure in `prompt`. bash/zsh use heredoc; PowerShell use here-string and pipe the JSON into `cueme cue <agent_id> -`.
60
+
61
+ Returns:
62
+
63
+ - plain text (stdout)
64
+
65
+ Payload protocol (payload object):
66
+
67
+ - required: {"type": "choice" | "confirm" | "form"}
68
+ - choice: {"type":"choice","options":["...",...],"allow_multiple":false}
69
+ - confirm: {"type":"confirm","text":"...","confirm_label":"Confirm","cancel_label":"Cancel"}
70
+ - form: {"type":"form","fields":[{"label":"...","kind":"text","options":["...",...],"allow_multiple":false}, ...]}
71
+
72
+ Minimal examples:
73
+
74
+ - choice: {"type":"choice","options":["Continue","Stop"]}
75
+ - confirm: {"type":"confirm","text":"Continue?"}
76
+ - form: {"type":"form","fields":[{"label":"Env","options":["prod","staging"]}]}
77
+
78
+ ### pause(agent_id: str, prompt?: str) -> text
79
+
80
+ Command:
81
+
82
+ - `cueme pause <agent_id> <prompt|->`
83
+
84
+ `prompt`:
85
+
86
+ - Pass a prompt string as the positional `prompt` argument.
87
+ - If `-` is used, instructions are read from stdin.
88
+ - If stdin is empty, `pause` will use the default pause prompt.
89
+
90
+ Returns:
91
+
92
+ - plain text (stdout)
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,14 @@ async function main() {
54
56
  'cueme',
55
57
  '',
56
58
  'Usage:',
57
- ' cueme join',
58
- ' cueme recall --hints "..."',
59
- ' cueme cue --agent_id "..." --prompt "..." [--payload "{...}"] [--timeout 600]',
60
- ' cueme pause --agent_id "..." [--prompt "..."]',
59
+ ' cueme join <agent_runtime>',
60
+ ' cueme recall <hints>',
61
+ ' cueme cue <agent_id> -',
62
+ ' cueme pause <agent_id> [prompt|-]',
63
+ ' cueme migrate',
64
+ '',
65
+ 'Cue stdin JSON envelope:',
66
+ ' {"prompt":"...","payload":{...}}',
61
67
  '',
62
68
  'Output:',
63
69
  ' - join/recall/cue/pause: plain text (stdout)',
@@ -66,6 +72,114 @@ async function main() {
66
72
  return;
67
73
  }
68
74
 
75
+ if (parsed.timeout != null) {
76
+ process.stderr.write('error: --timeout is not supported (fixed to 10 minutes)\n');
77
+ process.exitCode = 2;
78
+ return;
79
+ }
80
+
81
+ if (parsed.agent_id != null || parsed.prompt != null || parsed.hints != null) {
82
+ process.stderr.write('error: --agent_id/--prompt/--hints flags are not supported; use positional args\n');
83
+ process.exitCode = 2;
84
+ return;
85
+ }
86
+
87
+ if (sub === 'join') {
88
+ const agentRuntime = pos[0];
89
+ if (!agentRuntime) {
90
+ process.stderr.write('error: missing <agent_runtime>\n');
91
+ process.exitCode = 2;
92
+ return;
93
+ }
94
+ parsed.agent_runtime = String(agentRuntime);
95
+ }
96
+
97
+ if (sub === 'recall') {
98
+ const hints = pos[0];
99
+ if (!hints) {
100
+ process.stderr.write('error: missing <hints>\n');
101
+ process.exitCode = 2;
102
+ return;
103
+ }
104
+ parsed.hints = String(hints);
105
+ }
106
+
107
+ if (sub === 'cue') {
108
+ const agentId = pos[0];
109
+ if (!agentId) {
110
+ process.stderr.write('error: missing <agent_id>\n');
111
+ process.exitCode = 2;
112
+ return;
113
+ }
114
+
115
+ if (parsed.payload != null) {
116
+ process.stderr.write('error: --payload is not supported for cue. Use stdin JSON envelope.\n');
117
+ process.exitCode = 2;
118
+ return;
119
+ }
120
+
121
+ parsed.agent_id = String(agentId);
122
+
123
+ const promptPos = pos[1];
124
+ if (promptPos !== '-') {
125
+ process.stderr.write('error: cue requires stdin JSON. Usage: cueme cue <agent_id> -\n');
126
+ process.exitCode = 2;
127
+ return;
128
+ }
129
+
130
+ if (pos.length > 2) {
131
+ process.stderr.write('error: cue only accepts <agent_id> - and stdin JSON.\n');
132
+ process.exitCode = 2;
133
+ return;
134
+ }
135
+
136
+ const raw = await readAllStdin();
137
+ let env;
138
+ try {
139
+ env = JSON.parse(raw);
140
+ } catch {
141
+ process.stderr.write('error: stdin must be valid JSON (envelope: {"prompt": "...", "payload": {...}})\n');
142
+ process.exitCode = 2;
143
+ return;
144
+ }
145
+
146
+ if (!env || typeof env !== 'object') {
147
+ process.stderr.write('error: stdin JSON must be an object\n');
148
+ process.exitCode = 2;
149
+ return;
150
+ }
151
+
152
+ const prompt = typeof env.prompt === 'string' ? env.prompt : '';
153
+ if (!prompt.trim()) {
154
+ process.stderr.write('error: stdin JSON must include non-empty string field "prompt"\n');
155
+ process.exitCode = 2;
156
+ return;
157
+ }
158
+
159
+ parsed.prompt = prompt;
160
+ parsed.payload = env.payload == null ? null : JSON.stringify(env.payload);
161
+ }
162
+
163
+ if (sub === 'pause') {
164
+ const agentId = pos[0];
165
+ if (!agentId) {
166
+ process.stderr.write('error: missing <agent_id>\n');
167
+ process.exitCode = 2;
168
+ return;
169
+ }
170
+ parsed.agent_id = String(agentId);
171
+
172
+ const promptPos = pos[1];
173
+ if (promptPos === '-') {
174
+ const stdinPrompt = await readAllStdin();
175
+ if (stdinPrompt && stdinPrompt.trim().length > 0) {
176
+ parsed.prompt = stdinPrompt;
177
+ }
178
+ } else if (promptPos != null) {
179
+ parsed.prompt = String(promptPos);
180
+ }
181
+ }
182
+
69
183
  const result = await handleCommand({ subcommand: sub, args: parsed });
70
184
  process.stdout.write(extractTextFromResult(result) + '\n');
71
185
  }
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: '', images: [] };
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: '', images: [] };
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 images = Array.isArray(userResp.images) ? userResp.images : [];
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 (images.length > 0) {
55
- contents.push({ type: 'text', text: '用户希望继续,并附加了图片:' });
82
+ } else if (files.length > 0) {
83
+ contents.push({ type: 'text', text: '用户希望继续,并附加了文件:' });
56
84
  }
57
85
 
58
- for (const img of images) {
59
- if (!img) continue;
86
+ if (fileLines.length > 0) {
60
87
  contents.push({
61
- type: 'image',
62
- data: img.base64_data,
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
- async function handleJoin(db) {
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\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. Call pause(agent_id) to suspend and wait for resume.\n\n',
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: '', images: [] },
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.images || userResp.images.length === 0)) {
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: '', images: [] });
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: '', images: [] },
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 || 'Waiting for your confirmation. Click Continue when you are ready.';
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 === 'join') return await handleJoin(db);
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();
package/src/io.js CHANGED
@@ -1,4 +1,5 @@
1
1
  function readAllStdin() {
2
+ if (process.stdin.isTTY) return Promise.resolve('');
2
3
  return new Promise((resolve, reject) => {
3
4
  let data = '';
4
5
  process.stdin.setEncoding('utf8');