mcp-surveys-cli 0.1.0 → 0.2.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 CHANGED
@@ -3,9 +3,13 @@
3
3
  Tiny dependency-free CLI for the hosted mcp-surveys API.
4
4
 
5
5
  ```bash
6
+ npx mcp-surveys-cli install-skill
7
+ npx mcp-surveys-cli template decision > survey.json
6
8
  npx mcp-surveys-cli schema
7
9
  npx mcp-surveys-cli create survey.json
10
+ npx mcp-surveys-cli wait <survey_id> <result_token> --format markdown
8
11
  npx mcp-surveys-cli answers <survey_id> <result_token>
12
+ npx mcp-surveys-cli stats
9
13
  ```
10
14
 
11
15
  Use `MCP_SURVEYS_BASE_URL` or `--base-url` to point at another instance.
@@ -1,10 +1,102 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
4
6
  import process from "node:process";
5
7
  import { fileURLToPath } from "node:url";
6
8
 
7
9
  const DEFAULT_BASE_URL = "https://mcp.voevoda-sailing.ru";
10
+ const SKILL_NAME = "mcp-surveys-cli";
11
+ const SKILL_TEXT = `---
12
+ name: mcp-surveys-cli
13
+ description: Use when an agent can run shell commands and needs short-lived human surveys through uvx or npx.
14
+ ---
15
+
16
+ # mcp-surveys-cli
17
+
18
+ Use the CLI plus this skill as the default setup. It avoids remote MCP setup,
19
+ keeps context small, and works in any agent host that can run \`uvx\` or \`npx\`.
20
+
21
+ Default hosted instance:
22
+
23
+ \`\`\`bash
24
+ uvx mcp-surveys-cli schema
25
+ npx mcp-surveys-cli schema
26
+ uvx mcp-surveys-cli template decision > survey.json
27
+ uvx mcp-surveys-cli create survey.json
28
+ uvx mcp-surveys-cli wait <survey_id> <result_token> --format markdown
29
+ uvx mcp-surveys-cli answers <survey_id> <result_token>
30
+ \`\`\`
31
+
32
+ \`create\` prints \`survey_id\`, \`public_url\`, \`result_token\`, and expiry data. Send only \`public_url\` to the human. Keep \`result_token\` private.
33
+
34
+ Use \`MCP_SURVEYS_BASE_URL\` or \`--base-url\` for another instance.
35
+
36
+ Prefer structured buttons, ranking, matching, scale, and \`binary_tradeoff\`; use \`text\` only when the answer cannot fit those shapes.
37
+ `;
38
+ const TEMPLATES = {
39
+ decision: {
40
+ title: "Decision capture",
41
+ description: "Quick button ritual. Link expires in one hour.",
42
+ questions: [
43
+ {
44
+ id: "choice",
45
+ type: "single_choice",
46
+ prompt: "Which option should we choose?",
47
+ required: true,
48
+ allow_custom: true,
49
+ options: [{ id: "a", text: "Option A" }, { id: "b", text: "Option B" }],
50
+ },
51
+ {
52
+ id: "confidence",
53
+ type: "scale",
54
+ prompt: "How confident are you?",
55
+ required: true,
56
+ min: 0,
57
+ max: 100,
58
+ step: 5,
59
+ min_label: "Guess",
60
+ max_label: "Certain",
61
+ },
62
+ ],
63
+ },
64
+ confidence: {
65
+ title: "Confidence check",
66
+ description: "Collect confidence without summoning a paragraph.",
67
+ questions: [
68
+ {
69
+ id: "confidence",
70
+ type: "scale",
71
+ prompt: "How confident are you?",
72
+ required: true,
73
+ min: 0,
74
+ max: 100,
75
+ step: 5,
76
+ min_label: "Guess",
77
+ max_label: "Certain",
78
+ },
79
+ ],
80
+ },
81
+ prioritization: {
82
+ title: "Priority stack",
83
+ description: "Make the human sort the tiny pile.",
84
+ questions: [
85
+ {
86
+ id: "priorities",
87
+ type: "ranking",
88
+ prompt: "Rank these by priority.",
89
+ required: true,
90
+ allow_custom: true,
91
+ options: [
92
+ { id: "speed", text: "Move fast" },
93
+ { id: "risk", text: "Reduce risk" },
94
+ { id: "quality", text: "Improve quality" },
95
+ ],
96
+ },
97
+ ],
98
+ },
99
+ };
8
100
 
9
101
  function usage() {
10
102
  return `Usage: mcp-surveys-cli [--base-url URL] <command>
@@ -17,6 +109,10 @@ Commands:
17
109
  answers <survey_id> <result_token>
18
110
  question <survey_id> <result_token> <question_id>
19
111
  export <survey_id> <result_token> [--format markdown|json]
112
+ wait <survey_id> <result_token> [--timeout seconds] [--interval seconds] [--format markdown|json]
113
+ template <decision|confidence|prioritization>
114
+ install-skill [--target agents|claude|both] [--force]
115
+ stats
20
116
  schema`;
21
117
  }
