@vibecodetown/mcp-server 2.1.4 → 2.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.
@@ -0,0 +1,146 @@
1
+ // adapters/mcp-ts/src/auth/credential_store.ts
2
+ // Secure local credential storage for GitHub/NPM tokens
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { readFile, writeFile, mkdir, unlink, chmod } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ const CONFIG_DIR = join(homedir(), ".vibe-pm");
8
+ const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
9
+ /**
10
+ * Credential Store - Secure local storage for API tokens
11
+ *
12
+ * Storage: ~/.vibe-pm/credentials.json (0o600 permissions)
13
+ * Override: VIBECODE_CREDENTIALS_FILE env var
14
+ *
15
+ * Priority for GitHub token:
16
+ * 1. GITHUB_TOKEN env var
17
+ * 2. GH_TOKEN env var
18
+ * 3. Stored credential
19
+ *
20
+ * Priority for NPM token:
21
+ * 1. NPM_TOKEN env var
22
+ * 2. Stored credential
23
+ */
24
+ export class CredentialStore {
25
+ cache = null;
26
+ resolveCredentialsPath() {
27
+ const override = (process.env.VIBECODE_CREDENTIALS_FILE || "").trim();
28
+ return override || CREDENTIALS_FILE;
29
+ }
30
+ /**
31
+ * Get GitHub token (env var takes priority)
32
+ */
33
+ async getGitHubToken() {
34
+ // Env vars take priority
35
+ const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
36
+ if (envToken)
37
+ return envToken.trim();
38
+ // Fall back to stored credential
39
+ const creds = await this.load();
40
+ return creds?.github_token || null;
41
+ }
42
+ /**
43
+ * Get NPM token (env var takes priority)
44
+ */
45
+ async getNpmToken() {
46
+ // Env var takes priority
47
+ const envToken = process.env.NPM_TOKEN;
48
+ if (envToken)
49
+ return envToken.trim();
50
+ // Fall back to stored credential
51
+ const creds = await this.load();
52
+ return creds?.npm_token || null;
53
+ }
54
+ /**
55
+ * Store GitHub token
56
+ */
57
+ async setGitHubToken(token) {
58
+ const creds = (await this.load()) || {};
59
+ creds.github_token = token;
60
+ creds.updated_at = new Date().toISOString();
61
+ await this.save(creds);
62
+ }
63
+ /**
64
+ * Store NPM token
65
+ */
66
+ async setNpmToken(token) {
67
+ const creds = (await this.load()) || {};
68
+ creds.npm_token = token;
69
+ creds.updated_at = new Date().toISOString();
70
+ await this.save(creds);
71
+ }
72
+ /**
73
+ * Remove a credential
74
+ */
75
+ async remove(key) {
76
+ const creds = await this.load();
77
+ if (!creds)
78
+ return;
79
+ delete creds[key];
80
+ creds.updated_at = new Date().toISOString();
81
+ await this.save(creds);
82
+ }
83
+ /**
84
+ * Clear all credentials
85
+ */
86
+ async clear() {
87
+ this.cache = null;
88
+ const credPath = this.resolveCredentialsPath();
89
+ if (existsSync(credPath)) {
90
+ await unlink(credPath);
91
+ }
92
+ }
93
+ /**
94
+ * Check what credentials are stored (without exposing values)
95
+ */
96
+ async status() {
97
+ const creds = await this.load();
98
+ return {
99
+ github: !!creds?.github_token,
100
+ npm: !!creds?.npm_token,
101
+ path: this.resolveCredentialsPath(),
102
+ };
103
+ }
104
+ /**
105
+ * Load credentials from disk
106
+ */
107
+ async load() {
108
+ if (this.cache)
109
+ return this.cache;
110
+ const credPath = this.resolveCredentialsPath();
111
+ if (!existsSync(credPath))
112
+ return null;
113
+ try {
114
+ const data = await readFile(credPath, "utf-8");
115
+ this.cache = JSON.parse(data);
116
+ return this.cache;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ /**
123
+ * Save credentials to disk with secure permissions
124
+ */
125
+ async save(creds) {
126
+ this.cache = creds;
127
+ // Ensure config directory exists
128
+ if (!existsSync(CONFIG_DIR)) {
129
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
130
+ }
131
+ const credPath = this.resolveCredentialsPath();
132
+ const data = JSON.stringify(creds, null, 2);
133
+ // Write with restricted permissions (owner read/write only)
134
+ await writeFile(credPath, data, { mode: 0o600 });
135
+ // Ensure permissions on existing file
136
+ await chmod(credPath, 0o600);
137
+ }
138
+ }
139
+ // Singleton instance
140
+ let _store = null;
141
+ export function getCredentialStore() {
142
+ if (!_store) {
143
+ _store = new CredentialStore();
144
+ }
145
+ return _store;
146
+ }
@@ -5,11 +5,13 @@
5
5
  * - Token caching and persistence
6
6
  * - Offline JWT verification
7
7
  * - Feature gating based on entitlements
8
+ * - Credential storage (GitHub/NPM tokens)
8
9
  */
9
10
  export { TokenCache } from './token_cache.js';
10
11
  export { TokenVerifier } from './token_verifier.js';
11
12
  export { AuthGate } from './gate.js';
12
13
  export { PUBLIC_KEY } from './public_key.js';
14
+ export { CredentialStore, getCredentialStore } from './credential_store.js';
13
15
  // Re-export convenience functions
14
16
  import { TokenCache } from './token_cache.js';
15
17
  import { TokenVerifier } from './token_verifier.js';
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * Provides gate enforcement for work orders before execution.
5
5
  * All work orders must pass through the gate before any work can proceed.
6
+ *
7
+ * All subprocess invocations go through cli_invoker for centralized management.
6
8
  */
7
- import { spawn } from "child_process";
9
+ import { invokeSystem } from "../runtime/cli_invoker.js";
8
10
  import { WorkOrderV1Schema } from "../generated/work_order_v1.js";
9
11
  import { GateResultV1Schema } from "../generated/gate_result_v1.js";
10
12
  /**
@@ -30,76 +32,56 @@ export async function runSystemDesignGate(workOrder, options) {
30
32
  pushBool("fail-fast", options.failFast);
31
33
  pushBool("semgrep", options.semgrep);
32
34
  pushBool("runtime", options.runtime);
33
- return new Promise((resolve) => {
34
- const child = spawn(pythonExe, [
35
- "-m",
36
- "vibecoding_helper",
37
- "--repo-root",
38
- repoRoot,
39
- "gate-check",
40
- ...overrideFlags,
41
- "--work-order",
42
- workOrderJson,
43
- "--format",
44
- "json",
45
- ], {
46
- cwd: repoRoot,
47
- timeout: timeoutMs,
48
- env: { ...process.env, PYTHONIOENCODING: "utf-8" },
49
- });
50
- let stdout = "";
51
- let stderr = "";
52
- child.stdout?.on("data", (data) => {
53
- stdout += data.toString();
54
- });
55
- child.stderr?.on("data", (data) => {
56
- stderr += data.toString();
57
- });
58
- child.on("close", (code) => {
59
- // Exit code 0 = ALLOW, 2 = BLOCK, other = error
60
- if (code === null) {
61
- resolve({
62
- success: false,
63
- error: "Gate check timed out",
64
- });
65
- return;
66
- }
67
- if (code !== 0 && code !== 2) {
68
- resolve({
69
- success: false,
70
- error: `Gate check failed with code ${code}: ${stderr || stdout}`,
71
- });
72
- return;
73
- }
74
- try {
75
- const result = JSON.parse(stdout.trim());
76
- const validated = GateResultV1Schema.safeParse(result);
77
- if (!validated.success) {
78
- resolve({
79
- success: false,
80
- error: `Invalid gate result: ${validated.error.message}`,
81
- });
82
- return;
83
- }
84
- resolve({
85
- success: true,
86
- result: validated.data,
87
- });
88
- }
89
- catch (e) {
90
- resolve({
91
- success: false,
92
- error: `Failed to parse gate result: ${e instanceof Error ? e.message : String(e)}`,
93
- });
94
- }
95
- });
96
- child.on("error", (err) => {
97
- resolve({
98
- success: false,
99
- error: `Gate check process error: ${err.message}`,
100
- });
101
- });
35
+ const args = [
36
+ "-m",
37
+ "vibecoding_helper",
38
+ "--repo-root",
39
+ repoRoot,
40
+ "gate-check",
41
+ ...overrideFlags,
42
+ "--work-order",
43
+ workOrderJson,
44
+ "--format",
45
+ "json",
46
+ ];
47
+ const result = await invokeSystem(pythonExe, args, repoRoot, {
48
+ env: { PYTHONIOENCODING: "utf-8" },
49
+ timeoutMs,
102
50
  });
51
+ // Exit code 124 = timeout
52
+ if (result.exitCode === 124) {
53
+ return {
54
+ success: false,
55
+ error: "Gate check timed out",
56
+ };
57
+ }
58
+ // Exit code 0 = ALLOW, 2 = BLOCK, other = error
59
+ if (result.exitCode !== 0 && result.exitCode !== 2) {
60
+ return {
61
+ success: false,
62
+ error: `Gate check failed with code ${result.exitCode}: ${result.stderr || result.stdout}`,
63
+ };
64
+ }
65
+ try {
66
+ const parsed = JSON.parse(result.stdout.trim());
67
+ const validated = GateResultV1Schema.safeParse(parsed);
68
+ if (!validated.success) {
69
+ return {
70
+ success: false,
71
+ error: `Invalid gate result: ${validated.error.message}`,
72
+ };
73
+ }
74
+ return {
75
+ success: true,
76
+ result: validated.data,
77
+ };
78
+ }
79
+ catch (e) {
80
+ return {
81
+ success: false,
82
+ error: `Failed to parse gate result: ${e instanceof Error ? e.message : String(e)}`,
83
+ };
84
+ }
103
85
  }
104
86
  /**
105
87
  * Execute a work order only if it passes the gate
package/build/index.js CHANGED
@@ -71,6 +71,8 @@ server.tool("vibe_pm.export_output", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.export_o
71
71
  server.tool("vibe_pm.search_oss", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.search_oss"], pm.searchOssInputSchema.shape, async (input) => pm.vibePmSearchOss(pm.searchOssInputSchema.parse(input)));
72
72
  server.tool("vibe_pm.zoekt_evidence", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.zoekt_evidence"], pm.zoektEvidenceInputSchema.shape, async (input) => pm.vibePmZoektEvidence(pm.zoektEvidenceInputSchema.parse(input)));
73
73
  server.tool("vibe_pm.gate", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.gate"], pm.gateInputSchema.shape, async (input) => pm.vibePmGate(pm.gateInputSchema.parse(input)));
74
+ server.tool("vibe_pm.save_rule", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.save_rule"], pm.saveRuleInputSchema.shape, async (input) => pm.vibePmSaveRule(pm.saveRuleInputSchema.parse(input)));
75
+ server.tool("vibe_pm.list_rules", VIBE_PM_TOOL_DESCRIPTIONS["vibe_pm.list_rules"], pm.listRulesInputSchema.shape, async (input) => pm.vibePmListRules(pm.listRulesInputSchema.parse(input)));
74
76
  // ============================================================
75
77
  // OPTIONAL: react_perf.* Tools (Performance Analysis)
76
78
  // ============================================================
@@ -1,33 +1,47 @@
1
- import { spawnSync } from "node:child_process";
1
+ /**
2
+ * Git utilities for local mode
3
+ *
4
+ * All git commands go through cli_invoker for centralized subprocess management.
5
+ *
6
+ * @module local-mode/git
7
+ */
8
+ import { invokeGitSync } from "../runtime/cli_invoker.js";
9
+ /**
10
+ * Get the git repository root directory
11
+ *
12
+ * @param cwd - Current working directory
13
+ * @returns Git root path or null if not in a git repository
14
+ */
2
15
  export function getGitRoot(cwd) {
3
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
4
- cwd,
5
- encoding: "utf-8",
6
- stdio: ["ignore", "pipe", "pipe"],
7
- });
8
- if (result.status !== 0)
16
+ const result = invokeGitSync(["rev-parse", "--show-toplevel"], cwd);
17
+ if (result.exitCode !== 0)
9
18
  return null;
10
- const out = (result.stdout ?? "").trim();
19
+ const out = result.stdout.trim();
11
20
  return out.length > 0 ? out : null;
12
21
  }
