@smartspec/cli 0.0.0-stage → 0.1.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 +51 -2
- package/cli.js +37 -0
- package/lib/agents.js +89 -0
- package/lib/apps.js +38 -0
- package/lib/bridge.js +136 -0
- package/lib/claude-code.js +35 -0
- package/lib/commands.js +180 -0
- package/lib/install.js +118 -0
- package/lib/launch.js +96 -0
- package/lib/platform.js +42 -0
- package/lib/server.json +1066 -0
- package/lib/wait.js +84 -0
- package/mcp.js +20 -0
- package/package.json +28 -4
package/lib/wait.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { readToken } from './launch.js';
|
|
4
|
+
import { mcpUrl } from './platform.js';
|
|
5
|
+
|
|
6
|
+
// `smartspec wait`: stays until something the AI asked of the user has happened, then says what and
|
|
7
|
+
// exits. An AI runs it in the background, and its ending wakes the AI. It asks the app, which holds
|
|
8
|
+
// the request open until then; it uses http, as fetch gives up on a reply after five minutes.
|
|
9
|
+
|
|
10
|
+
export const EXIT = { happened: 0, usage: 1, timedOut: 2, refused: 3, noApp: 4 };
|
|
11
|
+
|
|
12
|
+
export const WAIT_HELP = `Usage: npx @smartspec/cli wait proposal --id <id> [--timeout <seconds>]
|
|
13
|
+
|
|
14
|
+
proposal --id <id> The user has accepted or discarded this proposal, or it was replaced
|
|
15
|
+
|
|
16
|
+
Prints what happened as JSON. Exits with 0 when it has happened, 1 if the command is wrong, 2 if
|
|
17
|
+
the timeout passed, 3 if the app says this can never happen, and 4 if the app isn't running or quits.`;
|
|
18
|
+
|
|
19
|
+
export async function wait(argv, { env = process.env, out = console.log, err = console.error } = {}) {
|
|
20
|
+
let values;
|
|
21
|
+
let positionals;
|
|
22
|
+
try {
|
|
23
|
+
({ values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { id: { type: 'string' }, timeout: { type: 'string' } } }));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
err(error.message);
|
|
26
|
+
return EXIT.usage;
|
|
27
|
+
}
|
|
28
|
+
const [what] = positionals;
|
|
29
|
+
const timeout = values.timeout === undefined ? null : Number(values.timeout);
|
|
30
|
+
const id = values.id;
|
|
31
|
+
if (positionals.length !== 1 || what !== 'proposal' || !id || (timeout !== null && !(timeout > 0))) {
|
|
32
|
+
err(WAIT_HELP);
|
|
33
|
+
return EXIT.usage;
|
|
34
|
+
}
|
|
35
|
+
const token = readToken({ env });
|
|
36
|
+
if (!token) {
|
|
37
|
+
err("SmartSpec hasn't been run on this computer yet.");
|
|
38
|
+
return EXIT.noApp;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const reply = await ask(`${mcpUrl(env)}/wait`, token, { what, proposal: id }, timeout);
|
|
42
|
+
if (reply.timedOut) {
|
|
43
|
+
err(`Nothing yet after ${timeout} seconds.`);
|
|
44
|
+
return EXIT.timedOut;
|
|
45
|
+
}
|
|
46
|
+
if (reply.lost) {
|
|
47
|
+
err('SmartSpec isn’t running. Open it, then wait again.');
|
|
48
|
+
return EXIT.noApp;
|
|
49
|
+
}
|
|
50
|
+
if (reply.status === 200) {
|
|
51
|
+
out(reply.text);
|
|
52
|
+
return EXIT.happened;
|
|
53
|
+
}
|
|
54
|
+
let reason = `SmartSpec refused the request (HTTP ${reply.status}).`;
|
|
55
|
+
try {
|
|
56
|
+
reason = JSON.parse(reply.text).error ?? reason;
|
|
57
|
+
} catch {}
|
|
58
|
+
err(reason);
|
|
59
|
+
return reply.status === 401 || reply.status === 403 ? EXIT.noApp : EXIT.refused;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ask(url, token, body, timeoutSeconds) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let timer = null;
|
|
65
|
+
const done = (reply) => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
resolve(reply);
|
|
68
|
+
};
|
|
69
|
+
const request = http.request(url, { method: 'POST', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' } }, (res) => {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
72
|
+
res.on('end', () => done({ status: res.statusCode, text: Buffer.concat(chunks).toString('utf8').trim() }));
|
|
73
|
+
res.on('error', () => done({ lost: true }));
|
|
74
|
+
});
|
|
75
|
+
request.on('error', () => done({ lost: true }));
|
|
76
|
+
if (timeoutSeconds !== null) {
|
|
77
|
+
timer = setTimeout(() => {
|
|
78
|
+
done({ timedOut: true });
|
|
79
|
+
request.destroy();
|
|
80
|
+
}, timeoutSeconds * 1000);
|
|
81
|
+
}
|
|
82
|
+
request.end(JSON.stringify(body));
|
|
83
|
+
});
|
|
84
|
+
}
|
package/mcp.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SmartSpec's MCP server, as AI apps start it: the copy inside the SmartSpec app (run by the app's
|
|
3
|
+
// own executable as Node), or `npx @smartspec/cli mcp` (see cli.js). It opens the installed SmartSpec app when the
|
|
4
|
+
// AI first uses it, and passes the AI's messages to it. Only MCP messages go to stdout; progress
|
|
5
|
+
// and errors go to stderr.
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { runBridge } from './lib/bridge.js';
|
|
8
|
+
import { ensureRunning, readToken } from './lib/launch.js';
|
|
9
|
+
import { VERSION } from './lib/platform.js';
|
|
10
|
+
|
|
11
|
+
const log = (message) => process.stderr.write(`[smartspec] ${message}\n`);
|
|
12
|
+
const offline = JSON.parse(readFileSync(new URL('./lib/server.json', import.meta.url), 'utf8'));
|
|
13
|
+
|
|
14
|
+
await runBridge({
|
|
15
|
+
ensure: () => ensureRunning({ log }),
|
|
16
|
+
token: () => readToken(),
|
|
17
|
+
log,
|
|
18
|
+
offline,
|
|
19
|
+
version: VERSION,
|
|
20
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartspec/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The smartspec command: installs the SmartSpec app, sets up Claude Desktop and Claude Code to use it, checks the connection, and runs SmartSpec's MCP connection for agents set up with npx.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"smartspec": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.js",
|
|
12
|
+
"mcp.js",
|
|
13
|
+
"lib/"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/tw050x/smartspec.git",
|
|
21
|
+
"directory": "packages/cli"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/tw050x/smartspec",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"planning",
|
|
27
|
+
"contracts",
|
|
28
|
+
"claude"
|
|
29
|
+
]
|
|
30
|
+
}
|