@preapexis/pi-kit 1.1.2 → 1.2.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.
|
@@ -3,29 +3,21 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
|
|
4
4
|
type LiteLlmModelInfo = {
|
|
5
5
|
model_name?: string;
|
|
6
|
-
litellm_params?: {
|
|
7
|
-
model?: string;
|
|
8
|
-
};
|
|
9
6
|
model_info?: {
|
|
10
7
|
id?: string;
|
|
11
8
|
name?: string;
|
|
12
9
|
display_name?: string;
|
|
13
|
-
description?: string;
|
|
14
10
|
max_tokens?: number;
|
|
15
11
|
max_input_tokens?: number;
|
|
16
12
|
context_window?: number;
|
|
17
13
|
input_cost_per_token?: number;
|
|
18
14
|
output_cost_per_token?: number;
|
|
19
15
|
supports_vision?: boolean;
|
|
20
|
-
supports_function_calling?: boolean;
|
|
21
|
-
mode?: string;
|
|
22
16
|
};
|
|
23
17
|
};
|
|
24
18
|
|
|
25
19
|
type OpenAiModel = {
|
|
26
20
|
id?: string;
|
|
27
|
-
object?: string;
|
|
28
|
-
owned_by?: string;
|
|
29
21
|
};
|
|
30
22
|
|
|
31
23
|
type ModelPayload = {
|
|
@@ -45,10 +37,6 @@ type PiModel = {
|
|
|
45
37
|
};
|
|
46
38
|
contextWindow: number;
|
|
47
39
|
maxTokens: number;
|
|
48
|
-
compat?: {
|
|
49
|
-
supportsDeveloperRole?: boolean;
|
|
50
|
-
supportsReasoningEffort?: boolean;
|
|
51
|
-
};
|
|
52
40
|
};
|
|
53
41
|
|
|
54
42
|
const PROVIDER_ID = "litellm";
|
|
@@ -59,9 +47,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
59
47
|
process.env.LITELLM_BASE_URL ?? DEFAULT_BASE_URL
|
|
60
48
|
);
|
|
61
49
|
|
|
62
|
-
async function registerLiteLlmProvider(): Promise<
|
|
50
|
+
async function registerLiteLlmProvider(): Promise<number> {
|
|
63
51
|
const models = await discoverModels(baseUrl);
|
|
64
52
|
|
|
53
|
+
if (models.length === 0) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
65
57
|
pi.registerProvider(PROVIDER_ID, {
|
|
66
58
|
name: "LiteLLM",
|
|
67
59
|
baseUrl,
|
|
@@ -69,20 +61,54 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
69
61
|
api: "openai-completions",
|
|
70
62
|
models
|
|
71
63
|
});
|
|
64
|
+
|
|
65
|
+
return models.length;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
|
|
68
|
+
try {
|
|
69
|
+
const count = await registerLiteLlmProvider();
|
|
70
|
+
|
|
71
|
+
if (count > 0) {
|
|
72
|
+
console.log(`[litellm] Registered ${count} models from ${baseUrl}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log("[litellm] No models found. Provider was not registered.");
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.log(
|
|
78
|
+
[
|
|
79
|
+
"[litellm] Provider skipped.",
|
|
80
|
+
error instanceof Error ? error.message : String(error),
|
|
81
|
+
"Start LiteLLM and run /litellm-refresh."
|
|
82
|
+
].join("\n")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
75
85
|
|
|
76
86
|
pi.registerCommand("litellm-refresh", {
|
|
77
87
|
description: "Refresh LiteLLM models from the LiteLLM proxy",
|
|
78
88
|
handler: async (_args, ctx) => {
|
|
79
89
|
try {
|
|
80
|
-
await registerLiteLlmProvider();
|
|
90
|
+
const count = await registerLiteLlmProvider();
|
|
91
|
+
|
|
92
|
+
if (count === 0) {
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
[
|
|
95
|
+
"LiteLLM refresh finished, but no models were found.",
|
|
96
|
+
"",
|
|
97
|
+
`Base URL: ${baseUrl}`,
|
|
98
|
+
"",
|
|
99
|
+
"Make sure LiteLLM is running."
|
|
100
|
+
].join("\n"),
|
|
101
|
+
"warning"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
81
106
|
|
|
82
107
|
ctx.ui.notify(
|
|
83
108
|
[
|
|
84
109
|
"LiteLLM models refreshed.",
|
|
85
110
|
"",
|
|
111
|
+
`Models found: ${count}`,
|
|
86
112
|
`Base URL: ${baseUrl}`,
|
|
87
113
|
"",
|
|
88
114
|
"Run /model to select a LiteLLM model."
|
|
@@ -129,26 +155,11 @@ async function discoverModels(baseUrl: string): Promise<PiModel[]> {
|
|
|
129
155
|
return models;
|
|
130
156
|
}
|
|
131
157
|
} catch {
|
|
132
|
-
// Try
|
|
158
|
+
// Try next endpoint.
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (fallbackModels.length > 0) {
|
|
139
|
-
return fallbackModels;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
throw new Error(
|
|
143
|
-
[
|
|
144
|
-
"Could not discover LiteLLM models.",
|
|
145
|
-
"",
|
|
146
|
-
`Tried: ${endpoints.join(", ")}`,
|
|
147
|
-
"",
|
|
148
|
-
"Make sure LiteLLM is running and set LITELLM_API_KEY if your proxy requires auth.",
|
|
149
|
-
"You can also set LITELLM_MODELS as a comma-separated fallback."
|
|
150
|
-
].join("\n")
|
|
151
|
-
);
|
|
162
|
+
return getFallbackModels();
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
async function fetchModelPayload(url: string): Promise<ModelPayload> {
|
|
@@ -228,11 +239,7 @@ function modelFromPayloadItem(
|
|
|
228
239
|
cacheWrite: 0
|
|
229
240
|
},
|
|
230
241
|
contextWindow,
|
|
231
|
-
maxTokens
|
|
232
|
-
compat: {
|
|
233
|
-
supportsDeveloperRole: false,
|
|
234
|
-
supportsReasoningEffort: false
|
|
235
|
-
}
|
|
242
|
+
maxTokens
|
|
236
243
|
};
|
|
237
244
|
}
|
|
238
245
|
|
|
@@ -293,10 +300,6 @@ function getFallbackModels(): PiModel[] {
|
|
|
293
300
|
cacheWrite: 0
|
|
294
301
|
},
|
|
295
302
|
contextWindow: 128000,
|
|
296
|
-
maxTokens: 4096
|
|
297
|
-
compat: {
|
|
298
|
-
supportsDeveloperRole: false,
|
|
299
|
-
supportsReasoningEffort: false
|
|
300
|
-
}
|
|
303
|
+
maxTokens: 4096
|
|
301
304
|
}));
|
|
302
305
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
type EventContext = Parameters<Parameters<ExtensionAPI["on"]>[1]>[1];
|
|
5
|
+
|
|
6
|
+
type ToolDecision =
|
|
7
|
+
| {
|
|
8
|
+
block: true;
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
| undefined;
|
|
12
|
+
|
|
13
|
+
type InputRecord = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
const STATUS_KEY = "workspace-guard";
|
|
16
|
+
|
|
17
|
+
export default function (pi: ExtensionAPI): void {
|
|
18
|
+
let workspaceRoot: string | null = null;
|
|
19
|
+
|
|
20
|
+
function normalize(filePath: string): string {
|
|
21
|
+
return path.resolve(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isInsideWorkspace(targetPath: string): boolean {
|
|
25
|
+
if (!workspaceRoot) return true;
|
|
26
|
+
|
|
27
|
+
const root = normalize(workspaceRoot);
|
|
28
|
+
const target = normalize(targetPath);
|
|
29
|
+
|
|
30
|
+
return target === root || target.startsWith(root + path.sep);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inputRecord(input: unknown): InputRecord {
|
|
34
|
+
if (input && typeof input === "object") {
|
|
35
|
+
return input as InputRecord;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getPathValue(input: InputRecord): string | undefined {
|
|
42
|
+
const keys = [
|
|
43
|
+
"path",
|
|
44
|
+
"filePath",
|
|
45
|
+
"filepath",
|
|
46
|
+
"dir",
|
|
47
|
+
"directory",
|
|
48
|
+
"cwd",
|
|
49
|
+
"root"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
const value = input[key];
|
|
54
|
+
|
|
55
|
+
if (typeof value === "string" && value.trim()) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveFromWorkspaceOrCwd(ctx: EventContext, value: string): string {
|
|
64
|
+
if (path.isAbsolute(value)) {
|
|
65
|
+
return normalize(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalize(path.join(ctx.cwd, value));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function commandLooksOutside(command: string): boolean {
|
|
72
|
+
const patterns = [
|
|
73
|
+
/\bcd\s+\.\./i,
|
|
74
|
+
/\bcd\s+["']?\//i,
|
|
75
|
+
/\bcd\s+["']?[a-zA-Z]:\\/i,
|
|
76
|
+
/\bpushd\s+\.\./i,
|
|
77
|
+
/\bpushd\s+["']?\//i,
|
|
78
|
+
/\bpushd\s+["']?[a-zA-Z]:\\/i,
|
|
79
|
+
/\bgit\s+-C\s+\.\./i,
|
|
80
|
+
/\bnpm\s+--prefix\s+\.\./i,
|
|
81
|
+
/\bpnpm\s+-C\s+\.\./i,
|
|
82
|
+
/\byarn\s+--cwd\s+\.\./i,
|
|
83
|
+
/\bSet-Location\s+\.\./i,
|
|
84
|
+
/\bsl\s+\.\./i
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
return patterns.some((pattern) => pattern.test(command));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function confirmOutsideAccess(
|
|
91
|
+
ctx: EventContext,
|
|
92
|
+
action: string,
|
|
93
|
+
targetPath: string
|
|
94
|
+
): Promise<ToolDecision> {
|
|
95
|
+
if (!workspaceRoot) return undefined;
|
|
96
|
+
|
|
97
|
+
if (isInsideWorkspace(targetPath)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const message = [
|
|
102
|
+
`Workspace guard blocked outside-${action} access.`,
|
|
103
|
+
"",
|
|
104
|
+
`Workspace root: ${workspaceRoot}`,
|
|
105
|
+
`Requested path: ${targetPath}`,
|
|
106
|
+
"",
|
|
107
|
+
"Allow this one time?"
|
|
108
|
+
].join("\n");
|
|
109
|
+
|
|
110
|
+
if (!ctx.hasUI) {
|
|
111
|
+
return {
|
|
112
|
+
block: true,
|
|
113
|
+
reason: `Outside workspace ${action} blocked: ${targetPath}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ok = await ctx.ui.confirm("Outside workspace access", message);
|
|
118
|
+
|
|
119
|
+
if (!ok) {
|
|
120
|
+
return {
|
|
121
|
+
block: true,
|
|
122
|
+
reason: `Outside workspace ${action} cancelled by user.`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
130
|
+
workspaceRoot = normalize(ctx.cwd);
|
|
131
|
+
|
|
132
|
+
if (ctx.hasUI) {
|
|
133
|
+
ctx.ui.setStatus(STATUS_KEY, "workspace: locked");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
pi.on("before_agent_start", async (event) => {
|
|
138
|
+
return {
|
|
139
|
+
systemPrompt:
|
|
140
|
+
event.systemPrompt +
|
|
141
|
+
`
|
|
142
|
+
|
|
143
|
+
Workspace boundary rules:
|
|
144
|
+
- Treat the current working directory as the workspace root.
|
|
145
|
+
- Do not read, search, edit, write, delete, or inspect files outside the current workspace unless the user explicitly asks.
|
|
146
|
+
- Before using any path outside the workspace, ask the user for permission.
|
|
147
|
+
- Prefer relative paths inside the current repository.
|
|
148
|
+
- Do not run commands that cd, pushd, Set-Location, or otherwise move outside the workspace unless explicitly requested.
|
|
149
|
+
- If outside-workspace context seems useful, ask first instead of checking it silently.
|
|
150
|
+
`
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
155
|
+
if (!workspaceRoot) {
|
|
156
|
+
workspaceRoot = normalize(ctx.cwd);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const input = inputRecord(event.input);
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
event.toolName === "read" ||
|
|
163
|
+
event.toolName === "write" ||
|
|
164
|
+
event.toolName === "edit" ||
|
|
165
|
+
event.toolName === "ls" ||
|
|
166
|
+
event.toolName === "grep" ||
|
|
167
|
+
event.toolName === "find"
|
|
168
|
+
) {
|
|
169
|
+
const rawPath = getPathValue(input);
|
|
170
|
+
|
|
171
|
+
if (rawPath) {
|
|
172
|
+
const targetPath = resolveFromWorkspaceOrCwd(ctx, rawPath);
|
|
173
|
+
|
|
174
|
+
return await confirmOutsideAccess(ctx, event.toolName, targetPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (event.toolName === "bash") {
|
|
179
|
+
const command = String(input.command ?? "");
|
|
180
|
+
|
|
181
|
+
const rawCwd = typeof input.cwd === "string" ? input.cwd : undefined;
|
|
182
|
+
|
|
183
|
+
if (rawCwd) {
|
|
184
|
+
const targetCwd = resolveFromWorkspaceOrCwd(ctx, rawCwd);
|
|
185
|
+
const decision = await confirmOutsideAccess(ctx, "command", targetCwd);
|
|
186
|
+
|
|
187
|
+
if (decision) return decision;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (commandLooksOutside(command)) {
|
|
191
|
+
if (!ctx.hasUI) {
|
|
192
|
+
return {
|
|
193
|
+
block: true,
|
|
194
|
+
reason: "Command that may leave the workspace was blocked."
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ok = await ctx.ui.confirm(
|
|
199
|
+
"Command may leave workspace",
|
|
200
|
+
[
|
|
201
|
+
"This command appears to move outside the current workspace.",
|
|
202
|
+
"",
|
|
203
|
+
`Workspace root: ${workspaceRoot}`,
|
|
204
|
+
"",
|
|
205
|
+
command,
|
|
206
|
+
"",
|
|
207
|
+
"Allow this one time?"
|
|
208
|
+
].join("\n")
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!ok) {
|
|
212
|
+
return {
|
|
213
|
+
block: true,
|
|
214
|
+
reason: "Outside-workspace command cancelled by user."
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return undefined;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
pi.registerCommand("workspace-root", {
|
|
224
|
+
description: "Show the current locked workspace root",
|
|
225
|
+
handler: async (_args, ctx) => {
|
|
226
|
+
if (!ctx.hasUI) return;
|
|
227
|
+
|
|
228
|
+
ctx.ui.notify(
|
|
229
|
+
`Workspace root:\n\n${workspaceRoot ?? normalize(ctx.cwd)}`,
|
|
230
|
+
"info"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
package/package.json
CHANGED
package/themes/latte-review.json
CHANGED
package/themes/safe-dark.json
CHANGED