@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.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 +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import { type AuthCredential, SqliteAuthCredentialStore, type TSchema } from "@oh-my-pi/pi-ai";
|
|
20
|
+
import { piEscapeRegexLiteral, piJoinPath } from "@oh-my-pi/pi-ai/providers/cursor-pi-args";
|
|
20
21
|
import { getKeybindings, type Keybinding, Text } from "@oh-my-pi/pi-tui";
|
|
21
22
|
import {
|
|
22
23
|
getAgentDbPath,
|
|
@@ -139,6 +140,25 @@ export interface LsToolOptions {
|
|
|
139
140
|
operations?: LsOperations;
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
export interface EditOperations {
|
|
144
|
+
readFile: (absolutePath: string) => Promise<Buffer>;
|
|
145
|
+
writeFile: (absolutePath: string, content: string) => Promise<void>;
|
|
146
|
+
access: (absolutePath: string) => Promise<void>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface EditToolOptions {
|
|
150
|
+
operations?: EditOperations;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface WriteOperations {
|
|
154
|
+
writeFile: (absolutePath: string, content: string) => Promise<void>;
|
|
155
|
+
mkdir: (dir: string) => Promise<void>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface WriteToolOptions {
|
|
159
|
+
operations?: WriteOperations;
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
const legacyBashSchema = Type.Object({
|
|
143
163
|
command: Type.String({ description: "Bash command to execute" }),
|
|
144
164
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
|
|
@@ -298,16 +318,6 @@ function lineRangePath(readPath: string, offset: number | undefined, limit: numb
|
|
|
298
318
|
return `${readPath}:${start}-${end}`;
|
|
299
319
|
}
|
|
300
320
|
|
|
301
|
-
function escapeRegexLiteral(value: string): string {
|
|
302
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function joinLegacyGlob(searchPath: string, pattern: string): string {
|
|
306
|
-
if (path.isAbsolute(pattern)) return pattern;
|
|
307
|
-
if (!searchPath || searchPath === ".") return pattern;
|
|
308
|
-
return path.join(searchPath, pattern);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
321
|
function normalizeLegacyLimit(limit: number | undefined, fallback: number): number {
|
|
312
322
|
if (limit === undefined || !Number.isFinite(limit)) return fallback;
|
|
313
323
|
return Math.max(1, Math.floor(limit));
|
|
@@ -512,7 +522,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
|
|
|
512
522
|
renderResult: legacyRenderResult,
|
|
513
523
|
execute: (toolCallId, params, signal, onUpdate) => {
|
|
514
524
|
const rawPattern = stringField(params, "pattern") ?? "";
|
|
515
|
-
const pattern = booleanField(params, "literal") ?
|
|
525
|
+
const pattern = booleanField(params, "literal") ? piEscapeRegexLiteral(rawPattern) : rawPattern;
|
|
516
526
|
const searchPath = stringField(params, "path") ?? ".";
|
|
517
527
|
const glob = stringField(params, "glob");
|
|
518
528
|
const context = numberField(params, "context");
|
|
@@ -529,7 +539,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
|
|
|
529
539
|
toolCallId,
|
|
530
540
|
{
|
|
531
541
|
pattern,
|
|
532
|
-
path: glob ?
|
|
542
|
+
path: glob ? piJoinPath(searchPath, glob) : searchPath,
|
|
533
543
|
case: booleanField(params, "ignoreCase") ? false : undefined,
|
|
534
544
|
},
|
|
535
545
|
signal,
|
|
@@ -587,7 +597,7 @@ export function createFindToolDefinition(cwd: string, options?: FindToolOptions)
|
|
|
587
597
|
}
|
|
588
598
|
return tool.execute(
|
|
589
599
|
toolCallId,
|
|
590
|
-
{ path:
|
|
600
|
+
{ path: piJoinPath(searchPath, pattern), hidden: true, gitignore: true, limit },
|
|
591
601
|
signal,
|
|
592
602
|
onUpdate,
|
|
593
603
|
);
|
|
@@ -645,6 +655,40 @@ export function createLsTool(cwd: string, options?: LsToolOptions): ToolDefiniti
|
|
|
645
655
|
return createLsToolDefinition(cwd, options);
|
|
646
656
|
}
|
|
647
657
|
|
|
658
|
+
/** Create the legacy edit tool definition. */
|
|
659
|
+
export function createEditToolDefinition(cwd: string, options?: EditToolOptions): ToolDefinition {
|
|
660
|
+
if (options?.operations) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
"Legacy EditToolOptions.operations is not supported: OMP's built-in edit tool writes the local " +
|
|
663
|
+
"filesystem natively and exposes no pluggable operations seam. Register a custom edit tool via " +
|
|
664
|
+
"defineTool() instead of passing operations to createEditTool()/createEditToolDefinition().",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return legacyBuiltinTool(cwd, "edit");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Create the legacy edit tool. */
|
|
671
|
+
export function createEditTool(cwd: string, options?: EditToolOptions): ToolDefinition {
|
|
672
|
+
return createEditToolDefinition(cwd, options);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Create the legacy write tool definition. */
|
|
676
|
+
export function createWriteToolDefinition(cwd: string, options?: WriteToolOptions): ToolDefinition {
|
|
677
|
+
if (options?.operations) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
"Legacy WriteToolOptions.operations is not supported: OMP's built-in write tool writes the local " +
|
|
680
|
+
"filesystem natively and exposes no pluggable operations seam. Register a custom write tool via " +
|
|
681
|
+
"defineTool() instead of passing operations to createWriteTool()/createWriteToolDefinition().",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return legacyBuiltinTool(cwd, "write");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Create the legacy write tool. */
|
|
688
|
+
export function createWriteTool(cwd: string, options?: WriteToolOptions): ToolDefinition {
|
|
689
|
+
return createWriteToolDefinition(cwd, options);
|
|
690
|
+
}
|
|
691
|
+
|
|
648
692
|
/** Create legacy read, bash, edit, and write tools. */
|
|
649
693
|
export function createCodingTools(cwd: string): ToolDefinition[] {
|
|
650
694
|
return LEGACY_CODING_TOOL_NAMES.map(name => legacyBuiltinTool(cwd, name));
|
|
@@ -1378,6 +1422,14 @@ export function getPackageDir(): string {
|
|
|
1378
1422
|
// check during validation (issue #6583).
|
|
1379
1423
|
export { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
1380
1424
|
|
|
1425
|
+
// Same barrel gap for two more legacy package-root exports: pi re-exported the
|
|
1426
|
+
// `CONFIG_DIR_NAME` constant and the CLI parser `parseArgs`. In omp
|
|
1427
|
+
// `CONFIG_DIR_NAME` lives in `@oh-my-pi/pi-utils` and `parseArgs` in
|
|
1428
|
+
// `../cli/args`, neither of which the barrel below forwards, so legacy
|
|
1429
|
+
// extensions importing either fail Bun's static export check during validation.
|
|
1430
|
+
export { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils";
|
|
1431
|
+
export { parseArgs } from "../cli/args";
|
|
1432
|
+
|
|
1381
1433
|
export * from "../index";
|
|
1382
1434
|
export { formatBytes as formatSize } from "../tools/render-utils";
|
|
1383
1435
|
export { copyToClipboard } from "../utils/clipboard";
|
|
@@ -20,6 +20,7 @@ export * from "./omp-protocol";
|
|
|
20
20
|
export * from "./parse";
|
|
21
21
|
export * from "./router";
|
|
22
22
|
export * from "./rule-protocol";
|
|
23
|
+
export * from "./security-protocol";
|
|
23
24
|
export * from "./skill-protocol";
|
|
24
25
|
export * from "./ssh-protocol";
|
|
25
26
|
export type * from "./types";
|
|
@@ -390,8 +390,9 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
392
|
* Install a process-global override that wins over the AgentRegistry-based
|
|
393
|
-
* derivation. Used by SDK consumers that wire
|
|
394
|
-
* `
|
|
393
|
+
* derivation. Used by top-level SDK consumers that wire
|
|
394
|
+
* `localProtocolOptions` on `createAgentSession`; subagents keep their
|
|
395
|
+
* inherited mapping session-bound.
|
|
395
396
|
*/
|
|
396
397
|
static setOverride(value: LocalProtocolOptions | undefined): void {
|
|
397
398
|
LocalProtocolHandler.#override = value;
|
|
@@ -97,7 +97,10 @@ function formatAvailableResources(mcpManager: MCPManager): string {
|
|
|
97
97
|
.getConnectedServers()
|
|
98
98
|
.flatMap(name => {
|
|
99
99
|
const serverResources = mcpManager.getServerResources(name);
|
|
100
|
-
|
|
100
|
+
if (!serverResources) return [];
|
|
101
|
+
const concrete = serverResources.resources.map(r => ` ${r.uri} (${name})`);
|
|
102
|
+
const templates = serverResources.templates.map(t => ` ${t.uriTemplate} (${name}, template)`);
|
|
103
|
+
return [...concrete, ...templates];
|
|
101
104
|
})
|
|
102
105
|
.join("\n");
|
|
103
106
|
return available || " (none)";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, `ssh://`, `vault://`, and `xd://`).
|
|
2
|
+
* Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `security://`, `skill://`, `ssh://`, `vault://`, and `xd://`).
|
|
3
3
|
*
|
|
4
4
|
* One process-global router with one handler per scheme. Access via
|
|
5
5
|
* `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
|
|
@@ -15,6 +15,7 @@ import { MemoryProtocolHandler } from "./memory-protocol";
|
|
|
15
15
|
import { OmpProtocolHandler } from "./omp-protocol";
|
|
16
16
|
import { extractUriScheme, parseInternalUrl } from "./parse";
|
|
17
17
|
import { RuleProtocolHandler } from "./rule-protocol";
|
|
18
|
+
import { SecurityProtocolHandler } from "./security-protocol";
|
|
18
19
|
import { SkillProtocolHandler } from "./skill-protocol";
|
|
19
20
|
import { SshProtocolHandler } from "./ssh-protocol";
|
|
20
21
|
import type {
|
|
@@ -42,6 +43,8 @@ export class InternalUrlRouter {
|
|
|
42
43
|
this.register(new VaultProtocolHandler());
|
|
43
44
|
this.register(new SkillProtocolHandler());
|
|
44
45
|
this.register(new RuleProtocolHandler());
|
|
46
|
+
// Reserved OMP-owned security-analysis namespace; vendor adapters normalize into its store.
|
|
47
|
+
this.register(new SecurityProtocolHandler());
|
|
45
48
|
this.register(new McpProtocolHandler());
|
|
46
49
|
this.register(new IssueProtocolHandler());
|
|
47
50
|
this.register(new PrProtocolHandler());
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isSettingsInitialized, settings } from "../config/settings";
|
|
4
|
+
import { getDefault } from "../config/settings-schema";
|
|
5
|
+
import type { SecurityFinding } from "../security/contracts";
|
|
6
|
+
import { createPublicSecurityScan, redactPrivateSecurityMetadata } from "../security/provenance";
|
|
7
|
+
import { createSecurityResource } from "../security/resource-output";
|
|
8
|
+
import type { SecurityScanSummary } from "../security/store";
|
|
9
|
+
import { SecurityStore } from "../security/store";
|
|
10
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
11
|
+
|
|
12
|
+
export type SecurityStoreResolver = (cwd: string, signal?: AbortSignal) => Promise<SecurityStore>;
|
|
13
|
+
|
|
14
|
+
export function isSecurityEnabled(): boolean {
|
|
15
|
+
if (!isSettingsInitialized()) return getDefault("security.enabled");
|
|
16
|
+
try {
|
|
17
|
+
return settings.get("security.enabled");
|
|
18
|
+
} catch {
|
|
19
|
+
return getDefault("security.enabled");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function securityEnabledFromContext(context?: ResolveContext): boolean | undefined {
|
|
24
|
+
if (!context?.settings || typeof context.settings !== "object") return undefined;
|
|
25
|
+
try {
|
|
26
|
+
const get = Reflect.get(context.settings, "get");
|
|
27
|
+
if (typeof get !== "function") return undefined;
|
|
28
|
+
const enabled = Reflect.apply(get, context.settings, ["security.enabled"]);
|
|
29
|
+
return typeof enabled === "boolean" ? enabled : undefined;
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SECURITY_DISABLED_MESSAGE =
|
|
36
|
+
"security:// is disabled. Enable it by setting `security.enabled = true` (Settings → Tools → Security).";
|
|
37
|
+
|
|
38
|
+
export class SecurityDisabledError extends Error {
|
|
39
|
+
constructor() {
|
|
40
|
+
super(SECURITY_DISABLED_MESSAGE);
|
|
41
|
+
this.name = "SecurityDisabledError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function splitSecurityPath(url: InternalUrl): string[] {
|
|
46
|
+
const host = url.rawHost || url.hostname;
|
|
47
|
+
const pathname = (url.rawPathname ?? url.pathname).replace(/^\/+/, "");
|
|
48
|
+
return [host, ...pathname.split("/")].filter(Boolean).map(segment => decodeURIComponent(segment));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatScans(scans: SecurityScanSummary[]): string {
|
|
52
|
+
if (scans.length === 0) return "# Security scans\n\nNo scans are stored for this project.\n";
|
|
53
|
+
const rows = scans.map(
|
|
54
|
+
scan =>
|
|
55
|
+
`- \`${scan.id}\` — ${scan.status}; ${scan.findingCount} finding(s); ${scan.producer.name}; ${scan.createdAt}`,
|
|
56
|
+
);
|
|
57
|
+
return `# Security scans\n\n${rows.join("\n")}\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatFinding(finding: SecurityFinding): string {
|
|
61
|
+
const locations = finding.occurrences.flatMap(occurrence => occurrence.locations);
|
|
62
|
+
const locationLines = locations.map(location => {
|
|
63
|
+
const end = location.endLine && location.endLine !== location.startLine ? `-${location.endLine}` : "";
|
|
64
|
+
return [
|
|
65
|
+
`- \`${sanitizeText(location.path)}:${location.startLine}${end}\``,
|
|
66
|
+
location.role ? ` (${sanitizeText(location.role)})` : "",
|
|
67
|
+
].join("");
|
|
68
|
+
});
|
|
69
|
+
const evidence = finding.evidence.map(
|
|
70
|
+
item => `- **${sanitizeText(item.label)}** — ${sanitizeText(item.explanation)}`,
|
|
71
|
+
);
|
|
72
|
+
return [
|
|
73
|
+
`# ${sanitizeText(finding.title)}`,
|
|
74
|
+
"",
|
|
75
|
+
`- ID: \`${finding.id}\``,
|
|
76
|
+
`- Rule: \`${sanitizeText(finding.ruleId)}\``,
|
|
77
|
+
`- Severity: **${finding.severity.level}**`,
|
|
78
|
+
`- Confidence: **${finding.confidence.level}**`,
|
|
79
|
+
`- Disposition: **${finding.disposition.status}**`,
|
|
80
|
+
`- Fingerprint: \`${finding.fingerprint}\``,
|
|
81
|
+
"",
|
|
82
|
+
"## Summary",
|
|
83
|
+
"",
|
|
84
|
+
sanitizeText(finding.summary),
|
|
85
|
+
"",
|
|
86
|
+
"## Locations",
|
|
87
|
+
"",
|
|
88
|
+
...(locationLines.length > 0 ? locationLines : ["No source locations recorded."]),
|
|
89
|
+
"",
|
|
90
|
+
"## Evidence",
|
|
91
|
+
"",
|
|
92
|
+
...(evidence.length > 0 ? evidence : ["No expanded evidence recorded."]),
|
|
93
|
+
"",
|
|
94
|
+
"## Remediation",
|
|
95
|
+
"",
|
|
96
|
+
sanitizeText(finding.remediation ?? "No remediation guidance recorded."),
|
|
97
|
+
"",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class SecurityProtocolHandler implements ProtocolHandler {
|
|
102
|
+
readonly scheme = "security";
|
|
103
|
+
readonly immutable = true;
|
|
104
|
+
readonly #resolveStore: SecurityStoreResolver;
|
|
105
|
+
readonly #enabled: () => boolean;
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
resolveStore: SecurityStoreResolver = (cwd, signal) => SecurityStore.openForCwd(cwd, { signal }),
|
|
109
|
+
enabled: () => boolean = isSecurityEnabled,
|
|
110
|
+
) {
|
|
111
|
+
this.#resolveStore = resolveStore;
|
|
112
|
+
this.#enabled = enabled;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async #store(context?: ResolveContext): Promise<SecurityStore> {
|
|
116
|
+
return this.#resolveStore(path.resolve(context?.cwd ?? process.cwd()), context?.signal);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
120
|
+
if (!(securityEnabledFromContext(context) ?? this.#enabled())) throw new SecurityDisabledError();
|
|
121
|
+
const parts = splitSecurityPath(url);
|
|
122
|
+
const store = await this.#store(context);
|
|
123
|
+
if (parts.length === 0) {
|
|
124
|
+
return createSecurityResource({
|
|
125
|
+
url: "security://",
|
|
126
|
+
content: [
|
|
127
|
+
"# Security",
|
|
128
|
+
"",
|
|
129
|
+
"OMP-owned software-security analysis resources. The namespace is read-only; use explicit security commands or tools for mutations.",
|
|
130
|
+
"",
|
|
131
|
+
"- `security://scans` — list scans",
|
|
132
|
+
"",
|
|
133
|
+
].join("\n"),
|
|
134
|
+
contentType: "text/markdown",
|
|
135
|
+
isDirectory: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (parts[0] !== "scans") throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
139
|
+
if (parts.length === 1) {
|
|
140
|
+
return createSecurityResource({
|
|
141
|
+
url: "security://scans",
|
|
142
|
+
content: formatScans(await store.listScans()),
|
|
143
|
+
contentType: "text/markdown",
|
|
144
|
+
isDirectory: true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const scanId = parts[1];
|
|
148
|
+
const bundle = await store.getBundle(scanId);
|
|
149
|
+
if (!bundle) throw new Error(`Unknown security scan: ${scanId}`);
|
|
150
|
+
if (parts.length === 2) {
|
|
151
|
+
return createSecurityResource({
|
|
152
|
+
url: `security://scans/${scanId}`,
|
|
153
|
+
content: [
|
|
154
|
+
`# Security scan ${scanId}`,
|
|
155
|
+
"",
|
|
156
|
+
`- Status: **${bundle.scan.status}**`,
|
|
157
|
+
`- Producer: **${sanitizeText(bundle.scan.producer.name)}**`,
|
|
158
|
+
`- Findings: **${bundle.findings.length}**`,
|
|
159
|
+
`- Coverage: **${bundle.scan.coverage.completeness}**`,
|
|
160
|
+
`- Target: \`${sanitizeText(bundle.scan.target.displayName)}\``,
|
|
161
|
+
"",
|
|
162
|
+
"Resources: `manifest`, `findings`, `coverage`, `report`, `sarif`, `provenance`.",
|
|
163
|
+
"",
|
|
164
|
+
].join("\n"),
|
|
165
|
+
contentType: "text/markdown",
|
|
166
|
+
isDirectory: true,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
switch (parts[2]) {
|
|
170
|
+
case "manifest":
|
|
171
|
+
if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
172
|
+
return createSecurityResource({
|
|
173
|
+
url: `security://scans/${scanId}/manifest`,
|
|
174
|
+
content: `${JSON.stringify(createPublicSecurityScan(bundle.scan, { includePlan: true }), null, 2)}\n`,
|
|
175
|
+
contentType: "application/json",
|
|
176
|
+
});
|
|
177
|
+
case "findings": {
|
|
178
|
+
if (parts.length === 3) {
|
|
179
|
+
const listing = bundle.findings.map(finding =>
|
|
180
|
+
[
|
|
181
|
+
`- \`${finding.id}\` **${finding.severity.level}** — ${sanitizeText(finding.title)}`,
|
|
182
|
+
` (\`${sanitizeText(finding.ruleId)}\`)`,
|
|
183
|
+
].join(""),
|
|
184
|
+
);
|
|
185
|
+
return createSecurityResource({
|
|
186
|
+
url: `security://scans/${scanId}/findings`,
|
|
187
|
+
content: `# Findings for ${scanId}\n\n${listing.length > 0 ? listing.join("\n") : "No findings."}\n`,
|
|
188
|
+
contentType: "text/markdown",
|
|
189
|
+
isDirectory: true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (parts.length !== 4) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
193
|
+
const findingId = parts[3];
|
|
194
|
+
const finding = await store.getFinding(scanId, findingId);
|
|
195
|
+
if (!finding) throw new Error(`Unknown security finding: ${findingId}`);
|
|
196
|
+
return createSecurityResource({
|
|
197
|
+
url: `security://scans/${scanId}/findings/${findingId}`,
|
|
198
|
+
content: formatFinding(finding),
|
|
199
|
+
contentType: "text/markdown",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
case "coverage":
|
|
203
|
+
if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
204
|
+
return createSecurityResource({
|
|
205
|
+
url: `security://scans/${scanId}/coverage`,
|
|
206
|
+
content: `${JSON.stringify(bundle.scan.coverage, null, 2)}\n`,
|
|
207
|
+
contentType: "application/json",
|
|
208
|
+
});
|
|
209
|
+
case "report":
|
|
210
|
+
if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
211
|
+
if (bundle.report === undefined) throw new Error(`Security scan ${scanId} has no report`);
|
|
212
|
+
return createSecurityResource({
|
|
213
|
+
url: `security://scans/${scanId}/report`,
|
|
214
|
+
content: bundle.report,
|
|
215
|
+
contentType: "text/markdown",
|
|
216
|
+
});
|
|
217
|
+
case "sarif":
|
|
218
|
+
if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
219
|
+
if (bundle.sarif === undefined) throw new Error(`Security scan ${scanId} has no SARIF export`);
|
|
220
|
+
return createSecurityResource({
|
|
221
|
+
url: `security://scans/${scanId}/sarif`,
|
|
222
|
+
content: `${JSON.stringify(bundle.sarif, null, 2)}\n`,
|
|
223
|
+
contentType: "application/json",
|
|
224
|
+
});
|
|
225
|
+
case "provenance":
|
|
226
|
+
if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
227
|
+
return createSecurityResource({
|
|
228
|
+
url: `security://scans/${scanId}/provenance`,
|
|
229
|
+
content: `${JSON.stringify(redactPrivateSecurityMetadata(bundle.scan.provenance), null, 2)}\n`,
|
|
230
|
+
contentType: "application/json",
|
|
231
|
+
});
|
|
232
|
+
default:
|
|
233
|
+
throw new Error(`Unknown security resource: security://${parts.join("/")}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async complete(query = "", context?: ResolveContext): Promise<UrlCompletion[]> {
|
|
238
|
+
if (!(securityEnabledFromContext(context) ?? this.#enabled())) return [];
|
|
239
|
+
const store = await this.#store(context);
|
|
240
|
+
const scans = await store.listScans();
|
|
241
|
+
const candidates: UrlCompletion[] = [{ value: "scans", label: "Scans", description: "Stored security scans" }];
|
|
242
|
+
for (const scan of scans.slice(0, 50)) {
|
|
243
|
+
const prefix = `scans/${scan.id}`;
|
|
244
|
+
candidates.push({
|
|
245
|
+
value: prefix,
|
|
246
|
+
label: scan.id,
|
|
247
|
+
description: `${scan.status}; ${scan.findingCount} findings`,
|
|
248
|
+
});
|
|
249
|
+
for (const child of ["manifest", "findings", "coverage", "report", "sarif", "provenance"]) {
|
|
250
|
+
candidates.push({ value: `${prefix}/${child}`, label: `${scan.id}/${child}` });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
254
|
+
if (!normalizedQuery) return candidates;
|
|
255
|
+
return candidates.filter(candidate =>
|
|
256
|
+
[candidate.value, candidate.label ?? "", candidate.description ?? ""].some(value =>
|
|
257
|
+
value.toLowerCase().includes(normalizedQuery),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the internal URL routing system.
|
|
3
3
|
*
|
|
4
|
-
* Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, `ssh://`, `vault://`, and `xd://`) are resolved by tools like read,
|
|
4
|
+
* Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `security://`, `skill://`, `ssh://`, `vault://`, and `xd://`) are resolved by tools like read,
|
|
5
5
|
* providing access to agent outputs and server resources without exposing filesystem paths.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
|
|
2
|
+
import type { TerminalOutputOptions } from "./terminal-output";
|
|
3
|
+
import {
|
|
4
|
+
TERMINAL_OUTPUT_WORKER_ARG,
|
|
5
|
+
type TerminalOutputWorkerRequest,
|
|
6
|
+
type TerminalOutputWorkerResult,
|
|
7
|
+
} from "./terminal-output-worker-protocol";
|
|
8
|
+
|
|
9
|
+
/** Replay legacy broker PTY bytes without evaluating xterm in the client process. */
|
|
10
|
+
export async function renderTerminalOutputIsolated(
|
|
11
|
+
output: string,
|
|
12
|
+
options: TerminalOutputOptions,
|
|
13
|
+
): Promise<string[] | undefined> {
|
|
14
|
+
const hostEntry = workerHostEntry();
|
|
15
|
+
const worker = hostEntry
|
|
16
|
+
? new Worker(hostEntry, { type: "module", argv: [TERMINAL_OUTPUT_WORKER_ARG] })
|
|
17
|
+
: new Worker(new URL("./terminal-output-worker.ts", import.meta.url).href, { type: "module" });
|
|
18
|
+
const pending = Promise.withResolvers<string[] | undefined>();
|
|
19
|
+
const onMessage = (event: MessageEvent<TerminalOutputWorkerResult>): void => {
|
|
20
|
+
if (event.data.ok) pending.resolve(event.data.rows);
|
|
21
|
+
else pending.reject(new Error(event.data.error));
|
|
22
|
+
};
|
|
23
|
+
const onError = (event: ErrorEvent): void => {
|
|
24
|
+
pending.reject(event.error instanceof Error ? event.error : new Error(event.message));
|
|
25
|
+
};
|
|
26
|
+
const onClose = (): void => {
|
|
27
|
+
pending.reject(new Error("Terminal output worker exited before responding"));
|
|
28
|
+
};
|
|
29
|
+
worker.addEventListener("message", onMessage);
|
|
30
|
+
worker.addEventListener("error", onError);
|
|
31
|
+
worker.addEventListener("close", onClose);
|
|
32
|
+
try {
|
|
33
|
+
const request: TerminalOutputWorkerRequest = { output, options };
|
|
34
|
+
worker.postMessage(request);
|
|
35
|
+
return await pending.promise;
|
|
36
|
+
} finally {
|
|
37
|
+
worker.removeEventListener("message", onMessage);
|
|
38
|
+
worker.removeEventListener("error", onError);
|
|
39
|
+
worker.removeEventListener("close", onClose);
|
|
40
|
+
worker.terminate();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Distribution smoke for source, npm-bundle, and compiled worker routing. */
|
|
45
|
+
export async function smokeTestTerminalOutputWorker(): Promise<void> {
|
|
46
|
+
const rows = await renderTerminalOutputIsolated("old\r\x1b[2K\x1b[1;32mready\x1b[0m", {
|
|
47
|
+
head: false,
|
|
48
|
+
maxRows: 10,
|
|
49
|
+
});
|
|
50
|
+
if (rows?.length !== 1 || rows[0] !== "\x1b[0m\x1b[1;38;5;2mready") {
|
|
51
|
+
throw new Error("terminal output worker smoke mismatch");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TerminalOutputOptions } from "./terminal-output";
|
|
2
|
+
|
|
3
|
+
/** Hidden CLI selector for legacy PTY replay outside the client process. */
|
|
4
|
+
export const TERMINAL_OUTPUT_WORKER_ARG = "__omp_worker_terminal_output";
|
|
5
|
+
|
|
6
|
+
export interface TerminalOutputWorkerRequest {
|
|
7
|
+
output: string;
|
|
8
|
+
options: TerminalOutputOptions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TerminalOutputWorkerResult = { ok: true; rows: string[] | undefined } | { ok: false; error: string };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
3
|
+
import { renderTerminalOutput } from "./terminal-output";
|
|
4
|
+
import type { TerminalOutputWorkerRequest, TerminalOutputWorkerResult } from "./terminal-output-worker-protocol";
|
|
5
|
+
|
|
6
|
+
if (!parentPort) throw new Error("terminal-output-worker: missing parentPort");
|
|
7
|
+
|
|
8
|
+
const port = parentPort;
|
|
9
|
+
const inbox = consumeWorkerInbox();
|
|
10
|
+
const handle = async (message: unknown): Promise<void> => {
|
|
11
|
+
const request = message as TerminalOutputWorkerRequest;
|
|
12
|
+
let result: TerminalOutputWorkerResult;
|
|
13
|
+
try {
|
|
14
|
+
result = { ok: true, rows: await renderTerminalOutput(request.output, request.options) };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
result = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
17
|
+
}
|
|
18
|
+
port.postMessage(result);
|
|
19
|
+
port.close();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (inbox) inbox.bind(message => void handle(message));
|
|
23
|
+
else port.on("message", message => void handle(message));
|
package/src/lsp/client.ts
CHANGED
|
@@ -27,6 +27,7 @@ const fileOperationLocks = new Map<string, Promise<void>>();
|
|
|
27
27
|
/** Negative cache of recent init failures so a broken server fails fast instead of re-spawning per call. */
|
|
28
28
|
const INIT_FAILURE_BACKOFF_MS = 3 * 60 * 1000;
|
|
29
29
|
const initFailures = new Map<string, { at: number; message: string }>();
|
|
30
|
+
const READER_EXIT_GRACE_MS = 100;
|
|
30
31
|
|
|
31
32
|
// Idle timeout configuration (disabled by default)
|
|
32
33
|
let idleTimeoutMs: number | null = null;
|
|
@@ -303,6 +304,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
303
304
|
|
|
304
305
|
const framer = new MessageFramer(Buffer.from(client.messageBuffer));
|
|
305
306
|
|
|
307
|
+
let readerFailed = false;
|
|
306
308
|
try {
|
|
307
309
|
while (true) {
|
|
308
310
|
const { done, value } = await reader.read();
|
|
@@ -391,6 +393,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
} catch (err) {
|
|
396
|
+
readerFailed = true;
|
|
394
397
|
// Connection closed or error - reject all pending requests
|
|
395
398
|
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
396
399
|
pending.reject(new Error(`LSP connection closed: ${err}`));
|
|
@@ -401,6 +404,9 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
401
404
|
client.messageBuffer = framer.remainder();
|
|
402
405
|
reader.releaseLock();
|
|
403
406
|
client.isReading = false;
|
|
407
|
+
if (!readerFailed && client.proc.exitCode === null) {
|
|
408
|
+
await waitForExit(client, READER_EXIT_GRACE_MS);
|
|
409
|
+
}
|
|
404
410
|
// Reader exited while the server process is still alive (unrecoverable
|
|
405
411
|
// read error or bad stream state): nothing will route responses anymore,
|
|
406
412
|
// so tear the client down — the next call respawns instead of timing out.
|
|
@@ -676,6 +682,15 @@ const PROJECT_LOAD_TIMEOUT_MS = 15_000;
|
|
|
676
682
|
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
677
683
|
const EXIT_TIMEOUT_MS = 1_000;
|
|
678
684
|
|
|
685
|
+
function clientKey(config: ServerConfig, cwd: string): string {
|
|
686
|
+
return `${config.command}:${cwd}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Allow an explicit user reload to retry a matching initialization failure immediately. */
|
|
690
|
+
export function clearInitializationFailure(config: ServerConfig, cwd: string): void {
|
|
691
|
+
initFailures.delete(clientKey(config, cwd));
|
|
692
|
+
}
|
|
693
|
+
|
|
679
694
|
/**
|
|
680
695
|
* Get or create an LSP client for the given server configuration and working directory.
|
|
681
696
|
* @param config - Server configuration
|
|
@@ -692,7 +707,7 @@ export async function getOrCreateClient(
|
|
|
692
707
|
initTimeoutMs?: number,
|
|
693
708
|
signal?: AbortSignal,
|
|
694
709
|
): Promise<LspClient> {
|
|
695
|
-
const key =
|
|
710
|
+
const key = clientKey(config, cwd);
|
|
696
711
|
|
|
697
712
|
// Check if client already exists
|
|
698
713
|
const existingClient = clients.get(key);
|
|
@@ -865,13 +880,13 @@ export async function getActiveOrPendingClient(
|
|
|
865
880
|
signal?: AbortSignal,
|
|
866
881
|
): Promise<LspClient | undefined> {
|
|
867
882
|
throwIfAborted(signal);
|
|
868
|
-
const client = clients.get(
|
|
883
|
+
const client = clients.get(clientKey(config, cwd));
|
|
869
884
|
if (client) {
|
|
870
885
|
client.lastActivity = Date.now();
|
|
871
886
|
return client;
|
|
872
887
|
}
|
|
873
888
|
|
|
874
|
-
const pending = clientLocks.get(
|
|
889
|
+
const pending = clientLocks.get(clientKey(config, cwd));
|
|
875
890
|
if (!pending) return undefined;
|
|
876
891
|
try {
|
|
877
892
|
return await untilAborted(signal, pending);
|
package/src/lsp/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { formatPathRelativeToCwd, resolveToCwd } from "../tools/path-utils";
|
|
|
17
17
|
import { ToolAbortError, ToolError, throwIfAborted } from "../tools/tool-errors";
|
|
18
18
|
import { clampTimeout } from "../tools/tool-timeouts";
|
|
19
19
|
import {
|
|
20
|
+
clearInitializationFailure,
|
|
20
21
|
ensureFileOpen,
|
|
21
22
|
FileChangeType,
|
|
22
23
|
getActiveClients,
|
|
@@ -1598,6 +1599,9 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1598
1599
|
_context?: AgentToolContext,
|
|
1599
1600
|
): Promise<AgentToolResult<LspToolDetails>> {
|
|
1600
1601
|
const { action, file, line, symbol, query, new_name, apply, timeout } = params;
|
|
1602
|
+
if (this.session.lspReadOnly && !LSP_READONLY_ACTIONS.has(action)) {
|
|
1603
|
+
throw new ToolError(`LSP action ${action} is disabled in this read-only session`);
|
|
1604
|
+
}
|
|
1601
1605
|
const timeoutSec = clampTimeout("lsp", timeout, this.session.settings.get("tools.maxTimeout"));
|
|
1602
1606
|
const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
|
|
1603
1607
|
const callerSignal = signal;
|
|
@@ -2345,6 +2349,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
2345
2349
|
const outputs: string[] = [];
|
|
2346
2350
|
for (const [workspaceServerName, workspaceServerConfig] of servers) {
|
|
2347
2351
|
throwIfAborted(signal);
|
|
2352
|
+
clearInitializationFailure(workspaceServerConfig, this.session.cwd);
|
|
2348
2353
|
try {
|
|
2349
2354
|
const workspaceClient = await getOrCreateClient(
|
|
2350
2355
|
workspaceServerConfig,
|
|
@@ -2377,6 +2382,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
2377
2382
|
|
|
2378
2383
|
const [serverName, serverConfig] = serverInfo;
|
|
2379
2384
|
|
|
2385
|
+
if (action === "reload") clearInitializationFailure(serverConfig, this.session.cwd);
|
|
2386
|
+
|
|
2380
2387
|
try {
|
|
2381
2388
|
const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
|
|
2382
2389
|
const targetFile = resolvedFile;
|
package/src/lsp/render.ts
CHANGED
|
@@ -582,7 +582,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
582
582
|
for (let i = 0; i < lines.length; i++) {
|
|
583
583
|
const isLast = i === lines.length - 1;
|
|
584
584
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
585
|
-
output += `\n ${theme.fg("dim", branch)} ${lines[i]}`;
|
|
585
|
+
output += `\n ${theme.fg("dim", branch)} ${truncateToWidth(replaceTabs(lines[i]), TRUNCATE_LENGTHS.CONTENT)}`;
|
|
586
586
|
}
|
|
587
587
|
return output.split("\n");
|
|
588
588
|
}
|