@oh-my-pi/pi-coding-agent 16.1.19 → 16.1.21

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +3795 -3760
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/cli/gallery-cli.d.ts +6 -0
  5. package/dist/types/commands/gallery.d.ts +1 -1
  6. package/dist/types/config/service-tier.d.ts +34 -0
  7. package/dist/types/config/settings-schema.d.ts +36 -33
  8. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  9. package/dist/types/mcp/oauth-flow.d.ts +42 -5
  10. package/dist/types/modes/components/custom-editor.d.ts +32 -0
  11. package/dist/types/modes/components/tool-execution.d.ts +5 -5
  12. package/dist/types/modes/controllers/event-controller.d.ts +10 -0
  13. package/dist/types/modes/controllers/input-controller.d.ts +2 -1
  14. package/dist/types/session/agent-session.d.ts +2 -0
  15. package/dist/types/task/executor.d.ts +9 -1
  16. package/dist/types/task/parallel.d.ts +17 -1
  17. package/dist/types/tools/index.d.ts +3 -1
  18. package/dist/types/utils/clipboard.d.ts +10 -0
  19. package/package.json +13 -13
  20. package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
  21. package/src/advisor/__tests__/advisor.test.ts +44 -0
  22. package/src/advisor/advise-tool.ts +33 -0
  23. package/src/autolearn/controller.ts +17 -2
  24. package/src/cli/gallery-cli.ts +31 -2
  25. package/src/cli/gallery-fixtures/agentic.ts +13 -4
  26. package/src/commands/gallery.ts +11 -3
  27. package/src/config/service-tier.ts +87 -0
  28. package/src/config/settings-schema.ts +48 -23
  29. package/src/eval/agent-bridge.ts +2 -0
  30. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
  31. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
  32. package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
  33. package/src/main.ts +1 -0
  34. package/src/mcp/manager.ts +12 -3
  35. package/src/mcp/oauth-flow.ts +121 -7
  36. package/src/mcp/transports/stdio.ts +5 -0
  37. package/src/modes/components/chat-transcript-builder.ts +31 -0
  38. package/src/modes/components/custom-editor.test.ts +80 -0
  39. package/src/modes/components/custom-editor.ts +86 -6
  40. package/src/modes/components/tool-execution.ts +50 -25
  41. package/src/modes/controllers/event-controller.ts +57 -8
  42. package/src/modes/controllers/input-controller.ts +213 -93
  43. package/src/modes/controllers/mcp-command-controller.ts +18 -2
  44. package/src/modes/utils/ui-helpers.ts +40 -0
  45. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  46. package/src/prompts/system/autolearn-nudge.md +4 -2
  47. package/src/prompts/tools/todo.md +1 -1
  48. package/src/sdk.ts +1 -0
  49. package/src/session/agent-session.ts +76 -15
  50. package/src/session/session-history-format.ts +21 -4
  51. package/src/task/executor.ts +79 -2
  52. package/src/task/index.ts +11 -6
  53. package/src/task/parallel.ts +59 -7
  54. package/src/tools/index.ts +3 -1
  55. package/src/tools/irc.ts +16 -2
  56. package/src/tools/todo.ts +20 -10
  57. package/src/utils/clipboard.ts +57 -0
  58. package/src/utils/shell-snapshot-fn-env.sh +60 -0
  59. package/src/utils/shell-snapshot.ts +77 -20
@@ -15,9 +15,11 @@ import type { Settings } from "../config/settings";
15
15
  import autolearnGuidance from "../prompts/system/autolearn-guidance.md" with { type: "text" };
16
16
  import autolearnGuidanceLearn from "../prompts/system/autolearn-guidance-learn.md" with { type: "text" };
17
17
  import autolearnNudge from "../prompts/system/autolearn-nudge.md" with { type: "text" };
18
+ import autolearnNudgeAutoContinue from "../prompts/system/autolearn-nudge-autocontinue.md" with { type: "text" };
18
19
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
19
20
 
20
- const AUTOLEARN_NUDGE = autolearnNudge.trim();
21
+ const AUTOLEARN_NUDGE_PASSIVE = autolearnNudge.trim();
22
+ const AUTOLEARN_NUDGE_AUTOCONTINUE = autolearnNudgeAutoContinue.trim();
21
23
  const DEFAULT_MIN_TOOL_CALLS = 5;
22
24
 
