@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
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/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
matchesKey,
|
|
4
|
+
replaceTabs,
|
|
5
|
+
sliceWithWidth,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
} from "@oh-my-pi/pi-tui";
|
|
9
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { type ThemeColor, theme } from "../modes/theme/theme";
|
|
11
|
+
|
|
12
|
+
/** Distinct states of a realtime call connection. */
|
|
13
|
+
export type LivePhase = "connecting" | "listening" | "working" | "speaking" | "muted" | "error";
|
|
14
|
+
/** Configuration callbacks for user interactions in the visualizer. */
|
|
15
|
+
export interface LiveVisualizerOptions {
|
|
16
|
+
onStop(): void;
|
|
17
|
+
onToggleMute(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeTranscript(text: string): string {
|
|
21
|
+
return replaceTabs(sanitizeText(text)).replace(/\s+/g, " ").trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truncateFromStart(text: string, width: number): string {
|
|
25
|
+
if (width <= 0) return "";
|
|
26
|
+
const textWidth = visibleWidth(text);
|
|
27
|
+
if (textWidth <= width) return text;
|
|
28
|
+
if (width === 1) return "…";
|
|
29
|
+
return `…${sliceWithWidth(text, textWidth - width + 1, width - 1, true).text}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A compact, fixed-height terminal component for displaying a realtime call. */
|
|
33
|
+
export class LiveVisualizer implements Component {
|
|
34
|
+
readonly wantsKeyRelease = false;
|
|
35
|
+
|
|
36
|
+
readonly #options: LiveVisualizerOptions;
|
|
37
|
+
|
|
38
|
+
#phase: LivePhase = "connecting";
|
|
39
|
+
#inputLevel = 0;
|
|
40
|
+
#displayLevel = 0;
|
|
41
|
+
#frame = 0;
|
|
42
|
+
#userTranscript = "";
|
|
43
|
+
|
|
44
|
+
#cache:
|
|
45
|
+
| {
|
|
46
|
+
width: number;
|
|
47
|
+
phase: LivePhase;
|
|
48
|
+
displayLevel: number;
|
|
49
|
+
frame: number;
|
|
50
|
+
userTranscript: string;
|
|
51
|
+
lines: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
| undefined;
|
|
54
|
+
|
|
55
|
+
constructor(options: LiveVisualizerOptions) {
|
|
56
|
+
this.#options = options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Updates the current call phase. */
|
|
60
|
+
setPhase(phase: LivePhase): void {
|
|
61
|
+
if (this.#phase !== phase) {
|
|
62
|
+
this.#phase = phase;
|
|
63
|
+
this.invalidate();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Updates the microphone volume level (0..1). */
|
|
68
|
+
setInputLevel(level: number): void {
|
|
69
|
+
const next = Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0;
|
|
70
|
+
if (this.#inputLevel === next) return;
|
|
71
|
+
this.#inputLevel = next;
|
|
72
|
+
if (next > this.#displayLevel) this.#displayLevel = next;
|
|
73
|
+
this.invalidate();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Advances the spectrum animation and its peak decay. */
|
|
77
|
+
setFrame(frame: number): void {
|
|
78
|
+
const nextLevel = Math.max(this.#inputLevel, this.#displayLevel * 0.84);
|
|
79
|
+
if (this.#frame !== frame || this.#displayLevel !== nextLevel) {
|
|
80
|
+
this.#frame = frame;
|
|
81
|
+
this.#displayLevel = nextLevel;
|
|
82
|
+
this.invalidate();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Updates the user's streaming voice transcript. */
|
|
87
|
+
setTranscript(text: string): void {
|
|
88
|
+
const normalized = normalizeTranscript(text);
|
|
89
|
+
if (this.#userTranscript === normalized) return;
|
|
90
|
+
this.#userTranscript = normalized;
|
|
91
|
+
this.invalidate();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Clears the user's voice transcript row. */
|
|
95
|
+
clearTranscript(): void {
|
|
96
|
+
if (!this.#userTranscript) return;
|
|
97
|
+
this.#userTranscript = "";
|
|
98
|
+
this.invalidate();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Processes user keypresses. */
|
|
102
|
+
handleInput(data: string): void {
|
|
103
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
104
|
+
this.#options.onStop();
|
|
105
|
+
} else if (matchesKey(data, "space")) {
|
|
106
|
+
this.#options.onToggleMute();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Clears the render cache. */
|
|
111
|
+
invalidate(): void {
|
|
112
|
+
this.#cache = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Renders the microphone spectrum into a compact fixed-height panel. */
|
|
116
|
+
render(width: number): readonly string[] {
|
|
117
|
+
if (
|
|
118
|
+
this.#cache &&
|
|
119
|
+
this.#cache.width === width &&
|
|
120
|
+
this.#cache.phase === this.#phase &&
|
|
121
|
+
this.#cache.displayLevel === this.#displayLevel &&
|
|
122
|
+
this.#cache.frame === this.#frame &&
|
|
123
|
+
this.#cache.userTranscript === this.#userTranscript
|
|
124
|
+
) {
|
|
125
|
+
return this.#cache.lines;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lines = this.#renderLines(width);
|
|
129
|
+
this.#cache = {
|
|
130
|
+
width,
|
|
131
|
+
phase: this.#phase,
|
|
132
|
+
displayLevel: this.#displayLevel,
|
|
133
|
+
frame: this.#frame,
|
|
134
|
+
userTranscript: this.#userTranscript,
|
|
135
|
+
lines,
|
|
136
|
+
};
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#renderLines(maxWidth: number): readonly string[] {
|
|
141
|
+
const width = Math.max(2, maxWidth);
|
|
142
|
+
const innerWidth = width - 2;
|
|
143
|
+
const border = (content: string): string =>
|
|
144
|
+
theme.fg("border", "│") + content + (width > 1 ? theme.fg("border", "│") : "");
|
|
145
|
+
const top = theme.fg("border", `┌${"─".repeat(innerWidth)}${width > 1 ? "┐" : ""}`);
|
|
146
|
+
const spectrumColor: ThemeColor = this.#phase === "muted" ? "dim" : this.#phase === "error" ? "error" : "success";
|
|
147
|
+
const spectrum = this.#generateSpectrum(innerWidth, 2);
|
|
148
|
+
const spectrumRows = spectrum.map(row => border(theme.fg(spectrumColor, row)));
|
|
149
|
+
const transcript = this.#renderTranscript(this.#userTranscript, innerWidth, border);
|
|
150
|
+
return [top, ...spectrumRows, transcript, this.#renderFooter(width, innerWidth)];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#renderTranscript(transcript: string, innerWidth: number, border: (content: string) => string): string {
|
|
154
|
+
const content = truncateFromStart(transcript, innerWidth);
|
|
155
|
+
const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
|
|
156
|
+
return border(theme.fg("accent", content) + padding);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#renderFooter(width: number, innerWidth: number): string {
|
|
160
|
+
const spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
161
|
+
const staticIcons: Record<LivePhase, string> = {
|
|
162
|
+
connecting: "○",
|
|
163
|
+
listening: "●",
|
|
164
|
+
working: "○",
|
|
165
|
+
speaking: "»",
|
|
166
|
+
muted: "×",
|
|
167
|
+
error: "!",
|
|
168
|
+
};
|
|
169
|
+
const icon = this.#phase === "working" ? spinners[this.#frame % spinners.length] : staticIcons[this.#phase];
|
|
170
|
+
const phaseColors: Record<LivePhase, ThemeColor> = {
|
|
171
|
+
connecting: "dim",
|
|
172
|
+
listening: "success",
|
|
173
|
+
working: "warning",
|
|
174
|
+
speaking: "accent",
|
|
175
|
+
muted: "dim",
|
|
176
|
+
error: "error",
|
|
177
|
+
};
|
|
178
|
+
const status = `${icon} ${this.#phase}`;
|
|
179
|
+
const fullLabel = ` ${status} · space mute · esc end `;
|
|
180
|
+
const shortLabel = ` ${status} `;
|
|
181
|
+
const label =
|
|
182
|
+
innerWidth >= visibleWidth(fullLabel) + 1
|
|
183
|
+
? fullLabel
|
|
184
|
+
: innerWidth >= visibleWidth(shortLabel) + 1
|
|
185
|
+
? shortLabel
|
|
186
|
+
: "";
|
|
187
|
+
if (!label) {
|
|
188
|
+
return theme.fg("border", `└${"─".repeat(innerWidth)}${width > 1 ? "┘" : ""}`);
|
|
189
|
+
}
|
|
190
|
+
const remaining = Math.max(0, innerWidth - visibleWidth(label) - 1);
|
|
191
|
+
return (
|
|
192
|
+
theme.fg("border", "└─") +
|
|
193
|
+
theme.fg(phaseColors[this.#phase], truncateToWidth(label, innerWidth - 1)) +
|
|
194
|
+
theme.fg("border", `${"─".repeat(remaining)}${width > 1 ? "┘" : ""}`)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#generateSpectrum(width: number, rows: number): string[] {
|
|
199
|
+
const blocks = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
200
|
+
const output = Array.from({ length: rows }, () => "");
|
|
201
|
+
const energy = this.#phase === "muted" ? 0 : Math.min(1, Math.sqrt(this.#displayLevel * 5));
|
|
202
|
+
const maxHeight = rows * (blocks.length - 1);
|
|
203
|
+
for (let column = 0; column < width; column += 1) {
|
|
204
|
+
const carrier = 0.5 + 0.5 * Math.sin(this.#frame * 0.43 + column * 0.71);
|
|
205
|
+
const shimmer = 0.5 + 0.5 * Math.sin(this.#frame * 0.19 - column * 1.17);
|
|
206
|
+
const height = Math.round(energy * (0.3 + carrier * 0.5 + shimmer * 0.2) * maxHeight);
|
|
207
|
+
for (let row = 0; row < rows; row += 1) {
|
|
208
|
+
const units = Math.max(0, Math.min(blocks.length - 1, height - (rows - row - 1) * 8));
|
|
209
|
+
output[row] += blocks[units];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/lsp/client.ts
CHANGED
|
@@ -364,7 +364,15 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
364
364
|
if (pending) {
|
|
365
365
|
client.pendingRequests.delete(message.id);
|
|
366
366
|
if ("error" in message && message.error) {
|
|
367
|
-
|
|
367
|
+
// Include the JSON-RPC error code: `isMethodNotFoundError` matches
|
|
368
|
+
// `-32601` by substring, so method-not-found is recognized even when
|
|
369
|
+
// the server's message text is nonstandard (e.g. "Unknown request").
|
|
370
|
+
const code = message.error.code;
|
|
371
|
+
pending.reject(
|
|
372
|
+
new Error(
|
|
373
|
+
`LSP error${typeof code === "number" ? ` ${code}` : ""}: ${message.error.message}`,
|
|
374
|
+
),
|
|
375
|
+
);
|
|
368
376
|
} else {
|
|
369
377
|
pending.resolve(message.result);
|
|
370
378
|
}
|
|
@@ -1181,9 +1189,19 @@ async function waitForExit(client: LspClient, timeoutMs: number): Promise<boolea
|
|
|
1181
1189
|
}
|
|
1182
1190
|
|
|
1183
1191
|
/**
|
|
1184
|
-
*
|
|
1192
|
+
* Tear down a specific client instance using the LSP shutdown/exit handshake.
|
|
1193
|
+
*
|
|
1194
|
+
* Removes the client from the registry by identity first (never evicting a
|
|
1195
|
+
* newer client already republished under the same key), then performs a bounded
|
|
1196
|
+
* graceful shutdown, force-killing and awaiting confirmed process exit.
|
|
1197
|
+
*
|
|
1198
|
+
* @returns `true` once the process is confirmed exited, `false` if it outlived
|
|
1199
|
+
* the shutdown budget — callers reporting a restart must treat `false` as a
|
|
1200
|
+
* failed teardown, not a completed restart.
|
|
1185
1201
|
*/
|
|
1186
|
-
async function shutdownClientInstance(client: LspClient): Promise<
|
|
1202
|
+
export async function shutdownClientInstance(client: LspClient): Promise<boolean> {
|
|
1203
|
+
if (clients.get(client.name) === client) clients.delete(client.name);
|
|
1204
|
+
|
|
1187
1205
|
const err = new Error("LSP client shutdown");
|
|
1188
1206
|
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
1189
1207
|
pending.reject(err);
|
|
@@ -1196,21 +1214,23 @@ async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
|
1196
1214
|
);
|
|
1197
1215
|
if (shutdownCompleted) {
|
|
1198
1216
|
await sendNotification(client, "exit", undefined).catch(() => {});
|
|
1199
|
-
if (await waitForExit(client, EXIT_TIMEOUT_MS)) return;
|
|
1217
|
+
if (await waitForExit(client, EXIT_TIMEOUT_MS)) return true;
|
|
1200
1218
|
}
|
|
1201
1219
|
|
|
1202
1220
|
client.proc.kill();
|
|
1203
|
-
await waitForExit(client, EXIT_TIMEOUT_MS);
|
|
1221
|
+
return await waitForExit(client, EXIT_TIMEOUT_MS);
|
|
1204
1222
|
}
|
|
1205
1223
|
|
|
1206
1224
|
/**
|
|
1207
1225
|
* Shutdown a specific client by key.
|
|
1226
|
+
*
|
|
1227
|
+
* @returns `true` when the client is gone (already absent or confirmed exited),
|
|
1228
|
+
* `false` if a live process outlived the shutdown budget.
|
|
1208
1229
|
*/
|
|
1209
|
-
export async function shutdownClient(key: string): Promise<
|
|
1230
|
+
export async function shutdownClient(key: string): Promise<boolean> {
|
|
1210
1231
|
const client = clients.get(key);
|
|
1211
|
-
if (!client) return;
|
|
1212
|
-
|
|
1213
|
-
await shutdownClientInstance(client);
|
|
1232
|
+
if (!client) return true;
|
|
1233
|
+
return await shutdownClientInstance(client);
|
|
1214
1234
|
}
|
|
1215
1235
|
|
|
1216
1236
|
// =============================================================================
|
package/src/lsp/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
sendNotification,
|
|
30
30
|
sendRequest,
|
|
31
31
|
setIdleTimeout,
|
|
32
|
+
shutdownClientInstance,
|
|
32
33
|
supportsDocumentDiagnostics,
|
|
33
34
|
syncContent,
|
|
34
35
|
WARMUP_TIMEOUT_MS,
|
|
@@ -507,12 +508,18 @@ function isMethodNotFoundError(err: unknown): boolean {
|
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
|
|
510
|
-
|
|
511
|
+
throwIfAborted(signal);
|
|
512
|
+
// rust-analyzer exposes a real reload request. Every other server rejects it
|
|
513
|
+
// with method-not-found — that alone justifies the generic fallback. A caller
|
|
514
|
+
// cancel or tool timeout must propagate, never be mistaken for an unsupported
|
|
515
|
+
// method and swallowed into a bogus "Restarted" (issue #6369).
|
|
511
516
|
try {
|
|
512
517
|
await sendRequest(client, "rust-analyzer/reloadWorkspace", null, signal);
|
|
513
518
|
return `Reloaded ${serverName}`;
|
|
514
|
-
} catch {
|
|
515
|
-
|
|
519
|
+
} catch (err) {
|
|
520
|
+
throwIfAborted(signal);
|
|
521
|
+
if (!isMethodNotFoundError(err)) throw err;
|
|
522
|
+
// Method not supported — fall through to the generic reload.
|
|
516
523
|
}
|
|
517
524
|
// workspace/didChangeConfiguration is a notification per spec; sending it
|
|
518
525
|
// as a request hangs until the tool deadline on servers that route it to
|
|
@@ -521,7 +528,15 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
|
|
|
521
528
|
await sendNotification(client, "workspace/didChangeConfiguration", { settings: {} }, signal);
|
|
522
529
|
return `Reloaded ${serverName}`;
|
|
523
530
|
} catch {
|
|
524
|
-
|
|
531
|
+
throwIfAborted(signal);
|
|
532
|
+
// The reload notification could not be delivered — the connection is
|
|
533
|
+
// wedged or the process already died. Tear the client down (removing it
|
|
534
|
+
// from the registry by identity and awaiting confirmed process exit) so
|
|
535
|
+
// the next request cold-starts a fresh client. A kill that never confirms
|
|
536
|
+
// exit is not a restart: surface the teardown failure truthfully.
|
|
537
|
+
if (!(await shutdownClientInstance(client))) {
|
|
538
|
+
throw new Error(`Failed to restart ${serverName}: server process did not exit after kill`);
|
|
539
|
+
}
|
|
525
540
|
return `Restarted ${serverName}`;
|
|
526
541
|
}
|
|
527
542
|
}
|
package/src/main.ts
CHANGED
|
@@ -391,7 +391,9 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
|
|
|
391
391
|
authStorage: args.authStorage,
|
|
392
392
|
modelRegistry: args.modelRegistry,
|
|
393
393
|
agentId,
|
|
394
|
-
|
|
394
|
+
// Preserve reserve-policy confirmation until ACP capabilities are known
|
|
395
|
+
// without enabling AskTool or other UI-only session behavior.
|
|
396
|
+
deferUsageReserveConfirmation: true,
|
|
395
397
|
enableMCP: false,
|
|
396
398
|
titleSystemPrompt,
|
|
397
399
|
});
|
|
@@ -834,6 +836,11 @@ export async function buildSessionOptions(
|
|
|
834
836
|
cwd: parsed.cwd ?? getProjectDir(),
|
|
835
837
|
autoApprove: parsed.autoApprove ?? false,
|
|
836
838
|
};
|
|
839
|
+
const cliDirs = parsed.addDir ?? [];
|
|
840
|
+
const settingsDirs = activeSettings.get("workspace.additionalDirectories");
|
|
841
|
+
if (cliDirs.length > 0 || settingsDirs.length > 0) {
|
|
842
|
+
options.additionalDirectories = [...new Set([...cliDirs, ...settingsDirs])];
|
|
843
|
+
}
|
|
837
844
|
if (parsed.maxTime !== undefined) {
|
|
838
845
|
options.deadline = Date.now() + parsed.maxTime * 1000;
|
|
839
846
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { addMCPServer, readDisabledServers, readMCPConfigFile, setServerDisabled } from "./config-writer";
|
|
6
|
+
|
|
7
|
+
describe("config-writer concurrent mutations", () => {
|
|
8
|
+
let dir: string;
|
|
9
|
+
let filePath: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-config-"));
|
|
13
|
+
filePath = path.join(dir, "mcp.json");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("preserves both servers when two adds race the same file", async () => {
|
|
21
|
+
await Promise.all([
|
|
22
|
+
addMCPServer(filePath, "alpha", { type: "stdio", command: "a" }),
|
|
23
|
+
addMCPServer(filePath, "bravo", { type: "stdio", command: "b" }),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const config = await readMCPConfigFile(filePath);
|
|
27
|
+
expect(Object.keys(config.mcpServers ?? {}).sort()).toEqual(["alpha", "bravo"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("preserves both denylist edits when disable calls race", async () => {
|
|
31
|
+
await Promise.all([setServerDisabled(filePath, "alpha", true), setServerDisabled(filePath, "bravo", true)]);
|
|
32
|
+
|
|
33
|
+
expect((await readDisabledServers(filePath)).sort()).toEqual(["alpha", "bravo"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("writes into a directory that does not exist yet", async () => {
|
|
37
|
+
const nestedPath = path.join(dir, "nested", "deep", "mcp.json");
|
|
38
|
+
await addMCPServer(nestedPath, "alpha", { type: "stdio", command: "a" });
|
|
39
|
+
|
|
40
|
+
const config = await readMCPConfigFile(nestedPath);
|
|
41
|
+
expect(Object.keys(config.mcpServers ?? {})).toEqual(["alpha"]);
|
|
42
|
+
});
|
|
43
|
+
});
|
package/src/mcp/config-writer.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for reading/writing .omp/mcp.json files at user or project level.
|
|
5
5
|
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import * as fs from "node:fs";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import { invalidate as invalidateFsCache } from "../capability/fs";
|
|
11
|
+
import { withFileLock } from "../config/file-lock";
|
|
10
12
|
|
|
11
13
|
import { validateServerConfig } from "./config";
|
|
12
14
|
import { MCP_CONFIG_SCHEMA_URL, type MCPConfigFile, type MCPServerConfig } from "./types";
|
|
@@ -18,6 +20,20 @@ function withSchema(config: MCPConfigFile): MCPConfigFile {
|
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Serialize a read-modify-write against one config file.
|
|
25
|
+
*
|
|
26
|
+
* Wraps {@link withFileLock} but first ensures the config's parent directory
|
|
27
|
+
* exists, because the lock directory (`${filePath}.lock`) is created with a
|
|
28
|
+
* non-recursive `mkdir` — without this the very first write (before the config
|
|
29
|
+
* file or its parent exists) would fail to acquire the lock with ENOENT.
|
|
30
|
+
*/
|
|
31
|
+
function withConfigLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
32
|
+
return fs.promises
|
|
33
|
+
.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 })
|
|
34
|
+
.then(() => withFileLock(filePath, fn));
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
/**
|
|
22
38
|
* Read an MCP config file.
|
|
23
39
|
* Returns empty config if file doesn't exist.
|
|
@@ -45,13 +61,20 @@ export async function writeMCPConfigFile(filePath: string, config: MCPConfigFile
|
|
|
45
61
|
const dir = path.dirname(filePath);
|
|
46
62
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
47
63
|
|
|
48
|
-
// Write to temp file
|
|
49
|
-
|
|
64
|
+
// Write to a per-writer temp file, then atomically rename into place. The
|
|
65
|
+
// temp name is unique (pid + random) so two concurrent writers to the same
|
|
66
|
+
// config never share one `.tmp` path and rename each other's file out from
|
|
67
|
+
// under them (which surfaced as ENOENT or a clobbered final file).
|
|
68
|
+
const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
50
69
|
const content = JSON.stringify(withSchema(config), null, 2);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
try {
|
|
71
|
+
await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
72
|
+
// Rename to final path (atomic on most systems)
|
|
73
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await fs.promises.rm(tmpPath, { force: true }).catch(() => {});
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
55
78
|
// Invalidate the capability fs cache so subsequent reads see the new content
|
|
56
79
|
invalidateFsCache(filePath);
|
|
57
80
|
}
|
|
@@ -97,25 +120,26 @@ export async function addMCPServer(filePath: string, name: string, config: MCPSe
|
|
|
97
120
|
throw new Error(`Invalid server config: ${errors.join("; ")}`);
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
//
|
|
101
|
-
|
|
123
|
+
// Serialize the read-modify-write under a per-file lock so a concurrent
|
|
124
|
+
// mutation cannot overwrite this one (lost update). The lock also guards
|
|
125
|
+
// against cross-process writers sharing the same config file.
|
|
126
|
+
await withConfigLock(filePath, async () => {
|
|
127
|
+
const existing = await readMCPConfigFile(filePath);
|
|
102
128
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Add server
|
|
109
|
-
const updated: MCPConfigFile = {
|
|
110
|
-
...existing,
|
|
111
|
-
mcpServers: {
|
|
112
|
-
...existing.mcpServers,
|
|
113
|
-
[name]: config,
|
|
114
|
-
},
|
|
115
|
-
};
|
|
129
|
+
// Check for duplicate name
|
|
130
|
+
if (existing.mcpServers?.[name]) {
|
|
131
|
+
throw new Error(`Server "${name}" already exists in ${filePath}`);
|
|
132
|
+
}
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
const updated: MCPConfigFile = {
|
|
135
|
+
...existing,
|
|
136
|
+
mcpServers: {
|
|
137
|
+
...existing.mcpServers,
|
|
138
|
+
[name]: config,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
await writeMCPConfigFile(filePath, updated);
|
|
142
|
+
});
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
/**
|
|
@@ -137,20 +161,19 @@ export async function updateMCPServer(filePath: string, name: string, config: MC
|
|
|
137
161
|
throw new Error(`Invalid server config: ${errors.join("; ")}`);
|
|
138
162
|
}
|
|
139
163
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await writeMCPConfigFile(filePath, updated);
|
|
164
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
165
|
+
await withConfigLock(filePath, async () => {
|
|
166
|
+
const existing = await readMCPConfigFile(filePath);
|
|
167
|
+
|
|
168
|
+
const updated: MCPConfigFile = {
|
|
169
|
+
...existing,
|
|
170
|
+
mcpServers: {
|
|
171
|
+
...existing.mcpServers,
|
|
172
|
+
[name]: config,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
await writeMCPConfigFile(filePath, updated);
|
|
176
|
+
});
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/**
|
|
@@ -159,23 +182,21 @@ export async function updateMCPServer(filePath: string, name: string, config: MC
|
|
|
159
182
|
* @throws Error if server doesn't exist
|
|
160
183
|
*/
|
|
161
184
|
export async function removeMCPServer(filePath: string, name: string): Promise<void> {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Check if server exists
|
|
166
|
-
if (!existing.mcpServers?.[name]) {
|
|
167
|
-
throw new Error(`Server "${name}" not found in ${filePath}`);
|
|
168
|
-
}
|
|
185
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
186
|
+
await withConfigLock(filePath, async () => {
|
|
187
|
+
const existing = await readMCPConfigFile(filePath);
|
|
169
188
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
...existing,
|
|
174
|
-
mcpServers: remaining,
|
|
175
|
-
};
|
|
189
|
+
if (!existing.mcpServers?.[name]) {
|
|
190
|
+
throw new Error(`Server "${name}" not found in ${filePath}`);
|
|
191
|
+
}
|
|
176
192
|
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
const { [name]: _removed, ...remaining } = existing.mcpServers;
|
|
194
|
+
const updated: MCPConfigFile = {
|
|
195
|
+
...existing,
|
|
196
|
+
mcpServers: remaining,
|
|
197
|
+
};
|
|
198
|
+
await writeMCPConfigFile(filePath, updated);
|
|
199
|
+
});
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
/**
|
|
@@ -207,25 +228,28 @@ export async function readDisabledServers(filePath: string): Promise<string[]> {
|
|
|
207
228
|
* Add or remove a server name from the disabled servers list.
|
|
208
229
|
*/
|
|
209
230
|
export async function setServerDisabled(filePath: string, name: string, disabled: boolean): Promise<void> {
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
232
|
+
await withConfigLock(filePath, async () => {
|
|
233
|
+
const config = await readMCPConfigFile(filePath);
|
|
234
|
+
const current = new Set(config.disabledServers ?? []);
|
|
212
235
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
if (disabled) {
|
|
237
|
+
current.add(name);
|
|
238
|
+
} else {
|
|
239
|
+
current.delete(name);
|
|
240
|
+
}
|
|
218
241
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
const updated: MCPConfigFile = {
|
|
243
|
+
...config,
|
|
244
|
+
disabledServers: current.size > 0 ? Array.from(current).sort() : undefined,
|
|
245
|
+
};
|
|
223
246
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
if (!updated.disabledServers) {
|
|
248
|
+
delete updated.disabledServers;
|
|
249
|
+
}
|
|
227
250
|
|
|
228
|
-
|
|
251
|
+
await writeMCPConfigFile(filePath, updated);
|
|
252
|
+
});
|
|
229
253
|
}
|
|
230
254
|
|
|
231
255
|
/**
|
|
@@ -243,25 +267,28 @@ export async function readEnabledServers(filePath: string): Promise<string[]> {
|
|
|
243
267
|
* NOT override the `disabledServers` denylist.
|
|
244
268
|
*/
|
|
245
269
|
export async function setServerForceEnabled(filePath: string, name: string, force: boolean): Promise<void> {
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
// Serialize the read-modify-write (see addMCPServer).
|
|
271
|
+
await withConfigLock(filePath, async () => {
|
|
272
|
+
const config = await readMCPConfigFile(filePath);
|
|
273
|
+
const current = new Set(config.enabledServers ?? []);
|
|
248
274
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
275
|
+
if (force) {
|
|
276
|
+
current.add(name);
|
|
277
|
+
} else {
|
|
278
|
+
current.delete(name);
|
|
279
|
+
}
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
const updated: MCPConfigFile = {
|
|
282
|
+
...config,
|
|
283
|
+
enabledServers: current.size > 0 ? Array.from(current).sort() : undefined,
|
|
284
|
+
};
|
|
259
285
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
286
|
+
if (!updated.enabledServers) {
|
|
287
|
+
delete updated.enabledServers;
|
|
288
|
+
}
|
|
263
289
|
|
|
264
|
-
|
|
290
|
+
await writeMCPConfigFile(filePath, updated);
|
|
291
|
+
});
|
|
265
292
|
}
|
|
266
293
|
|
|
267
294
|
/** Paths and target state for toggling one MCP server across known config files. */
|