@compilr-dev/sdk 0.29.1 → 0.29.3

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/dist/index.d.ts CHANGED
@@ -67,7 +67,7 @@ export type { GuideEntry, ContentTopic, ContentSection, GuideToolConfig } from '
67
67
  export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, createCanvasTools, createImageTools, ProjectAnchorStore, FileArtifactService, } from './platform/index.js';
68
68
  export type { ProjectAnchorStoreConfig, FileArtifactServiceConfig, ImageToolsConfig, ImageResizer, } from './platform/index.js';
69
69
  export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './platform/index.js';
70
- export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker, SkillEligibilityContext, SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './skills/index.js';
70
+ export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker, InstalledFromMarker, SkillEligibilityContext, SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './skills/index.js';
71
71
  export { RESERVED_MACRO_NAMES, isReservedMacroName, parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills, resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill as validateSkillQuality, getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readSkillScopeConfig, readSkillScopeConfigSync, writeSkillScopeConfig, getSkillBindings, resolveSkillBinding, } from './skills/index.js';
72
72
  export type { SkillScope, SkillPromptResolution, AvailableSkillEntry, SkillSources, } from './skills/index.js';
73
73
  export { resolveSkillPrompt, getAllAvailableSkills } from './skills/index.js';
@@ -77,6 +77,8 @@ export { skillReachability, isUnreachable } from './skills/index.js';
77
77
  export { patchSkillFrontmatter } from './skills/index.js';
78
78
  export { isPlaceholderDescription } from './skills/index.js';
79
79
  export { readSkillFolder, unreadSkillFiles } from './skills/index.js';
80
+ export { findInstallableSkills, normaliseSkillName, resolveInstallName, planInstall, describeInstallPlan, buildInstalledFromPatch, } from './skills/index.js';
81
+ export type { InstallCandidate, InstallPlanItem } from './skills/index.js';
80
82
  export type { SkillFolderEntry } from './skills/index.js';
81
83
  export type { FrontmatterPatch } from './skills/index.js';
82
84
  export type { SkillReachability } from './skills/index.js';
@@ -107,8 +109,10 @@ export { FileEpisodeStore, EpisodeRecorder, isSignificantWork, extractAffectedFi
107
109
  export type { FileEpisodeStoreOptions, EpisodeRecorderConfig, WorkSummaryAnchorConfig, PendingToolSignal, EpisodeFile, } from './episodes/index.js';
108
110
  export { resolveNext, evaluateBranchCondition, walkPastBranches, lookupLabel, applyBacktrack, } from './flow-runner/index.js';
109
111
  export type { BacktrackResult } from './flow-runner/index.js';
110
- export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, } from './mcp-config.js';
111
- export type { MCPServerEntry, MCPConfigFile, ResolvedMCPServer } from './mcp-config.js';
112
+ export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, readMCPConfig, assertWritable, canConnect, } from './mcp-config.js';
113
+ export type { MCPServerEntry, MCPConfigFile, ResolvedMCPServer, MCPConfigRead, ConfigValue, } from './mcp-config.js';
114
+ export { serverHealth, needsAttention, describeHealth, estimateToolTokens, estimateServerCost, formatTokens, disabledTools, isToolEnabled, setToolEnabled, filterMcpToolsByEnablement, isSecretRef, secretRefFor, parseSecretRef, looksLikeSecret, resolveValues, describeMissing, planSecretStorage, toStoredValues, secretRefsOf, } from './mcp/index.js';
115
+ export type { MCPServerHealth, MCPServerState, MCPConnectionStatus, HealthInput, MCPToolShape, ServerCost, SecretRef, ResolveResult, } from './mcp/index.js';
112
116
  export { generateProject, isGitConfigured, generateCompilrMd, generateConfigJson, generateReadmeMd, generateCodingStandardsMd, generatePackageJson, generateTsconfig, generateGitignore, generateCompilrMdForImport, detectProjectInfo, detectGitInfo, prettifyName, getLanguageLabel, getFrameworkLabel, validateImportPath, isValidProjectName, projectExists, TECH_STACK_LABELS, CODING_STANDARDS_LABELS, REPO_PATTERN_LABELS, WORKFLOW_VERSION, } from './project-generator/index.js';
113
117
  export type { TechStack, CodingStandards, GeneratorRepoPattern, ProjectConfig, GenerationResult, CompilrConfig, DetectedProject, GitInfo, ImportProjectConfig, } from './project-generator/index.js';
