@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2
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 +91 -0
- package/dist/cli.js +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
package/src/tools/read.ts
CHANGED
|
@@ -748,10 +748,7 @@ function splitPdfImageMemberReadPath(readPath: string): { pdfPath: string; membe
|
|
|
748
748
|
|
|
749
749
|
const readSchema = type({
|
|
750
750
|
path: type("string").describe(
|
|
751
|
-
|
|
752
|
-
),
|
|
753
|
-
"selector?": type("string").describe(
|
|
754
|
-
'selector without a leading colon (e.g. "50-100", "raw", "raw:50-100", "conflicts"); keeps `path` literal when filenames contain colons',
|
|
751
|
+
"Local path, internal URI (e.g. memory://, skill://), or URL. Inline selectors are supported.",
|
|
755
752
|
),
|
|
756
753
|
});
|
|
757
754
|
|
|
@@ -2122,11 +2119,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2122
2119
|
_toolContext?: AgentToolContext,
|
|
2123
2120
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2124
2121
|
let { path: readPath } = params;
|
|
2125
|
-
let explicitSelector = params.selector?.trim() || undefined;
|
|
2126
|
-
let explicitParsedSelector = explicitSelector === undefined ? undefined : parseSel(explicitSelector);
|
|
2127
|
-
if (explicitSelector !== undefined && explicitParsedSelector?.kind === "none") {
|
|
2128
|
-
throw invalidSelector(explicitSelector);
|
|
2129
|
-
}
|
|
2130
2122
|
if (readPath.startsWith("file://")) {
|
|
2131
2123
|
readPath = expandPath(readPath);
|
|
2132
2124
|
}
|
|
@@ -2147,13 +2139,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2147
2139
|
if (!this.session.settings.get("fetch.enabled")) {
|
|
2148
2140
|
throw new ToolError("URL reads are disabled by settings.");
|
|
2149
2141
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
}
|
|
2153
|
-
const urlRaw =
|
|
2154
|
-
explicitParsedSelector === undefined ? parsedUrlTarget.raw : isRawSelector(explicitParsedSelector);
|
|
2155
|
-
const urlRanges =
|
|
2156
|
-
explicitParsedSelector?.kind === "lines" ? explicitParsedSelector.ranges : parsedUrlTarget.ranges;
|
|
2142
|
+
const urlRaw = parsedUrlTarget.raw;
|
|
2143
|
+
const urlRanges = parsedUrlTarget.ranges;
|
|
2157
2144
|
if (urlRanges !== undefined && urlRanges.length > 1) {
|
|
2158
2145
|
const cached = await loadReadUrlCacheEntry(
|
|
2159
2146
|
this.session,
|
|
@@ -2169,14 +2156,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2169
2156
|
immutable: true,
|
|
2170
2157
|
});
|
|
2171
2158
|
}
|
|
2172
|
-
const
|
|
2173
|
-
const
|
|
2174
|
-
const urlLimit =
|
|
2175
|
-
explicitParsedSelector?.kind === "lines" && urlRange
|
|
2176
|
-
? urlRange.endLine !== undefined
|
|
2177
|
-
? urlRange.endLine - urlRange.startLine + 1
|
|
2178
|
-
: undefined
|
|
2179
|
-
: parsedUrlTarget.limit;
|
|
2159
|
+
const urlOffset = parsedUrlTarget.offset;
|
|
2160
|
+
const urlLimit = parsedUrlTarget.limit;
|
|
2180
2161
|
if (urlOffset !== undefined || urlLimit !== undefined) {
|
|
2181
2162
|
const cached = await loadReadUrlCacheEntry(
|
|
2182
2163
|
this.session,
|
|
@@ -2202,10 +2183,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2202
2183
|
// Use the internal-URL-aware splitter so malformed selectors are peeled
|
|
2203
2184
|
// off the URL and surfaced via parseSel rather than confusing handlers.
|
|
2204
2185
|
const internalRouter = InternalUrlRouter.instance();
|
|
2186
|
+
let promotedSelector: string | undefined;
|
|
2205
2187
|
if (internalRouter.canHandle(readPath)) {
|
|
2206
|
-
const internalTarget =
|
|
2207
|
-
|
|
2208
|
-
const parsed = explicitParsedSelector ?? parseSel(internalTarget.sel);
|
|
2188
|
+
const internalTarget = splitInternalUrlSel(readPath);
|
|
2189
|
+
const parsed = parseSel(internalTarget.sel);
|
|
2209
2190
|
if (internalTarget.sel !== undefined && parsed.kind === "none") {
|
|
2210
2191
|
throw new ToolError(
|
|
2211
2192
|
`Invalid selector ':${internalTarget.sel}' on '${internalTarget.path}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`,
|
|
@@ -2223,15 +2204,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2223
2204
|
});
|
|
2224
2205
|
if (localFile) {
|
|
2225
2206
|
readPath = localFile.path;
|
|
2226
|
-
//
|
|
2227
|
-
//
|
|
2228
|
-
|
|
2229
|
-
// would otherwise shadow the intended local:// URL selector semantics
|
|
2230
|
-
// (issue #4618 reviewer feedback on c493d12).
|
|
2231
|
-
if (explicitSelector === undefined && internalTarget.sel !== undefined) {
|
|
2232
|
-
explicitSelector = internalTarget.sel;
|
|
2233
|
-
explicitParsedSelector = parsed;
|
|
2234
|
-
}
|
|
2207
|
+
// Preserve a local:// selector separately so a sibling literal file
|
|
2208
|
+
// cannot shadow the URL's selector semantics during filesystem routing.
|
|
2209
|
+
promotedSelector = internalTarget.sel;
|
|
2235
2210
|
} else {
|
|
2236
2211
|
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2237
2212
|
}
|
|
@@ -2246,16 +2221,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2246
2221
|
|
|
2247
2222
|
// Prefer a literal filesystem match over selector interpretation so real
|
|
2248
2223
|
// POSIX filenames containing selector-looking suffixes win over structured
|
|
2249
|
-
// archive / sqlite / pdf-image dispatch.
|
|
2250
|
-
//
|
|
2251
|
-
// the literal file test:1-2", without recursively depending on whether a
|
|
2252
|
-
// longer `test:1-2:1-2` filename also exists (issue #4618).
|
|
2224
|
+
// archive / sqlite / pdf-image dispatch. A selector promoted from local://
|
|
2225
|
+
// remains separate so it cannot be mistaken for part of the resolved path.
|
|
2253
2226
|
const literalSplit =
|
|
2254
|
-
|
|
2227
|
+
promotedSelector === undefined
|
|
2255
2228
|
? await splitPathAndSelPreferringLiteral(readPath, this.session.cwd)
|
|
2256
|
-
: { path: readPath, sel:
|
|
2229
|
+
: { path: readPath, sel: promotedSelector };
|
|
2257
2230
|
const rawPathIsLiteral =
|
|
2258
|
-
|
|
2231
|
+
promotedSelector !== undefined
|
|
2259
2232
|
? readPath.includes(":") && (await probeLiteralPathExists(readPath, this.session.cwd)) !== "missing"
|
|
2260
2233
|
: literalSplit.sel === undefined && splitPathAndSel(readPath).sel !== undefined;
|
|
2261
2234
|
|
|
@@ -2263,9 +2236,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2263
2236
|
const archivePath = await this.#resolveArchiveReadPath(readPath, suffixCache, signal);
|
|
2264
2237
|
if (archivePath) {
|
|
2265
2238
|
const archiveSubPath =
|
|
2266
|
-
|
|
2239
|
+
promotedSelector === undefined
|
|
2267
2240
|
? splitPathAndSel(archivePath.archiveSubPath)
|
|
2268
|
-
: { path: archivePath.archiveSubPath, sel:
|
|
2241
|
+
: { path: archivePath.archiveSubPath, sel: promotedSelector };
|
|
2269
2242
|
const archiveParsed = parseSel(archiveSubPath.sel);
|
|
2270
2243
|
return this.#readArchive(
|
|
2271
2244
|
readPath,
|
|
@@ -3319,8 +3292,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3319
3292
|
interface ReadRenderArgs {
|
|
3320
3293
|
path?: unknown;
|
|
3321
3294
|
file_path?: unknown;
|
|
3322
|
-
selector?: unknown;
|
|
3323
|
-
sel?: string;
|
|
3324
3295
|
// Legacy fields from old schema — tolerated for in-flight tool calls during transition
|
|
3325
3296
|
offset?: number;
|
|
3326
3297
|
limit?: number;
|
|
@@ -3384,20 +3355,10 @@ function formatReadPathLink(
|
|
|
3384
3355
|
|
|
3385
3356
|
export const readToolRenderer = {
|
|
3386
3357
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
3387
|
-
const baseRawPath =
|
|
3388
|
-
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3389
|
-
const explicitSelector =
|
|
3390
|
-
typeof args.selector === "string"
|
|
3391
|
-
? args.selector.trim().replace(/^:+/, "")
|
|
3392
|
-
: args.sel?.trim().replace(/^:+/, "");
|
|
3393
3358
|
const rawPath =
|
|
3394
|
-
|
|
3395
|
-
if (isReadableUrlPath(
|
|
3396
|
-
return renderReadUrlCall(
|
|
3397
|
-
{ path: rawPath, raw: args.raw || explicitSelector?.toLowerCase() === "raw" },
|
|
3398
|
-
_options,
|
|
3399
|
-
uiTheme,
|
|
3400
|
-
);
|
|
3359
|
+
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3360
|
+
if (isReadableUrlPath(rawPath)) {
|
|
3361
|
+
return renderReadUrlCall({ path: rawPath, raw: args.raw }, _options, uiTheme);
|
|
3401
3362
|
}
|
|
3402
3363
|
|
|
3403
3364
|
const offset = args.offset;
|
|
@@ -3438,14 +3399,8 @@ export const readToolRenderer = {
|
|
|
3438
3399
|
if (result.isError) {
|
|
3439
3400
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
3440
3401
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
3441
|
-
const baseRawPath =
|
|
3442
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3443
|
-
const explicitSelector =
|
|
3444
|
-
typeof args?.selector === "string"
|
|
3445
|
-
? args.selector.trim().replace(/^:+/, "")
|
|
3446
|
-
: args?.sel?.trim().replace(/^:+/, "");
|
|
3447
3402
|
const rawPath =
|
|
3448
|
-
|
|
3403
|
+
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3449
3404
|
const filePath =
|
|
3450
3405
|
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
3451
3406
|
shortenPath(rawPath);
|
|
@@ -3472,14 +3427,8 @@ export const readToolRenderer = {
|
|
|
3472
3427
|
// echo next to the styled warning line below.
|
|
3473
3428
|
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
3474
3429
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
3475
|
-
const baseRawPath =
|
|
3476
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3477
|
-
const explicitSelector =
|
|
3478
|
-
typeof args?.selector === "string"
|
|
3479
|
-
? args.selector.trim().replace(/^:+/, "")
|
|
3480
|
-
: args?.sel?.trim().replace(/^:+/, "");
|
|
3481
3430
|
const rawPath =
|
|
3482
|
-
|
|
3431
|
+
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3483
3432
|
const renderPath = splitReadRenderPath(rawPath);
|
|
3484
3433
|
const lang = getLanguageFromPath(renderPath.path);
|
|
3485
3434
|
|
package/src/tools/write.ts
CHANGED
|
@@ -83,27 +83,96 @@ const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
|
83
83
|
const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
|
|
84
84
|
|
|
85
85
|
const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/;
|
|
86
|
+
/**
|
|
87
|
+
* The head of a per-id directive line — `<id>:` / `<id>=` (optionally `#`-prefixed),
|
|
88
|
+
* regardless of whether its value is a valid `@side` token. Used only to sharpen the
|
|
89
|
+
* error message when a directive block is malformed (e.g. `15: some literal text`).
|
|
90
|
+
*/
|
|
91
|
+
const BULK_DIRECTIVE_HEAD_RE = /^#?\d+\s*[:=]/;
|
|
92
|
+
|
|
93
|
+
function truncateDirectiveLine(line: string): string {
|
|
94
|
+
return line.length > 60 ? `${line.slice(0, 57)}…` : line;
|
|
95
|
+
}
|
|
86
96
|
|
|
87
97
|
/**
|
|
88
98
|
* Parse `conflict://*` per-id directive content: every non-empty line must be
|
|
89
|
-
* `<id>: @side` (also accepted: `#<id> = @side`)
|
|
90
|
-
*
|
|
91
|
-
*
|
|
99
|
+
* `<id>: @side` (also accepted: `#<id> = @side`), where `@side` is one of
|
|
100
|
+
* `@ours` / `@theirs` / `@base` / `@both`.
|
|
101
|
+
*
|
|
102
|
+
* Returns `null` only when NO line is directive-shaped (→ uniform bulk mode).
|
|
103
|
+
* Throws on duplicate ids, and — critically — on a *partial* directive block:
|
|
104
|
+
* content that mixes valid `<id>: @side` lines with lines that aren't. Without
|
|
105
|
+
* that guard a per-id write carrying any non-token value (a literal or
|
|
106
|
+
* multi-line replacement, e.g. `15: <multi-line content>`) fell through to
|
|
107
|
+
* uniform bulk mode, which pasted the raw directive text verbatim into every
|
|
108
|
+
* block and still reported success. Per-id bulk is token-only; literal or
|
|
109
|
+
* multi-line replacements must go through individual `conflict://<N>` writes.
|
|
92
110
|
*/
|
|
93
111
|
function parseBulkDirectives(content: string): Map<number, string> | null {
|
|
94
112
|
const map = new Map<number, string>();
|
|
113
|
+
const stray: string[] = [];
|
|
114
|
+
let sawDirective = false;
|
|
95
115
|
for (const raw of content.split("\n")) {
|
|
96
116
|
const line = raw.trim();
|
|
97
117
|
if (line.length === 0) continue;
|
|
98
118
|
const match = line.match(BULK_DIRECTIVE_RE);
|
|
99
|
-
if (!match)
|
|
119
|
+
if (!match) {
|
|
120
|
+
stray.push(line);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
sawDirective = true;
|
|
100
124
|
const id = Number.parseInt(match[1], 10);
|
|
101
125
|
if (map.has(id)) {
|
|
102
126
|
throw new ToolError(`Bulk directive lists conflict #${id} twice — each id may appear once.`);
|
|
103
127
|
}
|
|
104
128
|
map.set(id, match[2]);
|
|
105
129
|
}
|
|
106
|
-
|
|
130
|
+
// No directive lines at all → not a per-id block; caller uses uniform mode.
|
|
131
|
+
if (!sawDirective) return null;
|
|
132
|
+
if (stray.length > 0) {
|
|
133
|
+
const sample = stray[0]!;
|
|
134
|
+
const tokenHint = BULK_DIRECTIVE_HEAD_RE.test(sample)
|
|
135
|
+
? `Per-id bulk only accepts the tokens @ours/@theirs/@base/@both — one side per id, single line. `
|
|
136
|
+
: "";
|
|
137
|
+
throw new ToolError(
|
|
138
|
+
`Malformed \`conflict://*\` per-id block: ${stray.length} line(s) are not \`<id>: @side\` directives (first: \`${truncateDirectiveLine(sample)}\`). ` +
|
|
139
|
+
tokenHint +
|
|
140
|
+
`Literal or multi-line replacement content isn't supported in a per-id block — resolve those blocks with individual \`write({ path: "conflict://<N>", content })\` calls (you can issue several at once). ` +
|
|
141
|
+
`For a pure pick-a-side pass, make every non-empty line \`<id>: @ours\` (or @theirs/@base/@both).`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return map;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve per-id directives, preferring the pre-strip `raw` content and falling
|
|
149
|
+
* back to the hashline-stripped `stripped` content.
|
|
150
|
+
*
|
|
151
|
+
* Raw is preferred because the `<id>:` directive heads look exactly like
|
|
152
|
+
* hashline `LINE:` prefixes and would be eaten by stripping. When the two
|
|
153
|
+
* contents are identical (hashline mode off) a single parse decides everything,
|
|
154
|
+
* so a malformed-block error propagates straight through — the previous
|
|
155
|
+
* `?? parseBulkDirectives(...)` chain would have swallowed it and silently
|
|
156
|
+
* degraded to uniform bulk mode, pasting the raw directive text into every
|
|
157
|
+
* block. When they differ, a malformed raw block still defers to a *clean*
|
|
158
|
+
* stripped block, but otherwise surfaces its error rather than degrading.
|
|
159
|
+
*/
|
|
160
|
+
function resolveBulkDirectives(raw: string, stripped: string): Map<number, string> | null {
|
|
161
|
+
if (raw === stripped) return parseBulkDirectives(raw);
|
|
162
|
+
let rawResult: Map<number, string> | null;
|
|
163
|
+
try {
|
|
164
|
+
rawResult = parseBulkDirectives(raw);
|
|
165
|
+
} catch (rawError) {
|
|
166
|
+
let fallback: Map<number, string> | null = null;
|
|
167
|
+
try {
|
|
168
|
+
fallback = parseBulkDirectives(stripped);
|
|
169
|
+
} catch {
|
|
170
|
+
fallback = null;
|
|
171
|
+
}
|
|
172
|
+
if (fallback) return fallback;
|
|
173
|
+
throw rawError;
|
|
174
|
+
}
|
|
175
|
+
return rawResult ?? parseBulkDirectives(stripped);
|
|
107
176
|
}
|
|
108
177
|
|
|
109
178
|
const writeSchema = type({
|
|
@@ -716,7 +785,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
716
785
|
// winner — one call instead of one write per conflict. Parsed from the
|
|
717
786
|
// PRE-strip content: hashline prefix stripping would otherwise eat the
|
|
718
787
|
// `<id>: ` heads as echoed line numbers.
|
|
719
|
-
const directives =
|
|
788
|
+
const directives = resolveBulkDirectives(rawContent, replacementContent);
|
|
720
789
|
if (directives) {
|
|
721
790
|
const known = new Set(allEntries.map(entry => entry.id));
|
|
722
791
|
const unknown = [...directives.keys()].filter(id => !known.has(id));
|
|
@@ -898,9 +967,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
898
967
|
}
|
|
899
968
|
return { content: [{ type: "text", text: resultText }], details: {} };
|
|
900
969
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
970
|
+
if (scheme !== "local") {
|
|
971
|
+
throw new ToolError(
|
|
972
|
+
`${scheme}:// URLs are read-only for write; use the protocol-specific tool for mutations.`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
// local:// is backed by the session-local artifact sandbox and is
|
|
976
|
+
// resolved by resolvePlanPath below so write/read share the same root.
|
|
904
977
|
}
|
|
905
978
|
|
|
906
979
|
const conflictUri = parseConflictUri(path);
|
package/src/tools/yield.ts
CHANGED
|
@@ -198,6 +198,14 @@ function wrapYieldParameters(dataSchema: Record<string, unknown>): Record<string
|
|
|
198
198
|
*/
|
|
199
199
|
const MAX_SCHEMA_RETRIES = 3;
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Max consecutive untyped empty-result submissions before the yield tool fails
|
|
203
|
+
* the child explicitly. Some weak tool callers can acknowledge the required
|
|
204
|
+
* wrapper in prose while repeatedly sending `{ result: {} }`; without a hard
|
|
205
|
+
* stop the parent waits forever.
|
|
206
|
+
*/
|
|
207
|
+
const MAX_EMPTY_RESULT_RETRIES = 3;
|
|
208
|
+
|
|
201
209
|
export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
202
210
|
readonly name = "yield";
|
|
203
211
|
readonly approval = "read" as const;
|
|
@@ -217,6 +225,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
217
225
|
#knownSectionLabels: readonly string[] = [];
|
|
218
226
|
#isKnownSection?: (label: string) => boolean;
|
|
219
227
|
#schemaValidationFailures = 0;
|
|
228
|
+
#emptyResultFailures = 0;
|
|
220
229
|
|
|
221
230
|
constructor(session: ToolSession) {
|
|
222
231
|
let validate: ((value: unknown) => JsonSchemaValidationResult) | undefined;
|
|
@@ -322,8 +331,26 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
322
331
|
throw new Error("result cannot contain both data and error");
|
|
323
332
|
}
|
|
324
333
|
if (errorMessage === undefined && data === undefined && yieldType === undefined) {
|
|
334
|
+
this.#emptyResultFailures++;
|
|
335
|
+
if (this.#emptyResultFailures > MAX_EMPTY_RESULT_RETRIES) {
|
|
336
|
+
const attemptCount = this.#emptyResultFailures;
|
|
337
|
+
this.#emptyResultFailures = 0;
|
|
338
|
+
const error =
|
|
339
|
+
`yield result stayed empty after ${attemptCount} consecutive attempt(s); aborting child instead of retrying forever. ` +
|
|
340
|
+
'Submit success as `{ "result": { "data": <your output> } }` or failure as `{ "result": { "error": "message" } }`.';
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: `Task aborted: ${error}` }],
|
|
343
|
+
details: {
|
|
344
|
+
data: undefined,
|
|
345
|
+
status: "aborted",
|
|
346
|
+
error,
|
|
347
|
+
type: yieldType,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const remaining = MAX_EMPTY_RESULT_RETRIES - this.#emptyResultFailures;
|
|
325
352
|
throw new Error(
|
|
326
|
-
|
|
353
|
+
`result must contain either \`data\` or \`error\`. Use \`{result: {data: <your output>}}\` for success or \`{result: {error: "message"}}\` for failure. Empty untyped result retries remaining before abort: ${remaining}.`,
|
|
327
354
|
);
|
|
328
355
|
}
|
|
329
356
|
|
|
@@ -370,6 +397,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
370
397
|
}
|
|
371
398
|
}
|
|
372
399
|
|
|
400
|
+
this.#emptyResultFailures = 0;
|
|
373
401
|
const responseText =
|
|
374
402
|
status === "aborted"
|
|
375
403
|
? `Task aborted: ${errorMessage}`
|
|
@@ -40,6 +40,8 @@ const THINKING_TAG_ENVELOPE_RE = /<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>/
|
|
|
40
40
|
const THINKING_FENCE_ENVELOPE_RE = /```(?:thinking|reasoning)\b[\s\S]*?```/gi;
|
|
41
41
|
const LEADING_THINKING_TAG_RE = /^\s*<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>\s*/i;
|
|
42
42
|
const LEADING_THINKING_FENCE_RE = /^\s*```(?:thinking|reasoning)\b[\s\S]*?```\s*/i;
|
|
43
|
+
const LEADING_PROSE_THINKING_PREAMBLE_RE =
|
|
44
|
+
/^[ \t]*(?:(?:here(?:['’]s| is)[ \t]+(?:a|the|my)[ \t]+)|my[ \t]+)?(?:thinking|thought|reasoning)[ \t]+process[ \t]*:?[ \t]*(?:\r?\n|$)/i;
|
|
43
45
|
|
|
44
46
|
function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel?: Model<Api>): Model<Api> | undefined {
|
|
45
47
|
const availableModels = registry.getAvailable();
|
|
@@ -250,13 +252,15 @@ function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): stri
|
|
|
250
252
|
}
|
|
251
253
|
// Stay lenient: prefer the first closed title marker in visible text, then
|
|
252
254
|
// fall back to a plain sentence after stripping only known leading leaked
|
|
253
|
-
// thinking envelopes plus any stray/unclosed title tag fragment.
|
|
255
|
+
// thinking envelopes plus any stray/unclosed title tag fragment. Reject a
|
|
256
|
+
// prose thinking preamble only on the markerless path: a later marked title
|
|
257
|
+
// remains authoritative.
|
|
254
258
|
const markedTitle = extractVisibleMarkedTitle(textTitle);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
if (markedTitle !== undefined) return unwrapJsonTitle(markedTitle);
|
|
260
|
+
const cleanedTextTitle = stripLeadingLeakedThinkingMarkup(textTitle)
|
|
261
|
+
.replace(/<\/?title>/gi, "")
|
|
262
|
+
.trim();
|
|
263
|
+
if (LEADING_PROSE_THINKING_PREAMBLE_RE.test(cleanedTextTitle)) return "";
|
|
260
264
|
return unwrapJsonTitle(cleanedTextTitle);
|
|
261
265
|
}
|
|
262
266
|
|
package/src/web/search/index.ts
CHANGED
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
formatSearchProviderFailure,
|
|
22
22
|
formatSearchProviderFailures,
|
|
23
23
|
getSearchProvider,
|
|
24
|
-
|
|
24
|
+
getSearchProviderLabel,
|
|
25
|
+
resolveProviderCandidates,
|
|
25
26
|
type SearchProvider,
|
|
27
|
+
type SearchProviderCandidate,
|
|
26
28
|
} from "./provider";
|
|
27
29
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
28
30
|
import type { SearchProviderId, SearchResponse } from "./types";
|
|
@@ -128,25 +130,18 @@ async function executeSearch(
|
|
|
128
130
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
129
131
|
const { authStorage, sessionId, signal } = options;
|
|
130
132
|
const explicitProvider = params.provider;
|
|
131
|
-
let
|
|
133
|
+
let candidates: SearchProviderCandidate[];
|
|
132
134
|
if (explicitProvider && explicitProvider !== "auto") {
|
|
133
135
|
const provider = await getSearchProvider(explicitProvider);
|
|
134
|
-
|
|
135
|
-
? [
|
|
136
|
-
:
|
|
136
|
+
candidates = (await provider.isExplicitlyAvailable(authStorage))
|
|
137
|
+
? [{ id: explicitProvider, explicit: true }]
|
|
138
|
+
: resolveProviderCandidates("auto");
|
|
137
139
|
} else if (explicitProvider === "auto") {
|
|
138
140
|
// Explicit `--provider auto` bypasses the configured preferred provider
|
|
139
141
|
// for this invocation; exclusions still apply.
|
|
140
|
-
|
|
142
|
+
candidates = resolveProviderCandidates("auto");
|
|
141
143
|
} else {
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
if (providers.length === 0) {
|
|
145
|
-
const message = "No web search provider configured.";
|
|
146
|
-
return {
|
|
147
|
-
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
148
|
-
details: { response: { provider: "none", sources: [] }, error: message },
|
|
149
|
-
};
|
|
144
|
+
candidates = resolveProviderCandidates();
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
// Invariant across providers; read once and tolerate an uninitialized
|
|
@@ -166,11 +161,22 @@ async function executeSearch(
|
|
|
166
161
|
geminiModel = undefined;
|
|
167
162
|
}
|
|
168
163
|
|
|
169
|
-
const failures: Array<{ provider: SearchProvider
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
const failures: Array<{ provider: Pick<SearchProvider, "id" | "label">; error: unknown }> = [];
|
|
165
|
+
let availableProviderCount = 0;
|
|
166
|
+
let lastProvider: Pick<SearchProvider, "id" | "label"> | undefined;
|
|
167
|
+
for (const candidate of candidates) {
|
|
168
|
+
let provider: SearchProvider | undefined;
|
|
169
|
+
const providerMeta = { id: candidate.id, label: getSearchProviderLabel(candidate.id) };
|
|
170
|
+
lastProvider = providerMeta;
|
|
173
171
|
try {
|
|
172
|
+
provider = await getSearchProvider(candidate.id);
|
|
173
|
+
const available = candidate.explicit
|
|
174
|
+
? await provider.isExplicitlyAvailable(authStorage)
|
|
175
|
+
: await provider.isAvailable(authStorage);
|
|
176
|
+
if (!available) continue;
|
|
177
|
+
availableProviderCount++;
|
|
178
|
+
lastProvider = provider;
|
|
179
|
+
|
|
174
180
|
const response = await provider.search({
|
|
175
181
|
query: params.query,
|
|
176
182
|
limit: params.limit,
|
|
@@ -203,20 +209,31 @@ async function executeSearch(
|
|
|
203
209
|
// failure and the loop falls through to the next provider (or to the
|
|
204
210
|
// summary error), masking the cancellation.
|
|
205
211
|
throwIfAborted(signal);
|
|
206
|
-
failures.push({ provider, error });
|
|
212
|
+
failures.push({ provider: provider ?? providerMeta, error });
|
|
207
213
|
}
|
|
208
214
|
}
|
|
209
215
|
|
|
216
|
+
if (availableProviderCount === 0 && failures.length === 0) {
|
|
217
|
+
const message = "No web search provider configured.";
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
220
|
+
details: { response: { provider: "none", sources: [] }, error: message },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
const lastFailure = failures[failures.length - 1];
|
|
211
225
|
const baseMessage = lastFailure
|
|
212
226
|
? formatSearchProviderFailure(lastFailure.error, lastFailure.provider)
|
|
213
|
-
: `Unknown error from ${lastProvider
|
|
227
|
+
: `Unknown error from ${lastProvider?.label ?? "web search provider"}`;
|
|
214
228
|
const message =
|
|
215
|
-
|
|
229
|
+
failures.length > 1 ? `All web search providers failed: ${formatSearchProviderFailures(failures)}` : baseMessage;
|
|
216
230
|
|
|
217
231
|
return {
|
|
218
232
|
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
219
|
-
details: {
|
|
233
|
+
details: {
|
|
234
|
+
response: { provider: lastFailure?.provider.id ?? lastProvider?.id ?? "none", sources: [] },
|
|
235
|
+
error: message,
|
|
236
|
+
},
|
|
220
237
|
};
|
|
221
238
|
}
|
|
222
239
|
|
|
@@ -210,10 +210,34 @@ export function isSearchProviderExcluded(id: SearchProviderId): boolean {
|
|
|
210
210
|
return excludedProvIds.has(id);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
export interface SearchProviderCandidate {
|
|
214
|
+
id: SearchProviderId;
|
|
215
|
+
explicit: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Return provider candidates in fallback order without loading their modules. */
|
|
219
|
+
export function resolveProviderCandidates(
|
|
220
|
+
preferredProvider: SearchProviderId | "auto" = preferredProvId,
|
|
221
|
+
): SearchProviderCandidate[] {
|
|
222
|
+
const candidates: SearchProviderCandidate[] = [];
|
|
223
|
+
|
|
224
|
+
if (preferredProvider !== "auto" && !isSearchProviderExcluded(preferredProvider)) {
|
|
225
|
+
candidates.push({ id: preferredProvider, explicit: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const id of SEARCH_PROVIDER_ORDER) {
|
|
229
|
+
if (id === preferredProvider || isSearchProviderExcluded(id)) continue;
|
|
230
|
+
candidates.push({ id, explicit: false });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return candidates;
|
|
234
|
+
}
|
|
235
|
+
|
|
213
236
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
237
|
+
* Resolve the complete available provider chain.
|
|
238
|
+
*
|
|
239
|
+
* This compatibility helper loads every candidate. Search execution should use
|
|
240
|
+
* {@link resolveProviderCandidates} so fallback modules load only when reached.
|
|
217
241
|
*/
|
|
218
242
|
export async function resolveProviderChain(
|
|
219
243
|
authStorage: AuthStorage,
|
|
@@ -221,19 +245,12 @@ export async function resolveProviderChain(
|
|
|
221
245
|
): Promise<SearchProvider[]> {
|
|
222
246
|
const providers: SearchProvider[] = [];
|
|
223
247
|
|
|
224
|
-
|
|
225
|
-
const provider = await getSearchProvider(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const id of SEARCH_PROVIDER_ORDER) {
|
|
232
|
-
if (id === preferredProvider || isSearchProviderExcluded(id)) continue;
|
|
233
|
-
const provider = await getSearchProvider(id);
|
|
234
|
-
if (await provider.isAvailable(authStorage)) {
|
|
235
|
-
providers.push(provider);
|
|
236
|
-
}
|
|
248
|
+
for (const candidate of resolveProviderCandidates(preferredProvider)) {
|
|
249
|
+
const provider = await getSearchProvider(candidate.id);
|
|
250
|
+
const available = candidate.explicit
|
|
251
|
+
? await provider.isExplicitlyAvailable(authStorage)
|
|
252
|
+
: await provider.isAvailable(authStorage);
|
|
253
|
+
if (available) providers.push(provider);
|
|
237
254
|
}
|
|
238
255
|
|
|
239
256
|
return providers;
|