cueme 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/docs/proto.md +110 -0
- package/package.json +2 -1
- package/protocol.md +36 -43
- package/src/cli.js +147 -11
- package/src/envelope.js +92 -0
- package/src/handler.js +40 -193
- package/src/io.js +61 -4
- package/src/proto.js +384 -0
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ A command protocol adapter for Cue, compatible with the existing SQLite mailbox
|
|
|
24
24
|
|
|
25
25
|
Note: image sending is currently unavailable (WIP).
|
|
26
26
|
|
|
27
|
-
## Quick start (
|
|
27
|
+
## Quick start (3 steps)
|
|
28
28
|
|
|
29
29
|
### Step 1: Install cueme
|
|
30
30
|
|
|
@@ -81,6 +81,24 @@ cueme cue <agent_id> -
|
|
|
81
81
|
cueme pause <agent_id> -
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
You can also provide the prompt as a positional argument:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cueme pause <agent_id> "Paused. Click Continue when you are ready."
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### proto
|
|
91
|
+
|
|
92
|
+
Example (macOS + Windsurf):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cueme proto apply windsurf
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For detailed `proto` usage (config, init, render, markers), see:
|
|
99
|
+
|
|
100
|
+
- [`docs/proto.md`](./docs/proto.md)
|
|
101
|
+
|
|
84
102
|
All commands output plain text to stdout.
|
|
85
103
|
|
|
86
104
|
---
|
package/docs/proto.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# cueme proto
|
|
2
|
+
|
|
3
|
+
This document describes the `cueme proto` command family.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`cueme proto` injects the shared `protocol.md` into a specific agent file by composing:
|
|
8
|
+
|
|
9
|
+
`final_proto = prefix(agent) + "\n\n" + protocol.md`
|
|
10
|
+
|
|
11
|
+
The injected content is managed between sentinel markers and may be overwritten by `cueme proto apply`.
|
|
12
|
+
|
|
13
|
+
## Config
|
|
14
|
+
|
|
15
|
+
Config file path:
|
|
16
|
+
|
|
17
|
+
`~/.cue/cueme.json`
|
|
18
|
+
|
|
19
|
+
Required keys:
|
|
20
|
+
|
|
21
|
+
- `cueme.proto.path`: map of injection target paths by `<platform>.<agent>`
|
|
22
|
+
- `platform`: `linux` | `macos` | `windows`
|
|
23
|
+
- supports `~` and `%ENV%` expansions (e.g. `%APPDATA%`, `%USERPROFILE%`)
|
|
24
|
+
- `cueme.proto.prefix`: map of prefix by `<agent>`
|
|
25
|
+
- can be a string or string array (joined with `\n`)
|
|
26
|
+
- `cueme.proto.protocol_path`: absolute or relative path to the shared `protocol.md`
|
|
27
|
+
- supports `~` and `%ENV%` expansions
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"cueme.proto.path": {
|
|
34
|
+
"macos.vscode": "~/Library/Application Support/Code/User/prompts/human_proto.md",
|
|
35
|
+
"macos.windsurf": "~/.codeium/windsurf/memories/global_rules.md",
|
|
36
|
+
|
|
37
|
+
"windows.vscode": "%APPDATA%\\Code\\User\\prompts\\human_proto.md",
|
|
38
|
+
"windows.windsurf": "%USERPROFILE%\\.codeium\\windsurf\\memories\\global_rules.md",
|
|
39
|
+
|
|
40
|
+
"linux.vscode": "~/.config/Code/User/prompts/human_proto.md",
|
|
41
|
+
"linux.windsurf": "~/.codeium/windsurf/memories/global_rules.md"
|
|
42
|
+
},
|
|
43
|
+
"cueme.proto.prefix": {
|
|
44
|
+
"vscode": [
|
|
45
|
+
"---",
|
|
46
|
+
"applyTo: '**'",
|
|
47
|
+
"---"
|
|
48
|
+
],
|
|
49
|
+
"windsurf": []
|
|
50
|
+
},
|
|
51
|
+
"cueme.proto.protocol_path": "~/path/to/protocol.md"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Sentinel markers
|
|
56
|
+
|
|
57
|
+
Injected content is managed between these markers:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
<!-- HUMAN_AGENT_PROTO_BEGIN -->
|
|
61
|
+
... managed content ...
|
|
62
|
+
<!-- HUMAN_AGENT_PROTO_END -->
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
|
|
67
|
+
- Markers are written in the standardized `HUMAN_*` form.
|
|
68
|
+
- Existing files that still use `HUAMN_*` markers are also recognized for replacement.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
### Render (print to stdout)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cueme proto <agent>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Generates and prints `final_proto` to stdout.
|
|
79
|
+
|
|
80
|
+
### Apply (inject into agent file)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cueme proto apply <agent>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Behavior:
|
|
87
|
+
|
|
88
|
+
- Resolves the target path using `cueme.proto.path["<platform>.<agent>"]`.
|
|
89
|
+
- Writes/updates the managed block in the target file.
|
|
90
|
+
- Preserves the target file's existing EOL style when updating.
|
|
91
|
+
|
|
92
|
+
### Init (create config)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cueme proto init
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Creates `~/.cue/cueme.json` if missing (never overwrites).
|
|
99
|
+
|
|
100
|
+
Auto-detect (current platform only):
|
|
101
|
+
|
|
102
|
+
- `vscode`: `.vscode/prompts/human_proto.md` (workspace) then platform user path
|
|
103
|
+
- `windsurf`: `.codeium/windsurf/memories/global_rules.md` (workspace) then platform user path
|
|
104
|
+
|
|
105
|
+
### Helpers
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cueme proto ls
|
|
109
|
+
cueme proto path <agent>
|
|
110
|
+
```
|
package/package.json
CHANGED
package/protocol.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
当用户接受 HAP:你必须在每次阶段性输出或准备结束回复前调用 `cueme cue`(或 `cueme pause`)等待用户继续。
|
|
6
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
|
+
**IMPORTANT: cueme 命令不可放在后台运行。** All cueme commands must run in blocking/foreground mode to ensure proper interaction flow.
|
|
20
|
+
|
|
7
21
|
## cueme (command interface)
|
|
8
22
|
|
|
9
23
|
### join() -> message
|
|
@@ -14,7 +28,7 @@ Command:
|
|
|
14
28
|
|
|
15
29
|
Returns (plain text):
|
|
16
30
|
|
|
17
|
-
- a short message (includes `agent_id=...`, `project_dir=...`, `agent_runtime=...`)
|
|
31
|
+
- a short message (includes `agent_id=...`, `project_dir=...`, `agent_terminal=...`, `agent_runtime=...`)
|
|
18
32
|
|
|
19
33
|
`agent_runtime`:
|
|
20
34
|
|
|
@@ -32,46 +46,36 @@ Returns (plain text):
|
|
|
32
46
|
|
|
33
47
|
- a short message (includes `agent_id=...`)
|
|
34
48
|
|
|
35
|
-
### cue(prompt: str, agent_id: str, payload?:
|
|
49
|
+
### cue(prompt: str, agent_id: str, payload?: object | null) -> text
|
|
36
50
|
|
|
37
51
|
Command:
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`prompt`:
|
|
42
|
-
|
|
43
|
-
- Pass a prompt string as the positional `prompt` argument.
|
|
44
|
-
- If `-` is used, instructions are read from stdin.
|
|
53
|
+
`cueme cue <agent_id> -`
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
stdin envelope (tag blocks; tags must be alone on their line):
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
<cueme_prompt>
|
|
58
|
+
...raw prompt text...
|
|
59
|
+
</cueme_prompt>
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
<cueme_payload>
|
|
62
|
+
...JSON object or null...
|
|
63
|
+
</cueme_payload>
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
Rules:
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
- `<cueme_prompt>` block required; content must be non-empty after trim.
|
|
68
|
+
- `<cueme_payload>` block optional; if present: JSON object or null. Blank content is treated as null.
|
|
69
|
+
- Only whitespace allowed outside these blocks.
|
|
70
|
+
- Legacy JSON envelope is not supported.
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`$prompt = @'
|
|
61
|
-
<your prompt here>
|
|
62
|
-
'@
|
|
63
|
-
$prompt | cueme cue <agent_id> -`
|
|
72
|
+
Tip: when you need clearer structured interaction, prefer `payload` (choice/confirm/form) over encoding structure in `prompt`.
|
|
64
73
|
|
|
65
74
|
Returns:
|
|
66
75
|
|
|
67
76
|
- plain text (stdout)
|
|
68
77
|
|
|
69
|
-
|
|
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):
|
|
78
|
+
Payload protocol (payload object):
|
|
75
79
|
|
|
76
80
|
- required: {"type": "choice" | "confirm" | "form"}
|
|
77
81
|
- choice: {"type":"choice","options":["...",...],"allow_multiple":false}
|
|
@@ -87,24 +91,13 @@ Minimal examples:
|
|
|
87
91
|
### pause(agent_id: str, prompt?: str) -> text
|
|
88
92
|
|
|
89
93
|
Command:
|
|
94
|
+
- `cueme pause <agent_id> [<prompt> | -]`
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`prompt`:
|
|
96
|
+
stdin envelope (when `-` is used): tag blocks (prompt only; tags must be alone on their line):
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
<cueme_prompt>
|
|
99
|
+
...raw prompt text...
|
|
100
|
+
</cueme_prompt>
|
|
98
101
|
|
|
99
102
|
Returns:
|
|
100
|
-
|
|
101
103
|
- 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,5 +1,25 @@
|
|
|
1
1
|
const { readAllStdin } = require('./io');
|
|
2
2
|
const { handleCommand } = require('./handler');
|
|
3
|
+
const { parseTagBlocksEnvelope } = require('./envelope');
|
|
4
|
+
const { protoApply, protoInit, protoLs, protoPath, protoRender } = require('./proto');
|
|
5
|
+
const pkg = require('../package.json');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
async function parseStdinTagBlocksOrExit({ parsed, allow_payload }) {
|
|
10
|
+
const raw = await readAllStdin();
|
|
11
|
+
const env = parseTagBlocksEnvelope(raw, { allow_payload });
|
|
12
|
+
if (!env.ok) {
|
|
13
|
+
process.stderr.write(env.error);
|
|
14
|
+
process.exitCode = 2;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
parsed.prompt = env.prompt;
|
|
18
|
+
if (allow_payload !== false) {
|
|
19
|
+
parsed.payload = env.payload == null ? null : JSON.stringify(env.payload);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
3
23
|
|
|
4
24
|
function parseArgs(argv) {
|
|
5
25
|
const args = argv.slice(2);
|
|
@@ -46,6 +66,25 @@ function extractTextFromResult(result) {
|
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
async function main() {
|
|
69
|
+
const first = process.argv[2];
|
|
70
|
+
if (first === '-v' || first === '--version') {
|
|
71
|
+
process.stdout.write(String(pkg.version || '') + '\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (first === '-p' || first === '--protocol') {
|
|
76
|
+
const protocolPath = path.join(__dirname, '..', 'protocol.md');
|
|
77
|
+
try {
|
|
78
|
+
const content = fs.readFileSync(protocolPath, 'utf8');
|
|
79
|
+
process.stdout.write(String(content || ''));
|
|
80
|
+
if (!String(content || '').endsWith('\n')) process.stdout.write('\n');
|
|
81
|
+
} catch {
|
|
82
|
+
process.stderr.write('error: failed to read protocol.md\n');
|
|
83
|
+
process.exitCode = 2;
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
49
88
|
const parsed = parseArgs(process.argv);
|
|
50
89
|
const sub = parsed._[0];
|
|
51
90
|
const pos = parsed._.slice(1);
|
|
@@ -56,11 +95,30 @@ async function main() {
|
|
|
56
95
|
'cueme',
|
|
57
96
|
'',
|
|
58
97
|
'Usage:',
|
|
98
|
+
' cueme -v|--version',
|
|
99
|
+
' cueme -p|--protocol',
|
|
100
|
+
' cueme proto <agent>',
|
|
101
|
+
' cueme proto apply <agent>',
|
|
102
|
+
' cueme proto init',
|
|
103
|
+
' cueme proto ls',
|
|
104
|
+
' cueme proto path <agent>',
|
|
59
105
|
' cueme join <agent_runtime>',
|
|
60
106
|
' cueme recall <hints>',
|
|
61
|
-
' cueme cue <agent_id>
|
|
107
|
+
' cueme cue <agent_id> -',
|
|
62
108
|
' cueme pause <agent_id> [prompt|-]',
|
|
63
|
-
'
|
|
109
|
+
'',
|
|
110
|
+
'Cue stdin envelope (tag blocks; tags must be alone on their line):',
|
|
111
|
+
' <cueme_prompt>',
|
|
112
|
+
' ...raw prompt text...',
|
|
113
|
+
' </cueme_prompt>',
|
|
114
|
+
' <cueme_payload>',
|
|
115
|
+
' ...JSON object or null...',
|
|
116
|
+
' </cueme_payload>',
|
|
117
|
+
'',
|
|
118
|
+
'Pause stdin envelope (tag blocks; tags must be alone on their line):',
|
|
119
|
+
' <cueme_prompt>',
|
|
120
|
+
' ...raw prompt text...',
|
|
121
|
+
' </cueme_prompt>',
|
|
64
122
|
'',
|
|
65
123
|
'Output:',
|
|
66
124
|
' - join/recall/cue/pause: plain text (stdout)',
|
|
@@ -101,6 +159,59 @@ async function main() {
|
|
|
101
159
|
parsed.hints = String(hints);
|
|
102
160
|
}
|
|
103
161
|
|
|
162
|
+
if (sub === 'proto') {
|
|
163
|
+
const action = pos[0];
|
|
164
|
+
try {
|
|
165
|
+
if (!action) {
|
|
166
|
+
process.stderr.write('error: missing <agent>\n');
|
|
167
|
+
process.exitCode = 2;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (action === 'ls') {
|
|
172
|
+
process.stdout.write(protoLs());
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (action === 'init') {
|
|
177
|
+
process.stdout.write(protoInit() + '\n');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (action === 'apply') {
|
|
182
|
+
const agent = pos[1];
|
|
183
|
+
if (!agent) {
|
|
184
|
+
process.stderr.write('error: missing <agent>\n');
|
|
185
|
+
process.exitCode = 2;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
process.stdout.write(protoApply(String(agent)) + '\n');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (action === 'path') {
|
|
193
|
+
const agent = pos[1];
|
|
194
|
+
if (!agent) {
|
|
195
|
+
process.stderr.write('error: missing <agent>\n');
|
|
196
|
+
process.exitCode = 2;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
process.stdout.write(protoPath(String(agent)) + '\n');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const agent = action;
|
|
204
|
+
const rendered = protoRender(String(agent));
|
|
205
|
+
process.stdout.write(rendered);
|
|
206
|
+
if (!String(rendered || '').endsWith('\n')) process.stdout.write('\n');
|
|
207
|
+
return;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
process.stderr.write((err && err.message ? err.message : 'error: proto failed') + '\n');
|
|
210
|
+
process.exitCode = 2;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
104
215
|
if (sub === 'cue') {
|
|
105
216
|
const agentId = pos[0];
|
|
106
217
|
if (!agentId) {
|
|
@@ -108,16 +219,30 @@ async function main() {
|
|
|
108
219
|
process.exitCode = 2;
|
|
109
220
|
return;
|
|
110
221
|
}
|
|
222
|
+
|
|
223
|
+
if (parsed.payload != null) {
|
|
224
|
+
process.stderr.write('error: --payload is not supported for cue. Use stdin tag-blocks envelope.\n');
|
|
225
|
+
process.exitCode = 2;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
111
229
|
parsed.agent_id = String(agentId);
|
|
112
230
|
|
|
113
231
|
const promptPos = pos[1];
|
|
114
|
-
if (promptPos
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} else {
|
|
119
|
-
parsed.prompt = '';
|
|
232
|
+
if (promptPos !== '-') {
|
|
233
|
+
process.stderr.write('error: cue requires stdin tag-blocks. Usage: cueme cue <agent_id> -\n');
|
|
234
|
+
process.exitCode = 2;
|
|
235
|
+
return;
|
|
120
236
|
}
|
|
237
|
+
|
|
238
|
+
if (pos.length > 2) {
|
|
239
|
+
process.stderr.write('error: cue only accepts <agent_id> - and stdin tag-blocks envelope.\n');
|
|
240
|
+
process.exitCode = 2;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const ok = await parseStdinTagBlocksOrExit({ parsed, allow_payload: true });
|
|
245
|
+
if (!ok) return;
|
|
121
246
|
}
|
|
122
247
|
|
|
123
248
|
if (sub === 'pause') {
|
|
@@ -127,14 +252,25 @@ async function main() {
|
|
|
127
252
|
process.exitCode = 2;
|
|
128
253
|
return;
|
|
129
254
|
}
|
|
255
|
+
|
|
256
|
+
if (parsed.payload != null) {
|
|
257
|
+
process.stderr.write('error: --payload is not supported for pause. Use tag-blocks stdin or positional prompt.\n');
|
|
258
|
+
process.exitCode = 2;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
130
262
|
parsed.agent_id = String(agentId);
|
|
131
263
|
|
|
132
264
|
const promptPos = pos[1];
|
|
133
265
|
if (promptPos === '-') {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
266
|
+
if (pos.length > 2) {
|
|
267
|
+
process.stderr.write('error: pause only accepts <agent_id> [prompt|-] (tag-blocks stdin when "-" is used).\n');
|
|
268
|
+
process.exitCode = 2;
|
|
269
|
+
return;
|
|
137
270
|
}
|
|
271
|
+
|
|
272
|
+
const ok = await parseStdinTagBlocksOrExit({ parsed, allow_payload: false });
|
|
273
|
+
if (!ok) return;
|
|
138
274
|
} else if (promptPos != null) {
|
|
139
275
|
parsed.prompt = String(promptPos);
|
|
140
276
|
}
|
package/src/envelope.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function parseTagBlocksEnvelope(raw, opts = {}) {
|
|
2
|
+
const allowPayload = opts.allow_payload !== false;
|
|
3
|
+
const text = String(raw == null ? '' : raw);
|
|
4
|
+
const trimmed = text.trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return { ok: false, error: 'error: stdin must use tag-blocks envelope\n' };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (trimmed.startsWith('{')) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
error:
|
|
13
|
+
'error: legacy JSON envelope is not supported. Use <cueme_prompt>...</cueme_prompt> and optional <cueme_payload>...</cueme_payload>\n',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const promptOpen = '<cueme_prompt>';
|
|
18
|
+
const promptClose = '</cueme_prompt>';
|
|
19
|
+
const payloadOpen = '<cueme_payload>';
|
|
20
|
+
const payloadClose = '</cueme_payload>';
|
|
21
|
+
|
|
22
|
+
function findTagLineIndex(lines, tag) {
|
|
23
|
+
return lines.findIndex((l) => l.trim() === tag);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
27
|
+
const lines = normalized.split('\n');
|
|
28
|
+
|
|
29
|
+
const pOpenIdx = findTagLineIndex(lines, promptOpen);
|
|
30
|
+
const pCloseIdx = findTagLineIndex(lines, promptClose);
|
|
31
|
+
if (pOpenIdx < 0 || pCloseIdx < 0 || pCloseIdx <= pOpenIdx) {
|
|
32
|
+
return { ok: false, error: 'error: missing <cueme_prompt> block\n' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pre = lines.slice(0, pOpenIdx).join('\n');
|
|
36
|
+
if (pre.trim().length > 0) {
|
|
37
|
+
return { ok: false, error: 'error: only whitespace is allowed outside blocks\n' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const promptText = lines.slice(pOpenIdx + 1, pCloseIdx).join('\n');
|
|
41
|
+
if (!promptText.trim()) {
|
|
42
|
+
return { ok: false, error: 'error: <cueme_prompt> content must be non-empty\n' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const remainingLines = lines.slice(pCloseIdx + 1);
|
|
46
|
+
const remainingText = remainingLines.join('\n');
|
|
47
|
+
|
|
48
|
+
const rOpenIdx = findTagLineIndex(remainingLines, payloadOpen);
|
|
49
|
+
const rCloseIdx = findTagLineIndex(remainingLines, payloadClose);
|
|
50
|
+
|
|
51
|
+
let payload = null;
|
|
52
|
+
|
|
53
|
+
if (rOpenIdx >= 0 || rCloseIdx >= 0) {
|
|
54
|
+
if (!allowPayload) {
|
|
55
|
+
return { ok: false, error: 'error: <cueme_payload> is not supported for pause\n' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (rOpenIdx < 0 || rCloseIdx < 0 || rCloseIdx <= rOpenIdx) {
|
|
59
|
+
return { ok: false, error: 'error: invalid <cueme_payload> block\n' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const between = remainingLines.slice(0, rOpenIdx).join('\n');
|
|
63
|
+
const after = remainingLines.slice(rCloseIdx + 1).join('\n');
|
|
64
|
+
if (between.trim().length > 0 || after.trim().length > 0) {
|
|
65
|
+
return { ok: false, error: 'error: only whitespace is allowed outside blocks\n' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const payloadRaw = remainingLines.slice(rOpenIdx + 1, rCloseIdx).join('\n').trim();
|
|
69
|
+
if (!payloadRaw || payloadRaw === 'null') {
|
|
70
|
+
payload = null;
|
|
71
|
+
} else {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(payloadRaw);
|
|
75
|
+
} catch {
|
|
76
|
+
return { ok: false, error: 'error: <cueme_payload> must be JSON object or null\n' };
|
|
77
|
+
}
|
|
78
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
79
|
+
return { ok: false, error: 'error: <cueme_payload> must be JSON object or null\n' };
|
|
80
|
+
}
|
|
81
|
+
payload = parsed;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
if (remainingText.trim().length > 0) {
|
|
85
|
+
return { ok: false, error: 'error: only whitespace is allowed outside blocks\n' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ok: true, prompt: promptText, payload };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { parseTagBlocksEnvelope };
|
package/src/handler.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
2
|
const { sleep } = require('./io');
|
|
6
3
|
const { generateName } = require('./naming');
|
|
7
4
|
const { openDb, initSchema, run, get, nowIso, getDbPath } = require('./db');
|
|
@@ -99,9 +96,48 @@ function normalizeAgentRuntime(raw) {
|
|
|
99
96
|
return s ? s : 'unknown';
|
|
100
97
|
}
|
|
101
98
|
|
|
99
|
+
function detectAgentTerminal() {
|
|
100
|
+
const platform = process.platform;
|
|
101
|
+
if (platform === 'win32') {
|
|
102
|
+
const comspec = (process.env.ComSpec ?? '').toString().toLowerCase();
|
|
103
|
+
const psModulePath = (process.env.PSModulePath ?? '').toString();
|
|
104
|
+
const shell = (process.env.SHELL ?? '').toString().toLowerCase();
|
|
105
|
+
const nuVersion = (process.env.NU_VERSION ?? '').toString();
|
|
106
|
+
const msystem = (process.env.MSYSTEM ?? '').toString().toLowerCase();
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
shell.includes('powershell') ||
|
|
110
|
+
shell.includes('pwsh') ||
|
|
111
|
+
comspec.includes('powershell') ||
|
|
112
|
+
comspec.includes('pwsh') ||
|
|
113
|
+
psModulePath
|
|
114
|
+
) {
|
|
115
|
+
return 'powershell';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (nuVersion || shell.includes('nushell') || shell.endsWith('/nu') || shell === 'nu') return 'nushell';
|
|
119
|
+
|
|
120
|
+
if (shell.endsWith('/bash') || shell === 'bash' || msystem) return 'bash';
|
|
121
|
+
|
|
122
|
+
if (comspec.endsWith('cmd.exe') || comspec.includes('\\cmd.exe') || comspec.includes('/cmd.exe')) return 'cmd';
|
|
123
|
+
return 'unknown';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isWsl = Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
|
|
127
|
+
|
|
128
|
+
const shellPath = (process.env.SHELL ?? '').toString().toLowerCase();
|
|
129
|
+
if (shellPath.endsWith('/zsh') || shellPath === 'zsh') return 'zsh';
|
|
130
|
+
if (shellPath.endsWith('/bash') || shellPath === 'bash') return 'bash';
|
|
131
|
+
if (shellPath.endsWith('/fish') || shellPath === 'fish') return 'fish';
|
|
132
|
+
if (shellPath.endsWith('/nu') || shellPath === 'nu' || shellPath.includes('nushell')) return 'nushell';
|
|
133
|
+
if (isWsl && (shellPath.endsWith('/bash') || shellPath === 'bash' || !shellPath)) return 'bash';
|
|
134
|
+
return 'unknown';
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
async function handleJoin(db, agent_runtime) {
|
|
103
138
|
const agent_id = generateName();
|
|
104
139
|
const project_dir = process.cwd();
|
|
140
|
+
const agent_terminal = detectAgentTerminal();
|
|
105
141
|
const normalized_runtime = normalizeAgentRuntime(agent_runtime);
|
|
106
142
|
return {
|
|
107
143
|
ok: true,
|
|
@@ -110,6 +146,7 @@ async function handleJoin(db, agent_runtime) {
|
|
|
110
146
|
message:
|
|
111
147
|
`agent_id=${agent_id}\n` +
|
|
112
148
|
`project_dir=${project_dir}\n` +
|
|
149
|
+
`agent_terminal=${agent_terminal}\n` +
|
|
113
150
|
`agent_runtime=${normalized_runtime}\n\n` +
|
|
114
151
|
'Use this agent_id when calling cue(prompt, agent_id).' +
|
|
115
152
|
' You must remember this agent_id: when calling cue(), pass it as agent_id so the system knows who you are.' +
|
|
@@ -314,197 +351,11 @@ async function handlePause(db, { agent_id, prompt }) {
|
|
|
314
351
|
});
|
|
315
352
|
}
|
|
316
353
|
|
|
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
|
-
|
|
498
354
|
async function handleCommand({ subcommand, args }) {
|
|
499
355
|
const { db, dbPath } = openDb();
|
|
500
356
|
try {
|
|
501
357
|
await initSchema(db);
|
|
502
358
|
|
|
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
359
|
if (subcommand === 'join') return await handleJoin(db, args.agent_runtime);
|
|
509
360
|
|
|
510
361
|
if (subcommand === 'recall') {
|
|
@@ -526,10 +377,6 @@ async function handleCommand({ subcommand, args }) {
|
|
|
526
377
|
return await handlePause(db, { agent_id, prompt });
|
|
527
378
|
}
|
|
528
379
|
|
|
529
|
-
if (subcommand === 'migrate') {
|
|
530
|
-
return await handleMigrate(db);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
380
|
return { ok: false, error: `unknown subcommand: ${subcommand}`, data: { db_path: dbPath } };
|
|
534
381
|
} finally {
|
|
535
382
|
db.close();
|
package/src/io.js
CHANGED
|
@@ -1,12 +1,69 @@
|
|
|
1
1
|
function readAllStdin() {
|
|
2
2
|
if (process.stdin.isTTY) return Promise.resolve('');
|
|
3
3
|
return new Promise((resolve, reject) => {
|
|
4
|
-
|
|
5
|
-
process.stdin.setEncoding('utf8');
|
|
4
|
+
const chunks = [];
|
|
6
5
|
process.stdin.on('data', (chunk) => {
|
|
7
|
-
|
|
6
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
7
|
+
});
|
|
8
|
+
process.stdin.on('end', () => {
|
|
9
|
+
const buf = Buffer.concat(chunks);
|
|
10
|
+
if (!buf || buf.length === 0) return resolve('');
|
|
11
|
+
|
|
12
|
+
// BOM detection
|
|
13
|
+
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
|
|
14
|
+
return resolve(buf.slice(3).toString('utf8'));
|
|
15
|
+
}
|
|
16
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
17
|
+
return resolve(buf.slice(2).toString('utf16le'));
|
|
18
|
+
}
|
|
19
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
20
|
+
// UTF-16BE is rare; decode by swapping bytes.
|
|
21
|
+
const swapped = Buffer.alloc(buf.length - 2);
|
|
22
|
+
for (let i = 2; i + 1 < buf.length; i += 2) {
|
|
23
|
+
swapped[i - 2] = buf[i + 1];
|
|
24
|
+
swapped[i - 1] = buf[i];
|
|
25
|
+
}
|
|
26
|
+
return resolve(swapped.toString('utf16le'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Heuristic: PowerShell piping to native exe often uses UTF-16LE without BOM.
|
|
30
|
+
const sampleLen = Math.min(buf.length, 256);
|
|
31
|
+
|
|
32
|
+
// For small samples, use odd/even byte distribution (UTF-16LE ASCII often has 0x00 in odd indices).
|
|
33
|
+
if (sampleLen <= 64) {
|
|
34
|
+
let oddTotal = 0;
|
|
35
|
+
let evenTotal = 0;
|
|
36
|
+
let oddZeros = 0;
|
|
37
|
+
let evenZeros = 0;
|
|
38
|
+
for (let i = 0; i < sampleLen; i += 1) {
|
|
39
|
+
if (i % 2 === 0) {
|
|
40
|
+
evenTotal += 1;
|
|
41
|
+
if (buf[i] === 0x00) evenZeros += 1;
|
|
42
|
+
} else {
|
|
43
|
+
oddTotal += 1;
|
|
44
|
+
if (buf[i] === 0x00) oddZeros += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const oddZeroRatio = oddTotal > 0 ? oddZeros / oddTotal : 0;
|
|
49
|
+
const evenZeroRatio = evenTotal > 0 ? evenZeros / evenTotal : 0;
|
|
50
|
+
|
|
51
|
+
if (oddZeroRatio > 0.6 && evenZeroRatio < 0.2) {
|
|
52
|
+
return resolve(buf.toString('utf16le'));
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
let zeros = 0;
|
|
56
|
+
for (let i = 0; i < sampleLen; i += 1) {
|
|
57
|
+
if (buf[i] === 0x00) zeros += 1;
|
|
58
|
+
}
|
|
59
|
+
const zeroRatio = zeros / sampleLen;
|
|
60
|
+
if (zeroRatio > 0.2) {
|
|
61
|
+
return resolve(buf.toString('utf16le'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return resolve(buf.toString('utf8'));
|
|
8
66
|
});
|
|
9
|
-
process.stdin.on('end', () => resolve(data));
|
|
10
67
|
process.stdin.on('error', reject);
|
|
11
68
|
});
|
|
12
69
|
}
|
package/src/proto.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const BEGIN_MARKER = '<!-- HUMAN_AGENT_PROTO_BEGIN -->';
|
|
6
|
+
const END_MARKER = '<!-- HUMAN_AGENT_PROTO_END -->';
|
|
7
|
+
|
|
8
|
+
const BEGIN_MARKER_RE = /<!--\s*(?:HUMAN|HUAMN)_AGENT_PROTO_BEGIN\s*-->/;
|
|
9
|
+
const END_MARKER_RE = /<!--\s*(?:HUMAN|HUAMN)_AGENT_PROTO_END\s*-->/;
|
|
10
|
+
|
|
11
|
+
function getPlatformKey() {
|
|
12
|
+
const p = process.platform;
|
|
13
|
+
if (p === 'win32') return 'windows';
|
|
14
|
+
if (p === 'darwin') return 'macos';
|
|
15
|
+
if (p === 'linux') return 'linux';
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function configPath() {
|
|
20
|
+
return path.join(os.homedir(), '.cue', 'cueme.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function expandPath(p) {
|
|
24
|
+
if (typeof p !== 'string') return p;
|
|
25
|
+
let s = p;
|
|
26
|
+
|
|
27
|
+
// Expand leading ~
|
|
28
|
+
if (s === '~' || s.startsWith('~/') || s.startsWith('~\\')) {
|
|
29
|
+
s = path.join(os.homedir(), s.slice(2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Expand %ENV% variables (Windows-style)
|
|
33
|
+
s = s.replace(/%([A-Za-z0-9_]+)%/g, (_, name) => {
|
|
34
|
+
const v = process.env[name];
|
|
35
|
+
return typeof v === 'string' ? v : '';
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pathExists(p) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.existsSync(p);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detectVscodeCandidates({ platform }) {
|
|
50
|
+
const candidates = [];
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
|
|
53
|
+
// Workspace-level (if you want repo-local rules)
|
|
54
|
+
candidates.push(path.join(cwd, '.vscode', 'prompts', 'human_proto.md'));
|
|
55
|
+
|
|
56
|
+
// User-level prompts
|
|
57
|
+
const home = os.homedir();
|
|
58
|
+
if (platform === 'macos') {
|
|
59
|
+
candidates.push(
|
|
60
|
+
path.join(home, 'Library', 'Application Support', 'Code', 'User', 'prompts', 'human_proto.md')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (platform === 'linux') {
|
|
64
|
+
// User supplied: ~/.config/Code/User/prompts/
|
|
65
|
+
candidates.push(path.join(home, '.config', 'Code', 'User', 'prompts', 'human_proto.md'));
|
|
66
|
+
}
|
|
67
|
+
if (platform === 'windows') {
|
|
68
|
+
const appData = process.env.APPDATA;
|
|
69
|
+
if (appData) candidates.push(path.join(appData, 'Code', 'User', 'prompts', 'human_proto.md'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return candidates;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function detectWindsurfCandidates({ platform }) {
|
|
76
|
+
const candidates = [];
|
|
77
|
+
const cwd = process.cwd();
|
|
78
|
+
|
|
79
|
+
// Workspace-level (repo-local)
|
|
80
|
+
candidates.push(path.join(cwd, '.codeium', 'windsurf', 'memories', 'global_rules.md'));
|
|
81
|
+
|
|
82
|
+
// User-level (standard)
|
|
83
|
+
const home = os.homedir();
|
|
84
|
+
const userProfile = process.env.USERPROFILE || home;
|
|
85
|
+
if (platform === 'macos') candidates.push(path.join(home, '.codeium', 'windsurf', 'memories', 'global_rules.md'));
|
|
86
|
+
if (platform === 'linux') candidates.push(path.join(home, '.codeium', 'windsurf', 'memories', 'global_rules.md'));
|
|
87
|
+
if (platform === 'windows') {
|
|
88
|
+
candidates.push(path.join(userProfile, '.codeium', 'windsurf', 'memories', 'global_rules.md'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return candidates;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function firstExistingPath(candidates) {
|
|
95
|
+
for (const p of candidates) {
|
|
96
|
+
if (typeof p === 'string' && p.trim().length > 0 && pathExists(p)) return p;
|
|
97
|
+
}
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defaultPathMapTemplate() {
|
|
102
|
+
const out = {};
|
|
103
|
+
|
|
104
|
+
const home = os.homedir();
|
|
105
|
+
const appData = process.env.APPDATA || '';
|
|
106
|
+
const userProfile = process.env.USERPROFILE || home;
|
|
107
|
+
|
|
108
|
+
// VSCode
|
|
109
|
+
out['macos.vscode'] = path.join(
|
|
110
|
+
home,
|
|
111
|
+
'Library',
|
|
112
|
+
'Application Support',
|
|
113
|
+
'Code',
|
|
114
|
+
'User',
|
|
115
|
+
'prompts',
|
|
116
|
+
'human_proto.md'
|
|
117
|
+
);
|
|
118
|
+
out['windows.vscode'] = appData
|
|
119
|
+
? path.join(appData, 'Code', 'User', 'prompts', 'human_proto.md')
|
|
120
|
+
: path.join(userProfile, 'AppData', 'Roaming', 'Code', 'User', 'prompts', 'human_proto.md');
|
|
121
|
+
out['linux.vscode'] = path.join(home, '.config', 'Code', 'User', 'prompts', 'human_proto.md');
|
|
122
|
+
|
|
123
|
+
// Windsurf
|
|
124
|
+
out['macos.windsurf'] = path.join(home, '.codeium', 'windsurf', 'memories', 'global_rules.md');
|
|
125
|
+
out['windows.windsurf'] = path.join(userProfile, '.codeium', 'windsurf', 'memories', 'global_rules.md');
|
|
126
|
+
out['linux.windsurf'] = path.join(home, '.codeium', 'windsurf', 'memories', 'global_rules.md');
|
|
127
|
+
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function defaultConfigTemplate() {
|
|
132
|
+
const protocolPath = path.join(__dirname, '..', 'protocol.md');
|
|
133
|
+
return {
|
|
134
|
+
'cueme.proto.path': defaultPathMapTemplate(),
|
|
135
|
+
'cueme.proto.prefix': {
|
|
136
|
+
windsurf: [],
|
|
137
|
+
vscode: ['---', 'applyTo: "**"', '---'],
|
|
138
|
+
},
|
|
139
|
+
'cueme.proto.protocol_path': protocolPath,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function detectAndFillTemplatePaths(tpl) {
|
|
144
|
+
const platform = getPlatformKey();
|
|
145
|
+
const keyVscode = `${platform}.vscode`;
|
|
146
|
+
const keyWindsurf = `${platform}.windsurf`;
|
|
147
|
+
|
|
148
|
+
const pathMap = tpl['cueme.proto.path'] || {};
|
|
149
|
+
const detected = { platform, vscode: '', windsurf: '' };
|
|
150
|
+
|
|
151
|
+
if (typeof pathMap !== 'object' || Array.isArray(pathMap)) {
|
|
152
|
+
return { tpl, detected };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// If current platform key is empty or missing, try detect.
|
|
156
|
+
if (typeof pathMap[keyVscode] !== 'string' || pathMap[keyVscode].trim().length === 0) {
|
|
157
|
+
const p = firstExistingPath(detectVscodeCandidates({ platform }));
|
|
158
|
+
if (p) {
|
|
159
|
+
pathMap[keyVscode] = p;
|
|
160
|
+
detected.vscode = p;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof pathMap[keyWindsurf] !== 'string' || pathMap[keyWindsurf].trim().length === 0) {
|
|
165
|
+
const p = firstExistingPath(detectWindsurfCandidates({ platform }));
|
|
166
|
+
if (p) {
|
|
167
|
+
pathMap[keyWindsurf] = p;
|
|
168
|
+
detected.windsurf = p;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
tpl['cueme.proto.path'] = pathMap;
|
|
173
|
+
return { tpl, detected };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function initConfigIfMissing() {
|
|
177
|
+
const p = configPath();
|
|
178
|
+
if (fs.existsSync(p)) {
|
|
179
|
+
return { created: false, path: p, detected: null };
|
|
180
|
+
}
|
|
181
|
+
ensureDirForFile(p);
|
|
182
|
+
const { tpl, detected } = detectAndFillTemplatePaths(defaultConfigTemplate());
|
|
183
|
+
fs.writeFileSync(p, JSON.stringify(tpl, null, 2) + '\n', 'utf8');
|
|
184
|
+
return { created: true, path: p, detected };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readConfigOrThrow({ auto_init } = {}) {
|
|
188
|
+
const p = configPath();
|
|
189
|
+
let raw;
|
|
190
|
+
try {
|
|
191
|
+
raw = fs.readFileSync(p, 'utf8');
|
|
192
|
+
} catch {
|
|
193
|
+
if (auto_init) {
|
|
194
|
+
initConfigIfMissing();
|
|
195
|
+
try {
|
|
196
|
+
raw = fs.readFileSync(p, 'utf8');
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(`error: cannot read config: ${p}`);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error(`error: cannot read config: ${p}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let cfg;
|
|
206
|
+
try {
|
|
207
|
+
cfg = JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error(`error: config is not valid JSON: ${p}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!cfg || typeof cfg !== 'object') {
|
|
213
|
+
throw new Error(`error: config must be a JSON object: ${p}`);
|
|
214
|
+
}
|
|
215
|
+
return cfg;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function detectEol(text) {
|
|
219
|
+
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function ensureDirForFile(filePath) {
|
|
223
|
+
const dir = path.dirname(filePath);
|
|
224
|
+
if (!fs.existsSync(dir)) {
|
|
225
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildFinalProto({ cfg, agent }) {
|
|
230
|
+
const prefixMap = cfg['cueme.proto.prefix'] || {};
|
|
231
|
+
const protocolPath = cfg['cueme.proto.protocol_path'];
|
|
232
|
+
|
|
233
|
+
const prefixRaw = prefixMap[agent];
|
|
234
|
+
let prefix;
|
|
235
|
+
if (typeof prefixRaw === 'string') {
|
|
236
|
+
prefix = prefixRaw;
|
|
237
|
+
} else if (Array.isArray(prefixRaw) && prefixRaw.every((x) => typeof x === 'string')) {
|
|
238
|
+
prefix = prefixRaw.join('\n');
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error(`error: prefix not configured: cueme.proto.prefix["${agent}"]`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (typeof protocolPath !== 'string' || protocolPath.trim().length === 0) {
|
|
244
|
+
throw new Error('error: cannot read protocol.md');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const protocolPathExpanded = expandPath(protocolPath);
|
|
248
|
+
const resolvedProtocolPath = path.isAbsolute(protocolPathExpanded)
|
|
249
|
+
? protocolPathExpanded
|
|
250
|
+
: path.resolve(process.cwd(), protocolPathExpanded);
|
|
251
|
+
|
|
252
|
+
let protocol;
|
|
253
|
+
try {
|
|
254
|
+
protocol = fs.readFileSync(resolvedProtocolPath, 'utf8');
|
|
255
|
+
} catch {
|
|
256
|
+
throw new Error('error: cannot read protocol.md');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return `${prefix}\n\n${protocol}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resolveTargetPath({ cfg, agent }) {
|
|
263
|
+
const platform = getPlatformKey();
|
|
264
|
+
const key = `${platform}.${agent}`;
|
|
265
|
+
const pathMap = cfg['cueme.proto.path'] || {};
|
|
266
|
+
const p = pathMap[key];
|
|
267
|
+
if (typeof p !== 'string' || p.trim().length === 0) {
|
|
268
|
+
throw new Error(`error: target path not configured: cueme.proto.path["${key}"]`);
|
|
269
|
+
}
|
|
270
|
+
const expanded = expandPath(p);
|
|
271
|
+
return path.isAbsolute(expanded) ? expanded : path.resolve(process.cwd(), expanded);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function makeManagedBlock({ finalProto, eol }) {
|
|
275
|
+
const normalized = String(finalProto || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
276
|
+
const protoLines = normalized.split('\n');
|
|
277
|
+
return [BEGIN_MARKER, ...protoLines, END_MARKER].join(eol) + eol;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function applyManagedBlock({ existing, finalProto }) {
|
|
281
|
+
const eol = detectEol(existing);
|
|
282
|
+
const block = makeManagedBlock({ finalProto, eol });
|
|
283
|
+
|
|
284
|
+
const beginMatch = existing.match(BEGIN_MARKER_RE);
|
|
285
|
+
const endMatch = existing.match(END_MARKER_RE);
|
|
286
|
+
if (beginMatch && endMatch && endMatch.index > beginMatch.index) {
|
|
287
|
+
const beginIdx = beginMatch.index;
|
|
288
|
+
const endIdx = endMatch.index;
|
|
289
|
+
const endLen = endMatch[0].length;
|
|
290
|
+
|
|
291
|
+
const before = existing.slice(0, beginIdx);
|
|
292
|
+
const after = existing.slice(endIdx + endLen);
|
|
293
|
+
|
|
294
|
+
const afterTrim = after.startsWith(eol) ? after.slice(eol.length) : after;
|
|
295
|
+
return before + block + afterTrim;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let out = existing;
|
|
299
|
+
if (!out.endsWith(eol)) out += eol;
|
|
300
|
+
out += block;
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function listAgents({ cfg }) {
|
|
305
|
+
const prefixMap = cfg['cueme.proto.prefix'] || {};
|
|
306
|
+
const pathMap = cfg['cueme.proto.path'] || {};
|
|
307
|
+
|
|
308
|
+
const agents = new Set();
|
|
309
|
+
for (const k of Object.keys(prefixMap)) agents.add(k);
|
|
310
|
+
for (const k of Object.keys(pathMap)) {
|
|
311
|
+
const parts = String(k).split('.');
|
|
312
|
+
if (parts.length === 2) agents.add(parts[1]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return Array.from(agents).sort();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function protoRender(agent) {
|
|
319
|
+
const cfg = readConfigOrThrow({ auto_init: true });
|
|
320
|
+
return buildFinalProto({ cfg, agent });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function protoPath(agent) {
|
|
324
|
+
const cfg = readConfigOrThrow({ auto_init: true });
|
|
325
|
+
return resolveTargetPath({ cfg, agent });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function protoLs() {
|
|
329
|
+
const cfg = readConfigOrThrow({ auto_init: true });
|
|
330
|
+
return listAgents({ cfg }).join('\n') + '\n';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function protoApply(agent) {
|
|
334
|
+
const cfg = readConfigOrThrow({ auto_init: true });
|
|
335
|
+
const targetPath = resolveTargetPath({ cfg, agent });
|
|
336
|
+
const finalProto = buildFinalProto({ cfg, agent });
|
|
337
|
+
|
|
338
|
+
let existing = '';
|
|
339
|
+
let exists = false;
|
|
340
|
+
try {
|
|
341
|
+
existing = fs.readFileSync(targetPath, 'utf8');
|
|
342
|
+
exists = true;
|
|
343
|
+
} catch {
|
|
344
|
+
existing = '';
|
|
345
|
+
exists = false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const eol = exists ? detectEol(existing) : os.EOL;
|
|
349
|
+
const managedBlock = makeManagedBlock({ finalProto, eol });
|
|
350
|
+
|
|
351
|
+
let out;
|
|
352
|
+
if (!exists) {
|
|
353
|
+
out = managedBlock;
|
|
354
|
+
} else {
|
|
355
|
+
out = applyManagedBlock({ existing, finalProto });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
ensureDirForFile(targetPath);
|
|
359
|
+
fs.writeFileSync(targetPath, out, 'utf8');
|
|
360
|
+
|
|
361
|
+
return `ok: applied to ${targetPath}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function protoInit() {
|
|
365
|
+
const { created, path: p, detected } = initConfigIfMissing();
|
|
366
|
+
if (!created) return `ok: exists ${p}`;
|
|
367
|
+
const platform = detected && detected.platform ? detected.platform : getPlatformKey();
|
|
368
|
+
const keyVscode = `${platform}.vscode`;
|
|
369
|
+
const keyWindsurf = `${platform}.windsurf`;
|
|
370
|
+
const vs = detected && detected.vscode ? 'detected' : 'empty';
|
|
371
|
+
const ws = detected && detected.windsurf ? 'detected' : 'empty';
|
|
372
|
+
return `ok: initialized ${p} (auto-detect: ${keyVscode}=${vs}, ${keyWindsurf}=${ws})`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
BEGIN_MARKER,
|
|
377
|
+
END_MARKER,
|
|
378
|
+
getPlatformKey,
|
|
379
|
+
protoApply,
|
|
380
|
+
protoInit,
|
|
381
|
+
protoLs,
|
|
382
|
+
protoPath,
|
|
383
|
+
protoRender,
|
|
384
|
+
};
|