@juicesharp/rpiv-advisor 1.8.0 → 1.8.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.
Files changed (2) hide show
  1. package/advisor.ts +23 -47
  2. package/package.json +49 -46
package/advisor.ts CHANGED
@@ -13,9 +13,7 @@
13
13
  * via pi.setActiveTools(). Selection is in-memory and resets each session.
14
14
  */
15
15
 
16
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
- import { homedir } from "node:os";
18
- import { dirname, join } from "node:path";
16
+ import { readFileSync } from "node:fs";
19
17
  import { fileURLToPath } from "node:url";
20
18
  import type { Api, Model, StopReason, Usage } from "@earendil-works/pi-ai";
21
19
  import { completeSimple, getSupportedThinkingLevels, type Message, type ThinkingLevel } from "@earendil-works/pi-ai";
@@ -29,6 +27,8 @@ import {
29
27
  type ToolInfo,
30
28
  } from "@earendil-works/pi-coding-agent";
31
29
  import type { SelectItem } from "@earendil-works/pi-tui";
30
+ import type { GuidanceFields } from "@juicesharp/rpiv-config";
31
+ import { configPath, loadJsonConfig, saveJsonConfig, validateGuidanceFields } from "@juicesharp/rpiv-config";
32
32
  import { Type } from "typebox";
33
33
  import { showAdvisorPicker, showEffortPicker } from "./advisor-ui.js";
34
34
 
@@ -41,9 +41,7 @@ export const ADVISOR_TOOL_NAME = "advisor";
41
41
  const TOOL_LABEL = "Advisor";
42
42
 
43
43
  // Persistence
44
- const CONFIG_DIR = join(homedir(), ".config", "rpiv-advisor");
45
- const ADVISOR_CONFIG_PATH = join(CONFIG_DIR, "advisor.json");
46
- const CONFIG_FILE_MODE = 0o600;
44
+ const ADVISOR_CONFIG_PATH = configPath("rpiv-advisor", "advisor.json");
47
45
 
48
46
  // Selector sentinels — double-underscore form is collision-proof against real provider:id keys
49
47
  const NO_ADVISOR_VALUE = "__no_advisor__";
@@ -62,6 +60,7 @@ const CHECKMARK = " ✓";
62
60
  const MSG_ADVISOR_DISABLED = "Advisor disabled";
63
61
  const MSG_REQUIRES_INTERACTIVE = "/advisor requires interactive mode";
64
62
  const MSG_ADVISOR_NUDGE = "Please advise on the executor's situation above.";
63
+ const MSG_PERSIST_FAILED = "Failed to save advisor selection — selection not persisted";
65
64
 
66
65
  // Errors (static)
67
66
  const ERR_NO_MODEL = "No advisor model is configured. The user can enable one with the /advisor command.";
@@ -95,11 +94,6 @@ const msgConsulting = (label: string, effort: ThinkingLevel | undefined) =>
95
94
  // Config file persistence (cross-session)
96
95
  // ---------------------------------------------------------------------------
97
96
 
