mcp-surveys-cli 0.1.0 → 0.2.1

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,14 @@
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.
16
+ Outdated-version notices go to stderr and never block the command.
@@ -1,10 +1,103 @@
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 VERSION = "0.2.1";
11
+ const SKILL_NAME = "mcp-surveys-cli";
12
+ const SKILL_TEXT = `---
13
+ name: mcp-surveys-cli
14
+ description: Use when an agent can run shell commands and needs short-lived human surveys through uvx or npx.
15
+ ---
16
+
17
+ # mcp-surveys-cli
18
+
19
+ Use the CLI plus this skill as the default setup. It avoids remote MCP setup,
20
+ keeps context small, and works in any agent host that can run \`uvx\` or \`npx\`.
21
+
22
+ Default hosted instance:
23
+
24
+ \`\`\`bash
25
+ uvx mcp-surveys-cli schema
26
+ npx mcp-surveys-cli schema
27
+ uvx mcp-surveys-cli template decision > survey.json
28
+ uvx mcp-surveys-cli create survey.json
29
+ uvx mcp-surveys-cli wait <survey_id> <result_token> --format markdown
30
+ uvx mcp-surveys-cli answers <survey_id> <result_token>
31
+ \`\`\`
32
+
33
+ \`create\` prints \`survey_id\`, \`public_url\`, \`result_token\`, and expiry data. Send only \`public_url\` to the human. Keep \`result_token\` private.
34
+
35
+ Use \`MCP_SURVEYS_BASE_URL\` or \`--base-url\` for another instance.
36
+
37
+ Prefer structured buttons, ranking, matching, scale, and \`binary_tradeoff\`; use \`text\` only when the answer cannot fit those shapes.
38
+ `;
39
+ const TEMPLATES = {
40
+ decision: {
41
+ title: "Decision capture",
42
+ description: "Quick button ritual. Link expires in one hour.",
43
+ questions: [
44
+ {
45
+ id: "choice",
46
+ type: "single_choice",
47
+ prompt: "Which option should we choose?",
48
+ required: true,
49
+ allow_custom: true,
50
+ options: [{ id: "a", text: "Option A" }, { id: "b", text: "Option B" }],
51
+ },
52
+ {
53
+ id: "confidence",
54
+ type: "scale",
55
+ prompt: "How confident are you?",
56
+ required: true,
57
+ min: 0,
58
+ max: 100,
59
+ step: 5,
60
+ min_label: "Guess",
61
+ max_label: "Certain",
62
+ },
63
+ ],
64
+ },
65
+ confidence: {
66
+ title: "Confidence check",
67
+ description: "Collect confidence without summoning a paragraph.",
68
+ questions: [
69
+ {
70
+ id: "confidence",
71
+ type: "scale",
72
+ prompt: "How confident are you?",
73
+ required: true,
74
+ min: 0,
75
+ max: 100,
76
+ step: 5,
77
+ min_label: "Guess",
78
+ max_label: "Certain",
79
+ },
80
+ ],
81
+ },
82
+ prioritization: {
83
+ title: "Priority stack",
84
+ description: "Make the human sort the tiny pile.",
85
+ questions: [
86
+ {
87
+ id: "priorities",
88
+ type: "ranking",
89
+ prompt: "Rank these by priority.",
90
+ required: true,
91
+ allow_custom: true,
92
+ options: [
93
+ { id: "speed", text: "Move fast" },
94
+ { id: "risk", text: "Reduce risk" },
95
+ { id: "quality", text: "Improve quality" },
96
+ ],
97
+ },
98
+ ],
99
+ },
100
+ };
8
101
 
9
102
  function usage() {
10
103
  return `Usage: mcp-surveys-cli [--base-url URL] <command>
@@ -17,6 +110,10 @@ Commands:
17
110
  answers <survey_id> <result_token>
18
111
  question <survey_id> <result_token> <question_id>
19
112
  export <survey_id> <result_token> [--format markdown|json]
113
+ wait <survey_id> <result_token> [--timeout seconds] [--interval seconds] [--format markdown|json]
114
+ template <decision|confidence|prioritization>
115
+ install-skill [--target agents|claude|both] [--force]
116
+ stats
20
117
  schema`;
21
118
  }
22
119
 
@@ -43,6 +140,46 @@ function endpoint(baseUrl, path) {
43
140
  return `${baseUrl.replace(/\/+$/, "")}${path}`;
44
141
  }
45
142
 
