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/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
+ }