@vibecodr/cli 0.1.8

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 (49) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +201 -0
  3. package/README.md +66 -0
  4. package/dist/auth/official-client.js +10 -0
  5. package/dist/auth/token-manager.js +424 -0
  6. package/dist/bin/vibecodr-mcp.js +101 -0
  7. package/dist/cli/errors.js +38 -0
  8. package/dist/cli/output.js +52 -0
  9. package/dist/cli/parse.js +84 -0
  10. package/dist/clients/base.js +30 -0
  11. package/dist/clients/codex.js +136 -0
  12. package/dist/clients/cursor.js +91 -0
  13. package/dist/clients/vscode.js +138 -0
  14. package/dist/clients/windsurf.js +81 -0
  15. package/dist/commands/call.js +123 -0
  16. package/dist/commands/config.js +124 -0
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/doctor.js +17 -0
  19. package/dist/commands/install.js +63 -0
  20. package/dist/commands/login.js +41 -0
  21. package/dist/commands/logout.js +26 -0
  22. package/dist/commands/pulse-setup.js +82 -0
  23. package/dist/commands/status.js +64 -0
  24. package/dist/commands/tools.js +82 -0
  25. package/dist/commands/uninstall.js +55 -0
  26. package/dist/core/interactive-input.js +114 -0
  27. package/dist/core/mcp-client.js +82 -0
  28. package/dist/core/renderers.js +34 -0
  29. package/dist/doctor/run.js +132 -0
  30. package/dist/platform/browser.js +79 -0
  31. package/dist/platform/exec.js +23 -0
  32. package/dist/platform/paths.js +36 -0
  33. package/dist/platform/prompt.js +19 -0
  34. package/dist/storage/config-store.js +72 -0
  35. package/dist/storage/file-lock.js +41 -0
  36. package/dist/storage/install-manifest.js +80 -0
  37. package/dist/storage/secret-store.js +301 -0
  38. package/dist/types/auth.js +1 -0
  39. package/dist/types/config.js +21 -0
  40. package/dist/types/install.js +1 -0
  41. package/docs/architecture.md +35 -0
  42. package/docs/auth.md +66 -0
  43. package/docs/clients.md +42 -0
  44. package/docs/commands.md +134 -0
  45. package/docs/contributors.md +68 -0
  46. package/docs/install.md +90 -0
  47. package/docs/licensing.md +20 -0
  48. package/docs/troubleshooting.md +97 -0
  49. package/package.json +40 -0
