@hk_net/pi-advisor 0.1.3 → 0.1.5

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 - 2026-07-20
4
+
5
+ ### Changed
6
+
7
+ - Correct and clarify the current Pi compatibility and autocomplete documentation.
8
+
9
+ ## 0.1.4 - 2026-07-20
10
+
11
+ ### Changed
12
+
13
+ - Derive advisor thinking-level choices from Pi's supported levels for the selected reviewer model.
14
+ - Add `max` support where the selected reviewer model exposes it.
15
+ - Improve `/advisor` autocomplete: `/adviso...` completes without a trailing space; model completion leads to that model's thinking-level picker; `on-done` and `when-stuck` open their value pickers directly.
16
+
3
17
  ## 0.1.3 - 2026-06-24
4
18
 
5
19
  ### Changed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-advisor
2
2
 
3
- A [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) extension that offers a
3
+ A [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) extension for pi `>=0.80.10` that offers a
4
4
  parameterless `advisor` tool — inspired by Claude Code's advisor, but expanded with additional
5
5
  nudges and a manual review procedure. The regular model calls `advisor` to get a second opinion
6
6
  from an explicitly configured **stronger reviewer model** that sees the *entire* conversation transcript. Beyond the
@@ -66,7 +66,7 @@ JSON, resolved **project-over-global** (first scope that defines a key wins):
66
66
  ```jsonc
67
67
  {
68
68
  "model": "openai-codex/gpt-5.5", // "provider/id", or "none" to disable + hide the tool
69
- "thinking": "high", // off|minimal|low|medium|high|xhigh (default high)
69
+ "thinking": "high", // a level supported by the selected reviewer model (default high)
70
70
  "onDone": false, // auto-review when the agent finishes a task
71
71
  "whenStuck": 0, // auto-consult after N consecutive errors or N repeated identical tool calls (0 = off)
72
72
  "timeoutMs": 120000 // advisor call timeout in ms (0 = use provider default)
@@ -100,6 +100,8 @@ are selectable.
100
100
  | `/advisor status` | Show the resolved configuration |
101
101
  | `/advise [show\|pipe\|steer]` | Run a one-off review now; default is `pipe` when idle and `steer` while the agent is running |
102
102
 
103
+ Autocomplete completes `/adviso...` to `/advisor` without Pi's trailing-space insertion. A completed reviewer model opens that model's supported thinking-level picker on the next Tab. `/advisor on-done` and `/advisor when-stuck` open their value pickers directly. `/advise` keeps Pi's standard completion behavior.
104
+
103
105
  ### `/advise` modes
104
106
 
105
107
  - **`/advise`** — quick advice injection: sends feedback as a user message when idle, or as a
@@ -168,5 +170,4 @@ tool's `promptGuidelines`. Two opt-in deterministic triggers, configurable per p
168
170
  also layers a `ctx.ui.addAutocompleteProvider()` on `session_start` so `/advisor ...` and
169
171
  `/advise ...` completions replace the whole argument segment and suppress irrelevant path
170
172
  completion while typing command arguments (pi ≥ 0.79.1).
171
- - The package's `examples/extensions/summarize.ts` remains the canonical extension reference; with pi-ai
172
- `0.80.1`, import legacy `complete()` calls from `@earendil-works/pi-ai/compat`.
173
+ - Use `complete()` from `@earendil-works/pi-ai/compat` for Pi's compatibility streaming helper.
package/advisor.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * {
16
16
  * "model": "provider/id" | "none", // "none" disables + hides the tool
17
- * "thinking":"off|minimal|low|medium|high|xhigh", // default "high"
17
+ * "thinking":"<model-supported level>", // default "high"
18
18
  * "onDone": true, // auto-review when the agent finishes (default off)
19
19
  * "whenStuck": 3, // auto-consult after N consecutive tool errors (0/off)
20
20
  * "timeoutMs": 120000 // advisor call timeout in ms (0 = use provider default)
@@ -40,8 +40,8 @@
40
40
  * call advisor, nudged by the tool's prompt guidelines. The optional deterministic
41
41
  * triggers and `/advise` command provide additional ways to request reviewer feedback.
42
42
  */
43
- import { Type } from "@earendil-works/pi-ai";
44
- import type { Api, Model } from "@earendil-works/pi-ai";
43
+ import { getSupportedThinkingLevels, Type } from "@earendil-works/pi-ai";
44
+ import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
45
45
  import { complete } from "@earendil-works/pi-ai/compat";
46
46
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
47
47
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
@@ -51,9 +51,8 @@ import * as fs from "node:fs";
51
51
  import * as os from "node:os";
52
52
  import * as path from "node:path";
53
53
 
54
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
55
- type ThinkingLevel = (typeof THINKING_LEVELS)[number];
56
- const DEFAULT_THINKING: ThinkingLevel = "high";
54
+ type ThinkingLevel = ModelThinkingLevel;
55
+ const DEFAULT_THINKING = "high";
57
56
  const DISABLED = "none";
58
57
  const MAX_VISIBLE_MODEL_CHOICES = 12;
59
58
  export const MAX_TOOL_CALL_ARGS_CHARS = 800;
@@ -71,23 +70,21 @@ const ADVISOR_FIRST_TOKEN_ITEMS: AutocompleteItem[] = [
71
70
  { value: "?", label: "?", description: "Show /advisor usage" },
72
71
  ];
73
72
 
74
- const THINKING_LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
75
- off: "Disable extended thinking",
76
- minimal: "Smallest available thinking budget",
77
- low: "Light reasoning",
78
- medium: "Balanced default reasoning",
79
- high: "More reasoning for harder tasks",
80
- xhigh: "Maximum reasoning budget",
81
- };
82
-
83
- // Cached model list for autocomplete (refreshed on session_start)
73
+ // Cached model list for autocomplete (refreshed on session_start).
84
74
  let cachedModelSpecs: string[] = [];
75
+ let cachedModels = new Map<string, Model<Api>>();
76
+
77
+ function cacheModels(models: Model<Api>[]): void {
78
+ cachedModels = new Map(models.map((model) => [`${model.provider}/${model.id}`, model]));
79
+ cachedModelSpecs = [...cachedModels.keys()].sort();
80
+ }
85
81
 
86
- function thinkingLevelItems(prefix: string): AutocompleteItem[] {
82
+ function thinkingLevelItems(model: Model<Api> | undefined, prefix: string): AutocompleteItem[] {
83
+ if (!model) return [];
87
84
  const normalized = prefix.toLowerCase().trim();
88
- return (THINKING_LEVELS as readonly string[])
85
+ return getSupportedThinkingLevels(model)
89
86
  .filter((level) => level.startsWith(normalized))
90
- .map((level) => ({ value: level, label: level, description: THINKING_LEVEL_DESCRIPTIONS[level as ThinkingLevel] }));
87
+ .map((level) => ({ value: level, label: level, description: "Supported by this reviewer model" }));
91
88
  }
92
89
 
93
90
  export function getAdvisorCompletions(args: string): AutocompleteItem[] | null {
@@ -99,12 +96,11 @@ export function getAdvisorCompletions(args: string): AutocompleteItem[] | null {
99
96
  const normalized = prefix.toLowerCase();
100
97
 
101
98
  if (tokens.length === 0) {
102
- // No tokens yet — suggest subcommands, thinking levels, and model list.
103
- const levels = thinkingLevelItems(prefix);
99
+ // No tokens yet — suggest subcommands and available reviewer models.
104
100
  const models = cachedModelSpecs
105
101
  .filter((spec) => spec.toLowerCase().includes(normalized))
106
102
  .map((spec) => ({ value: spec, label: spec }));
107
- return [...ADVISOR_FIRST_TOKEN_ITEMS, ...levels, ...models];
103
+ return [...ADVISOR_FIRST_TOKEN_ITEMS, ...models];
108
104
  }
109
105
 
110
106
  const head = tokens[0].toLowerCase();
@@ -127,14 +123,27 @@ export function getAdvisorCompletions(args: string): AutocompleteItem[] | null {
127
123
  return []; // These are terminal commands
128
124
  }
129
125
 
126
+ // A model plus thinking level is a complete /advisor command. Do not offer
127
+ // another level picker after the user types a trailing space.
128
+ if (tokens.length > 1 && hasTrailingSpace) return [];
129
+
130
130
  if (completingSecondToken) {
131
- return thinkingLevelItems(prefix).map((item) => ({
131
+ return thinkingLevelItems(cachedModels.get(tokens[0]), prefix).map((item) => ({
132
+ ...item,
133
+ value: `${tokens[0]} ${item.value}`,
134
+ }));
135
+ }
136
+
137
+ // After model-name completion, a further Tab opens that model's level picker.
138
+ const exactModel = cachedModels.get(tokens[0]);
139
+ if (exactModel) {
140
+ return thinkingLevelItems(exactModel, "").map((item) => ({
132
141
  ...item,
133
142
  value: `${tokens[0]} ${item.value}`,
134
143
  }));
135
144
  }
136
145
 
137
- // First token: could be a subcommand, model spec, or thinking level — prefer explicit commands and models first.
146
+ // First token: could be a subcommand or model spec — prefer explicit commands.
138
147
  const firstTokenMatches = ADVISOR_FIRST_TOKEN_ITEMS.filter((item) => item.value.startsWith(normalized));
139
148
  const modelMatches = cachedModelSpecs
140
149
  .filter((spec) => spec.toLowerCase().includes(normalized))
@@ -142,7 +151,7 @@ export function getAdvisorCompletions(args: string): AutocompleteItem[] | null {
142
151
  if (firstTokenMatches.length > 0 || modelMatches.length > 0) {
143
152
  return [...firstTokenMatches, ...modelMatches];
144
153
  }
145
- return thinkingLevelItems(prefix);
154
+ return [];
146
155
  }
147
156
 
148
157
  function getAdviseCompletions(prefix: string): AutocompleteItem[] | null {
@@ -160,6 +169,10 @@ function commandArgumentCompletions(command: "advisor" | "advise", args: string)
160
169
  return { prefix: args, items };
161
170
  }
162
171
 
172
+ function isAdvisorCommandPrefix(value: string): boolean {
173
+ return value.startsWith("/adviso") && "/advisor".startsWith(value);
174
+ }
175
+
163
176
  export type AdviseMode = "show" | "pipe" | "steer";
164
177
 
165
178
  export function resolveAdviseMode(args: string | undefined, isIdle: boolean): AdviseMode | undefined {
@@ -198,7 +211,7 @@ Always return your advice as visible assistant text. Do not return reasoning-onl
198
211
 
199
212
  type AdvisorConfig = {
200
213
  model?: string;
201
- thinking?: ThinkingLevel;
214
+ thinking?: string;
202
215
  onDone?: boolean;
203
216
  whenStuck?: number;
204
217
  timeoutMs?: number;
@@ -222,10 +235,10 @@ export function validateAdvisorConfig(raw: unknown, source = "advisor config"):
222
235
  else warn('"model" must be a string');
223
236
  }
224
237
  if (input.thinking !== undefined) {
225
- if (typeof input.thinking === "string" && (THINKING_LEVELS as readonly string[]).includes(input.thinking)) {
226
- clean.thinking = input.thinking as ThinkingLevel;
238
+ if (typeof input.thinking === "string" && input.thinking.trim()) {
239
+ clean.thinking = input.thinking.trim();
227
240
  } else {
228
- warn(`"thinking" must be one of: ${THINKING_LEVELS.join(", ")}`);
241
+ warn('"thinking" must be a non-empty string');
229
242
  }
230
243
  }
231
244
  if (input.onDone !== undefined) {
@@ -279,15 +292,14 @@ const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes
279
292
  type EffectiveAdvisorConfig = {
280
293
  spec: string | undefined;
281
294
  source: string;
282
- thinking: ThinkingLevel;
295
+ thinking: string;
283
296
  onDone: boolean;
284
297
  whenStuck: number;
285
298
  timeoutMs: number;
286
299
  };
287
300
 
288
- function envThinkingLevel(): ThinkingLevel | undefined {
289
- const env = process.env.PI_ADVISOR_EFFORT?.trim();
290
- return env && (THINKING_LEVELS as readonly string[]).includes(env) ? (env as ThinkingLevel) : undefined;
301
+ function envThinkingLevel(): string | undefined {
302
+ return process.env.PI_ADVISOR_EFFORT?.trim() || undefined;
291
303
  }
292
304
 
293
305
  function envTimeoutMs(): number | undefined {
@@ -332,7 +344,7 @@ function effectiveModelSpec(cwd: string, projectTrusted = true): { spec: string
332
344
  return { spec, source };
333
345
  }
334
346
 
335
- function effectiveThinking(cwd: string, projectTrusted = true): ThinkingLevel {
347
+ function effectiveThinking(cwd: string, projectTrusted = true): string {
336
348
  return resolveEffectiveConfig(cwd, projectTrusted).thinking;
337
349
  }
338
350
 
@@ -396,7 +408,7 @@ async function resolveAdvisor(ctx: ExtensionContext): Promise<Resolved | null> {
396
408
  const cwd = ctx.cwd;
397
409
  const projectTrusted = contextProjectTrusted(ctx);
398
410
  const { spec, source } = effectiveModelSpec(cwd, projectTrusted);
399
- const thinking = effectiveThinking(cwd, projectTrusted);
411
+ const requestedThinking = effectiveThinking(cwd, projectTrusted);
400
412
  const timeoutMs = effectiveTimeoutMs(cwd, projectTrusted);
401
413
  const warnings: string[] = [];
402
414
 
@@ -404,14 +416,23 @@ async function resolveAdvisor(ctx: ExtensionContext): Promise<Resolved | null> {
404
416
 
405
417
  // Refresh before resolving so OAuth/subscription-backed model mutations and
406
418
  // newly logged-in providers are visible to advisor just like they are to /model.
407
- refreshAvailableModels(ctx);
419
+ cacheModels(refreshAvailableModels(ctx));
408
420
 
409
421
  if (!spec) {
410
422
  throw new Error("Advisor is not configured. Choose a trusted reviewer model with /advisor before sending transcripts.");
411
423
  }
412
424
 
413
425
  const hit = await tryModel(ctx, spec);
414
- if (hit) return { ...hit, thinking, timeoutMs, warnings };
426
+ if (hit) {
427
+ const supported = getSupportedThinkingLevels(hit.model);
428
+ const thinking = supported.includes(requestedThinking as ThinkingLevel)
429
+ ? (requestedThinking as ThinkingLevel)
430
+ : (supported.includes(DEFAULT_THINKING) ? DEFAULT_THINKING : supported[0] ?? "off");
431
+ if (thinking !== requestedThinking) {
432
+ warnings.push(`Thinking level "${requestedThinking}" is unsupported by ${spec}; using "${thinking}".`);
433
+ }
434
+ return { ...hit, thinking, timeoutMs, warnings };
435
+ }
415
436
 
416
437
  throw new Error(
417
438
  `Configured advisor model "${spec}" (${source}) is unavailable or lacks auth. Choose another model with /advisor or set PI_ADVISOR_MODEL.`,
@@ -677,9 +698,7 @@ export default function advisorExtension(pi: ExtensionAPI) {
677
698
 
678
699
  pi.on("session_start", async (_event, ctx) => {
679
700
  // Cache model specs for autocomplete.
680
- cachedModelSpecs = refreshAvailableModels(ctx)
681
- .map((m) => `${m.provider}/${m.id}`)
682
- .sort();
701
+ cacheModels(refreshAvailableModels(ctx));
683
702
  stuckErrors = 0;
684
703
  autoReviewedThisRound = false;
685
704
  applyActivation(ctx.cwd, contextProjectTrusted(ctx));
@@ -694,8 +713,24 @@ export default function advisorExtension(pi: ExtensionAPI) {
694
713
  async getSuggestions(lines, cursorLine, cursorCol, options) {
695
714
  const line = lines[cursorLine] ?? "";
696
715
  const beforeCursor = line.slice(0, cursorCol);
697
- const match = beforeCursor.match(/^\/(advisor|advise)\s+(.*)$/);
716
+ if (cursorCol === line.length && isAdvisorCommandPrefix(beforeCursor)) {
717
+ if (beforeCursor === "/advisor") {
718
+ return { prefix: "", items: getAdvisorCompletions("") ?? [] };
719
+ }
720
+ return {
721
+ prefix: beforeCursor,
722
+ items: [{ value: "advisor", label: "advisor", description: "Configure the advisor" }],
723
+ };
724
+ }
725
+ if (beforeCursor === "/advisor ") {
726
+ return { prefix: "", items: getAdvisorCompletions("") ?? [] };
727
+ }
728
+ const triggerMatch = beforeCursor.match(/^\/advisor\s+(on-done|when-stuck)$/);
729
+ if (triggerMatch && cursorCol === line.length) {
730
+ return { prefix: "", items: getAdvisorCompletions(`${triggerMatch[1]} `) ?? [] };
731
+ }
698
732
 
733
+ const match = beforeCursor.match(/^\/(advisor|advise)\s+(.*)$/);
699
734
  if (!match) return current.getSuggestions(lines, cursorLine, cursorCol, options);
700
735
 
701
736
  const command = match[1] as "advisor" | "advise";
@@ -704,13 +739,40 @@ export default function advisorExtension(pi: ExtensionAPI) {
704
739
  },
705
740
 
706
741
  applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
742
+ const line = lines[cursorLine] ?? "";
743
+ const beforeCursor = line.slice(0, cursorCol);
744
+ if (cursorCol === line.length && isAdvisorCommandPrefix(beforeCursor)) {
745
+ if (beforeCursor !== "/advisor") {
746
+ return {
747
+ lines: [...lines.slice(0, cursorLine), "/advisor", ...lines.slice(cursorLine + 1)],
748
+ cursorLine,
749
+ cursorCol: "/advisor".length,
750
+ };
751
+ }
752
+ const nextLine = `/advisor ${item.value}`;
753
+ return {
754
+ lines: [...lines.slice(0, cursorLine), nextLine, ...lines.slice(cursorLine + 1)],
755
+ cursorLine,
756
+ cursorCol: nextLine.length,
757
+ };
758
+ }
759
+ if (
760
+ (beforeCursor === "/advisor " || /^\/advisor\s+(on-done|when-stuck)$/.test(beforeCursor)) &&
761
+ cursorCol === line.length
762
+ ) {
763
+ const nextLine = `/advisor ${item.value}`;
764
+ return {
765
+ lines: [...lines.slice(0, cursorLine), nextLine, ...lines.slice(cursorLine + 1)],
766
+ cursorLine,
767
+ cursorCol: nextLine.length,
768
+ };
769
+ }
707
770
  return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
708
771
  },
709
772
 
710
773
  shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
711
- const line = lines[cursorLine] ?? "";
712
- const beforeCursor = line.slice(0, cursorCol);
713
- if (/^\/(advisor|advise)\s+/.test(beforeCursor)) return false;
774
+ // Let Tab reach getSuggestions(). This provider handles advisor syntax
775
+ // before the built-in file provider is consulted.
714
776
  return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
715
777
  },
716
778
  }));
@@ -895,7 +957,7 @@ export default function advisorExtension(pi: ExtensionAPI) {
895
957
  const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
896
958
  const cwd = ctx.cwd;
897
959
  const head = tokens[0]?.toLowerCase();
898
- refreshAvailableModels(ctx);
960
+ cacheModels(refreshAvailableModels(ctx));
899
961
 
900
962
  if (head === "?") {
901
963
  ctx.ui.notify(
@@ -987,10 +1049,13 @@ export default function advisorExtension(pi: ExtensionAPI) {
987
1049
  }
988
1050
  modelValue = tokens[0];
989
1051
  if (tokens[1]) {
990
- if (!(THINKING_LEVELS as readonly string[]).includes(tokens[1].toLowerCase())) {
991
- return ctx.ui.notify(`Invalid thinking level "${tokens[1]}". One of: ${THINKING_LEVELS.join(", ")}.`, "error");
1052
+ const model = ctx.modelRegistry.find(parsed.provider, parsed.id);
1053
+ const supported = model ? getSupportedThinkingLevels(model) : [];
1054
+ const requested = tokens[1].toLowerCase() as ThinkingLevel;
1055
+ if (!supported.includes(requested)) {
1056
+ return ctx.ui.notify(`Invalid thinking level "${tokens[1]}". Supported: ${supported.join(", ") || "off"}.`, "error");
992
1057
  }
993
- thinkingArg = tokens[1].toLowerCase() as ThinkingLevel;
1058
+ thinkingArg = requested;
994
1059
  }
995
1060
  }
996
1061
 
@@ -998,11 +1063,14 @@ export default function advisorExtension(pi: ExtensionAPI) {
998
1063
  if (!file) return;
999
1064
 
1000
1065
  let thinking = thinkingArg;
1001
- if (!thinking && modelValue !== DISABLED && tokens.length === 0) {
1066
+ if (!thinking && modelValue !== DISABLED && tokens.length === 0 && modelValue) {
1067
+ const parsed = parseSpec(modelValue);
1068
+ const model = parsed ? ctx.modelRegistry.find(parsed.provider, parsed.id) : undefined;
1069
+ const supported = model ? getSupportedThinkingLevels(model) : [];
1002
1070
  const KEEP = "keep current";
1003
- const pick = await ctx.ui.select("Thinking level", ["high (default)", "xhigh", "medium", "low", "minimal", "off", KEEP]);
1071
+ const pick = await ctx.ui.select("Thinking level", [...supported, KEEP]);
1004
1072
  if (pick === undefined) return;
1005
- if (pick !== KEEP) thinking = pick.split(" ")[0] as ThinkingLevel;
1073
+ if (pick !== KEEP) thinking = pick as ThinkingLevel;
1006
1074
  }
1007
1075
 
1008
1076
  persist(file, { model: modelValue, ...(thinking ? { thinking } : {}) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hk_net/pi-advisor",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Pi advisor extension: consult a configured reviewer model on the full conversation transcript",
5
5
  "type": "module",
6
6
  "private": false,
@@ -27,9 +27,9 @@
27
27
  "CHANGELOG.md"
28
28
  ],
29
29
  "peerDependencies": {
30
- "@earendil-works/pi-ai": ">=0.80.2",
31
- "@earendil-works/pi-coding-agent": ">=0.80.2",
32
- "@earendil-works/pi-tui": ">=0.80.2"
30
+ "@earendil-works/pi-ai": ">=0.80.10",
31
+ "@earendil-works/pi-coding-agent": ">=0.80.10",
32
+ "@earendil-works/pi-tui": ">=0.80.10"
33
33
  },
34
34
  "pi": {
35
35
  "extensions": [