pi-free 1.0.8 → 2.0.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/README.md +95 -46
  3. package/config.ts +165 -120
  4. package/constants.ts +22 -61
  5. package/index.ts +186 -0
  6. package/lib/json-persistence.ts +11 -10
  7. package/lib/logger.ts +2 -2
  8. package/lib/model-enhancer.ts +20 -20
  9. package/lib/open-browser.ts +41 -0
  10. package/lib/provider-cache.ts +106 -0
  11. package/lib/registry.ts +144 -0
  12. package/package.json +67 -82
  13. package/provider-factory.ts +25 -41
  14. package/provider-failover/benchmark-lookup.ts +247 -0
  15. package/provider-failover/benchmarks-chunk-0.ts +2010 -0
  16. package/provider-failover/benchmarks-chunk-1.ts +1988 -0
  17. package/provider-failover/benchmarks-chunk-2.ts +2010 -0
  18. package/provider-failover/benchmarks-chunk-3.ts +2010 -0
  19. package/provider-failover/benchmarks-chunk-4.ts +1969 -0
  20. package/provider-failover/hardcoded-benchmarks.ts +22 -10025
  21. package/provider-helper.ts +38 -37
  22. package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
  23. package/providers/cline/cline-models.ts +128 -0
  24. package/providers/{cline.ts → cline/cline.ts} +300 -257
  25. package/providers/cloudflare/cloudflare.ts +368 -0
  26. package/providers/dynamic-built-in/index.ts +513 -0
  27. package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
  28. package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
  29. package/providers/kilo/kilo.ts +235 -0
  30. package/providers/{modal.ts → modal/modal.ts} +4 -3
  31. package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
  32. package/providers/ollama/ollama.ts +172 -0
  33. package/providers/opencode-session.ts +34 -34
  34. package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
  35. package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
  36. package/providers/qwen/qwen.ts +202 -0
  37. package/provider-failover/auto-switch.ts +0 -350
  38. package/provider-failover/errors.ts +0 -275
  39. package/provider-failover/index.ts +0 -238
  40. package/providers/cline-models.ts +0 -77
  41. package/providers/factory.ts +0 -125
  42. package/providers/fireworks.ts +0 -49
  43. package/providers/go.ts +0 -216
  44. package/providers/kilo.ts +0 -146
  45. package/providers/mistral.ts +0 -144
  46. package/providers/ollama.ts +0 -113
  47. package/providers/openrouter.ts +0 -175
  48. package/providers/qwen.ts +0 -127
  49. package/providers/zen.ts +0 -371
  50. package/usage/commands.ts +0 -17
  51. package/usage/cumulative.ts +0 -193
  52. package/usage/formatters.ts +0 -115
  53. package/usage/index.ts +0 -46
  54. package/usage/limits.ts +0 -148
  55. package/usage/metrics.ts +0 -222
  56. package/usage/sessions.ts +0 -355
  57. package/usage/store.ts +0 -99
  58. package/usage/tracking.ts +0 -329
  59. package/usage/types.ts +0 -26
  60. package/usage/widget.ts +0 -90
  61. package/widget/data.ts +0 -113
  62. package/widget/format.ts +0 -26
  63. package/widget/render.ts +0 -117
