@plmbr/notebook-intelligence 5.0.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/LICENSE +674 -0
- package/README.md +412 -0
- package/lib/api.d.ts +288 -0
- package/lib/api.js +927 -0
- package/lib/cell-output-bundle.d.ts +25 -0
- package/lib/cell-output-bundle.js +129 -0
- package/lib/cell-output-toolbar.d.ts +26 -0
- package/lib/cell-output-toolbar.js +188 -0
- package/lib/chat-progress-feedback.d.ts +3 -0
- package/lib/chat-progress-feedback.js +27 -0
- package/lib/chat-sidebar.d.ts +92 -0
- package/lib/chat-sidebar.js +3452 -0
- package/lib/command-ids.d.ts +39 -0
- package/lib/command-ids.js +44 -0
- package/lib/components/ask-user-question.d.ts +2 -0
- package/lib/components/ask-user-question.js +85 -0
- package/lib/components/checkbox.d.ts +2 -0
- package/lib/components/checkbox.js +30 -0
- package/lib/components/claude-mcp-panel.d.ts +2 -0
- package/lib/components/claude-mcp-panel.js +275 -0
- package/lib/components/claude-mcp-paste.d.ts +7 -0
- package/lib/components/claude-mcp-paste.js +104 -0
- package/lib/components/claude-session-picker.d.ts +8 -0
- package/lib/components/claude-session-picker.js +127 -0
- package/lib/components/form-dialog.d.ts +25 -0
- package/lib/components/form-dialog.js +35 -0
- package/lib/components/launcher-picker.d.ts +6 -0
- package/lib/components/launcher-picker.js +135 -0
- package/lib/components/mcp-util.d.ts +2 -0
- package/lib/components/mcp-util.js +37 -0
- package/lib/components/notebook-generation-popover.d.ts +7 -0
- package/lib/components/notebook-generation-popover.js +60 -0
- package/lib/components/pill.d.ts +2 -0
- package/lib/components/pill.js +5 -0
- package/lib/components/plugins-panel.d.ts +3 -0
- package/lib/components/plugins-panel.js +466 -0
- package/lib/components/settings-panel.d.ts +11 -0
- package/lib/components/settings-panel.js +742 -0
- package/lib/components/skills-panel.d.ts +2 -0
- package/lib/components/skills-panel.js +1264 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/icons.d.ts +45 -0
- package/lib/icons.js +54 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +2079 -0
- package/lib/markdown-renderer.d.ts +10 -0
- package/lib/markdown-renderer.js +64 -0
- package/lib/notebook-generation-toolbar.d.ts +16 -0
- package/lib/notebook-generation-toolbar.js +197 -0
- package/lib/notebook-generation.d.ts +8 -0
- package/lib/notebook-generation.js +12 -0
- package/lib/open-file-refresh-watcher-env.d.ts +4 -0
- package/lib/open-file-refresh-watcher-env.js +33 -0
- package/lib/open-file-refresh-watcher.d.ts +97 -0
- package/lib/open-file-refresh-watcher.js +190 -0
- package/lib/shell-utils.d.ts +6 -0
- package/lib/shell-utils.js +9 -0
- package/lib/task-target-notebook.d.ts +2 -0
- package/lib/task-target-notebook.js +28 -0
- package/lib/terminal-drag-format.d.ts +9 -0
- package/lib/terminal-drag-format.js +23 -0
- package/lib/terminal-drag.d.ts +12 -0
- package/lib/terminal-drag.js +268 -0
- package/lib/tokens.d.ts +149 -0
- package/lib/tokens.js +88 -0
- package/lib/tour/tour-anchors.d.ts +18 -0
- package/lib/tour/tour-anchors.js +18 -0
- package/lib/tour/tour-config.d.ts +66 -0
- package/lib/tour/tour-config.js +99 -0
- package/lib/tour/tour-defaults.json +58 -0
- package/lib/tour/tour-events.d.ts +19 -0
- package/lib/tour/tour-events.js +30 -0
- package/lib/tour/tour-overlay.d.ts +6 -0
- package/lib/tour/tour-overlay.js +350 -0
- package/lib/tour/tour-state.d.ts +20 -0
- package/lib/tour/tour-state.js +81 -0
- package/lib/tour/tour-steps.d.ts +33 -0
- package/lib/tour/tour-steps.js +216 -0
- package/lib/utils.d.ts +53 -0
- package/lib/utils.js +385 -0
- package/package.json +258 -0
- package/schema/plugin.json +42 -0
- package/src/api.ts +1424 -0
- package/src/cell-output-bundle.ts +176 -0
- package/src/cell-output-toolbar.ts +232 -0
- package/src/chat-progress-feedback.ts +35 -0
- package/src/chat-sidebar.tsx +5147 -0
- package/src/command-ids.ts +67 -0
- package/src/components/ask-user-question.tsx +151 -0
- package/src/components/checkbox.tsx +62 -0
- package/src/components/claude-mcp-panel.tsx +543 -0
- package/src/components/claude-mcp-paste.ts +132 -0
- package/src/components/claude-session-picker.tsx +214 -0
- package/src/components/form-dialog.tsx +75 -0
- package/src/components/launcher-picker.tsx +237 -0
- package/src/components/mcp-util.ts +53 -0
- package/src/components/notebook-generation-popover.tsx +127 -0
- package/src/components/pill.tsx +15 -0
- package/src/components/plugins-panel.tsx +774 -0
- package/src/components/settings-panel.tsx +1631 -0
- package/src/components/skills-panel.tsx +2084 -0
- package/src/handler.ts +51 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +2583 -0
- package/src/markdown-renderer.tsx +153 -0
- package/src/notebook-generation-toolbar.tsx +281 -0
- package/src/notebook-generation.ts +23 -0
- package/src/open-file-refresh-watcher-env.ts +52 -0
- package/src/open-file-refresh-watcher.ts +260 -0
- package/src/shell-utils.ts +10 -0
- package/src/svg.d.ts +4 -0
- package/src/task-target-notebook.ts +37 -0
- package/src/terminal-drag-format.ts +29 -0
- package/src/terminal-drag.ts +382 -0
- package/src/tokens.ts +171 -0
- package/src/tour/tour-anchors.ts +21 -0
- package/src/tour/tour-config.ts +160 -0
- package/src/tour/tour-events.ts +34 -0
- package/src/tour/tour-overlay.tsx +474 -0
- package/src/tour/tour-state.ts +87 -0
- package/src/tour/tour-steps.ts +281 -0
- package/src/utils.ts +455 -0
- package/style/base.css +3238 -0
- package/style/icons/cell-toolbar-bug.svg +5 -0
- package/style/icons/cell-toolbar-chat.svg +5 -0
- package/style/icons/cell-toolbar-sparkle.svg +5 -0
- package/style/icons/claude.svg +1 -0
- package/style/icons/copilot-warning.svg +1 -0
- package/style/icons/copilot.svg +1 -0
- package/style/icons/copy.svg +1 -0
- package/style/icons/openai.svg +1 -0
- package/style/icons/opencode.svg +1 -0
- package/style/icons/sparkles-warning.svg +5 -0
- package/style/icons/sparkles.svg +1 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare namespace CommandIDs {
|
|
2
|
+
const chatuserInput = "notebook-intelligence:chat-user-input";
|
|
3
|
+
const insertAtCursor = "notebook-intelligence:insert-at-cursor";
|
|
4
|
+
const addCodeAsNewCell = "notebook-intelligence:add-code-as-new-cell";
|
|
5
|
+
const createNewFile = "notebook-intelligence:create-new-file";
|
|
6
|
+
const createNewNotebookFromPython = "notebook-intelligence:create-new-notebook-from-py";
|
|
7
|
+
const renameNotebook = "notebook-intelligence:rename-notebook";
|
|
8
|
+
const addCodeCellToNotebook = "notebook-intelligence:add-code-cell-to-notebook";
|
|
9
|
+
const addMarkdownCellToNotebook = "notebook-intelligence:add-markdown-cell-to-notebook";
|
|
10
|
+
const editorGenerateCode = "notebook-intelligence:editor-generate-code";
|
|
11
|
+
const editorExplainThisCode = "notebook-intelligence:editor-explain-this-code";
|
|
12
|
+
const editorFixThisCode = "notebook-intelligence:editor-fix-this-code";
|
|
13
|
+
const editorExplainThisOutput = "notebook-intelligence:editor-explain-this-output";
|
|
14
|
+
const editorTroubleshootThisOutput = "notebook-intelligence:editor-troubleshoot-this-output";
|
|
15
|
+
const editorAskAboutThisOutput = "notebook-intelligence:editor-ask-about-this-output";
|
|
16
|
+
const openGitHubCopilotLoginDialog = "notebook-intelligence:open-github-copilot-login-dialog";
|
|
17
|
+
const openConfigurationDialog = "notebook-intelligence:open-configuration-dialog";
|
|
18
|
+
const addMarkdownCellToActiveNotebook = "notebook-intelligence:add-markdown-cell-to-active-notebook";
|
|
19
|
+
const addCodeCellToActiveNotebook = "notebook-intelligence:add-code-cell-to-active-notebook";
|
|
20
|
+
const deleteCellAtIndex = "notebook-intelligence:delete-cell-at-index";
|
|
21
|
+
const insertCellAtIndex = "notebook-intelligence:insert-cell-at-index";
|
|
22
|
+
const getCellTypeAndSource = "notebook-intelligence:get-cell-type-and-source";
|
|
23
|
+
const setCellTypeAndSource = "notebook-intelligence:set-cell-type-and-source";
|
|
24
|
+
const getNumberOfCells = "notebook-intelligence:get-number-of-cells";
|
|
25
|
+
const getCellOutput = "notebook-intelligence:get-cell-output";
|
|
26
|
+
const runCellAtIndex = "notebook-intelligence:run-cell-at-index";
|
|
27
|
+
const getCurrentFileContent = "notebook-intelligence:get-current-file-content";
|
|
28
|
+
const setCurrentFileContent = "notebook-intelligence:set-current-file-content";
|
|
29
|
+
const openMCPConfigEditor = "notebook-intelligence:open-mcp-config-editor";
|
|
30
|
+
const showFormInputDialog = "notebook-intelligence:show-form-input-dialog";
|
|
31
|
+
const runCommandInTerminal = "notebook-intelligence:run-command-in-terminal";
|
|
32
|
+
const openClaudeCodeLauncher = "notebook-intelligence:open-claude-code-launcher";
|
|
33
|
+
const openOpenCodeLauncher = "notebook-intelligence:open-opencode-launcher";
|
|
34
|
+
const openPiLauncher = "notebook-intelligence:open-pi-launcher";
|
|
35
|
+
const openGitHubCopilotCliLauncher = "notebook-intelligence:open-github-copilot-cli-launcher";
|
|
36
|
+
const openCodexLauncher = "notebook-intelligence:open-codex-launcher";
|
|
37
|
+
const showTour = "notebook-intelligence:show-tour";
|
|
38
|
+
const focusChatInput = "notebook-intelligence:focus-chat-input";
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
// Centralized command-id constants for the JupyterLab plugin. Hoisted out
|
|
3
|
+
// of `src/index.ts` so non-JL modules (and Jest tests) can import them
|
|
4
|
+
// without pulling the JupyterLab packages into their dependency graph.
|
|
5
|
+
export var CommandIDs;
|
|
6
|
+
(function (CommandIDs) {
|
|
7
|
+
CommandIDs.chatuserInput = 'notebook-intelligence:chat-user-input';
|
|
8
|
+
CommandIDs.insertAtCursor = 'notebook-intelligence:insert-at-cursor';
|
|
9
|
+
CommandIDs.addCodeAsNewCell = 'notebook-intelligence:add-code-as-new-cell';
|
|
10
|
+
CommandIDs.createNewFile = 'notebook-intelligence:create-new-file';
|
|
11
|
+
CommandIDs.createNewNotebookFromPython = 'notebook-intelligence:create-new-notebook-from-py';
|
|
12
|
+
CommandIDs.renameNotebook = 'notebook-intelligence:rename-notebook';
|
|
13
|
+
CommandIDs.addCodeCellToNotebook = 'notebook-intelligence:add-code-cell-to-notebook';
|
|
14
|
+
CommandIDs.addMarkdownCellToNotebook = 'notebook-intelligence:add-markdown-cell-to-notebook';
|
|
15
|
+
CommandIDs.editorGenerateCode = 'notebook-intelligence:editor-generate-code';
|
|
16
|
+
CommandIDs.editorExplainThisCode = 'notebook-intelligence:editor-explain-this-code';
|
|
17
|
+
CommandIDs.editorFixThisCode = 'notebook-intelligence:editor-fix-this-code';
|
|
18
|
+
CommandIDs.editorExplainThisOutput = 'notebook-intelligence:editor-explain-this-output';
|
|
19
|
+
CommandIDs.editorTroubleshootThisOutput = 'notebook-intelligence:editor-troubleshoot-this-output';
|
|
20
|
+
CommandIDs.editorAskAboutThisOutput = 'notebook-intelligence:editor-ask-about-this-output';
|
|
21
|
+
CommandIDs.openGitHubCopilotLoginDialog = 'notebook-intelligence:open-github-copilot-login-dialog';
|
|
22
|
+
CommandIDs.openConfigurationDialog = 'notebook-intelligence:open-configuration-dialog';
|
|
23
|
+
CommandIDs.addMarkdownCellToActiveNotebook = 'notebook-intelligence:add-markdown-cell-to-active-notebook';
|
|
24
|
+
CommandIDs.addCodeCellToActiveNotebook = 'notebook-intelligence:add-code-cell-to-active-notebook';
|
|
25
|
+
CommandIDs.deleteCellAtIndex = 'notebook-intelligence:delete-cell-at-index';
|
|
26
|
+
CommandIDs.insertCellAtIndex = 'notebook-intelligence:insert-cell-at-index';
|
|
27
|
+
CommandIDs.getCellTypeAndSource = 'notebook-intelligence:get-cell-type-and-source';
|
|
28
|
+
CommandIDs.setCellTypeAndSource = 'notebook-intelligence:set-cell-type-and-source';
|
|
29
|
+
CommandIDs.getNumberOfCells = 'notebook-intelligence:get-number-of-cells';
|
|
30
|
+
CommandIDs.getCellOutput = 'notebook-intelligence:get-cell-output';
|
|
31
|
+
CommandIDs.runCellAtIndex = 'notebook-intelligence:run-cell-at-index';
|
|
32
|
+
CommandIDs.getCurrentFileContent = 'notebook-intelligence:get-current-file-content';
|
|
33
|
+
CommandIDs.setCurrentFileContent = 'notebook-intelligence:set-current-file-content';
|
|
34
|
+
CommandIDs.openMCPConfigEditor = 'notebook-intelligence:open-mcp-config-editor';
|
|
35
|
+
CommandIDs.showFormInputDialog = 'notebook-intelligence:show-form-input-dialog';
|
|
36
|
+
CommandIDs.runCommandInTerminal = 'notebook-intelligence:run-command-in-terminal';
|
|
37
|
+
CommandIDs.openClaudeCodeLauncher = 'notebook-intelligence:open-claude-code-launcher';
|
|
38
|
+
CommandIDs.openOpenCodeLauncher = 'notebook-intelligence:open-opencode-launcher';
|
|
39
|
+
CommandIDs.openPiLauncher = 'notebook-intelligence:open-pi-launcher';
|
|
40
|
+
CommandIDs.openGitHubCopilotCliLauncher = 'notebook-intelligence:open-github-copilot-cli-launcher';
|
|
41
|
+
CommandIDs.openCodexLauncher = 'notebook-intelligence:open-codex-launcher';
|
|
42
|
+
CommandIDs.showTour = 'notebook-intelligence:show-tour';
|
|
43
|
+
CommandIDs.focusChatInput = 'notebook-intelligence:focus-chat-input';
|
|
44
|
+
})(CommandIDs || (CommandIDs = {}));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
import React, { useId, useState } from 'react';
|
|
3
|
+
export function AskUserQuestion(props) {
|
|
4
|
+
var _a;
|
|
5
|
+
const userQuestions = props.userQuestions.content;
|
|
6
|
+
const [selectedAnswers, setSelectedAnswers] = useState({});
|
|
7
|
+
// Form-scoped id prefix so DOM ids stay unique even when two questions
|
|
8
|
+
// share label text (or several AskUserQuestion forms render in the
|
|
9
|
+
// same chat transcript). React.useId() is the purpose-built primitive
|
|
10
|
+
// here: stable for the component's lifetime, SSR-safe, and survives
|
|
11
|
+
// StrictMode double-render without producing mismatched id/htmlFor
|
|
12
|
+
// pairs (a Math.random fallback could mis-pair under cache eviction).
|
|
13
|
+
// The server-provided identifier is preferred when present so two
|
|
14
|
+
// remounts of the same form keep the same DOM ids.
|
|
15
|
+
const reactId = useId();
|
|
16
|
+
const serverId = (_a = userQuestions.identifier) === null || _a === void 0 ? void 0 : _a.id;
|
|
17
|
+
const formIdPrefix = typeof serverId === 'string' && serverId.length > 0
|
|
18
|
+
? `nbi-auq-${serverId}`
|
|
19
|
+
: `nbi-auq${reactId}`;
|
|
20
|
+
const onOptionSelected = (question, option) => {
|
|
21
|
+
var _a, _b, _c;
|
|
22
|
+
if (question.multiSelect) {
|
|
23
|
+
if ((_a = selectedAnswers[question.question]) === null || _a === void 0 ? void 0 : _a.includes(option.label)) {
|
|
24
|
+
setSelectedAnswers({
|
|
25
|
+
...selectedAnswers,
|
|
26
|
+
[question.question]: ((_b = selectedAnswers[question.question]) !== null && _b !== void 0 ? _b : []).filter((o) => o !== option.label)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
setSelectedAnswers({
|
|
31
|
+
...selectedAnswers,
|
|
32
|
+
[question.question]: [
|
|
33
|
+
...((_c = selectedAnswers[question.question]) !== null && _c !== void 0 ? _c : []),
|
|
34
|
+
option.label
|
|
35
|
+
]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
setSelectedAnswers({
|
|
41
|
+
...selectedAnswers,
|
|
42
|
+
[question.question]: [option.label]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return (React.createElement(React.Fragment, null,
|
|
47
|
+
userQuestions.title ? (React.createElement("div", null,
|
|
48
|
+
React.createElement("b", null, userQuestions.title))) : null,
|
|
49
|
+
userQuestions.message ? React.createElement("div", null, userQuestions.message) : null,
|
|
50
|
+
React.createElement("form", { className: "ask-user-question-form", onSubmit: event => {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
props.onSubmit(selectedAnswers);
|
|
53
|
+
} },
|
|
54
|
+
userQuestions.questions.map((question, qIndex) => {
|
|
55
|
+
const questionDomId = `${formIdPrefix}-q${qIndex}`;
|
|
56
|
+
// A single-select group is a radio group with a shared name so
|
|
57
|
+
// screen readers announce "1 of N selected" rather than
|
|
58
|
+
// treating each option as an independent checkbox. The wrapper
|
|
59
|
+
// role mirrors the input type: radiogroup for radios, group
|
|
60
|
+
// (the ARIA-1.2 fallback when no dedicated checkbox-group role
|
|
61
|
+
// exists) for checkboxes.
|
|
62
|
+
const inputType = question.multiSelect ? 'checkbox' : 'radio';
|
|
63
|
+
const groupRole = question.multiSelect ? 'group' : 'radiogroup';
|
|
64
|
+
return (React.createElement("div", { className: "ask-user-question-container", key: questionDomId, role: groupRole, "aria-labelledby": `${questionDomId}-label` },
|
|
65
|
+
React.createElement("div", { className: "ask-user-question-question", id: `${questionDomId}-label` }, question.question),
|
|
66
|
+
React.createElement("div", { className: "ask-user-question-header" }, question.header),
|
|
67
|
+
React.createElement("div", { className: "ask-user-question-options" }, question.options.map((option, oIndex) => {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
const optionDomId = `${questionDomId}-o${oIndex}`;
|
|
70
|
+
return (React.createElement("div", { className: "ask-user-question-option", key: optionDomId },
|
|
71
|
+
React.createElement("div", { className: "ask-user-question-option-input-container" },
|
|
72
|
+
React.createElement("input", { id: optionDomId, name: questionDomId, type: inputType, checked: (_b = (_a = selectedAnswers[question.question]) === null || _a === void 0 ? void 0 : _a.includes(option.label)) !== null && _b !== void 0 ? _b : false, onChange: () => onOptionSelected(question, option) }),
|
|
73
|
+
React.createElement("label", { htmlFor: optionDomId, className: "ask-user-question-option-label-container" },
|
|
74
|
+
React.createElement("div", { className: "ask-user-question-option-label" }, option.label),
|
|
75
|
+
React.createElement("div", { className: "ask-user-question-option-description" }, option.description)))));
|
|
76
|
+
}))));
|
|
77
|
+
}),
|
|
78
|
+
React.createElement("div", { className: "ask-user-question-footer" },
|
|
79
|
+
React.createElement("button", { type: "submit", className: "jp-Dialog-button jp-mod-accept jp-mod-styled" },
|
|
80
|
+
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, userQuestions.submitLabel)),
|
|
81
|
+
React.createElement("button", { type: "button", className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => {
|
|
82
|
+
props.onCancel();
|
|
83
|
+
} },
|
|
84
|
+
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, userQuestions.cancelLabel))))));
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from '../icons';
|
|
4
|
+
export function CheckBoxItem(props) {
|
|
5
|
+
const indent = props.indent || 0;
|
|
6
|
+
const disabled = props.disabled || false;
|
|
7
|
+
const checked = !!props.checked;
|
|
8
|
+
const activate = (event) => {
|
|
9
|
+
if (!disabled) {
|
|
10
|
+
props.onClick(event);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
// Custom checkbox widget. Tracks the WAI-ARIA pattern: role='checkbox'
|
|
14
|
+
// + aria-checked + tabIndex=0 + Space/Enter to toggle. Disabled state
|
|
15
|
+
// is exposed via aria-disabled so the element stays focusable; a
|
|
16
|
+
// screen reader user can still announce "checkbox, disabled" and hear
|
|
17
|
+
// what's off without being able to toggle it.
|
|
18
|
+
return (React.createElement("div", { className: `checkbox-item checkbox-item-indent-${indent} ${props.header ? 'checkbox-item-header' : ''}`, title: props.tooltip || props.title || '', role: "checkbox", "aria-checked": checked, "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : 0, onClick: activate, onKeyDown: event => {
|
|
19
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
20
|
+
// Space scrolls the page by default, Enter is a no-op on a
|
|
21
|
+
// <div>; prevent both so the checkbox can own them.
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
activate(event);
|
|
24
|
+
}
|
|
25
|
+
} },
|
|
26
|
+
React.createElement("div", { className: "checkbox-item-toggle" },
|
|
27
|
+
checked ? (React.createElement(MdCheckBox, { className: "checkbox-icon", style: { opacity: disabled ? 0.5 : 1 }, "aria-hidden": "true" })) : (React.createElement(MdOutlineCheckBoxOutlineBlank, { className: "checkbox-icon", style: { opacity: disabled ? 0.5 : 1 }, "aria-hidden": "true" })),
|
|
28
|
+
React.createElement("span", { style: { opacity: disabled ? 0.5 : 1 } }, props.label)),
|
|
29
|
+
props.title && (React.createElement("div", { className: "checkbox-item-description" }, props.title))));
|
|
30
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
4
|
+
import { NBIAPI } from '../api';
|
|
5
|
+
import { FormDialog } from './form-dialog';
|
|
6
|
+
import { configToInput, parseKVLines, parseMcpJsonEntry } from './claude-mcp-paste';
|
|
7
|
+
const SCOPES = ['user', 'project', 'local'];
|
|
8
|
+
const TRANSPORTS = ['stdio', 'sse', 'http'];
|
|
9
|
+
const SCOPE_HINT = {
|
|
10
|
+
user: 'available in all your projects',
|
|
11
|
+
project: 'shared via the project repo (.mcp.json)',
|
|
12
|
+
local: 'this project, this user only'
|
|
13
|
+
};
|
|
14
|
+
export function SettingsPanelComponentClaudeMCP(_props) {
|
|
15
|
+
var _a;
|
|
16
|
+
const [servers, setServers] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
20
|
+
const [pendingRemoval, setPendingRemoval] = useState(null);
|
|
21
|
+
const [pendingToggle, setPendingToggle] = useState(null);
|
|
22
|
+
const refresh = async () => {
|
|
23
|
+
var _a;
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
try {
|
|
27
|
+
const list = await NBIAPI.listClaudeMCPServers();
|
|
28
|
+
setServers(list);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
refresh();
|
|
39
|
+
}, []);
|
|
40
|
+
const handleRemove = async (srv) => {
|
|
41
|
+
var _a;
|
|
42
|
+
const ok = await showDialog({
|
|
43
|
+
title: 'Remove MCP server?',
|
|
44
|
+
body: `"${srv.name}" will be removed from Claude's ${srv.scope}-scope config.`,
|
|
45
|
+
buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
|
|
46
|
+
});
|
|
47
|
+
if (!ok.button.accept) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setPendingRemoval(srv);
|
|
51
|
+
try {
|
|
52
|
+
await NBIAPI.removeClaudeMCPServer(srv.name, srv.scope);
|
|
53
|
+
await refresh();
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
setError(`Failed to remove: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setPendingRemoval(null);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const handleToggleDisabled = async (srv) => {
|
|
63
|
+
var _a;
|
|
64
|
+
setPendingToggle(srv);
|
|
65
|
+
try {
|
|
66
|
+
await NBIAPI.setClaudeMCPServerDisabled(srv.name, srv.scope, !srv.disabledForWorkspace);
|
|
67
|
+
await refresh();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
setError(`Failed to update workspace state: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
setPendingToggle(null);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const handleAddSubmit = async (input) => {
|
|
77
|
+
// Errors are rendered inside the dialog so they're not hidden behind the
|
|
78
|
+
// modal backdrop; rethrow so the dialog can keep itself open.
|
|
79
|
+
await NBIAPI.addClaudeMCPServer(input);
|
|
80
|
+
setAddOpen(false);
|
|
81
|
+
await refresh();
|
|
82
|
+
};
|
|
83
|
+
const grouped = {
|
|
84
|
+
user: [],
|
|
85
|
+
project: [],
|
|
86
|
+
local: []
|
|
87
|
+
};
|
|
88
|
+
for (const srv of servers) {
|
|
89
|
+
((_a = grouped[srv.scope]) !== null && _a !== void 0 ? _a : grouped.user).push(srv);
|
|
90
|
+
}
|
|
91
|
+
return (React.createElement("div", { className: "config-dialog-body nbi-skills-panel" },
|
|
92
|
+
React.createElement("div", { className: "nbi-skills-header" },
|
|
93
|
+
React.createElement("div", { className: "nbi-skills-title" }, "Claude MCP"),
|
|
94
|
+
React.createElement("div", { className: "nbi-skills-header-actions" },
|
|
95
|
+
React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: refresh, disabled: loading, title: "Re-read Claude's MCP config from disk" },
|
|
96
|
+
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, loading ? 'Refreshing…' : 'Refresh')),
|
|
97
|
+
React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => setAddOpen(true) },
|
|
98
|
+
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Add server")))),
|
|
99
|
+
React.createElement("div", { className: "nbi-info-banner", role: "note" },
|
|
100
|
+
"These are Claude Code's own MCP servers (read from",
|
|
101
|
+
' ',
|
|
102
|
+
React.createElement("code", null, "~/.claude.json"),
|
|
103
|
+
", project ",
|
|
104
|
+
React.createElement("code", null, ".mcp.json"),
|
|
105
|
+
", and the per-user-per-project block). Independent of the NBI MCP Servers tab."),
|
|
106
|
+
error && (React.createElement("div", { className: "nbi-skills-error", role: "alert" }, error)),
|
|
107
|
+
SCOPES.map(scope => (React.createElement(ClaudeMCPScopeSection, { key: scope, scope: scope, servers: grouped[scope], loading: loading, pendingRemoval: pendingRemoval, pendingToggle: pendingToggle, onRemove: handleRemove, onToggleDisabled: handleToggleDisabled }))),
|
|
108
|
+
addOpen && (React.createElement(ClaudeMCPAddDialog, { onCancel: () => setAddOpen(false), onSubmit: handleAddSubmit }))));
|
|
109
|
+
}
|
|
110
|
+
function ClaudeMCPScopeSection(props) {
|
|
111
|
+
return (React.createElement("div", { className: "nbi-skills-section" },
|
|
112
|
+
React.createElement("div", { className: "nbi-skills-section-caption", title: SCOPE_HINT[props.scope] }, props.scope.toUpperCase()),
|
|
113
|
+
props.servers.length === 0 ? (React.createElement("div", { className: "nbi-skills-empty" }, props.loading ? 'Loading…' : 'No servers in this scope.')) : (props.servers.map(srv => {
|
|
114
|
+
var _a, _b, _c, _d;
|
|
115
|
+
return (React.createElement(ClaudeMCPRow, { key: `${srv.scope}-${srv.name}`, srv: srv, removing: ((_a = props.pendingRemoval) === null || _a === void 0 ? void 0 : _a.name) === srv.name &&
|
|
116
|
+
((_b = props.pendingRemoval) === null || _b === void 0 ? void 0 : _b.scope) === srv.scope, toggling: ((_c = props.pendingToggle) === null || _c === void 0 ? void 0 : _c.name) === srv.name &&
|
|
117
|
+
((_d = props.pendingToggle) === null || _d === void 0 ? void 0 : _d.scope) === srv.scope, onRemove: () => props.onRemove(srv), onToggleDisabled: () => props.onToggleDisabled(srv) }));
|
|
118
|
+
}))));
|
|
119
|
+
}
|
|
120
|
+
function ClaudeMCPRow(props) {
|
|
121
|
+
const { srv } = props;
|
|
122
|
+
const summary = srv.transport === 'stdio'
|
|
123
|
+
? [srv.command, ...srv.args].filter(Boolean).join(' ')
|
|
124
|
+
: srv.url;
|
|
125
|
+
const disabled = srv.disabledForWorkspace;
|
|
126
|
+
const toggleLabel = disabled
|
|
127
|
+
? props.toggling
|
|
128
|
+
? 'Enabling…'
|
|
129
|
+
: 'Enable for workspace'
|
|
130
|
+
: props.toggling
|
|
131
|
+
? 'Disabling…'
|
|
132
|
+
: 'Disable for workspace';
|
|
133
|
+
const toggleTitle = disabled
|
|
134
|
+
? 'Re-enable this server for the current Jupyter workspace'
|
|
135
|
+
: 'Hide this server from Claude in the current Jupyter workspace (other workspaces unaffected)';
|
|
136
|
+
return (React.createElement("div", { className: `nbi-skill-row${disabled ? ' nbi-skill-row-disabled' : ''}` },
|
|
137
|
+
React.createElement("div", { className: "nbi-skill-row-main" },
|
|
138
|
+
React.createElement("div", { className: "nbi-skill-row-name" },
|
|
139
|
+
srv.name,
|
|
140
|
+
disabled && (React.createElement("span", { className: "nbi-skill-row-badge" }, "Disabled for workspace"))),
|
|
141
|
+
React.createElement("div", { className: "nbi-skill-row-description" },
|
|
142
|
+
React.createElement("code", null, srv.transport),
|
|
143
|
+
summary && React.createElement("span", null,
|
|
144
|
+
": ",
|
|
145
|
+
summary))),
|
|
146
|
+
React.createElement("div", { className: "nbi-skill-row-actions", onClick: e => e.stopPropagation() },
|
|
147
|
+
React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: props.onToggleDisabled, disabled: props.toggling, title: toggleTitle },
|
|
148
|
+
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, toggleLabel)),
|
|
149
|
+
React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: props.onRemove, disabled: props.removing },
|
|
150
|
+
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, props.removing ? 'Removing…' : 'Remove')))));
|
|
151
|
+
}
|
|
152
|
+
const JSON_PLACEHOLDER = `"server-key": {
|
|
153
|
+
"command": "uvx",
|
|
154
|
+
"args": ["server-package@latest"]
|
|
155
|
+
}`;
|
|
156
|
+
const FORM_TAB_ID = 'nbi-mcp-add-tab-form';
|
|
157
|
+
const JSON_TAB_ID = 'nbi-mcp-add-tab-json';
|
|
158
|
+
const FORM_PANEL_ID = 'nbi-mcp-add-panel-form';
|
|
159
|
+
const JSON_PANEL_ID = 'nbi-mcp-add-panel-json';
|
|
160
|
+
function ClaudeMCPAddDialog(props) {
|
|
161
|
+
const [scope, setScope] = useState('user');
|
|
162
|
+
const [mode, setMode] = useState('form');
|
|
163
|
+
const [transport, setTransport] = useState('stdio');
|
|
164
|
+
const [name, setName] = useState('');
|
|
165
|
+
const [commandOrUrl, setCommandOrUrl] = useState('');
|
|
166
|
+
const [argsText, setArgsText] = useState('');
|
|
167
|
+
const [envText, setEnvText] = useState('');
|
|
168
|
+
const [headersText, setHeadersText] = useState('');
|
|
169
|
+
const [jsonText, setJsonText] = useState('');
|
|
170
|
+
const [submitting, setSubmitting] = useState(false);
|
|
171
|
+
const [submitError, setSubmitError] = useState(null);
|
|
172
|
+
const nameInputRef = useRef(null);
|
|
173
|
+
const jsonInputRef = useRef(null);
|
|
174
|
+
const formTabRef = useRef(null);
|
|
175
|
+
const jsonTabRef = useRef(null);
|
|
176
|
+
// Skip the first effect run so we don't steal focus from the Scope select
|
|
177
|
+
// when the dialog mounts; only re-focus when the user actively switches.
|
|
178
|
+
const hasMountedRef = useRef(false);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
var _a, _b;
|
|
181
|
+
if (!hasMountedRef.current) {
|
|
182
|
+
hasMountedRef.current = true;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (mode === 'json') {
|
|
186
|
+
(_a = jsonInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
(_b = nameInputRef.current) === null || _b === void 0 ? void 0 : _b.focus();
|
|
190
|
+
}
|
|
191
|
+
}, [mode]);
|
|
192
|
+
const canSubmit = mode === 'json'
|
|
193
|
+
? jsonText.trim().length > 0 && !submitting
|
|
194
|
+
: name.trim() && commandOrUrl.trim() && !submitting;
|
|
195
|
+
const handleSubmit = async () => {
|
|
196
|
+
var _a;
|
|
197
|
+
if (!canSubmit) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
setSubmitError(null);
|
|
201
|
+
setSubmitting(true);
|
|
202
|
+
try {
|
|
203
|
+
let input;
|
|
204
|
+
if (mode === 'json') {
|
|
205
|
+
const { name: parsedName, config } = parseMcpJsonEntry(jsonText);
|
|
206
|
+
input = configToInput(parsedName, config, scope);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const argsList = argsText
|
|
210
|
+
.split('\n')
|
|
211
|
+
.map(line => line.trim())
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
input = {
|
|
214
|
+
name: name.trim(),
|
|
215
|
+
scope,
|
|
216
|
+
transport,
|
|
217
|
+
commandOrUrl: commandOrUrl.trim(),
|
|
218
|
+
args: transport === 'stdio' ? argsList : undefined,
|
|
219
|
+
env: transport === 'stdio' ? parseKVLines(envText, '=') : undefined,
|
|
220
|
+
headers: transport === 'stdio' ? undefined : parseKVLines(headersText, ':')
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
await props.onSubmit(input);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
setSubmitError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
setSubmitting(false);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const onTabsKeyDown = (event) => {
|
|
233
|
+
var _a;
|
|
234
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
const next = mode === 'form' ? 'json' : 'form';
|
|
239
|
+
setMode(next);
|
|
240
|
+
(_a = (next === 'form' ? formTabRef : jsonTabRef).current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
241
|
+
};
|
|
242
|
+
return (React.createElement(FormDialog, { title: "Add MCP server", submitLabel: "Add", submitInProgressLabel: "Adding\u2026", canSubmit: Boolean(canSubmit), submitting: submitting, error: submitError, onCancel: props.onCancel, onSubmit: handleSubmit },
|
|
243
|
+
React.createElement("div", { className: "nbi-form-field" },
|
|
244
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-scope" }, "Scope"),
|
|
245
|
+
React.createElement("select", { id: "nbi-mcp-add-scope", value: scope, onChange: e => setScope(e.target.value) }, SCOPES.map(s => (React.createElement("option", { key: s, value: s },
|
|
246
|
+
s,
|
|
247
|
+
" \u2014 ",
|
|
248
|
+
SCOPE_HINT[s]))))),
|
|
249
|
+
React.createElement("div", { className: "nbi-form-field" },
|
|
250
|
+
React.createElement("label", { id: "nbi-mcp-add-input-mode-label" }, "Input mode"),
|
|
251
|
+
React.createElement("div", { className: "nbi-segmented-control", role: "tablist", "aria-labelledby": "nbi-mcp-add-input-mode-label", onKeyDown: onTabsKeyDown },
|
|
252
|
+
React.createElement("button", { type: "button", role: "tab", id: FORM_TAB_ID, ref: formTabRef, "aria-selected": mode === 'form', "aria-controls": FORM_PANEL_ID, tabIndex: mode === 'form' ? 0 : -1, className: `nbi-segmented-control-option${mode === 'form' ? ' is-active' : ''}`, onClick: () => setMode('form') }, "Form"),
|
|
253
|
+
React.createElement("button", { type: "button", role: "tab", id: JSON_TAB_ID, ref: jsonTabRef, "aria-selected": mode === 'json', "aria-controls": JSON_PANEL_ID, tabIndex: mode === 'json' ? 0 : -1, className: `nbi-segmented-control-option${mode === 'json' ? ' is-active' : ''}`, onClick: () => setMode('json') }, "JSON"))),
|
|
254
|
+
mode === 'json' ? (React.createElement("div", { className: "nbi-form-field", role: "tabpanel", id: JSON_PANEL_ID, "aria-labelledby": JSON_TAB_ID },
|
|
255
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-json" }, "JSON"),
|
|
256
|
+
React.createElement("textarea", { id: "nbi-mcp-add-json", ref: jsonInputRef, rows: 10, value: jsonText, onChange: e => setJsonText(e.target.value), placeholder: JSON_PLACEHOLDER, spellCheck: false, autoFocus: true }))) : (React.createElement("div", { role: "tabpanel", id: FORM_PANEL_ID, "aria-labelledby": FORM_TAB_ID, className: "nbi-mcp-add-form-panel" },
|
|
257
|
+
React.createElement("div", { className: "nbi-form-field" },
|
|
258
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-name" }, "Name"),
|
|
259
|
+
React.createElement("input", { id: "nbi-mcp-add-name", ref: nameInputRef, type: "text", value: name, onChange: e => setName(e.target.value), placeholder: "my-server", autoFocus: true })),
|
|
260
|
+
React.createElement("div", { className: "nbi-form-field" },
|
|
261
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-transport" }, "Transport"),
|
|
262
|
+
React.createElement("select", { id: "nbi-mcp-add-transport", value: transport, onChange: e => setTransport(e.target.value) }, TRANSPORTS.map(t => (React.createElement("option", { key: t, value: t }, t))))),
|
|
263
|
+
React.createElement("div", { className: "nbi-form-field" },
|
|
264
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-command-or-url" }, transport === 'stdio' ? 'Command' : 'URL'),
|
|
265
|
+
React.createElement("input", { id: "nbi-mcp-add-command-or-url", type: "text", value: commandOrUrl, onChange: e => setCommandOrUrl(e.target.value), placeholder: transport === 'stdio' ? 'npx' : 'https://example.com/mcp' })),
|
|
266
|
+
transport === 'stdio' && (React.createElement("div", { className: "nbi-form-field" },
|
|
267
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-args" }, "Args (one per line)"),
|
|
268
|
+
React.createElement("textarea", { id: "nbi-mcp-add-args", rows: 3, value: argsText, onChange: e => setArgsText(e.target.value), placeholder: '-y\n@scope/package@latest' }))),
|
|
269
|
+
transport === 'stdio' && (React.createElement("div", { className: "nbi-form-field" },
|
|
270
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-env" }, "Environment (KEY=value, one per line)"),
|
|
271
|
+
React.createElement("textarea", { id: "nbi-mcp-add-env", rows: 3, value: envText, onChange: e => setEnvText(e.target.value), placeholder: "API_KEY=\u2026" }))),
|
|
272
|
+
transport !== 'stdio' && (React.createElement("div", { className: "nbi-form-field" },
|
|
273
|
+
React.createElement("label", { htmlFor: "nbi-mcp-add-headers" }, "Headers (Name: value, one per line)"),
|
|
274
|
+
React.createElement("textarea", { id: "nbi-mcp-add-headers", rows: 3, value: headersText, onChange: e => setHeadersText(e.target.value), placeholder: "Authorization: Bearer \u2026" })))))));
|
|
275
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ClaudeMCPScope, IClaudeMCPAddInput } from '../api';
|
|
2
|
+
export declare function parseMcpJsonEntry(raw: string): {
|
|
3
|
+
name: string;
|
|
4
|
+
config: Record<string, any>;
|
|
5
|
+
};
|
|
6
|
+
export declare function configToInput(name: string, config: Record<string, any>, scope: ClaudeMCPScope): IClaudeMCPAddInput;
|
|
7
|
+
export declare function parseKVLines(text: string, separator: string): Record<string, string>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
const VALID_TRANSPORTS = [
|
|
3
|
+
'stdio',
|
|
4
|
+
'sse',
|
|
5
|
+
'http'
|
|
6
|
+
];
|
|
7
|
+
// Accept any of these paste shapes:
|
|
8
|
+
// "server-key": { ... } (bare key + object)
|
|
9
|
+
// { "server-key": { ... } } (wrapped, one entry)
|
|
10
|
+
// { "mcpServers": { "server-key": { ... } } } (full mcp.json)
|
|
11
|
+
export function parseMcpJsonEntry(raw) {
|
|
12
|
+
const trimmed = raw.trim().replace(/^,|,$/g, '');
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
throw new Error('JSON is empty.');
|
|
15
|
+
}
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(trimmed);
|
|
19
|
+
}
|
|
20
|
+
catch (_a) {
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(`{${trimmed}}`);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
throw new Error(`Invalid JSON: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error('JSON must describe a server entry.');
|
|
30
|
+
}
|
|
31
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === 'object') {
|
|
32
|
+
parsed = parsed.mcpServers;
|
|
33
|
+
}
|
|
34
|
+
const keys = Object.keys(parsed);
|
|
35
|
+
if (keys.length === 0) {
|
|
36
|
+
throw new Error('Expected at least one server entry.');
|
|
37
|
+
}
|
|
38
|
+
if (keys.length > 1) {
|
|
39
|
+
throw new Error('Multiple servers found; paste one entry at a time.');
|
|
40
|
+
}
|
|
41
|
+
const name = keys[0];
|
|
42
|
+
const config = parsed[name];
|
|
43
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
44
|
+
throw new Error(`Server "${name}" config must be an object.`);
|
|
45
|
+
}
|
|
46
|
+
return { name, config };
|
|
47
|
+
}
|
|
48
|
+
export function configToInput(name, config, scope) {
|
|
49
|
+
const url = typeof config.url === 'string' ? config.url.trim() : '';
|
|
50
|
+
const command = typeof config.command === 'string' ? config.command.trim() : '';
|
|
51
|
+
if (!url && !command) {
|
|
52
|
+
throw new Error('Server config must include "command" or "url".');
|
|
53
|
+
}
|
|
54
|
+
const explicitTransport = typeof config.transport === 'string' &&
|
|
55
|
+
VALID_TRANSPORTS.includes(config.transport)
|
|
56
|
+
? config.transport
|
|
57
|
+
: null;
|
|
58
|
+
// If the user pasted an explicit `transport`, it must match the shape of
|
|
59
|
+
// the rest of the config: `stdio` requires `command`, `sse`/`http` require
|
|
60
|
+
// `url`. A mismatch is more likely a typo than intent, and silently
|
|
61
|
+
// downgrading to whatever shape we see is the footgun reviewers flagged.
|
|
62
|
+
if (explicitTransport === 'stdio' && !command) {
|
|
63
|
+
throw new Error('transport: "stdio" requires a "command" field.');
|
|
64
|
+
}
|
|
65
|
+
if ((explicitTransport === 'http' || explicitTransport === 'sse') && !url) {
|
|
66
|
+
throw new Error(`transport: "${explicitTransport}" requires a "url" field.`);
|
|
67
|
+
}
|
|
68
|
+
const transport = explicitTransport !== null && explicitTransport !== void 0 ? explicitTransport : (url ? 'http' : 'stdio');
|
|
69
|
+
const commandOrUrl = transport === 'stdio' ? command : url;
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
scope,
|
|
73
|
+
transport,
|
|
74
|
+
commandOrUrl,
|
|
75
|
+
args: transport === 'stdio' && Array.isArray(config.args)
|
|
76
|
+
? config.args.map(String)
|
|
77
|
+
: undefined,
|
|
78
|
+
env: transport === 'stdio' ? toStringRecord(config.env) : undefined,
|
|
79
|
+
headers: transport !== 'stdio' ? toStringRecord(config.headers) : undefined
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function parseKVLines(text, separator) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const line of text.split('\n')) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const idx = trimmed.indexOf(separator);
|
|
90
|
+
if (idx > 0) {
|
|
91
|
+
out[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function toStringRecord(obj) {
|
|
97
|
+
const out = {};
|
|
98
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
99
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
100
|
+
out[String(k)] = String(v);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IClaudeSessionInfo, IClaudeSessionList } from '../api';
|
|
3
|
+
export interface IClaudeSessionPickerProps {
|
|
4
|
+
onResume: (session: IClaudeSessionInfo) => void;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
fetchSessions?: () => Promise<IClaudeSessionList>;
|
|
7
|
+
}
|
|
8
|
+
export declare function ClaudeSessionPicker(props: IClaudeSessionPickerProps): JSX.Element;
|