114
118
  export { readFileTool, writeFileTool, createBashTool, bashTool, bashOutputTool, killShellTool, grepTool, globTool, editTool, todoWriteTool, todoReadTool, createTodoTools, getDefaultTodoStore, TodoStore, webFetchTool, suggestTool, } from '@compilr-dev/agents';
package/dist/index.js CHANGED
@@ -160,6 +160,7 @@ export { skillReachability, isUnreachable } from './skills/index.js';
160
160
  export { patchSkillFrontmatter } from './skills/index.js';
161
161
  export { isPlaceholderDescription } from './skills/index.js';
162
162
  export { readSkillFolder, unreadSkillFiles } from './skills/index.js';
163
+ export { findInstallableSkills, normaliseSkillName, resolveInstallName, planInstall, describeInstallPlan, buildInstalledFromPatch, } from './skills/index.js';
163
164
  export { generateSkillCatalog, toCatalogEntry, createLoadSkillTool } from './skills/index.js';
164
165
  export { platformMacros, designMacro, sketchMacro, prdMacro, refineMacro, refineItemMacro, architectureMacro, sessionNotesMacro, buildMacro, scaffoldMacro, outlineMacro, literatureReviewMacro, draftSectionMacro, peerReviewMacro, researchScaffoldMacro, businessVisionMacro, marketAnalysisMacro, competitorAnalysisMacro, financialModelMacro, pitchOutlineMacro, businessReviewMacro, brandSetupMacro, contentStrategyMacro, contentCalendarMacro, createContentMacro, contentReviewMacro, curriculumDesignMacro, lessonPlanMacro, assessmentDesignMacro, courseReviewMacro, bookOutlineMacro, characterDesignMacro, plotThreadsMacro, sceneBreakdownMacro, bookReviewMacro, } from './skills/index.js';
165
166
  // =============================================================================
@@ -231,7 +232,9 @@ export { resolveNext, evaluateBranchCondition, walkPastBranches, lookupLabel, ap
231
232
  // =============================================================================
232
233
  // Shared MCP Configuration
233
234
  // =============================================================================
234
- export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, } from './mcp-config.js';
235
+ export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, readMCPConfig, assertWritable, canConnect, } from './mcp-config.js';
236
+ // MCP — health, per-tool enablement, secrets, cost (mcp-restyling Phase 1)
237
+ export { serverHealth, needsAttention, describeHealth, estimateToolTokens, estimateServerCost, formatTokens, disabledTools, isToolEnabled, setToolEnabled, filterMcpToolsByEnablement, isSecretRef, secretRefFor, parseSecretRef, looksLikeSecret, resolveValues, describeMissing, planSecretStorage, toStoredValues, secretRefsOf, } from './mcp/index.js';
235
238
  // =============================================================================
236
239
  // Project Generator (templates, scaffolding, detection)