23
25
  /**
@@ -110,7 +112,20 @@ export class AutoLearnController {
110
112
 
111
113
  // Auto-run a capture turn only when explicitly enabled; otherwise the
112
114
  // hidden reminder rides the next real turn passively.
115
+ //
116
+ // The two paths get DIFFERENT nudge text:
117
+ // - Auto-continue spawns a synthetic turn with the nudge as its only
118
+ // user-role payload, so the prompt must be terminal — it tells the
119
+ // agent to capture and STOP, and must not be read as the user's
120
+ // reply to any pending question. Without that contract, the agent
121
+ // conflates the synthetic prompt with user approval and resumes
122
+ // prior work (e.g. commits/pushes an unanswered "want me to push?"
123
+ // question — #3504).
124
+ // - Passive mode appends the nudge to the user's real next message,
125
+ // so the agent must answer the user normally and treat the capture
126
+ // as additive — not as the whole turn's job.
113
127
  const autoContinue = this.#settings.get("autolearn.autoContinue") === true;
128
+ const content = autoContinue ? AUTOLEARN_NUDGE_AUTOCONTINUE : AUTOLEARN_NUDGE_PASSIVE;
114
129
  // Arm suppression synchronously: the synthetic capture turn's agent_end
115
130
  // fires inside sendCustomMessage (before it resolves), so the flag must be
116
131
  // set before then. Disarm when no turn actually started — a deferred/queued
@@ -122,7 +137,7 @@ export class AutoLearnController {
122
137
  .sendCustomMessage(
123
138
  {
124
139
  customType: "autolearn-nudge",
125
- content: AUTOLEARN_NUDGE,
140
+ content,
126
141
  display: false,
127
142
  attribution: "user",
128
143
  },
@@ -21,13 +21,42 @@ import { captureGalleryScreenshots } from "./gallery-screenshot";
21
21
  export const GALLERY_STATES = ["streaming", "progress", "success", "error"] as const;
22
22
  export type GalleryState = (typeof GALLERY_STATES)[number];
23
23
 
24
- const STATE_LABELS: Record<GalleryState, string> = {
24
+ /** User-facing labels printed above each rendered lifecycle state. */
25
+ export const GALLERY_STATE_LABELS: Record<GalleryState, string> = {
25
26
  streaming: "streaming args",
26
27
  progress: "in progress",
27
28
  success: "done",
28
29
  error: "failed",
29
30
  };
30
31
 
32
+ const GALLERY_STATE_ALIASES: Record<string, GalleryState> = {
33
+ streaming: "streaming",
34
+ "streaming args": "streaming",
35
+ progress: "progress",
36
+ "in progress": "progress",
37
+ success: "success",
38
+ done: "success",
39
+ error: "error",
40
+ failed: "error",
41
+ };
42
+
43
+ /** Accepted `--state` tokens, including legacy lifecycle names and displayed labels. */
44
+ export const GALLERY_STATE_TOKENS = Object.keys(GALLERY_STATE_ALIASES);
45
+
46
+ /** Normalize user-provided `--state` tokens to the internal gallery lifecycle states. */
47
+ export function parseGalleryStates(states: readonly string[] | undefined): GalleryState[] | undefined {
48
+ if (!states || states.length === 0) return undefined;
49
+ const parsed: GalleryState[] = [];
50
+ for (const raw of states) {
51
+ const state = GALLERY_STATE_ALIASES[raw.trim().toLowerCase()];
52
+ if (!state) {
53
+ throw new Error(`Invalid --state '${raw}'. Valid values: ${GALLERY_STATE_TOKENS.join(", ")}`);
54
+ }
55
+ if (!parsed.includes(state)) parsed.push(state);
56
+ }
57
+ return parsed;
58
+ }
59
+
31
60
  export interface GalleryCommandArgs {
32
61
  /** Render width in columns (defaults to terminal width, clamped). */
33
62
  width?: number;
@@ -166,7 +195,7 @@ async function renderGallerySections(
166
195
  const heading = fixture.label && fixture.label !== name ? `${name} — ${fixture.label}` : name;
167
196
  const lines: string[] = ["", sectionRule(heading, width)];
168
197
  for (const state of states) {
169
- lines.push("", theme.fg("dim", ` · ${STATE_LABELS[state]}`));
198
+ lines.push("", theme.fg("dim", ` · ${GALLERY_STATE_LABELS[state]}`));
170
199
  try {
171
200
  for (const line of await renderGalleryState(name, fixture, state, width, expanded)) lines.push(line);
172
201
  } catch (err) {
@@ -263,6 +263,11 @@ export const agenticFixtures: Record<string, GalleryFixture> = {
263
263
  ],
264
264
  } satisfies IrcDetails,
265
265
  },
266
+ errorResult: {
267
+ isError: true,
268
+ content: [{ type: "text", text: "IRC inbox failed: message store unavailable." }],
269
+ details: { op: "inbox" } satisfies IrcDetails,
270
+ },
266
271
  },
267
272
 
268
273
  irc_list: {
@@ -309,6 +314,11 @@ export const agenticFixtures: Record<string, GalleryFixture> = {
309
314
  ],
310
315
  } satisfies IrcDetails,
311
316
  },