22
118
 
@@ -64,10 +160,71 @@ function writeJson(write, value) {
64
160
  write(`${JSON.stringify(value)}\n`);
65
161
  }
66
162
 
163
+ function option(args, name, fallback) {
164
+ const index = args.indexOf(name);
165
+ return index === -1 ? fallback : args[index + 1];
166
+ }
167
+
168
+ function withoutOptions(args, names) {
169
+ const cleaned = [];
170
+ for (let index = 0; index < args.length; index += 1) {
171
+ if (names.includes(args[index])) index += 1;
172
+ else if (args[index] !== "--force") cleaned.push(args[index]);
173
+ }
174
+ return cleaned;
175
+ }
176
+
177
+ async function installSkill(args, home) {
178
+ const target = option(args, "--target", "agents");
179
+ const force = args.includes("--force");
180
+ const homes = {
181
+ agents: join(home, ".agents", "skills", SKILL_NAME),
182
+ claude: join(home, ".claude", "skills", SKILL_NAME),
183
+ };
184
+ const selected = target === "both" ? [homes.agents, homes.claude] : [homes[target]];
185
+ if (!selected[0]) throw new Error("unknown target; use agents, claude, or both");
186
+ const installed = [];
187
+ for (const directory of selected) {
188
+ const path = join(directory, "SKILL.md");
189
+ let current = null;
190
+ try {
191
+ current = await readFile(path, "utf8");
192
+ } catch {}
193
+ if (current !== null && current !== SKILL_TEXT && !force) {
194
+ throw new Error(`${path} already exists; use --force to replace it`);
195
+ }
196
+ await mkdir(directory, { recursive: true });
197
+ await writeFile(path, SKILL_TEXT, "utf8");
198
+ installed.push(path);
199
+ }
200
+ return { installed };
201
+ }
202
+
203
+ async function waitForCompletion(baseUrl, args, request, sleep) {
204
+ const cleaned = withoutOptions(args, ["--timeout", "--interval", "--format"]);
205
+ const [surveyId, resultToken] = cleaned;
206
+ const timeout = Number(option(args, "--timeout", "3600"));
207
+ const interval = Number(option(args, "--interval", "5"));
208
+ const format = option(args, "--format", "markdown");
209
+ const deadline = Date.now() + timeout * 1000;
210
+ let summary = null;
211
+ while (true) {
212
+ summary = await request("POST", endpoint(baseUrl, `/api/agent/surveys/${surveyId}/summary`), { result_token: resultToken });
213
+ if (summary.status === "completed") {
214
+ return request("POST", endpoint(baseUrl, `/api/agent/surveys/${surveyId}/export`), { result_token: resultToken, format }, true);
215
+ }
216
+ const remaining = deadline - Date.now();
217
+ if (remaining <= 0) throw new Error(`timed out waiting for completion: ${JSON.stringify(summary)}`);
218
+ await sleep(Math.min(Math.max(interval, 0.1) * 1000, remaining));
219
+ }
220
+ }
221
+
67
222
  export async function run(argv, io = {}) {
68
223
  const write = io.write || ((value) => process.stdout.write(value));
69
224
  const error = io.error || ((value) => process.stderr.write(value));
70
225
  const request = io.request || httpRequest;
226
+ const sleep = io.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
227
+ const home = io.home || homedir();
71
228
  const stdin = io.stdin ?? "";
72
229
  const { baseUrl, command, args } = parse(argv);
73
230
 
@@ -94,6 +251,15 @@ export async function run(argv, io = {}) {
94
251
  const formatIndex = args.indexOf("--format");
95
252
  const format = formatIndex === -1 ? "markdown" : args[formatIndex + 1];
96
253
  write(await request("POST", endpoint(baseUrl, `/api/agent/surveys/${args[0]}/export`), { result_token: args[1], format }, true));
254
+ } else if (command === "wait") {
255
+ write(await waitForCompletion(baseUrl, args, request, sleep));
256
+ } else if (command === "template") {
257
+ if (!TEMPLATES[args[0]]) throw new Error(`unknown template: ${args[0]}`);
258
+ writeJson(write, TEMPLATES[args[0]]);
259
+ } else if (command === "install-skill") {
260
+ writeJson(write, await installSkill(args, home));
261
+ } else if (command === "stats") {
262
+ writeJson(write, await request("GET", endpoint(baseUrl, "/api/agent/stats")));
97
263
  } else if (command === "schema") {
98
264
  writeJson(write, await request("GET", endpoint(baseUrl, "/api/agent/question-schema")));
99
265
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-surveys-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Tiny npx CLI for the hosted mcp-surveys API.",
5
5
  "type": "module",
6
6
  "bin": {