@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.4
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 +80 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/transcript-container.ts +13 -7
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-loader.ts +31 -3
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/stt/recorder.ts +68 -55
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +61 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/zip.ts +16 -1
package/src/eval/jl/prelude.jl
CHANGED
|
@@ -519,7 +519,7 @@ function completion(prompt::String; model="default", system=nothing, schema=noth
|
|
|
519
519
|
return schema === nothing ? text : Main.json_parse(string(text))
|
|
520
520
|
end
|
|
521
521
|
|
|
522
|
-
function agent(prompt::String; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
522
|
+
function agent(prompt::String; agent="task", model=nothing, label=nothing, schema=nothing, schema_mode=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
523
523
|
args_dict = Dict{String, Any}("prompt" => prompt)
|
|
524
524
|
if agent !== nothing
|
|
525
525
|
args_dict["agent"] = agent
|
|
@@ -533,8 +533,9 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
533
533
|
if schema !== nothing
|
|
534
534
|
args_dict["schema"] = schema
|
|
535
535
|
end
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
if schema_mode !== nothing
|
|
537
|
+
args_dict["schemaMode"] = schema_mode
|
|
538
|
+
end
|
|
538
539
|
if isolated !== nothing
|
|
539
540
|
args_dict["isolated"] = Bool(isolated)
|
|
540
541
|
end
|
|
@@ -548,13 +549,13 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
548
549
|
for (k, v) in kwargs
|
|
549
550
|
args_dict[string(k)] = v
|
|
550
551
|
end
|
|
551
|
-
# Tell the bridge a handle is wanted so it preserves the backing artifacts.
|
|
552
552
|
if handle_result
|
|
553
553
|
args_dict["handle"] = true
|
|
554
554
|
end
|
|
555
555
|
res = __omp_call_bridge("__agent__", args_dict)
|
|
556
556
|
text = res isa AbstractDict ? get(res, "text", res) : res
|
|
557
|
-
|
|
557
|
+
has_data = res isa AbstractDict && haskey(res, "data")
|
|
558
|
+
parsed = has_data ? res["data"] : (schema === nothing ? text : Main.json_parse(string(text)))
|
|
558
559
|
if !handle_result
|
|
559
560
|
return parsed
|
|
560
561
|
end
|
|
@@ -569,7 +570,7 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
|
|
|
569
570
|
"id" => get(details, "id", nothing),
|
|
570
571
|
"agent" => get(details, "agent", nothing)
|
|
571
572
|
)
|
|
572
|
-
if schema !== nothing
|
|
573
|
+
if has_data || schema !== nothing
|
|
573
574
|
node["data"] = parsed
|
|
574
575
|
end
|
|
575
576
|
for (src_key, dst_key) in (
|
|
@@ -104,20 +104,21 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
104
104
|
"agent",
|
|
105
105
|
opts,
|
|
106
106
|
rest,
|
|
107
|
-
["agent", "model", "label", "schema", "isolated", "apply", "merge"],
|
|
108
|
-
"{ agent, model, label, schema, isolated, apply, merge, handle }",
|
|
107
|
+
["agent", "model", "label", "schema", "isolated", "apply", "merge", "schemaMode"],
|
|
108
|
+
"{ agent, model, label, schema, isolated, apply, merge, schemaMode, handle }",
|
|
109
109
|
);
|
|
110
110
|
const { handle, ...callArgs } = o;
|
|
111
111
|
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs, handle: Boolean(handle) });
|
|
112
112
|
const text = res && typeof res === "object" ? res.text : res;
|
|
113
|
-
const
|
|
113
|
+
const hasData = res && typeof res === "object" && hasOwn(res, "data");
|
|
114
|
+
const parsed = hasData ? res.data : hasOwn(callArgs, "schema") ? JSON.parse(text) : text;
|
|
114
115
|
if (!handle) return parsed;
|
|
115
116
|
const details = res && typeof res === "object" ? res.details : undefined;
|
|
116
117
|
if (!details || typeof details !== "object" || details.id == null) {
|
|
117
118
|
return { text, output: text, handle: null, id: null, agent: null };
|
|
118
119
|
}
|
|
119
120
|
const node = { text, output: text, handle: `agent://${details.id}`, id: details.id, agent: details.agent ?? null };
|
|
120
|
-
if (hasOwn(callArgs, "schema")) node.data = parsed;
|
|
121
|
+
if (hasData || hasOwn(callArgs, "schema")) node.data = parsed;
|
|
121
122
|
for (const key of ["isolated", "patchPath", "branchName", "nestedPatches", "changesApplied", "isolationSummary"]) {
|
|
122
123
|
if (details[key] !== undefined) node[key] = details[key];
|
|
123
124
|
}
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -487,48 +487,17 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
487
487
|
model=None,
|
|
488
488
|
label=None,
|
|
489
489
|
schema=None,
|
|
490
|
+
schema_mode=None,
|
|
490
491
|
isolated=None,
|
|
491
492
|
apply=None,
|
|
492
493
|
merge=None,
|
|
493
494
|
handle=False,
|
|
494
495
|
):
|
|
495
|
-
"""Run a subagent and return its final output.
|
|
496
|
-
|
|
497
|
-
`
|
|
498
|
-
`
|
|
499
|
-
|
|
500
|
-
supplied the parsed object is returned. Share background by writing a
|
|
501
|
-
local:// file and referencing it in the prompt.
|
|
502
|
-
|
|
503
|
-
Pass `isolated=True` to run the subagent inside an isolation worktree
|
|
504
|
-
(copy-on-write of the parent repo) so parallel `agent()` spawns can
|
|
505
|
-
edit overlapping files safely. Strict opt-in, mirroring the `task`
|
|
506
|
-
tool: the default is non-isolated regardless of `task.isolation.mode`.
|
|
507
|
-
`isolated=True` while the setting is `"none"` errors out instead of
|
|
508
|
-
silently downgrading.
|
|
509
|
-
|
|
510
|
-
When isolated, `apply=False` keeps captured changes inside the
|
|
511
|
-
worktree and surfaces the root patch path, branch name, and nested
|
|
512
|
-
repository patches through the DAG node dict (combine with
|
|
513
|
-
`handle=True` to receive them — see below; the bare return type
|
|
514
|
-
stays bytes/string/parsed object and has nowhere to expose artifacts).
|
|
515
|
-
`merge=False` forces patch mode even when `task.isolation.merge` is
|
|
516
|
-
`"branch"`, avoiding the per-call git lock + repo mutation that branch
|
|
517
|
-
mode performs.
|
|
518
|
-
|
|
519
|
-
Set `handle=True` to receive a DAG node dict instead of bare
|
|
520
|
-
text: ``{"text", "output", "handle", "id", "agent"}`` where ``handle``
|
|
521
|
-
is the spawned agent's recoverable ``agent://<id>`` URI. A downstream
|
|
522
|
-
``pipeline``/``parallel`` stage embeds that ``handle`` (or ``output``)
|
|
523
|
-
in its prompt so a large transcript flows through the graph by
|
|
524
|
-
reference, never re-inlined. When ``schema`` is also set the parsed
|
|
525
|
-
object lands under ``"data"``. When the spawn ran isolated the node
|
|
526
|
-
also carries ``"isolated"`` and, when present, ``"patch_path"``,
|
|
527
|
-
``"branch_name"``, ``"nested_patches"``, ``"changes_applied"``
|
|
528
|
-
(``True``/``False``/``None`` — ``None`` means ``apply=False``), and
|
|
529
|
-
``"isolation_summary"``. If
|
|
530
|
-
the bridge returns no recoverable id the node still resolves with
|
|
531
|
-
``handle=None`` — the helper never throws.
|
|
496
|
+
"""Run a subagent and return its final output or structured data.
|
|
497
|
+
|
|
498
|
+
`schema` overrides agent and session schemas. `schema_mode` is
|
|
499
|
+
`"permissive"` or `"strict"`. `handle=True` returns the child output
|
|
500
|
+
reference and metadata, with parsed data under `"data"` when available.
|
|
532
501
|
"""
|
|
533
502
|
args = {"prompt": prompt}
|
|
534
503
|
if agent is not None:
|
|
@@ -539,6 +508,8 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
539
508
|
args["label"] = label
|
|
540
509
|
if schema is not None:
|
|
541
510
|
args["schema"] = schema
|
|
511
|
+
if schema_mode is not None:
|
|
512
|
+
args["schemaMode"] = schema_mode
|
|
542
513
|
if isolated is not None:
|
|
543
514
|
args["isolated"] = bool(isolated)
|
|
544
515
|
if apply is not None:
|
|
@@ -549,7 +520,8 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
549
520
|
args["handle"] = True
|
|
550
521
|
res = _bridge_call("__agent__", args)
|
|
551
522
|
text = res.get("text") if isinstance(res, dict) else res
|
|
552
|
-
|
|
523
|
+
has_data = isinstance(res, dict) and "data" in res
|
|
524
|
+
parsed = res["data"] if has_data else json.loads(text) if schema is not None else text
|
|
553
525
|
if not handle:
|
|
554
526
|
return parsed
|
|
555
527
|
details = res.get("details") if isinstance(res, dict) else None
|
|
@@ -568,7 +540,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
568
540
|
"id": details["id"],
|
|
569
541
|
"agent": details.get("agent"),
|
|
570
542
|
}
|
|
571
|
-
if schema is not None:
|
|
543
|
+
if has_data or schema is not None:
|
|
572
544
|
node["data"] = parsed
|
|
573
545
|
for src_key, dst_key in (
|
|
574
546
|
("isolated", "isolated"),
|
package/src/eval/rb/prelude.rb
CHANGED
|
@@ -392,22 +392,21 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
|
|
392
392
|
schema.nil? ? text : JSON.parse(text)
|
|
393
393
|
end
|
|
394
394
|
|
|
395
|
-
def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, isolated: nil, apply: nil, merge: nil, handle: false)
|
|
395
|
+
def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, schema_mode: nil, isolated: nil, apply: nil, merge: nil, handle: false)
|
|
396
396
|
args = { "prompt" => prompt }
|
|
397
397
|
args["agent"] = agent unless agent.nil?
|
|
398
398
|
args["model"] = model unless model.nil?
|
|
399
399
|
args["label"] = label unless label.nil?
|
|
400
400
|
args["schema"] = schema unless schema.nil?
|
|
401
|
-
|
|
402
|
-
# with `apply`/`merge` controlling the post-run patch/branch merge.
|
|
401
|
+
args["schemaMode"] = schema_mode unless schema_mode.nil?
|
|
403
402
|
args["isolated"] = !!isolated unless isolated.nil?
|
|
404
403
|
args["apply"] = !!apply unless apply.nil?
|
|
405
404
|
args["merge"] = !!merge unless merge.nil?
|
|
406
|
-
# Tell the bridge a handle is wanted so it preserves the backing artifacts.
|
|
407
405
|
args["handle"] = true if handle
|
|
408
406
|
res = OmpBridge.call("__agent__", args)
|
|
409
407
|
text = res.is_a?(Hash) ? res["text"] : res
|
|
410
|
-
|
|
408
|
+
has_data = res.is_a?(Hash) && res.key?("data")
|
|
409
|
+
parsed = has_data ? res["data"] : (schema.nil? ? text : JSON.parse(text))
|
|
411
410
|
return parsed unless handle
|
|
412
411
|
details = res.is_a?(Hash) ? res["details"] : nil
|
|
413
412
|
if !details.is_a?(Hash) || details["id"].nil?
|
|
@@ -420,7 +419,7 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
|
|
420
419
|
"id" => details["id"],
|
|
421
420
|
"agent" => details["agent"],
|
|
422
421
|
}
|
|
423
|
-
node["data"] = parsed
|
|
422
|
+
node["data"] = parsed if has_data || !schema.nil?
|
|
424
423
|
{
|
|
425
424
|
"isolated" => "isolated",
|
|
426
425
|
"patchPath" => "patch_path",
|
|
@@ -12,12 +12,18 @@
|
|
|
12
12
|
* the same module identity as a direct `@oh-my-pi/pi-coding-agent` import.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import { Database } from "bun:sqlite";
|
|
16
|
+
import * as fs from "node:fs";
|
|
16
17
|
import * as path from "node:path";
|
|
17
18
|
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
18
|
-
import type
|
|
19
|
+
import { type AuthCredential, SqliteAuthCredentialStore, type TSchema } from "@oh-my-pi/pi-ai";
|
|
19
20
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
getAgentDbPath,
|
|
23
|
+
getAgentDir,
|
|
24
|
+
getProjectDir,
|
|
25
|
+
parseFrontmatter as parseOmpFrontmatter,
|
|
26
|
+
} from "@oh-my-pi/pi-utils";
|
|
21
27
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
22
28
|
import { type SettingPath, Settings } from "../config/settings";
|
|
23
29
|
import { EditTool } from "../edit";
|
|
@@ -606,16 +612,16 @@ export function createLsToolDefinition(cwd: string, options?: LsToolOptions): To
|
|
|
606
612
|
const ops = options?.operations;
|
|
607
613
|
const exists = ops
|
|
608
614
|
? await ops.exists(absolutePath)
|
|
609
|
-
: await fs.stat(absolutePath).then(
|
|
615
|
+
: await fs.promises.stat(absolutePath).then(
|
|
610
616
|
() => true,
|
|
611
617
|
() => false,
|
|
612
618
|
);
|
|
613
619
|
if (!exists) throw new Error(`Path not found: ${absolutePath}`);
|
|
614
|
-
const stat = ops ? await ops.stat(absolutePath) : await fs.stat(absolutePath);
|
|
620
|
+
const stat = ops ? await ops.stat(absolutePath) : await fs.promises.stat(absolutePath);
|
|
615
621
|
if (!stat.isDirectory()) {
|
|
616
622
|
return { content: [{ type: "text", text: rawPath }] };
|
|
617
623
|
}
|
|
618
|
-
const entries = ops ? await ops.readdir(absolutePath) : await fs.readdir(absolutePath);
|
|
624
|
+
const entries = ops ? await ops.readdir(absolutePath) : await fs.promises.readdir(absolutePath);
|
|
619
625
|
const sorted = [...entries].sort((a, b) => a.localeCompare(b));
|
|
620
626
|
const limited = sorted.slice(0, limit);
|
|
621
627
|
const output = limited.join("\n");
|
|
@@ -1100,7 +1106,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
|
|
1100
1106
|
: path.resolve(this.#state.cwd, resourcePath);
|
|
1101
1107
|
const files: string[] = [];
|
|
1102
1108
|
try {
|
|
1103
|
-
const stat = await fs.stat(resolvedPath);
|
|
1109
|
+
const stat = await fs.promises.stat(resolvedPath);
|
|
1104
1110
|
if (stat.isDirectory()) {
|
|
1105
1111
|
const glob = new Bun.Glob("**/*.md");
|
|
1106
1112
|
for await (const entry of glob.scan({ cwd: resolvedPath, absolute: false, onlyFiles: true })) {
|
|
@@ -1288,6 +1294,46 @@ export async function createAgentSession(
|
|
|
1288
1294
|
return ompCreateAgentSession(forwarded);
|
|
1289
1295
|
}
|
|
1290
1296
|
|
|
1297
|
+
/**
|
|
1298
|
+
* Synchronous auth storage surface retained for legacy extensions.
|
|
1299
|
+
*
|
|
1300
|
+
* Modern OMP auth storage is asynchronous, while older provider extensions
|
|
1301
|
+
* call `AuthStorage.create().get()` during module initialization.
|
|
1302
|
+
*/
|
|
1303
|
+
export class AuthStorage {
|
|
1304
|
+
constructor() {
|
|
1305
|
+
fs.mkdirSync(path.dirname(getAgentDbPath()), { recursive: true, mode: 0o700 });
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
static create(): AuthStorage {
|
|
1309
|
+
return new AuthStorage();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
get(provider: string): AuthCredential | undefined {
|
|
1313
|
+
const store = new SqliteAuthCredentialStore(new Database(getAgentDbPath()));
|
|
1314
|
+
try {
|
|
1315
|
+
return store.listAuthCredentials(provider)[0]?.credential;
|
|
1316
|
+
} finally {
|
|
1317
|
+
store.close();
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
set(provider: string, credential: AuthCredential): void {
|
|
1322
|
+
const store = new SqliteAuthCredentialStore(new Database(getAgentDbPath()));
|
|
1323
|
+
try {
|
|
1324
|
+
store.upsertAuthCredentialForProvider(provider, credential);
|
|
1325
|
+
} finally {
|
|
1326
|
+
store.close();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Read the first active credential for a legacy extension provider. */
|
|
1332
|
+
export function readStoredCredential(provider: string): AuthCredential | undefined {
|
|
1333
|
+
const storage = AuthStorage.create();
|
|
1334
|
+
return storage.get(provider);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1291
1337
|
export * from "../index";
|
|
1292
1338
|
export { formatBytes as formatSize } from "../tools/render-utils";
|
|
1293
1339
|
export { Type } from "./typebox";
|
package/src/lsp/client.ts
CHANGED
|
@@ -142,6 +142,9 @@ const CLIENT_CAPABILITIES = {
|
|
|
142
142
|
codeDescriptionSupport: true,
|
|
143
143
|
dataSupport: true,
|
|
144
144
|
},
|
|
145
|
+
diagnostic: {
|
|
146
|
+
dynamicRegistration: true,
|
|
147
|
+
},
|
|
145
148
|
},
|
|
146
149
|
window: {
|
|
147
150
|
workDoneProgress: true,
|
|
@@ -456,6 +459,58 @@ async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequ
|
|
|
456
459
|
}
|
|
457
460
|
}
|
|
458
461
|
|
|
462
|
+
interface DynamicCapabilityRegistration {
|
|
463
|
+
id?: unknown;
|
|
464
|
+
method?: unknown;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
interface DynamicCapabilityParams {
|
|
468
|
+
registrations?: DynamicCapabilityRegistration[];
|
|
469
|
+
unregisterations?: DynamicCapabilityRegistration[];
|
|
470
|
+
unregistrations?: DynamicCapabilityRegistration[];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function updateDynamicCapabilities(client: LspClient, message: LspJsonRpcRequest): void {
|
|
474
|
+
const params = message.params as DynamicCapabilityParams;
|
|
475
|
+
if (message.method === "client/registerCapability") {
|
|
476
|
+
if (!Array.isArray(params.registrations)) return;
|
|
477
|
+
let registrations = client.dynamicCapabilityRegistrations;
|
|
478
|
+
if (!registrations) {
|
|
479
|
+
registrations = new Map();
|
|
480
|
+
client.dynamicCapabilityRegistrations = registrations;
|
|
481
|
+
}
|
|
482
|
+
for (const registration of params.registrations) {
|
|
483
|
+
if (typeof registration.id === "string" && typeof registration.method === "string") {
|
|
484
|
+
registrations.set(registration.id, registration.method);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const registrations = client.dynamicCapabilityRegistrations;
|
|
491
|
+
if (!registrations) return;
|
|
492
|
+
const unregistrations = params.unregisterations ?? params.unregistrations;
|
|
493
|
+
if (!Array.isArray(unregistrations)) return;
|
|
494
|
+
for (const registration of unregistrations) {
|
|
495
|
+
if (typeof registration.id === "string") {
|
|
496
|
+
registrations.delete(registration.id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Whether the server advertised LSP 3.17 document diagnostic pulls statically or through registration. */
|
|
502
|
+
export function supportsDocumentDiagnostics(client: LspClient): boolean {
|
|
503
|
+
const staticProvider = client.serverCapabilities?.diagnosticProvider;
|
|
504
|
+
if (staticProvider) return true;
|
|
505
|
+
|
|
506
|
+
const registrations = client.dynamicCapabilityRegistrations;
|
|
507
|
+
if (!registrations) return false;
|
|
508
|
+
for (const method of registrations.values()) {
|
|
509
|
+
if (method === "textDocument/diagnostic") return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
459
514
|
/**
|
|
460
515
|
* Respond to a server-initiated request.
|
|
461
516
|
*/
|
|
@@ -478,6 +533,7 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
|
|
478
533
|
return;
|
|
479
534
|
}
|
|
480
535
|
if (message.method === "client/registerCapability" || message.method === "client/unregisterCapability") {
|
|
536
|
+
updateDynamicCapabilities(client, message);
|
|
481
537
|
// Some servers block semantic requests until dynamic registration succeeds.
|
|
482
538
|
await sendResponse(client, message.id, null, message.method);
|
|
483
539
|
return;
|
|
@@ -685,6 +741,7 @@ export async function getOrCreateClient(
|
|
|
685
741
|
requestId: 0,
|
|
686
742
|
diagnostics: new Map(),
|
|
687
743
|
diagnosticsVersion: 0,
|
|
744
|
+
dynamicCapabilityRegistrations: new Map(),
|
|
688
745
|
openFiles: new Map(),
|
|
689
746
|
pendingRequests: new Map(),
|
|
690
747
|
messageBuffer: new Uint8Array(0),
|
package/src/lsp/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
sendNotification,
|
|
29
29
|
sendRequest,
|
|
30
30
|
setIdleTimeout,
|
|
31
|
+
supportsDocumentDiagnostics,
|
|
31
32
|
syncContent,
|
|
32
33
|
WARMUP_TIMEOUT_MS,
|
|
33
34
|
waitForProjectLoaded,
|
|
@@ -530,17 +531,49 @@ interface WaitForDiagnosticsOptions {
|
|
|
530
531
|
settleMs?: number;
|
|
531
532
|
}
|
|
532
533
|
|
|
534
|
+
function requestDocumentDiagnostics(
|
|
535
|
+
client: LspClient,
|
|
536
|
+
uri: string,
|
|
537
|
+
signal: AbortSignal | undefined,
|
|
538
|
+
timeoutMs: number,
|
|
539
|
+
): Promise<Diagnostic[] | undefined> {
|
|
540
|
+
return sendRequest(client, "textDocument/diagnostic", { textDocument: { uri } }, signal, timeoutMs)
|
|
541
|
+
.then(report => {
|
|
542
|
+
if (!report || typeof report !== "object" || !("kind" in report) || report.kind !== "full") {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
if (!("items" in report) || !Array.isArray(report.items)) return undefined;
|
|
546
|
+
return report.items;
|
|
547
|
+
})
|
|
548
|
+
.catch(err => {
|
|
549
|
+
if (!signal?.aborted) {
|
|
550
|
+
logger.debug("LSP document diagnostic pull failed", { server: client.name, uri, error: String(err) });
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
533
556
|
async function waitForDiagnostics(
|
|
534
557
|
client: LspClient,
|
|
535
558
|
uri: string,
|
|
536
559
|
options: WaitForDiagnosticsOptions = {},
|
|
537
560
|
): Promise<Diagnostic[]> {
|
|
538
561
|
const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, settleMs = DIAGNOSTICS_SETTLE_MS } = options;
|
|
539
|
-
const
|
|
562
|
+
const deadline = Date.now() + timeoutMs;
|
|
563
|
+
let pullAttempted = false;
|
|
564
|
+
let pullResultPromise: Promise<{ diagnostics: Diagnostic[] | undefined }> | undefined;
|
|
565
|
+
let pulled: Diagnostic[] | undefined;
|
|
540
566
|
let settledRef: PublishedDiagnostics | undefined;
|
|
541
567
|
let settledAt = 0;
|
|
542
|
-
while (Date.now()
|
|
568
|
+
while (Date.now() < deadline) {
|
|
543
569
|
throwIfAborted(signal);
|
|
570
|
+
if (!pullAttempted && supportsDocumentDiagnostics(client)) {
|
|
571
|
+
pullAttempted = true;
|
|
572
|
+
pullResultPromise = requestDocumentDiagnostics(client, uri, signal, Math.max(1, deadline - Date.now())).then(
|
|
573
|
+
diagnostics => ({ diagnostics }),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
544
577
|
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
545
578
|
const published = client.diagnostics.get(uri);
|
|
546
579
|
if (published && versionOk) {
|
|
@@ -557,13 +590,36 @@ async function waitForDiagnostics(
|
|
|
557
590
|
return published.diagnostics;
|
|
558
591
|
}
|
|
559
592
|
}
|
|
560
|
-
|
|
593
|
+
|
|
594
|
+
const pollMs = Math.min(DIAGNOSTICS_POLL_MS, Math.max(0, deadline - Date.now()));
|
|
595
|
+
if (!pullResultPromise) {
|
|
596
|
+
await Bun.sleep(pollMs);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const pullResult = await Promise.race([pullResultPromise, Bun.sleep(pollMs).then(() => undefined)]);
|
|
600
|
+
if (pullResult) {
|
|
601
|
+
pullResultPromise = undefined;
|
|
602
|
+
pulled = pullResult.diagnostics;
|
|
603
|
+
if (pulled !== undefined) break;
|
|
604
|
+
}
|
|
561
605
|
}
|
|
606
|
+
|
|
562
607
|
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
563
|
-
|
|
564
|
-
|
|
608
|
+
const published = client.diagnostics.get(uri);
|
|
609
|
+
if (published && versionOk) {
|
|
610
|
+
return published.diagnostics;
|
|
611
|
+
}
|
|
612
|
+
if (pullResultPromise) {
|
|
613
|
+
pulled = (await pullResultPromise).diagnostics;
|
|
565
614
|
}
|
|
566
|
-
|
|
615
|
+
throwIfAborted(signal);
|
|
616
|
+
if (pulled === undefined) return [];
|
|
617
|
+
client.diagnostics.set(uri, {
|
|
618
|
+
diagnostics: pulled,
|
|
619
|
+
version: expectedDocumentVersion ?? client.openFiles.get(uri)?.version ?? null,
|
|
620
|
+
});
|
|
621
|
+
client.diagnosticsVersion += 1;
|
|
622
|
+
return pulled;
|
|
567
623
|
}
|
|
568
624
|
|
|
569
625
|
/** Project type detection result */
|
package/src/lsp/types.ts
CHANGED
|
@@ -387,6 +387,7 @@ export interface LspServerCapabilities {
|
|
|
387
387
|
referencesProvider?: boolean;
|
|
388
388
|
documentSymbolProvider?: boolean;
|
|
389
389
|
workspaceSymbolProvider?: boolean;
|
|
390
|
+
diagnosticProvider?: boolean | Record<string, unknown>;
|
|
390
391
|
[key: string]: unknown;
|
|
391
392
|
}
|
|
392
393
|
|
|
@@ -398,6 +399,8 @@ export interface LspClient {
|
|
|
398
399
|
requestId: number;
|
|
399
400
|
diagnostics: Map<string, PublishedDiagnostics>;
|
|
400
401
|
diagnosticsVersion: number;
|
|
402
|
+
/** Dynamic capability registrations keyed by the server-provided registration ID. */
|
|
403
|
+
dynamicCapabilityRegistrations?: Map<string, string>;
|
|
401
404
|
openFiles: Map<string, OpenFile>;
|
|
402
405
|
pendingRequests: Map<number | string, PendingRequest>;
|
|
403
406
|
messageBuffer: Uint8Array;
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -385,6 +385,13 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
385
385
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
386
386
|
if (!this.#resolvedClientId) {
|
|
387
387
|
await this.#tryRegisterClient(redirectUri);
|
|
388
|
+
// `unapproved_client` explicitly establishes that registration cannot
|
|
389
|
+
// produce the required client id. Other DCR failures stay on the
|
|
390
|
+
// clientless probe path because they may be transient or caused by
|
|
391
|
+
// unrelated registration metadata.
|
|
392
|
+
if (!this.#resolvedClientId && this.#isDefinitiveRegistrationRejection()) {
|
|
393
|
+
throw this.#missingClientIdError();
|
|
394
|
+
}
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
const authUrl = new URL(this.config.authorizationUrl);
|
|
@@ -691,6 +698,19 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
691
698
|
}
|
|
692
699
|
}
|
|
693
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Whether the provider explicitly rejected this client as unapproved.
|
|
703
|
+
*
|
|
704
|
+
* HTTP status alone is insufficient: payload errors such as
|
|
705
|
+
* `invalid_client_metadata` and `invalid_redirect_uri` do not establish that
|
|
706
|
+
* the authorization endpoint requires a client id. Keep those on the
|
|
707
|
+
* clientless probe path.
|
|
708
|
+
*/
|
|
709
|
+
#isDefinitiveRegistrationRejection(): boolean {
|
|
710
|
+
const failure = this.#registrationFailure;
|
|
711
|
+
return failure?.status === 403 && /\bunapproved_client\b/i.test(failure.detail ?? "");
|
|
712
|
+
}
|
|
713
|
+
|
|
694
714
|
/**
|
|
695
715
|
* Build the error thrown when the authorize probe confirms the provider
|
|
696
716
|
* demands a `client_id`. When dynamic client registration was attempted and
|