143
+ function versionParts(version) {
144
+ return version.split(".").filter((part) => /^\d+$/.test(part)).map(Number);
145
+ }
146
+
147
+ function newerThan(left, right) {
148
+ const a = versionParts(left);
149
+ const b = versionParts(right);
150
+ const length = Math.max(a.length, b.length);
151
+ for (let i = 0; i < length; i += 1) {
152
+ if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
153
+ }
154
+ return false;
155
+ }
156
+
157
+ async function latestVersion() {
158
+ const controller = new AbortController();
159
+ const timeout = setTimeout(() => controller.abort(), 750);
160
+ try {
161
+ const response = await fetch("https://registry.npmjs.org/mcp-surveys-cli/latest", { signal: controller.signal });
162
+ return (await response.json()).version;
163
+ } finally {
164
+ clearTimeout(timeout);
165
+ }
166
+ }
167
+
168
+ async function warnIfOutdated(error, latestVersionFn, version) {
169
+ if (process.env.MCP_SURVEYS_SKIP_VERSION_CHECK) return;
170
+ try {
171
+ const latest = await latestVersionFn();
172
+ if (newerThan(latest, version)) {
173
+ error(
174
+ `notice: mcp-surveys-cli ${version} is outdated; latest is ${latest}. ` +
175
+ "LLM agent: update now before long-lived workflows get weird. " +
176
+ "Use `uvx --refresh-package mcp-surveys-cli mcp-surveys-cli ...` " +
177
+ "or `npx mcp-surveys-cli@latest ...`.\n",
178
+ );
179
+ }
180
+ } catch {}
181
+ }
182
+
46
183
  async function httpRequest(method, url, body, raw = false) {
47
184
  const response = await fetch(url, {
48
185
  method,
@@ -64,12 +201,76 @@ function writeJson(write, value) {
64
201
  write(`${JSON.stringify(value)}\n`);
65
202
  }
66
203
 
204
+ function option(args, name, fallback) {
205
+ const index = args.indexOf(name);
206
+ return index === -1 ? fallback : args[index + 1];
207
+ }
208
+
209
+ function withoutOptions(args, names) {
210
+ const cleaned = [];
211
+ for (let index = 0; index < args.length; index += 1) {
212
+ if (names.includes(args[index])) index += 1;
213
+ else if (args[index] !== "--force") cleaned.push(args[index]);
214
+ }
215
+ return cleaned;
216
+ }
217
+
218
+ async function installSkill(args, home) {
219
+ const target = option(args, "--target", "agents");
220
+ const force = args.includes("--force");
221
+ const homes = {
222
+ agents: join(home, ".agents", "skills", SKILL_NAME),
223
+ claude: join(home, ".claude", "skills", SKILL_NAME),
224
+ };
225
+ const selected = target === "both" ? [homes.agents, homes.claude] : [homes[target]];
226
+ if (!selected[0]) throw new Error("unknown target; use agents, claude, or both");
227
+ const installed = [];
228
+ for (const directory of selected) {
229
+ const path = join(directory, "SKILL.md");
230
+ let current = null;
231
+ try {
232
+ current = await readFile(path, "utf8");
233
+ } catch {}
234
+ if (current !== null && current !== SKILL_TEXT && !force) {
235
+ throw new Error(`${path} already exists; use --force to replace it`);
236
+ }
237
+ await mkdir(directory, { recursive: true });
238
+ await writeFile(path, SKILL_TEXT, "utf8");
239
+ installed.push(path);
240
+ }
241
+ return { installed };
242
+ }
243
+
244
+ async function waitForCompletion(baseUrl, args, request, sleep) {
245
+ const cleaned = withoutOptions(args, ["--timeout", "--interval", "--format"]);
246
+ const [surveyId, resultToken] = cleaned;
247
+ const timeout = Number(option(args, "--timeout", "3600"));
248
+ const interval = Number(option(args, "--interval", "5"));
249
+ const format = option(args, "--format", "markdown");
250
+ const deadline = Date.now() + timeout * 1000;
251
+ let summary = null;
252
+ while (true) {
253
+ summary = await request("POST", endpoint(baseUrl, `/api/agent/surveys/${surveyId}/summary`), { result_token: resultToken });
254
+ if (summary.status === "completed") {
255
+ return request("POST", endpoint(baseUrl, `/api/agent/surveys/${surveyId}/export`), { result_token: resultToken, format }, true);
256
+ }
257
+ const remaining = deadline - Date.now();
258
+ if (remaining <= 0) throw new Error(`timed out waiting for completion: ${JSON.stringify(summary)}`);
259
+ await sleep(Math.min(Math.max(interval, 0.1) * 1000, remaining));
260
+ }
261
+ }
262
+
67
263
  export async function run(argv, io = {}) {
68
264
  const write = io.write || ((value) => process.stdout.write(value));
69
265
  const error = io.error || ((value) => process.stderr.write(value));
70
266
  const request = io.request || httpRequest;
267
+ const latestVersionFn = io.latestVersion || latestVersion;
268
+ const version = io.version || VERSION;
269
+ const sleep = io.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
270
+ const home = io.home || homedir();
71
271
  const stdin = io.stdin ?? "";
72
272
  const { baseUrl, command, args } = parse(argv);
273
+ await warnIfOutdated(error, latestVersionFn, version);
73
274
 
74
275
  try {
75
276
  if (command === "create") {
@@ -94,6 +295,15 @@ export async function run(argv, io = {}) {
94
295
  const formatIndex = args.indexOf("--format");
95
296
  const format = formatIndex === -1 ? "markdown" : args[formatIndex + 1];
96
297
  write(await request("POST", endpoint(baseUrl, `/api/agent/surveys/${args[0]}/export`), { result_token: args[1], format }, true));
298
+ } else if (command === "wait") {
299
+ write(await waitForCompletion(baseUrl, args, request, sleep));
300
+ } else if (command === "template") {
301
+ if (!TEMPLATES[args[0]]) throw new Error(`unknown template: ${args[0]}`);
302
+ writeJson(write, TEMPLATES[args[0]]);
303
+ } else if (command === "install-skill") {
304
+ writeJson(write, await installSkill(args, home));
305
+ } else if (command === "stats") {
306
+ writeJson(write, await request("GET", endpoint(baseUrl, "/api/agent/stats")));
97
307
  } else if (command === "schema") {
98
308
  writeJson(write, await request("GET", endpoint(baseUrl, "/api/agent/question-schema")));
99
309
  } 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.1",
4
4
  "description": "Tiny npx CLI for the hosted mcp-surveys API.",
5
5
  "type": "module",
6
6
  "bin": {