pi-advisor-flow 0.5.4 → 0.5.6
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 +21 -1
- package/README.md +50 -19
- package/dist/index.js +5543 -0
- package/package.json +14 -10
- package/src/config/storage.ts +163 -56
- package/src/ui/manual-dialog-render.ts +8 -1
- package/src/ui/model-selector.ts +6 -5
- package/src/ui/text-setting-submenu.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"author": "Philip Brembeck",
|
|
20
20
|
"type": "module",
|
|
21
|
-
"main": "
|
|
21
|
+
"main": "dist/index.js",
|
|
22
22
|
"types": "extensions/index.ts",
|
|
23
23
|
"files": [
|
|
24
|
+
"dist",
|
|
24
25
|
"extensions",
|
|
25
26
|
"src",
|
|
26
27
|
"CHANGELOG.md",
|
|
@@ -32,15 +33,18 @@
|
|
|
32
33
|
"bench:evaluate": "bun bench/src/cli.ts evaluate",
|
|
33
34
|
"bench:replay": "bun bench/src/cli.ts replay",
|
|
34
35
|
"bench:screen": "bun bench/src/cli.ts screen",
|
|
36
|
+
"build": "bun build ./extensions/index.ts --outfile dist/index.js --target node --format esm --external @earendil-works/pi-ai --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --external typebox",
|
|
35
37
|
"check:boundaries": "node scripts/check-module-boundaries.mjs",
|
|
36
38
|
"format": "bunx ultracite fix --linter-enabled=false",
|
|
37
39
|
"lint": "bunx ultracite check",
|
|
38
40
|
"lint:fix": "bunx ultracite fix",
|
|
41
|
+
"prepack": "bun run build >/dev/null",
|
|
39
42
|
"package:check": "node scripts/check-package.mjs",
|
|
40
43
|
"prepare": "husky",
|
|
41
44
|
"socket": "bunx socket optimize",
|
|
42
|
-
"test": "bun test",
|
|
45
|
+
"test": "bun test && bun run test:node-loader",
|
|
43
46
|
"test:bench": "bun --config=./bunfig.bench.toml test ./bench/test/",
|
|
47
|
+
"test:node-loader": "node --test test/default-resource-loader.node.mjs",
|
|
44
48
|
"typecheck": "tsc --noEmit"
|
|
45
49
|
},
|
|
46
50
|
"lint-staged": {
|
|
@@ -58,9 +62,9 @@
|
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
60
64
|
"@biomejs/biome": "2.5.12",
|
|
61
|
-
"@earendil-works/pi-ai": "^0.
|
|
62
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
63
|
-
"@earendil-works/pi-tui": "^0.
|
|
65
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
66
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
67
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
64
68
|
"@types/node": "^26.4.1",
|
|
65
69
|
"bun-types": "1.4.1",
|
|
66
70
|
"husky": "^9.1.7",
|
|
@@ -70,9 +74,9 @@
|
|
|
70
74
|
"ultracite": "7.10.8"
|
|
71
75
|
},
|
|
72
76
|
"peerDependencies": {
|
|
73
|
-
"@earendil-works/pi-ai": "^0.84.1",
|
|
74
|
-
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
75
|
-
"@earendil-works/pi-tui": "^0.84.1",
|
|
77
|
+
"@earendil-works/pi-ai": "^0.84.1 || ^0.85.1",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "^0.84.1 || ^0.85.1",
|
|
79
|
+
"@earendil-works/pi-tui": "^0.84.1 || ^0.85.1",
|
|
76
80
|
"typebox": "^1.1.38"
|
|
77
81
|
},
|
|
78
82
|
"peerDependenciesMeta": {
|
|
@@ -91,7 +95,7 @@
|
|
|
91
95
|
},
|
|
92
96
|
"pi": {
|
|
93
97
|
"extensions": [
|
|
94
|
-
"./
|
|
98
|
+
"./dist/index.js"
|
|
95
99
|
],
|
|
96
100
|
"image": "https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/hero.png"
|
|
97
101
|
}
|
package/src/config/storage.ts
CHANGED
|
@@ -53,6 +53,125 @@ export const configPaths = (ctx: ExtensionContext) => [
|
|
|
53
53
|
const configuredModelRef = (value: string | undefined): string | undefined =>
|
|
54
54
|
value?.trim() || undefined;
|
|
55
55
|
|
|
56
|
+
const SAVED_CONFIG_KEYS = [
|
|
57
|
+
"advisor",
|
|
58
|
+
"advisorAutoLoopGate",
|
|
59
|
+
"advisorBlockOnBlocked",
|
|
60
|
+
"advisorCollapseResponses",
|
|
61
|
+
"advisorCompletionGate",
|
|
62
|
+
"advisorCustomInvocation",
|
|
63
|
+
"advisorEffort",
|
|
64
|
+
"advisorFailureGate",
|
|
65
|
+
"advisorGitContext",
|
|
66
|
+
"advisorGitContextMaxChars",
|
|
67
|
+
"advisorHerdrIntegration",
|
|
68
|
+
"advisorLoopThreshold",
|
|
69
|
+
"advisorMaxCallsPerSession",
|
|
70
|
+
"advisorPlanGate",
|
|
71
|
+
"advisorRedactSecrets",
|
|
72
|
+
"advisorScoutEnabled",
|
|
73
|
+
"advisorSessionSummary",
|
|
74
|
+
"advisorToolPolicies",
|
|
75
|
+
"advisorToolResultMaxBytes",
|
|
76
|
+
"advisorToolResultMaxLines",
|
|
77
|
+
"advisorTrackedFileContent",
|
|
78
|
+
"advisorUntrackedContent",
|
|
79
|
+
"alwaysOn",
|
|
80
|
+
"contextMaxChars",
|
|
81
|
+
"executor",
|
|
82
|
+
"executorEffort",
|
|
83
|
+
"gateFailureMode",
|
|
84
|
+
"showUsageDetails",
|
|
85
|
+
"showUsageFooter",
|
|
86
|
+
"simpleMode",
|
|
87
|
+
] as const satisfies readonly (keyof AdvisorConfig)[];
|
|
88
|
+
type SavedConfigKey = (typeof SAVED_CONFIG_KEYS)[number];
|
|
89
|
+
type ConfigState = {
|
|
90
|
+
[Key in SavedConfigKey]: AdvisorConfig[Key];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const currentConfigState = (): ConfigState => ({
|
|
94
|
+
advisor: configuredModelRef(advisorRef),
|
|
95
|
+
advisorAutoLoopGate: advisorAutoLoopGateRef,
|
|
96
|
+
advisorBlockOnBlocked: advisorBlockOnBlockedRef,
|
|
97
|
+
advisorCollapseResponses: advisorCollapseResponsesRef,
|
|
98
|
+
advisorCompletionGate: advisorCompletionGateRef,
|
|
99
|
+
advisorCustomInvocation: advisorCustomInvocationRef,
|
|
100
|
+
advisorEffort: advisorEffortRef,
|
|
101
|
+
advisorFailureGate: advisorFailureGateRef,
|
|
102
|
+
advisorGitContext: advisorGitContextRef,
|
|
103
|
+
advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
|
|
104
|
+
advisorHerdrIntegration: advisorHerdrIntegrationRef,
|
|
105
|
+
advisorLoopThreshold: advisorLoopThresholdRef,
|
|
106
|
+
advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef,
|
|
107
|
+
advisorPlanGate: advisorPlanGateRef,
|
|
108
|
+
advisorRedactSecrets: advisorRedactSecretsRef,
|
|
109
|
+
advisorScoutEnabled: advisorScoutEnabledRef,
|
|
110
|
+
advisorSessionSummary: advisorSessionSummaryRef,
|
|
111
|
+
advisorToolPolicies: { ...advisorToolPoliciesRef },
|
|
112
|
+
advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
113
|
+
advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
|
|
114
|
+
advisorTrackedFileContent: advisorTrackedFileContentRef,
|
|
115
|
+
advisorUntrackedContent: advisorUntrackedContentRef,
|
|
116
|
+
alwaysOn: alwaysOnRef,
|
|
117
|
+
contextMaxChars: contextMaxCharsRef,
|
|
118
|
+
executor: configuredModelRef(executorRef),
|
|
119
|
+
executorEffort: executorEffortRef,
|
|
120
|
+
gateFailureMode: advisorFailureModeRef,
|
|
121
|
+
showUsageDetails: showUsageDetailsRef,
|
|
122
|
+
showUsageFooter: showUsageFooterRef,
|
|
123
|
+
simpleMode: simpleModeRef,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const sameConfigValue = <Value>(left: Value, right: Value) =>
|
|
127
|
+
JSON.stringify(left) === JSON.stringify(right);
|
|
128
|
+
|
|
129
|
+
const readExistingConfig = (path: string): Record<string, unknown> => {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
132
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
133
|
+
? (parsed as Record<string, unknown>)
|
|
134
|
+
: {};
|
|
135
|
+
} catch {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const shouldPersistConfigKey = (
|
|
141
|
+
key: SavedConfigKey,
|
|
142
|
+
persistAdvisor: boolean,
|
|
143
|
+
persistExecutor: boolean
|
|
144
|
+
) =>
|
|
145
|
+
(key !== "advisor" || persistAdvisor) &&
|
|
146
|
+
(key !== "executor" || persistExecutor);
|
|
147
|
+
|
|
148
|
+
const applyChangedConfigValues = (
|
|
149
|
+
data: Record<string, unknown>,
|
|
150
|
+
current: ConfigState,
|
|
151
|
+
changedKeys: readonly SavedConfigKey[],
|
|
152
|
+
persistAdvisor: boolean,
|
|
153
|
+
persistExecutor: boolean
|
|
154
|
+
) => {
|
|
155
|
+
for (const key of changedKeys) {
|
|
156
|
+
if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const value = current[key];
|
|
160
|
+
const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
|
|
161
|
+
if (value === undefined || isEmptyModelRef) {
|
|
162
|
+
delete data[key];
|
|
163
|
+
} else {
|
|
164
|
+
data[key] = value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Tracks the runtime state represented by the last successful load/save. A
|
|
170
|
+
// save can then merge fresh disk contents without treating untouched in-memory
|
|
171
|
+
// values as an instruction to overwrite an external edit.
|
|
172
|
+
let loadedConfigState: ConfigState | undefined;
|
|
173
|
+
let loadedConfigPath: string | undefined;
|
|
174
|
+
|
|
56
175
|
const readConfig = (path: string): AdvisorConfig => {
|
|
57
176
|
try {
|
|
58
177
|
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -135,10 +254,17 @@ export const loadConfig = (_ctx: ExtensionContext) => {
|
|
|
135
254
|
// prompts, gates, budgets, disclosure, redaction, integrations, and consent
|
|
136
255
|
// remain under the user's global Pi configuration.
|
|
137
256
|
setAdvisorOutcomeLoggingRef(globalConfig?.advisorOutcomeLogging === true);
|
|
257
|
+
loadedConfigState = currentConfigState();
|
|
258
|
+
loadedConfigPath = global;
|
|
138
259
|
return existsSync(global) ? global : null;
|
|
139
260
|
};
|
|
140
261
|
|
|
141
|
-
/**
|
|
262
|
+
/**
|
|
263
|
+
* Saves user-controlled configuration globally without outcome consent.
|
|
264
|
+
*
|
|
265
|
+
* Only values changed since the last load/save are written. This lets an
|
|
266
|
+
* external edit to advisor.json coexist with a later settings or model save.
|
|
267
|
+
*/
|
|
142
268
|
export interface SaveConfigOptions {
|
|
143
269
|
persistAdvisor?: boolean;
|
|
144
270
|
persistExecutor?: boolean;
|
|
@@ -151,70 +277,51 @@ export const saveConfig = (
|
|
|
151
277
|
const path = join(getAgentDir(), "advisor.json");
|
|
152
278
|
const persistAdvisor = options.persistAdvisor ?? true;
|
|
153
279
|
const persistExecutor = options.persistExecutor ?? true;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
existing.advisorMaxCallsPerSession = undefined;
|
|
280
|
+
const existing = readExistingConfig(path);
|
|
281
|
+
const current = currentConfigState();
|
|
282
|
+
const baseline = loadedConfigPath === path ? loadedConfigState : undefined;
|
|
283
|
+
const changedKeys = baseline
|
|
284
|
+
? SAVED_CONFIG_KEYS.filter(
|
|
285
|
+
(key) => !sameConfigValue(current[key], baseline[key])
|
|
286
|
+
)
|
|
287
|
+
: [...SAVED_CONFIG_KEYS];
|
|
288
|
+
if (changedKeys.length === 0) {
|
|
289
|
+
return path;
|
|
165
290
|
}
|
|
166
|
-
const data = {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
advisorEffort: advisorEffortRef,
|
|
175
|
-
advisorFailureGate: advisorFailureGateRef,
|
|
176
|
-
advisorLoopThreshold: advisorLoopThresholdRef,
|
|
177
|
-
advisorPlanGate: advisorPlanGateRef,
|
|
178
|
-
contextMaxChars: contextMaxCharsRef,
|
|
179
|
-
...(persistExecutor && executorRef ? { executor: executorRef } : {}),
|
|
180
|
-
executorEffort: executorEffortRef,
|
|
181
|
-
...(advisorMaxCallsPerSessionRef === undefined
|
|
182
|
-
? {}
|
|
183
|
-
: { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
|
|
184
|
-
advisorGitContext: advisorGitContextRef,
|
|
185
|
-
advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
|
|
186
|
-
advisorHerdrIntegration: advisorHerdrIntegrationRef,
|
|
187
|
-
advisorRedactSecrets: advisorRedactSecretsRef,
|
|
188
|
-
advisorScoutEnabled: advisorScoutEnabledRef,
|
|
189
|
-
advisorSessionSummary: advisorSessionSummaryRef,
|
|
190
|
-
advisorToolPolicies: advisorToolPoliciesRef,
|
|
191
|
-
advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
192
|
-
advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
|
|
193
|
-
advisorTrackedFileContent: advisorTrackedFileContentRef,
|
|
194
|
-
advisorUntrackedContent: advisorUntrackedContentRef,
|
|
195
|
-
alwaysOn: alwaysOnRef,
|
|
196
|
-
gateFailureMode: advisorFailureModeRef,
|
|
197
|
-
showUsageDetails: showUsageDetailsRef,
|
|
198
|
-
showUsageFooter: showUsageFooterRef,
|
|
199
|
-
simpleMode: simpleModeRef,
|
|
200
|
-
};
|
|
291
|
+
const data = { ...existing };
|
|
292
|
+
applyChangedConfigValues(
|
|
293
|
+
data,
|
|
294
|
+
current,
|
|
295
|
+
changedKeys,
|
|
296
|
+
persistAdvisor,
|
|
297
|
+
persistExecutor
|
|
298
|
+
);
|
|
201
299
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
202
300
|
resetConfigCache();
|
|
301
|
+
const nextLoadedState = { ...current };
|
|
302
|
+
if (!persistAdvisor) {
|
|
303
|
+
nextLoadedState.advisor = baseline
|
|
304
|
+
? baseline.advisor
|
|
305
|
+
: configuredModelRef(
|
|
306
|
+
typeof existing.advisor === "string" ? existing.advisor : undefined
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (!persistExecutor) {
|
|
310
|
+
nextLoadedState.executor = baseline
|
|
311
|
+
? baseline.executor
|
|
312
|
+
: configuredModelRef(
|
|
313
|
+
typeof existing.executor === "string" ? existing.executor : undefined
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
loadedConfigState = nextLoadedState;
|
|
317
|
+
loadedConfigPath = path;
|
|
203
318
|
return path;
|
|
204
319
|
};
|
|
205
320
|
|
|
206
321
|
/** Outcome logging is deliberately written only to the global Pi configuration. */
|
|
207
322
|
export const saveGlobalOutcomeLogging = (enabled: boolean) => {
|
|
208
323
|
const path = join(getAgentDir(), "advisor.json");
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
212
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
213
|
-
existing = parsed as Record<string, unknown>;
|
|
214
|
-
}
|
|
215
|
-
} catch {
|
|
216
|
-
/* create it */
|
|
217
|
-
}
|
|
324
|
+
const existing = readExistingConfig(path);
|
|
218
325
|
writeFileSync(
|
|
219
326
|
path,
|
|
220
327
|
`${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
|
|
@@ -135,8 +135,15 @@ export const renderManualAdvisorDialog = (
|
|
|
135
135
|
const cancelLabel = cancel
|
|
136
136
|
? theme.fg("accent", "[Cancel]")
|
|
137
137
|
: theme.fg("text", "[Cancel]");
|
|
138
|
+
const actionButtons = `${submitLabel} ${cancelLabel}`;
|
|
139
|
+
// Center the buttons in the content area; the 2-column focus marker sits
|
|
140
|
+
// left of the computed offset.
|
|
141
|
+
const buttonOffset = Math.max(
|
|
142
|
+
0,
|
|
143
|
+
Math.floor((contentWidth - visibleWidth(actionButtons)) / 2) - 2
|
|
144
|
+
);
|
|
138
145
|
addLine(
|
|
139
|
-
`${focusMarker("actions")}
|
|
146
|
+
`${focusMarker("actions")}${" ".repeat(buttonOffset)}${actionButtons}`
|
|
140
147
|
);
|
|
141
148
|
addWrapped(interactionHint, "dim");
|
|
142
149
|
addLine("");
|
package/src/ui/model-selector.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Input,
|
|
7
7
|
type Keybindings,
|
|
8
8
|
type KeybindingsManager,
|
|
9
|
+
truncateToWidth,
|
|
9
10
|
} from "@earendil-works/pi-tui";
|
|
10
11
|
import type { SearchableModelSelectorOptions } from "./types.js";
|
|
11
12
|
|
|
@@ -58,9 +59,9 @@ export class SearchableModelSelector implements Component, Focusable {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
render(width: number): string[] {
|
|
61
|
-
const lines: string[] = ["
|
|
62
|
+
const lines: string[] = [this.theme.fg("border", "─".repeat(width))];
|
|
62
63
|
lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
|
|
63
|
-
const inputLines = this.searchInput.render(width -
|
|
64
|
+
const inputLines = this.searchInput.render(Math.max(1, width - 10));
|
|
64
65
|
lines.push(
|
|
65
66
|
` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`
|
|
66
67
|
);
|
|
@@ -108,10 +109,10 @@ export class SearchableModelSelector implements Component, Focusable {
|
|
|
108
109
|
}
|
|
109
110
|
lines.push("");
|
|
110
111
|
lines.push(
|
|
111
|
-
` ${this.theme.fg("
|
|
112
|
+
` ${this.theme.fg("dim", "Type to search · ↑↓: navigate · Enter: select · Esc: cancel")}`
|
|
112
113
|
);
|
|
113
|
-
lines.push("
|
|
114
|
-
return lines;
|
|
114
|
+
lines.push(this.theme.fg("border", "─".repeat(width)));
|
|
115
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
handleInput(keyData: string): void {
|
|
@@ -61,6 +61,11 @@ export class TextSettingSubmenu implements Component, Focusable {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
handleInput(keyData: string): void {
|
|
64
|
+
const before = this.input.getValue();
|
|
64
65
|
this.input.handleInput(keyData);
|
|
66
|
+
// A failed submit leaves the error visible; the first value change clears it.
|
|
67
|
+
if (this.error && this.input.getValue() !== before) {
|
|
68
|
+
this.error = undefined;
|
|
69
|
+
}
|
|
65
70
|
}
|
|
66
71
|
}
|