@stigg/terminal 0.0.1-alpha
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/LICENSE +15 -0
- package/README.md +47 -0
- package/dist/api/client.d.ts +6 -0
- package/dist/api/client.js +48 -0
- package/dist/api/format-key.d.ts +7 -0
- package/dist/api/format-key.js +12 -0
- package/dist/api/graphql-client.d.ts +5 -0
- package/dist/api/graphql-client.js +44 -0
- package/dist/api/operations.d.ts +65 -0
- package/dist/api/operations.js +77 -0
- package/dist/api/types.d.ts +18 -0
- package/dist/api/types.js +1 -0
- package/dist/auth/callback-server.d.ts +14 -0
- package/dist/auth/callback-server.js +145 -0
- package/dist/auth/config.d.ts +2 -0
- package/dist/auth/config.js +24 -0
- package/dist/auth/oauth.d.ts +17 -0
- package/dist/auth/oauth.js +94 -0
- package/dist/auth/storage.d.ts +6 -0
- package/dist/auth/storage.js +34 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +79 -0
- package/dist/commands/dash.d.ts +1 -0
- package/dist/commands/dash.js +24 -0
- package/dist/commands/debug.d.ts +1 -0
- package/dist/commands/debug.js +53 -0
- package/dist/commands/env.d.ts +1 -0
- package/dist/commands/env.js +15 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +59 -0
- package/dist/commands/mcp.d.ts +7 -0
- package/dist/commands/mcp.js +37 -0
- package/dist/commands/skills.d.ts +6 -0
- package/dist/commands/skills.js +48 -0
- package/dist/headless/host-agent.d.ts +19 -0
- package/dist/headless/host-agent.js +64 -0
- package/dist/headless/init-phase1.d.ts +9 -0
- package/dist/headless/init-phase1.js +173 -0
- package/dist/headless/init-phase2.d.ts +36 -0
- package/dist/headless/init-phase2.js +150 -0
- package/dist/headless/next-step-prompt.d.ts +7 -0
- package/dist/headless/next-step-prompt.js +25 -0
- package/dist/headless/options.d.ts +30 -0
- package/dist/headless/options.js +77 -0
- package/dist/headless/reporter.d.ts +16 -0
- package/dist/headless/reporter.js +41 -0
- package/dist/headless/setup.d.ts +29 -0
- package/dist/headless/setup.js +80 -0
- package/dist/launch/agent.d.ts +55 -0
- package/dist/launch/agent.js +134 -0
- package/dist/mcp/clients/base.d.ts +49 -0
- package/dist/mcp/clients/base.js +66 -0
- package/dist/mcp/clients/claude-code.d.ts +22 -0
- package/dist/mcp/clients/claude-code.js +120 -0
- package/dist/mcp/clients/claude-desktop.d.ts +9 -0
- package/dist/mcp/clients/claude-desktop.js +35 -0
- package/dist/mcp/clients/codex.d.ts +13 -0
- package/dist/mcp/clients/codex.js +113 -0
- package/dist/mcp/clients/cursor.d.ts +9 -0
- package/dist/mcp/clients/cursor.js +26 -0
- package/dist/mcp/clients/index.d.ts +7 -0
- package/dist/mcp/clients/index.js +27 -0
- package/dist/mcp/clients/mcp-remote.d.ts +11 -0
- package/dist/mcp/clients/mcp-remote.js +13 -0
- package/dist/mcp/clients/vscode.d.ts +17 -0
- package/dist/mcp/clients/vscode.js +84 -0
- package/dist/mcp/clients.d.ts +4 -0
- package/dist/mcp/clients.js +50 -0
- package/dist/mcp/config-merge.d.ts +9 -0
- package/dist/mcp/config-merge.js +51 -0
- package/dist/mcp/writer.d.ts +6 -0
- package/dist/mcp/writer.js +65 -0
- package/dist/setup/storage.d.ts +21 -0
- package/dist/setup/storage.js +34 -0
- package/dist/skills/install.d.ts +19 -0
- package/dist/skills/install.js +64 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +1 -0
- package/dist/ui/components/Card.d.ts +11 -0
- package/dist/ui/components/Card.js +19 -0
- package/dist/ui/components/ContextRow.d.ts +11 -0
- package/dist/ui/components/ContextRow.js +8 -0
- package/dist/ui/components/Footer.d.ts +10 -0
- package/dist/ui/components/Footer.js +9 -0
- package/dist/ui/components/Header.d.ts +11 -0
- package/dist/ui/components/Header.js +6 -0
- package/dist/ui/components/JsonPreview.d.ts +13 -0
- package/dist/ui/components/JsonPreview.js +25 -0
- package/dist/ui/components/SectionTitle.d.ts +6 -0
- package/dist/ui/components/SectionTitle.js +5 -0
- package/dist/ui/hooks/useAsyncEffect.d.ts +3 -0
- package/dist/ui/hooks/useAsyncEffect.js +20 -0
- package/dist/ui/hooks/useResize.d.ts +4 -0
- package/dist/ui/hooks/useResize.js +20 -0
- package/dist/ui/hud.d.ts +15 -0
- package/dist/ui/hud.js +30 -0
- package/dist/ui/ink-theme.d.ts +2 -0
- package/dist/ui/ink-theme.js +34 -0
- package/dist/ui/intro/LogoView.d.ts +12 -0
- package/dist/ui/intro/LogoView.js +226 -0
- package/dist/ui/intro/MatrixIntro.d.ts +6 -0
- package/dist/ui/intro/MatrixIntro.js +80 -0
- package/dist/ui/intro/logo.d.ts +6 -0
- package/dist/ui/intro/logo.js +21 -0
- package/dist/ui/messages.d.ts +5 -0
- package/dist/ui/messages.js +5 -0
- package/dist/ui/screens/DashScreen.d.ts +6 -0
- package/dist/ui/screens/DashScreen.js +27 -0
- package/dist/ui/screens/DebugScreen.d.ts +6 -0
- package/dist/ui/screens/DebugScreen.js +39 -0
- package/dist/ui/screens/InitScreen.d.ts +6 -0
- package/dist/ui/screens/InitScreen.js +138 -0
- package/dist/ui/screens/MenuScreen.d.ts +7 -0
- package/dist/ui/screens/MenuScreen.js +38 -0
- package/dist/ui/state.d.ts +72 -0
- package/dist/ui/state.js +107 -0
- package/dist/ui/steps/AccountStep.d.ts +8 -0
- package/dist/ui/steps/AccountStep.js +42 -0
- package/dist/ui/steps/ApiKeyStep.d.ts +8 -0
- package/dist/ui/steps/ApiKeyStep.js +91 -0
- package/dist/ui/steps/ClientsStep.d.ts +10 -0
- package/dist/ui/steps/ClientsStep.js +69 -0
- package/dist/ui/steps/CredentialKindStep.d.ts +7 -0
- package/dist/ui/steps/CredentialKindStep.js +18 -0
- package/dist/ui/steps/EnvironmentStep.d.ts +8 -0
- package/dist/ui/steps/EnvironmentStep.js +37 -0
- package/dist/ui/steps/LoginStep.d.ts +7 -0
- package/dist/ui/steps/LoginStep.js +56 -0
- package/dist/ui/steps/SkillsStep.d.ts +7 -0
- package/dist/ui/steps/SkillsStep.js +7 -0
- package/dist/ui/steps/SummaryStep.d.ts +8 -0
- package/dist/ui/steps/SummaryStep.js +41 -0
- package/dist/ui/steps/WritingStep.d.ts +10 -0
- package/dist/ui/steps/WritingStep.js +96 -0
- package/dist/ui/theme.d.ts +53 -0
- package/dist/ui/theme.js +66 -0
- package/dist/ui/tui/App.d.ts +10 -0
- package/dist/ui/tui/App.js +51 -0
- package/dist/ui/tui/components/ContextStrip.d.ts +11 -0
- package/dist/ui/tui/components/ContextStrip.js +8 -0
- package/dist/ui/tui/components/TitleBar.d.ts +6 -0
- package/dist/ui/tui/components/TitleBar.js +18 -0
- package/dist/ui/tui/components/WizardChecklist.d.ts +7 -0
- package/dist/ui/tui/components/WizardChecklist.js +69 -0
- package/dist/ui/tui/hooks/keyboard-hints-utils.d.ts +26 -0
- package/dist/ui/tui/hooks/keyboard-hints-utils.js +69 -0
- package/dist/ui/tui/hooks/useKeyBindings.d.ts +14 -0
- package/dist/ui/tui/hooks/useKeyBindings.js +44 -0
- package/dist/ui/tui/hooks/useKeyboardHints.d.ts +13 -0
- package/dist/ui/tui/hooks/useKeyboardHints.js +38 -0
- package/dist/ui/tui/hooks/useStdoutDimensions.d.ts +8 -0
- package/dist/ui/tui/hooks/useStdoutDimensions.js +28 -0
- package/dist/ui/tui/primitives/BlinkingLabel.d.ts +19 -0
- package/dist/ui/tui/primitives/BlinkingLabel.js +25 -0
- package/dist/ui/tui/primitives/ConfirmPrompt.d.ts +16 -0
- package/dist/ui/tui/primitives/ConfirmPrompt.js +36 -0
- package/dist/ui/tui/primitives/KeyboardHintsBar.d.ts +2 -0
- package/dist/ui/tui/primitives/KeyboardHintsBar.js +8 -0
- package/dist/ui/tui/primitives/PickerMenu.d.ts +38 -0
- package/dist/ui/tui/primitives/PickerMenu.js +162 -0
- package/dist/ui/tui/primitives/PromptLabel.d.ts +6 -0
- package/dist/ui/tui/primitives/PromptLabel.js +6 -0
- package/dist/ui/tui/primitives/ScreenContainer.d.ts +39 -0
- package/dist/ui/tui/primitives/ScreenContainer.js +39 -0
- package/dist/ui/tui/primitives/Spinner.d.ts +7 -0
- package/dist/ui/tui/primitives/Spinner.js +18 -0
- package/dist/ui/tui/screens/DashScreen.d.ts +6 -0
- package/dist/ui/tui/screens/DashScreen.js +37 -0
- package/dist/ui/tui/screens/DebugScreen.d.ts +6 -0
- package/dist/ui/tui/screens/DebugScreen.js +48 -0
- package/dist/ui/tui/screens/EnvScreen.d.ts +6 -0
- package/dist/ui/tui/screens/EnvScreen.js +192 -0
- package/dist/ui/tui/screens/InitScreen.d.ts +9 -0
- package/dist/ui/tui/screens/InitScreen.js +102 -0
- package/dist/ui/tui/screens/MenuScreen.d.ts +7 -0
- package/dist/ui/tui/screens/MenuScreen.js +84 -0
- package/dist/ui/tui/start-tui.d.ts +11 -0
- package/dist/ui/tui/start-tui.js +72 -0
- package/dist/ui/tui/steps/AccountStep.d.ts +8 -0
- package/dist/ui/tui/steps/AccountStep.js +42 -0
- package/dist/ui/tui/steps/ApiKeyStep.d.ts +8 -0
- package/dist/ui/tui/steps/ApiKeyStep.js +53 -0
- package/dist/ui/tui/steps/ClientsStep.d.ts +10 -0
- package/dist/ui/tui/steps/ClientsStep.js +52 -0
- package/dist/ui/tui/steps/CredentialKindStep.d.ts +7 -0
- package/dist/ui/tui/steps/CredentialKindStep.js +18 -0
- package/dist/ui/tui/steps/EnvironmentStep.d.ts +8 -0
- package/dist/ui/tui/steps/EnvironmentStep.js +38 -0
- package/dist/ui/tui/steps/LoginStep.d.ts +8 -0
- package/dist/ui/tui/steps/LoginStep.js +79 -0
- package/dist/ui/tui/steps/SkillsStep.d.ts +7 -0
- package/dist/ui/tui/steps/SkillsStep.js +7 -0
- package/dist/ui/tui/steps/SummaryStep.d.ts +10 -0
- package/dist/ui/tui/steps/SummaryStep.js +133 -0
- package/dist/ui/tui/steps/WritingStep.d.ts +10 -0
- package/dist/ui/tui/steps/WritingStep.js +101 -0
- package/package.json +62 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { HostAgent } from '../headless/host-agent.js';
|
|
2
|
+
import type { ClientId } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The subset of {@link ClientId}s we can launch as a foreground CLI agent that
|
|
5
|
+
* inherits this terminal. GUI clients (`claude-desktop`, `vscode`) are excluded —
|
|
6
|
+
* they can't take over the TTY.
|
|
7
|
+
*/
|
|
8
|
+
export type LaunchableAgentId = 'claude-code' | 'codex' | 'cursor';
|
|
9
|
+
export interface LaunchableAgent {
|
|
10
|
+
id: LaunchableAgentId;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LaunchRequest {
|
|
14
|
+
agentId: LaunchableAgentId;
|
|
15
|
+
/** Seed message passed to the agent as its first user turn. */
|
|
16
|
+
prompt: string;
|
|
17
|
+
/** When true, pass the agent's permission-bypass flag. */
|
|
18
|
+
skipPermissions: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function agentName(id: LaunchableAgentId): string;
|
|
21
|
+
export declare function skipPermissionFlag(id: LaunchableAgentId): string;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the launch binary for an agent, or `null` when it isn't available.
|
|
24
|
+
*
|
|
25
|
+
* - `claude-code` reuses {@link findClaudeBinary} (known install paths + PATH,
|
|
26
|
+
* honoring `STIGG_CLAUDE_BINARY`).
|
|
27
|
+
* - `codex` / `cursor` honor a `STIGG_*_BINARY` override (unset → real lookup,
|
|
28
|
+
* empty string → treat as not found, any other value → that path) and
|
|
29
|
+
* otherwise probe PATH. Note this is independent of the MCP client's
|
|
30
|
+
* `isClientSupported()` — e.g. Cursor the IDE can be installed without the
|
|
31
|
+
* `cursor-agent` CLI being on PATH.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveAgentBinary(id: LaunchableAgentId): string | null;
|
|
34
|
+
/** All agents whose launch binary is currently resolvable, in preference order. */
|
|
35
|
+
export declare function listLaunchableAgents(): LaunchableAgent[];
|
|
36
|
+
/**
|
|
37
|
+
* Pick which agent to preselect in the launch picker: the host agent if it's
|
|
38
|
+
* launchable, else the first configured client that's launchable, else the
|
|
39
|
+
* first available agent. Returns `undefined` only when nothing is launchable.
|
|
40
|
+
*/
|
|
41
|
+
export declare function chooseDefaultAgent(hostAgent: HostAgent | null, selectedClients: ClientId[] | undefined, launchable: LaunchableAgent[]): LaunchableAgentId | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Spawn the agent so it inherits this terminal, seeded with `prompt` as its
|
|
44
|
+
* first user message. Resolves with the child's exit code; rejects if the
|
|
45
|
+
* binary can't be found or fails to spawn.
|
|
46
|
+
*
|
|
47
|
+
* MUST be called only after the Ink TUI has fully unmounted and restored the
|
|
48
|
+
* terminal (see `start-tui.ts`) — never while Ink owns the TTY.
|
|
49
|
+
*/
|
|
50
|
+
export declare function launchAgent(req: LaunchRequest): Promise<number>;
|
|
51
|
+
/**
|
|
52
|
+
* Best-effort copy to the system clipboard. Resolves `false` when no clipboard
|
|
53
|
+
* tool is available (the caller should then fall back to showing the text).
|
|
54
|
+
*/
|
|
55
|
+
export declare function copyPromptToClipboard(text: string): Promise<boolean>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { findClaudeBinary } from '../mcp/clients/claude-code.js';
|
|
3
|
+
const AGENT_NAMES = {
|
|
4
|
+
'claude-code': 'Claude Code',
|
|
5
|
+
codex: 'Codex',
|
|
6
|
+
cursor: 'Cursor',
|
|
7
|
+
};
|
|
8
|
+
const AGENT_BINARIES = {
|
|
9
|
+
'claude-code': 'claude',
|
|
10
|
+
codex: 'codex',
|
|
11
|
+
cursor: 'cursor-agent',
|
|
12
|
+
};
|
|
13
|
+
/** Flag that disables the agent's interactive permission prompts. */
|
|
14
|
+
const AGENT_SKIP_PERMISSION_FLAGS = {
|
|
15
|
+
'claude-code': '--dangerously-skip-permissions',
|
|
16
|
+
codex: '--yolo',
|
|
17
|
+
cursor: '--force',
|
|
18
|
+
};
|
|
19
|
+
/** Stable display/preference order. */
|
|
20
|
+
const LAUNCHABLE_ORDER = ['claude-code', 'codex', 'cursor'];
|
|
21
|
+
/** Env override that lets tests stand in a fake binary, mirroring STIGG_CLAUDE_BINARY. */
|
|
22
|
+
const BINARY_OVERRIDE_ENV = {
|
|
23
|
+
codex: 'STIGG_CODEX_BINARY',
|
|
24
|
+
cursor: 'STIGG_CURSOR_AGENT_BINARY',
|
|
25
|
+
};
|
|
26
|
+
export function agentName(id) {
|
|
27
|
+
return AGENT_NAMES[id];
|
|
28
|
+
}
|
|
29
|
+
export function skipPermissionFlag(id) {
|
|
30
|
+
return AGENT_SKIP_PERMISSION_FLAGS[id];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* POSIX `command -v` existence check. Windows is out of scope for agent launch
|
|
34
|
+
* (the binaries here are POSIX CLIs and detection relies on `sh`).
|
|
35
|
+
*/
|
|
36
|
+
function commandExists(cmd) {
|
|
37
|
+
try {
|
|
38
|
+
execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'pipe' });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the launch binary for an agent, or `null` when it isn't available.
|
|
47
|
+
*
|
|
48
|
+
* - `claude-code` reuses {@link findClaudeBinary} (known install paths + PATH,
|
|
49
|
+
* honoring `STIGG_CLAUDE_BINARY`).
|
|
50
|
+
* - `codex` / `cursor` honor a `STIGG_*_BINARY` override (unset → real lookup,
|
|
51
|
+
* empty string → treat as not found, any other value → that path) and
|
|
52
|
+
* otherwise probe PATH. Note this is independent of the MCP client's
|
|
53
|
+
* `isClientSupported()` — e.g. Cursor the IDE can be installed without the
|
|
54
|
+
* `cursor-agent` CLI being on PATH.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveAgentBinary(id) {
|
|
57
|
+
if (id === 'claude-code')
|
|
58
|
+
return findClaudeBinary();
|
|
59
|
+
const envVar = BINARY_OVERRIDE_ENV[id];
|
|
60
|
+
if (envVar in process.env) {
|
|
61
|
+
const override = process.env[envVar];
|
|
62
|
+
return override ? override : null;
|
|
63
|
+
}
|
|
64
|
+
const binary = AGENT_BINARIES[id];
|
|
65
|
+
return commandExists(binary) ? binary : null;
|
|
66
|
+
}
|
|
67
|
+
/** All agents whose launch binary is currently resolvable, in preference order. */
|
|
68
|
+
export function listLaunchableAgents() {
|
|
69
|
+
return LAUNCHABLE_ORDER.filter((id) => resolveAgentBinary(id) !== null).map((id) => ({
|
|
70
|
+
id,
|
|
71
|
+
name: AGENT_NAMES[id],
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Pick which agent to preselect in the launch picker: the host agent if it's
|
|
76
|
+
* launchable, else the first configured client that's launchable, else the
|
|
77
|
+
* first available agent. Returns `undefined` only when nothing is launchable.
|
|
78
|
+
*/
|
|
79
|
+
export function chooseDefaultAgent(hostAgent, selectedClients, launchable) {
|
|
80
|
+
if (launchable.length === 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
const available = new Set(launchable.map((a) => a.id));
|
|
83
|
+
const isLaunchable = (id) => available.has(id) && LAUNCHABLE_ORDER.includes(id);
|
|
84
|
+
if (hostAgent && isLaunchable(hostAgent.id))
|
|
85
|
+
return hostAgent.id;
|
|
86
|
+
const fromSelected = selectedClients?.find(isLaunchable);
|
|
87
|
+
if (fromSelected)
|
|
88
|
+
return fromSelected;
|
|
89
|
+
return launchable[0].id;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Spawn the agent so it inherits this terminal, seeded with `prompt` as its
|
|
93
|
+
* first user message. Resolves with the child's exit code; rejects if the
|
|
94
|
+
* binary can't be found or fails to spawn.
|
|
95
|
+
*
|
|
96
|
+
* MUST be called only after the Ink TUI has fully unmounted and restored the
|
|
97
|
+
* terminal (see `start-tui.ts`) — never while Ink owns the TTY.
|
|
98
|
+
*/
|
|
99
|
+
export function launchAgent(req) {
|
|
100
|
+
const binary = resolveAgentBinary(req.agentId);
|
|
101
|
+
if (!binary) {
|
|
102
|
+
return Promise.reject(new Error(`${AGENT_NAMES[req.agentId]} CLI ("${AGENT_BINARIES[req.agentId]}") was not found on your PATH`));
|
|
103
|
+
}
|
|
104
|
+
const args = req.skipPermissions
|
|
105
|
+
? [AGENT_SKIP_PERMISSION_FLAGS[req.agentId], req.prompt]
|
|
106
|
+
: [req.prompt];
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const child = spawn(binary, args, { stdio: 'inherit', cwd: process.cwd() });
|
|
109
|
+
child.on('error', reject);
|
|
110
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const CLIPBOARD_COMMANDS = [
|
|
114
|
+
{ bin: 'pbcopy', args: [] },
|
|
115
|
+
{ bin: 'wl-copy', args: [] },
|
|
116
|
+
{ bin: 'xclip', args: ['-selection', 'clipboard'] },
|
|
117
|
+
{ bin: 'xsel', args: ['--clipboard', '--input'] },
|
|
118
|
+
];
|
|
119
|
+
/**
|
|
120
|
+
* Best-effort copy to the system clipboard. Resolves `false` when no clipboard
|
|
121
|
+
* tool is available (the caller should then fall back to showing the text).
|
|
122
|
+
*/
|
|
123
|
+
export function copyPromptToClipboard(text) {
|
|
124
|
+
const cmd = CLIPBOARD_COMMANDS.find((c) => commandExists(c.bin));
|
|
125
|
+
if (!cmd)
|
|
126
|
+
return Promise.resolve(false);
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const child = spawn(cmd.bin, cmd.args);
|
|
129
|
+
child.on('error', () => resolve(false));
|
|
130
|
+
child.on('close', (code) => resolve(code === 0));
|
|
131
|
+
child.stdin?.on('error', () => resolve(false));
|
|
132
|
+
child.stdin?.end(text);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ClientId, Credential, WriteResult } from '../../types.js';
|
|
2
|
+
import { type McpServerConfig } from '../writer.js';
|
|
3
|
+
export declare const STIGG_MCP_URL = "https://mcp.stigg.io";
|
|
4
|
+
export declare const STIGG_SERVER_NAME = "stigg";
|
|
5
|
+
export type { McpServerConfig };
|
|
6
|
+
export interface InstallResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
/** File path for JSON/TOML installs, or a label like "claude mcp add" for CLI installs. */
|
|
9
|
+
target: string;
|
|
10
|
+
/** Set when an existing entry was backed up before overwrite. */
|
|
11
|
+
backedUpTo?: string;
|
|
12
|
+
/** Present iff ok=false. */
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare abstract class McpClient {
|
|
16
|
+
abstract readonly id: ClientId;
|
|
17
|
+
abstract readonly name: string;
|
|
18
|
+
/** Human-readable short label for the picker (typically the config path or the CLI command). */
|
|
19
|
+
abstract installTarget(): string;
|
|
20
|
+
/** Is this client installed/usable on this machine? */
|
|
21
|
+
abstract isClientSupported(): Promise<boolean>;
|
|
22
|
+
/** Does this client already have a Stigg MCP entry? */
|
|
23
|
+
abstract hasExistingEntry(): Promise<boolean>;
|
|
24
|
+
/** Install or overwrite the Stigg MCP entry for this client. */
|
|
25
|
+
abstract addServer(credential: Credential): Promise<InstallResult>;
|
|
26
|
+
/** Remove the Stigg MCP entry, if any. */
|
|
27
|
+
abstract removeServer(): Promise<InstallResult>;
|
|
28
|
+
/** Map an InstallResult to the WriteResult shape used by the wizard. */
|
|
29
|
+
toWriteResult(r: InstallResult): WriteResult;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Concrete template for clients whose config is "edit JSON at a known path
|
|
33
|
+
* and put an entry under <propertyName>.<serverName>". Subclasses override:
|
|
34
|
+
* - getConfigPath() — required
|
|
35
|
+
* - getServerConfig() — required (the entry shape)
|
|
36
|
+
* - getServerPropertyName() — optional, defaults to "mcpServers"
|
|
37
|
+
*/
|
|
38
|
+
export declare abstract class DefaultJsonMcpClient extends McpClient {
|
|
39
|
+
protected readonly serverName = "stigg";
|
|
40
|
+
abstract getConfigPath(): string;
|
|
41
|
+
abstract getServerConfig(credential: Credential): McpServerConfig;
|
|
42
|
+
getServerPropertyName(): string;
|
|
43
|
+
installTarget(): string;
|
|
44
|
+
hasExistingEntry(): Promise<boolean>;
|
|
45
|
+
addServer(credential: Credential): Promise<InstallResult>;
|
|
46
|
+
removeServer(): Promise<InstallResult>;
|
|
47
|
+
}
|
|
48
|
+
export declare function prettifyPath(p: string): string;
|
|
49
|
+
export declare function errorMessage(e: unknown): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { hasMcpEntry, removeMcpEntry, writeMcpEntry } from '../writer.js';
|
|
3
|
+
export const STIGG_MCP_URL = 'https://mcp.stigg.io';
|
|
4
|
+
export const STIGG_SERVER_NAME = 'stigg';
|
|
5
|
+
export class McpClient {
|
|
6
|
+
/** Map an InstallResult to the WriteResult shape used by the wizard. */
|
|
7
|
+
toWriteResult(r) {
|
|
8
|
+
return {
|
|
9
|
+
path: r.target,
|
|
10
|
+
client: this.id,
|
|
11
|
+
written: r.ok,
|
|
12
|
+
backedUpTo: r.backedUpTo,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Concrete template for clients whose config is "edit JSON at a known path
|
|
18
|
+
* and put an entry under <propertyName>.<serverName>". Subclasses override:
|
|
19
|
+
* - getConfigPath() — required
|
|
20
|
+
* - getServerConfig() — required (the entry shape)
|
|
21
|
+
* - getServerPropertyName() — optional, defaults to "mcpServers"
|
|
22
|
+
*/
|
|
23
|
+
export class DefaultJsonMcpClient extends McpClient {
|
|
24
|
+
serverName = STIGG_SERVER_NAME;
|
|
25
|
+
getServerPropertyName() {
|
|
26
|
+
return 'mcpServers';
|
|
27
|
+
}
|
|
28
|
+
installTarget() {
|
|
29
|
+
return prettifyPath(this.getConfigPath());
|
|
30
|
+
}
|
|
31
|
+
async hasExistingEntry() {
|
|
32
|
+
return hasMcpEntry(this.getConfigPath(), this.getServerPropertyName(), this.serverName);
|
|
33
|
+
}
|
|
34
|
+
async addServer(credential) {
|
|
35
|
+
const path = this.getConfigPath();
|
|
36
|
+
try {
|
|
37
|
+
const entry = this.getServerConfig(credential);
|
|
38
|
+
const result = await writeMcpEntry(path, this.getServerPropertyName(), this.serverName, entry);
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
target: prettifyPath(result.path),
|
|
42
|
+
backedUpTo: result.backedUpTo ? prettifyPath(result.backedUpTo) : undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return { ok: false, target: prettifyPath(path), reason: errorMessage(e) };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async removeServer() {
|
|
50
|
+
const path = this.getConfigPath();
|
|
51
|
+
try {
|
|
52
|
+
const result = await removeMcpEntry(path, this.getServerPropertyName(), this.serverName);
|
|
53
|
+
return { ok: result.written, target: prettifyPath(path) };
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
return { ok: false, target: prettifyPath(path), reason: errorMessage(e) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function prettifyPath(p) {
|
|
61
|
+
const home = homedir();
|
|
62
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
63
|
+
}
|
|
64
|
+
export function errorMessage(e) {
|
|
65
|
+
return e instanceof Error ? e.message : String(e);
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClientId, Credential } from '../../types.js';
|
|
2
|
+
import { type InstallResult, McpClient } from './base.js';
|
|
3
|
+
/**
|
|
4
|
+
* Locate the `claude` CLI binary using PostHog wizard's lookup order:
|
|
5
|
+
* three known install paths, then the shell PATH. Cached for the
|
|
6
|
+
* lifetime of the process.
|
|
7
|
+
*
|
|
8
|
+
* Tests can short-circuit via STIGG_CLAUDE_BINARY:
|
|
9
|
+
* - unset → real lookup (default)
|
|
10
|
+
* - empty string → behave as if not found
|
|
11
|
+
* - any other value → that string is returned as the binary
|
|
12
|
+
*/
|
|
13
|
+
export declare function findClaudeBinary(): string | null;
|
|
14
|
+
export declare class ClaudeCodeMcpClient extends McpClient {
|
|
15
|
+
readonly id: ClientId;
|
|
16
|
+
readonly name = "Claude Code";
|
|
17
|
+
installTarget(): string;
|
|
18
|
+
isClientSupported(): Promise<boolean>;
|
|
19
|
+
hasExistingEntry(): Promise<boolean>;
|
|
20
|
+
addServer(credential: Credential): Promise<InstallResult>;
|
|
21
|
+
removeServer(): Promise<InstallResult>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { errorMessage, McpClient, STIGG_MCP_URL, STIGG_SERVER_NAME, } from './base.js';
|
|
6
|
+
let cachedBinary;
|
|
7
|
+
/**
|
|
8
|
+
* Locate the `claude` CLI binary using PostHog wizard's lookup order:
|
|
9
|
+
* three known install paths, then the shell PATH. Cached for the
|
|
10
|
+
* lifetime of the process.
|
|
11
|
+
*
|
|
12
|
+
* Tests can short-circuit via STIGG_CLAUDE_BINARY:
|
|
13
|
+
* - unset → real lookup (default)
|
|
14
|
+
* - empty string → behave as if not found
|
|
15
|
+
* - any other value → that string is returned as the binary
|
|
16
|
+
*/
|
|
17
|
+
export function findClaudeBinary() {
|
|
18
|
+
// Direct process.env read (not optionalEnv): we need to distinguish unset
|
|
19
|
+
// from empty-string, since '' means "treat as not found" for tests.
|
|
20
|
+
if ('STIGG_CLAUDE_BINARY' in process.env) {
|
|
21
|
+
const override = process.env.STIGG_CLAUDE_BINARY;
|
|
22
|
+
return override ? override : null;
|
|
23
|
+
}
|
|
24
|
+
if (cachedBinary !== undefined)
|
|
25
|
+
return cachedBinary;
|
|
26
|
+
const candidates = [
|
|
27
|
+
join(homedir(), '.claude', 'local', 'claude'),
|
|
28
|
+
'/usr/local/bin/claude',
|
|
29
|
+
'/opt/homebrew/bin/claude',
|
|
30
|
+
];
|
|
31
|
+
for (const path of candidates) {
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
cachedBinary = path;
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
execSync('command -v claude', { stdio: 'pipe' });
|
|
39
|
+
cachedBinary = 'claude';
|
|
40
|
+
return 'claude';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
cachedBinary = null;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class ClaudeCodeMcpClient extends McpClient {
|
|
48
|
+
id = 'claude-code';
|
|
49
|
+
name = 'Claude Code';
|
|
50
|
+
installTarget() {
|
|
51
|
+
return 'claude mcp add';
|
|
52
|
+
}
|
|
53
|
+
isClientSupported() {
|
|
54
|
+
return Promise.resolve(findClaudeBinary() !== null);
|
|
55
|
+
}
|
|
56
|
+
hasExistingEntry() {
|
|
57
|
+
const bin = findClaudeBinary();
|
|
58
|
+
if (!bin)
|
|
59
|
+
return Promise.resolve(false);
|
|
60
|
+
try {
|
|
61
|
+
execFileSync(bin, ['mcp', 'get', STIGG_SERVER_NAME], { stdio: 'pipe' });
|
|
62
|
+
return Promise.resolve(true);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return Promise.resolve(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
addServer(credential) {
|
|
69
|
+
const bin = findClaudeBinary();
|
|
70
|
+
if (!bin) {
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
ok: false,
|
|
73
|
+
target: 'claude mcp add',
|
|
74
|
+
reason: 'claude CLI not found on PATH',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Idempotent overwrite: remove first (may not exist), then add.
|
|
78
|
+
try {
|
|
79
|
+
execFileSync(bin, ['mcp', 'remove', '--scope', 'user', STIGG_SERVER_NAME], { stdio: 'pipe' });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// already absent — fine
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
execFileSync(bin, [
|
|
86
|
+
'mcp',
|
|
87
|
+
'add',
|
|
88
|
+
'--scope',
|
|
89
|
+
'user',
|
|
90
|
+
'--transport',
|
|
91
|
+
'http',
|
|
92
|
+
STIGG_SERVER_NAME,
|
|
93
|
+
STIGG_MCP_URL,
|
|
94
|
+
'--header',
|
|
95
|
+
`X-API-KEY: ${credential.value}`,
|
|
96
|
+
], { stdio: 'pipe' });
|
|
97
|
+
return Promise.resolve({ ok: true, target: 'claude mcp add' });
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return Promise.resolve({ ok: false, target: 'claude mcp add', reason: errorMessage(e) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
removeServer() {
|
|
104
|
+
const bin = findClaudeBinary();
|
|
105
|
+
if (!bin) {
|
|
106
|
+
return Promise.resolve({
|
|
107
|
+
ok: false,
|
|
108
|
+
target: 'claude mcp remove',
|
|
109
|
+
reason: 'claude CLI not found on PATH',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
execFileSync(bin, ['mcp', 'remove', '--scope', 'user', STIGG_SERVER_NAME], { stdio: 'pipe' });
|
|
114
|
+
return Promise.resolve({ ok: true, target: 'claude mcp remove' });
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
return Promise.resolve({ ok: false, target: 'claude mcp remove', reason: errorMessage(e) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClientId, Credential } from '../../types.js';
|
|
2
|
+
import { DefaultJsonMcpClient, type McpServerConfig } from './base.js';
|
|
3
|
+
export declare class ClaudeDesktopMcpClient extends DefaultJsonMcpClient {
|
|
4
|
+
readonly id: ClientId;
|
|
5
|
+
readonly name = "Claude Desktop";
|
|
6
|
+
getConfigPath(): string;
|
|
7
|
+
isClientSupported(): Promise<boolean>;
|
|
8
|
+
getServerConfig(credential: Credential): McpServerConfig;
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { access, constants } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { optionalEnv } from '../../auth/config.js';
|
|
5
|
+
import { DefaultJsonMcpClient } from './base.js';
|
|
6
|
+
import { buildMcpRemoteEntry } from './mcp-remote.js';
|
|
7
|
+
export class ClaudeDesktopMcpClient extends DefaultJsonMcpClient {
|
|
8
|
+
id = 'claude-desktop';
|
|
9
|
+
name = 'Claude Desktop';
|
|
10
|
+
getConfigPath() {
|
|
11
|
+
const override = optionalEnv('STIGG_CLAUDE_DESKTOP_CONFIG');
|
|
12
|
+
if (override)
|
|
13
|
+
return override;
|
|
14
|
+
if (process.platform === 'darwin') {
|
|
15
|
+
return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
16
|
+
}
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
return join(process.env.APPDATA ?? '', 'Claude', 'claude_desktop_config.json');
|
|
19
|
+
}
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
async isClientSupported() {
|
|
23
|
+
if (process.platform !== 'darwin' && process.platform !== 'win32')
|
|
24
|
+
return false;
|
|
25
|
+
const configDir = dirname(this.getConfigPath());
|
|
26
|
+
return pathExists(configDir);
|
|
27
|
+
}
|
|
28
|
+
getServerConfig(credential) {
|
|
29
|
+
// Claude Desktop only supports stdio transport — wrap our HTTP MCP with mcp-remote.
|
|
30
|
+
return buildMcpRemoteEntry(credential);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function pathExists(path) {
|
|
34
|
+
return access(path, constants.F_OK).then(() => true, () => false);
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClientId, Credential } from '../../types.js';
|
|
2
|
+
import { type InstallResult, McpClient } from './base.js';
|
|
3
|
+
export declare class CodexMcpClient extends McpClient {
|
|
4
|
+
readonly id: ClientId;
|
|
5
|
+
readonly name = "Codex";
|
|
6
|
+
private readonly serverName;
|
|
7
|
+
installTarget(): string;
|
|
8
|
+
getConfigPath(): string;
|
|
9
|
+
isClientSupported(): Promise<boolean>;
|
|
10
|
+
hasExistingEntry(): Promise<boolean>;
|
|
11
|
+
addServer(credential: Credential): Promise<InstallResult>;
|
|
12
|
+
removeServer(): Promise<InstallResult>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import TOML from '@iarna/toml';
|
|
6
|
+
import { optionalEnv } from '../../auth/config.js';
|
|
7
|
+
import { errorMessage, McpClient, prettifyPath, STIGG_SERVER_NAME, } from './base.js';
|
|
8
|
+
import { buildMcpRemoteEntry } from './mcp-remote.js';
|
|
9
|
+
export class CodexMcpClient extends McpClient {
|
|
10
|
+
id = 'codex';
|
|
11
|
+
name = 'Codex';
|
|
12
|
+
serverName = STIGG_SERVER_NAME;
|
|
13
|
+
installTarget() {
|
|
14
|
+
return prettifyPath(this.getConfigPath());
|
|
15
|
+
}
|
|
16
|
+
getConfigPath() {
|
|
17
|
+
return optionalEnv('STIGG_CODEX_CONFIG') ?? join(homedir(), '.codex', 'config.toml');
|
|
18
|
+
}
|
|
19
|
+
isClientSupported() {
|
|
20
|
+
return commandExists('codex');
|
|
21
|
+
}
|
|
22
|
+
async hasExistingEntry() {
|
|
23
|
+
const parsed = await readToml(this.getConfigPath());
|
|
24
|
+
if (!parsed)
|
|
25
|
+
return false;
|
|
26
|
+
const servers = parsed.mcp_servers;
|
|
27
|
+
return typeof servers === 'object' && servers !== null && this.serverName in servers;
|
|
28
|
+
}
|
|
29
|
+
async addServer(credential) {
|
|
30
|
+
const path = this.getConfigPath();
|
|
31
|
+
try {
|
|
32
|
+
const { text, parsed } = (await readTomlWithText(path)) ?? { text: '', parsed: {} };
|
|
33
|
+
const entry = buildMcpRemoteEntry(credential);
|
|
34
|
+
const servers = isObject(parsed.mcp_servers) ? { ...parsed.mcp_servers } : {};
|
|
35
|
+
const existed = this.serverName in servers;
|
|
36
|
+
servers[this.serverName] = entry;
|
|
37
|
+
const next = { ...parsed, mcp_servers: servers };
|
|
38
|
+
let backedUpTo;
|
|
39
|
+
if (existed && text) {
|
|
40
|
+
backedUpTo = `${path}.bak.${Date.now()}`;
|
|
41
|
+
await writeFile(backedUpTo, text, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
await mkdir(dirname(path), { recursive: true });
|
|
44
|
+
const stringified = TOML.stringify(next);
|
|
45
|
+
await writeFile(path, ensureNewline(stringified), 'utf8');
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
target: prettifyPath(path),
|
|
49
|
+
backedUpTo: backedUpTo ? prettifyPath(backedUpTo) : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return { ok: false, target: prettifyPath(path), reason: errorMessage(e) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async removeServer() {
|
|
57
|
+
const path = this.getConfigPath();
|
|
58
|
+
try {
|
|
59
|
+
const tomlData = await readTomlWithText(path);
|
|
60
|
+
if (!tomlData)
|
|
61
|
+
return { ok: false, target: prettifyPath(path) };
|
|
62
|
+
const { text, parsed } = tomlData;
|
|
63
|
+
if (!isObject(parsed.mcp_servers) || !(this.serverName in parsed.mcp_servers)) {
|
|
64
|
+
return { ok: false, target: prettifyPath(path) };
|
|
65
|
+
}
|
|
66
|
+
const rest = Object.fromEntries(Object.entries(parsed.mcp_servers).filter(([k]) => k !== this.serverName));
|
|
67
|
+
const next = { ...parsed, mcp_servers: rest };
|
|
68
|
+
const backedUpTo = `${path}.bak.${Date.now()}`;
|
|
69
|
+
await writeFile(backedUpTo, text, 'utf8');
|
|
70
|
+
const stringified = TOML.stringify(next);
|
|
71
|
+
await writeFile(path, ensureNewline(stringified), 'utf8');
|
|
72
|
+
return { ok: true, target: prettifyPath(path), backedUpTo: prettifyPath(backedUpTo) };
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return { ok: false, target: prettifyPath(path), reason: errorMessage(e) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readToml(path) {
|
|
80
|
+
const r = await readTomlWithText(path);
|
|
81
|
+
return r?.parsed ?? null;
|
|
82
|
+
}
|
|
83
|
+
async function readTomlWithText(path) {
|
|
84
|
+
let text;
|
|
85
|
+
try {
|
|
86
|
+
text = await readFile(path, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (e.code === 'ENOENT')
|
|
90
|
+
return null;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
if (!text.trim())
|
|
94
|
+
return { text, parsed: {} };
|
|
95
|
+
return { text, parsed: TOML.parse(text) };
|
|
96
|
+
}
|
|
97
|
+
function isObject(v) {
|
|
98
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
99
|
+
}
|
|
100
|
+
function ensureNewline(s) {
|
|
101
|
+
return s.endsWith('\n') ? s : `${s}\n`;
|
|
102
|
+
}
|
|
103
|
+
function commandExists(cmd) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
try {
|
|
106
|
+
execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'pipe' });
|
|
107
|
+
resolve(true);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
resolve(false);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClientId, Credential } from '../../types.js';
|
|
2
|
+
import { DefaultJsonMcpClient, type McpServerConfig } from './base.js';
|
|
3
|
+
export declare class CursorMcpClient extends DefaultJsonMcpClient {
|
|
4
|
+
readonly id: ClientId;
|
|
5
|
+
readonly name = "Cursor";
|
|
6
|
+
getConfigPath(): string;
|
|
7
|
+
isClientSupported(): Promise<boolean>;
|
|
8
|
+
getServerConfig(credential: Credential): McpServerConfig;
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { access, constants } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { optionalEnv } from '../../auth/config.js';
|
|
5
|
+
import { DefaultJsonMcpClient, STIGG_MCP_URL } from './base.js';
|
|
6
|
+
export class CursorMcpClient extends DefaultJsonMcpClient {
|
|
7
|
+
id = 'cursor';
|
|
8
|
+
name = 'Cursor';
|
|
9
|
+
getConfigPath() {
|
|
10
|
+
return optionalEnv('STIGG_CURSOR_CONFIG') ?? join(homedir(), '.cursor', 'mcp.json');
|
|
11
|
+
}
|
|
12
|
+
async isClientSupported() {
|
|
13
|
+
if (process.platform !== 'darwin' && process.platform !== 'win32')
|
|
14
|
+
return false;
|
|
15
|
+
return pathExists(join(homedir(), '.cursor'));
|
|
16
|
+
}
|
|
17
|
+
getServerConfig(credential) {
|
|
18
|
+
return {
|
|
19
|
+
url: STIGG_MCP_URL,
|
|
20
|
+
headers: { 'X-API-KEY': credential.value },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function pathExists(path) {
|
|
25
|
+
return access(path, constants.F_OK).then(() => true, () => false);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClientId } from '../../types.js';
|
|
2
|
+
import type { McpClient } from './base.js';
|
|
3
|
+
export type { InstallResult, McpServerConfig } from './base.js';
|
|
4
|
+
export { McpClient, DefaultJsonMcpClient } from './base.js';
|
|
5
|
+
export declare function getAllClients(): readonly McpClient[];
|
|
6
|
+
export declare function getClientById(id: ClientId): McpClient | undefined;
|
|
7
|
+
export declare function getSupportedClients(): Promise<McpClient[]>;
|