supipowers 2.0.1 → 2.1.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/README.md +10 -6
- package/package.json +4 -2
- package/skills/harness/SKILL.md +1 -0
- package/src/bootstrap.ts +5 -133
- package/src/commands/clear.ts +6 -6
- package/src/commands/release.ts +3 -1
- package/src/commands/update.ts +1 -1
- package/src/config/defaults.ts +5 -5
- package/src/config/loader.ts +1 -0
- package/src/config/schema.ts +2 -6
- package/src/context/analyzer.ts +104 -35
- package/src/context-mode/knowledge/store.ts +381 -43
- package/src/context-mode/tools.ts +41 -3
- package/src/deps/registry.ts +1 -12
- package/src/fix-pr/assessment.ts +1 -0
- package/src/fix-pr/prompt-builder.ts +1 -0
- package/src/git/commit.ts +76 -18
- package/src/harness/command.ts +103 -6
- package/src/harness/default-agents/docs.md +39 -0
- package/src/harness/docs/config.ts +29 -0
- package/src/harness/docs/glob-match.ts +27 -0
- package/src/harness/docs/index-renderer.ts +82 -0
- package/src/harness/docs/provenance.ts +125 -0
- package/src/harness/docs/regen-decision.ts +167 -0
- package/src/harness/docs/representative-files.ts +175 -0
- package/src/harness/docs/source-hash.ts +106 -0
- package/src/harness/docs/validator.ts +233 -0
- package/src/harness/hooks/layer-context-inject.ts +35 -1
- package/src/harness/hooks/register.ts +24 -3
- package/src/harness/pipeline.ts +20 -5
- package/src/harness/pr-comment/baseline.ts +105 -0
- package/src/harness/pr-comment/ci-env.ts +120 -0
- package/src/harness/pr-comment/gh-poster.ts +227 -0
- package/src/harness/pr-comment/handler.ts +198 -0
- package/src/harness/pr-comment/render.ts +297 -0
- package/src/harness/pr-comment/status.ts +95 -0
- package/src/harness/pr-comment/types.ts +73 -0
- package/src/harness/pr-comment/workflow-summary.ts +47 -0
- package/src/harness/project-paths.ts +95 -0
- package/src/harness/stages/design.ts +1 -0
- package/src/harness/stages/discover.ts +1 -13
- package/src/harness/stages/docs.ts +708 -0
- package/src/harness/stages/implement-apply.ts +877 -0
- package/src/harness/stages/implement.ts +64 -51
- package/src/harness/stages/plan.ts +25 -16
- package/src/harness/stages/validate.ts +370 -0
- package/src/harness/storage.ts +142 -0
- package/src/harness/tools.ts +130 -0
- package/src/mempalace/bridge.ts +207 -41
- package/src/mempalace/config.ts +10 -4
- package/src/mempalace/format.ts +122 -6
- package/src/mempalace/hooks.ts +204 -56
- package/src/mempalace/installer-helper.ts +18 -4
- package/src/mempalace/python/mempalace_bridge.py +128 -3
- package/src/mempalace/runtime.ts +55 -18
- package/src/mempalace/schema.ts +151 -30
- package/src/mempalace/session-summary.ts +5 -0
- package/src/mempalace/tool.ts +17 -4
- package/src/mempalace/upstream-limits.ts +69 -0
- package/src/planning/approval-flow.ts +25 -2
- package/src/planning/planning-ask-tool.ts +34 -4
- package/src/planning/system-prompt.ts +1 -1
- package/src/tool-catalog/active-tool-controller.ts +0 -22
- package/src/tool-catalog/active-tool-planner.ts +0 -26
- package/src/tool-catalog/tool-groups.ts +1 -9
- package/src/types.ts +87 -8
- package/src/ui-design/session.ts +114 -10
- package/src/utils/executable.ts +10 -1
- package/src/workspace/state-paths.ts +1 -1
- package/src/commands/mcp.ts +0 -814
- package/src/mcp/activation.ts +0 -77
- package/src/mcp/config.ts +0 -223
- package/src/mcp/docs.ts +0 -154
- package/src/mcp/gateway.ts +0 -103
- package/src/mcp/lifecycle.ts +0 -79
- package/src/mcp/manager-tool.ts +0 -104
- package/src/mcp/mcpc.ts +0 -113
- package/src/mcp/registry.ts +0 -98
- package/src/mcp/triggers.ts +0 -62
- package/src/mcp/types.ts +0 -95
package/src/mcp/activation.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { ServerConfig } from "./types.js";
|
|
2
|
-
|
|
3
|
-
/** Extract $tags from user input, matching against registered server names */
|
|
4
|
-
export function parseTags(text: string, registeredNames: Set<string>): string[] {
|
|
5
|
-
const tags: string[] = [];
|
|
6
|
-
// Sort names by length (longest first) to match "figma-plugin" before "figma"
|
|
7
|
-
const sorted = [...registeredNames].sort((a, b) => b.length - a.length);
|
|
8
|
-
|
|
9
|
-
for (const name of sorted) {
|
|
10
|
-
// Build regex: word-boundary-aware, case-insensitive
|
|
11
|
-
const pattern = new RegExp(`(?:^|[\\s(])\\$${escapeRegex(name)}(?=$|[\\s).,;:!?])`, "gi");
|
|
12
|
-
if (pattern.test(text)) {
|
|
13
|
-
tags.push(name);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return tags;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function escapeRegex(s: string): string {
|
|
21
|
-
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Check if a trigger/antiTrigger matches the message */
|
|
25
|
-
function matchesAny(words: string[], message: string): boolean {
|
|
26
|
-
const lower = message.toLowerCase();
|
|
27
|
-
return words.some((w) => {
|
|
28
|
-
const pattern = new RegExp(`\\b${escapeRegex(w.toLowerCase())}\\b`);
|
|
29
|
-
return pattern.test(lower);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Determine if a server should be active for this turn */
|
|
34
|
-
export function shouldActivate(
|
|
35
|
-
config: ServerConfig,
|
|
36
|
-
message: string,
|
|
37
|
-
isTagged: boolean,
|
|
38
|
-
): boolean {
|
|
39
|
-
if (!config.enabled) return false;
|
|
40
|
-
|
|
41
|
-
// Tag overrides everything (if taggable)
|
|
42
|
-
if (isTagged && config.taggable) return true;
|
|
43
|
-
|
|
44
|
-
switch (config.activation) {
|
|
45
|
-
case "always":
|
|
46
|
-
return true;
|
|
47
|
-
case "contextual": {
|
|
48
|
-
const triggerMatch = config.triggers.length > 0 && matchesAny(config.triggers, message);
|
|
49
|
-
if (!triggerMatch) return false;
|
|
50
|
-
// AntiTrigger wins
|
|
51
|
-
const antiMatch = config.antiTriggers.length > 0 && matchesAny(config.antiTriggers, message);
|
|
52
|
-
return !antiMatch;
|
|
53
|
-
}
|
|
54
|
-
case "disabled":
|
|
55
|
-
return false;
|
|
56
|
-
default:
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Compute which servers should be active for a given message */
|
|
62
|
-
export function computeActiveServers(
|
|
63
|
-
servers: Record<string, ServerConfig>,
|
|
64
|
-
message: string,
|
|
65
|
-
taggedNames: string[],
|
|
66
|
-
): string[] {
|
|
67
|
-
const tagSet = new Set(taggedNames);
|
|
68
|
-
const active: string[] = [];
|
|
69
|
-
|
|
70
|
-
for (const [name, config] of Object.entries(servers)) {
|
|
71
|
-
if (shouldActivate(config, message, tagSet.has(name))) {
|
|
72
|
-
active.push(name);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return active;
|
|
77
|
-
}
|
package/src/mcp/config.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
// src/mcp/config.ts
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import type { PlatformPaths } from "../platform/types.js";
|
|
6
|
-
import type { McpRegistry, ServerConfig, HostMcpServer } from "./types.js";
|
|
7
|
-
import { createEmptyRegistry, isValidServerName } from "./types.js";
|
|
8
|
-
|
|
9
|
-
function getMcpConfigPath(paths: PlatformPaths, cwd: string): string {
|
|
10
|
-
return paths.project(cwd, ".mcp.json");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getGlobalMcpConfigPath(paths: PlatformPaths): string {
|
|
14
|
-
return paths.global(".mcp.json");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readJsonSafe(filePath: string): Record<string, unknown> | null {
|
|
18
|
-
try {
|
|
19
|
-
if (!fs.existsSync(filePath)) return null;
|
|
20
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function loadMcpRegistry(paths: PlatformPaths, cwd: string): McpRegistry {
|
|
27
|
-
const globalData = readJsonSafe(getGlobalMcpConfigPath(paths)) as McpRegistry | null;
|
|
28
|
-
const projectData = readJsonSafe(getMcpConfigPath(paths, cwd)) as McpRegistry | null;
|
|
29
|
-
|
|
30
|
-
const merged: McpRegistry = createEmptyRegistry();
|
|
31
|
-
|
|
32
|
-
// Global servers first
|
|
33
|
-
if (globalData?.servers) {
|
|
34
|
-
for (const [name, config] of Object.entries(globalData.servers)) {
|
|
35
|
-
merged.servers[name] = applyDefaults(config);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Project servers override global
|
|
40
|
-
if (projectData?.servers) {
|
|
41
|
-
for (const [name, config] of Object.entries(projectData.servers)) {
|
|
42
|
-
merged.servers[name] = applyDefaults(config);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return merged;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function applyDefaults(partial: Partial<ServerConfig>): ServerConfig {
|
|
50
|
-
return {
|
|
51
|
-
transport: "http",
|
|
52
|
-
activation: "contextual",
|
|
53
|
-
taggable: true,
|
|
54
|
-
triggers: [],
|
|
55
|
-
antiTriggers: [],
|
|
56
|
-
enabled: true,
|
|
57
|
-
authPending: false,
|
|
58
|
-
addedAt: new Date().toISOString(),
|
|
59
|
-
...partial,
|
|
60
|
-
} as ServerConfig;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function saveMcpRegistry(paths: PlatformPaths, cwd: string, registry: McpRegistry): void {
|
|
64
|
-
const configPath = getMcpConfigPath(paths, cwd);
|
|
65
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
66
|
-
fs.writeFileSync(configPath, JSON.stringify(registry, null, 2) + "\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Load project-only registry (not merged with global) for save operations */
|
|
70
|
-
function loadProjectRegistry(paths: PlatformPaths, cwd: string): McpRegistry {
|
|
71
|
-
const data = readJsonSafe(getMcpConfigPath(paths, cwd)) as McpRegistry | null;
|
|
72
|
-
return data ?? createEmptyRegistry();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function addServer(
|
|
76
|
-
paths: PlatformPaths,
|
|
77
|
-
cwd: string,
|
|
78
|
-
name: string,
|
|
79
|
-
config: Partial<ServerConfig>,
|
|
80
|
-
): { ok: boolean; reason?: string } {
|
|
81
|
-
const validation = isValidServerName(name);
|
|
82
|
-
if (!validation.valid) return { ok: false, reason: validation.reason };
|
|
83
|
-
|
|
84
|
-
const registry = loadProjectRegistry(paths, cwd);
|
|
85
|
-
registry.servers[name] = applyDefaults({ ...config, addedAt: new Date().toISOString() });
|
|
86
|
-
saveMcpRegistry(paths, cwd, registry);
|
|
87
|
-
return { ok: true };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function removeServer(paths: PlatformPaths, cwd: string, name: string): void {
|
|
91
|
-
const registry = loadProjectRegistry(paths, cwd);
|
|
92
|
-
delete registry.servers[name];
|
|
93
|
-
saveMcpRegistry(paths, cwd, registry);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function updateServer(
|
|
97
|
-
paths: PlatformPaths,
|
|
98
|
-
cwd: string,
|
|
99
|
-
name: string,
|
|
100
|
-
updates: Partial<ServerConfig>,
|
|
101
|
-
): { ok: boolean; reason?: string } {
|
|
102
|
-
const registry = loadProjectRegistry(paths, cwd);
|
|
103
|
-
if (!registry.servers[name]) return { ok: false, reason: `Server "${name}" not found` };
|
|
104
|
-
registry.servers[name] = { ...registry.servers[name], ...updates };
|
|
105
|
-
saveMcpRegistry(paths, cwd, registry);
|
|
106
|
-
return { ok: true };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function getServerConfig(
|
|
110
|
-
paths: PlatformPaths,
|
|
111
|
-
cwd: string,
|
|
112
|
-
name: string,
|
|
113
|
-
): ServerConfig | undefined {
|
|
114
|
-
const registry = loadMcpRegistry(paths, cwd);
|
|
115
|
-
return registry.servers[name];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Lockfile ──────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
const LOCK_TIMEOUT_MS = 5000;
|
|
121
|
-
|
|
122
|
-
export function acquireLock(paths: PlatformPaths, cwd: string): { acquired: boolean; release: () => void } {
|
|
123
|
-
const lockPath = paths.project(cwd, ".mcp.lock");
|
|
124
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
125
|
-
|
|
126
|
-
// Check for stale lock
|
|
127
|
-
if (fs.existsSync(lockPath)) {
|
|
128
|
-
try {
|
|
129
|
-
const data = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
130
|
-
const age = Date.now() - data.timestamp;
|
|
131
|
-
if (age < LOCK_TIMEOUT_MS) {
|
|
132
|
-
return { acquired: false, release: () => {} };
|
|
133
|
-
}
|
|
134
|
-
} catch { /* corrupt lock, overwrite */ }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
acquired: true,
|
|
141
|
-
release: () => {
|
|
142
|
-
try { fs.unlinkSync(lockPath); } catch { /* already cleaned */ }
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// ── Host Config Discovery ─────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
interface HostMcpRaw {
|
|
151
|
-
mcpServers?: Record<string, {
|
|
152
|
-
type?: "stdio" | "http" | "sse";
|
|
153
|
-
command?: string;
|
|
154
|
-
args?: string[];
|
|
155
|
-
url?: string;
|
|
156
|
-
headers?: Record<string, string>;
|
|
157
|
-
env?: Record<string, string>;
|
|
158
|
-
enabled?: boolean;
|
|
159
|
-
auth?: unknown;
|
|
160
|
-
oauth?: unknown;
|
|
161
|
-
}>;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Discover MCP servers from host config files:
|
|
166
|
-
* - User-level OMP: paths.agent("mcp.json") → ~/.omp/agent/mcp.json
|
|
167
|
-
* - Project-level OMP: <cwd>/<dotDir>/mcp.json → <cwd>/.omp/mcp.json
|
|
168
|
-
* - Claude Code: ~/.claude.json
|
|
169
|
-
*
|
|
170
|
-
* All use the same { mcpServers: Record<string, ...> } shape.
|
|
171
|
-
*/
|
|
172
|
-
export function discoverHostMcpServers(
|
|
173
|
-
paths: PlatformPaths,
|
|
174
|
-
cwd: string,
|
|
175
|
-
homeDir: string = homedir(),
|
|
176
|
-
): HostMcpServer[] {
|
|
177
|
-
const dot = paths.dotDir;
|
|
178
|
-
|
|
179
|
-
// OMP agent-level (user) config: ~/.omp/agent/mcp.json
|
|
180
|
-
const userPath = paths.agent("mcp.json");
|
|
181
|
-
// OMP project-level config: <cwd>/.omp/mcp.json
|
|
182
|
-
const projectPath = path.join(cwd, dot, "mcp.json");
|
|
183
|
-
// Claude Code user config: ~/.claude.json
|
|
184
|
-
const claudeCodePath = path.join(homeDir, ".claude.json");
|
|
185
|
-
|
|
186
|
-
const sources: Array<{ scope: HostMcpServer["scope"]; filePath: string }> = [
|
|
187
|
-
{ scope: "user", filePath: userPath },
|
|
188
|
-
{ scope: "project", filePath: projectPath },
|
|
189
|
-
{ scope: "claude-code", filePath: claudeCodePath },
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
const discovered: HostMcpServer[] = [];
|
|
193
|
-
|
|
194
|
-
for (const { scope, filePath } of sources) {
|
|
195
|
-
const data = readJsonSafe(filePath) as HostMcpRaw | null;
|
|
196
|
-
if (!data?.mcpServers) continue;
|
|
197
|
-
|
|
198
|
-
for (const [name, raw] of Object.entries(data.mcpServers)) {
|
|
199
|
-
// Skip explicitly disabled servers
|
|
200
|
-
if (raw.enabled === false) continue;
|
|
201
|
-
|
|
202
|
-
const effectiveType = raw.type ?? "stdio";
|
|
203
|
-
let transport: HostMcpServer["transport"] = "stdio";
|
|
204
|
-
if (effectiveType === "http") transport = "http";
|
|
205
|
-
else if (effectiveType === "sse") transport = "sse";
|
|
206
|
-
|
|
207
|
-
discovered.push({
|
|
208
|
-
name,
|
|
209
|
-
scope,
|
|
210
|
-
transport,
|
|
211
|
-
url: raw.url,
|
|
212
|
-
command: raw.command,
|
|
213
|
-
args: raw.args,
|
|
214
|
-
env: raw.env,
|
|
215
|
-
headers: raw.headers,
|
|
216
|
-
enabled: raw.enabled,
|
|
217
|
-
hasAuth: raw.auth !== undefined || raw.oauth !== undefined,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return discovered;
|
|
223
|
-
}
|
package/src/mcp/docs.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { McpTool, ServerConfig } from "./types.js";
|
|
4
|
-
|
|
5
|
-
// ── README Generation ────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export function generateReadme(name: string, config: ServerConfig, tools: McpTool[]): string {
|
|
8
|
-
const lines: string[] = [];
|
|
9
|
-
|
|
10
|
-
lines.push(`# ${name} (MCP Server)`);
|
|
11
|
-
lines.push("");
|
|
12
|
-
if (config.url) lines.push(`**URL:** ${config.url}`);
|
|
13
|
-
if (config.command) lines.push(`**Command:** ${config.command} ${(config.args ?? []).join(" ")}`);
|
|
14
|
-
lines.push(`**Transport:** ${config.transport.toUpperCase()} | **Session:** @supi-${name}`);
|
|
15
|
-
if (config.auth) lines.push(`**Auth:** ${config.auth.type}${config.auth.profile ? ` (profile: ${config.auth.profile})` : ""}`);
|
|
16
|
-
lines.push("");
|
|
17
|
-
|
|
18
|
-
if (config.triggers?.length > 0) {
|
|
19
|
-
lines.push("## When to Use");
|
|
20
|
-
for (const t of config.triggers) lines.push(`- ${t}`);
|
|
21
|
-
lines.push("");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
lines.push("## Tools");
|
|
25
|
-
lines.push("");
|
|
26
|
-
for (const tool of tools) {
|
|
27
|
-
lines.push(`### ${tool.name}`);
|
|
28
|
-
if (tool.description) lines.push(tool.description);
|
|
29
|
-
if (tool.inputSchema) {
|
|
30
|
-
const props = (tool.inputSchema as any).properties;
|
|
31
|
-
if (props) {
|
|
32
|
-
lines.push("**Parameters:**");
|
|
33
|
-
for (const [pName, pDef] of Object.entries(props)) {
|
|
34
|
-
const def = pDef as any;
|
|
35
|
-
lines.push(`- \`${pName}\` (${def.type ?? "any"}) — ${def.description ?? ""}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
lines.push("");
|
|
40
|
-
lines.push("**Example:**");
|
|
41
|
-
lines.push("```bash");
|
|
42
|
-
lines.push(`mcpc --json @supi-${name} tools-call ${tool.name}`);
|
|
43
|
-
lines.push("```");
|
|
44
|
-
lines.push("");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
lines.push("---");
|
|
48
|
-
lines.push(`*Generated by supipowers. Refresh with /supi:mcp refresh ${name}*`);
|
|
49
|
-
|
|
50
|
-
return lines.join("\n");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function writeReadme(basePath: string, name: string, content: string): void {
|
|
54
|
-
const dir = path.join(basePath, "mcpc", name);
|
|
55
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
-
fs.writeFileSync(path.join(dir, "README.md"), content);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function writeToolsCache(basePath: string, name: string, tools: McpTool[]): void {
|
|
60
|
-
const dir = path.join(basePath, "mcpc", name);
|
|
61
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
-
fs.writeFileSync(path.join(dir, "tools.json"), JSON.stringify(tools, null, 2));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── Skill Generation ─────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
export function generateSkill(servers: Record<string, { tools: McpTool[] }>): string {
|
|
68
|
-
const names = Object.keys(servers);
|
|
69
|
-
const triggerList = names.map((n) => `$${n}`).join(", ");
|
|
70
|
-
|
|
71
|
-
const lines: string[] = [];
|
|
72
|
-
lines.push("---");
|
|
73
|
-
lines.push("name: supi:mcpc");
|
|
74
|
-
lines.push(`description: Use mcpc CLI to interact with MCP servers managed by supipowers. Triggers on: ${triggerList} or any mcpc tool invocation.`);
|
|
75
|
-
lines.push("allowed-tools: Bash(mcpc:*)");
|
|
76
|
-
lines.push("---");
|
|
77
|
-
lines.push("");
|
|
78
|
-
lines.push("# mcpc: MCP tools via supipowers");
|
|
79
|
-
lines.push("");
|
|
80
|
-
lines.push("Use the `mcpc_<name>` gateway tools to interact with MCP servers.");
|
|
81
|
-
lines.push("Each server has a dedicated tool registered in your Available Tools.");
|
|
82
|
-
lines.push("");
|
|
83
|
-
lines.push("## Quick Reference");
|
|
84
|
-
lines.push("");
|
|
85
|
-
lines.push("The gateway tools accept `tool` (MCP tool name) and `args` (key-value object).");
|
|
86
|
-
lines.push("Under the hood, they run: `mcpc --json @supi-<name> tools-call <tool> key:=value`");
|
|
87
|
-
lines.push("");
|
|
88
|
-
lines.push("## Available Servers");
|
|
89
|
-
lines.push("");
|
|
90
|
-
|
|
91
|
-
for (const [name, { tools }] of Object.entries(servers)) {
|
|
92
|
-
lines.push(`### ${name} (@supi-${name})`);
|
|
93
|
-
lines.push(`Tools: ${tools.map((t) => t.name).join(", ")}`);
|
|
94
|
-
lines.push("");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return lines.join("\n");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function writeSkill(basePath: string, content: string): void {
|
|
101
|
-
const dir = path.join(basePath, "skills", "mcpc");
|
|
102
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
-
fs.writeFileSync(path.join(dir, "SKILL.md"), content);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── AGENTS.md ────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
const MARKER_START = "<!-- supipowers:mcpc:start -->";
|
|
109
|
-
const MARKER_END = "<!-- supipowers:mcpc:end -->";
|
|
110
|
-
|
|
111
|
-
function buildManagedSection(servers: Record<string, { description: string }>): string {
|
|
112
|
-
const entries = Object.entries(servers);
|
|
113
|
-
if (entries.length === 0) return "";
|
|
114
|
-
|
|
115
|
-
const lines: string[] = [];
|
|
116
|
-
lines.push(MARKER_START);
|
|
117
|
-
lines.push("## MCP Servers (managed by supipowers)");
|
|
118
|
-
lines.push("");
|
|
119
|
-
lines.push("The following MCP servers are available via `$tag` or the `mcpc_<name>` tool:");
|
|
120
|
-
lines.push("");
|
|
121
|
-
for (const [name, { description }] of entries) {
|
|
122
|
-
lines.push(`- **$${name}** — ${description}. Details: .omp/supipowers/mcpc/${name}/README.md`);
|
|
123
|
-
}
|
|
124
|
-
lines.push("");
|
|
125
|
-
lines.push("Use `/supi:mcp` to manage servers. Invoke the `supi:mcpc` skill for usage patterns.");
|
|
126
|
-
lines.push(MARKER_END);
|
|
127
|
-
return lines.join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function updateAgentsMd(cwd: string, servers: Record<string, { description: string }>): void {
|
|
131
|
-
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
132
|
-
const section = buildManagedSection(servers);
|
|
133
|
-
|
|
134
|
-
if (!fs.existsSync(agentsPath)) {
|
|
135
|
-
if (section) fs.writeFileSync(agentsPath, section + "\n");
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let content = fs.readFileSync(agentsPath, "utf-8");
|
|
140
|
-
const startIdx = content.indexOf(MARKER_START);
|
|
141
|
-
const endIdx = content.indexOf(MARKER_END);
|
|
142
|
-
|
|
143
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
144
|
-
// Replace existing section
|
|
145
|
-
const before = content.slice(0, startIdx);
|
|
146
|
-
const after = content.slice(endIdx + MARKER_END.length);
|
|
147
|
-
content = section ? before + section + after : before.trimEnd() + after;
|
|
148
|
-
} else if (section) {
|
|
149
|
-
// Append
|
|
150
|
-
content = content.trimEnd() + "\n\n" + section + "\n";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
fs.writeFileSync(agentsPath, content);
|
|
154
|
-
}
|
package/src/mcp/gateway.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// src/mcp/gateway.ts
|
|
2
|
-
import type { ServerConfig, McpTool } from "./types.js";
|
|
3
|
-
import type { McpcClient } from "./mcpc.js";
|
|
4
|
-
|
|
5
|
-
const SNIPPET_TOOL_LIMIT = 10;
|
|
6
|
-
|
|
7
|
-
export function buildPromptSnippet(name: string, tools: McpTool[]): string {
|
|
8
|
-
const toolNames = tools.slice(0, SNIPPET_TOOL_LIMIT).map((t) => t.name);
|
|
9
|
-
const suffix = tools.length > SNIPPET_TOOL_LIMIT
|
|
10
|
-
? ` (${tools.length} tools, see README for full list)`
|
|
11
|
-
: ` (${tools.length} tools)`;
|
|
12
|
-
return `mcpc_${name} — ${toolNames.join(", ")}${suffix}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildPromptGuidelines(config: ServerConfig): string[] {
|
|
16
|
-
const guidelines: string[] = [];
|
|
17
|
-
if (config.triggers.length > 0) {
|
|
18
|
-
guidelines.push(`Use when the context involves: ${config.triggers.join(", ")}`);
|
|
19
|
-
}
|
|
20
|
-
if (config.antiTriggers.length > 0) {
|
|
21
|
-
guidelines.push(`Do NOT use for: ${config.antiTriggers.join(", ")}`);
|
|
22
|
-
}
|
|
23
|
-
return guidelines;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface GatewayToolDef {
|
|
27
|
-
name: string;
|
|
28
|
-
label: string;
|
|
29
|
-
description: string;
|
|
30
|
-
promptSnippet: string;
|
|
31
|
-
promptGuidelines: string[];
|
|
32
|
-
parameters: any; // TypeBox schema — built at registration time
|
|
33
|
-
executeFn: (toolName: string, args?: Record<string, unknown>) => Promise<any>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function buildGatewayToolDef(
|
|
37
|
-
name: string,
|
|
38
|
-
config: ServerConfig,
|
|
39
|
-
tools: McpTool[],
|
|
40
|
-
): GatewayToolDef {
|
|
41
|
-
const descriptions = tools.slice(0, 5).map((t) => t.description).filter(Boolean);
|
|
42
|
-
const summary = descriptions.join(". ").slice(0, 200);
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
name: `mcpc_${name}`,
|
|
46
|
-
label: `${name} (via mcpc)`,
|
|
47
|
-
description: summary || `MCP server: ${name}`,
|
|
48
|
-
promptSnippet: buildPromptSnippet(name, tools),
|
|
49
|
-
promptGuidelines: buildPromptGuidelines(config),
|
|
50
|
-
parameters: {}, // Placeholder — actual TypeBox schema built at registration time
|
|
51
|
-
executeFn: async () => {}, // Placeholder — wired at registration time
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Build the mcpc args serialization and execute a tool call via mcpc.
|
|
57
|
-
* Returns content blocks for the agent.
|
|
58
|
-
*/
|
|
59
|
-
export async function executeGatewayCall(
|
|
60
|
-
client: McpcClient,
|
|
61
|
-
sessionName: string,
|
|
62
|
-
toolName: string,
|
|
63
|
-
args?: Record<string, unknown>,
|
|
64
|
-
): Promise<{ content: Array<{ type: string; text: string }>; details: Record<string, any> }> {
|
|
65
|
-
let result = await client.toolsCall(sessionName, toolName, args);
|
|
66
|
-
|
|
67
|
-
// Session crash (exit code 3) — restart and retry once
|
|
68
|
-
if (result.code === 3) {
|
|
69
|
-
const restartResult = await client.restart(sessionName);
|
|
70
|
-
if (restartResult.code !== 0) {
|
|
71
|
-
throw new Error(`Session @supi-${sessionName} crashed and couldn't recover. Run /supi:mcp login ${sessionName} or check network.`);
|
|
72
|
-
}
|
|
73
|
-
result = await client.toolsCall(sessionName, toolName, args);
|
|
74
|
-
if (result.code !== 0) {
|
|
75
|
-
throw new Error(result.error || `mcpc call failed after restart (code ${result.code})`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Stale tool cache (exit code 2) — hint to refresh
|
|
80
|
-
if (result.code === 2) {
|
|
81
|
-
const refreshed = await client.toolsList(sessionName);
|
|
82
|
-
const toolNames = refreshed.tools.map((t) => t.name).join(", ");
|
|
83
|
-
throw new Error(`Tool "${toolName}" not found. Server tools have been refreshed. Available: [${toolNames}]. Please retry.`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (result.code !== 0) {
|
|
87
|
-
throw new Error(result.error || `mcpc exited with code ${result.code}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// mcpc --json returns { content: [...] } for tool calls
|
|
91
|
-
if (result.data?.content) {
|
|
92
|
-
return {
|
|
93
|
-
content: result.data.content,
|
|
94
|
-
details: { serverSession: `@supi-${sessionName}`, toolName },
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Fallback: wrap raw output
|
|
99
|
-
return {
|
|
100
|
-
content: [{ type: "text", text: typeof result.data === "string" ? result.data : JSON.stringify(result.data) }],
|
|
101
|
-
details: { serverSession: `@supi-${sessionName}`, toolName },
|
|
102
|
-
};
|
|
103
|
-
}
|
package/src/mcp/lifecycle.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// src/mcp/lifecycle.ts
|
|
2
|
-
import type { McpRegistry, ServerState } from "./types.js";
|
|
3
|
-
import type { McpcClient } from "./mcpc.js";
|
|
4
|
-
|
|
5
|
-
const RETRY_DELAY_MS = 2000;
|
|
6
|
-
|
|
7
|
-
export async function initializeMcpServers(
|
|
8
|
-
registry: McpRegistry,
|
|
9
|
-
client: McpcClient,
|
|
10
|
-
): Promise<Record<string, ServerState>> {
|
|
11
|
-
const states: Record<string, ServerState> = {};
|
|
12
|
-
|
|
13
|
-
for (const [name, config] of Object.entries(registry.servers)) {
|
|
14
|
-
if (!config.enabled) {
|
|
15
|
-
states[name] = { config, status: "disconnected" };
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const target = config.url ?? config.command ?? "";
|
|
20
|
-
|
|
21
|
-
// Build auth header for bearer tokens
|
|
22
|
-
let authHeader: string | undefined;
|
|
23
|
-
if (config.auth?.type === "bearer" && config.auth.envVar) {
|
|
24
|
-
const token = process.env[config.auth.envVar];
|
|
25
|
-
if (token) authHeader = `Authorization: Bearer ${token}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let connectResult = await client.connect(target, name, authHeader);
|
|
29
|
-
|
|
30
|
-
// Auth error
|
|
31
|
-
if (connectResult.code === 4) {
|
|
32
|
-
states[name] = { config: { ...config, authPending: true }, status: "auth-pending" };
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Network error — retry once
|
|
37
|
-
if (connectResult.code === 3) {
|
|
38
|
-
await delay(RETRY_DELAY_MS);
|
|
39
|
-
connectResult = await client.connect(target, name, authHeader);
|
|
40
|
-
if (connectResult.code !== 0) {
|
|
41
|
-
states[name] = { config, status: "offline" };
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (connectResult.code !== 0) {
|
|
47
|
-
states[name] = { config, status: "offline" };
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Fetch tool catalog
|
|
52
|
-
const toolsResult = await client.toolsList(name);
|
|
53
|
-
const catalog = {
|
|
54
|
-
serverName: name,
|
|
55
|
-
tools: toolsResult.tools,
|
|
56
|
-
fetchedAt: new Date().toISOString(),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
states[name] = { config, status: "connected", catalog };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return states;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function shutdownMcpServers(
|
|
66
|
-
sessionNames: string[],
|
|
67
|
-
client: McpcClient,
|
|
68
|
-
closeOnExit: boolean,
|
|
69
|
-
): Promise<void> {
|
|
70
|
-
if (!closeOnExit) return;
|
|
71
|
-
|
|
72
|
-
await Promise.allSettled(
|
|
73
|
-
sessionNames.map((name) => client.close(name)),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function delay(ms: number): Promise<void> {
|
|
78
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
79
|
-
}
|