@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.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 +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -240,45 +240,12 @@ export class InputController {
|
|
|
240
240
|
text = slashResult;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
// Handle skill commands (/skill:name [args])
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (skillPath) {
|
|
250
|
-
this.ctx.editor.addToHistory(text);
|
|
251
|
-
this.ctx.editor.setText("");
|
|
252
|
-
try {
|
|
253
|
-
const content = await Bun.file(skillPath).text();
|
|
254
|
-
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
255
|
-
const metaLines = [`Skill: ${skillPath}`];
|
|
256
|
-
if (args) {
|
|
257
|
-
metaLines.push(`User: ${args}`);
|
|
258
|
-
}
|
|
259
|
-
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
260
|
-
const skillName = commandName.slice("skill:".length);
|
|
261
|
-
const details: SkillPromptDetails = {
|
|
262
|
-
name: skillName || commandName,
|
|
263
|
-
path: skillPath,
|
|
264
|
-
args: args || undefined,
|
|
265
|
-
lineCount: body ? body.split("\n").length : 0,
|
|
266
|
-
};
|
|
267
|
-
await this.ctx.session.promptCustomMessage(
|
|
268
|
-
{
|
|
269
|
-
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
270
|
-
content: message,
|
|
271
|
-
display: true,
|
|
272
|
-
details,
|
|
273
|
-
attribution: "user",
|
|
274
|
-
},
|
|
275
|
-
{ streamingBehavior: "followUp" },
|
|
276
|
-
);
|
|
277
|
-
} catch (err) {
|
|
278
|
-
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
-
}
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
243
|
+
// Handle skill commands (/skill:name [args]). Enter ⇒ steer (matches the
|
|
244
|
+
// free-text Enter semantics applied a few lines below at the streaming
|
|
245
|
+
// branch). Ctrl+Enter routes through `handleFollowUp` and dispatches the
|
|
246
|
+
// same helper with `"followUp"`.
|
|
247
|
+
if (await this.#invokeSkillCommand(text, "steer")) {
|
|
248
|
+
return;
|
|
282
249
|
}
|
|
283
250
|
|
|
284
251
|
// Handle bash command (! for normal, !! for excluded from context)
|
|
@@ -439,16 +406,95 @@ export class InputController {
|
|
|
439
406
|
}
|
|
440
407
|
}
|
|
441
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Dispatch a `/skill:<name> [args]` invocation through `promptCustomMessage`
|
|
411
|
+
* using the supplied `streamingBehavior`. Returns true if the text was a
|
|
412
|
+
* recognised skill command and was dispatched. A failure to load the skill
|
|
413
|
+
* file is surfaced via `showError` but still returns true — the editor was
|
|
414
|
+
* already cleared on the success path, so falling through to plain-text
|
|
415
|
+
* handling at that point would double-submit. Returns false when the text
|
|
416
|
+
* isn't a `/skill:` prefix or the command name isn't a registered skill,
|
|
417
|
+
* so the caller can fall through to plain-text handling (this branch
|
|
418
|
+
* leaves the editor state untouched). `streamingBehavior` is only consulted
|
|
419
|
+
* while the agent is streaming; the idle path of `promptCustomMessage`
|
|
420
|
+
* ignores it.
|
|
421
|
+
*/
|
|
422
|
+
async #invokeSkillCommand(text: string, streamingBehavior: "steer" | "followUp"): Promise<boolean> {
|
|
423
|
+
if (!text.startsWith("/skill:")) return false;
|
|
424
|
+
const spaceIndex = text.indexOf(" ");
|
|
425
|
+
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
426
|
+
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
427
|
+
const skillPath = this.ctx.skillCommands?.get(commandName);
|
|
428
|
+
if (!skillPath) return false;
|
|
429
|
+
this.ctx.editor.addToHistory(text);
|
|
430
|
+
this.ctx.editor.setText("");
|
|
431
|
+
try {
|
|
432
|
+
const content = await Bun.file(skillPath).text();
|
|
433
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
434
|
+
const metaLines = [`Skill: ${skillPath}`];
|
|
435
|
+
if (args) {
|
|
436
|
+
metaLines.push(`User: ${args}`);
|
|
437
|
+
}
|
|
438
|
+
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
439
|
+
const skillName = commandName.slice("skill:".length);
|
|
440
|
+
const details: SkillPromptDetails = {
|
|
441
|
+
name: skillName || commandName,
|
|
442
|
+
path: skillPath,
|
|
443
|
+
args: args || undefined,
|
|
444
|
+
lineCount: body ? body.split("\n").length : 0,
|
|
445
|
+
};
|
|
446
|
+
// When the agent is streaming, register the compact slash-form text as
|
|
447
|
+
// the pending-display twin BEFORE dispatching the CustomMessage. The
|
|
448
|
+
// returned tag is embedded in details so AgentSession.#handleAgentEvent
|
|
449
|
+
// can remove the matching display entry when the agent consumes this
|
|
450
|
+
// message (mirrors the user-message dequeue path).
|
|
451
|
+
if (this.ctx.session.isStreaming) {
|
|
452
|
+
const tag = this.ctx.session.enqueueCustomMessageDisplay(text, streamingBehavior);
|
|
453
|
+
details.__pendingDisplayTag = tag;
|
|
454
|
+
}
|
|
455
|
+
await this.ctx.session.promptCustomMessage(
|
|
456
|
+
{
|
|
457
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
458
|
+
content: message,
|
|
459
|
+
display: true,
|
|
460
|
+
details,
|
|
461
|
+
attribution: "user",
|
|
462
|
+
},
|
|
463
|
+
{ streamingBehavior },
|
|
464
|
+
);
|
|
465
|
+
if (this.ctx.session.isStreaming) {
|
|
466
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
467
|
+
this.ctx.ui.requestRender();
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
|
|
442
475
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
443
476
|
async handleFollowUp(): Promise<void> {
|
|
444
477
|
const text = this.ctx.editor.getText().trim();
|
|
445
478
|
if (!text) return;
|
|
446
479
|
|
|
480
|
+
// Compaction first: while compacting, free text gets queued via
|
|
481
|
+
// `queueCompactionMessage`, and `/skill:*` rides the same queue so a
|
|
482
|
+
// skill typed during compaction is not lost or short-circuited through
|
|
483
|
+
// `promptCustomMessage`. The skill text is queued verbatim; whether
|
|
484
|
+
// the queued entry is later re-parsed into a skill invocation is a
|
|
485
|
+
// separate concern owned by the compaction-resume path.
|
|
447
486
|
if (this.ctx.session.isCompacting) {
|
|
448
487
|
this.ctx.queueCompactionMessage(text, "followUp");
|
|
449
488
|
return;
|
|
450
489
|
}
|
|
451
490
|
|
|
491
|
+
// Skill commands invoke through the custom-message path regardless of
|
|
492
|
+
// which keybinding submitted them. Enter routes them as `steer`;
|
|
493
|
+
// Ctrl+Enter (this handler) routes them as `followUp`.
|
|
494
|
+
if (await this.#invokeSkillCommand(text, "followUp")) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
452
498
|
if (this.ctx.session.isStreaming) {
|
|
453
499
|
this.ctx.editor.addToHistory(text);
|
|
454
500
|
this.ctx.editor.setText("");
|
|
@@ -37,11 +37,11 @@ import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../
|
|
|
37
37
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
38
38
|
import { shortenPath } from "../../tools/render-utils";
|
|
39
39
|
import { openPath } from "../../utils/open";
|
|
40
|
-
import { DynamicBorder } from "../components/dynamic-border";
|
|
41
40
|
import { MCPAddWizard } from "../components/mcp-add-wizard";
|
|
42
41
|
import { parseCommandArgs } from "../shared";
|
|
43
42
|
import { theme } from "../theme/theme";
|
|
44
43
|
import type { InteractiveModeContext } from "../types";
|
|
44
|
+
import { groupBySource, parseRemoveArgs, readScopeFlag, showCommandMessage } from "./command-controller-shared";
|
|
45
45
|
|
|
46
46
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
47
47
|
const { promise: timeoutPromise, reject } = Promise.withResolvers<T>();
|
|
@@ -49,6 +49,22 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string)
|
|
|
49
49
|
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Outcome of {@link MCPCommandController}'s OAuth handler.
|
|
54
|
+
*
|
|
55
|
+
* `clientId`/`clientSecret` are populated when the OAuth provider required (or
|
|
56
|
+
* accepted) dynamic client registration; callers MUST persist them alongside
|
|
57
|
+
* `credentialId` so subsequent token refreshes and reauthorizations can reuse
|
|
58
|
+
* the same registered client. Both are also set when the caller pre-supplied a
|
|
59
|
+
* client id via the wizard or `oauth.clientId` in `mcp.json`, in which case the
|
|
60
|
+
* write-back is a no-op.
|
|
61
|
+
*/
|
|
62
|
+
interface OAuthFlowResult {
|
|
63
|
+
credentialId: string;
|
|
64
|
+
clientId?: string;
|
|
65
|
+
clientSecret?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
type MCPAddScope = "user" | "project";
|
|
53
69
|
type MCPAddTransport = "http" | "sse";
|
|
54
70
|
|
|
@@ -207,11 +223,11 @@ export class MCPCommandController {
|
|
|
207
223
|
break;
|
|
208
224
|
}
|
|
209
225
|
if (argToken === "--scope") {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
return { scope, error:
|
|
226
|
+
const r = readScopeFlag(tokens[i + 1]);
|
|
227
|
+
if (!r.ok) {
|
|
228
|
+
return { scope, error: r.error };
|
|
213
229
|
}
|
|
214
|
-
scope =
|
|
230
|
+
scope = r.scope;
|
|
215
231
|
i += 2;
|
|
216
232
|
continue;
|
|
217
233
|
}
|
|
@@ -406,7 +422,7 @@ export class MCPCommandController {
|
|
|
406
422
|
|
|
407
423
|
try {
|
|
408
424
|
const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
|
|
409
|
-
const
|
|
425
|
+
const oauthResult = await this.#handleOAuthFlow(
|
|
410
426
|
oauth.authorizationUrl,
|
|
411
427
|
oauth.tokenUrl,
|
|
412
428
|
oauth.clientId ?? finalConfig.oauth?.clientId ?? "",
|
|
@@ -416,14 +432,21 @@ export class MCPCommandController {
|
|
|
416
432
|
finalConfig.oauth?.callbackPath,
|
|
417
433
|
finalConfig.oauth?.redirectUri,
|
|
418
434
|
);
|
|
435
|
+
const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? finalConfig.oauth?.clientId;
|
|
436
|
+
const persistedClientSecret = oauthResult.clientSecret ?? finalConfig.oauth?.clientSecret;
|
|
419
437
|
finalConfig = {
|
|
420
438
|
...finalConfig,
|
|
421
439
|
auth: {
|
|
422
440
|
type: "oauth",
|
|
423
|
-
credentialId,
|
|
441
|
+
credentialId: oauthResult.credentialId,
|
|
424
442
|
tokenUrl: oauth.tokenUrl,
|
|
425
|
-
clientId:
|
|
426
|
-
clientSecret:
|
|
443
|
+
clientId: persistedClientId,
|
|
444
|
+
clientSecret: persistedClientSecret,
|
|
445
|
+
},
|
|
446
|
+
oauth: {
|
|
447
|
+
...finalConfig.oauth,
|
|
448
|
+
clientId: persistedClientId ?? finalConfig.oauth?.clientId,
|
|
449
|
+
clientSecret: persistedClientSecret ?? finalConfig.oauth?.clientSecret,
|
|
427
450
|
},
|
|
428
451
|
};
|
|
429
452
|
} catch (oauthError) {
|
|
@@ -488,7 +511,7 @@ export class MCPCommandController {
|
|
|
488
511
|
callbackPort?: number,
|
|
489
512
|
callbackPath?: string,
|
|
490
513
|
redirectUri?: string,
|
|
491
|
-
): Promise<
|
|
514
|
+
): Promise<OAuthFlowResult> {
|
|
492
515
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
493
516
|
let parsedAuthUrl: URL;
|
|
494
517
|
|
|
@@ -600,7 +623,11 @@ export class MCPCommandController {
|
|
|
600
623
|
// Store under a synthetic provider name
|
|
601
624
|
await authStorage.set(credentialId, oauthCredential);
|
|
602
625
|
|
|
603
|
-
return
|
|
626
|
+
return {
|
|
627
|
+
credentialId,
|
|
628
|
+
clientId: flow.resolvedClientId,
|
|
629
|
+
clientSecret: flow.registeredClientSecret,
|
|
630
|
+
};
|
|
604
631
|
} catch (error) {
|
|
605
632
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
606
633
|
|
|
@@ -984,23 +1011,7 @@ export class MCPCommandController {
|
|
|
984
1011
|
|
|
985
1012
|
// Show discovered servers (from .claude.json, .cursor/mcp.json, .vscode/mcp.json, etc.)
|
|
986
1013
|
if (discoveredServers.length > 0) {
|
|
987
|
-
|
|
988
|
-
const bySource = new Map<string, typeof discoveredServers>();
|
|
989
|
-
for (const entry of discoveredServers) {
|
|
990
|
-
const key = `${entry.source.providerName}|${entry.source.path}`;
|
|
991
|
-
let group = bySource.get(key);
|
|
992
|
-
if (!group) {
|
|
993
|
-
group = [];
|
|
994
|
-
bySource.set(key, group);
|
|
995
|
-
}
|
|
996
|
-
group.push(entry);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
for (const [key, entries] of bySource) {
|
|
1000
|
-
const sepIdx = key.indexOf("|");
|
|
1001
|
-
const providerName = key.slice(0, sepIdx);
|
|
1002
|
-
const sourcePath = key.slice(sepIdx + 1);
|
|
1003
|
-
const shortPath = shortenPath(sourcePath);
|
|
1014
|
+
for (const { providerName, shortPath, items: entries } of groupBySource(discoveredServers, e => e.source)) {
|
|
1004
1015
|
lines.push(theme.fg("accent", providerName) + theme.fg("muted", ` (${shortPath}):`));
|
|
1005
1016
|
for (const { name } of entries) {
|
|
1006
1017
|
const state = this.ctx.mcpManager!.getConnectionStatus(name);
|
|
@@ -1037,32 +1048,12 @@ export class MCPCommandController {
|
|
|
1037
1048
|
async #handleRemove(text: string): Promise<void> {
|
|
1038
1049
|
const match = text.match(/^\/mcp\s+(?:remove|rm)\b\s*(.*)$/i);
|
|
1039
1050
|
const rest = match?.[1]?.trim() ?? "";
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
let scope: "project" | "user" = "project";
|
|
1044
|
-
let i = 0;
|
|
1045
|
-
|
|
1046
|
-
if (tokens.length > 0 && !tokens[0].startsWith("-")) {
|
|
1047
|
-
name = tokens[0];
|
|
1048
|
-
i = 1;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
while (i < tokens.length) {
|
|
1052
|
-
const token = tokens[i];
|
|
1053
|
-
if (token === "--scope") {
|
|
1054
|
-
const value = tokens[i + 1];
|
|
1055
|
-
if (!value || (value !== "project" && value !== "user")) {
|
|
1056
|
-
this.ctx.showError("Invalid --scope value. Use project or user.");
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
scope = value;
|
|
1060
|
-
i += 2;
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
this.ctx.showError(`Unknown option: ${token}`);
|
|
1051
|
+
const parsed = parseRemoveArgs(rest);
|
|
1052
|
+
if (!parsed.ok) {
|
|
1053
|
+
this.ctx.showError(parsed.error);
|
|
1064
1054
|
return;
|
|
1065
1055
|
}
|
|
1056
|
+
const { name, scope } = parsed.value;
|
|
1066
1057
|
|
|
1067
1058
|
if (!name) {
|
|
1068
1059
|
this.ctx.showError("Server name required. Usage: /mcp remove <name> [--scope project|user]");
|
|
@@ -1348,7 +1339,7 @@ export class MCPCommandController {
|
|
|
1348
1339
|
|
|
1349
1340
|
this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
|
|
1350
1341
|
|
|
1351
|
-
const
|
|
1342
|
+
const oauthResult = await this.#handleOAuthFlow(
|
|
1352
1343
|
oauth.authorizationUrl,
|
|
1353
1344
|
oauth.tokenUrl,
|
|
1354
1345
|
oauth.clientId ?? found.config.oauth?.clientId ?? "",
|
|
@@ -1359,14 +1350,22 @@ export class MCPCommandController {
|
|
|
1359
1350
|
found.config.oauth?.redirectUri,
|
|
1360
1351
|
);
|
|
1361
1352
|
|
|
1353
|
+
const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? found.config.oauth?.clientId;
|
|
1354
|
+
const persistedClientSecret = oauthResult.clientSecret ?? (oauthClientSecret || undefined);
|
|
1355
|
+
|
|
1362
1356
|
const updated: MCPServerConfig = {
|
|
1363
1357
|
...baseConfig,
|
|
1364
1358
|
auth: {
|
|
1365
1359
|
type: "oauth",
|
|
1366
|
-
credentialId,
|
|
1360
|
+
credentialId: oauthResult.credentialId,
|
|
1367
1361
|
tokenUrl: oauth.tokenUrl,
|
|
1368
|
-
clientId:
|
|
1369
|
-
clientSecret:
|
|
1362
|
+
clientId: persistedClientId,
|
|
1363
|
+
clientSecret: persistedClientSecret,
|
|
1364
|
+
},
|
|
1365
|
+
oauth: {
|
|
1366
|
+
...found.config.oauth,
|
|
1367
|
+
clientId: persistedClientId ?? found.config.oauth?.clientId,
|
|
1368
|
+
clientSecret: persistedClientSecret ?? found.config.oauth?.clientSecret,
|
|
1370
1369
|
},
|
|
1371
1370
|
};
|
|
1372
1371
|
await updateMCPServer(found.filePath, name, updated);
|
|
@@ -1929,10 +1928,6 @@ export class MCPCommandController {
|
|
|
1929
1928
|
* Show a message in the chat
|
|
1930
1929
|
*/
|
|
1931
1930
|
#showMessage(text: string): void {
|
|
1932
|
-
this.ctx
|
|
1933
|
-
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
1934
|
-
this.ctx.chatContainer.addChild(new Text(text, 1, 1));
|
|
1935
|
-
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
1936
|
-
this.ctx.ui.requestRender();
|
|
1931
|
+
showCommandMessage(this.ctx, text);
|
|
1937
1932
|
}
|
|
1938
1933
|
}
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles /ssh subcommands for managing SSH host configurations.
|
|
5
5
|
*/
|
|
6
|
-
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
6
|
import { getProjectDir, getSSHConfigPath } from "@oh-my-pi/pi-utils";
|
|
8
7
|
import { type SSHHost, sshCapability } from "../../capability/ssh";
|
|
9
8
|
import { loadCapability } from "../../discovery";
|
|
10
9
|
import { addSSHHost, readSSHConfigFile, removeSSHHost, type SSHHostConfig } from "../../ssh/config-writer";
|
|
11
|
-
import { shortenPath } from "../../tools/render-utils";
|
|
12
|
-
import { DynamicBorder } from "../components/dynamic-border";
|
|
13
10
|
import { parseCommandArgs } from "../shared";
|
|
14
11
|
import { theme } from "../theme/theme";
|
|
15
12
|
import type { InteractiveModeContext } from "../types";
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
import {
|
|
14
|
+
groupBySource,
|
|
15
|
+
parseRemoveArgs,
|
|
16
|
+
readScopeFlag,
|
|
17
|
+
type ScopeValue,
|
|
18
|
+
showCommandMessage,
|
|
19
|
+
} from "./command-controller-shared";
|
|
18
20
|
|
|
19
21
|
export class SSHCommandController {
|
|
20
22
|
constructor(private ctx: InteractiveModeContext) {}
|
|
@@ -90,7 +92,7 @@ export class SSHCommandController {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
let name: string | undefined;
|
|
93
|
-
let scope:
|
|
95
|
+
let scope: ScopeValue = "project";
|
|
94
96
|
let host: string | undefined;
|
|
95
97
|
let username: string | undefined;
|
|
96
98
|
let port: number | undefined;
|
|
@@ -167,12 +169,12 @@ export class SSHCommandController {
|
|
|
167
169
|
continue;
|
|
168
170
|
}
|
|
169
171
|
if (argToken === "--scope") {
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
this.ctx.showError(
|
|
172
|
+
const r = readScopeFlag(tokens[i + 1]);
|
|
173
|
+
if (!r.ok) {
|
|
174
|
+
this.ctx.showError(r.error);
|
|
173
175
|
return;
|
|
174
176
|
}
|
|
175
|
-
scope =
|
|
177
|
+
scope = r.scope;
|
|
176
178
|
i += 2;
|
|
177
179
|
continue;
|
|
178
180
|
}
|
|
@@ -300,23 +302,7 @@ export class SSHCommandController {
|
|
|
300
302
|
|
|
301
303
|
// Show discovered hosts (from ssh.json, .ssh.json in project root, etc.)
|
|
302
304
|
if (discoveredHosts.length > 0) {
|
|
303
|
-
|
|
304
|
-
const bySource = new Map<string, SSHHost[]>();
|
|
305
|
-
for (const host of discoveredHosts) {
|
|
306
|
-
const key = `${host._source.providerName}|${host._source.path}`;
|
|
307
|
-
let group = bySource.get(key);
|
|
308
|
-
if (!group) {
|
|
309
|
-
group = [];
|
|
310
|
-
bySource.set(key, group);
|
|
311
|
-
}
|
|
312
|
-
group.push(host);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
for (const [key, hosts] of bySource) {
|
|
316
|
-
const sepIdx = key.indexOf("|");
|
|
317
|
-
const providerName = key.slice(0, sepIdx);
|
|
318
|
-
const sourcePath = key.slice(sepIdx + 1);
|
|
319
|
-
const shortPath = shortenPath(sourcePath);
|
|
305
|
+
for (const { providerName, shortPath, items: hosts } of groupBySource(discoveredHosts, h => h._source)) {
|
|
320
306
|
lines.push(
|
|
321
307
|
theme.fg("accent", "Discovered") +
|
|
322
308
|
theme.fg("muted", ` (${providerName}: ${shortPath}):`) +
|
|
@@ -357,33 +343,12 @@ export class SSHCommandController {
|
|
|
357
343
|
async #handleRemove(text: string): Promise<void> {
|
|
358
344
|
const match = text.match(/^\/ssh\s+(?:remove|rm)\b\s*(.*)$/i);
|
|
359
345
|
const rest = match?.[1]?.trim() ?? "";
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
let scope: "project" | "user" = "project";
|
|
364
|
-
let i = 0;
|
|
365
|
-
|
|
366
|
-
if (tokens.length > 0 && !tokens[0].startsWith("-")) {
|
|
367
|
-
name = tokens[0];
|
|
368
|
-
i = 1;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
while (i < tokens.length) {
|
|
372
|
-
const token = tokens[i];
|
|
373
|
-
if (token === "--scope") {
|
|
374
|
-
const value = tokens[i + 1];
|
|
375
|
-
if (!value || (value !== "project" && value !== "user")) {
|
|
376
|
-
this.ctx.showError("Invalid --scope value. Use project or user.");
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
scope = value;
|
|
380
|
-
i += 2;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
this.ctx.showError(`Unknown option: ${token}`);
|
|
346
|
+
const parsed = parseRemoveArgs(rest);
|
|
347
|
+
if (!parsed.ok) {
|
|
348
|
+
this.ctx.showError(parsed.error);
|
|
384
349
|
return;
|
|
385
350
|
}
|
|
386
|
-
|
|
351
|
+
const { name, scope } = parsed.value;
|
|
387
352
|
if (!name) {
|
|
388
353
|
this.ctx.showError("Host name required. Usage: /ssh remove <name> [--scope project|user]");
|
|
389
354
|
return;
|
|
@@ -412,10 +377,6 @@ export class SSHCommandController {
|
|
|
412
377
|
* Show a message in the chat
|
|
413
378
|
*/
|
|
414
379
|
#showMessage(text: string): void {
|
|
415
|
-
this.ctx
|
|
416
|
-
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
417
|
-
this.ctx.chatContainer.addChild(new Text(text, 1, 1));
|
|
418
|
-
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
419
|
-
this.ctx.ui.requestRender();
|
|
380
|
+
showCommandMessage(this.ctx, text);
|
|
420
381
|
}
|
|
421
382
|
}
|