98
- interface GuidanceFields {
99
- promptSnippet?: string;
100
- promptGuidelines?: string[];
101
- }
102
-
103
97
  interface AdvisorConfig {
104
98
  modelKey?: string;
105
99
  effort?: ThinkingLevel;
@@ -108,37 +102,17 @@ interface AdvisorConfig {
108
102
  }
109
103
 
110
104
  export function loadAdvisorConfig(): AdvisorConfig {
111
- if (!existsSync(ADVISOR_CONFIG_PATH)) return {};
112
- try {
113
- return JSON.parse(readFileSync(ADVISOR_CONFIG_PATH, "utf-8")) as AdvisorConfig;
114
- } catch {
115
- return {};
116
- }
105
+ return loadJsonConfig<AdvisorConfig>(ADVISOR_CONFIG_PATH);
117
106
  }
118
107
 
119
- function validateGuidanceFields(fields: unknown): GuidanceFields {
120
- if (!fields || typeof fields !== "object") return {};
121
- const g = fields as Record<string, unknown>;
122
- const result: GuidanceFields = {};
123
- if (typeof g.promptSnippet === "string" && g.promptSnippet.length > 0) {
124
- result.promptSnippet = g.promptSnippet;
125
- }
126
- if (
127
- Array.isArray(g.promptGuidelines) &&
128
- g.promptGuidelines.length > 0 &&
129
- g.promptGuidelines.every((s) => typeof s === "string" && s.length > 0)
130
- ) {
131
- result.promptGuidelines = g.promptGuidelines;
132
- }
133
- return result;
134
- }
108
+ // validateGuidanceFields is now imported from @juicesharp/rpiv-config
135
109
 
136
110
  function validateDisabledForModels(value: unknown): string[] {
137
111
  if (!Array.isArray(value)) return [];
138
112
  return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
139
113
  }
140
114
 
141
- export function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | undefined): void {
115
+ export function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | undefined): boolean {
142
116
  const existing = loadAdvisorConfig();
143
117
  const config: AdvisorConfig = { ...existing };
144
118
  // Delete (rather than omit) to clear fields that may exist in the spread
@@ -148,17 +122,7 @@ export function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel
148
122
  else delete config.modelKey;
149
123
  if (effort) config.effort = effort;
150
124
  else delete config.effort;
151
- try {
152
- mkdirSync(dirname(ADVISOR_CONFIG_PATH), { recursive: true });
153
- writeFileSync(ADVISOR_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
154
- } catch {
155
- // write may fail on disk-full or permission errors — best effort only
156
- }
157
- try {
158
- chmodSync(ADVISOR_CONFIG_PATH, CONFIG_FILE_MODE);
159
- } catch {
160
- // chmod may fail on some filesystems — best effort only
161
- }
125
+ return saveJsonConfig(ADVISOR_CONFIG_PATH, config);
162
126
  }
163
127
 
164
128
  function parseModelKey(key: string): { provider: string; modelId: string } | undefined {
@@ -630,9 +594,16 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
630
594
  const activeHas = activeTools.includes(ADVISOR_TOOL_NAME);
631
595
 
632
596
  if (choice === NO_ADVISOR_VALUE) {
597
+ // Persist BEFORE applying in-memory state so a save failure can't
598
+ // leave the model setter and the active-tools registry divergent
599
+ // (review I2: early-return on failure skipped the tool-list update
600
+ // and left "model=undefined + tool still registered" stranded).
601
+ if (!saveAdvisorConfig(undefined, undefined)) {
602
+ ctx.ui.notify(MSG_PERSIST_FAILED, "error");
603
+ return;
604
+ }
633
605
  setAdvisorModel(undefined);
634
606
  setAdvisorEffort(undefined);
635
- saveAdvisorConfig(undefined, undefined);
636
607
  if (activeHas) {
637
608
  pi.setActiveTools(activeTools.filter((n) => n !== ADVISOR_TOOL_NAME));
638
609
  }
@@ -668,9 +639,14 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
668
639
  effortChoice = effortResult === OFF_VALUE ? undefined : (effortResult as ThinkingLevel);
669
640
  }
670
641
 
642
+ // Persist BEFORE applying in-memory state — same rationale as the
643
+ // disable branch above (review I2).
644
+ if (!saveAdvisorConfig(modelKey(picked), effortChoice)) {
645
+ ctx.ui.notify(MSG_PERSIST_FAILED, "error");
646
+ return;
647
+ }
671
648
  setAdvisorEffort(effortChoice);
672
649
  setAdvisorModel(picked);
673
- saveAdvisorConfig(modelKey(picked), effortChoice);
674
650
 
675
651
  // Re-read after the effort-picker await — the snapshot taken before
676
652
  // `showEffortPicker` is stale once execution yields.
package/package.json CHANGED
@@ -1,48 +1,51 @@
1
1
  {
2
- "name": "@juicesharp/rpiv-advisor",
3
- "version": "1.8.0",
4
- "description": "Pi extension. A second opinion the model can request from a stronger reviewer model before it acts.",
5
- "keywords": [
6
- "pi-package",
7
- "pi-extension",
8
- "rpiv",
9
- "advisor"
10
- ],
11
- "type": "module",
12
- "license": "MIT",
13
- "author": "juicesharp",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/juicesharp/rpiv-mono.git",
17
- "directory": "packages/rpiv-advisor"
18
- },
19
- "homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-advisor#readme",
20
- "bugs": {
21
- "url": "https://github.com/juicesharp/rpiv-mono/issues"
22
- },
23
- "publishConfig": {
24
- "access": "public"
25
- },
26
- "scripts": {
27
- "test": "vitest run"
28
- },
29
- "files": [
30
- "index.ts",
31
- "advisor.ts",
32
- "advisor-ui.ts",
33
- "prompts/",
34
- "README.md",
35
- "LICENSE"
36
- ],
37
- "pi": {
38
- "extensions": [
39
- "./index.ts"
40
- ]
41
- },
42
- "peerDependencies": {
43
- "@earendil-works/pi-ai": "*",
44
- "@earendil-works/pi-coding-agent": "*",
45
- "@earendil-works/pi-tui": "*",
46
- "typebox": "*"
47
- }
2
+ "name": "@juicesharp/rpiv-advisor",
3
+ "version": "1.8.2",
4
+ "description": "Pi extension. A second opinion the model can request from a stronger reviewer model before it acts.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "rpiv",
9
+ "advisor"
10
+ ],
11
+ "type": "module",
12
+ "license": "MIT",
13
+ "author": "juicesharp",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/juicesharp/rpiv-mono.git",
17
+ "directory": "packages/rpiv-advisor"
18
+ },
19
+ "homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-advisor#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/juicesharp/rpiv-mono/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "test": "vitest run"
28
+ },
29
+ "files": [
30
+ "index.ts",
31
+ "advisor.ts",
32
+ "advisor-ui.ts",
33
+ "prompts/",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "pi": {
38
+ "extensions": [
39
+ "./index.ts"
40
+ ]
41
+ },
42
+ "dependencies": {
43
+ "@juicesharp/rpiv-config": "^1.8.2"
44
+ },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-ai": "*",
47
+ "@earendil-works/pi-coding-agent": "*",
48
+ "@earendil-works/pi-tui": "*",
49
+ "typebox": "*"
50
+ }
48
51
  }