@vibecodr/cli 0.1.8 → 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/CHANGELOG.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.2.0
4
4
 
5
5
  - rename the npm package to `@vibecodr/cli` while keeping `vibecodr` as the primary executable and `vibecodr-mcp` as a compatibility alias
6
+ - add the hardened `pulse` lifecycle command group for list/get/status/run/archive/restore plus create/deploy aliases
7
+ - redact source, descriptor, token, secret, and inline file-content fields from CLI-displayed MCP arguments and results
8
+ - require explicit confirmation for known mutating MCP tools when invoked through the generic `call` command
6
9
 
7
10
  ## 0.1.8
8
11
 
package/README.md CHANGED
@@ -25,6 +25,8 @@ 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`
@@ -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,28 @@ function computeExpiresAt(expiresIn) {
23
23
  return undefined;
24
24
  return new Date(Date.now() + expiresIn * 1000).toISOString();
25
25
  }
26
+ function normalizeServerUrlForSessionMatch(value) {
27
+ if (!value)
28
+ return undefined;
29
+ try {
30
+ const url = new URL(value);
31
+ url.hash = "";
32
+ url.search = "";
33
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
34
+ return url.toString();
35
+ }
36
+ catch {
37
+ return value.replace(/\/+$/, "");
38
+ }
39
+ }
40
+ function sessionMatchesServer(session, serverUrl) {
41
+ if (!serverUrl)
42
+ return true;
43
+ const target = normalizeServerUrlForSessionMatch(serverUrl);
44
+ const sessionServer = normalizeServerUrlForSessionMatch(session.serverUrl);
45
+ const sessionResource = normalizeServerUrlForSessionMatch(session.resourceUrl);
46
+ return Boolean(target && (target === sessionServer || target === sessionResource));
47
+ }
26
48
  async function startLoopbackListener(timeoutSec) {
27
49
  const callbackPath = "/oauth/callback/vibecodr";
28
50
  const server = createServer();
@@ -108,11 +130,17 @@ export class TokenManager {
108
130
  return {
109
131
  profileName: name,
110
132
  profile,
111
- serverUrl: globalOptions.serverUrl || profile.serverUrl
133
+ serverUrl: profile.serverUrl
112
134
  };
113
135
  }
114
- async getSession(profileName) {
115
- return await this.secretStore.get(profileName);
136
+ async getSession(profileName, serverUrl) {
137
+ const session = await this.secretStore.get(profileName);
138
+ if (!session)
139
+ return undefined;
140
+ // SECURITY: Never replay a stored bearer token to a different MCP origin.
141
+ // Profiles may be repointed during development; the existing token remains
142
+ // valid only for the server/resource that issued it.
143
+ return sessionMatchesServer(session, serverUrl) ? session : undefined;
116
144
  }
117
145
  sessionState(session) {
118
146
  if (!session)
@@ -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";
@@ -16,6 +16,8 @@ import { runConfigCommand } from "../commands/config.js";
16
16
  import { runInstallCommand } from "../commands/install.js";
17
17
  import { runUninstallCommand } from "../commands/uninstall.js";
18
18
  import { runPulseSetupCommand } from "../commands/pulse-setup.js";
19
+ import { runPulsePublishCommand } from "../commands/pulse-publish.js";
20
+ import { runPulseCommand } from "../commands/pulse.js";
19
21
  function helpText() {
20
22
  return [
21
23
  "vibecodr <command> [options]",
@@ -32,21 +34,31 @@ function helpText() {
32
34
  " uninstall <client>",
33
35
  " config",
34
36
  " pulse-setup [--descriptor-setup-json <json> | --descriptor-setup-file <path>]",
37
+ " pulse-publish --name <name> (--code <source> | --code-file <path>) --confirm",
38
+ " pulse <list|get|status|run|archive|restore|create|deploy>",
39
+ " Publishes a standalone Pulse with private source/metadata visibility by default.",
40
+ " The runtime URL is still public HTTP unless the Pulse code rejects callers.",
35
41
  "",
36
42
  "Global flags:",
37
43
  " --profile <name>",
38
- " --server-url <url>",
39
44
  " --json",
40
45
  " --verbose",
41
46
  " --non-interactive"
42
47
  ].join("\n");
43
48
  }
49
+ function versionText() {
50
+ return String(CLIENT_INFO.version);
51
+ }
44
52
  async function main() {
45
53
  const { command, commandArgs, globalOptions } = parseGlobalOptions(process.argv.slice(2));
46
- if (!command || command === "--help" || command === "help") {
54
+ if (!command || isHelpToken(command)) {
47
55
  process.stdout.write(helpText() + "\n");
48
56
  return;
49
57
  }
58
+ if (isVersionToken(command)) {
59
+ process.stdout.write(versionText() + "\n");
60
+ return;
61
+ }
50
62
  const configStore = new ConfigStore();
51
63
  const secretStore = new SecretStore();
52
64
  const tokenManager = new TokenManager(configStore, secretStore);
@@ -91,6 +103,12 @@ async function main() {
91
103
  case "pulse-setup":
92
104
  await runPulseSetupCommand(commandArgs, context);
93
105
  return;
106
+ case "pulse-publish":
107
+ await runPulsePublishCommand(commandArgs, context);
108
+ return;
109
+ case "pulse":
110
+ await runPulseCommand(commandArgs, context);
111
+ return;
94
112
  default:
95
113
  throw new CliError("usage.command", `Unknown command: ${command}`, EXIT_CODES.usage);
96
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)
@@ -55,12 +65,9 @@ export function parseGlobalOptions(argv) {
55
65
  continue;
56
66
  }
57
67
  if (token === "--server-url") {
58
- const value = argv[index + 1];
59
- if (!value)
60
- throw new CliError("usage.missing_server_url", "Missing value for --server-url.", EXIT_CODES.usage);
61
- globalOptions.serverUrl = value;
62
- index += 1;
63
- continue;
68
+ throw new CliError("usage.unknown_global_flag", "--server-url is no longer accepted as a global runtime override.", EXIT_CODES.usage, {
69
+ nextStep: "Create a separate profile with `vibecodr config profile create <name> --server-url <url>` and login to that profile. Stored tokens are bound to the server they were issued for."
70
+ });
64
71
  }
65
72
  if (token === "--json") {
66
73
  globalOptions.json = true;
@@ -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;
@@ -32,7 +44,7 @@ async function readStdin() {
32
44
  }
33
45
  export async function callToolWithRetry(context, toolName, input, allowLogin) {
34
46
  const { profileName, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
35
- const existingSession = await context.tokenManager.getSession(profileName);
47
+ const existingSession = await context.tokenManager.getSession(profileName, serverUrl);
36
48
  try {
37
49
  return {
38
50
  result: await context.runtimeClient.callTool(serverUrl, existingSession?.accessToken, toolName, input),
@@ -54,7 +66,7 @@ export async function callToolWithRetry(context, toolName, input, allowLogin) {
54
66
  await context.tokenManager.login(context.globalOptions, {
55
67
  scope
56
68
  });
57
- const nextSession = await context.tokenManager.getSession(profileName);
69
+ const nextSession = await context.tokenManager.getSession(profileName, serverUrl);
58
70
  return {
59
71
  result: await context.runtimeClient.callTool(serverUrl, nextSession?.accessToken, toolName, input),
60
72
  ...(nextSession ? { session: nextSession } : {})
@@ -65,7 +77,7 @@ export async function callToolWithRetry(context, toolName, input, allowLogin) {
65
77
  }
66
78
  async function listToolsWithRetry(context, allowLogin) {
67
79
  const { profileName, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
68
- const existingSession = await context.tokenManager.getSession(profileName);
80
+ const existingSession = await context.tokenManager.getSession(profileName, serverUrl);
69
81
  try {
70
82
  return await context.runtimeClient.listTools(serverUrl, existingSession?.accessToken);
71
83
  }
@@ -80,16 +92,18 @@ async function listToolsWithRetry(context, allowLogin) {
80
92
  await context.tokenManager.login(context.globalOptions, {
81
93
  scope: challengedScope(error)
82
94
  });
83
- const nextSession = await context.tokenManager.getSession(profileName);
95
+ const nextSession = await context.tokenManager.getSession(profileName, serverUrl);
84
96
  return await context.runtimeClient.listTools(serverUrl, nextSession?.accessToken);
85
97
  }
86
98
  throw error;
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
  });
@@ -0,0 +1,93 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { parseFlags } from "../cli/parse.js";
4
+ import { renderToolResult } from "../core/renderers.js";
5
+ import { callToolWithRetry } from "./call.js";
6
+ import { showHelpIfRequested } from "./help.js";
7
+ const PUBLISH_STANDALONE_PULSE_TOOL_NAME = "publish_standalone_pulse";
8
+ const PULSE_VISIBILITIES = new Set(["public", "unlisted", "private"]);
9
+ const DEFAULT_PULSE_VISIBILITY = "private";
10
+ function parseJsonObject(raw, source) {
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(raw);
14
+ }
15
+ catch (error) {
16
+ throw new CliError("usage.invalid_json", `${source} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.usage);
17
+ }
18
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
19
+ throw new CliError("usage.invalid_json", `${source} must be a JSON object.`, EXIT_CODES.usage);
20
+ }
21
+ return parsed;
22
+ }
23
+ async function parsePulsePublishInput(args) {
24
+ const { flags } = parseFlags(args, {
25
+ valueFlags: ["name", "code", "code-file", "descriptor-json", "descriptor-file", "slug", "visibility"],
26
+ booleanFlags: ["confirm"]
27
+ });
28
+ const name = typeof flags["name"] === "string" ? flags["name"].trim() : "";
29
+ if (!name) {
30
+ throw new CliError("usage.pulse_name_required", "Usage: pulse-publish --name <name> (--code <source> | --code-file <path>) --confirm", EXIT_CODES.usage);
31
+ }
32
+ const hasCode = typeof flags["code"] === "string";
33
+ const hasCodeFile = typeof flags["code-file"] === "string";
34
+ if (hasCode === hasCodeFile) {
35
+ throw new CliError("usage.pulse_code_required", "Use exactly one of --code or --code-file.", EXIT_CODES.usage);
36
+ }
37
+ const code = hasCode ? String(flags["code"]) : await readFile(String(flags["code-file"]), "utf8");
38
+ if (!code.trim()) {
39
+ throw new CliError("usage.pulse_code_empty", "Pulse source code cannot be empty.", EXIT_CODES.usage);
40
+ }
41
+ const hasDescriptorJson = typeof flags["descriptor-json"] === "string";
42
+ const hasDescriptorFile = typeof flags["descriptor-file"] === "string";
43
+ if (hasDescriptorJson && hasDescriptorFile) {
44
+ throw new CliError("usage.duplicate_descriptor", "Use either --descriptor-json or --descriptor-file, not both.", EXIT_CODES.usage);
45
+ }
46
+ const input = {
47
+ name,
48
+ code,
49
+ visibility: DEFAULT_PULSE_VISIBILITY,
50
+ confirmed: flags["confirm"] === true
51
+ };
52
+ if (hasDescriptorJson) {
53
+ input["descriptor"] = parseJsonObject(String(flags["descriptor-json"]), "--descriptor-json");
54
+ }
55
+ else if (hasDescriptorFile) {
56
+ input["descriptor"] = parseJsonObject(await readFile(String(flags["descriptor-file"]), "utf8"), "--descriptor-file");
57
+ }
58
+ if (typeof flags["slug"] === "string" && flags["slug"].trim()) {
59
+ input["slug"] = flags["slug"].trim();
60
+ }
61
+ if (typeof flags["visibility"] === "string") {
62
+ if (!PULSE_VISIBILITIES.has(flags["visibility"])) {
63
+ throw new CliError("usage.invalid_visibility", "Pulse visibility must be public, unlisted, or private.", EXIT_CODES.usage);
64
+ }
65
+ input["visibility"] = flags["visibility"];
66
+ }
67
+ if (input["confirmed"] !== true) {
68
+ throw new CliError("usage.confirmation_required", "Publishing a standalone Pulse requires explicit confirmation. Re-run with --confirm after the user confirms.", EXIT_CODES.usage, {
69
+ nextStep: "Ask one clear confirmation question, then pass --confirm only after the user says yes."
70
+ });
71
+ }
72
+ return input;
73
+ }
74
+ function redactPulsePublishArguments(input) {
75
+ const { code: _code, descriptor: _descriptor, ...safe } = input;
76
+ return {
77
+ ...safe,
78
+ ...(input["descriptor"] ? { descriptorProvided: true } : {}),
79
+ sourceProvided: true
80
+ };
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;
85
+ const input = await parsePulsePublishInput(args);
86
+ const { result } = await callToolWithRetry(context, PUBLISH_STANDALONE_PULSE_TOOL_NAME, input, true);
87
+ context.output.success({
88
+ schemaVersion: 1,
89
+ tool: PUBLISH_STANDALONE_PULSE_TOOL_NAME,
90
+ arguments: redactPulsePublishArguments(input),
91
+ result
92
+ }, [renderToolResult(result)]);
93
+ }
@@ -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,11 +24,13 @@ 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
  });
