@vovy-ai/go 0.3.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +125 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,7 +18,7 @@ Installed globally (`npm i -g @vovy-ai/go`), the command is just `vovy`.
18
18
 
19
19
  `npx @vovy-ai/go doctor` also reports a deterministic "always-on token footprint" — every installed skill file plus every registered MCP tool definition, the tokens a session pays whether or not any of it ever fires — and tells you which Context Engine backend your project gets: `typescript` (scope-aware, resolved through your project's own TypeScript) or `tree-sitter` (name-matching fallback), with the one-line upgrade if you're on the fallback.
20
20
 
21
- It's MIT-licensed and runs entirely on your machine — no account, no API key, nothing phoning home.
21
+ It's MIT-licensed and runs entirely on your machine — no account, no API key, no background telemetry. The only network call in the CLI is a one-time, two-question install survey that sends answers **only if you type them**; skip it (or set `VOVY_NO_SURVEY=1`) and nothing is ever sent.
22
22
 
23
23
  ## What it installs
24
24
 
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { realpathSync } from "fs";
4
+ import { readFileSync as readFileSync3, realpathSync } from "fs";
5
+ import { dirname as dirname2, join as join4 } from "path";
6
+ import { createInterface } from "readline/promises";
5
7
  import { fileURLToPath } from "url";
6
8
  import { parseArgs } from "util";
7
9
  import { ADAPTERS as ADAPTERS2 } from "@vovy-ai/host-detect";
@@ -164,8 +166,109 @@ function countMemoryEntries(cwd) {
164
166
  return count;
165
167
  }
166
168
 
169
+ // src/commands/survey.ts
170
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
171
+ import { dirname, join as join3 } from "path";
172
+ var SURVEY_URL = "https://swrrttvhfnsiesmkbwia.supabase.co/rest/v1/vovy_cli_survey";
173
+ var SURVEY_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InN3cnJ0dHZoZm5zaWVzbWtid2lhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDQ0ODI1MjQsImV4cCI6MjA2MDA1ODUyNH0.5BMG6KY-49J_Gz8Qg02Hf3DEHgiy3evtiXqP77non_A";
174
+ var SOURCES = [
175
+ { key: "1", value: "x_twitter", label: "X / Twitter" },
176
+ { key: "2", value: "github", label: "GitHub" },
177
+ { key: "3", value: "friend", label: "A friend or colleague" },
178
+ { key: "4", value: "search", label: "Search" },
179
+ { key: "5", value: "youtube", label: "YouTube" },
180
+ { key: "6", value: "other", label: "Other" }
181
+ ];
182
+ function statePath(env) {
183
+ return join3(env.home, ".vovy", "cli-state.json");
184
+ }
185
+ function readState(env) {
186
+ try {
187
+ return JSON.parse(readFileSync(statePath(env), "utf8"));
188
+ } catch {
189
+ return {};
190
+ }
191
+ }
192
+ function markSurveyOffered(env) {
193
+ const path = statePath(env);
194
+ mkdirSync(dirname(path), { recursive: true });
195
+ writeFileSync(path, `${JSON.stringify({ ...readState(env), surveyOffered: true }, null, 2)}
196
+ `);
197
+ }
198
+ function shouldOfferSurvey(env, isTty) {
199
+ if (!isTty) return false;
200
+ if (process.env.VOVY_NO_SURVEY) return false;
201
+ if (process.env.CI) return false;
202
+ if (readState(env).surveyOffered) return false;
203
+ return true;
204
+ }
205
+ function buildSurveyPayload(answers, cliVersion2) {
206
+ if (answers.source === void 0 && answers.rating === void 0) return null;
207
+ const payload = { cli_version: cliVersion2.slice(0, 20) };
208
+ if (answers.source !== void 0) payload.source = answers.source;
209
+ if (answers.source === "other" && answers.sourceOther) {
210
+ payload.source_other = answers.sourceOther.slice(0, 120);
211
+ }
212
+ if (answers.rating !== void 0) payload.rating = answers.rating;
213
+ return payload;
214
+ }
215
+ async function askSurveyQuestions(rl) {
216
+ const answers = {};
217
+ const sourceMenu = SOURCES.map((s) => ` [${s.key}] ${s.label}`).join("\n");
218
+ const sourceInput = (await rl.question(`
219
+ How did you hear about Vovy?
220
+ ${sourceMenu}
221
+ > `)).trim();
222
+ const source = SOURCES.find((s) => s.key === sourceInput);
223
+ if (source) {
224
+ answers.source = source.value;
225
+ if (source.value === "other") {
226
+ const detail = (await rl.question("Where? (optional) > ")).trim();
227
+ if (detail) answers.sourceOther = detail;
228
+ }
229
+ }
230
+ const ratingInput = (await rl.question("\nExperience so far, 1 (rough) to 5 (great)? > ")).trim();
231
+ const rating = Number.parseInt(ratingInput, 10);
232
+ if (Number.isInteger(rating) && rating >= 1 && rating <= 5) answers.rating = rating;
233
+ return answers;
234
+ }
235
+ async function sendSurvey(payload, url = SURVEY_URL) {
236
+ try {
237
+ const response = await fetch(url, {
238
+ method: "POST",
239
+ headers: { apikey: SURVEY_ANON_KEY, "Content-Type": "application/json" },
240
+ body: JSON.stringify(payload),
241
+ signal: AbortSignal.timeout(2500)
242
+ });
243
+ return response.ok;
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+ async function offerSurvey(env, cliVersion2, makeInterface) {
249
+ markSurveyOffered(env);
250
+ console.log(
251
+ "\nOne-time question (never asked again \u2014 press Enter to skip either one).\nIf you answer, the answers and the CLI version are sent to the Vovy team; skipping sends nothing."
252
+ );
253
+ const rl = makeInterface();
254
+ try {
255
+ const answers = await askSurveyQuestions(rl);
256
+ const payload = buildSurveyPayload(answers, cliVersion2);
257
+ if (!payload) {
258
+ console.log("Skipped \u2014 nothing was sent.");
259
+ return;
260
+ }
261
+ const sent = await sendSurvey(payload);
262
+ console.log(
263
+ sent ? "Thanks \u2014 that helps more than you'd think." : "Thanks! (Couldn't reach the survey endpoint \u2014 nothing else to do.)"
264
+ );
265
+ } finally {
266
+ rl.close();
267
+ }
268
+ }
269
+
167
270
  // src/commands/uninstall.ts
168
- import { existsSync as existsSync2, readFileSync, rmSync, writeFileSync } from "fs";
271
+ import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
169
272
  import {
170
273
  removeCodexMcpEntry,
171
274
  removeJsonMcpEntry
@@ -180,19 +283,19 @@ function runUninstall(opts) {
180
283
  const removedSkillPaths = [];
181
284
  for (const skill of SKILL_MANIFEST) {
182
285
  const path = adapter.skillFilePath(opts.env, scope, skill.id);
183
- if (existsSync2(path)) {
286
+ if (existsSync3(path)) {
184
287
  removedSkillPaths.push(path);
185
288
  if (!dryRun) rmSync(path, { force: true });
186
289
  }
187
290
  }
188
291
  let removedMcpEntry = null;
189
292
  const mcpPath = adapter.mcpConfigPath(opts.env, scope);
190
- if (mcpPath && existsSync2(mcpPath)) {
191
- const existing = readFileSync(mcpPath, "utf8");
293
+ if (mcpPath && existsSync3(mcpPath)) {
294
+ const existing = readFileSync2(mcpPath, "utf8");
192
295
  const next = adapter.id === "codex" ? removeCodexMcpEntry(existing, VOVY_ID) : removeJsonMcpEntry(existing, VOVY_ID);
193
296
  if (next !== null) {
194
297
  removedMcpEntry = mcpPath;
195
- if (!dryRun) writeFileSync(mcpPath, next, "utf8");
298
+ if (!dryRun) writeFileSync2(mcpPath, next, "utf8");
196
299
  }
197
300
  }
198
301
  return { adapter, removedSkillPaths, removedMcpEntry };
@@ -206,6 +309,14 @@ function realEnv() {
206
309
  }
207
310
 
208
311
  // src/index.ts
312
+ function cliVersion() {
313
+ try {
314
+ const pkgPath = join4(dirname2(fileURLToPath(import.meta.url)), "..", "package.json");
315
+ return JSON.parse(readFileSync3(pkgPath, "utf8")).version ?? "unknown";
316
+ } catch {
317
+ return "unknown";
318
+ }
319
+ }
209
320
  var HELP = `Vovy \u2014 drop-in skills for vibe coding safely with the AI tool you already use. Run via \`npx @vovy-ai/go\` (or \`vovy\` if installed globally).
210
321
 
211
322
  Usage:
@@ -251,7 +362,7 @@ function actionSymbol(action) {
251
362
  if (action === "created") return "+";
252
363
  return "~";
253
364
  }
254
- function cmdInstall(argv) {
365
+ async function cmdInstall(argv) {
255
366
  const flags = parseCommonFlags(argv);
256
367
  if (flags.help) return console.log(HELP);
257
368
  const env = realEnv();
@@ -288,6 +399,13 @@ Add to ~/.claude/settings.json:
288
399
  (Faster variant: npm i -g @vovy-ai/go, then use "vovy statusline" as the command.)`
289
400
  );
290
401
  }
402
+ if (!flags.dryRun && shouldOfferSurvey(env, process.stdout.isTTY === true)) {
403
+ await offerSurvey(
404
+ env,
405
+ cliVersion(),
406
+ () => createInterface({ input: process.stdin, output: process.stdout })
407
+ );
408
+ }
291
409
  }
292
410
  function cmdStatusline() {
293
411
  console.log(buildStatusline(realEnv()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vovy-ai/go",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Drop-in MCP server + skill pack that teaches non-technical founders to vibe code safely — free forever. npx @vovy-ai/go install",
5
5
  "keywords": [
6
6
  "vibe-coding",