cawdev-cli 0.9.0
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 +175 -0
- package/lib/ansi.mjs +224 -0
- package/lib/cawdev.mjs +104 -0
- package/lib/code-map.mjs +164 -0
- package/lib/harness-prompt.mjs +197 -0
- package/lib/roadmap-format.mjs +453 -0
- package/lib/run-plugin.mjs +119 -0
- package/lib/secrets.mjs +290 -0
- package/lib/stage-tools.mjs +384 -0
- package/lib/tool-line.mjs +92 -0
- package/lib/tool-rules.mjs +282 -0
- package/lib/transcript-batch.mjs +88 -0
- package/lib/usage-limit.mjs +80 -0
- package/lib/usage-report.mjs +142 -0
- package/lib/usage.mjs +119 -0
- package/mcp/README.md +273 -0
- package/mcp/orchestration-smoke.mjs +267 -0
- package/mcp/server.mjs +2163 -0
- package/mcp/smoke.mjs +220 -0
- package/package.json +20 -0
- package/runner/README.md +930 -0
- package/runner/attach.mjs +2397 -0
- package/runner/banner.mjs +106 -0
- package/runner/bootstrap.mjs +501 -0
- package/runner/brand.mjs +57 -0
- package/runner/cawdev.mjs +414 -0
- package/runner/control.mjs +225 -0
- package/runner/history.mjs +91 -0
- package/runner/input.mjs +355 -0
- package/runner/macbook-laptop.json +48 -0
- package/runner/runner.mjs +7445 -0
- package/runner/scrollback.mjs +165 -0
- package/runner/select.mjs +316 -0
- package/runner/session-store.mjs +78 -0
- package/runner/sign-in.mjs +210 -0
- package/runner/stub-agent.mjs +212 -0
- package/runner/token-store.mjs +107 -0
package/mcp/smoke.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A scripted JSON-RPC session against tools/mcp/server.mjs, over real stdio.
|
|
3
|
+
//
|
|
4
|
+
// CAWDEV_URL=… CAWDEV_TOKEN=… node tools/mcp/smoke.mjs [project]
|
|
5
|
+
//
|
|
6
|
+
// This is what CI runs against the compose stack. It drives the server the way
|
|
7
|
+
// a client does — spawn, write lines to stdin, read lines from stdout — rather
|
|
8
|
+
// than importing its functions, because the parts most likely to break are the
|
|
9
|
+
// transport and the framing, and importing would skip exactly those.
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
// The project must be named explicitly. This test CREATES an entry and then
|
|
18
|
+
// declines it, and entries cannot be deleted — so falling back to
|
|
19
|
+
// CAWDEV_PROJECT would quietly leave test artifacts in whatever real roadmap
|
|
20
|
+
// the environment happened to point at. It did exactly that once.
|
|
21
|
+
const project = process.argv[2];
|
|
22
|
+
if (!project) {
|
|
23
|
+
console.error(
|
|
24
|
+
'Usage: node tools/mcp/smoke.mjs <project>\n\n' +
|
|
25
|
+
'Name the project explicitly: this test creates a roadmap entry and declines it, and\n' +
|
|
26
|
+
'entries cannot be deleted. Use a scratch project, not one whose roadmap you care about.',
|
|
27
|
+
);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const server = spawn(process.execPath, [join(here, 'server.mjs')], {
|
|
32
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
33
|
+
env: process.env,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let buffer = '';
|
|
37
|
+
const waiting = new Map();
|
|
38
|
+
|
|
39
|
+
server.stdout.on('data', (chunk) => {
|
|
40
|
+
buffer += chunk;
|
|
41
|
+
let newline;
|
|
42
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
43
|
+
const line = buffer.slice(0, newline).trim();
|
|
44
|
+
buffer = buffer.slice(newline + 1);
|
|
45
|
+
if (!line) continue;
|
|
46
|
+
|
|
47
|
+
const message = JSON.parse(line);
|
|
48
|
+
const resolve = waiting.get(message.id);
|
|
49
|
+
if (resolve) {
|
|
50
|
+
waiting.delete(message.id);
|
|
51
|
+
resolve(message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let nextId = 1;
|
|
57
|
+
|
|
58
|
+
function request(method, params) {
|
|
59
|
+
const id = nextId++;
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
waiting.set(id, resolve);
|
|
62
|
+
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
63
|
+
setTimeout(() => reject(new Error(`${method} timed out after 20s`)), 20_000).unref();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function notify(method, params) {
|
|
68
|
+
server.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const checks = [];
|
|
72
|
+
function check(description, condition, detail = '') {
|
|
73
|
+
checks.push({ description, ok: Boolean(condition), detail });
|
|
74
|
+
console.log(`${condition ? 'ok ' : 'FAIL'} ${description}${detail && !condition ? `\n ${detail}` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Tool results carry their text in content[0]; a refusal sets isError. */
|
|
78
|
+
function textOf(result) {
|
|
79
|
+
return result?.content?.map((part) => part.text).join('\n') ?? '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const initialize = await request('initialize', {
|
|
84
|
+
protocolVersion: '2024-11-05',
|
|
85
|
+
capabilities: {},
|
|
86
|
+
clientInfo: { name: 'cawdev-smoke', version: '1' },
|
|
87
|
+
});
|
|
88
|
+
check('initialize answers with a server name', initialize.result?.serverInfo?.name === 'cawdev',
|
|
89
|
+
JSON.stringify(initialize));
|
|
90
|
+
check('initialize declares the tools capability', initialize.result?.capabilities?.tools !== undefined);
|
|
91
|
+
notify('notifications/initialized');
|
|
92
|
+
|
|
93
|
+
const list = await request('tools/list');
|
|
94
|
+
const names = (list.result?.tools ?? []).map((tool) => tool.name);
|
|
95
|
+
check('tools/list returns the roadmap and changelog verbs', names.includes('roadmap_list')
|
|
96
|
+
&& names.includes('roadmap_set_status') && names.includes('roadmap_comment')
|
|
97
|
+
&& names.includes('changelog_add'),
|
|
98
|
+
names.join(', '));
|
|
99
|
+
check('every tool has a description and an input schema',
|
|
100
|
+
(list.result?.tools ?? []).every((tool) => tool.description && tool.inputSchema));
|
|
101
|
+
|
|
102
|
+
// The guarantee that matters more than any feature.
|
|
103
|
+
check('there is no delete tool, for roadmap or changelog',
|
|
104
|
+
!names.some((name) => name.includes('delete') || name.includes('remove')),
|
|
105
|
+
names.join(', '));
|
|
106
|
+
|
|
107
|
+
const where = await request('tools/call', { name: 'roadmap_where', arguments: {} });
|
|
108
|
+
check('roadmap_where says which platform and who we are',
|
|
109
|
+
textOf(where.result).includes('platform:') && textOf(where.result).includes('you are:'),
|
|
110
|
+
textOf(where.result));
|
|
111
|
+
|
|
112
|
+
const statuses = await request('tools/call', { name: 'roadmap_statuses', arguments: {} });
|
|
113
|
+
// R84 retired CODING; IN_DEVELOPMENT is the status that carries a branch now.
|
|
114
|
+
check('roadmap_statuses says what IN DEVELOPMENT requires',
|
|
115
|
+
/IN DEVELOPMENT\s+\(requires a branch\)/.test(textOf(statuses.result)),
|
|
116
|
+
textOf(statuses.result));
|
|
117
|
+
|
|
118
|
+
const listed = await request('tools/call', {
|
|
119
|
+
name: 'roadmap_list',
|
|
120
|
+
arguments: { project, brief: true },
|
|
121
|
+
});
|
|
122
|
+
check('roadmap_list returns entries', !listed.result?.isError && textOf(listed.result).includes('R1'),
|
|
123
|
+
textOf(listed.result));
|
|
124
|
+
|
|
125
|
+
const created = await request('tools/call', {
|
|
126
|
+
name: 'roadmap_create',
|
|
127
|
+
arguments: {
|
|
128
|
+
project,
|
|
129
|
+
title: 'smoke test entry (safe to decline)',
|
|
130
|
+
body: 'Created by tools/mcp/smoke.mjs.',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
check('roadmap_create makes an entry', !created.result?.isError
|
|
134
|
+
&& /Created R\d+/.test(textOf(created.result)), textOf(created.result));
|
|
135
|
+
|
|
136
|
+
const number = Number(/Created R(\d+)/.exec(textOf(created.result))?.[1]);
|
|
137
|
+
|
|
138
|
+
// A status that is not given what it requires must come back as a readable
|
|
139
|
+
// refusal, not a crash — and the message should say what is missing.
|
|
140
|
+
const refused = await request('tools/call', {
|
|
141
|
+
name: 'roadmap_set_status',
|
|
142
|
+
arguments: { project, number, status: 'IN_DEVELOPMENT' },
|
|
143
|
+
});
|
|
144
|
+
check('IN DEVELOPMENT without a branch is refused, readably',
|
|
145
|
+
refused.result?.isError && /branch/i.test(textOf(refused.result)),
|
|
146
|
+
textOf(refused.result));
|
|
147
|
+
|
|
148
|
+
const moved = await request('tools/call', {
|
|
149
|
+
name: 'roadmap_set_status',
|
|
150
|
+
arguments: { project, number, status: 'IN_DEVELOPMENT', branch: 'smoke-test' },
|
|
151
|
+
});
|
|
152
|
+
check('IN DEVELOPMENT with a branch is accepted', !moved.result?.isError
|
|
153
|
+
&& textOf(moved.result).includes('smoke-test'), textOf(moved.result));
|
|
154
|
+
|
|
155
|
+
// R37: the argument, beside the decision. An agent that can read the
|
|
156
|
+
// discussion stops re-proposing what was talked out months ago, so
|
|
157
|
+
// roadmap_get has to actually carry it.
|
|
158
|
+
const commented = await request('tools/call', {
|
|
159
|
+
name: 'roadmap_comment',
|
|
160
|
+
arguments: { project, number, body: 'A smoke-test comment. It cannot be deleted.' },
|
|
161
|
+
});
|
|
162
|
+
// R127: the reply says "card N", not "RN" — this call never reads the card,
|
|
163
|
+
// so it does not know the kind and a prefix would be a guess.
|
|
164
|
+
check('roadmap_comment says something beside the entry', !commented.result?.isError
|
|
165
|
+
&& /Commented on card \d+/.test(textOf(commented.result)), textOf(commented.result));
|
|
166
|
+
|
|
167
|
+
const fetched = await request('tools/call', {
|
|
168
|
+
name: 'roadmap_get',
|
|
169
|
+
arguments: { project, number },
|
|
170
|
+
});
|
|
171
|
+
check('roadmap_get carries the discussion, not just the body',
|
|
172
|
+
!fetched.result?.isError
|
|
173
|
+
&& textOf(fetched.result).includes('the discussion')
|
|
174
|
+
&& textOf(fetched.result).includes('A smoke-test comment.'),
|
|
175
|
+
textOf(fetched.result));
|
|
176
|
+
|
|
177
|
+
const declined = await request('tools/call', {
|
|
178
|
+
name: 'roadmap_decline',
|
|
179
|
+
arguments: {
|
|
180
|
+
project,
|
|
181
|
+
number,
|
|
182
|
+
reason: 'A smoke-test entry. Declined so it leaves a trace rather than vanishing.',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
check('roadmap_decline closes it with a reason', !declined.result?.isError
|
|
186
|
+
&& textOf(declined.result).includes('DECLINED'), textOf(declined.result));
|
|
187
|
+
|
|
188
|
+
// R199: feedback that is not a card. The smoke token holds roadmap:write,
|
|
189
|
+
// which covers backlog:write, so this is the same door an ordinary session
|
|
190
|
+
// files through. A backlog item is not an entry — it has no number — and it
|
|
191
|
+
// stays pending until a person triages it, which this script cannot do.
|
|
192
|
+
const filed = await request('tools/call', {
|
|
193
|
+
name: 'backlog_file',
|
|
194
|
+
arguments: {
|
|
195
|
+
project,
|
|
196
|
+
kind: 'FEATURE',
|
|
197
|
+
title: 'smoke test feedback (safe to refuse)',
|
|
198
|
+
body: 'Filed by tools/mcp/smoke.mjs. Refuse it.',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
check('backlog_file files feedback as pending, not as a card', !filed.result?.isError
|
|
202
|
+
&& /as pending/.test(textOf(filed.result)) && !/Created [Ri]\d+/.test(textOf(filed.result)),
|
|
203
|
+
textOf(filed.result));
|
|
204
|
+
|
|
205
|
+
const changelog = await request('tools/call', {
|
|
206
|
+
name: 'changelog_list',
|
|
207
|
+
arguments: { project },
|
|
208
|
+
});
|
|
209
|
+
check('changelog_list works', !changelog.result?.isError, textOf(changelog.result));
|
|
210
|
+
|
|
211
|
+
const unknown = await request('tools/call', { name: 'roadmap_teleport', arguments: {} });
|
|
212
|
+
check('an unknown tool is a protocol error, not a crash', unknown.error?.code === -32602,
|
|
213
|
+
JSON.stringify(unknown));
|
|
214
|
+
} finally {
|
|
215
|
+
server.stdin.end();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const failed = checks.filter((entry) => !entry.ok);
|
|
219
|
+
console.log(`\n${checks.length - failed.length}/${checks.length} checks passed.`);
|
|
220
|
+
process.exit(failed.length ? 1 : 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cawdev-cli",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "cawdev's terminal client and runner daemon — zero dependencies, on purpose",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cawdev": "./runner/cawdev.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"runner",
|
|
11
|
+
"lib",
|
|
12
|
+
"mcp",
|
|
13
|
+
"!**/*.test.mjs",
|
|
14
|
+
"!runner/test-platform.mjs"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"license": "UNLICENSED"
|
|
20
|
+
}
|