pi-advisor-flow 0.2.4 → 0.2.7
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 +232 -0
- package/README.md +37 -9
- package/package.json +48 -29
- package/src/commands.ts +34 -4
- package/src/config.ts +112 -22
- package/src/conversation.ts +34 -1
- package/src/git.ts +190 -0
- package/src/herdr.ts +9 -9
- package/src/outcomes.ts +85 -0
- package/src/preferences.ts +50 -0
- package/src/session-state.ts +36 -1
- package/src/tools.ts +307 -33
- package/src/ui.ts +104 -24
- package/src/untracked.ts +121 -0
package/src/ui.ts
CHANGED
|
@@ -195,9 +195,12 @@ export interface AdvisorSettings {
|
|
|
195
195
|
effort?: string;
|
|
196
196
|
failureGate: boolean;
|
|
197
197
|
failureMode?: "block-session" | "block-tool" | "warn-and-continue";
|
|
198
|
+
gitContext?: "off" | "summary" | "full";
|
|
199
|
+
gitContextMaxChars?: number;
|
|
198
200
|
herdrIntegration?: boolean;
|
|
199
201
|
loopThreshold?: number;
|
|
200
202
|
maxCallsPerSession?: number;
|
|
203
|
+
outcomeLogging?: boolean;
|
|
201
204
|
planGate: boolean;
|
|
202
205
|
redactSecrets?: boolean;
|
|
203
206
|
sessionSummary?: boolean;
|
|
@@ -205,6 +208,7 @@ export interface AdvisorSettings {
|
|
|
205
208
|
toolPolicies?: Record<string, "full" | "summary" | "exclude">;
|
|
206
209
|
toolResultMaxBytes?: number;
|
|
207
210
|
toolResultMaxLines?: number;
|
|
211
|
+
untrackedContent?: boolean;
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
type AdvisorSettingsRow =
|
|
@@ -227,7 +231,11 @@ type AdvisorSettingsRow =
|
|
|
227
231
|
| "toolResultMaxLines"
|
|
228
232
|
| "toolResultMaxBytes"
|
|
229
233
|
| "redactSecrets"
|
|
234
|
+
| "gitContext"
|
|
235
|
+
| "gitContextMaxChars"
|
|
230
236
|
| "toolPolicies"
|
|
237
|
+
| "outcomeLogging"
|
|
238
|
+
| "untrackedContent"
|
|
231
239
|
| "save";
|
|
232
240
|
|
|
233
241
|
const ADVANCED_ROWS: AdvisorSettingsRow[] = [
|
|
@@ -248,7 +256,11 @@ const ADVANCED_ROWS: AdvisorSettingsRow[] = [
|
|
|
248
256
|
"toolResultMaxLines",
|
|
249
257
|
"toolResultMaxBytes",
|
|
250
258
|
"redactSecrets",
|
|
259
|
+
"gitContext",
|
|
260
|
+
"gitContextMaxChars",
|
|
251
261
|
"toolPolicies",
|
|
262
|
+
"untrackedContent",
|
|
263
|
+
"outcomeLogging",
|
|
252
264
|
];
|
|
253
265
|
|
|
254
266
|
const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
|
|
@@ -361,7 +373,13 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
361
373
|
private visibleRows(): AdvisorSettingsRow[] {
|
|
362
374
|
return this.settings.simpleMode
|
|
363
375
|
? ["context", "simpleMode", "alwaysOn", "save"]
|
|
364
|
-
: [
|
|
376
|
+
: [
|
|
377
|
+
"context",
|
|
378
|
+
"simpleMode",
|
|
379
|
+
"alwaysOn",
|
|
380
|
+
...ADVANCED_ROWS.slice(1),
|
|
381
|
+
"save",
|
|
382
|
+
];
|
|
365
383
|
}
|
|
366
384
|
|
|
367
385
|
private selectedRowId(): AdvisorSettingsRow {
|
|
@@ -465,10 +483,58 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
465
483
|
this.options.onCancel();
|
|
466
484
|
}
|
|
467
485
|
|
|
486
|
+
/** Disclosure and output-limit rows, split out to keep each builder simple. */
|
|
487
|
+
private disclosureRows(): string[] {
|
|
488
|
+
const onOff = (value: boolean) => (value ? "On" : "Off");
|
|
489
|
+
return [
|
|
490
|
+
this.row(
|
|
491
|
+
"Tool result lines",
|
|
492
|
+
String(this.settings.toolResultMaxLines ?? 2000),
|
|
493
|
+
"toolResultMaxLines"
|
|
494
|
+
),
|
|
495
|
+
this.row(
|
|
496
|
+
"Tool result bytes",
|
|
497
|
+
String(this.settings.toolResultMaxBytes ?? 50 * 1024),
|
|
498
|
+
"toolResultMaxBytes"
|
|
499
|
+
),
|
|
500
|
+
this.row(
|
|
501
|
+
"Redact common secrets",
|
|
502
|
+
onOff(this.settings.redactSecrets ?? false),
|
|
503
|
+
"redactSecrets"
|
|
504
|
+
),
|
|
505
|
+
this.row(
|
|
506
|
+
"Repository context",
|
|
507
|
+
this.settings.gitContext ?? "summary",
|
|
508
|
+
"gitContext"
|
|
509
|
+
),
|
|
510
|
+
this.row(
|
|
511
|
+
"Repository context chars",
|
|
512
|
+
String(this.settings.gitContextMaxChars ?? 20_000),
|
|
513
|
+
"gitContextMaxChars"
|
|
514
|
+
),
|
|
515
|
+
this.row(
|
|
516
|
+
"Tool disclosure policies",
|
|
517
|
+
Object.keys(this.settings.toolPolicies ?? {}).length
|
|
518
|
+
? "Exact names configured"
|
|
519
|
+
: "All tools: full",
|
|
520
|
+
"toolPolicies"
|
|
521
|
+
),
|
|
522
|
+
this.row(
|
|
523
|
+
"Untracked file content",
|
|
524
|
+
onOff(this.settings.untrackedContent ?? false),
|
|
525
|
+
"untrackedContent"
|
|
526
|
+
),
|
|
527
|
+
this.row(
|
|
528
|
+
"Outcome logging (global)",
|
|
529
|
+
onOff(this.settings.outcomeLogging ?? false),
|
|
530
|
+
"outcomeLogging"
|
|
531
|
+
),
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
|
|
468
535
|
private advancedRows(width: number): string[] {
|
|
469
536
|
const onOff = (value: boolean) => (value ? "On" : "Off");
|
|
470
537
|
const rows = [
|
|
471
|
-
this.row("Context window", this.currentContext().label, "context"),
|
|
472
538
|
this.row("Advisor reasoning", this.currentEffort(), "effort"),
|
|
473
539
|
this.row("Plan gate", onOff(this.settings.planGate), "planGate"),
|
|
474
540
|
this.row("Failure gate", onOff(this.settings.failureGate), "failureGate"),
|
|
@@ -524,28 +590,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
524
590
|
onOff(this.settings.herdrIntegration ?? true),
|
|
525
591
|
"herdrIntegration"
|
|
526
592
|
),
|
|
527
|
-
this.
|
|
528
|
-
"Tool result lines",
|
|
529
|
-
String(this.settings.toolResultMaxLines ?? 2000),
|
|
530
|
-
"toolResultMaxLines"
|
|
531
|
-
),
|
|
532
|
-
this.row(
|
|
533
|
-
"Tool result bytes",
|
|
534
|
-
String(this.settings.toolResultMaxBytes ?? 50 * 1024),
|
|
535
|
-
"toolResultMaxBytes"
|
|
536
|
-
),
|
|
537
|
-
this.row(
|
|
538
|
-
"Redact common secrets",
|
|
539
|
-
onOff(this.settings.redactSecrets ?? false),
|
|
540
|
-
"redactSecrets"
|
|
541
|
-
),
|
|
542
|
-
this.row(
|
|
543
|
-
"Tool disclosure policies",
|
|
544
|
-
Object.keys(this.settings.toolPolicies ?? {}).length
|
|
545
|
-
? "Exact names configured"
|
|
546
|
-
: "All tools: full",
|
|
547
|
-
"toolPolicies"
|
|
548
|
-
),
|
|
593
|
+
...this.disclosureRows(),
|
|
549
594
|
];
|
|
550
595
|
if (this.editingCustom) {
|
|
551
596
|
rows.push(
|
|
@@ -630,6 +675,9 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
630
675
|
}
|
|
631
676
|
}
|
|
632
677
|
const heading = `Recent history${" ".repeat(Math.max(1, trackWidth - "Recent history".length - "Full branch".length))}Full branch`;
|
|
678
|
+
lines.push(
|
|
679
|
+
` ${this.row("Context window", this.currentContext().label, "context")}`
|
|
680
|
+
);
|
|
633
681
|
lines.push(` ${theme.fg("muted", heading)}`);
|
|
634
682
|
lines.push(` ${theme.fg("muted", track.join(""))}`);
|
|
635
683
|
lines.push(` ${theme.fg("text", labels.join(""))}`);
|
|
@@ -794,6 +842,38 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
794
842
|
case "redactSecrets":
|
|
795
843
|
this.settings.redactSecrets = !(this.settings.redactSecrets ?? false);
|
|
796
844
|
break;
|
|
845
|
+
case "untrackedContent":
|
|
846
|
+
this.settings.untrackedContent = !(
|
|
847
|
+
this.settings.untrackedContent ?? false
|
|
848
|
+
);
|
|
849
|
+
break;
|
|
850
|
+
case "outcomeLogging":
|
|
851
|
+
this.settings.outcomeLogging = !(this.settings.outcomeLogging ?? false);
|
|
852
|
+
break;
|
|
853
|
+
case "gitContext": {
|
|
854
|
+
const levels: AdvisorSettings["gitContext"][] = [
|
|
855
|
+
"off",
|
|
856
|
+
"summary",
|
|
857
|
+
"full",
|
|
858
|
+
];
|
|
859
|
+
const index = Math.max(
|
|
860
|
+
0,
|
|
861
|
+
levels.indexOf(this.settings.gitContext ?? "summary")
|
|
862
|
+
);
|
|
863
|
+
this.settings.gitContext =
|
|
864
|
+
levels[Math.max(0, Math.min(levels.length - 1, index + direction))];
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
case "gitContextMaxChars": {
|
|
868
|
+
const values = [0, 5000, 10_000, 20_000, 50_000, 100_000];
|
|
869
|
+
const index = Math.max(
|
|
870
|
+
0,
|
|
871
|
+
values.indexOf(this.settings.gitContextMaxChars ?? 20_000)
|
|
872
|
+
);
|
|
873
|
+
this.settings.gitContextMaxChars =
|
|
874
|
+
values[Math.max(0, Math.min(values.length - 1, index + direction))];
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
797
877
|
case "toolResultMaxLines": {
|
|
798
878
|
const values = [0, 500, 1000, 2000, 5000, 10_000];
|
|
799
879
|
const index = Math.max(
|
package/src/untracked.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { redactAndCapText } from "./conversation.js";
|
|
5
|
+
|
|
6
|
+
export const UNTRACKED_FILE_MAX_BYTES = 8 * 1024;
|
|
7
|
+
export const UNTRACKED_TOTAL_MAX_BYTES = 24 * 1024;
|
|
8
|
+
export interface UntrackedAttachment {
|
|
9
|
+
bytes: number;
|
|
10
|
+
path: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PATH_SEGMENTS = /[\\/]/;
|
|
15
|
+
|
|
16
|
+
const within = (root: string, candidate: string) => {
|
|
17
|
+
const path = relative(root, candidate);
|
|
18
|
+
return path !== "" && !path.startsWith("..") && !path.includes("../");
|
|
19
|
+
};
|
|
20
|
+
const normalizeRelativePath = (root: string, path: string) =>
|
|
21
|
+
relative(root, resolve(root, path));
|
|
22
|
+
|
|
23
|
+
const normalizeRequestedPath = (root: string, value: unknown) => {
|
|
24
|
+
if (
|
|
25
|
+
typeof value !== "string" ||
|
|
26
|
+
!value ||
|
|
27
|
+
isAbsolute(value) ||
|
|
28
|
+
value.split(PATH_SEGMENTS).includes("..")
|
|
29
|
+
) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return normalizeRelativePath(root, value);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const untracked = (cwd: string, path: string) => {
|
|
36
|
+
const output = execFileSync(
|
|
37
|
+
"git",
|
|
38
|
+
["ls-files", "--others", "--exclude-standard", "-z", "--", path],
|
|
39
|
+
{
|
|
40
|
+
cwd,
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
43
|
+
shell: false,
|
|
44
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
45
|
+
timeout: 5000,
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
const expected = normalizeRelativePath(cwd, path);
|
|
50
|
+
return output
|
|
51
|
+
.split("\0")
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.some((entry) => normalizeRelativePath(cwd, entry) === expected);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Returns only exact, permitted untracked regular-file bodies. Refusals are silent. */
|
|
57
|
+
export const readUntrackedFiles = async (
|
|
58
|
+
cwd: string,
|
|
59
|
+
requested: string[],
|
|
60
|
+
enabled: boolean,
|
|
61
|
+
redact: boolean
|
|
62
|
+
): Promise<UntrackedAttachment[]> => {
|
|
63
|
+
if (!(enabled && Array.isArray(requested))) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const root = await realpath(cwd).catch(() => undefined);
|
|
67
|
+
if (!root) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const unique = new Set<string>();
|
|
71
|
+
const attachments: UntrackedAttachment[] = [];
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const name of requested) {
|
|
74
|
+
const normalizedName = normalizeRequestedPath(root, name);
|
|
75
|
+
if (!normalizedName || unique.has(normalizedName)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
unique.add(normalizedName);
|
|
79
|
+
try {
|
|
80
|
+
const absolute = resolve(root, normalizedName);
|
|
81
|
+
if (!(within(root, absolute) && untracked(root, normalizedName))) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Sequentially enforce the aggregate disclosure budget.
|
|
85
|
+
// biome-ignore lint/performance/noAwaitInLoops: each accepted file consumes the remaining budget.
|
|
86
|
+
const stats = await lstat(absolute);
|
|
87
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const resolved = await realpath(absolute);
|
|
91
|
+
if (!within(root, resolved)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const file = await open(resolved, "r");
|
|
95
|
+
try {
|
|
96
|
+
const available = Math.min(
|
|
97
|
+
UNTRACKED_FILE_MAX_BYTES,
|
|
98
|
+
UNTRACKED_TOTAL_MAX_BYTES - total
|
|
99
|
+
);
|
|
100
|
+
if (available <= 0) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const buffer = Buffer.alloc(available + 1);
|
|
104
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
105
|
+
const text = redactAndCapText(
|
|
106
|
+
buffer.subarray(0, bytesRead).toString("utf8"),
|
|
107
|
+
available,
|
|
108
|
+
redact
|
|
109
|
+
);
|
|
110
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
111
|
+
attachments.push({ bytes, path: normalizedName, text });
|
|
112
|
+
total += bytes;
|
|
113
|
+
} finally {
|
|
114
|
+
await file.close();
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
/* refuse unreadable or non-git paths */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return attachments;
|
|
121
|
+
};
|