29
32
  const { profileName, profile, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
30
- const session = await context.tokenManager.getSession(profileName);
33
+ const session = await context.tokenManager.getSession(profileName, serverUrl);
31
34
  const sessionState = context.tokenManager.sessionState(session);
32
35
  const installs = flags["show-installs"]
33
36
  ? await Promise.all((await new InstallManifestStore().find(() => true)).map((entry) => inspectInstall(entry)))
@@ -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;
@@ -9,7 +10,7 @@ function challengedScope(error) {
9
10
  }
10
11
  async function loadToolsWithRetry(context, allowLogin) {
11
12
  const { profileName, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
12
- const existingSession = await context.tokenManager.getSession(profileName);
13
+ const existingSession = await context.tokenManager.getSession(profileName, serverUrl);
13
14
  try {
14
15
  return {
15
16
  tools: await context.runtimeClient.listTools(serverUrl, existingSession?.accessToken),
@@ -30,7 +31,7 @@ async function loadToolsWithRetry(context, allowLogin) {
30
31
  await context.tokenManager.login(context.globalOptions, {
31
32
  scope: challengedScope(error)
32
33
  });
33
- const nextSession = await context.tokenManager.getSession(profileName);
34
+ const nextSession = await context.tokenManager.getSession(profileName, serverUrl);
34
35
  return {
35
36
  tools: await context.runtimeClient.listTools(serverUrl, nextSession?.accessToken),
36
37
  ...(nextSession ? { session: nextSession } : {})
@@ -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
+ }
@@ -1,35 +1,35 @@
1
- # Architecture
2
-
3
- The Vibecodr CLI is a client of the hosted Vibecodr MCP gateway.
4
-
5
- ## Boundary
6
-
7
- - hosted MCP gateway/server repo: `Vibecodr-MCP`
8
- - CLI client repo: `Vibecodr-MCP-CLI`
9
- - CLI package: `@vibecodr/cli`
10
- - primary executable: `vibecodr`
11
- - compatibility executable: `vibecodr-mcp`
12
- - legacy package compatibility: `@vibecodr/mcp`
13
- - default MCP URL: `https://openai.vibecodr.space/mcp`
14
-
15
- This repo does not run the hosted server. It installs client config, performs CLI-owned OAuth, discovers the live tool catalog, and calls tools over Streamable HTTP MCP.
16
-
17
- ## Auth Ownership
18
-
19
- `vibecodr login` stores OAuth tokens for the CLI profile only.
20
-
21
- Codex, Cursor, VS Code, Windsurf, ChatGPT, and other MCP clients own separate OAuth sessions. Installing MCP config into those clients points them at the same server, but it does not copy CLI tokens into them.
22
-
23
- ## Why The Repos Are Separate
24
-
25
- The CLI is permissively licensed and safe to distribute as a public client package. The hosted gateway implementation is source-available under a different license because it contains server-side orchestration, OAuth gateway behavior, Cloudflare deployment wiring, and Vibecodr API integration code.
26
-
27
- The package name is `@vibecodr/cli` because this repo distributes the user-facing command-line client. The older `@vibecodr/mcp` package name is kept only as a compatibility/deprecation surface; the bare `vibecodr` executable remains the canonical user command.
28
-
29
- Local config directories and secure-token service names intentionally keep their historical `vibecodr-mcp` / `@vibecodr/mcp` identifiers during this migration. Those names are storage compatibility keys, not the public npm package identity.
30
-
31
- Keeping the repos separate makes the contract clear:
32
-
33
- - users can use the hosted service normally
34
- - users can install and inspect the CLI freely under this repo license
35
- - commercial reuse of the gateway implementation remains governed by the server repo license
1
+ # Architecture
2
+
3
+ The Vibecodr CLI is a client of the hosted Vibecodr MCP gateway.
4
+
5
+ ## Boundary
6
+
7
+ - hosted MCP gateway/server repo: `Vibecodr-MCP`
8
+ - CLI client repo: `Vibecodr-MCP-CLI`
9
+ - CLI package: `@vibecodr/cli`
10
+ - primary executable: `vibecodr`
11
+ - compatibility executable: `vibecodr-mcp`
12
+ - legacy package compatibility: `@vibecodr/mcp`
13
+ - default MCP URL: `https://openai.vibecodr.space/mcp`
14
+
15
+ This repo does not run the hosted server. It installs client config, performs CLI-owned OAuth, discovers the live tool catalog, and calls tools over Streamable HTTP MCP.
16
+
17
+ ## Auth Ownership
18
+
19
+ `vibecodr login` stores OAuth tokens for the CLI profile only.
20
+
21
+ Codex, Cursor, VS Code, Windsurf, ChatGPT, and other MCP clients own separate OAuth sessions. Installing MCP config into those clients points them at the same server, but it does not copy CLI tokens into them.
22
+
23
+ ## Why The Repos Are Separate
24
+
25
+ The CLI is permissively licensed and safe to distribute as a public client package. The hosted gateway implementation is source-available under a different license because it contains server-side orchestration, OAuth gateway behavior, Cloudflare deployment wiring, and Vibecodr API integration code.
26
+
27
+ The package name is `@vibecodr/cli` because this repo distributes the user-facing command-line client. The older `@vibecodr/mcp` package name is kept only as a compatibility/deprecation surface; the bare `vibecodr` executable remains the canonical user command.
28
+
29
+ Local config directories and secure-token service names intentionally keep their historical `vibecodr-mcp` / `@vibecodr/mcp` identifiers during this migration. Those names are storage compatibility keys, not the public npm package identity.
30
+
31
+ Keeping the repos separate makes the contract clear:
32
+
33
+ - users can use the hosted service normally
34
+ - users can install and inspect the CLI freely under this repo license
35
+ - commercial reuse of the gateway implementation remains governed by the server repo license
package/docs/auth.md CHANGED
@@ -1,33 +1,33 @@
1
1
  # Auth
2
2
 
3
- `vibecodr login` authenticates the CLI itself to the hosted Vibecodr MCP server. It does not log Codex, Cursor, VS Code, Windsurf, ChatGPT, or any other MCP client into MCP.
3
+ `vibecodr login` authenticates the CLI itself to the hosted Vibecodr MCP server. It does not log Codex, Cursor, VS Code, Windsurf, ChatGPT, or any other MCP client into MCP.
4
+
5
+ Vibecodr has one hosted MCP gateway. The CLI is one client of that gateway, with its own local OAuth token store.
4
6
 
5
- Vibecodr has one hosted MCP gateway. The CLI is one client of that gateway, with its own local OAuth token store.
6
-
7
- Compatibility alias:
8
-
9
- - `vibecodr-mcp login`
7
+ Compatibility alias:
8
+
9
+ - `vibecodr-mcp login`
10
10
 
11
11
  ## Implemented now
12
12
 
13
13
  - protected-resource and authorization-server discovery against the MCP server
14
14
  - PKCE S256 enforcement
15
15
  - loopback callback on `127.0.0.1`
16
- - secure token storage in the OS credential store via `@napi-rs/keyring`
17
- - proactive refresh before protected runtime commands when a refresh token is available
18
- - `logout` local token deletion plus best-effort revocation
19
-
20
- The plaintext file secret store is for local automated tests only. It is ignored unless both `VIBECDR_MCP_INSECURE_SECRET_STORE_PATH` and `VIBECDR_MCP_ENABLE_INSECURE_SECRET_STORE=true` are set.
21
-
22
- The local config and secure-token storage keys intentionally keep their historical `vibecodr-mcp` / `@vibecodr/mcp` names during the `@vibecodr/cli` package rename. That preserves existing CLI sessions instead of forcing users to re-authenticate for a package-name migration.
23
-
24
- Supported OS credential stores:
25
-
26
- - macOS: Keychain
27
- - Windows: Credential Manager
28
- - Linux: Secret Service through a desktop keyring such as GNOME Keyring or KWallet
29
-
30
- Linux systems need a running, unlocked keyring on the current D-Bus session. Headless Linux should use a real Secret Service setup for persistent CLI login, or let the target MCP client own its own OAuth flow instead of storing CLI tokens.
16
+ - secure token storage in the OS credential store via `@napi-rs/keyring`
17
+ - proactive refresh before protected runtime commands when a refresh token is available
18
+ - `logout` local token deletion plus best-effort revocation
19
+
20
+ The plaintext file secret store is for local automated tests only. It is ignored unless both `VIBECDR_MCP_INSECURE_SECRET_STORE_PATH` and `VIBECDR_MCP_ENABLE_INSECURE_SECRET_STORE=true` are set.
21
+
22
+ The local config and secure-token storage keys intentionally keep their historical `vibecodr-mcp` / `@vibecodr/mcp` names during the `@vibecodr/cli` package rename. That preserves existing CLI sessions instead of forcing users to re-authenticate for a package-name migration.
23
+
24
+ Supported OS credential stores:
25
+
26
+ - macOS: Keychain
27
+ - Windows: Credential Manager
28
+ - Linux: Secret Service through a desktop keyring such as GNOME Keyring or KWallet
29
+
30
+ Linux systems need a running, unlocked keyring on the current D-Bus session. Headless Linux should use a real Secret Service setup for persistent CLI login, or let the target MCP client own its own OAuth flow instead of storing CLI tokens.
31
31
 
32
32
  ## Registration modes
33
33
 
package/docs/commands.md CHANGED
@@ -7,11 +7,14 @@ This page documents the command surface implemented in the current repo.
7
7
  All commands accept:
8
8
 
9
9
  - `--profile <name>`
10
- - `--server-url <url>`
11
10
  - `--json`
12
11
  - `--verbose`
13
12
  - `--non-interactive`
14
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
+
15
18
  ## Commands
16
19
 
17
20
  ### `login`
@@ -55,12 +58,14 @@ This always reads the live tool catalog from the MCP server.
55
58
 
56
59
  Syntax:
57
60
 
58
- `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]`
59
62
 
60
63
  `--interactive` currently supports top-level scalar object fields.
61
64
 
62
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.
63
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
+
64
69
  ### `pulse-setup`
65
70
 
66
71
  Syntax:
@@ -73,6 +78,31 @@ The CLI does not maintain separate Pulse setup copy; it reads MCP output derived
73
78
 
74
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.
75
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
+
76
106
  ### `doctor`
77
107
 
78
108
  Syntax:
package/docs/install.md CHANGED
@@ -15,14 +15,14 @@ node dist/bin/vibecodr-mcp.js install codex --json
15
15
  After the package is published:
16
16
 
17
17
  ```bash
18
- npx -y -p @vibecodr/cli vibecodr install codex
18
+ npx -y -p @vibecodr/cli vibecodr install codex
19
19
  ```
20
20
 
21
21
  Direct CLI-only usage:
22
22
 
23
23
  ```bash
24
- npx -y -p @vibecodr/cli vibecodr login
25
- npx -y -p @vibecodr/cli vibecodr tools --json
24
+ npx -y -p @vibecodr/cli vibecodr login
25
+ npx -y -p @vibecodr/cli vibecodr tools --json
26
26
  ```
27
27
 
28
28
  ## Client commands
package/docs/licensing.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # Licensing
2
2
 
3
- This repository is the public Vibecodr CLI surface.
4
-
5
- - Package name: `@vibecodr/cli`
6
- - Primary executable name: `vibecodr`
7
- - Compatibility executable name: `vibecodr-mcp`
8
- - Legacy package compatibility: `@vibecodr/mcp`
3
+ This repository is the public Vibecodr CLI surface.
4
+
5
+ - Package name: `@vibecodr/cli`
6
+ - Primary executable name: `vibecodr`
7
+ - Compatibility executable name: `vibecodr-mcp`
8
+ - Legacy package compatibility: `@vibecodr/mcp`
9
9
  - Repo scope: direct CLI runtime, auth, diagnostics, and later thin installer adapters
10
10
  - License: Apache-2.0
11
11
 
12
- The hosted MCP gateway/server remains separate from this repo. That separation keeps hosted-service use distinct from any source-code reuse terms applied to the server implementation repo.
13
-
14
- Practical split:
15
-
16
- - this CLI repo governs distribution, modification, and reuse of the public client code
17
- - the hosted Vibecodr MCP service remains governed by Vibecodr service terms for account holders
18
- - the server implementation repo can carry different source-available terms without changing whether a commercial team may use the hosted service
12
+ The hosted MCP gateway/server remains separate from this repo. That separation keeps hosted-service use distinct from any source-code reuse terms applied to the server implementation repo.
19
13
 
20
- See [`architecture.md`](./architecture.md) for the client/server boundary.
14
+ Practical split:
15
+
16
+ - this CLI repo governs distribution, modification, and reuse of the public client code
17
+ - the hosted Vibecodr MCP service remains governed by Vibecodr service terms for account holders
18
+ - the server implementation repo can carry different source-available terms without changing whether a commercial team may use the hosted service
19
+
20
+ See [`architecture.md`](./architecture.md) for the client/server boundary.
@@ -30,15 +30,15 @@ Run:
30
30
  vibecodr doctor --json
31
31
  ```
32
32
 
33
- If the `secret-store` check fails, the CLI cannot safely store tokens yet.
34
-
35
- The plaintext file secret store is intentionally test-only. If `VIBECDR_MCP_INSECURE_SECRET_STORE_PATH` is set without `VIBECDR_MCP_ENABLE_INSECURE_SECRET_STORE=true`, the CLI refuses to start rather than silently storing tokens outside the OS credential store.
36
-
37
- Platform notes:
38
-
39
- - macOS uses Keychain. Unlock the login keychain and approve Terminal or Node access if prompted.
40
- - Windows uses Credential Manager. Run from a normal signed-in desktop session with Credential Manager available.
41
- - Linux uses Secret Service. Install libsecret support and run from a session with an unlocked GNOME Keyring or KWallet. Headless Linux needs an explicit Secret Service setup for persistent CLI login.
33
+ If the `secret-store` check fails, the CLI cannot safely store tokens yet.
34
+
35
+ The plaintext file secret store is intentionally test-only. If `VIBECDR_MCP_INSECURE_SECRET_STORE_PATH` is set without `VIBECDR_MCP_ENABLE_INSECURE_SECRET_STORE=true`, the CLI refuses to start rather than silently storing tokens outside the OS credential store.
36
+
37
+ Platform notes:
38
+
39
+ - macOS uses Keychain. Unlock the login keychain and approve Terminal or Node access if prompted.
40
+ - Windows uses Credential Manager. Run from a normal signed-in desktop session with Credential Manager available.
41
+ - Linux uses Secret Service. Install libsecret support and run from a session with an unlocked GNOME Keyring or KWallet. Headless Linux needs an explicit Secret Service setup for persistent CLI login.
42
42
 
43
43
  ## Proxy or TLS issues
44
44
 
@@ -81,9 +81,9 @@ The CLI writes the native path.
81
81
 
82
82
  Codex config install and CLI login are separate.
83
83
 
84
- - `install codex` configures Codex
85
- - `login` authenticates the Vibecodr CLI only
86
- - Codex will still own its own OAuth behavior when you use the server inside Codex
84
+ - `install codex` configures Codex
85
+ - `login` authenticates the Vibecodr CLI only
86
+ - Codex will still own its own OAuth behavior when you use the server inside Codex
87
87
 
88
88
  ## VS Code CLI not found
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodr/cli",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
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",