@@ -0,0 +1,80 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ import { writeFileWithBackup } from "./file-lock.js";
6
+ function windowsAppDataPath() {
7
+ return process.env["APPDATA"] || join(homedir(), "AppData", "Roaming");
8
+ }
9
+ export function defaultInstallManifestPath() {
10
+ if (process.env["VIBECDR_MCP_INSTALL_MANIFEST_PATH"])
11
+ return process.env["VIBECDR_MCP_INSTALL_MANIFEST_PATH"];
12
+ switch (process.platform) {
13
+ case "win32":
14
+ return join(windowsAppDataPath(), "Vibecodr", "MCP", "installs.json");
15
+ case "darwin":
16
+ return join(homedir(), "Library", "Application Support", "Vibecodr MCP", "installs.json");
17
+ default:
18
+ return join(process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config"), "vibecodr-mcp", "installs.json");
19
+ }
20
+ }
21
+ export class InstallManifestStore {
22
+ filePath;
23
+ constructor(filePath = defaultInstallManifestPath()) {
24
+ this.filePath = filePath;
25
+ }
26
+ async load() {
27
+ try {
28
+ const raw = await readFile(this.filePath, "utf8");
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(raw);
32
+ }
33
+ catch (error) {
34
+ throw new CliError("install.manifest_parse", `Install manifest at ${this.filePath} is not valid JSON.`, EXIT_CODES.installConflict, {
35
+ cause: error,
36
+ nextStep: "Repair or remove the invalid manifest file, then retry."
37
+ });
38
+ }
39
+ return {
40
+ version: 1,
41
+ installs: Array.isArray(parsed.installs) ? parsed.installs : []
42
+ };
43
+ }
44
+ catch (error) {
45
+ if (error.code === "ENOENT") {
46
+ return {
47
+ version: 1,
48
+ installs: []
49
+ };
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+ async save(manifest) {
55
+ await writeFileWithBackup(this.filePath, JSON.stringify(manifest, null, 2) + "\n");
56
+ }
57
+ async upsert(entry) {
58
+ const manifest = await this.load();
59
+ const installs = manifest.installs.filter((install) => !(install.client === entry.client && install.scope === entry.scope && install.name === entry.name && install.location === entry.location));
60
+ installs.push(entry);
61
+ await this.save({
62
+ version: 1,
63
+ installs
64
+ });
65
+ }
66
+ async remove(matcher) {
67
+ const manifest = await this.load();
68
+ const removed = manifest.installs.filter(matcher);
69
+ const remaining = manifest.installs.filter((entry) => !matcher(entry));
70
+ await this.save({
71
+ version: 1,
72
+ installs: remaining
73
+ });
74
+ return removed;
75
+ }
76
+ async find(matcher) {
77
+ const manifest = await this.load();
78
+ return manifest.installs.filter(matcher);
79
+ }
80
+ }
@@ -0,0 +1,301 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ import { secretStoreDirectory } from "../platform/paths.js";
6
+ import { writeFileWithBackup } from "./file-lock.js";
7
+ const SERVICE_NAME = "@vibecodr/mcp";
8
+ const KEY_BYTES = 32;
9
+ const CURRENT_SECRET_FILE_VERSION = 1;
10
+ const INSECURE_SECRET_STORE_PATH_ENV = "VIBECDR_MCP_INSECURE_SECRET_STORE_PATH";
11
+ const ENABLE_INSECURE_SECRET_STORE_ENV = "VIBECDR_MCP_ENABLE_INSECURE_SECRET_STORE";
12
+ let asyncEntryCtorPromise;
13
+ export function secureStoreHelpForPlatform(platform = process.platform) {
14
+ switch (platform) {
15
+ case "darwin":
16
+ return "macOS uses Keychain. Unlock the login keychain, allow Terminal or Node access if macOS prompts, then retry.";
17
+ case "win32":
18
+ return "Windows uses Credential Manager. Run from a normal signed-in desktop session and confirm Windows Credential Manager is available, then retry.";
19
+ case "linux":
20
+ return "Linux uses the Secret Service API. Install libsecret support and make sure a desktop keyring such as GNOME Keyring or KWallet is running and unlocked on the current D-Bus session; on headless Linux, run login from a session with a secret service or let the target MCP client own OAuth instead.";
21
+ default:
22
+ return "Enable a native credential store supported by @napi-rs/keyring for this OS, then retry.";
23
+ }
24
+ }
25
+ function secureStoreNextStep() {
26
+ return `${secureStoreHelpForPlatform()} The plaintext file secret store is only for local automated tests and requires ${ENABLE_INSECURE_SECRET_STORE_ENV}=true.`;
27
+ }
28
+ async function loadAsyncEntryCtor() {
29
+ if (!asyncEntryCtorPromise) {
30
+ asyncEntryCtorPromise = import("@napi-rs/keyring")
31
+ .then((mod) => mod.AsyncEntry)
32
+ .catch((error) => {
33
+ throw new CliError("storage.secret_store_unavailable", "The native secure credential store binding is unavailable.", EXIT_CODES.secretStoreUnavailable, {
34
+ cause: error,
35
+ nextStep: secureStoreNextStep()
36
+ });
37
+ });
38
+ }
39
+ return await asyncEntryCtorPromise;
40
+ }
41
+ function profileAccount(profile) {
42
+ return `profile:${profile}`;
43
+ }
44
+ function profileKeyAccount(profile) {
45
+ return `profile-key:${profile}`;
46
+ }
47
+ function base64UrlEncode(buf) {
48
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
49
+ }
50
+ function base64UrlDecode(value) {
51
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
52
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
53
+ return Buffer.from(padded, "base64");
54
+ }
55
+ function sanitizeProfile(profile) {
56
+ return encodeURIComponent(profile);
57
+ }
58
+ function isSessionRecord(value) {
59
+ return Boolean(value)
60
+ && typeof value === "object"
61
+ && value["schemaVersion"] === 1
62
+ && typeof value["serverUrl"] === "string"
63
+ && typeof value["accessToken"] === "string";
64
+ }
65
+ function encryptSession(session, key) {
66
+ const iv = randomBytes(12);
67
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
68
+ const plaintext = Buffer.from(JSON.stringify(session), "utf8");
69
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
70
+ const tag = cipher.getAuthTag();
71
+ return {
72
+ version: CURRENT_SECRET_FILE_VERSION,
73
+ iv: base64UrlEncode(iv),
74
+ tag: base64UrlEncode(tag),
75
+ ciphertext: base64UrlEncode(ciphertext)
76
+ };
77
+ }
78
+ function decryptSession(envelope, key) {
79
+ if (envelope.version !== CURRENT_SECRET_FILE_VERSION) {
80
+ throw new Error(`Unsupported secret file version: ${envelope.version}`);
81
+ }
82
+ const decipher = createDecipheriv("aes-256-gcm", key, base64UrlDecode(envelope.iv));
83
+ decipher.setAuthTag(base64UrlDecode(envelope.tag));
84
+ const plaintext = Buffer.concat([
85
+ decipher.update(base64UrlDecode(envelope.ciphertext)),
86
+ decipher.final()
87
+ ]).toString("utf8");
88
+ return JSON.parse(plaintext);
89
+ }
90
+ async function readFileStore(path) {
91
+ try {
92
+ return JSON.parse(await readFile(path, "utf8"));
93
+ }
94
+ catch (error) {
95
+ if (error.code === "ENOENT")
96
+ return {};
97
+ throw error;
98
+ }
99
+ }
100
+ async function writeFileStore(path, data) {
101
+ await mkdir(dirname(path), { recursive: true });
102
+ const tempPath = `${path}.tmp`;
103
+ await writeFile(tempPath, JSON.stringify(data, null, 2) + "\n", "utf8");
104
+ await rename(tempPath, path);
105
+ }
106
+ function resolveEnvFileStorePath() {
107
+ const path = process.env[INSECURE_SECRET_STORE_PATH_ENV]?.trim();
108
+ if (path === "undefined" || path === "null")
109
+ return undefined;
110
+ if (!path)
111
+ return undefined;
112
+ if (process.env[ENABLE_INSECURE_SECRET_STORE_ENV] !== "true") {
113
+ throw new CliError("storage.insecure_secret_store_blocked", "Refusing to use the plaintext secret store without explicit opt-in.", EXIT_CODES.secretStoreUnavailable, {
114
+ nextStep: `Unset ${INSECURE_SECRET_STORE_PATH_ENV}, or set ${ENABLE_INSECURE_SECRET_STORE_ENV}=true only for local tests.`
115
+ });
116
+ }
117
+ return path;
118
+ }
119
+ export class SecretStore {
120
+ fileStorePath;
121
+ encryptedStoreDir;
122
+ entryFactory;
123
+ constructor(options) {
124
+ this.fileStorePath = options?.fileStorePath ?? resolveEnvFileStorePath();
125
+ this.encryptedStoreDir = options?.encryptedStoreDir ?? secretStoreDirectory();
126
+ this.entryFactory = options?.entryFactory ?? (async (service, username) => {
127
+ const AsyncEntry = await loadAsyncEntryCtor();
128
+ return new AsyncEntry(service, username);
129
+ });
130
+ }
131
+ async entry(profile) {
132
+ return await this.entryFactory(SERVICE_NAME, profileAccount(profile));
133
+ }
134
+ async keyEntry(profile) {
135
+ return await this.entryFactory(SERVICE_NAME, profileKeyAccount(profile));
136
+ }
137
+ encryptedSessionPath(profile) {
138
+ return join(this.encryptedStoreDir, `${sanitizeProfile(profile)}.json`);
139
+ }
140
+ async loadLegacyInlineSession(profile) {
141
+ const entry = await this.entry(profile);
142
+ const raw = await entry.getPassword();
143
+ if (!raw)
144
+ return undefined;
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(raw);
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ return isSessionRecord(parsed) ? parsed : undefined;
153
+ }
154
+ async getOrCreateProfileKey(profile) {
155
+ const entry = await this.keyEntry(profile);
156
+ const raw = await entry.getPassword();
157
+ if (raw)
158
+ return base64UrlDecode(raw);
159
+ const generated = randomBytes(KEY_BYTES);
160
+ await entry.setPassword(base64UrlEncode(generated));
161
+ return generated;
162
+ }
163
+ async readEncryptedSession(profile) {
164
+ const path = this.encryptedSessionPath(profile);
165
+ try {
166
+ const raw = await readFile(path, "utf8");
167
+ const parsed = JSON.parse(raw);
168
+ const keyEntry = await this.keyEntry(profile);
169
+ const encodedKey = await keyEntry.getPassword();
170
+ if (!encodedKey)
171
+ return undefined;
172
+ const session = decryptSession(parsed, base64UrlDecode(encodedKey));
173
+ return isSessionRecord(session) ? session : undefined;
174
+ }
175
+ catch (error) {
176
+ if (error.code === "ENOENT")
177
+ return undefined;
178
+ throw error;
179
+ }
180
+ }
181
+ async persistEncryptedSession(profile, session) {
182
+ const path = this.encryptedSessionPath(profile);
183
+ await mkdir(dirname(path), { recursive: true });
184
+ const key = await this.getOrCreateProfileKey(profile);
185
+ const envelope = encryptSession(session, key);
186
+ await writeFileWithBackup(path, JSON.stringify(envelope, null, 2) + "\n");
187
+ }
188
+ async removeEncryptedSession(profile) {
189
+ const path = this.encryptedSessionPath(profile);
190
+ try {
191
+ await readFile(path, "utf8");
192
+ await rm(path, { force: true });
193
+ return true;
194
+ }
195
+ catch (error) {
196
+ if (error.code === "ENOENT")
197
+ return false;
198
+ throw error;
199
+ }
200
+ }
201
+ async deleteCredentialBestEffort(loadEntry) {
202
+ try {
203
+ const entry = await loadEntry;
204
+ return await entry.deleteCredential();
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
210
+ async get(profile) {
211
+ if (this.fileStorePath) {
212
+ const data = await readFileStore(this.fileStorePath);
213
+ return data[profile];
214
+ }
215
+ try {
216
+ const encrypted = await this.readEncryptedSession(profile);
217
+ if (encrypted)
218
+ return encrypted;
219
+ const legacy = await this.loadLegacyInlineSession(profile);
220
+ if (legacy) {
221
+ await this.persistEncryptedSession(profile, legacy);
222
+ await (await this.entry(profile)).deleteCredential().catch(() => undefined);
223
+ return legacy;
224
+ }
225
+ return undefined;
226
+ }
227
+ catch (error) {
228
+ throw new CliError("storage.secret_store_read_failed", "Unable to read the secure credential store.", EXIT_CODES.secretStoreUnavailable, {
229
+ cause: error,
230
+ nextStep: secureStoreNextStep()
231
+ });
232
+ }
233
+ }
234
+ async set(profile, session) {
235
+ if (this.fileStorePath) {
236
+ const data = await readFileStore(this.fileStorePath);
237
+ data[profile] = session;
238
+ await writeFileStore(this.fileStorePath, data);
239
+ return;
240
+ }
241
+ try {
242
+ await this.persistEncryptedSession(profile, session);
243
+ }
244
+ catch (error) {
245
+ throw new CliError("storage.secret_store_write_failed", "Unable to write to the secure credential store.", EXIT_CODES.secretStoreUnavailable, {
246
+ cause: error,
247
+ nextStep: secureStoreNextStep()
248
+ });
249
+ }
250
+ }
251
+ async delete(profile) {
252
+ if (this.fileStorePath) {
253
+ const data = await readFileStore(this.fileStorePath);
254
+ const existed = Boolean(data[profile]);
255
+ delete data[profile];
256
+ await writeFileStore(this.fileStorePath, data);
257
+ return existed;
258
+ }
259
+ try {
260
+ const [encryptedRemoved, legacyRemoved, keyRemoved] = await Promise.all([
261
+ this.removeEncryptedSession(profile),
262
+ this.deleteCredentialBestEffort(this.entry(profile)),
263
+ this.deleteCredentialBestEffort(this.keyEntry(profile))
264
+ ]);
265
+ return encryptedRemoved || legacyRemoved || keyRemoved;
266
+ }
267
+ catch (error) {
268
+ throw new CliError("storage.secret_store_delete_failed", "Unable to update the secure credential store.", EXIT_CODES.secretStoreUnavailable, {
269
+ cause: error,
270
+ nextStep: secureStoreNextStep()
271
+ });
272
+ }
273
+ }
274
+ async checkAvailability() {
275
+ if (this.fileStorePath) {
276
+ const data = await readFileStore(this.fileStorePath);
277
+ await writeFileStore(this.fileStorePath, data);
278
+ return { ok: true, summary: "Contributor-only file secret store is enabled." };
279
+ }
280
+ const profile = `doctor:${randomUUID()}`;
281
+ try {
282
+ await this.set(profile, {
283
+ schemaVersion: 1,
284
+ serverUrl: "https://example.com/mcp",
285
+ accessToken: "test",
286
+ registrationMode: "manual",
287
+ authorizationServerUrl: "https://example.com",
288
+ clientInformation: { client_id: "test" },
289
+ updatedAt: new Date().toISOString()
290
+ });
291
+ await this.delete(profile);
292
+ return { ok: true, summary: "Secure credential store is available." };
293
+ }
294
+ catch (error) {
295
+ if (error instanceof CliError) {
296
+ return { ok: false, summary: error.nextStep ? `${error.message} ${error.nextStep}` : error.message };
297
+ }
298
+ return { ok: false, summary: String(error) };
299
+ }
300
+ }
301
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ export const CONFIG_VERSION = 1;
2
+ export const DEFAULT_PROFILE = "default";
3
+ export const DEFAULT_SERVER_URL = "https://openai.vibecodr.space/mcp";
4
+ export function defaultProfileConfig() {
5
+ return {
6
+ serverUrl: DEFAULT_SERVER_URL,
7
+ browserMode: "print",
8
+ registrationMode: "auto",
9
+ defaultInstallScope: "user",
10
+ logLevel: "normal"
11
+ };
12
+ }
13
+ export function defaultConfigFile() {
14
+ return {
15
+ version: CONFIG_VERSION,
16
+ currentProfile: DEFAULT_PROFILE,
17
+ profiles: {
18
+ [DEFAULT_PROFILE]: defaultProfileConfig()
19
+ }
20
+ };
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +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
package/docs/auth.md ADDED
@@ -0,0 +1,66 @@
1
+ # Auth
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.
4
+
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`
10
+
11
+ ## Implemented now
12
+
13
+ - protected-resource and authorization-server discovery against the MCP server
14
+ - PKCE S256 enforcement
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.
31
+
32
+ ## Registration modes
33
+
34
+ The CLI understands these internal modes:
35
+
36
+ - `auto`
37
+ - `preregistered`
38
+ - `cimd`
39
+ - `dcr`
40
+ - `manual`
41
+
42
+ Current repo reality:
43
+
44
+ - `auto` now uses the committed official client metadata document URL for `https://openai.vibecodr.space/mcp`
45
+ - `cimd` for non-official servers still requires a real `VIBECDR_MCP_CIMD_CLIENT_ID` URL
46
+ - `dcr` works when the authorization server advertises `registration_endpoint`
47
+ - `manual` works with `VIBECDR_MCP_MANUAL_CLIENT_ID` or an interactive prompt
48
+
49
+ ## Runtime behavior
50
+
51
+ - `login` prints the authorization URL by default so the browser step is explicit and reliable across shells
52
+ - `login --browser open` opts into automatic browser launch
53
+ - `status` reads local session state without requiring the network unless `--probe` is used
54
+ - `tools` and `call` will attempt to reuse the stored session
55
+ - if the access token is close to expiry and a refresh token is present, the CLI refreshes before making the MCP request
56
+
57
+ ## Verified now
58
+
59
+ - automated mock coverage exercises DCR login, loopback callback handling, refresh, and logout revocation behavior
60
+ - unauthenticated `tools` works against public server surfaces without forcing login first
61
+ - unauthenticated public `call` works for noauth tools, while protected flows retry with refresh or interactive login
62
+
63
+ ## Remaining constraints
64
+
65
+ - CIMD for non-official servers still needs a real externally hosted client-id metadata document to be genuinely usable
66
+ - dedicated scope step-up UX is still folded into the normal re-auth path rather than a specialized prompt flow
@@ -0,0 +1,42 @@
1
+ # Clients
2
+
3
+ This matrix reflects current repo reality, not the full target spec.
4
+
5
+ | Client | Install status now | Uninstall status now | Scope support now | Auth owner |
6
+ | --- | --- | --- | --- | --- |
7
+ | Codex | Implemented | Implemented via CLI or TOML fallback | User | Codex |
8
+ | Cursor | Implemented | Implemented | User + project | Cursor |
9
+ | VS Code | Implemented | Project uninstall implemented, user uninstall not automated | User + project | VS Code |
10
+ | Windsurf | Implemented | Implemented | User | Windsurf |
11
+ | Direct CLI | Implemented | N/A | N/A | Vibecodr MCP CLI |
12
+
13
+ ## Exact surfaces used now
14
+
15
+ ### Codex
16
+
17
+ - primary install: `codex mcp add`
18
+ - primary uninstall: `codex mcp remove`
19
+ - fallback file: `~/.codex/config.toml`
20
+
21
+ ### Cursor
22
+
23
+ - user file: `~/.cursor/mcp.json`
24
+ - project file: `.cursor/mcp.json`
25
+ - optional open-client deeplink
26
+
27
+ ### VS Code
28
+
29
+ - user install: `code --add-mcp`
30
+ - user fallback when `--open-client` is present: `vscode:mcp/install?...`
31
+ - project file: `.vscode/mcp.json`
32
+
33
+ ### Windsurf
34
+
35
+ - native file: `~/.codeium/windsurf/mcp_config.json`
36
+ - docs note for legacy plugin path: `~/.codeium/mcp_config.json`
37
+
38
+ ## Current caveats
39
+
40
+ - installer adapters exist, but the direct CLI runtime remains the primary proofed surface
41
+ - VS Code user-scope uninstall still lacks a documented automated removal path in this repo
42
+ - enterprise allowlists, team policy blocks, and client-side OAuth behavior still need live validation in real environments
@@ -0,0 +1,134 @@
1
+ # Commands
2
+
3
+ This page documents the command surface implemented in the current repo.
4
+
5
+ ## Global flags
6
+
7
+ All commands accept:
8
+
9
+ - `--profile <name>`
10
+ - `--server-url <url>`
11
+ - `--json`
12
+ - `--verbose`
13
+ - `--non-interactive`
14
+
15
+ ## Commands
16
+
17
+ ### `login`
18
+
19
+ Syntax:
20
+
21
+ `vibecodr login [--scope <oauth-scope>] [--registration auto|preregistered|cimd|dcr|manual] [--browser open|print] [--timeout-sec <n>]`
22
+
23
+ Use this to authenticate the CLI itself.
24
+
25
+ Current default:
26
+
27
+ - `login` prints the authorization URL and waits for the loopback callback
28
+ - `--browser open` opts into automatic browser launch
29
+
30
+ ### `logout`
31
+
32
+ Syntax:
33
+
34
+ `vibecodr logout [--all] [--no-revoke]`
35
+
36
+ Use this to clear CLI auth state. It does not touch editor-owned auth.
37
+
38
+ ### `status`
39
+
40
+ Syntax:
41
+
42
+ `vibecodr status [--probe] [--show-installs]`
43
+
44
+ Without `--probe`, this reads only local state.
45
+
46
+ ### `tools`
47
+
48
+ Syntax:
49
+
50
+ `vibecodr tools [<tool-name>] [--search <text>] [--schema] [--no-login]`
51
+
52
+ This always reads the live tool catalog from the MCP server.
53
+
54
+ ### `call`
55
+
56
+ Syntax:
57
+
58
+ `vibecodr call <tool-name> [--input-json <json>] [--input-file <path>] [--stdin] [--interactive] [--no-login]`
59
+
60
+ `--interactive` currently supports top-level scalar object fields.
61
+
62
+ 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
+
64
+ ### `pulse-setup`
65
+
66
+ Syntax:
67
+
68
+ `vibecodr pulse-setup [--json] [--descriptor-setup-json <json> | --descriptor-setup-file <path>]`
69
+
70
+ Calls the live `get_pulse_setup_guidance` MCP tool. Pass a `PulseDescriptorSetupProjection` through `--descriptor-setup-json` or `--descriptor-setup-file` when you have one; the CLI forwards it as `descriptorSetup` and verifies the MCP response evaluated that descriptor. Without a descriptor projection, the command returns general Pulse setup rules and must not be treated as proof that a specific Pulse needs or does not need backend setup.
71
+
72
+ The CLI does not maintain separate Pulse setup copy; it reads MCP output derived from the API projection owned by `PulseDescriptor`.
73
+
74
+ 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
+
76
+ ### `doctor`
77
+
78
+ Syntax:
79
+
80
+ `vibecodr doctor [--client <client>]`
81
+
82
+ Supported client probes now:
83
+
84
+ - `codex`
85
+ - `cursor`
86
+ - `vscode`
87
+ - `windsurf`
88
+
89
+ ### `config`
90
+
91
+ Syntax:
92
+
93
+ - `vibecodr config path`
94
+ - `vibecodr config show`
95
+ - `vibecodr config set <key> <value>`
96
+ - `vibecodr config unset <key>`
97
+ - `vibecodr config profile list`
98
+ - `vibecodr config profile create <name> [--server-url <url>]`
99
+ - `vibecodr config profile use <name>`
100
+ - `vibecodr config profile delete <name> [--force]`
101
+
102
+ ### `install`
103
+
104
+ Syntax:
105
+
106
+ `vibecodr install <codex|cursor|vscode|windsurf> [--scope user|project] [--path <dir>] [--name <server-name>] [--open-client] [--overwrite] [--dry-run]`
107
+
108
+ Install config only. Runtime auth remains CLI-owned or editor-owned depending on where the server is used.
109
+
110
+ ### `uninstall`
111
+
112
+ Syntax:
113
+
114
+ `vibecodr uninstall <codex|cursor|vscode|windsurf> [--scope user|project] [--path <dir>] [--name <server-name>] [--dry-run]`
115
+
116
+ ## Exit codes
117
+
118
+ - `0` success
119
+ - `1` runtime or doctor check failure
120
+ - `2` usage error
121
+ - `3` config or filesystem error
122
+ - `4` auth required but unavailable in current mode
123
+ - `5` auth failed
124
+ - `6` network failure
125
+ - `7` protocol or discovery failure
126
+ - `8` tool failure
127
+ - `9` unsupported client or missing required executable
128
+ - `10` install or uninstall conflict
129
+ - `11` secure credential store unavailable
130
+ - `12` cancellation or auth timeout
131
+
132
+ ## Current note
133
+
134
+ The commands above are implemented now. The main remaining product constraint is VS Code user-scope uninstall, because there is still no documented removal surface that this repo can safely automate without inventing one.