@@ -1,257 +1,300 @@
1
- /**
2
- * Cline Provider Extension
3
- *
4
- * Provides access to Cline's free models (via their OpenRouter gateway).
5
- * Free model list is fetched from Cline's GitHub source — no account needed to browse.
6
- * Run /login cline to authenticate and make API calls.
7
- *
8
- * Auth flow based on pi-cline's proven implementation.
9
- *
10
- * Usage:
11
- * pi install git:github.com/apmantza/pi-free
12
- * # Models appear immediately; run /login cline to start chatting
13
- */
14
-
15
- import type { OAuthCredentials } from "@mariozechner/pi-ai";
16
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
17
- import { BASE_URL_CLINE, PROVIDER_CLINE } from "../constants.ts";
18
- import { incrementRequestCount } from "../usage/metrics.ts";
19
- import { logWarning } from "../lib/util.ts";
20
- import { loginCline, refreshClineToken } from "./cline-auth.ts";
21
- import { fetchClineModels } from "./cline-models.ts";
22
-
23
- // =============================================================================
24
- // Cline API headers (must match real Cline VS Code extension exactly)
25
- // =============================================================================
26
-
27
- const VS_CODE_VERSION = "1.109.3"; // vscode.version
28
- const CLINE_EXTENSION_VERSION = "3.76.0"; // ExtensionRegistryInfo.version
29
- let _currentTaskId = generateUlid();
30
-
31
- function generateUlid(): string {
32
- const CHARS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
33
- const now = Date.now();
34
- let ts = "";
35
- let t = now;
36
- for (let i = 0; i < 10; i++) {
37
- ts = CHARS[t % 32] + ts;
38
- t = Math.floor(t / 32);
39
- }
40
- const rand = new Uint8Array(16);
41
- crypto.getRandomValues(rand);
42
- let r = "";
43
- for (let i = 0; i < 16; i++) r += CHARS[rand[i] % 32];
44
- return ts + r;
45
- }
46
-
47
- function buildClineHeaders(): Record<string, string> {
48
- return {
49
- "HTTP-Referer": "https://cline.bot",
50
- "X-Title": "Cline",
51
- "X-Task-ID": _currentTaskId,
52
- "X-PLATFORM": "Visual Studio Code",
53
- "X-PLATFORM-VERSION": VS_CODE_VERSION, // VS Code version, NOT Cline version
54
- "X-CLIENT-TYPE": "VSCode Extension",
55
- "X-CLIENT-VERSION": CLINE_EXTENSION_VERSION, // Cline extension version
56
- "X-CORE-VERSION": CLINE_EXTENSION_VERSION, // Cline extension version
57
- "X-Is-Multiroot": "false",
58
- };
59
- }
60
-
61
- // =============================================================================
62
- // Token handling (pi-cline stores without workos: prefix, adds it in getApiKey)
63
- // =============================================================================
64
-
65
- function toApiKey(credentials: OAuthCredentials): string {
66
- const token = credentials.access;
67
- return token.startsWith("workos:") ? token : `workos:${token}`;
68
- }
69
-
70
- // =============================================================================
71
- // Context shaping — Cline's API requires a specific message envelope
72
- // =============================================================================
73
-
74
- const TASK_PROGRESS_BLOCK = `
75
- # task_progress List (Optional - Plan Mode)
76
-
77
- While in PLAN MODE, if you've outlined concrete steps or requirements for the user, you may include a preliminary todo list using the task_progress parameter.
78
-
79
- 1. To create or update a todo list, include the task_progress parameter in the next tool call
80
- 2. Review each item and update its status:
81
- - Mark completed items with: - [x]
82
- - Keep incomplete items as: - [ ]
83
- 3. Modify the list as needed
84
- 4. Ensure the list accurately reflects the current state`;
85
-
86
- function buildEnvironmentDetails(): string {
87
- const cwd = process.cwd();
88
- return `<environmentDetails>
89
- # Visual Studio Code Visible Files
90
- (No visible files)
91
-
92
- # Visual Studio Code Open Tabs
93
- (No open tabs)
94
-
95
- # Current Working Directory (${cwd}) Files
96
- (No files)
97
-
98
- # Context Window Usage
99
- 0 / 204.8K tokens used (0%)
100
-
101
- # Current Mode
102
- PLAN MODE
103
- </environmentDetails>`;
104
- }
105
-
106
- function extractText(content: unknown): string {
107
- if (typeof content === "string") return content.trim();
108
- if (Array.isArray(content)) {
109
- return (content as any[])
110
- .filter((p: any) => p?.type === "text" && typeof p?.text === "string")
111
- .map((p: any) => p.text)
112
- .join("\n\n")
113
- .trim();
114
- }
115
- return "";
116
- }
117
-
118
- function isClineWrapped(content: unknown): boolean {
119
- if (!Array.isArray(content)) return false;
120
- const texts = (content as any[])
121
- .filter((p: any) => p?.type === "text" && typeof p?.text === "string")
122
- .map((p: any) => p.text as string);
123
- return (
124
- texts.some((t) => /<task>[\s\S]*<\/task>/.test(t)) &&
125
- texts.some((t) => t.includes("task_progress List")) &&
126
- texts.some((t) => t.includes("<environmentDetails>"))
127
- );
128
- }
129
-
130
- function extractTaskBody(content: unknown): string {
131
- if (!Array.isArray(content)) return "";
132
- for (const p of content as any[]) {
133
- if (p?.type !== "text" || typeof p?.text !== "string") continue;
134
- const m = p.text.match(/<task>\s*([\s\S]*?)\s*<\/task>/);
135
- if (m?.[1]) return m[1].trim();
136
- }
137
- return "";
138
- }
139
-
140
- function shapeMessagesForCline(messages: any[]): any[] {
141
- let lastWrappedIdx = -1;
142
- let baseTranscript = "";
143
- for (let i = messages.length - 1; i >= 0; i--) {
144
- if (messages[i]?.role !== "user") continue;
145
- if (!isClineWrapped(messages[i]?.content)) continue;
146
- lastWrappedIdx = i;
147
- baseTranscript = extractTaskBody(messages[i].content);
148
- break;
149
- }
150
-
151
- const parts: string[] = baseTranscript ? [baseTranscript] : [];
152
- const startIdx = lastWrappedIdx >= 0 ? lastWrappedIdx + 1 : 0;
153
-
154
- for (let i = startIdx; i < messages.length; i++) {
155
- const msg = messages[i];
156
- const role = msg?.role ?? "user";
157
- if (role === "system") continue;
158
- if (role === "user" && isClineWrapped(msg?.content)) continue;
159
- const text = extractText(msg?.content).trim();
160
- if (!text) continue;
161
-
162
- if (role === "tool") {
163
- parts.push(`<tool_result>\n${text}\n</tool_result>`);
164
- } else if (role !== "assistant") {
165
- parts.push(`[${role}]\n${text}`);
166
- }
167
- }
168
-
169
- const transcript = parts.join("\n\n").trim() || "(no conversation yet)";
170
- const envDetails = buildEnvironmentDetails();
171
-
172
- const collapsed: any[] = [];
173
- const systemMsg = messages.find((m: any) => m?.role === "system");
174
- if (systemMsg) {
175
- const systemText = extractText(systemMsg.content);
176
- if (systemText) collapsed.push({ role: "system", content: systemText });
177
- }
178
-
179
- collapsed.push({
180
- role: "user",
181
- content: [
182
- { type: "text", text: `<task>\n${transcript}\n</task>` },
183
- { type: "text", text: TASK_PROGRESS_BLOCK },
184
- { type: "text", text: envDetails },
185
- ],
186
- });
187
-
188
- return collapsed;
189
- }
190
-
191
- // =============================================================================
192
- // Extension entry point
193
- // =============================================================================
194
-
195
- export default async function (pi: ExtensionAPI) {
196
- let models = await fetchClineModels().catch((err) => {
197
- logWarning("cline", "Failed to fetch free models at startup", err);
198
- return [];
199
- });
200
-
201
- function registerProvider(m = models) {
202
- pi.registerProvider(PROVIDER_CLINE, {
203
- baseUrl: BASE_URL_CLINE,
204
- api: "openai-completions" as const,
205
- authHeader: false,
206
- headers: buildClineHeaders(),
207
- models: m,
208
- oauth: {
209
- name: "Cline",
210
- login: loginCline,
211
- refreshToken: refreshClineToken,
212
- getApiKey: toApiKey,
213
- },
214
- });
215
- }
216
-
217
- registerProvider();
218
-
219
- // Cline-specific: refresh task ID and re-register headers before each agent run
220
- pi.on("before_agent_start", async (_event, ctx) => {
221
- if (ctx.model?.provider !== PROVIDER_CLINE) return;
222
- _currentTaskId = generateUlid();
223
- registerProvider();
224
- });
225
-
226
- // Cline-specific: shape messages to Cline's expected envelope format
227
- pi.on("context", async (event, ctx) => {
228
- if (ctx.model?.provider !== PROVIDER_CLINE) return;
229
- const sourceMessages = Array.isArray(event.messages) ? event.messages : [];
230
- return { messages: shapeMessagesForCline(sourceMessages) };
231
- });
232
-
233
- // Cline-specific: refresh model list at session start
234
- pi.on("session_start", async (_event, ctx) => {
235
- try {
236
- const fresh = await fetchClineModels();
237
- if (fresh.length > 0) {
238
- models = fresh;
239
- registerProvider(models);
240
- if (ctx.model?.provider === PROVIDER_CLINE) {
241
- ctx.ui.notify(
242
- `Cline: ${models.length} free models available`,
243
- "info",
244
- );
245
- }
246
- }
247
- } catch (err) {
248
- logWarning("cline", "Failed to refresh models at session start", err);
249
- }
250
- });
251
-
252
- // Keep lightweight request counting for now (internal only).
253
- pi.on("turn_end", async (_event, ctx) => {
254
- if (ctx.model?.provider !== PROVIDER_CLINE) return;
255
- incrementRequestCount(PROVIDER_CLINE);
256
- });
257
- }
1
+ /**
2
+ * Cline Provider Extension
3
+ *
4
+ * Provides access to Cline's free models (via their OpenRouter gateway).
5
+ * Free model list is fetched from Cline's GitHub source — no account needed to browse.
6
+ * Run /login cline to authenticate and make API calls.
7
+ *
8
+ * Auth flow based on pi-cline's proven implementation.
9
+ *
10
+ * Responds to global /free toggle (though Cline only provides free models without auth).
11
+ *
12
+ * Usage:
13
+ * pi install git:github.com/apmantza/pi-free
14
+ * # Models appear immediately; run /login cline to start chatting
15
+ */
16
+
17
+ import type { OAuthCredentials } from "@mariozechner/pi-ai";
18
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
19
+ import { BASE_URL_CLINE, PROVIDER_CLINE } from "../../constants.ts";
20
+ import { registerWithGlobalToggle } from "../../lib/registry.ts";
21
+ import { logWarning } from "../../lib/util.ts";
22
+ import { enhanceWithCI } from "../../provider-helper.ts";
23
+ import { loginCline, refreshClineToken } from "./cline-auth.ts";
24
+ import { fetchClineModels } from "./cline-models.ts";
25
+
26
+ // =============================================================================
27
+ // Cline API headers (must match real Cline VS Code extension exactly)
28
+ // =============================================================================
29
+
30
+ const VS_CODE_VERSION = "1.109.3";
31
+ const CLINE_EXTENSION_VERSION = "3.76.0";
32
+ let _currentTaskId = generateUlid();
33
+
34
+ function generateUlid(): string {
35
+ const CHARS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
36
+ const now = Date.now();
37
+ let ts = "";
38
+ let t = now;
39
+ for (let i = 0; i < 10; i++) {
40
+ ts = CHARS[t % 32] + ts;
41
+ t = Math.floor(t / 32);
42
+ }
43
+ const rand = new Uint8Array(16);
44
+ crypto.getRandomValues(rand);
45
+ let r = "";
46
+ for (let i = 0; i < 16; i++) r += CHARS[rand[i] % 32];
47
+ return ts + r;
48
+ }
49
+
50
+ function buildClineHeaders(): Record<string, string> {
51
+ return {
52
+ "HTTP-Referer": "https://cline.bot",
53
+ "X-Title": "Cline",
54
+ "X-Task-ID": _currentTaskId,
55
+ "X-PLATFORM": "Visual Studio Code",
56
+ "X-PLATFORM-VERSION": VS_CODE_VERSION,
57
+ "X-CLIENT-TYPE": "VSCode Extension",
58
+ "X-CLIENT-VERSION": CLINE_EXTENSION_VERSION,
59
+ "X-CORE-VERSION": CLINE_EXTENSION_VERSION,
60
+ "X-Is-Multiroot": "false",
61
+ };
62
+ }
63
+
64
+ function toApiKey(credentials: OAuthCredentials): string {
65
+ const token = credentials.access;
66
+ return token.startsWith("workos:") ? token : `workos:${token}`;
67
+ }
68
+
69
+ // =============================================================================
70
+ // Context shaping — Cline's API requires a specific message envelope
71
+ // =============================================================================
72
+
73
+ const TASK_PROGRESS_BLOCK = `
74
+ # task_progress List (Optional - Plan Mode)
75
+
76
+ While in PLAN MODE, if you've outlined concrete steps or requirements for the user, you may include a preliminary todo list using the task_progress parameter.
77
+
78
+ 1. To create or update a todo list, include the task_progress parameter in the next tool call
79
+ 2. Review each item and update its status:
80
+ - Mark completed items with: - [x]
81
+ - Keep incomplete items as: - [ ]
82
+ 3. Modify the list as needed
83
+ 4. Ensure the list accurately reflects the current state`;
84
+
85
+ function buildEnvironmentDetails(): string {
86
+ const cwd = process.cwd();
87
+ return `<environmentDetails>
88
+ # Visual Studio Code Visible Files
89
+ (No visible files)
90
+
91
+ # Visual Studio Code Open Tabs
92
+ (No open tabs)
93
+
94
+ # Current Working Directory (${cwd}) Files
95
+ (No files)
96
+
97
+ # Context Window Usage
98
+ 0 / 204.8K tokens used (0%)
99
+
100
+ # Current Mode
101
+ PLAN MODE
102
+ </environmentDetails>`;
103
+ }
104
+
105
+ function extractText(content: unknown): string {
106
+ if (typeof content === "string") return content.trim();
107
+ if (Array.isArray(content)) {
108
+ return (content as any[])
109
+ .filter((p: any) => p?.type === "text" && typeof p?.text === "string")
110
+ .map((p: any) => p.text)
111
+ .join("\n\n")
112
+ .trim();
113
+ }
114
+ return "";
115
+ }
116
+
117
+ function isClineWrapped(content: unknown): boolean {
118
+ if (!Array.isArray(content)) return false;
119
+ const texts = (content as any[])
120
+ .filter((p: any) => p?.type === "text" && typeof p?.text === "string")
121
+ .map((p: any) => p.text as string);
122
+ return (
123
+ texts.some((t) => /<task>[\s\S]*<\/task>/.test(t)) &&
124
+ texts.some((t) => t.includes("task_progress List")) &&
125
+ texts.some((t) => t.includes("<environmentDetails>"))
126
+ );
127
+ }
128
+
129
+ function extractTaskBody(content: unknown): string {
130
+ if (!Array.isArray(content)) return "";
131
+ for (const p of content as any[]) {
132
+ if (p?.type !== "text" || typeof p?.text !== "string") continue;
133
+ const m = p.text.match(/<task>\s*([\s\S]*?)\s*<\/task>/);
134
+ if (m?.[1]) return m[1].trim();
135
+ }
136
+ return "";
137
+ }
138
+
139
+ function shapeMessagesForCline(messages: any[]): any[] {
140
+ let lastWrappedIdx = -1;
141
+ let baseTranscript = "";
142
+ for (let i = messages.length - 1; i >= 0; i--) {
143
+ if (messages[i]?.role !== "user") continue;
144
+ if (!isClineWrapped(messages[i]?.content)) continue;
145
+ lastWrappedIdx = i;
146
+ baseTranscript = extractTaskBody(messages[i].content);
147
+ break;
148
+ }
149
+
150
+ const parts: string[] = baseTranscript ? [baseTranscript] : [];
151
+ const startIdx = lastWrappedIdx >= 0 ? lastWrappedIdx + 1 : 0;
152
+
153
+ for (let i = startIdx; i < messages.length; i++) {
154
+ const msg = messages[i];
155
+ const role = msg?.role ?? "user";
156
+ if (role === "system") continue;
157
+ if (role === "user" && isClineWrapped(msg?.content)) continue;
158
+ const text = extractText(msg?.content).trim();
159
+ if (!text) continue;
160
+
161
+ if (role === "tool") {
162
+ parts.push(`<tool_result>\n${text}\n</tool_result>`);
163
+ } else if (role !== "assistant") {
164
+ parts.push(`[${role}]\n${text}`);
165
+ }
166
+ }
167
+
168
+ const transcript = parts.join("\n\n").trim() || "(no conversation yet)";
169
+ const envDetails = buildEnvironmentDetails();
170
+
171
+ const collapsed: any[] = [];
172
+ const systemMsg = messages.find((m: any) => m?.role === "system");
173
+ if (systemMsg) {
174
+ const systemText = extractText(systemMsg.content);
175
+ if (systemText) collapsed.push({ role: "system", content: systemText });
176
+ }
177
+
178
+ collapsed.push({
179
+ role: "user",
180
+ content: [
181
+ { type: "text", text: `<task>\n${transcript}\n</task>` },
182
+ { type: "text", text: TASK_PROGRESS_BLOCK },
183
+ { type: "text", text: envDetails },
184
+ ],
185
+ });
186
+
187
+ return collapsed;
188
+ }
189
+
190
+ // =============================================================================
191
+ // Extension entry point
192
+ // =============================================================================
193
+
194
+ export default async function (pi: ExtensionAPI) {
195
+ // Fetch ALL models from OpenRouter (free and paid)
196
+ // The global /free toggle will filter based on cost.input
197
+ let allModels = await fetchClineModels(false).catch((err) => {
198
+ logWarning("cline", "Failed to fetch models at startup", err);
199
+ return [];
200
+ });
201
+
202
+ // Also fetch free-only list for the global toggle's free filter
203
+ let freeModels = allModels.filter((m) => m.cost.input === 0);
204
+
205
+ // Create re-register function for global toggle
206
+ const reRegister = (m: typeof allModels) => {
207
+ pi.registerProvider(PROVIDER_CLINE, {
208
+ baseUrl: BASE_URL_CLINE,
209
+ api: "openai-completions" as const,
210
+ authHeader: false,
211
+ headers: buildClineHeaders(),
212
+ models: enhanceWithCI(m),
213
+ oauth: {
214
+ name: "Cline",
215
+ login: loginCline,
216
+ refreshToken: refreshClineToken,
217
+ getApiKey: toApiKey,
218
+ },
219
+ });
220
+ };
221
+
222
+ // Register with global toggle (separate free and all lists)
223
+ registerWithGlobalToggle(
224
+ PROVIDER_CLINE,
225
+ { free: freeModels, all: allModels },
226
+ (m) => reRegister(m),
227
+ false, // no key until OAuth
228
+ );
229
+
230
+ // Initial registration with all models
231
+ reRegister(allModels);
232
+
233
+ // Per-provider toggle command (works independently of global /free)
234
+ let showPaidModels = false;
235
+ let currentModels = allModels;
236
+ pi.registerCommand("cline-toggle", {
237
+ description: "Toggle between free and all Cline models",
238
+ handler: async (_args, ctx) => {
239
+ showPaidModels = !showPaidModels;
240
+
241
+ // Determine which models to show
242
+ const modelsToShow =
243
+ showPaidModels && allModels.length > 0 ? allModels : freeModels;
244
+
245
+ currentModels = modelsToShow;
246
+ reRegister(modelsToShow);
247
+
248
+ const freeCount = freeModels.length;
249
+ const paidCount = allModels.length - freeCount;
250
+
251
+ if (showPaidModels && allModels.length > 0) {
252
+ ctx.ui.notify(
253
+ `cline: showing all ${allModels.length} models (${freeCount} free, ${paidCount} paid)`,
254
+ "info",
255
+ );
256
+ } else {
257
+ ctx.ui.notify(
258
+ `cline: showing ${freeCount} free models (${paidCount} paid hidden)`,
259
+ "info",
260
+ );
261
+ }
262
+ },
263
+ });
264
+
265
+ // Cline-specific: refresh task ID and re-register headers before each agent run
266
+ pi.on("before_agent_start", async (_event, ctx) => {
267
+ if (ctx.model?.provider !== PROVIDER_CLINE) return;
268
+ _currentTaskId = generateUlid();
269
+ reRegister(freeModels);
270
+ });
271
+
272
+ // Cline-specific: shape messages to Cline's expected envelope format
273
+ pi.on("context", async (event, ctx) => {
274
+ if (ctx.model?.provider !== PROVIDER_CLINE) return;
275
+ const sourceMessages = Array.isArray(event.messages) ? event.messages : [];
276
+ return { messages: shapeMessagesForCline(sourceMessages) };
277
+ });
278
+
279
+ // Cline-specific: refresh model list at session start
280
+ pi.on("session_start", async (_event, ctx) => {
281
+ try {
282
+ const fresh = await fetchClineModels(false);
283
+ if (fresh.length > 0) {
284
+ allModels = fresh;
285
+ freeModels = allModels.filter((m) => m.cost.input === 0);
286
+ reRegister(allModels);
287
+ if (ctx.model?.provider === PROVIDER_CLINE) {
288
+ const freeCount = freeModels.length;
289
+ const paidCount = allModels.length - freeCount;
290
+ ctx.ui.notify(
291
+ `Cline: ${freeCount} free, ${paidCount} paid models available`,
292
+ "info",
293
+ );
294
+ }
295
+ }
296
+ } catch (err) {
297
+ logWarning("cline", "Failed to refresh models at session start", err);
298
+ }
299
+ });
300
+ }