317
+ errorResult: {
318
+ isError: true,
319
+ content: [{ type: "text", text: "IRC list failed: agent hub is unavailable." }],
320
+ details: { op: "list" } satisfies IrcDetails,
321
+ },
312
322
  },
313
323
 
314
324
  goal: {
@@ -388,19 +398,18 @@ export const agenticFixtures: Record<string, GalleryFixture> = {
388
398
  },
389
399
  errorResult: {
390
400
  isError: true,
391
- content: [{ type: "text", text: "Job cancelled by user." }],
401
+ content: [{ type: "text", text: "1 job failed." }],
392
402
  details: {
393
403
  jobs: [
394
404
  {
395
405
  id: "job_d4",
396
406
  type: "task",
397
- status: "cancelled",
407
+ status: "failed",
398
408
  label: "Refactor the session store to Redis",
399
409
  durationMs: 52_300,
400
- errorText: "Aborted: superseded by goal re-scope.",
410
+ errorText: "Subagent exited 1: Redis connection string is missing.",
401
411
  },
402
412
  ],
403
- cancelled: [{ id: "job_d4", status: "cancelled" }],
404
413
  },
405
414
  },
406
415
  },
@@ -2,7 +2,7 @@
2
2
  * Render every built-in tool's renderer across its lifecycle states.
3
3
  */
4
4
  import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
- import { GALLERY_STATES, type GalleryState, runGalleryCommand } from "../cli/gallery-cli";
5
+ import { GALLERY_STATE_TOKENS, type GalleryState, parseGalleryStates, runGalleryCommand } from "../cli/gallery-cli";
6
6
 