22
+ /**
23
+ * Get the configured git hooks path
24
+ *
25
+ * @param cwd - Current working directory (should be git root)
26
+ * @returns Configured hooks path or null if not set
27
+ */
13
28
  export function getGitHooksPath(cwd) {
14
- const result = spawnSync("git", ["config", "--local", "--get", "core.hooksPath"], {
15
- cwd,
16
- encoding: "utf-8",
17
- stdio: ["ignore", "pipe", "pipe"],
18
- });
19
- if (result.status !== 0)
29
+ const result = invokeGitSync(["config", "--local", "--get", "core.hooksPath"], cwd);
30
+ if (result.exitCode !== 0)
20
31
  return null;
21
- const out = (result.stdout ?? "").trim();
32
+ const out = result.stdout.trim();
22
33
  return out.length > 0 ? out : null;
23
34
  }
35
+ /**
36
+ * Set the git hooks path
37
+ *
38
+ * @param cwd - Current working directory (should be git root)
39
+ * @param hooksPath - Path to hooks directory
40
+ * @returns Success or error
41
+ */
24
42
  export function setGitHooksPath(cwd, hooksPath) {
25
- const result = spawnSync("git", ["config", "--local", "core.hooksPath", hooksPath], {
26
- cwd,
27
- encoding: "utf-8",
28
- stdio: ["ignore", "pipe", "pipe"],
29
- });
30
- if (result.status === 0)
43
+ const result = invokeGitSync(["config", "--local", "core.hooksPath", hooksPath], cwd);
44
+ if (result.exitCode === 0)
31
45
  return { ok: true };
32
- return { ok: false, error: (result.stderr ?? "git config failed").trim() };
46
+ return { ok: false, error: (result.stderr || "git config failed").trim() };
33
47
  }