237
240
  // =============================================================================
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What a server costs to have configured.
3
+ *
4
+ * ⚠️ THE FIGURE THE WHOLE DESIGN TURNS ON. A server's tool descriptions are sent to the model on
5
+ * every turn whether the tools are called or not — `filesystem` alone is roughly 3.8k tokens of
6
+ * every agent's context, including a duplicate the server itself marks deprecated. Nothing in the
7
+ * app has ever said so.
8
+ *
9
+ * ⚠️ IT IS AN ESTIMATE AND MUST READ AS ONE. We do not run the provider's tokenizer, and the real
10
+ * number differs per model. Callers render `~3.0k`, never `3,024`: a figure with no decimals is a
11
+ * claim about precision we cannot support.
12
+ */
13
+ export interface MCPToolShape {
14
+ name: string;
15
+ description?: string;
16
+ /** JSON Schema for the tool's arguments. Reaches the model too, so it counts. */
17
+ inputSchema?: unknown;
18
+ }
19
+ /**
20
+ * Roughly how many tokens describing this tool costs.
21
+ *
22
+ * ⚠️ THE SCHEMA COUNTS. An early version measured the description alone and under-reported badly:
23
+ * a tool with a one-line summary and fifteen parameters is mostly schema. The name counts too —
24
+ * it is sent prefixed, as `mcp_<server>_<tool>`.
25
+ */
26
+ export declare function estimateToolTokens(tool: MCPToolShape, serverName?: string): number;
27
+ export interface ServerCost {
28
+ /** Tokens for the tools that are actually described to the model. */
29
+ enabledTokens: number;
30
+ /** Tokens if every tool were on — the baseline the saving is measured against. */
31
+ totalTokens: number;
32
+ enabledCount: number;
33
+ totalCount: number;
34
+ }
35
+ /**
36
+ * What this server adds to every agent turn, given which of its tools are on.
37
+ *
38
+ * ⚠️ ONE SOURCE, COMPUTED (D-3). The header stat, the row's TOOLS cell, the panel badge and the
39
+ * document's rail all read this — none of them counts for itself, and no figure is written into
40
+ * copy. Three of the four defects found while designing this were a number stated in one place
41
+ * while the truth lived in another.
42
+ */
43
+ export declare function estimateServerCost(tools: readonly MCPToolShape[], isEnabled: (toolName: string) => boolean, serverName?: string): ServerCost;
44
+ /**
45
+ * `~3.0k`, `~840`, `—`.
46
+ *
47
+ * The dash is for genuinely nothing — a tool that is off, or a server we cannot ask. It is not the
48
+ * same as `~0`, which would claim we measured and found zero.
49
+ */
50
+ export declare function formatTokens(tokens: number | null): string;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * What a server costs to have configured.
3
+ *
4
+ * ⚠️ THE FIGURE THE WHOLE DESIGN TURNS ON. A server's tool descriptions are sent to the model on
5
+ * every turn whether the tools are called or not — `filesystem` alone is roughly 3.8k tokens of
6
+ * every agent's context, including a duplicate the server itself marks deprecated. Nothing in the
7
+ * app has ever said so.
8
+ *
9
+ * ⚠️ IT IS AN ESTIMATE AND MUST READ AS ONE. We do not run the provider's tokenizer, and the real
10
+ * number differs per model. Callers render `~3.0k`, never `3,024`: a figure with no decimals is a
11
+ * claim about precision we cannot support.
12
+ */
13
+ /** Chars per token. Four is the long-standing rule of thumb for English prose and code. */
14
+ const CHARS_PER_TOKEN = 4;
15
+ /**
16
+ * Roughly how many tokens describing this tool costs.
17
+ *
18
+ * ⚠️ THE SCHEMA COUNTS. An early version measured the description alone and under-reported badly:
19
+ * a tool with a one-line summary and fifteen parameters is mostly schema. The name counts too —
20
+ * it is sent prefixed, as `mcp_<server>_<tool>`.
21
+ */
22
+ export function estimateToolTokens(tool, serverName = '') {
23
+ const prefixed = serverName === '' ? tool.name : `mcp_${serverName}_${tool.name}`;
24
+ let chars = prefixed.length + (tool.description ?? '').length;
25
+ if (tool.inputSchema !== undefined) {
26
+ try {
27
+ chars += JSON.stringify(tool.inputSchema).length;
28
+ }
29
+ catch {
30
+ /* circular or unserialisable — the rest of the estimate still stands */
31
+ }
32
+ }
33
+ return Math.ceil(chars / CHARS_PER_TOKEN);
34
+ }
35
+ /**
36
+ * What this server adds to every agent turn, given which of its tools are on.
37
+ *
38
+ * ⚠️ ONE SOURCE, COMPUTED (D-3). The header stat, the row's TOOLS cell, the panel badge and the
39
+ * document's rail all read this — none of them counts for itself, and no figure is written into
40
+ * copy. Three of the four defects found while designing this were a number stated in one place
41
+ * while the truth lived in another.
42
+ */
43
+ export function estimateServerCost(tools, isEnabled, serverName = '') {
44
+ let enabledTokens = 0;
45
+ let totalTokens = 0;
46
+ let enabledCount = 0;
47
+ for (const tool of tools) {
48
+ const cost = estimateToolTokens(tool, serverName);
49
+ totalTokens += cost;
50
+ if (isEnabled(tool.name)) {
51
+ enabledTokens += cost;
52
+ enabledCount++;
53
+ }
54
+ }
55
+ return { enabledTokens, totalTokens, enabledCount, totalCount: tools.length };
56
+ }
57
+ /**
58
+ * `~3.0k`, `~840`, `—`.
59
+ *
60
+ * The dash is for genuinely nothing — a tool that is off, or a server we cannot ask. It is not the
61
+ * same as `~0`, which would claim we measured and found zero.
62
+ */
63
+ export function formatTokens(tokens) {
64
+ if (tokens === null)
65
+ return '—';
66
+ if (tokens < 1000)
67
+ return `~${String(tokens)}`;
68
+ return `~${(tokens / 1000).toFixed(1)}k`;
69
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * What state an MCP server is actually in.
3
+ *
4
+ * ⚠️ THE DEFECT THIS MODULE EXISTS TO END. Desktop rendered a green dot and counted a server as
5
+ * "active" from `entry.disabled` — a CONFIGURATION flag. Whether a server is switched on and
6
+ * whether it answers are different questions, and the app was answering the second with the first.
7
+ * A server whose token had expired looked identical to a working one until an agent called it and
8
+ * the error surfaced mid-conversation.
9
+ *
10
+ * So every state here names what it was derived from, and there is a state for **not knowing**.
11
+ * Guessing green is how we got here (D-1).
12
+ */
13
+ /** What the connection layer reports. Mirrors `MCPClient.status` in @compilr-dev/agents. */
14
+ export type MCPConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
15
+ export type MCPServerState =
16
+ /** The user switched it off. Config, not connection. */
17
+ 'off'
18
+ /** We hold a live client. */
19
+ | 'connected'
20
+ /** We tried and it did not work. Carries a reason. */
21
+ | 'failing'
22
+ /** We have not tried yet — no client exists. NEVER rendered as connected. */
23
+ | 'unknown';
24
+ export interface MCPServerHealth {
25
+ state: MCPServerState;
26
+ /** Why it is failing, in the server's own words where we have them. */
27
+ reason: string | null;
28
+ /**
29
+ * ISO timestamp of the last successful connection, or null if it has never connected.
30
+ *
31
+ * ⚠️ ONE TIMESTAMP, NOT A HISTORY (D-6). The design asked for "every call has failed for 3 days";
32
+ * that is a count nothing records, and writing it anyway would be a claim with no measurement
33
+ * behind it — the same defect as the green dot, in prose.
34
+ */
35
+ lastConnectedAt: string | null;
36
+ }
37
+ export interface HealthInput {
38
+ /** `entry.disabled` from mcp.json. */
39
+ disabled?: boolean;
40
+ /** The live client's status, or null when no client exists for this server. */
41
+ status?: MCPConnectionStatus | null;
42
+ /** The last error we captured for it. */
43
+ error?: string | null;
44
+ lastConnectedAt?: string | null;
45
+ }
46
+ /**
47
+ * Derive a server's health from what we observed.
48
+ *
49
+ * `disabled` wins: a server the user switched off is `off` whatever a stale client says, because
50
+ * that is the state they chose. Everything else follows the connection.
51
+ */
52
+ export declare function serverHealth(input: HealthInput): MCPServerHealth;
53
+ /** Does this state need the user's attention? Drives the amber row and the panel icon. */
54
+ export declare function needsAttention(health: MCPServerHealth): boolean;
55
+ /**
56
+ * One line for the health strip and the list row.
57
+ *
58
+ * ⚠️ SAYS ONLY WHAT WE STORED. No counts, no durations — we keep one timestamp (D-6). Callers
59
+ * render the date; formatting is presentation and differs between a terminal and a window.
60
+ */
61
+ export declare function describeHealth(health: MCPServerHealth): string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * What state an MCP server is actually in.
3
+ *
4
+ * ⚠️ THE DEFECT THIS MODULE EXISTS TO END. Desktop rendered a green dot and counted a server as
5
+ * "active" from `entry.disabled` — a CONFIGURATION flag. Whether a server is switched on and
6
+ * whether it answers are different questions, and the app was answering the second with the first.
7
+ * A server whose token had expired looked identical to a working one until an agent called it and
8
+ * the error surfaced mid-conversation.
9
+ *
10
+ * So every state here names what it was derived from, and there is a state for **not knowing**.
11
+ * Guessing green is how we got here (D-1).
12
+ */
13
+ /**
14
+ * Derive a server's health from what we observed.
15
+ *
16
+ * `disabled` wins: a server the user switched off is `off` whatever a stale client says, because
17
+ * that is the state they chose. Everything else follows the connection.
18
+ */
19
+ export function serverHealth(input) {
20
+ const lastConnectedAt = input.lastConnectedAt ?? null;
21
+ const reason = input.error ?? null;
22
+ if (input.disabled === true)
23
+ return { state: 'off', reason: null, lastConnectedAt };
24
+ switch (input.status) {
25
+ case 'connected':
26
+ return { state: 'connected', reason: null, lastConnectedAt };
27
+ case 'error':
28
+ return { state: 'failing', reason, lastConnectedAt };
29
+ case 'connecting':
30
+ case 'disconnected':
31
+ case null:
32
+ case undefined:
33
+ /*
34
+ ⚠️ `disconnected` IS NOT `off`. It means we have a client that is not currently connected —
35
+ we do not know whether the server would answer. The user did not ask for this state and
36
+ must not be told the server is fine.
37
+ */
38
+ return { state: 'unknown', reason, lastConnectedAt };
39
+ }
40
+ }
41
+ /** Does this state need the user's attention? Drives the amber row and the panel icon. */
42
+ export function needsAttention(health) {
43
+ return health.state === 'failing';
44
+ }
45
+ /**
46
+ * One line for the health strip and the list row.
47
+ *
48
+ * ⚠️ SAYS ONLY WHAT WE STORED. No counts, no durations — we keep one timestamp (D-6). Callers
49
+ * render the date; formatting is presentation and differs between a terminal and a window.
50
+ */
51
+ export function describeHealth(health) {
52
+ switch (health.state) {
53
+ case 'off':
54
+ return 'Switched off. Its tools are not offered to any agent.';
55
+ case 'connected':
56
+ return 'Connected.';
57
+ case 'failing':
58
+ return health.reason ?? 'The last connection attempt failed.';
59
+ case 'unknown':
60
+ return health.lastConnectedAt === null
61
+ ? 'Not connected yet — it starts when an agent first needs it.'
62
+ : 'Not connected right now.';
63
+ }
64
+ }
@@ -0,0 +1,5 @@
1
+ /** MCP: health, per-tool enablement, secret references and context cost. */
2
+ export { serverHealth, needsAttention, describeHealth, type MCPServerHealth, type MCPServerState, type MCPConnectionStatus, type HealthInput, } from './health.js';
3
+ export { estimateToolTokens, estimateServerCost, formatTokens, type MCPToolShape, type ServerCost, } from './cost.js';
4
+ export { disabledTools, isToolEnabled, setToolEnabled, filterMcpToolsByEnablement, } from './tools.js';
5
+ export { isSecretRef, secretRefFor, parseSecretRef, looksLikeSecret, resolveValues, describeMissing, planSecretStorage, toStoredValues, secretRefsOf, type SecretRef, type ConfigValue, type ResolveResult, } from './secrets.js';
@@ -0,0 +1,5 @@
1
+ /** MCP: health, per-tool enablement, secret references and context cost. */
2
+ export { serverHealth, needsAttention, describeHealth, } from './health.js';
3
+ export { estimateToolTokens, estimateServerCost, formatTokens, } from './cost.js';
4
+ export { disabledTools, isToolEnabled, setToolEnabled, filterMcpToolsByEnablement, } from './tools.js';
5
+ export { isSecretRef, secretRefFor, parseSecretRef, looksLikeSecret, resolveValues, describeMissing, planSecretStorage, toStoredValues, secretRefsOf, } from './secrets.js';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Keeping credentials out of mcp.json.
3
+ *
4
+ * ⚠️ THE APP CURRENTLY TEACHES THE OPPOSITE. The environment field's own placeholder is
5
+ * `{"GITHUB_TOKEN": "ghp_..."}` — so the UI instructs you to paste a live token into a plain file
6
+ * that is shared with @compilr-dev/cli and sits in a directory people copy between machines and
7
+ * occasionally paste into a gist.
8
+ *
9
+ * A value becomes a REFERENCE in the file; the secret itself goes to the host's encrypted store
10
+ * (Desktop already has one: Electron safeStorage with an AES-256-GCM fallback, shipping for API
11
+ * keys). Non-secret settings stay as plain strings where they can be read and diffed.
12
+ *
13
+ * ⚠️ THE FILE IS SHARED, SO THIS MUST DEGRADE LOUDLY. A host that cannot resolve a reference must
14
+ * say which variable and where it lives — never pass an empty string to the server, which produces
15
+ * a 401 the user cannot explain and we cannot distinguish from a genuinely bad token.
16
+ */
17
+ import type { MCPServerEntry } from '../mcp-config.js';
18
+ /** What sits in mcp.json in place of a secret. */
19
+ export interface SecretRef {
20
+ secret: string;
21
+ }
22
+ export type ConfigValue = string | SecretRef;
23
+ export declare function isSecretRef(value: unknown): value is SecretRef;
24
+ /**
25
+ * The key a secret is stored under: `mcp:<server>:<variable>`.
26
+ *
27
+ * Namespaced by server so two servers may each hold a `GITHUB_TOKEN` without collision — which is
28
+ * the normal case, not an edge one.
29
+ */
30
+ export declare function secretRefFor(server: string, variable: string): SecretRef;
31
+ /** Parse a reference back into its parts, or null if it is not one of ours. */
32
+ export declare function parseSecretRef(ref: string): {
33
+ server: string;
34
+ variable: string;
35
+ } | null;
36
+ /**
37
+ * Does this look like something that should not sit in a plain file?
38
+ *
39
+ * Deliberately conservative and advisory only — it drives an OFFER ("store this in the keychain?"),
40
+ * never an automatic rewrite. Guessing wrong in the cautious direction costs a click; guessing
41
+ * wrong the other way writes a token to disk.
42
+ */
43
+ export declare function looksLikeSecret(variable: string, value: string): boolean;
44
+ export interface ResolveResult {
45
+ /** Ready to hand to the server. Only present when `missing` is empty. */
46
+ values: Record<string, string>;
47
+ /** References we could not resolve, by variable name. */
48
+ missing: {
49
+ variable: string;
50
+ ref: string;
51
+ }[];
52
+ }
53
+ /**
54
+ * Turn a stored map into real values, resolving any references through the host's store.
55
+ *
56
+ * ⚠️ MISSING SECRETS ARE REPORTED, NEVER SUBSTITUTED. Returning `''` for an unresolvable reference
57
+ * is what turns a configuration problem into an authentication error three layers away.
58
+ */
59
+ export declare function resolveValues(stored: Record<string, ConfigValue> | undefined, lookup: (ref: string) => string | null): ResolveResult;
60
+ /** One sentence per unresolvable reference, naming the variable and where the value belongs. */
61
+ export declare function describeMissing(server: string, missing: ResolveResult['missing']): string[];
62
+ /**
63
+ * Split a pasted plain-text map into what should be stored where.
64
+ *
65
+ * Used by the paste-config route to tell the user what will happen BEFORE anything is written.
66
+ */
67
+ export declare function planSecretStorage(server: string, plain: Record<string, string>): {
68
+ variable: string;
69
+ value: string;
70
+ toKeychain: boolean;
71
+ ref: SecretRef | null;
72
+ }[];
73
+ /** The map as it should be WRITTEN to mcp.json: secrets as references, the rest verbatim. */
74
+ export declare function toStoredValues(plan: ReturnType<typeof planSecretStorage>): Record<string, ConfigValue>;
75
+ /** Every secret reference an entry holds, for cleanup when a server is deleted. */
76
+ export declare function secretRefsOf(entry: Pick<MCPServerEntry, 'env' | 'headers'>): string[];
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Keeping credentials out of mcp.json.
3
+ *
4
+ * ⚠️ THE APP CURRENTLY TEACHES THE OPPOSITE. The environment field's own placeholder is
5
+ * `{"GITHUB_TOKEN": "ghp_..."}` — so the UI instructs you to paste a live token into a plain file
6
+ * that is shared with @compilr-dev/cli and sits in a directory people copy between machines and
7
+ * occasionally paste into a gist.
8
+ *
9
+ * A value becomes a REFERENCE in the file; the secret itself goes to the host's encrypted store
10
+ * (Desktop already has one: Electron safeStorage with an AES-256-GCM fallback, shipping for API
11
+ * keys). Non-secret settings stay as plain strings where they can be read and diffed.
12
+ *
13
+ * ⚠️ THE FILE IS SHARED, SO THIS MUST DEGRADE LOUDLY. A host that cannot resolve a reference must
14
+ * say which variable and where it lives — never pass an empty string to the server, which produces
15
+ * a 401 the user cannot explain and we cannot distinguish from a genuinely bad token.
16
+ */
17
+ export function isSecretRef(value) {
18
+ return (typeof value === 'object' &&
19
+ value !== null &&
20
+ 'secret' in value &&
21
+ typeof value.secret === 'string');
22
+ }
23
+ /**
24
+ * The key a secret is stored under: `mcp:<server>:<variable>`.
25
+ *
26
+ * Namespaced by server so two servers may each hold a `GITHUB_TOKEN` without collision — which is
27
+ * the normal case, not an edge one.
28
+ */
29
+ export function secretRefFor(server, variable) {
30
+ return { secret: `mcp:${server}:${variable}` };
31
+ }
32
+ /** Parse a reference back into its parts, or null if it is not one of ours. */
33
+ export function parseSecretRef(ref) {
34
+ const parts = ref.split(':');
35
+ if (parts.length !== 3 || parts[0] !== 'mcp')
36
+ return null;
37
+ if (parts[1] === '' || parts[2] === '')
38
+ return null;
39
+ return { server: parts[1], variable: parts[2] };
40
+ }
41
+ /**
42
+ * Does this look like something that should not sit in a plain file?
43
+ *
44
+ * Deliberately conservative and advisory only — it drives an OFFER ("store this in the keychain?"),
45
+ * never an automatic rewrite. Guessing wrong in the cautious direction costs a click; guessing
46
+ * wrong the other way writes a token to disk.
47
+ */
48
+ export function looksLikeSecret(variable, value) {
49
+ if (/^(ghp|gho|ghs|ghu|github_pat|sk-|xoxb-|xoxp-|AKIA|glpat-|pat_)/i.test(value))
50
+ return true;
51
+ if (/^Bearer\s+\S+/i.test(value))
52
+ return true;
53
+ if (/(token|secret|password|passwd|api[-_]?key|credential|auth)/i.test(variable)) {
54
+ // A name that sounds secret plus a value long enough to be one. `TOKEN_PATH=/tmp/t` is not.
55
+ return value.length >= 16 && !value.startsWith('/') && !value.startsWith('~');
56
+ }
57
+ // A long opaque run of base64/hex with no spaces or path separators.
58
+ return value.length >= 32 && /^[A-Za-z0-9_\-.+/=]+$/.test(value) && !value.includes('/');
59
+ }
60
+ /**
61
+ * Turn a stored map into real values, resolving any references through the host's store.
62
+ *
63
+ * ⚠️ MISSING SECRETS ARE REPORTED, NEVER SUBSTITUTED. Returning `''` for an unresolvable reference
64
+ * is what turns a configuration problem into an authentication error three layers away.
65
+ */
66
+ export function resolveValues(stored, lookup) {
67
+ const values = {};
68
+ const missing = [];
69
+ for (const [variable, value] of Object.entries(stored ?? {})) {
70
+ if (typeof value === 'string') {
71
+ values[variable] = value;
72
+ continue;
73
+ }
74
+ const found = lookup(value.secret);
75
+ if (found === null)
76
+ missing.push({ variable, ref: value.secret });
77
+ else
78
+ values[variable] = found;
79
+ }
80
+ return { values, missing };
81
+ }
82
+ /** One sentence per unresolvable reference, naming the variable and where the value belongs. */
83
+ export function describeMissing(server, missing) {
84
+ return missing.map((m) => `${m.variable} for ${server} is stored as a reference (${m.ref}) but no value was found. Open the server and set it again — it is kept in this machine's keychain, not in mcp.json, so it does not travel with the file.`);
85
+ }
86
+ /**
87
+ * Split a pasted plain-text map into what should be stored where.
88
+ *
89
+ * Used by the paste-config route to tell the user what will happen BEFORE anything is written.
90
+ */
91
+ export function planSecretStorage(server, plain) {
92
+ return Object.entries(plain).map(([variable, value]) => {
93
+ const toKeychain = looksLikeSecret(variable, value);
94
+ return {
95
+ variable,
96
+ value,
97
+ toKeychain,
98
+ ref: toKeychain ? secretRefFor(server, variable) : null,
99
+ };
100
+ });
101
+ }
102
+ /** The map as it should be WRITTEN to mcp.json: secrets as references, the rest verbatim. */
103
+ export function toStoredValues(plan) {
104
+ const out = {};
105
+ for (const item of plan)
106
+ out[item.variable] = item.ref ?? item.value;
107
+ return out;
108
+ }
109
+ /** Every secret reference an entry holds, for cleanup when a server is deleted. */
110
+ export function secretRefsOf(entry) {
111
+ const refs = [];
112
+ for (const map of [entry.env, entry.headers]) {
113
+ for (const value of Object.values(map ?? {})) {
114
+ if (isSecretRef(value))
115
+ refs.push(value.secret);
116
+ }
117
+ }
118
+ return refs;
119
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Which of a server's tools are described to the model.
3
+ *
4
+ * ⚠️ OFF MEANS NOT DESCRIBED — NEVER REMOVED (D-7). The tool stays on the server and stays callable
5
+ * by anything else pointed at it. We are choosing what goes into the model's context, and the copy
6
+ * must not imply we uninstalled anything.
7
+ *
8
+ * Stored as an OPT-OUT list, deliberately: a server that gains a tool in a later release has it
9
+ * enabled by default, which is what someone who never touched this screen expects. An opt-in list
10
+ * would silently hide every new tool and look like the server had stopped offering them.
11
+ */
12
+ import type { MCPServerEntry } from '../mcp-config.js';
13
+ /** Tools explicitly switched off for this server. Absent means "all of them are on". */
14
+ export declare function disabledTools(entry: Pick<MCPServerEntry, 'disabledTools'>): readonly string[];
15
+ export declare function isToolEnabled(entry: Pick<MCPServerEntry, 'disabledTools'>, toolName: string): boolean;
16
+ /**
17
+ * The entry with one tool switched on or off.
18
+ *
19
+ * Returns a NEW entry; the caller writes it. Switching the last one back on removes the key
20
+ * entirely rather than leaving `"disabledTools": []` behind, so a file nobody has customised stays
21
+ * clean and diffs stay small.
22
+ */
23
+ export declare function setToolEnabled(entry: MCPServerEntry, toolName: string, enabled: boolean): MCPServerEntry;
24
+ /**
25
+ * Drop the tools this server has switched off.
26
+ *
27
+ * ⚠️ MUST BE APPLIED ON EVERY PATH THAT REGISTERS MCP TOOLS. There are two in Desktop — the agent
28
+ * factory, and the post-connect catch-up for agents built before MCP finished connecting. The same
29
+ * split already bit MCP grants: filtering one path leaves the feature working except during
30
+ * startup, which is the same as not working.
31
+ *
32
+ * Shaped like `filterMcpToolsByGrant` so the two compose over the same array.
33
+ */
34
+ export declare function filterMcpToolsByEnablement<T extends {
35
+ definition: {
36
+ name: string;
37
+ };
38
+ }>(tools: readonly T[], serverOf: (toolName: string) => string | null, entryOf: (server: string) => Pick<MCPServerEntry, 'disabledTools'> | undefined,
39
+ /** Strip the `mcp_<server>_` prefix to get the name the server itself uses. */
40
+ bareName: (toolName: string, server: string) => string): T[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which of a server's tools are described to the model.
3
+ *
4
+ * ⚠️ OFF MEANS NOT DESCRIBED — NEVER REMOVED (D-7). The tool stays on the server and stays callable
5
+ * by anything else pointed at it. We are choosing what goes into the model's context, and the copy
6
+ * must not imply we uninstalled anything.
7
+ *
8
+ * Stored as an OPT-OUT list, deliberately: a server that gains a tool in a later release has it
9
+ * enabled by default, which is what someone who never touched this screen expects. An opt-in list
10
+ * would silently hide every new tool and look like the server had stopped offering them.
11
+ */
12
+ /** Tools explicitly switched off for this server. Absent means "all of them are on". */
13
+ export function disabledTools(entry) {
14
+ return entry.disabledTools ?? [];
15
+ }
16
+ export function isToolEnabled(entry, toolName) {
17
+ return !disabledTools(entry).includes(toolName);
18
+ }
19
+ /**
20
+ * The entry with one tool switched on or off.
21
+ *
22
+ * Returns a NEW entry; the caller writes it. Switching the last one back on removes the key
23
+ * entirely rather than leaving `"disabledTools": []` behind, so a file nobody has customised stays
24
+ * clean and diffs stay small.
25
+ */
26
+ export function setToolEnabled(entry, toolName, enabled) {
27
+ const current = new Set(disabledTools(entry));
28
+ if (enabled)
29
+ current.delete(toolName);
30
+ else
31
+ current.add(toolName);
32
+ const next = { ...entry };
33
+ if (current.size === 0)
34
+ delete next.disabledTools;
35
+ else
36
+ next.disabledTools = [...current].sort();
37
+ return next;
38
+ }
39
+ /**
40
+ * Drop the tools this server has switched off.
41
+ *
42
+ * ⚠️ MUST BE APPLIED ON EVERY PATH THAT REGISTERS MCP TOOLS. There are two in Desktop — the agent
43
+ * factory, and the post-connect catch-up for agents built before MCP finished connecting. The same
44
+ * split already bit MCP grants: filtering one path leaves the feature working except during
45
+ * startup, which is the same as not working.
46
+ *
47
+ * Shaped like `filterMcpToolsByGrant` so the two compose over the same array.
48
+ */
49
+ export function filterMcpToolsByEnablement(tools, serverOf, entryOf,
50
+ /** Strip the `mcp_<server>_` prefix to get the name the server itself uses. */
51
+ bareName) {
52
+ return tools.filter((tool) => {
53
+ const server = serverOf(tool.definition.name);
54
+ if (server === null)
55
+ return true;
56
+ const entry = entryOf(server);
57
+ if (!entry)
58
+ return true;
59
+ return isToolEnabled(entry, bareName(tool.definition.name, server));
60
+ });
61
+ }