@vibecodr/cli 0.1.9 → 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/CHANGELOG.md CHANGED
@@ -1,17 +1,25 @@
1
- # Changelog
2
-
3
- ## Unreleased
4
-
5
- - rename the npm package to `@vibecodr/cli` while keeping `vibecodr` as the primary executable and `vibecodr-mcp` as a compatibility alias
6
-
7
- ## 0.1.8
8
-
9
- - report the actual package version in MCP client metadata instead of a stale hardcoded value
10
- - publish the CLI after redeploying the hosted Vibecodr MCP gateway
11
-
12
- ## 0.1.7
13
-
14
- - add the `pulse-setup` command for live Pulse setup guidance
1
+ # Changelog
2
+
3
+ ## 0.2.1
4
+
5
+ - preserve encrypted offline sessions when refresh fails because the authorization server is temporarily unavailable
6
+ - continue clearing stored auth on OAuth `invalid_grant` so revoked or expired refresh tokens fail closed
7
+
8
+ ## 0.2.0
9
+
10
+ - rename the npm package to `@vibecodr/cli` while keeping `vibecodr` as the primary executable and `vibecodr-mcp` as a compatibility alias
11
+ - add the hardened `pulse` lifecycle command group for list/get/status/run/archive/restore plus create/deploy aliases
12
+ - redact source, descriptor, token, secret, and inline file-content fields from CLI-displayed MCP arguments and results
13
+ - require explicit confirmation for known mutating MCP tools when invoked through the generic `call` command
14
+
15
+ ## 0.1.8
16
+
17
+ - report the actual package version in MCP client metadata instead of a stale hardcoded value
18
+ - publish the CLI after redeploying the hosted Vibecodr MCP gateway
19
+
20
+ ## 0.1.7
21
+
22
+ - add the `pulse-setup` command for live Pulse setup guidance
15
23
  - align CLI Pulse setup docs with the gateway runtime contract for policy-bound secrets, Stripe-first webhook helper guidance, generic HMAC presets, and provider-scoped connections
16
24
  - refresh release-lock coverage for current in-range MCP SDK, keyring, and Node type packages before publishing
17
25
 
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
 
5
5
 
6
- # Vibecodr CLI
6
+ # Vibecodr CLI
7
7
 
8
- Direct terminal client for the hosted Vibecodr MCP server.
8
+ Direct terminal client for the hosted Vibecodr MCP server.
9
9
 
10
10
  This repository is intentionally separate from the PolyForm-licensed server implementation. The CLI is a client and installer surface, not a second server. It talks to the same hosted Vibecodr MCP gateway used by Codex, Cursor, VS Code, Windsurf, ChatGPT, and other MCP-capable clients.
11
11
 
@@ -25,26 +25,28 @@ Currently implemented command surface:
25
25
  - `tools`
26
26
  - `call`
27
27
  - `pulse-setup`
28
+ - `pulse-publish`
29
+ - `pulse`
28
30
  - `doctor`
29
31
  - `config`
30
32
  - `install`
31
33
  - `uninstall`
32
34
 
33
- Primary executable:
34
-
35
- - `vibecodr`
36
-
37
- Compatibility alias:
38
-
39
- - `vibecodr-mcp`
40
-
41
- Published package:
42
-
43
- - `@vibecodr/cli`
44
-
45
- Legacy package compatibility:
46
-
47
- - `@vibecodr/mcp`
35
+ Primary executable:
36
+
37
+ - `vibecodr`
38
+
39
+ Compatibility alias:
40
+
41
+ - `vibecodr-mcp`
42
+
43
+ Published package:
44
+
45
+ - `@vibecodr/cli`
46
+
47
+ Legacy package compatibility:
48
+
49
+ - `@vibecodr/mcp`
48
50
 
49
51
  The runtime path talks directly to `https://openai.vibecodr.space/mcp`. Editor installers are not part of the runtime path.
50
52
 
@@ -54,6 +56,8 @@ The official production auth path is now committed in package code through the s
54
56
 
55
57
  - `https://openai.vibecodr.space/.well-known/oauth-client/vibecodr-mcp.json`
56
58
 
59
+ Pulse lifecycle commands use the hosted MCP gateway as the authority boundary. The CLI redacts source, descriptor, token, secret, and inline file-content fields from local output, but the server still enforces OAuth, owner scoping, confirmation, no-delete policy, and model-safe response shaping for direct MCP callers.
60
+
57
61
  Documentation:
58
62
 
59
63
  - [docs/auth.md](docs/auth.md)
@@ -23,6 +23,12 @@ function computeExpiresAt(expiresIn) {
23
23
  return undefined;
24
24
  return new Date(Date.now() + expiresIn * 1000).toISOString();
25
25
  }