@@ -0,0 +1,176 @@
1
+ // adapters/mcp-ts/src/local-mode/project-state.ts
2
+ // Per-folder project state persistence
3
+ // Stores workflow state, reminders, and pending actions
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { getVibeRepoPaths } from "./paths.js";
7
+ const DEFAULT_STATE = {
8
+ schema_version: 1,
9
+ phases: [],
10
+ pending_actions: [],
11
+ reminders: [],
12
+ };
13
+ /**
14
+ * Load project state from .vibe/project_state.json
15
+ */
16
+ export function loadProjectState(basePath = process.cwd()) {
17
+ const paths = getVibeRepoPaths(basePath);
18
+ const statePath = path.join(paths.vibeDir, "project_state.json");
19
+ if (!fs.existsSync(statePath)) {
20
+ return null;
21
+ }
22
+ try {
23
+ const content = fs.readFileSync(statePath, "utf-8");
24
+ return JSON.parse(content);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * Save project state to .vibe/project_state.json
32
+ */
33
+ export function saveProjectState(state, basePath = process.cwd()) {
34
+ const paths = getVibeRepoPaths(basePath);
35
+ const statePath = path.join(paths.vibeDir, "project_state.json");
36
+ // Ensure .vibe directory exists
37
+ if (!fs.existsSync(paths.vibeDir)) {
38
+ fs.mkdirSync(paths.vibeDir, { recursive: true });
39
+ }
40
+ state.updated_at = new Date().toISOString();
41
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
42
+ }
43
+ /**
44
+ * Get or create project state
45
+ */
46
+ export function getProjectState(basePath = process.cwd()) {
47
+ const existing = loadProjectState(basePath);
48
+ if (existing) {
49
+ return existing;
50
+ }
51
+ const projectId = path.basename(basePath).toLowerCase().replace(/[^a-z0-9_-]/g, "_");
52
+ return {
53
+ ...DEFAULT_STATE,
54
+ project_id: projectId,
55
+ updated_at: new Date().toISOString(),
56
+ };
57
+ }
58
+ /**
59
+ * Add a pending action
60
+ */
61
+ export function addPendingAction(action, basePath = process.cwd()) {
62
+ const state = getProjectState(basePath);
63
+ // Check for duplicate
64
+ const exists = state.pending_actions.some((a) => a.type === action.type && a.reason === action.reason);
65
+ if (exists)
66
+ return;
67
+ state.pending_actions.push({
68
+ ...action,
69
+ created_at: new Date().toISOString(),
70
+ });
71
+ saveProjectState(state, basePath);
72
+ }
73
+ /**
74
+ * Remove a pending action by type
75
+ */
76
+ export function removePendingAction(type, basePath = process.cwd()) {
77
+ const state = getProjectState(basePath);
78
+ state.pending_actions = state.pending_actions.filter((a) => a.type !== type);
79
+ saveProjectState(state, basePath);
80
+ }
81
+ /**
82
+ * Get all pending actions (for reminders)
83
+ */
84
+ export function getPendingActions(basePath = process.cwd()) {
85
+ const state = loadProjectState(basePath);
86
+ return state?.pending_actions || [];
87
+ }
88
+ /**
89
+ * Update last commit info
90
+ */
91
+ export function updateLastCommit(info, basePath = process.cwd()) {
92
+ const state = getProjectState(basePath);
93
+ state.last_commit = info;
94
+ // If pushed, remove git_push pending action
95
+ if (info.pushed) {
96
+ state.pending_actions = state.pending_actions.filter((a) => a.type !== "git_push");
97
+ }
98
+ else {
99
+ // Add git_push pending action if not pushed
100
+ const exists = state.pending_actions.some((a) => a.type === "git_push");
101
+ if (!exists) {
102
+ state.pending_actions.push({
103
+ type: "git_push",
104
+ reason: `커밋 ${info.hash.slice(0, 7)} 푸시 필요`,
105
+ created_at: new Date().toISOString(),
106
+ priority: "medium",
107
+ });
108
+ }
109
+ }
110
+ saveProjectState(state, basePath);
111
+ }
112
+ /**
113
+ * Update last MCP build info
114
+ */
115
+ export function updateLastMcpBuild(info, basePath = process.cwd()) {
116
+ const state = getProjectState(basePath);
117
+ state.last_mcp_build = info;
118
+ // If published, remove mcp_build and npm_publish pending actions
119
+ if (info.published) {
120
+ state.pending_actions = state.pending_actions.filter((a) => a.type !== "mcp_build" && a.type !== "npm_publish");
121
+ }
122
+ saveProjectState(state, basePath);
123
+ }
124
+ /**
125
+ * Format pending actions as reminder text
126
+ */
127
+ export function formatReminders(basePath = process.cwd()) {
128
+ const actions = getPendingActions(basePath);
129
+ if (actions.length === 0)
130
+ return null;
131
+ const lines = ["## 📋 대기 중인 작업"];
132
+ // Sort by priority
133
+ const sorted = [...actions].sort((a, b) => {
134
+ const order = { high: 0, medium: 1, low: 2 };
135
+ return order[a.priority] - order[b.priority];
136
+ });
137
+ for (const action of sorted) {
138
+ const emoji = action.priority === "high" ? "🔴" : action.priority === "medium" ? "🟡" : "🟢";
139
+ const tool = getToolForAction(action.type);
140
+ lines.push(`- ${emoji} ${action.reason}${tool ? ` → \`${tool}\`` : ""}`);
141
+ }
142
+ return lines.join("\n");
143
+ }
144
+ function getToolForAction(type) {
145
+ const mapping = {
146
+ mcp_build: "vibe_pm.publish_mcp",
147
+ engine_build: null, // Manual
148
+ npm_publish: "vibe_pm.publish_mcp",
149
+ git_push: "vibe_pm.finalize_work",
150
+ review: "vibe_pm.inspect_code",
151
+ custom: null,
152
+ };
153
+ return mapping[type];
154
+ }
155
+ /**
156
+ * Check and add MCP build pending action if needed
157
+ */
158
+ export async function checkAndAddMcpBuildAction(basePath = process.cwd()) {
159
+ // Dynamically import to avoid circular dependency
160
+ const { needsMcpBuild } = await import("../tools/vibe_pm/publish_mcp.js");
161
+ try {
162
+ const needs = await needsMcpBuild(basePath);
163
+ if (needs) {
164
+ addPendingAction({
165
+ type: "mcp_build",
166
+ reason: "MCP 소스 변경됨 - 빌드 필요",
167
+ priority: "high",
168
+ }, basePath);
169
+ return true;
170
+ }
171
+ }
172
+ catch {
173
+ // ignore
174
+ }
175
+ return false;
176
+ }
@@ -763,9 +763,9 @@ jobs:
763
763
  - name: Run Vibe guard
764
764
  shell: bash
765
765
  env:
766
- # Default: WARN does not fail the job (seatbelt philosophy).
767
- # Opt-in strict mode: set to "true" in the generated workflow.
768
- VIBE_FAIL_ON_WARN: "false"
766
+ # P0-2: CI 환경에서는 WARN도 빌드 실패 처리 (기본값)
767
+ # 이전 동작이 필요하면 "false" 변경
768
+ VIBE_FAIL_ON_WARN: "true"
769
769
  run: |
770
770
  set -euo pipefail
771
771
  if [[ ! -f ".vibe/lib/validate.sh" ]]; then