@andares/pi-toolkits 0.7.3 → 0.9.0
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/README.md +78 -6
- package/README.zh-CN.md +77 -6
- package/package.json +13 -7
- package/src/features/ask/index.test.ts +118 -0
- package/src/features/ask/index.ts +31 -13
- package/src/features/ask/reasons.ts +2 -2
- package/src/features/autostart/autostart.test.ts +45 -2
- package/src/features/autostart/autostart.ts +4 -2
- package/src/features/autostart/index.ts +100 -48
- package/src/features/compact/index.ts +3 -3
- package/src/features/compact/prompt.ts +2 -2
- package/src/features/favorites/constants.ts +3 -3
- package/src/features/favorites/patch-cycle.ts +6 -6
- package/src/features/favorites/patch-selector.ts +4 -4
- package/src/features/permission/constants.test.ts +60 -0
- package/src/features/permission/constants.ts +158 -0
- package/src/features/permission/index.test.ts +340 -0
- package/src/features/permission/index.ts +333 -0
- package/src/features/permission/modes.test.ts +66 -0
- package/src/features/permission/modes.ts +82 -0
- package/src/features/permission/panel.ts +118 -0
- package/src/features/permission/paths.test.ts +111 -0
- package/src/features/permission/paths.ts +126 -0
- package/src/features/permission/policy.test.ts +237 -0
- package/src/features/permission/policy.ts +338 -0
- package/src/features/permission/prompt.ts +146 -0
- package/src/features/permission/session-rules.ts +99 -0
- package/src/features/permission/units.test.ts +205 -0
- package/src/features/permission/units.ts +995 -0
- package/src/features/stash/stash.ts +1 -1
- package/src/index.ts +15 -2
- package/src/lib/gates.test.ts +171 -0
- package/src/lib/gates.ts +153 -0
- package/src/lib/model-config.ts +2 -2
- package/src/lib/model-picker.ts +2 -2
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
AUTOSTART_CONFIG_PATH,
|
|
17
17
|
AutostartParseError,
|
|
18
18
|
commandNameOf,
|
|
19
|
+
GLOBAL_AUTOSTART_FILENAME,
|
|
19
20
|
parseAutostartConfig,
|
|
20
21
|
RUNNABLE_REASONS,
|
|
21
22
|
} from "./autostart.js";
|
|
@@ -138,8 +139,16 @@ interface Harness {
|
|
|
138
139
|
async function makeHarness(
|
|
139
140
|
config: string | null,
|
|
140
141
|
configAsDir = false,
|
|
142
|
+
globalConfig: string | null = null,
|
|
141
143
|
): Promise<Harness> {
|
|
142
144
|
const cwd = await mkdtemp(join(tmpdir(), "pi-autostart-test-"));
|
|
145
|
+
// The agent dir lives inside the temp project so cleanup covers it; passing
|
|
146
|
+
// it explicitly keeps the real ~/.pi/agent out of the test.
|
|
147
|
+
const agentDir = join(cwd, "agent");
|
|
148
|
+
await mkdir(agentDir, { recursive: true });
|
|
149
|
+
if (globalConfig !== null) {
|
|
150
|
+
await writeFile(join(agentDir, GLOBAL_AUTOSTART_FILENAME), globalConfig, "utf8");
|
|
151
|
+
}
|
|
143
152
|
if (configAsDir) {
|
|
144
153
|
// EISDIR setup: the config path exists but is a directory.
|
|
145
154
|
await mkdir(join(cwd, ".pi"), { recursive: true });
|
|
@@ -172,7 +181,7 @@ async function makeHarness(
|
|
|
172
181
|
}
|
|
173
182
|
},
|
|
174
183
|
} as unknown as ExtensionAPI;
|
|
175
|
-
registerAutostart(pi);
|
|
184
|
+
registerAutostart(pi, { agentDir });
|
|
176
185
|
return {
|
|
177
186
|
handler,
|
|
178
187
|
sent,
|
|
@@ -209,8 +218,9 @@ afterEach(async () => {
|
|
|
209
218
|
async function harness(
|
|
210
219
|
config: string | null,
|
|
211
220
|
configAsDir = false,
|
|
221
|
+
globalConfig: string | null = null,
|
|
212
222
|
): Promise<Harness> {
|
|
213
|
-
const h = await makeHarness(config, configAsDir);
|
|
223
|
+
const h = await makeHarness(config, configAsDir, globalConfig);
|
|
214
224
|
current = h;
|
|
215
225
|
return h;
|
|
216
226
|
}
|
|
@@ -312,4 +322,37 @@ describe("registerAutostart", () => {
|
|
|
312
322
|
expect(h.notifications).toHaveLength(1);
|
|
313
323
|
expect(h.notifications[0]?.type).toBe("error");
|
|
314
324
|
});
|
|
325
|
+
|
|
326
|
+
it("runs the global config before the project config", async () => {
|
|
327
|
+
const h = await harness(
|
|
328
|
+
'[command]\nrun = ["/add-dir ../project"]',
|
|
329
|
+
false,
|
|
330
|
+
'[command]\nrun = ["/ask on"]',
|
|
331
|
+
);
|
|
332
|
+
await h.start("startup");
|
|
333
|
+
expect(h.sent.map((entry) => entry.command)).toEqual([
|
|
334
|
+
"/ask on",
|
|
335
|
+
"/add-dir ../project",
|
|
336
|
+
]);
|
|
337
|
+
expect(h.notifications).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("runs the global config in an untrusted project but skips the project one", async () => {
|
|
341
|
+
const h = await harness(
|
|
342
|
+
'[command]\nrun = ["/add-dir ../project"]',
|
|
343
|
+
false,
|
|
344
|
+
'[command]\nrun = ["/ask on"]',
|
|
345
|
+
);
|
|
346
|
+
h.setTrusted(false);
|
|
347
|
+
await h.start("startup");
|
|
348
|
+
expect(h.sent.map((entry) => entry.command)).toEqual(["/ask on"]);
|
|
349
|
+
expect(h.notifications).toHaveLength(1);
|
|
350
|
+
expect(h.notifications[0]?.type).toBe("warning");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("keeps running the project config when the global one is absent", async () => {
|
|
354
|
+
const h = await harness('[command]\nrun = ["/ask on"]', false, null);
|
|
355
|
+
await h.start("startup");
|
|
356
|
+
expect(h.sent.map((entry) => entry.command)).toEqual(["/ask on"]);
|
|
357
|
+
});
|
|
315
358
|
});
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { parse as parseToml } from "smol-toml";
|
|
20
20
|
|
|
21
|
-
/** Project-relative location of the autostart config
|
|
22
|
-
* project-config convention, precedent: `.pi/mcp.json`). */
|
|
21
|
+
/** Project-relative location of the project autostart config. */
|
|
23
22
|
export const AUTOSTART_CONFIG_PATH = ".pi/autostart.toml";
|
|
24
23
|
|
|
24
|
+
/** File name of the operator's global autostart config, inside the agent dir. */
|
|
25
|
+
export const GLOBAL_AUTOSTART_FILENAME = "autostart.toml";
|
|
26
|
+
|
|
25
27
|
/**
|
|
26
28
|
* session_start reasons that replay autostart commands:
|
|
27
29
|
* - startup / new: fresh session in the project — replay
|
|
@@ -1,81 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* autostart feature — run
|
|
2
|
+
* autostart feature — run declared slash commands at session start.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the schema). Execution gates, in order:
|
|
4
|
+
* Two sources, executed in order, never merged:
|
|
6
5
|
*
|
|
7
|
-
* 1.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
6
|
+
* 1. `<agentDir>/autostart.toml` — the operator's own defaults. Runs in every
|
|
7
|
+
* project; no trust gate, because the file lives in pi's config directory
|
|
8
|
+
* and is not controlled by the project.
|
|
9
|
+
* 2. `<project root>/.pi/autostart.toml` — project-declared commands. Gated on
|
|
10
|
+
* `ctx.isProjectTrusted()`: an untrusted repository must not execute
|
|
11
|
+
* anything.
|
|
12
|
+
*
|
|
13
|
+
* Execution gates, in order:
|
|
14
|
+
*
|
|
15
|
+
* - reason filter — only startup/new/fork replay (see RUNNABLE_REASONS)
|
|
16
|
+
* - config file exists — a missing file is a silent opt-in skip
|
|
17
|
+
* - project trust (project source only)
|
|
18
|
+
* - per-command pre-validation against `pi.getCommands()`: only commands
|
|
19
|
+
* registered by an *extension* are dispatched. This is mandatory:
|
|
20
|
+
* sendUserMessage with an unhandled "/x" falls through prompt() to the
|
|
21
|
+
* model as a plain user message (pi agent-session.js), so an unknown
|
|
22
|
+
* command must be skipped here, never sent.
|
|
15
23
|
*
|
|
16
24
|
* Dispatch uses `sendUserMessage(cmd, { expandPromptTemplates: true })` —
|
|
17
25
|
* that flag is what routes "/cmd args" through _tryExecuteExtensionCommand
|
|
18
|
-
* instead of the model prompt path. The wrapper defaults it to false
|
|
19
|
-
* (each byte-identical across every published version of the supported
|
|
20
|
-
* window, 0.
|
|
21
|
-
*
|
|
26
|
+
* instead of the model prompt path. The wrapper defaults it to false in this
|
|
27
|
+
* package (each byte-identical across every published version of the supported
|
|
28
|
+
* window, 1.0.0 → 1.3.0; see AGENTS.md host-compat discipline for the version
|
|
29
|
+
* matrix).
|
|
22
30
|
*
|
|
23
31
|
* Only extension commands can run this way. Built-in commands (/compact,
|
|
24
32
|
* /reload, /resume, …) are dispatched at the TUI layer and cannot be
|
|
25
33
|
* triggered from the extension layer; skill commands (/skill:name) expand
|
|
26
|
-
* into prompts rather than execute. First-party use
|
|
27
|
-
* (pi-add-dir
|
|
34
|
+
* into prompts rather than execute. First-party use cases: `/add-dir`
|
|
35
|
+
* (pi-add-dir, idempotent) and `/ask-perm` (session-scoped permission).
|
|
28
36
|
*/
|
|
29
37
|
import { readFile } from "node:fs/promises";
|
|
30
38
|
import { join } from "node:path";
|
|
31
|
-
import
|
|
39
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
40
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
32
41
|
import {
|
|
33
42
|
AUTOSTART_CONFIG_PATH,
|
|
34
43
|
AutostartParseError,
|
|
35
44
|
commandNameOf,
|
|
45
|
+
GLOBAL_AUTOSTART_FILENAME,
|
|
36
46
|
parseAutostartConfig,
|
|
37
47
|
RUNNABLE_REASONS,
|
|
38
48
|
} from "./autostart.js";
|
|
39
49
|
|
|
40
|
-
export
|
|
41
|
-
pi
|
|
42
|
-
|
|
50
|
+
export interface AutostartOptions {
|
|
51
|
+
/** pi's config directory; defaults to `getAgentDir()`. */
|
|
52
|
+
readonly agentDir?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Source {
|
|
56
|
+
readonly path: string;
|
|
57
|
+
readonly label: string;
|
|
58
|
+
/** Project sources require trust; the operator's global file does not. */
|
|
59
|
+
readonly requiresTrust: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function messageOf(error: unknown): string {
|
|
63
|
+
return error instanceof Error ? error.message : String(error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerAutostart(
|
|
67
|
+
pi: ExtensionAPI,
|
|
68
|
+
options: AutostartOptions = {},
|
|
69
|
+
): void {
|
|
70
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
43
71
|
|
|
72
|
+
async function readCommands(
|
|
73
|
+
ctx: ExtensionContext,
|
|
74
|
+
source: Source,
|
|
75
|
+
): Promise<string[] | undefined> {
|
|
44
76
|
let text: string;
|
|
45
77
|
try {
|
|
46
|
-
text = await readFile(
|
|
78
|
+
text = await readFile(source.path, "utf8");
|
|
47
79
|
} catch (err) {
|
|
48
80
|
// Missing file = opt-in skip. Any other read failure (EACCES, EISDIR,
|
|
49
81
|
// …) is a misconfiguration the operator should hear about.
|
|
50
82
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
51
83
|
ctx.ui.notify(
|
|
52
|
-
`autostart: skipped — cannot read ${
|
|
84
|
+
`autostart: skipped ${source.label} — cannot read ${source.path}: ${messageOf(err)}`,
|
|
53
85
|
"warning",
|
|
54
86
|
);
|
|
55
87
|
}
|
|
56
|
-
return;
|
|
88
|
+
return undefined;
|
|
57
89
|
}
|
|
58
90
|
|
|
59
|
-
let commands: string[];
|
|
60
91
|
try {
|
|
61
|
-
|
|
92
|
+
return parseAutostartConfig(text);
|
|
62
93
|
} catch (err) {
|
|
63
94
|
const message = err instanceof AutostartParseError ? err.message : String(err);
|
|
64
95
|
ctx.ui.notify(
|
|
65
|
-
`autostart: skipped — ${message} (${
|
|
96
|
+
`autostart: skipped ${source.label} — ${message} (${source.path})`,
|
|
66
97
|
"error",
|
|
67
98
|
);
|
|
68
|
-
return;
|
|
99
|
+
return undefined;
|
|
69
100
|
}
|
|
70
|
-
|
|
101
|
+
}
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
pi.on("session_start", async (event, ctx) => {
|
|
104
|
+
if (!RUNNABLE_REASONS.has(event.reason)) return;
|
|
105
|
+
|
|
106
|
+
const sources: Source[] = [
|
|
107
|
+
{
|
|
108
|
+
path: join(agentDir, GLOBAL_AUTOSTART_FILENAME),
|
|
109
|
+
label: "global config",
|
|
110
|
+
requiresTrust: false,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
path: join(ctx.cwd, AUTOSTART_CONFIG_PATH),
|
|
114
|
+
label: "project config",
|
|
115
|
+
requiresTrust: true,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
79
118
|
|
|
80
119
|
// Only extension-registered commands are dispatchable; skill and
|
|
81
120
|
// prompt-template entries expand rather than execute.
|
|
@@ -87,23 +126,36 @@ export function registerAutostart(pi: ExtensionAPI): void {
|
|
|
87
126
|
),
|
|
88
127
|
);
|
|
89
128
|
|
|
90
|
-
for (const
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
129
|
+
for (const source of sources) {
|
|
130
|
+
const commands = await readCommands(ctx, source);
|
|
131
|
+
if (commands === undefined || commands.length === 0) continue;
|
|
132
|
+
|
|
133
|
+
if (source.requiresTrust && !ctx.isProjectTrusted()) {
|
|
93
134
|
ctx.ui.notify(
|
|
94
|
-
`autostart: skipped
|
|
135
|
+
`autostart: skipped ${source.label} — project not trusted (${source.path})`,
|
|
95
136
|
"warning",
|
|
96
137
|
);
|
|
97
138
|
continue;
|
|
98
139
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
|
|
141
|
+
for (const command of commands) {
|
|
142
|
+
const name = commandNameOf(command);
|
|
143
|
+
if (!name || !dispatchable.has(name)) {
|
|
144
|
+
ctx.ui.notify(
|
|
145
|
+
`autostart: skipped "${command}" — not a registered extension command`,
|
|
146
|
+
"warning",
|
|
147
|
+
);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// One failing command must not cancel the rest of the list.
|
|
151
|
+
try {
|
|
152
|
+
await pi.sendUserMessage(command, { expandPromptTemplates: true });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
ctx.ui.notify(
|
|
155
|
+
`autostart: "${command}" failed — ${messageOf(err)}`,
|
|
156
|
+
"error",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
107
159
|
}
|
|
108
160
|
}
|
|
109
161
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - prompts, message assembly, split-turn handling, output budgets, and the
|
|
13
13
|
* summary file-list tail are verbatim pi built-in behavior (copied from
|
|
14
14
|
* 0.87.1, byte-identical across every published version of the supported
|
|
15
|
-
* window, 0.
|
|
15
|
+
* window, 1.0.0 → 1.3.0) — only the executing model differs. The one
|
|
16
16
|
* exception is `serializeConversation`, which is imported from the host
|
|
17
17
|
* instead of mirrored, so it tracks whatever the running host does
|
|
18
18
|
* (1.2.0 stopped feeding assistant thinking into the request).
|
|
@@ -119,7 +119,7 @@ async function completeSummary(
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* pi's output-budget formula (compaction.js, 0.87.1 → 1.
|
|
122
|
+
* pi's output-budget formula (compaction.js, 0.87.1 → 1.3.0 unchanged): a
|
|
123
123
|
* fraction of reserveTokens clamped by the model's own output cap.
|
|
124
124
|
*/
|
|
125
125
|
function summarizationBudget(
|
|
@@ -136,7 +136,7 @@ function summarizationBudget(
|
|
|
136
136
|
/**
|
|
137
137
|
* Run the full compaction summarization with the configured model,
|
|
138
138
|
* replicating pi's compact() (0.87.1, byte-identical across the supported
|
|
139
|
-
* 0.
|
|
139
|
+
* 1.0.0 → 1.3.0 window):
|
|
140
140
|
* history summary (first-connection or iterative-update prompt, custom
|
|
141
141
|
* focus appended), plus a separate turn-prefix checkpoint when the cut point
|
|
142
142
|
* splits a turn, merged into one summary with the file-list tail. Returns
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* summaries produced by the configured compact model are formatted exactly
|
|
7
7
|
* like pi's own compaction output. Only the executing model differs.
|
|
8
8
|
* Verified byte-identical against every published version of the supported
|
|
9
|
-
* window (0.
|
|
9
|
+
* window (1.0.0 → 1.3.0): dist/core/compaction/compaction.js and
|
|
10
10
|
* core/usage-totals.js are unchanged files across that span, and the only
|
|
11
11
|
* utils.js change (1.2.0 dropping assistant thinking from
|
|
12
12
|
* serializeConversation) does not touch the strings mirrored here — that
|
|
@@ -203,7 +203,7 @@ export function formatFileOperations(
|
|
|
203
203
|
/**
|
|
204
204
|
* Combine two provider usage records field-wise (pi 0.87.1 compaction.js
|
|
205
205
|
* combineUsage, moved verbatim to core/usage-totals.js in 0.99.0 and
|
|
206
|
-
* unchanged through 1.
|
|
206
|
+
* unchanged through 1.3.0): used when
|
|
207
207
|
* a split turn produces two summarization calls.
|
|
208
208
|
*/
|
|
209
209
|
export function combineUsage<T extends UsageLike>(first: T, second: T): T {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* keybinding conflicts arise.
|
|
10
10
|
*
|
|
11
11
|
* Verified against every published version of the supported window
|
|
12
|
-
* (0.
|
|
12
|
+
* (1.0.0 → 1.3.0) keybinding
|
|
13
13
|
* tables + pi-tui input dispatch on WSL2 + Windows Terminal; re-run
|
|
14
14
|
* `pnpm check:keybindings` after every host upgrade before trusting this.
|
|
15
15
|
*/
|
|
@@ -30,11 +30,11 @@ export const FAVORITES_FILE = "pi-toolkits-favorites.json";
|
|
|
30
30
|
* Windows/WSL `tui.altScreen.search` resolves to ctrl+f, and
|
|
31
31
|
* `TuiAltScreen.handleViewportInput()` checks it before
|
|
32
32
|
* `shouldDeferViewportInputToOverlay()`, so the keystroke opens transcript
|
|
33
|
-
* search and the selector never sees it (verified in pi-tui 1.
|
|
33
|
+
* search and the selector never sees it (verified in pi-tui 1.3.0
|
|
34
34
|
* `dist/tui-alt-screen.js`: the search check is the first key branch and
|
|
35
35
|
* returns `{ consume: true }` unconditionally).
|
|
36
36
|
*
|
|
37
|
-
* Why ctrl+alt+f was discarded: clean at the pi layer across 0
|
|
37
|
+
* Why ctrl+alt+f was discarded: clean at the pi layer across 1.0–1.3 (only
|
|
38
38
|
* ctrl+alt+] is taken from that family) and byte-distinguishable without
|
|
39
39
|
* kitty — but grabbed at the OS/app layer on the author's machine by a
|
|
40
40
|
* Windows screenshot tool, so it never reached the terminal. Lesson: a key
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* model available" feedback); otherwise jump to it
|
|
16
16
|
* - the apply logic mirrors pi's _cycleScopedModel/_cycleAvailableModel
|
|
17
17
|
* (byte-identical across every published version in the supported window
|
|
18
|
-
* 0.
|
|
18
|
+
* 1.0.0 → 1.3.0:
|
|
19
19
|
* state swap, session persistence, default-model persistence only when
|
|
20
20
|
* `options.persist` is set, thinking-level re-clamp, model_select emit
|
|
21
21
|
* with source "cycle")
|
|
@@ -31,9 +31,9 @@ import { getFavoritesStore, modelKey } from "./store.js";
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* pi's `Model<any>` — the exact type of `ModelCycleResult.model`
|
|
34
|
-
* (pi-coding-agent 0
|
|
34
|
+
* (pi-coding-agent 1.0–1.3; the field is unchanged across that span).
|
|
35
35
|
* Derived here instead of importing `Model` from @earendil-works/pi-ai,
|
|
36
|
-
* whose index.d.ts (0
|
|
36
|
+
* whose index.d.ts (1.0 through 1.3.0 alike) re-exports it via an
|
|
37
37
|
* extensioned `"./types.ts"` specifier that not every TS resolver substitutes
|
|
38
38
|
* to `types.d.ts` (tsc 5.9 bundler does; other tsserver builds don't).
|
|
39
39
|
*/
|
|
@@ -57,7 +57,7 @@ interface CycleSessionInternals {
|
|
|
57
57
|
setDefaultModelAndProvider(provider: string, id: string): void;
|
|
58
58
|
};
|
|
59
59
|
/**
|
|
60
|
-
* pi 0
|
|
60
|
+
* pi 1.0–1.3: keeps a persisted default usable when cycling inside a
|
|
61
61
|
* non-empty --models scope.
|
|
62
62
|
*/
|
|
63
63
|
_addPersistedDefaultToNonEmptyScope(model: unknown): void;
|
|
@@ -140,7 +140,7 @@ export function applyCyclePatch(): boolean {
|
|
|
140
140
|
return original.call(this, direction, options);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// Same source and availability filtering as pi's stock cycling (0
|
|
143
|
+
// Same source and availability filtering as pi's stock cycling (1.0–1.3).
|
|
144
144
|
const scoped = this._scopedModels.length > 0;
|
|
145
145
|
let candidates: CycleCandidate[];
|
|
146
146
|
if (scoped) {
|
|
@@ -185,7 +185,7 @@ export function applyCyclePatch(): boolean {
|
|
|
185
185
|
: this._getThinkingLevelForModelSwitch();
|
|
186
186
|
this.agent.state.model = next.model;
|
|
187
187
|
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
|
|
188
|
-
// pi 0
|
|
188
|
+
// pi 1.0–1.3: only persist the default model when the caller asked for it.
|
|
189
189
|
if (options.persist) {
|
|
190
190
|
this.settingsManager.setDefaultModelAndProvider(
|
|
191
191
|
next.model.provider,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* keeps loading and ctrl+p falls back to stock behavior. The prototype
|
|
23
23
|
* shape touched here (handleInput/updateList/filterModels and the instance
|
|
24
24
|
* fields below) is byte-identical across every published version in
|
|
25
|
-
* pi 0.
|
|
25
|
+
* pi 1.0.0 → 1.3.0 (and 0.99.0 → 1.2.0 before the floor moved).
|
|
26
26
|
*/
|
|
27
27
|
import {
|
|
28
28
|
ModelSelectorComponent,
|
|
@@ -61,7 +61,7 @@ interface SelectorPatchTarget {
|
|
|
61
61
|
handleInput(keyData: string): void;
|
|
62
62
|
updateList(): void;
|
|
63
63
|
filterModels(query: string): void;
|
|
64
|
-
/** pi 0
|
|
64
|
+
/** pi 1.0–1.3: default-model badge source. */
|
|
65
65
|
isDefaultModel(model: ModelRef): boolean;
|
|
66
66
|
listContainer: ListContainerLike;
|
|
67
67
|
searchInput: SearchInputLike;
|
|
@@ -116,7 +116,7 @@ let patched = false;
|
|
|
116
116
|
export function applyModelSelectorPatches(): boolean {
|
|
117
117
|
if (patched) return true;
|
|
118
118
|
// SAFETY: ModelSelectorComponent's members are TS-private (compile-time
|
|
119
|
-
// only); the structural view matches pi 0
|
|
119
|
+
// only); the structural view matches pi 1.0–1.3's prototype shape
|
|
120
120
|
// (byte-identical across that span) and every touched method is
|
|
121
121
|
// existence-checked below before any patching happens.
|
|
122
122
|
const proto =
|
|
@@ -155,7 +155,7 @@ export function applyModelSelectorPatches(): boolean {
|
|
|
155
155
|
};
|
|
156
156
|
|
|
157
157
|
// ── 2. List rendering: hint row + bold bright yellow favorites ────────
|
|
158
|
-
// Replicates the built-in updateList layout (pi 0
|
|
158
|
+
// Replicates the built-in updateList layout (pi 1.0–1.3: fixed-width cursor
|
|
159
159
|
// and ✓ current-marker columns, provider badge, `· default` badge, scroll
|
|
160
160
|
// indicator, empty/error states, refresh status, Model Name footer) with
|
|
161
161
|
// one change: favorited model ids render in the favorite style.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed safety tables: credential paths are denied in every mode, destructive
|
|
3
|
+
* commands always ask.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { dangerReason, secretReason } from "./constants.js";
|
|
9
|
+
|
|
10
|
+
const HOME = homedir();
|
|
11
|
+
|
|
12
|
+
describe("secrets table", () => {
|
|
13
|
+
it("covers the credential directories exactly", () => {
|
|
14
|
+
expect(secretReason(join(HOME, ".ssh", "id_rsa"))).toBe("credentials");
|
|
15
|
+
expect(secretReason(join(HOME, ".ssh"))).toBe("credentials");
|
|
16
|
+
expect(secretReason(join(HOME, ".gnupg", "private-keys-v1.d"))).toBe("credentials");
|
|
17
|
+
expect(secretReason(join(HOME, ".aws", "credentials"))).toBe("credentials");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("does not over-match a sibling prefix", () => {
|
|
21
|
+
expect(secretReason(join(HOME, ".ssh-easy", "x"))).toBeUndefined();
|
|
22
|
+
expect(secretReason(join(HOME, ".ssh_evil", "x"))).toBeUndefined();
|
|
23
|
+
expect(secretReason(join(HOME, ".awsconfig", "x"))).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("covers pi auth files but nothing else under the agent dir", () => {
|
|
27
|
+
expect(secretReason(join(HOME, ".pi", "agent", "auth.json"))).toBe("pi credentials");
|
|
28
|
+
expect(secretReason(join(HOME, ".pi", "agent", "auth-foo.json"))).toBe("pi credentials");
|
|
29
|
+
expect(secretReason(join(HOME, ".pi", "agent", "settings.json"))).toBeUndefined();
|
|
30
|
+
expect(secretReason(join(HOME, ".pi", "agent", "xauth", "auth.json"))).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("covers .env files but not the templates", () => {
|
|
34
|
+
expect(secretReason("/proj/.env")).toBe("environment file");
|
|
35
|
+
expect(secretReason("/proj/.env.local")).toBe("environment file");
|
|
36
|
+
expect(secretReason("/proj/.env.example")).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("danger table", () => {
|
|
41
|
+
it("catches the destructive core", () => {
|
|
42
|
+
expect(dangerReason("rm", ["-rf", "/"], ["/"], "/proj")).toContain("recursive delete");
|
|
43
|
+
expect(dangerReason("rm", ["-rf", HOME], [HOME], "/proj")).toContain("recursive delete");
|
|
44
|
+
expect(dangerReason("rm", ["-rf", "/proj"], ["/proj"], "/proj")).toContain("recursive delete");
|
|
45
|
+
expect(dangerReason("git", ["push", "--force"], [], "/proj")).toBe("forced git push");
|
|
46
|
+
expect(dangerReason("git", ["push", "-f"], [], "/proj")).toBe("forced git push");
|
|
47
|
+
expect(dangerReason("git", ["reset", "--hard"], [], "/proj")).toBe("git reset --hard");
|
|
48
|
+
expect(dangerReason("git", ["clean", "-fd"], [], "/proj")).toBe("git clean -f");
|
|
49
|
+
expect(dangerReason("npm", ["publish"], [], "/proj")).toBe("package publish");
|
|
50
|
+
expect(dangerReason("dd", ["of=/dev/sda"], ["/dev/sda"], "/proj")).toBe("raw device write");
|
|
51
|
+
expect(dangerReason("sudo", ["rm", "x"], ["/proj/x"], "/proj")).toBe("privileged command");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not flag ordinary commands", () => {
|
|
55
|
+
expect(dangerReason("rm", ["-rf", "/proj/build"], ["/proj/build"], "/proj")).toBeUndefined();
|
|
56
|
+
expect(dangerReason("git", ["push"], [], "/proj")).toBeUndefined();
|
|
57
|
+
expect(dangerReason("git", ["log"], [], "/proj")).toBeUndefined();
|
|
58
|
+
expect(dangerReason("npm", ["view", "react"], [], "/proj")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|