@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
|
@@ -11,9 +11,9 @@ interface ConfigSchemaError {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Module-private cache of
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Module-private cache of JSON → YAML migrations this process already ran.
|
|
15
|
+
* Prevents `ConfigFile.relocate()` / repeated `tryLoad()` calls from re-running
|
|
16
|
+
* the migration over and over on the boot path.
|
|
17
17
|
*/
|
|
18
18
|
const migratedPaths = new Set<string>();
|
|
19
19
|
|
|
@@ -123,6 +123,7 @@ export type LoadResult<T> =
|
|
|
123
123
|
|
|
124
124
|
export class ConfigFile<T> implements IConfigFile<T> {
|
|
125
125
|
readonly #basePath: string;
|
|
126
|
+
readonly #yamlFallbackPath: string | null;
|
|
126
127
|
readonly #jsonMigrationPath: string | null;
|
|
127
128
|
#cache?: LoadResult<T>;
|
|
128
129
|
#auxValidate?: (value: T) => void;
|
|
@@ -134,13 +135,17 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
134
135
|
) {
|
|
135
136
|
this.#basePath = configPath;
|
|
136
137
|
if (configPath.endsWith(".yml")) {
|
|
138
|
+
this.#yamlFallbackPath = `${configPath.slice(0, -4)}.yaml`;
|
|
137
139
|
this.#jsonMigrationPath = `${configPath.slice(0, -4)}.json`;
|
|
138
140
|
} else if (configPath.endsWith(".yaml")) {
|
|
141
|
+
this.#yamlFallbackPath = null;
|
|
139
142
|
this.#jsonMigrationPath = `${configPath.slice(0, -5)}.json`;
|
|
140
143
|
} else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
|
|
144
|
+
this.#yamlFallbackPath = null;
|
|
141
145
|
// JSON configs are still supported without migration.
|
|
142
146
|
this.#jsonMigrationPath = null;
|
|
143
147
|
} else {
|
|
148
|
+
this.#yamlFallbackPath = null;
|
|
144
149
|
throw new Error(`Invalid config file path: ${configPath}`);
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -150,9 +155,11 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
150
155
|
* Sync callers (tests, settings init) hit this implicitly via {@link tryLoad}.
|
|
151
156
|
*/
|
|
152
157
|
#ensureMigrated(): void {
|
|
153
|
-
if (this.#jsonMigrationPath)
|
|
154
|
-
|
|
158
|
+
if (!this.#jsonMigrationPath) return;
|
|
159
|
+
if (this.#yamlFallbackPath && !fs.existsSync(this.#basePath) && fs.existsSync(this.#yamlFallbackPath)) {
|
|
160
|
+
return;
|
|
155
161
|
}
|
|
162
|
+
migrateJsonToYml(this.#jsonMigrationPath, this.#basePath);
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
relocate(configPath?: string): ConfigFile<T> {
|
|
@@ -163,9 +170,19 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
163
170
|
return result;
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
#resolveReadPath(): string {
|
|
174
|
+
if (fs.existsSync(this.#basePath)) {
|
|
175
|
+
return this.#basePath;
|
|
176
|
+
}
|
|
177
|
+
if (this.#yamlFallbackPath && fs.existsSync(this.#yamlFallbackPath)) {
|
|
178
|
+
return this.#yamlFallbackPath;
|
|
179
|
+
}
|
|
180
|
+
return this.#basePath;
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
getMtimeMs(): number | null {
|
|
167
184
|
try {
|
|
168
|
-
return fs.statSync(this
|
|
185
|
+
return fs.statSync(this.#resolveReadPath()).mtimeMs;
|
|
169
186
|
} catch (err) {
|
|
170
187
|
if (isEnoent(err)) return null;
|
|
171
188
|
throw err;
|
|
@@ -211,12 +228,13 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
211
228
|
#parseContent(content: string): LoadResult<T> {
|
|
212
229
|
try {
|
|
213
230
|
let parsed: unknown;
|
|
214
|
-
|
|
231
|
+
const readPath = this.#resolveReadPath();
|
|
232
|
+
if (readPath.endsWith(".json") || readPath.endsWith(".jsonc")) {
|
|
215
233
|
parsed = JSONC.parse(content);
|
|
216
|
-
} else if (
|
|
234
|
+
} else if (readPath.endsWith(".yml") || readPath.endsWith(".yaml")) {
|
|
217
235
|
parsed = YAML.parse(content);
|
|
218
236
|
} else {
|
|
219
|
-
throw new Error(`Invalid config file path: ${
|
|
237
|
+
throw new Error(`Invalid config file path: ${readPath}`);
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
const checked = this.schema(parsed);
|
|
@@ -255,7 +273,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
255
273
|
|
|
256
274
|
let content: string;
|
|
257
275
|
try {
|
|
258
|
-
content = fs.readFileSync(this
|
|
276
|
+
content = fs.readFileSync(this.#resolveReadPath(), "utf-8").trim();
|
|
259
277
|
} catch (error) {
|
|
260
278
|
if (isEnoent(error)) {
|
|
261
279
|
return this.#storeCache({ status: "not-found" });
|
|
@@ -275,7 +293,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
275
293
|
|
|
276
294
|
let content: string;
|
|
277
295
|
try {
|
|
278
|
-
content = (await Bun.file(this
|
|
296
|
+
content = (await Bun.file(this.#resolveReadPath()).text()).trim();
|
|
279
297
|
} catch (error) {
|
|
280
298
|
if (isEnoent(error)) {
|
|
281
299
|
return this.#storeCache({ status: "not-found" });
|
|
@@ -306,7 +324,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
306
324
|
}
|
|
307
325
|
|
|
308
326
|
path(): string {
|
|
309
|
-
return this.#
|
|
327
|
+
return this.#resolveReadPath();
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
invalidate() {
|
|
@@ -258,7 +258,7 @@ export interface ProviderDiscoveryState {
|
|
|
258
258
|
error?: string;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
/** Result of loading custom models
|
|
261
|
+
/** Result of loading custom models config. */
|
|
262
262
|
interface CustomModelsResult {
|
|
263
263
|
models?: CustomModelOverlay[];
|
|
264
264
|
overrides?: Map<string, ProviderOverride>;
|
|
@@ -309,7 +309,7 @@ interface CommandApiKeyResolution {
|
|
|
309
309
|
value?: string;
|
|
310
310
|
}
|
|
311
311
|
/**
|
|
312
|
-
* Resolve a models.yml secret/config value to an actual value.
|
|
312
|
+
* Resolve a models.yml/models.yaml secret/config value to an actual value.
|
|
313
313
|
* `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
|
|
314
314
|
* checked first and the input falls back to a literal value.
|
|
315
315
|
*/
|
|
@@ -822,7 +822,7 @@ export class ModelRegistry {
|
|
|
822
822
|
}
|
|
823
823
|
|
|
824
824
|
/**
|
|
825
|
-
* Reload models from disk (built-in + custom
|
|
825
|
+
* Reload models from disk (built-in + custom config).
|
|
826
826
|
*/
|
|
827
827
|
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
|
828
828
|
this.#reloadStaticModels();
|
|
@@ -938,7 +938,7 @@ export class ModelRegistry {
|
|
|
938
938
|
#reloadStaticModels(): void {
|
|
939
939
|
const currentMtime = this.#modelsConfigFile.getMtimeMs();
|
|
940
940
|
if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) {
|
|
941
|
-
//
|
|
941
|
+
// Models config unchanged since last load; reloading would be redundant.
|
|
942
942
|
return;
|
|
943
943
|
}
|
|
944
944
|
this.#modelsConfigFile.invalidate();
|
|
@@ -962,14 +962,14 @@ export class ModelRegistry {
|
|
|
962
962
|
}
|
|
963
963
|
|
|
964
964
|
/**
|
|
965
|
-
* Get any error from loading models
|
|
965
|
+
* Get any error from loading custom models config (undefined if no error).
|
|
966
966
|
*/
|
|
967
967
|
getError(): ConfigError | undefined {
|
|
968
968
|
return this.#configError;
|
|
969
969
|
}
|
|
970
970
|
|
|
971
971
|
#loadModels() {
|
|
972
|
-
// Load custom
|
|
972
|
+
// Load custom config first (to know which providers to override).
|
|
973
973
|
const {
|
|
974
974
|
models: customModels = [],
|
|
975
975
|
overrides = new Map(),
|
|
@@ -1909,7 +1909,7 @@ export class ModelRegistry {
|
|
|
1909
1909
|
|
|
1910
1910
|
/**
|
|
1911
1911
|
* Get all models (built-in + custom).
|
|
1912
|
-
* If
|
|
1912
|
+
* If custom config had errors, returns only built-in models.
|
|
1913
1913
|
*/
|
|
1914
1914
|
getAll(): Model<Api>[] {
|
|
1915
1915
|
return this.#models;
|
|
@@ -1209,20 +1209,27 @@ export function resolveModelOverride(
|
|
|
1209
1209
|
modelPatterns: string[],
|
|
1210
1210
|
modelRegistry: ModelLookupRegistry,
|
|
1211
1211
|
settings?: Settings,
|
|
1212
|
-
): { model?: Model<Api>; thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean } {
|
|
1212
|
+
): { model?: Model<Api>; thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean; warning?: string } {
|
|
1213
1213
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
1214
1214
|
const availableModels = modelRegistry.getAvailable();
|
|
1215
1215
|
const matchPreferences = getModelMatchPreferences(settings);
|
|
1216
|
+
let warning: string | undefined;
|
|
1216
1217
|
for (const pattern of modelPatterns) {
|
|
1217
|
-
const {
|
|
1218
|
+
const {
|
|
1219
|
+
model,
|
|
1220
|
+
thinkingLevel,
|
|
1221
|
+
explicitThinkingLevel,
|
|
1222
|
+
warning: patternWarning,
|
|
1223
|
+
} = resolveModelRoleValue(pattern, availableModels, {
|
|
1218
1224
|
settings,
|
|
1219
1225
|
matchPreferences,
|
|
1220
1226
|
});
|
|
1221
1227
|
if (model) {
|
|
1222
|
-
return { model, thinkingLevel, explicitThinkingLevel };
|
|
1228
|
+
return { model, thinkingLevel, explicitThinkingLevel, warning: patternWarning };
|
|
1223
1229
|
}
|
|
1230
|
+
if (!warning && patternWarning) warning = patternWarning;
|
|
1224
1231
|
}
|
|
1225
|
-
return { explicitThinkingLevel: false };
|
|
1232
|
+
return { explicitThinkingLevel: false, warning };
|
|
1226
1233
|
}
|
|
1227
1234
|
|
|
1228
1235
|
/**
|
|
@@ -1236,6 +1243,11 @@ export function resolveModelOverride(
|
|
|
1236
1243
|
* `modelRoles.task` pointing at an unqualified id whose only available
|
|
1237
1244
|
* provider variant has no configured credentials — see #985).
|
|
1238
1245
|
*
|
|
1246
|
+
* `sessionId` is forwarded to `getApiKey` so that session-sticky OAuth
|
|
1247
|
+
* credentials resolve correctly during the pre-flight auth check. Without it,
|
|
1248
|
+
* providers with multiple OAuth accounts may return `undefined` even though
|
|
1249
|
+
* the credential is usable once the subagent session starts — see #5325.
|
|
1250
|
+
*
|
|
1239
1251
|
* Keyless-by-design providers (llama.cpp, ollama, lm-studio) advertise the
|
|
1240
1252
|
* `kNoAuth` sentinel from `getApiKey` to signal that they do not require
|
|
1241
1253
|
* credentials. Those are treated as authenticated here so an explicitly
|
|
@@ -1251,18 +1263,20 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1251
1263
|
parentActiveModelPattern: string | undefined,
|
|
1252
1264
|
modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
|
|
1253
1265
|
settings?: Settings,
|
|
1266
|
+
sessionId?: string,
|
|
1254
1267
|
): Promise<{
|
|
1255
1268
|
model?: Model<Api>;
|
|
1256
1269
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
1257
1270
|
explicitThinkingLevel: boolean;
|
|
1258
1271
|
authFallbackUsed: boolean;
|
|
1272
|
+
warning?: string;
|
|
1259
1273
|
}> {
|
|
1260
1274
|
const primary = resolveModelOverride(modelPatterns, modelRegistry, settings);
|
|
1261
1275
|
if (!primary.model || !parentActiveModelPattern) {
|
|
1262
1276
|
return { ...primary, authFallbackUsed: false };
|
|
1263
1277
|
}
|
|
1264
1278
|
|
|
1265
|
-
const primaryKey = await modelRegistry.getApiKey(primary.model);
|
|
1279
|
+
const primaryKey = await modelRegistry.getApiKey(primary.model, sessionId);
|
|
1266
1280
|
if (primaryKey === kNoAuth || isAuthenticated(primaryKey)) {
|
|
1267
1281
|
return { ...primary, authFallbackUsed: false };
|
|
1268
1282
|
}
|
|
@@ -1274,12 +1288,12 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1274
1288
|
if (modelsAreEqual(fallback.model, primary.model)) {
|
|
1275
1289
|
return { ...primary, authFallbackUsed: false };
|
|
1276
1290
|
}
|
|
1277
|
-
const fallbackKey = await modelRegistry.getApiKey(fallback.model);
|
|
1291
|
+
const fallbackKey = await modelRegistry.getApiKey(fallback.model, sessionId);
|
|
1278
1292
|
if (!isAuthenticated(fallbackKey)) {
|
|
1279
1293
|
return { ...primary, authFallbackUsed: false };
|
|
1280
1294
|
}
|
|
1281
1295
|
|
|
1282
|
-
return { ...fallback, authFallbackUsed: true };
|
|
1296
|
+
return { ...fallback, authFallbackUsed: true, warning: primary.warning ?? fallback.warning };
|
|
1283
1297
|
}
|
|
1284
1298
|
|
|
1285
1299
|
/**
|
|
@@ -3592,6 +3592,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
3592
3592
|
description: "Enable the tts tool for on-device (Kokoro) or xAI Grok Voice speech-file synthesis",
|
|
3593
3593
|
},
|
|
3594
3594
|
},
|
|
3595
|
+
"generate_image.enabled": {
|
|
3596
|
+
type: "boolean",
|
|
3597
|
+
default: true,
|
|
3598
|
+
ui: {
|
|
3599
|
+
tab: "tools",
|
|
3600
|
+
group: "Available Tools",
|
|
3601
|
+
label: "Generate Image",
|
|
3602
|
+
description: "Enable the generate_image tool for text-to-image generation and editing",
|
|
3603
|
+
},
|
|
3604
|
+
},
|
|
3595
3605
|
|
|
3596
3606
|
"inspect_image.enabled": {
|
|
3597
3607
|
type: "boolean",
|
package/src/cursor.ts
CHANGED
|
@@ -93,6 +93,7 @@ async function executeTool(
|
|
|
93
93
|
result = buildToolErrorResult(message);
|
|
94
94
|
isError = true;
|
|
95
95
|
}
|
|
96
|
+
isError ||= result.isError === true;
|
|
96
97
|
|
|
97
98
|
const sanitizedFinalResult: AgentToolResult<unknown> = {
|
|
98
99
|
content: result.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)),
|
|
@@ -283,6 +284,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
283
284
|
result = buildToolErrorResult(message);
|
|
284
285
|
isError = true;
|
|
285
286
|
}
|
|
287
|
+
isError ||= result.isError === true;
|
|
286
288
|
|
|
287
289
|
// onUpdate may not fire for every chunk — flush any remaining output
|
|
288
290
|
// from the final result that wasn't already streamed.
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
scanSkillsFromDir,
|
|
25
25
|
} from "./helpers";
|
|
26
26
|
|
|
27
|
-
import { substitutePluginRoot } from "./substitute-plugin-root";
|
|
27
|
+
import { resolvePluginStdioPaths, substitutePluginRoot } from "./substitute-plugin-root";
|
|
28
28
|
|
|
29
29
|
const PROVIDER_ID = "claude-plugins";
|
|
30
30
|
const DISPLAY_NAME = "Claude Code Marketplace";
|
|
@@ -441,14 +441,20 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
441
441
|
continue;
|
|
442
442
|
}
|
|
443
443
|
const namespacedName = root.plugin ? `${root.plugin}:${serverName}` : serverName;
|
|
444
|
+
const substitutedCommand =
|
|
445
|
+
raw.command !== undefined ? substitutePluginRoot(raw.command, root.path) : undefined;
|
|
446
|
+
const substitutedCwd = raw.cwd !== undefined ? substitutePluginRoot(raw.cwd, root.path) : undefined;
|
|
447
|
+
// Root relative command/cwd at the plugin's config directory, not the
|
|
448
|
+
// session cwd (MCP stdio spawning resolves relative values there).
|
|
449
|
+
const rooted = resolvePluginStdioPaths({ command: substitutedCommand, cwd: substitutedCwd }, root.path);
|
|
444
450
|
const server: MCPServer = {
|
|
445
451
|
name: namespacedName,
|
|
446
452
|
...(raw.enabled !== undefined && { enabled: raw.enabled }),
|
|
447
453
|
...(raw.timeout !== undefined && { timeout: raw.timeout }),
|
|
448
|
-
...(
|
|
454
|
+
...(rooted.command !== undefined && { command: rooted.command }),
|
|
449
455
|
...(raw.args !== undefined && { args: substitutePluginRoot(raw.args, root.path) }),
|
|
450
456
|
...(raw.env !== undefined && { env: substitutePluginRoot(raw.env, root.path) }),
|
|
451
|
-
...(
|
|
457
|
+
...(rooted.cwd !== undefined && { cwd: rooted.cwd }),
|
|
452
458
|
...(raw.url !== undefined && { url: expandEnvVarsDeep(raw.url) }),
|
|
453
459
|
...(raw.headers !== undefined && { headers: expandEnvVarsDeep(raw.headers) }),
|
|
454
460
|
...(raw.auth !== undefined && { auth: raw.auth }),
|
|
@@ -30,6 +30,7 @@ import { type CustomTool, toolCapability } from "../capability/tool";
|
|
|
30
30
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
31
31
|
import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
|
|
32
32
|
import { listOmpExtensionRoots, type OmpExtensionRoot } from "./omp-extension-roots";
|
|
33
|
+
import { resolvePluginStdioPaths } from "./substitute-plugin-root";
|
|
33
34
|
|
|
34
35
|
const PROVIDER_ID = "omp-plugins";
|
|
35
36
|
const DISPLAY_NAME = "OMP Extension Packages";
|
|
@@ -301,14 +302,17 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
301
302
|
warnings.push(`[omp-plugins] Skipping MCP server "${serverName}" in ${mcpPath}: missing command or url`);
|
|
302
303
|
continue;
|
|
303
304
|
}
|
|
305
|
+
// Root relative command/cwd at the plugin's config directory, not the
|
|
306
|
+
// session cwd (MCP stdio spawning resolves relative values there).
|
|
307
|
+
const rooted = resolvePluginStdioPaths({ command: cfg.command, cwd: cfg.cwd }, root.path);
|
|
304
308
|
items.push({
|
|
305
309
|
name: serverName,
|
|
306
310
|
...(cfg.enabled !== undefined && { enabled: cfg.enabled }),
|
|
307
311
|
...(cfg.timeout !== undefined && { timeout: cfg.timeout }),
|
|
308
|
-
...(
|
|
312
|
+
...(rooted.command !== undefined && { command: rooted.command }),
|
|
309
313
|
...(cfg.args !== undefined && { args: cfg.args }),
|
|
310
314
|
...(cfg.env !== undefined && { env: cfg.env }),
|
|
311
|
-
...(
|
|
315
|
+
...(rooted.cwd !== undefined && { cwd: rooted.cwd }),
|
|
312
316
|
...(cfg.url !== undefined && { url: cfg.url }),
|
|
313
317
|
...(cfg.headers !== undefined && { headers: cfg.headers }),
|
|
314
318
|
...(cfg.auth !== undefined && { auth: cfg.auth }),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Recursively substitute ${CLAUDE_PLUGIN_ROOT} and ${OMP_PLUGIN_ROOT}
|
|
3
5
|
* with the actual plugin root path in strings, arrays, and plain objects.
|
|
@@ -27,3 +29,33 @@ export function substitutePluginRoot<T>(value: T, rootPath: string): T {
|
|
|
27
29
|
}
|
|
28
30
|
return value;
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rebase relative filesystem values in a discovered plugin stdio config against
|
|
35
|
+
* the directory of the `.mcp.json` that declared them.
|
|
36
|
+
*
|
|
37
|
+
* External plugin configs (bundled ChatGPT/Codex plugins, Claude marketplace
|
|
38
|
+
* plugins) express `command`/`cwd` relative to their own config file, but MCP
|
|
39
|
+
* stdio spawning roots relative values at the session cwd — so a plugin shipping
|
|
40
|
+
* `command: "./bin/server"`, `cwd: "."` launches from the wrong directory and
|
|
41
|
+
* fails with ENOENT. This resolves those against `configDir` instead:
|
|
42
|
+
*
|
|
43
|
+
* - relative `cwd` → resolved against `configDir`;
|
|
44
|
+
* - path-like `command` (`./`, `../`, or the Windows `.\`/`..\` forms) →
|
|
45
|
+
* resolved against `configDir`;
|
|
46
|
+
* - bare executables (`npx`, `uvx`, …) and absolute paths are left untouched.
|
|
47
|
+
*/
|
|
48
|
+
export function resolvePluginStdioPaths(
|
|
49
|
+
config: { command?: string; cwd?: string },
|
|
50
|
+
configDir: string,
|
|
51
|
+
): { command?: string; cwd?: string } {
|
|
52
|
+
const resolved: { command?: string; cwd?: string } = {};
|
|
53
|
+
if (typeof config.cwd === "string") {
|
|
54
|
+
resolved.cwd = path.isAbsolute(config.cwd) ? config.cwd : path.resolve(configDir, config.cwd);
|
|
55
|
+
}
|
|
56
|
+
if (config.command !== undefined) {
|
|
57
|
+
const isPathLike = /^\.\.?[/\\]/.test(config.command);
|
|
58
|
+
resolved.command = isPathLike ? path.resolve(configDir, config.command) : config.command;
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
@@ -603,21 +603,22 @@ describe("agent() through eval runtimes", () => {
|
|
|
603
603
|
});
|
|
604
604
|
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent-interrupt", settings);
|
|
605
605
|
mockAgents();
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
// unwind via KeyboardInterrupt instead of being hard-killed (which used to
|
|
611
|
-
// surface "[kernel] Python kernel shutdown" and lose all session state).
|
|
606
|
+
// Each kernel worker thread blocks in a synchronous `urllib` bridge call,
|
|
607
|
+
// joined by `parallel()`'s ThreadPoolExecutor exit. The host must keep
|
|
608
|
+
// those already-started calls attached until they settle, then interrupt
|
|
609
|
+
// the kernel before `parallel()` launches another wave.
|
|
612
610
|
let inFlight = 0;
|
|
611
|
+
let completed = 0;
|
|
613
612
|
let markSaturated: (() => void) | undefined;
|
|
614
613
|
const saturated = new Promise<void>(resolve => {
|
|
615
614
|
markSaturated = resolve;
|
|
616
615
|
});
|
|
617
|
-
|
|
616
|
+
const releaseAgents = Promise.withResolvers<void>();
|
|
617
|
+
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
618
618
|
// task.maxConcurrency=6 → six bridge calls block at once; signal then.
|
|
619
619
|
if (++inFlight >= 6) markSaturated?.();
|
|
620
|
-
await
|
|
620
|
+
await releaseAgents.promise;
|
|
621
|
+
completed++;
|
|
621
622
|
return singleResult(options, { output: options.assignment ?? "" });
|
|
622
623
|
});
|
|
623
624
|
|
|
@@ -640,8 +641,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
640
641
|
// bridge calls (condition-driven) instead of waiting a fixed wall second.
|
|
641
642
|
void saturated.then(() => ac.abort(new Error("external interrupt")));
|
|
642
643
|
|
|
643
|
-
const
|
|
644
|
-
const result = await executePython(
|
|
644
|
+
const resultPromise = executePython(
|
|
645
645
|
"import json\nprint(json.dumps(parallel([lambda n=n: agent(str(n)) for n in range(12)])))",
|
|
646
646
|
{
|
|
647
647
|
cwd: tempDir.path(),
|
|
@@ -653,13 +653,18 @@ describe("agent() through eval runtimes", () => {
|
|
|
653
653
|
signal: ac.signal,
|
|
654
654
|
},
|
|
655
655
|
);
|
|
656
|
-
|
|
656
|
+
await saturated;
|
|
657
|
+
await Promise.resolve();
|
|
658
|
+
expect(completed).toBe(0);
|
|
659
|
+
releaseAgents.resolve();
|
|
660
|
+
const result = await resultPromise;
|
|
657
661
|
|
|
658
|
-
// Cancelled, but cleanly: no hard-kill,
|
|
659
|
-
//
|
|
662
|
+
// Cancelled, but cleanly: no hard-kill, no orphaned bridge calls, and no
|
|
663
|
+
// second fan-out wave started after the deferred abort was delivered.
|
|
660
664
|
expect(result.cancelled).toBe(true);
|
|
661
665
|
expect(result.output).not.toContain("Python kernel shutdown");
|
|
662
|
-
expect(
|
|
666
|
+
expect(completed).toBe(6);
|
|
667
|
+
expect(runSpy).toHaveBeenCalledTimes(6);
|
|
663
668
|
|
|
664
669
|
// The persistent kernel survived the interrupt: prior state is intact.
|
|
665
670
|
const after = await executePython("print(PREP_MARKER)", {
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
isEvalTimeoutControlEvent,
|
|
6
6
|
withBridgeTimeoutPause,
|
|
7
7
|
} from "../bridge-timeout";
|
|
8
|
+
import { executeWithKernelBase, type GenericKernel } from "../executor-base";
|
|
8
9
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
10
|
+
import type { KernelDisplayOutput } from "../py/display";
|
|
9
11
|
|
|
10
12
|
describe("withBridgeTimeoutPause", () => {
|
|
11
13
|
it("emits one pause before the operation and one resume after it settles", async () => {
|
|
@@ -17,10 +19,12 @@ describe("withBridgeTimeoutPause", () => {
|
|
|
17
19
|
await Bun.sleep(80);
|
|
18
20
|
return "done";
|
|
19
21
|
},
|
|
22
|
+
{ deferExternalAbort: true },
|
|
20
23
|
);
|
|
21
24
|
|
|
22
25
|
expect(value).toBe("done");
|
|
23
26
|
expect(events.map(event => event.op)).toEqual([EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP]);
|
|
27
|
+
expect(events.every(event => event.deferExternalAbort === true)).toBe(true);
|
|
24
28
|
|
|
25
29
|
const settledCount = events.length;
|
|
26
30
|
await Bun.sleep(40);
|
|
@@ -62,3 +66,105 @@ describe("withBridgeTimeoutPause", () => {
|
|
|
62
66
|
expect(isEvalTimeoutControlEvent({ op: "agent", id: "subagent-1" })).toBe(false);
|
|
63
67
|
});
|
|
64
68
|
});
|
|
69
|
+
|
|
70
|
+
class TestCancelledError extends Error {
|
|
71
|
+
readonly timedOut: boolean;
|
|
72
|
+
|
|
73
|
+
constructor(timedOut: boolean) {
|
|
74
|
+
super(timedOut ? "timed out" : "cancelled");
|
|
75
|
+
this.name = "TestCancelledError";
|
|
76
|
+
this.timedOut = timedOut;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it("defers external aborts until an in-flight agent bridge call resumes", async () => {
|
|
81
|
+
const abortController = new AbortController();
|
|
82
|
+
const entered = Promise.withResolvers<void>();
|
|
83
|
+
const triggerAbort = Promise.withResolvers<void>();
|
|
84
|
+
const observed = Promise.withResolvers<boolean>();
|
|
85
|
+
const release = Promise.withResolvers<void>();
|
|
86
|
+
const kernel: GenericKernel<Record<string, string | null>> = {
|
|
87
|
+
async execute(_code, options) {
|
|
88
|
+
entered.resolve();
|
|
89
|
+
await triggerAbort.promise;
|
|
90
|
+
options.onDisplay({
|
|
91
|
+
type: "status",
|
|
92
|
+
event: { op: EVAL_TIMEOUT_PAUSE_OP, deferExternalAbort: true },
|
|
93
|
+
} satisfies KernelDisplayOutput);
|
|
94
|
+
abortController.abort(new Error("external interrupt"));
|
|
95
|
+
observed.resolve(options.signal?.aborted ?? false);
|
|
96
|
+
await release.promise;
|
|
97
|
+
options.onDisplay({
|
|
98
|
+
type: "status",
|
|
99
|
+
event: { op: EVAL_TIMEOUT_RESUME_OP, deferExternalAbort: true },
|
|
100
|
+
} satisfies KernelDisplayOutput);
|
|
101
|
+
return { status: "ok", cancelled: false, timedOut: false };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const resultPromise = executeWithKernelBase({
|
|
106
|
+
kernel,
|
|
107
|
+
code: "agent('slow')",
|
|
108
|
+
options: { signal: abortController.signal },
|
|
109
|
+
runIdPrefix: "test",
|
|
110
|
+
errorLogLabel: "test",
|
|
111
|
+
cancelledErrorClass: TestCancelledError,
|
|
112
|
+
buildKernelEnvPatch: () => ({}),
|
|
113
|
+
formatKernelTimeoutAnnotation: () => "kernel timed out",
|
|
114
|
+
formatTimeoutAnnotation: () => "timed out",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await entered.promise;
|
|
118
|
+
triggerAbort.resolve();
|
|
119
|
+
expect(await observed.promise).toBe(false);
|
|
120
|
+
release.resolve();
|
|
121
|
+
const result = await resultPromise;
|
|
122
|
+
expect(result.cancelled).toBe(true);
|
|
123
|
+
expect(result.exitCode).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not defer external aborts for a completion bridge call", async () => {
|
|
127
|
+
const abortController = new AbortController();
|
|
128
|
+
const entered = Promise.withResolvers<void>();
|
|
129
|
+
const triggerAbort = Promise.withResolvers<void>();
|
|
130
|
+
const observed = Promise.withResolvers<boolean>();
|
|
131
|
+
const release = Promise.withResolvers<void>();
|
|
132
|
+
const kernel: GenericKernel<Record<string, string | null>> = {
|
|
133
|
+
async execute(_code, options) {
|
|
134
|
+
entered.resolve();
|
|
135
|
+
await triggerAbort.promise;
|
|
136
|
+
options.onDisplay({
|
|
137
|
+
type: "status",
|
|
138
|
+
event: { op: EVAL_TIMEOUT_PAUSE_OP },
|
|
139
|
+
} satisfies KernelDisplayOutput);
|
|
140
|
+
abortController.abort(new Error("external interrupt"));
|
|
141
|
+
observed.resolve(options.signal?.aborted ?? false);
|
|
142
|
+
await release.promise;
|
|
143
|
+
options.onDisplay({
|
|
144
|
+
type: "status",
|
|
145
|
+
event: { op: EVAL_TIMEOUT_RESUME_OP },
|
|
146
|
+
} satisfies KernelDisplayOutput);
|
|
147
|
+
return { status: "ok", cancelled: false, timedOut: false };
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const resultPromise = executeWithKernelBase({
|
|
152
|
+
kernel,
|
|
153
|
+
code: "completion('slow')",
|
|
154
|
+
options: { signal: abortController.signal },
|
|
155
|
+
runIdPrefix: "test",
|
|
156
|
+
errorLogLabel: "test",
|
|
157
|
+
cancelledErrorClass: TestCancelledError,
|
|
158
|
+
buildKernelEnvPatch: () => ({}),
|
|
159
|
+
formatKernelTimeoutAnnotation: () => "kernel timed out",
|
|
160
|
+
formatTimeoutAnnotation: () => "timed out",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await entered.promise;
|
|
164
|
+
triggerAbort.resolve();
|
|
165
|
+
expect(await observed.promise).toBe(true);
|
|
166
|
+
release.resolve();
|
|
167
|
+
const result = await resultPromise;
|
|
168
|
+
expect(result.cancelled).toBe(true);
|
|
169
|
+
expect(result.exitCode).toBeUndefined();
|
|
170
|
+
});
|