26
+ function oauthErrorCode(error) {
27
+ if (!error || typeof error !== "object")
28
+ return undefined;
29
+ const code = error.errorCode;
30
+ return typeof code === "string" ? code : undefined;
31
+ }
26
32
  function normalizeServerUrlForSessionMatch(value) {
27
33
  if (!value)
28
34
  return undefined;
@@ -285,10 +291,17 @@ export class TokenManager {
285
291
  resource,
286
292
  ...(discovery.authorizationServerMetadata ? { metadata: discovery.authorizationServerMetadata } : {})
287
293
  }).catch(async (error) => {
288
- await this.secretStore.delete(profileName).catch(() => undefined);
289
- throw new CliError("auth.refresh_failed", "Failed to refresh the stored session.", EXIT_CODES.authFailed, {
294
+ const invalidGrant = oauthErrorCode(error) === "invalid_grant";
295
+ if (invalidGrant) {
296
+ await this.secretStore.delete(profileName).catch(() => undefined);
297
+ }
298
+ throw new CliError("auth.refresh_failed", invalidGrant
299
+ ? "The stored refresh token is invalid or expired."
300
+ : "Failed to refresh the stored session.", EXIT_CODES.authFailed, {
290
301
  cause: error,
291
- nextStep: "Run vibecodr login to re-authenticate."
302
+ nextStep: invalidGrant
303
+ ? "Run vibecodr login to re-authenticate."
304
+ : "Retry after the authorization server recovers. The stored offline session was preserved."
292
305
  });
293
306
  });
294
307
  const updated = {
@@ -2,9 +2,9 @@
2
2
  import { ConfigStore } from "../storage/config-store.js";
3
3
  import { SecretStore } from "../storage/secret-store.js";
4
4
  import { TokenManager } from "../auth/token-manager.js";
5
- import { McpRuntimeClient } from "../core/mcp-client.js";
5
+ import { CLIENT_INFO, McpRuntimeClient } from "../core/mcp-client.js";
6
6
  import { Output } from "../cli/output.js";
7
- import { parseGlobalOptions } from "../cli/parse.js";
7
+ import { isHelpToken, isVersionToken, parseGlobalOptions } from "../cli/parse.js";
8
8
  import { CliError, EXIT_CODES } from "../cli/errors.js";
9
9
  import { runLoginCommand } from "../commands/login.js";
10
10
  import { runLogoutCommand } from "../commands/logout.js";
@@ -17,6 +17,7 @@ import { runInstallCommand } from "../commands/install.js";
17
17
  import { runUninstallCommand } from "../commands/uninstall.js";
18
18
  import { runPulseSetupCommand } from "../commands/pulse-setup.js";
19
19
  import { runPulsePublishCommand } from "../commands/pulse-publish.js";
20
+ import { runPulseCommand } from "../commands/pulse.js";
20
21
  function helpText() {
21
22
  return [
22
23
  "vibecodr <command> [options]",
@@ -34,6 +35,7 @@ function helpText() {
34
35
  " config",
35
36
  " pulse-setup [--descriptor-setup-json <json> | --descriptor-setup-file <path>]",
36
37
  " pulse-publish --name <name> (--code <source> | --code-file <path>) --confirm",
38
+ " pulse <list|get|status|run|archive|restore|create|deploy>",
37
39
  " Publishes a standalone Pulse with private source/metadata visibility by default.",
38
40
  " The runtime URL is still public HTTP unless the Pulse code rejects callers.",
39
41
  "",
@@ -44,12 +46,19 @@ function helpText() {
44
46
  " --non-interactive"
45
47
  ].join("\n");
46
48
  }
49
+ function versionText() {
50
+ return String(CLIENT_INFO.version);
51
+ }
47
52
  async function main() {
48
53
  const { command, commandArgs, globalOptions } = parseGlobalOptions(process.argv.slice(2));
49
- if (!command || command === "--help" || command === "help") {
54
+ if (!command || isHelpToken(command)) {
50
55
  process.stdout.write(helpText() + "\n");
51
56
  return;
52
57
  }
58
+ if (isVersionToken(command)) {
59
+ process.stdout.write(versionText() + "\n");
60
+ return;
61
+ }
53
62
  const configStore = new ConfigStore();
54
63
  const secretStore = new SecretStore();
55
64
  const tokenManager = new TokenManager(configStore, secretStore);
@@ -97,6 +106,9 @@ async function main() {
97
106
  case "pulse-publish":
98
107
  await runPulsePublishCommand(commandArgs, context);
99
108
  return;
109
+ case "pulse":
110
+ await runPulseCommand(commandArgs, context);
111
+ return;
100
112
  default:
101
113
  throw new CliError("usage.command", `Unknown command: ${command}`, EXIT_CODES.usage);
102
114
  }
package/dist/cli/parse.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { CliError, EXIT_CODES } from "./errors.js";
2
2
  function normalizeFlagName(flag) {
3
- return flag.replace(/^--/, "");
3
+ return flag.replace(/^-+/, "");
4
+ }
5
+ export function isHelpToken(token) {
6
+ return token === "help" || token === "--help" || token === "-h" || token === "-help";
7
+ }
8
+ export function isVersionToken(token) {
9
+ return token === "--version" || token === "-v" || token === "-version";
4
10
  }
5
11
  export function parseFlags(args, options) {
6
12
  const valueFlags = new Set(options.valueFlags || []);
@@ -46,6 +52,10 @@ export function parseGlobalOptions(argv) {
46
52
  const token = argv[index];
47
53
  if (token === undefined)
48
54
  continue;
55
+ if (!command && (isHelpToken(token) || isVersionToken(token))) {
56
+ command = token;
57
+ continue;
58
+ }
49
59
  if (token === "--profile") {
50
60
  const value = argv[index + 1];
51
61
  if (!value)
@@ -3,7 +3,19 @@ import { promptText } from "../platform/prompt.js";
3
3
  import { parseFlags } from "../cli/parse.js";
4
4
  import { CliError, EXIT_CODES } from "../cli/errors.js";
5
5
  import { renderToolResult } from "../core/renderers.js";
6
+ import { redactForOutput } from "../core/redaction.js";
6
7
  import { promptObjectBySchema } from "../core/interactive-input.js";
8
+ import { showHelpIfRequested } from "./help.js";
9
+ const CONFIRMED_TOOL_NAMES = new Set([
10
+ "quick_publish_creation",
11
+ "publish_standalone_pulse",
12
+ "publish_draft_capsule",
13
+ "cancel_import_operation",
14
+ "update_live_vibe_metadata",
15
+ "run_pulse",
16
+ "archive_pulse",
17
+ "restore_pulse"
18
+ ]);
7
19
  function challengedScope(error) {
8
20
  if (!error.debugDetails || typeof error.debugDetails !== "object")
9
21
  return undefined;
@@ -87,9 +99,11 @@ async function listToolsWithRetry(context, allowLogin) {
87
99
  }
88
100
  }
89
101
  export async function runCallCommand(args, context) {
102
+ if (showHelpIfRequested(args, context, "Usage: vibecodr call <tool-name> [--input-json <json>] [--input-file <path>] [--stdin] [--interactive] [--no-login] [--confirm]"))
103
+ return;
90
104
  const { flags, positionals } = parseFlags(args, {
91
105
  valueFlags: ["input-json", "input-file"],
92
- booleanFlags: ["stdin", "interactive", "no-login"]
106
+ booleanFlags: ["stdin", "interactive", "no-login", "confirm"]
93
107
  });
94
108
  const toolName = positionals[0];
95
109
  if (!toolName) {
@@ -113,11 +127,17 @@ export async function runCallCommand(args, context) {
113
127
  }
114
128
  input = await promptObjectBySchema(promptText, toolName, tool.inputSchema);
115
129
  }
130
+ if (CONFIRMED_TOOL_NAMES.has(toolName) && flags["confirm"] !== true) {
131
+ throw new CliError("usage.confirmation_required", `Calling ${toolName} requires explicit confirmation. Re-run with --confirm after the user confirms.`, EXIT_CODES.usage);
132
+ }
133
+ if (CONFIRMED_TOOL_NAMES.has(toolName)) {
134
+ input = { ...input, confirmed: true };
135
+ }
116
136
  const { result } = await callToolWithRetry(context, toolName, input, !flags["no-login"]);
117
137
  context.output.success({
118
138
  schemaVersion: 1,
119
139
  tool: toolName,
120
- arguments: input,
121
- result
122
- }, [renderToolResult(result)]);
140
+ arguments: redactForOutput(input),
141
+ result: redactForOutput(result)
142
+ }, [renderToolResult(redactForOutput(result))]);
123
143
  }
@@ -1,6 +1,7 @@
1
1
  import { parseFlags } from "../cli/parse.js";
2
2
  import { CliError, EXIT_CODES } from "../cli/errors.js";
3
3
  import { defaultProfileConfig } from "../types/config.js";
4
+ import { showHelpIfRequested } from "./help.js";
4
5
  function updateProfileKey(profile, key, value) {
5
6
  switch (key) {
6
7
  case "server-url":
@@ -37,6 +38,8 @@ async function saveConfig(context, config) {
37
38
  await context.configStore.save(config);
38
39
  }
39
40
  export async function runConfigCommand(args, context) {
41
+ if (showHelpIfRequested(args, context, "Usage: vibecodr config path|show|set|unset|profile ..."))
42
+ return;
40
43
  const action = args[0];
41
44
  const config = await context.configStore.load();
42
45
  const currentProfileName = context.globalOptions.profile || config.currentProfile;
@@ -1,7 +1,10 @@
1
1
  import { parseFlags } from "../cli/parse.js";
2
2
  import { runDoctor } from "../doctor/run.js";
3
3
  import { EXIT_CODES } from "../cli/errors.js";
4
+ import { showHelpIfRequested } from "./help.js";
4
5
  export async function runDoctorCommand(args, context) {
6
+ if (showHelpIfRequested(args, context, "Usage: vibecodr doctor [--client <codex|cursor|vscode|windsurf>]"))
7
+ return;
5
8
  const { flags } = parseFlags(args, {
6
9
  valueFlags: ["client"]
7
10
  });
@@ -0,0 +1,7 @@
1
+ import { isHelpToken } from "../cli/parse.js";
2
+ export function showHelpIfRequested(args, _context, text) {
3
+ if (!args.some((arg) => isHelpToken(arg)))
4
+ return false;
5
+ process.stdout.write(`${text}\n`);
6
+ return true;
7
+ }
@@ -5,10 +5,13 @@ import { installCodex } from "../clients/codex.js";
5
5
  import { installCursor } from "../clients/cursor.js";
6
6
  import { installVsCode } from "../clients/vscode.js";
7
7
  import { installWindsurf } from "../clients/windsurf.js";
8
+ import { showHelpIfRequested } from "./help.js";
8
9
  function defaultName(serverUrl) {
9
10
  return serverUrl.includes("staging") ? "vibecodr-staging" : "vibecodr";
10
11
  }
11
12
  export async function runInstallCommand(args, context) {
13
+ if (showHelpIfRequested(args, context, "Usage: vibecodr install <codex|cursor|vscode|windsurf> [--scope user|project] [--path <dir>] [--name <server-name>] [--open-client] [--overwrite] [--dry-run]"))
14
+ return;
12
15
  const client = args[0];
13
16
  if (!client || !["codex", "cursor", "vscode", "windsurf"].includes(client)) {
14
17
  throw new CliError("usage.install_client", "Usage: install <codex|cursor|vscode|windsurf> [options]", EXIT_CODES.usage);
@@ -1,5 +1,8 @@
1
1
  import { parseFlags } from "../cli/parse.js";
2
+ import { showHelpIfRequested } from "./help.js";
2
3
  export async function runLoginCommand(args, context) {
4
+ if (showHelpIfRequested(args, context, "Usage: vibecodr login [--scope <oauth-scope>] [--registration auto|preregistered|cimd|dcr|manual] [--browser open|print] [--timeout-sec <n>]"))
5
+ return;
3
6
  const { flags } = parseFlags(args, {
4
7
  valueFlags: ["scope", "registration", "browser", "timeout-sec"]
5
8
  });
@@ -1,5 +1,8 @@
1
1
  import { parseFlags } from "../cli/parse.js";
2
+ import { showHelpIfRequested } from "./help.js";
2
3
  export async function runLogoutCommand(args, context) {
4
+ if (showHelpIfRequested(args, context, "Usage: vibecodr logout [--all] [--no-revoke]"))
5
+ return;
3
6
  const { flags } = parseFlags(args, {
4
7
  booleanFlags: ["all", "no-revoke"]
5
8
  });
@@ -3,6 +3,7 @@ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
3
  import { parseFlags } from "../cli/parse.js";
4
4
  import { renderToolResult } from "../core/renderers.js";
5
5
  import { callToolWithRetry } from "./call.js";
6
+ import { showHelpIfRequested } from "./help.js";
6
7
  const PUBLISH_STANDALONE_PULSE_TOOL_NAME = "publish_standalone_pulse";
7
8
  const PULSE_VISIBILITIES = new Set(["public", "unlisted", "private"]);
8
9
  const DEFAULT_PULSE_VISIBILITY = "private";
@@ -79,6 +80,8 @@ function redactPulsePublishArguments(input) {
79
80
  };
80
81
  }
81
82
  export async function runPulsePublishCommand(args, context) {
83
+ if (showHelpIfRequested(args, context, "Usage: vibecodr pulse-publish --name <name> (--code <source> | --code-file <path>) [--descriptor-json <json> | --descriptor-file <path>] [--slug <slug>] [--visibility public|unlisted|private] --confirm"))
84
+ return;
82
85
  const input = await parsePulsePublishInput(args);
83
86
  const { result } = await callToolWithRetry(context, PUBLISH_STANDALONE_PULSE_TOOL_NAME, input, true);
84
87
  context.output.success({
@@ -3,6 +3,7 @@ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
3
  import { parseFlags } from "../cli/parse.js";
4
4
  import { renderToolResult } from "../core/renderers.js";
5
5
  import { callToolWithRetry } from "./call.js";
6
+ import { showHelpIfRequested } from "./help.js";
6
7
  const PULSE_SETUP_TOOL_NAME = "get_pulse_setup_guidance";
7
8
  const PULSE_DESCRIPTOR_SOURCE_OF_TRUTH = "PulseDescriptor";
8
9
  function readStructuredContent(result) {
@@ -70,6 +71,8 @@ async function parsePulseSetupInput(args) {
70
71
  return {};
71
72
  }
72
73
  export async function runPulseSetupCommand(args, context) {
74
+ if (showHelpIfRequested(args, context, "Usage: vibecodr pulse-setup [--descriptor-setup-json <json> | --descriptor-setup-file <path>]"))
75
+ return;
73
76
  const input = await parsePulseSetupInput(args);
74
77
  const { result } = await callToolWithRetry(context, PULSE_SETUP_TOOL_NAME, input, true);
75
78
  assertDescriptorSetupGuidance(result, { expectsDescriptorSetup: Boolean(input["descriptorSetup"]) });
@@ -0,0 +1,145 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { isHelpToken, parseFlags } from "../cli/parse.js";
4
+ import { renderToolResult } from "../core/renderers.js";
5
+ import { redactForOutput } from "../core/redaction.js";
6
+ import { callToolWithRetry } from "./call.js";
7
+ import { runPulsePublishCommand } from "./pulse-publish.js";
8
+ const MAX_LIST_LIMIT = 25;
9
+ const PULSE_ACTIONS = {
10
+ get: { toolName: "get_pulse", requiresConfirm: false },
11
+ status: { toolName: "get_pulse_status", requiresConfirm: false },
12
+ run: { toolName: "run_pulse", requiresConfirm: true },
13
+ archive: { toolName: "archive_pulse", requiresConfirm: true },
14
+ restore: { toolName: "restore_pulse", requiresConfirm: true }
15
+ };
16
+ function pulseHelpText() {
17
+ return [
18
+ "Usage: vibecodr pulse <command> [options]",
19
+ "",
20
+ "Commands:",
21
+ " list [--limit <n>] [--offset <n>]",
22
+ " get <pulse-id>",
23
+ " status <pulse-id>",
24
+ " run <pulse-id> [--input-json <json> | --input-file <path>] --confirm",
25
+ " archive <pulse-id> --confirm",
26
+ " restore <pulse-id> --confirm",
27
+ " create --name <name> (--code <source> | --code-file <path>) --confirm",
28
+ " deploy --name <name> (--code <source> | --code-file <path>) --confirm",
29
+ "",
30
+ "Delete is intentionally not exposed by the CLI; archive a Pulse instead."
31
+ ].join("\n");
32
+ }
33
+ function parseBoundedInteger(raw, name, defaultValue, maxValue) {
34
+ if (raw === undefined)
35
+ return defaultValue;
36
+ const value = Number(raw);
37
+ if (!Number.isInteger(value) || value < 0) {
38
+ throw new CliError("usage.invalid_number", `${name} must be a non-negative integer.`, EXIT_CODES.usage);
39
+ }
40
+ return Math.min(value, maxValue);
41
+ }
42
+ function parsePulseId(raw) {
43
+ const pulseId = typeof raw === "string" ? raw.trim() : "";
44
+ if (!pulseId) {
45
+ throw new CliError("usage.pulse_id_required", "A Pulse id is required.", EXIT_CODES.usage);
46
+ }
47
+ if (pulseId.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(pulseId)) {
48
+ throw new CliError("usage.invalid_pulse_id", "Pulse id contains unsupported characters.", EXIT_CODES.usage);
49
+ }
50
+ return pulseId;
51
+ }
52
+ function parseJsonObject(raw, source) {
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(raw);
56
+ }
57
+ catch (error) {
58
+ throw new CliError("usage.invalid_json", `${source} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.usage);
59
+ }
60
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
61
+ throw new CliError("usage.invalid_json", `${source} must be a JSON object.`, EXIT_CODES.usage);
62
+ }
63
+ return parsed;
64
+ }
65
+ async function parseRunInput(flags) {
66
+ const hasInputJson = typeof flags["input-json"] === "string";
67
+ const hasInputFile = typeof flags["input-file"] === "string";
68
+ if (hasInputJson && hasInputFile) {
69
+ throw new CliError("usage.duplicate_input", "Use either --input-json or --input-file, not both.", EXIT_CODES.usage);
70
+ }
71
+ if (hasInputJson)
72
+ return parseJsonObject(String(flags["input-json"]), "--input-json");
73
+ if (hasInputFile)
74
+ return parseJsonObject(await readFile(String(flags["input-file"]), "utf8"), "--input-file");
75
+ return undefined;
76
+ }
77
+ async function invokePulseTool(context, toolName, input) {
78
+ const { result } = await callToolWithRetry(context, toolName, input, true);
79
+ context.output.success({
80
+ schemaVersion: 1,
81
+ tool: toolName,
82
+ arguments: redactForOutput(input),
83
+ result
84
+ }, [renderToolResult(result)]);
85
+ }
86
+ export async function runPulseCommand(args, context) {
87
+ const subcommand = args[0];
88
+ const commandArgs = args.slice(1);
89
+ if (!subcommand || isHelpToken(subcommand) || commandArgs.some((arg) => isHelpToken(arg))) {
90
+ context.output.info(pulseHelpText());
91
+ return;
92
+ }
93
+ if (subcommand === "create" || subcommand === "deploy") {
94
+ await runPulsePublishCommand(commandArgs, context);
95
+ return;
96
+ }
97
+ if (subcommand === "delete") {
98
+ throw new CliError("usage.pulse_delete_unavailable", "The CLI does not expose Pulse deletion. Archive the Pulse instead.", EXIT_CODES.usage);
99
+ }
100
+ if (subcommand === "logs") {
101
+ throw new CliError("usage.pulse_logs_unavailable", "Pulse logs are not exposed through the hardened CLI lifecycle surface yet.", EXIT_CODES.usage, { nextStep: "Use `vibecodr pulse status <pulse-id>` for deploy state, or inspect platform telemetry through the owner dashboard." });
102
+ }
103
+ if (subcommand === "list") {
104
+ const { flags, positionals } = parseFlags(commandArgs, {
105
+ valueFlags: ["limit", "offset"],
106
+ booleanFlags: []
107
+ });
108
+ if (positionals.length > 0) {
109
+ throw new CliError("usage.unexpected_argument", `Unexpected argument: ${positionals[0]}`, EXIT_CODES.usage);
110
+ }
111
+ await invokePulseTool(context, "list_pulses", {
112
+ limit: parseBoundedInteger(flags["limit"], "--limit", 10, MAX_LIST_LIMIT),
113
+ offset: parseBoundedInteger(flags["offset"], "--offset", 0, 10_000)
114
+ });
115
+ return;
116
+ }
117
+ const action = PULSE_ACTIONS[subcommand];
118
+ if (!action) {
119
+ throw new CliError("usage.command", `Unknown pulse command: ${subcommand}`, EXIT_CODES.usage);
120
+ }
121
+ const { flags, positionals } = parseFlags(commandArgs, {
122
+ valueFlags: ["input-json", "input-file"],
123
+ booleanFlags: ["confirm"]
124
+ });
125
+ const pulseId = parsePulseId(positionals[0]);
126
+ if (positionals.length > 1) {
127
+ throw new CliError("usage.unexpected_argument", `Unexpected argument: ${positionals[1]}`, EXIT_CODES.usage);
128
+ }
129
+ if (action.requiresConfirm && flags["confirm"] !== true) {
130
+ throw new CliError("usage.confirmation_required", `Pulse ${subcommand} requires explicit confirmation. Re-run with --confirm after the user confirms.`, EXIT_CODES.usage);
131
+ }
132
+ const input = {
133
+ pulseId,
134
+ ...(action.requiresConfirm ? { confirmed: true } : {})
135
+ };
136
+ if (subcommand === "run") {
137
+ const runInput = await parseRunInput(flags);
138
+ if (runInput !== undefined)
139
+ input["input"] = runInput;
140
+ }
141
+ else if (flags["input-json"] !== undefined || flags["input-file"] !== undefined) {
142
+ throw new CliError("usage.unknown_flag", "--input-json and --input-file are only valid for pulse run.", EXIT_CODES.usage);
143
+ }
144
+ await invokePulseTool(context, action.toolName, input);
145
+ }
@@ -1,5 +1,6 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import { parseFlags } from "../cli/parse.js";
3
+ import { showHelpIfRequested } from "./help.js";
3
4
  import { InstallManifestStore } from "../storage/install-manifest.js";
4
5
  async function inspectInstall(entry) {
5
6
  if (entry.method !== "file") {
@@ -23,6 +24,8 @@ async function inspectInstall(entry) {
23
24
  }
24
25
  }
25
26
  export async function runStatusCommand(args, context) {
27
+ if (showHelpIfRequested(args, context, "Usage: vibecodr status [--probe] [--show-installs]"))
28
+ return;
26
29
  const { flags } = parseFlags(args, {
27
30
  booleanFlags: ["probe", "show-installs"]
28
31
  });
@@ -1,6 +1,7 @@
1
1
  import { formatJson, summarizeToolSchema } from "../core/renderers.js";
2
2
  import { parseFlags } from "../cli/parse.js";
3
3
  import { CliError, EXIT_CODES } from "../cli/errors.js";
4
+ import { showHelpIfRequested } from "./help.js";
4
5
  function challengedScope(error) {
5
6
  if (!error.debugDetails || typeof error.debugDetails !== "object")
6
7
  return undefined;
@@ -40,6 +41,8 @@ async function loadToolsWithRetry(context, allowLogin) {
40
41
  }
41
42
  }
42
43
  export async function runToolsCommand(args, context) {
44
+ if (showHelpIfRequested(args, context, "Usage: vibecodr tools [<tool-name>] [--search <text>] [--schema] [--no-login]"))
45
+ return;
43
46
  const { flags, positionals } = parseFlags(args, {
44
47
  valueFlags: ["search"],
45
48
  booleanFlags: ["schema", "no-login"]
@@ -5,7 +5,10 @@ import { uninstallCodex } from "../clients/codex.js";
5
5
  import { uninstallCursor } from "../clients/cursor.js";
6
6
  import { uninstallVsCode } from "../clients/vscode.js";
7
7
  import { uninstallWindsurf } from "../clients/windsurf.js";
8
+ import { showHelpIfRequested } from "./help.js";
8
9
  export async function runUninstallCommand(args, context) {
10
+ if (showHelpIfRequested(args, context, "Usage: vibecodr uninstall <codex|cursor|vscode|windsurf> [--scope user|project] [--path <dir>] [--name <server-name>] [--dry-run]"))
11
+ return;
9
12
  const client = args[0];
10
13
  if (!client || !["codex", "cursor", "vscode", "windsurf"].includes(client)) {
11
14
  throw new CliError("usage.uninstall_client", "Usage: uninstall <codex|cursor|vscode|windsurf> [options]", EXIT_CODES.usage);
@@ -0,0 +1,44 @@
1
+ const REDACTED = "[redacted]";
2
+ const SENSITIVE_KEY_PATTERNS = [
3
+ /^authorization$/i,
4
+ /^cookie$/i,
5
+ /^set-cookie$/i,
6
+ /(^|[-_])token$/i,
7
+ /(^|[-_])secret($|[-_])/i,
8
+ /password/i,
9
+ /credential/i,
10
+ /^api[-_]?key$/i,
11
+ /(^|[-_])api[-_]?key$/i,
12
+ /^private[-_]?key$/i,
13
+ /^refresh[-_]?token$/i,
14
+ /^access[-_]?token$/i,
15
+ /^fileBase64$/i,
16
+ /^code$/i,
17
+ /^content$/i,
18
+ /^descriptor$/i
19
+ ];
20
+ const SENSITIVE_STRING_PATTERNS = [
21
+ /\bBearer\s+[A-Za-z0-9._~+/=-]+/i,
22
+ /\btok_[A-Za-z0-9._-]+/i,
23
+ /\bsk-[A-Za-z0-9._-]+/i,
24
+ /\b(token|secret|api[-_ ]?key)\s*[:=]\s*\S+/i
25
+ ];
26
+ function isSensitiveKey(key) {
27
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key));
28
+ }
29
+ export function redactForOutput(value, keyHint) {
30
+ if (keyHint && isSensitiveKey(keyHint))
31
+ return REDACTED;
32
+ if (Array.isArray(value))
33
+ return value.map((item) => redactForOutput(item));
34
+ if (typeof value === "string" && SENSITIVE_STRING_PATTERNS.some((pattern) => pattern.test(value))) {
35
+ return REDACTED;
36
+ }
37
+ if (!value || typeof value !== "object")
38
+ return value;
39
+ const output = {};
40
+ for (const [key, nested] of Object.entries(value)) {
41
+ output[key] = redactForOutput(nested, key);
42
+ }
43
+ return output;
44
+ }
package/docs/commands.md CHANGED
@@ -4,16 +4,16 @@ This page documents the command surface implemented in the current repo.
4
4
 
5
5
  ## Global flags
6
6
 
7
- All commands accept:
8
-
9
- - `--profile <name>`
10
- - `--json`
11
- - `--verbose`
12
- - `--non-interactive`
13
-
14
- Alternate MCP servers are profile-scoped, not runtime overrides. Use
15
- `vibecodr config profile create <name> --server-url <url>` and then login to
16
- that profile; stored tokens are bound to the server that issued them.
7
+ All commands accept:
8
+
9
+ - `--profile <name>`
10
+ - `--json`
11
+ - `--verbose`
12
+ - `--non-interactive`
13
+
14
+ Alternate MCP servers are profile-scoped, not runtime overrides. Use
15
+ `vibecodr config profile create <name> --server-url <url>` and then login to
16
+ that profile; stored tokens are bound to the server that issued them.
17
17
 
18
18
  ## Commands
19
19
 
@@ -58,12 +58,14 @@ This always reads the live tool catalog from the MCP server.
58
58
 
59
59
  Syntax:
60
60
 
61
- `vibecodr call <tool-name> [--input-json <json>] [--input-file <path>] [--stdin] [--interactive] [--no-login]`
61
+ `vibecodr call <tool-name> [--input-json <json>] [--input-file <path>] [--stdin] [--interactive] [--no-login] [--confirm]`
62
62
 
63
63
  `--interactive` currently supports top-level scalar object fields.
64
64
 
65
65
  For `quick_publish_creation` with `payload.importMode: "direct_files"`, pass file paths as normal slash-separated project paths such as `src/main.tsx` or `src/server/binding-proof.js`. Do not pre-encode slashes as `%2F`; the hosted MCP gateway encodes each URL segment when it writes files to Vibecodr.
66
66
 
67
+ Known mutating tools require explicit confirmation through `--confirm`. The CLI redacts secret, token, source, descriptor, and inline file-content fields from displayed arguments and results; the MCP gateway remains the authority boundary for OAuth, owner checks, confirmation, and output shaping.
68
+
67
69
  ### `pulse-setup`
68
70
 
69
71
  Syntax:
@@ -76,6 +78,31 @@ The CLI does not maintain separate Pulse setup copy; it reads MCP output derived
76
78
 
77
79
  The returned guidance should stay capability-shaped: `env.fetch` is Vibecodr policy-mediated fetch, `env.secrets.bearer/header/query/verifyHmac` are policy-bound secret helpers, `env.webhooks.verify("stripe")` is the first certified provider helper rather than the whole webhook model, non-Stripe signed webhooks use generic HMAC format presets such as `github-sha256`, `shopify-hmac-sha256`, and `slack-v0` until fixture-backed helpers exist, `env.connections.use(provider).fetch` is provider-scoped connected-account access, `env.log` is structured logging, `env.request` is sanitized request access, `env.runtime` is safe correlation metadata, and `env.waitUntil` is best-effort after-response work. The CLI must not introduce separate cleanup, platform-binding, dispatch, raw-token, raw-authorization, or physical-storage guidance.
78
80
 
81
+ ### `pulse-publish`
82
+
83
+ Syntax:
84
+
85
+ `vibecodr pulse-publish --name <name> (--code <source> | --code-file <path>) [--descriptor-json <json> | --descriptor-file <path>] [--slug <slug>] [--visibility public|unlisted|private] --confirm`
86
+
87
+ Calls `publish_standalone_pulse`. Standalone Pulse source/metadata visibility defaults to private. Private visibility does not add runtime authentication to the public Pulse URL. The CLI does not echo source code or descriptors in successful output.
88
+
89
+ ### `pulse`
90
+
91
+ Syntax:
92
+
93
+ - `vibecodr pulse list [--limit <n>] [--offset <n>]`
94
+ - `vibecodr pulse get <pulse-id>`
95
+ - `vibecodr pulse status <pulse-id>`
96
+ - `vibecodr pulse run <pulse-id> [--input-json <json> | --input-file <path>] --confirm`
97
+ - `vibecodr pulse archive <pulse-id> --confirm`
98
+ - `vibecodr pulse restore <pulse-id> --confirm`
99
+ - `vibecodr pulse create --name <name> (--code <source> | --code-file <path>) --confirm`
100
+ - `vibecodr pulse deploy --name <name> (--code <source> | --code-file <path>) --confirm`
101
+
102
+ `create` and `deploy` are aliases for the standalone publish flow. `run`, `archive`, and `restore` require explicit confirmation. `delete` is intentionally unavailable; archive a Pulse instead. `logs` are not exposed through the hardened lifecycle surface yet.
103
+
104
+ The CLI forwards lifecycle calls to MCP tools owned by the hosted gateway: `list_pulses`, `get_pulse`, `get_pulse_status`, `run_pulse`, `archive_pulse`, and `restore_pulse`. These server tools are hidden from default discovery but callable by exact name for owner recovery and CLI use.
105
+
79
106
  ### `doctor`
80
107
 
81
108
  Syntax:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodr/cli",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Vibecodr CLI for login, live MCP tool discovery, and live tool invocation.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",