7
7
  export default class Gallery extends Command {
8
8
  static description = "Preview tool renderers across streaming, in-progress, success, and failure states";
@@ -12,7 +12,7 @@ export default class Gallery extends Command {
12
12
  state: Flags.string({
13
13
  char: "s",
14
14
  description: "Render only the given lifecycle state(s)",
15
- options: [...GALLERY_STATES],
15
+ options: GALLERY_STATE_TOKENS,
16
16
  multiple: true,
17
17
  }),
18
18
  width: Flags.integer({ char: "w", description: "Render width in columns" }),
@@ -37,9 +37,17 @@ export default class Gallery extends Command {
37
37
 
38
38
  async run(): Promise<void> {
39
39
  const { flags } = await this.parse(Gallery);
40
+ let states: GalleryState[] | undefined;
41
+ try {
42
+ states = parseGalleryStates(flags.state);
43
+ } catch (err) {
44
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
45
+ process.exitCode = 1;
46
+ return;
47
+ }
40
48
  await runGalleryCommand({
41
49
  tool: flags.tool,
42
- states: flags.state as GalleryState[] | undefined,
50
+ states,
43
51
  width: flags.width,
44
52
  expanded: flags.expanded,
45
53
  plain: flags.plain,
@@ -0,0 +1,87 @@
1
+ import type { ServiceTier } from "@oh-my-pi/pi-ai";
2
+ import type { SubmenuOption } from "./settings-schema";
3
+
4
+ /**
5
+ * Service-tier setting values shared by every "Service Tier" setting. `"none"`
6
+ * is the omit-the-parameter sentinel; the remaining values mirror
7
+ * {@link ServiceTier}.
8
+ */
9
+ export const SERVICE_TIER_SETTING_VALUES = [
10
+ "none",
11
+ "auto",
12
+ "default",
13
+ "flex",
14
+ "scale",
15
+ "priority",
16
+ "openai-only",
17
+ "claude-only",
18
+ ] as const;
19
+
20
+ export type ServiceTierSettingValue = (typeof SERVICE_TIER_SETTING_VALUES)[number];
21
+
22
+ /** Variant value set for scoped service-tier settings (subagent/advisor) that can defer to the main agent. */
23
+ export const SERVICE_TIER_INHERIT_SETTING_VALUES = ["inherit", ...SERVICE_TIER_SETTING_VALUES] as const;
24
+
25
+ export type ServiceTierInheritSettingValue = (typeof SERVICE_TIER_INHERIT_SETTING_VALUES)[number];
26
+
27
+ /** Submenu descriptions shared by the base `serviceTier` setting. */
28
+ export const SERVICE_TIER_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierSettingValue>> = [
29
+ { value: "none", label: "None", description: "Omit service_tier parameter" },
30
+ { value: "auto", label: "Auto", description: "Use provider default tier selection (OpenAI)" },
31
+ { value: "default", label: "Default", description: "Standard priority processing (OpenAI)" },
32
+ { value: "flex", label: "Flex", description: "Flexible capacity tier when available (OpenAI)" },
33
+ { value: "scale", label: "Scale", description: "Scale Tier credits when available (OpenAI)" },
34
+ {
35
+ value: "priority",
36
+ label: "Priority",
37
+ description: "Priority on every supported provider (OpenAI `service_tier`, Anthropic fast mode)",
38
+ },
39
+ {
40
+ value: "openai-only",
41
+ label: "Priority (OpenAI only)",
42
+ description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere",
43
+ },
44
+ {
45
+ value: "claude-only",
46
+ label: "Priority (Claude only)",
47
+ description: "Anthropic fast mode on direct Claude requests; ignored elsewhere (incl. Bedrock/Vertex)",
48
+ },
49
+ ];
50
+
51
+ /** Submenu descriptions for inherit-capable service-tier settings. */
52
+ export const SERVICE_TIER_INHERIT_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierInheritSettingValue>> = [
53
+ { value: "inherit", label: "Inherit", description: "Use the main agent's Service Tier" },
54
+ ...SERVICE_TIER_OPTIONS,
55
+ ];
56
+
57
+ /**
58
+ * Resolve a service-tier setting value to the wire {@link ServiceTier} (or
59
+ * `undefined` to omit). `"inherit"` defers to `inherited`; `"none"` omits.
60
+ */
61
+ export function resolveServiceTierSetting(value: string, inherited: ServiceTier | undefined): ServiceTier | undefined {
62
+ if (value === "inherit") return inherited;
63
+ if (value === "none" || value === "") return undefined;
64
+ return value as ServiceTier;
65
+ }
66
+
67
+ /**
68
+ * Resolve the `serviceTier` *setting value* to stamp onto a subagent's settings
69
+ * snapshot.
70
+ *
71
+ * - A concrete `subagentSetting` (`"none"` or a tier) wins outright.
72
+ * - `"inherit"` defers to the parent's live effective tier when the caller has a
73
+ * live session (`inherited` passed as `ServiceTier | null`, where `null` means
74
+ * the parent explicitly has no tier — e.g. `/fast off`). When no live session
75
+ * is available (`inherited === undefined`, e.g. cold subagent revive) it falls
76
+ * back to the parent's configured `serviceTier` setting so behavior matches a
77
+ * plain settings snapshot.
78
+ */
79
+ export function resolveSubagentServiceTier(
80
+ subagentSetting: string,
81
+ configuredTier: ServiceTierSettingValue,
82
+ inherited: ServiceTier | null | undefined,
83
+ ): ServiceTierSettingValue {
84
+ if (subagentSetting !== "inherit") return subagentSetting as ServiceTierSettingValue;
85
+ if (inherited === undefined) return configuredTier;
86
+ return inherited ?? "none";
87
+ }
@@ -35,6 +35,12 @@ import {
35
35
  } from "../tts/models";
36
36
  import { EDIT_MODES } from "../utils/edit-mode";
37
37
  import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES, type SearchProviderId } from "../web/search/types";
38
+ import {
39
+ SERVICE_TIER_INHERIT_OPTIONS,
40
+ SERVICE_TIER_INHERIT_SETTING_VALUES,
41
+ SERVICE_TIER_OPTIONS,
42
+ SERVICE_TIER_SETTING_VALUES,
43
+ } from "./service-tier";
38
44
 
39
45
  /** Unified settings schema - single source of truth for all settings.
40
46
  *
@@ -1122,7 +1128,7 @@ export const SETTINGS_SCHEMA = {
1122
1128
 
1123
1129
  serviceTier: {
1124
1130
  type: "enum",
1125
- values: ["none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"] as const,
1131
+ values: SERVICE_TIER_SETTING_VALUES,
1126
1132
  default: "none",
1127
1133
  ui: {
1128
1134
  tab: "model",
@@ -1130,28 +1136,36 @@ export const SETTINGS_SCHEMA = {
1130
1136
  label: "Service Tier",
1131
1137
  description:
1132
1138
  'Processing priority hint (none = omit). OpenAI accepts the tier values directly; Anthropic realizes `priority` as `speed: "fast"` on supported Opus models. Scoped values target one family.',
1133
- options: [
1134
- { value: "none", label: "None", description: "Omit service_tier parameter" },
1135
- { value: "auto", label: "Auto", description: "Use provider default tier selection (OpenAI)" },
1136
- { value: "default", label: "Default", description: "Standard priority processing (OpenAI)" },
1137
- { value: "flex", label: "Flex", description: "Flexible capacity tier when available (OpenAI)" },
1138
- { value: "scale", label: "Scale", description: "Scale Tier credits when available (OpenAI)" },
1139
- {
1140
- value: "priority",
1141
- label: "Priority",
1142
- description: "Priority on every supported provider (OpenAI `service_tier`, Anthropic fast mode)",
1143
- },
1144
- {
1145
- value: "openai-only",
1146
- label: "Priority (OpenAI only)",
1147
- description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere",
1148
- },
1149
- {
1150
- value: "claude-only",
1151
- label: "Priority (Claude only)",
1152
- description: "Anthropic fast mode on direct Claude requests; ignored elsewhere (incl. Bedrock/Vertex)",
1153
- },
1154
- ],
1139
+ options: SERVICE_TIER_OPTIONS,
1140
+ },
1141
+ },
1142
+
1143
+ serviceTierSubagent: {
1144
+ type: "enum",
1145
+ values: SERVICE_TIER_INHERIT_SETTING_VALUES,
1146
+ default: "inherit",
1147
+ ui: {
1148
+ tab: "model",
1149
+ group: "Sampling",
1150
+ label: "Service Tier - Subagent",
1151
+ description:
1152
+ "Service Tier for spawned task/eval subagents. Inherit = match the main agent's live tier (tracks /fast); pick a value to scope subagents independently.",
1153
+ options: SERVICE_TIER_INHERIT_OPTIONS,
1154
+ },
1155
+ },
1156
+
1157
+ serviceTierAdvisor: {
1158
+ type: "enum",
1159
+ values: SERVICE_TIER_INHERIT_SETTING_VALUES,
1160
+ default: "none",
1161
+ ui: {
1162
+ tab: "model",
1163
+ group: "Sampling",
1164
+ label: "Service Tier - Advisor",
1165
+ description:
1166
+ "Service Tier for the advisor model. None = standard processing; Inherit = match the main agent's live tier; pick a value (e.g. Priority) to run the advisor on a faster serving path.",
1167
+ options: SERVICE_TIER_INHERIT_OPTIONS,
1168
+ condition: "advisorEnabled",
1155
1169
  },
1156
1170
  },
1157
1171
 
@@ -4041,6 +4055,17 @@ export const SETTINGS_SCHEMA = {
4041
4055
  },
4042
4056
 
4043
4057
  // Provider selection
4058
+ "providers.ollama-cloud.maxConcurrency": {
4059
+ type: "number",
4060
+ default: 3,
4061
+ ui: {
4062
+ tab: "providers",
4063
+ group: "Services",
4064
+ label: "Ollama Cloud Max Concurrency",
4065
+ description:
4066
+ "Maximum concurrent Ollama Cloud subagent runs per process; 0 disables the provider-specific limit",
4067
+ },
4068
+ },
4044
4069
  "providers.webSearch": {
4045
4070
  type: "enum",
4046
4071
  values: SEARCH_PROVIDER_PREFERENCES,
@@ -397,6 +397,8 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
397
397
  parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
398
398
  parentTelemetry: options.session.getTelemetry?.(),
399
399
  parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
400
+ // Live source of truth for `serviceTierSubagent: inherit` (null = explicit none).
401
+ parentServiceTier: options.session.getServiceTier ? (options.session.getServiceTier() ?? null) : undefined,
400
402
  // Deliberately omit parentEvalSessionId: the parent's Python kernel is
401
403
  // blocked on this bridge call, so sharing the eval session would deadlock
402
404
  // (subagent queues behind the parent's in-flight execution, parent waits
@@ -360,6 +360,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
360
360
  "@oh-my-pi/pi-coding-agent/config/models-config",
361
361
  "@oh-my-pi/pi-coding-agent/config/prompt-templates",
362
362
  "@oh-my-pi/pi-coding-agent/config/resolve-config-value",
363
+ "@oh-my-pi/pi-coding-agent/config/service-tier",
363
364
  "@oh-my-pi/pi-coding-agent/config/settings-schema",
364
365
  "@oh-my-pi/pi-coding-agent/config/settings",
365
366
  "@oh-my-pi/pi-coding-agent/dap/client",
@@ -483,7 +484,6 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
483
484
  "@oh-my-pi/pi-coding-agent/eval/js/executor",
484
485
  "@oh-my-pi/pi-coding-agent/eval/js/tool-bridge",
485
486
  "@oh-my-pi/pi-coding-agent/eval/js/worker-core",
486
- "@oh-my-pi/pi-coding-agent/eval/js/worker-entry",
487
487
  "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol",
488
488
  "@oh-my-pi/pi-coding-agent/eval/py/display",
489
489
  "@oh-my-pi/pi-coding-agent/eval/py/executor",
@@ -328,6 +328,7 @@ import * as bundledPiCodingAgentConfigModelsConfig from "@oh-my-pi/pi-coding-age
328
328
  import * as bundledPiCodingAgentConfigModelsConfigSchema from "@oh-my-pi/pi-coding-agent/config/models-config-schema";
329
329
  import * as bundledPiCodingAgentConfigPromptTemplates from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
330
330
  import * as bundledPiCodingAgentConfigResolveConfigValue from "@oh-my-pi/pi-coding-agent/config/resolve-config-value";
331
+ import * as bundledPiCodingAgentConfigServiceTier from "@oh-my-pi/pi-coding-agent/config/service-tier";
331
332
  import * as bundledPiCodingAgentConfigSettings from "@oh-my-pi/pi-coding-agent/config/settings";
332
333
  import * as bundledPiCodingAgentConfigSettingsSchema from "@oh-my-pi/pi-coding-agent/config/settings-schema";
333
334
  import * as bundledPiCodingAgentDap from "@oh-my-pi/pi-coding-agent/dap";
@@ -385,7 +386,6 @@ import * as bundledPiCodingAgentEvalJsContextManager from "@oh-my-pi/pi-coding-a
385
386
  import * as bundledPiCodingAgentEvalJsExecutor from "@oh-my-pi/pi-coding-agent/eval/js/executor";
386
387
  import * as bundledPiCodingAgentEvalJsToolBridge from "@oh-my-pi/pi-coding-agent/eval/js/tool-bridge";
387
388
  import * as bundledPiCodingAgentEvalJsWorkerCore from "@oh-my-pi/pi-coding-agent/eval/js/worker-core";
388
- import * as bundledPiCodingAgentEvalJsWorkerEntry from "@oh-my-pi/pi-coding-agent/eval/js/worker-entry";
389
389
  import * as bundledPiCodingAgentEvalJsWorkerProtocol from "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol";
390
390
  import * as bundledPiCodingAgentEvalPyDisplay from "@oh-my-pi/pi-coding-agent/eval/py/display";
391
391
  import * as bundledPiCodingAgentEvalPyExecutor from "@oh-my-pi/pi-coding-agent/eval/py/executor";
@@ -1802,6 +1802,9 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
1802
1802
  bundledPiCodingAgentConfigPromptTemplates as unknown as Readonly<Record<string, unknown>>,
1803
1803
  "@oh-my-pi/pi-coding-agent/config/resolve-config-value":
1804
1804
  bundledPiCodingAgentConfigResolveConfigValue as unknown as Readonly<Record<string, unknown>>,
1805
+ "@oh-my-pi/pi-coding-agent/config/service-tier": bundledPiCodingAgentConfigServiceTier as unknown as Readonly<
1806
+ Record<string, unknown>
1807
+ >,
1805
1808
  "@oh-my-pi/pi-coding-agent/config/settings-schema": bundledPiCodingAgentConfigSettingsSchema as unknown as Readonly<
1806
1809
  Record<string, unknown>
1807
1810
  >,
@@ -2100,9 +2103,6 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
2100
2103
  "@oh-my-pi/pi-coding-agent/eval/js/worker-core": bundledPiCodingAgentEvalJsWorkerCore as unknown as Readonly<
2101
2104
  Record<string, unknown>
2102
2105
  >,
2103
- "@oh-my-pi/pi-coding-agent/eval/js/worker-entry": bundledPiCodingAgentEvalJsWorkerEntry as unknown as Readonly<
2104
- Record<string, unknown>
2105
- >,
2106
2106
  "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol": bundledPiCodingAgentEvalJsWorkerProtocol as unknown as Readonly<
2107
2107
  Record<string, unknown>
2108
2108
  >,
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { isBuiltin } from "node:module";
2
3
  import * as path from "node:path";
3
4
  import * as url from "node:url";
4
5
  import { isCompiledBinary } from "@oh-my-pi/pi-utils";
@@ -456,10 +457,11 @@ const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'
456
457
 
457
458
  /**
458
459
  * Rewrite the extension-owned specifiers OMP must host-resolve — legacy
459
- * `@(scope)/pi-*`, bare TypeBox packages, and package `imports` aliases like
460
- * `#src/*` — to absolute `file://` URLs. Every other specifier (relative
461
- * siblings and third-party dependencies) is left untouched so Bun resolves it
462
- * natively from the extension's real on-disk location.
460
+ * `@(scope)/pi-*`, bare TypeBox packages, package `imports` aliases like
461
+ * `#src/*`, and extension-local bare dependencies — to absolute `file://` URLs
462
+ * or compiled-mode virtual specifiers. Relative siblings and built-in modules
463
+ * are left untouched so Bun resolves them from the extension's real on-disk
464
+ * location.
463
465
  */
464
466
  async function rewriteLegacyExtensionSource(source: string, importerPath: string): Promise<string> {
465
467
  const withPi = rewriteLegacyPiImports(source);
@@ -474,7 +476,12 @@ async function rewriteLegacyExtensionSource(source: string, importerPath: string
474
476
  `${prefix}${toImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`,
475
477
  )
476
478
  : withPi;
477
- return rewriteExtensionPackageImports(withTypeBox, importerPath);
479
+ return rewriteExtensionBareImports(await rewriteExtensionPackageImports(withTypeBox, importerPath), importerPath);
480
+ }
481
+
482
+ /** Test seam for compiled-binary legacy extension source rewriting. */
483
+ export async function __rewriteLegacyExtensionSourceForTests(source: string, importerPath: string): Promise<string> {
484
+ return rewriteLegacyExtensionSource(source, importerPath);
478
485
  }
479
486
 
480
487
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -688,6 +695,187 @@ async function rewriteExtensionPackageImports(source: string, importerPath: stri
688
695
  return `${rewritten}${source.slice(lastIndex)}`;
689
696
  }
690
697
 
698
+ const BARE_EXTENSION_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])([^"'()\s]+)(["'])/g;
699
+
700
+ function isBareExtensionDependencySpecifier(specifier: string): boolean {
701
+ if (
702
+ specifier.startsWith(".") ||
703
+ specifier.startsWith("/") ||
704
+ specifier.startsWith("#") ||
705
+ specifier.startsWith("node:") ||
706
+ specifier.startsWith("bun:") ||
707
+ /^[a-z][a-z0-9+.-]*:/i.test(specifier)
708
+ ) {
709
+ return false;
710
+ }
711
+ const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
712
+ return Boolean(packageName && !isBuiltin(packageName));
713
+ }
714
+
715
+ interface BarePackageSpecifier {
716
+ readonly name: string;
717
+ readonly subpath: string | null;
718
+ }
719
+
720
+ function splitBarePackageSpecifier(specifier: string): BarePackageSpecifier | null {
721
+ const parts = specifier.split("/");
722
+ if (specifier.startsWith("@")) {
723
+ const [scope, name, ...rest] = parts;
724
+ if (!scope || !name) return null;
725
+ return { name: `${scope}/${name}`, subpath: rest.length > 0 ? rest.join("/") : null };
726
+ }
727
+ const [name, ...rest] = parts;
728
+ if (!name) return null;
729
+ return { name, subpath: rest.length > 0 ? rest.join("/") : null };
730
+ }
731
+
732
+ async function findNodePackageRoot(packageName: string, importerPath: string): Promise<string | null> {
733
+ let dir = path.dirname(importerPath);
734
+ while (true) {
735
+ const candidate = path.join(dir, "node_modules", packageName);
736
+ if (await pathExists(path.join(candidate, "package.json"))) {
737
+ return candidate;
738
+ }
739
+ const parent = path.dirname(dir);
740
+ if (parent === dir) {
741
+ return null;
742
+ }
743
+ dir = parent;
744
+ }
745
+ }
746
+
747
+ async function readPackageManifest(packageRoot: string): Promise<Record<string, unknown> | null> {
748
+ try {
749
+ const manifest = await Bun.file(path.join(packageRoot, "package.json")).json();
750
+ return isRecord(manifest) ? manifest : null;
751
+ } catch {
752
+ return null;
753
+ }
754
+ }
755
+
756
+ async function resolvePackageExportTarget(
757
+ packageRoot: string,
758
+ target: string,
759
+ wildcard: string | null,
760
+ ): Promise<string | null> {
761
+ if (!target.startsWith("./")) {
762
+ return null;
763
+ }
764
+ const substituted = wildcard === null ? target : target.replaceAll("*", wildcard);
765
+ return resolveSourceModuleFile(path.resolve(packageRoot, substituted));
766
+ }
767
+
768
+ async function resolveNodePackageExport(
769
+ packageRoot: string,
770
+ subpath: string | null,
771
+ manifest: Record<string, unknown>,
772
+ ): Promise<string | null> {
773
+ const exportsField = manifest.exports;
774
+ const rootTarget = subpath === null ? selectPackageImportTarget(exportsField) : null;
775
+ if (rootTarget !== null && rootTarget !== PACKAGE_IMPORT_EXCLUDED) {
776
+ return resolvePackageExportTarget(packageRoot, rootTarget, null);
777
+ }
778
+ if (!isRecord(exportsField)) {
779
+ return null;
780
+ }
781
+
782
+ const exactKey = subpath === null ? "." : `./${subpath}`;
783
+ const exactTarget = selectPackageImportTarget(exportsField[exactKey]);
784
+ if (exactTarget !== null && exactTarget !== PACKAGE_IMPORT_EXCLUDED) {
785
+ return resolvePackageExportTarget(packageRoot, exactTarget, null);
786
+ }
787
+
788
+ for (const [key, entry] of Object.entries(exportsField)) {
789
+ const starIndex = key.indexOf("*");
790
+ if (starIndex === -1 || subpath === null) continue;
791
+ const prefix = key.slice(2, starIndex);
792
+ const suffix = key.slice(starIndex + 1);
793
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) {
794
+ continue;
795
+ }
796
+ const target = selectPackageImportTarget(entry);
797
+ if (target === null || target === PACKAGE_IMPORT_EXCLUDED) {
798
+ continue;
799
+ }
800
+ return resolvePackageExportTarget(
801
+ packageRoot,
802
+ target,
803
+ subpath.slice(prefix.length, subpath.length - suffix.length),
804
+ );
805
+ }
806
+ return null;
807
+ }
808
+
809
+ async function resolveNodePackageFallback(
810
+ packageRoot: string,
811
+ subpath: string | null,
812
+ manifest: Record<string, unknown>,
813
+ ): Promise<string | null> {
814
+ if (subpath !== null) {
815
+ return resolveSourceModuleFile(path.join(packageRoot, subpath));
816
+ }
817
+ for (const field of ["module", "main"]) {
818
+ const target = manifest[field];
819
+ if (typeof target === "string") {
820
+ const resolved = await resolveSourceModuleFile(path.resolve(packageRoot, target));
821
+ if (resolved) return resolved;
822
+ }
823
+ }
824
+ return resolveSourceModuleFile(path.join(packageRoot, "index"));
825
+ }
826
+
827
+ async function resolveNodePackageDependency(specifier: string, importerPath: string): Promise<string | null> {
828
+ const parsed = splitBarePackageSpecifier(specifier);
829
+ if (!parsed) return null;
830
+ const packageRoot = await findNodePackageRoot(parsed.name, importerPath);
831
+ if (!packageRoot) return null;
832
+ const manifest = await readPackageManifest(packageRoot);
833
+ if (!manifest) return null;
834
+ return (
835
+ (await resolveNodePackageExport(packageRoot, parsed.subpath, manifest)) ??
836
+ (await resolveNodePackageFallback(packageRoot, parsed.subpath, manifest))
837
+ );
838
+ }
839
+
840
+ async function resolveExtensionBareDependency(specifier: string, importerPath: string): Promise<string | null> {
841
+ if (!isBareExtensionDependencySpecifier(specifier)) {
842
+ return null;
843
+ }
844
+ try {
845
+ const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
846
+ if (resolved && resolved !== specifier && !resolved.startsWith("node:") && !resolved.startsWith("bun:")) {
847
+ return resolved;
848
+ }
849
+ } catch {
850
+ // Compiled binaries do not reliably resolve runtime extension node_modules.
851
+ }
852
+ return resolveNodePackageDependency(specifier, importerPath);
853
+ }
854
+
855
+ async function rewriteExtensionBareImports(source: string, importerPath: string): Promise<string> {
856
+ let rewritten = "";
857
+ let lastIndex = 0;
858
+ for (const match of source.matchAll(BARE_EXTENSION_IMPORT_SPECIFIER_REGEX)) {
859
+ const matchIndex = match.index;
860
+ if (matchIndex === undefined) continue;
861
+
862
+ const [fullMatch, prefix, specifier, suffix] = match;
863
+ if (!prefix || !specifier || !suffix) continue;
864
+
865
+ const resolved = await resolveExtensionBareDependency(specifier, importerPath);
866
+ if (!resolved) continue;
867
+
868
+ rewritten += source.slice(lastIndex, matchIndex);
869
+ rewritten += `${prefix}${toImportSpecifier(resolved)}${suffix}`;
870
+ lastIndex = matchIndex + fullMatch.length;
871
+ }
872
+
873
+ if (lastIndex === 0) {
874
+ return source;
875
+ }
876
+ return `${rewritten}${source.slice(lastIndex)}`;
877
+ }
878
+
691
879
  function escapeRegExp(value: string): string {
692
880
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
693
881
  }
package/src/main.ts CHANGED
@@ -140,6 +140,7 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
140
140
  "advisor.subagents",
141
141
  "advisor.syncBacklog",
142
142
  "advisor.immuneTurns",
143
+ "serviceTierAdvisor",
143
144
  ];
144
145
 
145
146
  const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [