@aerok/pi-toolkit 0.1.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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/NOTICE +6 -0
- package/README.md +277 -0
- package/extensions/bark/client.ts +79 -0
- package/extensions/bark/config.ts +246 -0
- package/extensions/bark/crypto.ts +31 -0
- package/extensions/bark/index.ts +317 -0
- package/extensions/bark/recap.ts +53 -0
- package/extensions/bark/redact.ts +21 -0
- package/extensions/bark/tui/model-selector.ts +102 -0
- package/extensions/bark/tui/settings.ts +213 -0
- package/extensions/bark/tui/setup.ts +375 -0
- package/extensions/bark/tui/theme.ts +49 -0
- package/extensions/bark/types.ts +68 -0
- package/extensions/image-placeholders/README.md +22 -0
- package/extensions/image-placeholders/index.ts +312 -0
- package/package.json +64 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
import { createBarkClient } from "./client.js";
|
|
6
|
+
import { loadResolvedConfig, validateResolvedConfig } from "./config.js";
|
|
7
|
+
import { fallbackSummary, summarizeForNotification } from "./recap.js";
|
|
8
|
+
import type { BarkEventKey, BarkLevel, CachedModel, NotifyPriority, ResolvedBarkConfig } from "./types.js";
|
|
9
|
+
import { RecapModelSelectorOverlay } from "./tui/model-selector.js";
|
|
10
|
+
import { BarkSettingsOverlay } from "./tui/settings.js";
|
|
11
|
+
import { BarkSetupOverlay } from "./tui/setup.js";
|
|
12
|
+
|
|
13
|
+
const ASK_USER_PROMPT_EVENT = "rpiv:ask-user:prompt";
|
|
14
|
+
const PERMISSION_PROMPT_EVENT = "permissions:ui_prompt";
|
|
15
|
+
const client = createBarkClient();
|
|
16
|
+
|
|
17
|
+
function mapPriority(priority: NotifyPriority): BarkLevel {
|
|
18
|
+
return { low: "passive", normal: "active", high: "timeSensitive" }[priority] as BarkLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function modelRefParts(reference: string): { provider: string; id: string } {
|
|
22
|
+
const slash = reference.indexOf("/");
|
|
23
|
+
return slash < 0
|
|
24
|
+
? { provider: reference, id: reference }
|
|
25
|
+
: { provider: reference.slice(0, slash), id: reference.slice(slash + 1) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assistantText(message: { role?: string; content?: unknown } | undefined): string | undefined {
|
|
29
|
+
if (message?.role !== "assistant") return undefined;
|
|
30
|
+
if (typeof message.content === "string") return message.content;
|
|
31
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
32
|
+
const text = message.content
|
|
33
|
+
.filter((block): block is { type: "text"; text: string } =>
|
|
34
|
+
typeof block === "object" && block !== null && (block as { type?: string }).type === "text" && typeof (block as { text?: unknown }).text === "string",
|
|
35
|
+
)
|
|
36
|
+
.map((block) => block.text)
|
|
37
|
+
.join("\n");
|
|
38
|
+
return text || undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function latestAssistantText(ctx: ExtensionContext, payload?: unknown): string | undefined {
|
|
42
|
+
const messages = (payload as { messages?: Array<{ role?: string; content?: unknown }> } | undefined)?.messages;
|
|
43
|
+
if (messages) {
|
|
44
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
45
|
+
const text = assistantText(messages[index]);
|
|
46
|
+
if (text) return text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const entries = ctx.sessionManager.getEntries();
|
|
50
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
51
|
+
const entry = entries[index];
|
|
52
|
+
if (entry?.type !== "message") continue;
|
|
53
|
+
const text = assistantText(entry.message);
|
|
54
|
+
if (text) return text;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function askUserMessage(payload: unknown): string {
|
|
60
|
+
const value = payload as { questions?: Array<{ question?: unknown }>; question?: unknown; context?: unknown };
|
|
61
|
+
const question = value?.questions?.[0]?.question ?? value?.question;
|
|
62
|
+
return typeof question === "string" && question.trim() ? question.trim() : "Pi is waiting for your answer.";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function permissionMessage(payload: unknown): string {
|
|
66
|
+
const value = payload as { agentName?: unknown; surface?: unknown; value?: unknown; message?: unknown; forwarded?: unknown };
|
|
67
|
+
const agent = typeof value?.agentName === "string" && value.agentName.trim() ? value.agentName.trim() : "Agent";
|
|
68
|
+
const request = [value?.surface, value?.value].filter((item): item is string => typeof item === "string" && item.trim().length > 0).join(" ");
|
|
69
|
+
const detail = request || (typeof value?.message === "string" ? value.message.trim() : "permission");
|
|
70
|
+
return `${agent} requested ${detail || "permission"}${value?.forwarded ? " (forwarded)" : ""}.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function recapBody(ctx: ExtensionContext, config: ResolvedBarkConfig, text: string): Promise<string> {
|
|
74
|
+
const secrets = [config.deviceKey, config.encryption.key, config.encryption.iv];
|
|
75
|
+
if (!config.recap.enabled) return fallbackSummary(text, secrets);
|
|
76
|
+
let model: Model<Api> | undefined;
|
|
77
|
+
if (config.recap.model) {
|
|
78
|
+
const ref = modelRefParts(config.recap.model);
|
|
79
|
+
model = ctx.modelRegistry.find(ref.provider, ref.id) as Model<Api> | undefined;
|
|
80
|
+
} else {
|
|
81
|
+
model = ctx.model as Model<Api> | undefined;
|
|
82
|
+
}
|
|
83
|
+
if (!model) return fallbackSummary(text, secrets);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
87
|
+
if (!auth.ok || !auth.apiKey) return fallbackSummary(text, secrets);
|
|
88
|
+
return summarizeForNotification(
|
|
89
|
+
text,
|
|
90
|
+
model,
|
|
91
|
+
{
|
|
92
|
+
apiKey: auth.apiKey,
|
|
93
|
+
headers: auth.headers as ProviderHeaders | undefined,
|
|
94
|
+
},
|
|
95
|
+
secrets,
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
return fallbackSummary(text, secrets);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function send(
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
title: string,
|
|
105
|
+
body: string,
|
|
106
|
+
options: { priority?: NotifyPriority; recapText?: string; force?: boolean; sessionName?: string; call?: boolean } = {},
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const config = loadResolvedConfig(ctx.cwd);
|
|
109
|
+
if (!options.force && !config.enabled) throw new Error("Bark notifications are disabled");
|
|
110
|
+
const errors = validateResolvedConfig(config);
|
|
111
|
+
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
112
|
+
const message = options.recapText ? await recapBody(ctx, config, options.recapText) : body;
|
|
113
|
+
const sessionName = options.sessionName;
|
|
114
|
+
await client.send(config, {
|
|
115
|
+
title,
|
|
116
|
+
body: sessionName ? `${sessionName}: ${message}` : message,
|
|
117
|
+
level: options.priority ? mapPriority(options.priority) : config.level,
|
|
118
|
+
group: config.group,
|
|
119
|
+
sound: config.sound,
|
|
120
|
+
icon: config.icon,
|
|
121
|
+
call: options.call,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function registryModels(ctx: ExtensionContext): CachedModel[] {
|
|
126
|
+
try {
|
|
127
|
+
const models = ctx.modelRegistry.getAvailable?.() ?? ctx.modelRegistry.getAll() ?? [];
|
|
128
|
+
return models.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openOverlay(
|
|
135
|
+
ctx: ExtensionContext,
|
|
136
|
+
create: (done: () => void, requestRender: () => void, theme: Theme) => {
|
|
137
|
+
render(width: number): string[];
|
|
138
|
+
invalidate(): void;
|
|
139
|
+
handleInput(data: string): void;
|
|
140
|
+
},
|
|
141
|
+
width: "60%" | "80%",
|
|
142
|
+
): void {
|
|
143
|
+
ctx.ui.custom(
|
|
144
|
+
(tui, theme, _keybindings, done) => create(() => done(undefined), () => tui.requestRender(), theme),
|
|
145
|
+
{ overlay: true, overlayOptions: { width, minWidth: width === "60%" ? 44 : 60, anchor: "center", margin: 2 } },
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default function barkExtension(pi: ExtensionAPI): void {
|
|
150
|
+
let sessionCtx: ExtensionContext | undefined;
|
|
151
|
+
let urgentNextTask = false;
|
|
152
|
+
let eventUnsubscribers: Array<() => void> = [];
|
|
153
|
+
|
|
154
|
+
const notifyEvent = (event: BarkEventKey, title: string, body: string, payload?: unknown): void => {
|
|
155
|
+
const call = event === "agent_settled" && urgentNextTask;
|
|
156
|
+
if (event === "agent_settled") {
|
|
157
|
+
urgentNextTask = false;
|
|
158
|
+
sessionCtx?.ui.setStatus("pi-toolkit-urgent", undefined);
|
|
159
|
+
}
|
|
160
|
+
const ctx = sessionCtx;
|
|
161
|
+
if (!ctx) return;
|
|
162
|
+
const config = loadResolvedConfig(ctx.cwd);
|
|
163
|
+
if (!config.enabled || !config.events[event]) return;
|
|
164
|
+
const sessionName = pi.getSessionName?.();
|
|
165
|
+
const notificationTitle = event === "agent_settled" && sessionName ? `Pi · ${sessionName}` : title;
|
|
166
|
+
const recapText = event === "agent_settled" || event === "agent_end" ? latestAssistantText(ctx, payload) : undefined;
|
|
167
|
+
void send(ctx, notificationTitle, body, {
|
|
168
|
+
recapText,
|
|
169
|
+
sessionName: event === "agent_settled" ? undefined : sessionName,
|
|
170
|
+
call,
|
|
171
|
+
}).catch(() => undefined);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
pi.registerCommand("toolkit:bark-setup", {
|
|
175
|
+
description: "Configure Bark server and device key",
|
|
176
|
+
handler: async (_args, ctx) => {
|
|
177
|
+
if (!ctx.hasUI) return void ctx.ui.notify("Bark setup requires an interactive UI.", "warning");
|
|
178
|
+
openOverlay(ctx, (done, requestRender, theme) => {
|
|
179
|
+
const overlay = new BarkSetupOverlay();
|
|
180
|
+
overlay.setTheme(theme);
|
|
181
|
+
overlay.onClose = done;
|
|
182
|
+
overlay.requestRender = requestRender;
|
|
183
|
+
return overlay;
|
|
184
|
+
}, "80%");
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.registerCommand("toolkit:bark-encryption-rotate", {
|
|
189
|
+
description: "Rotate the Bark encryption Key and IV",
|
|
190
|
+
handler: async (_args, ctx) => {
|
|
191
|
+
if (!ctx.hasUI) return void ctx.ui.notify("Encryption rotation requires an interactive UI.", "warning");
|
|
192
|
+
openOverlay(ctx, (done, requestRender, theme) => {
|
|
193
|
+
const overlay = new BarkSetupOverlay(client, "rotate");
|
|
194
|
+
overlay.setTheme(theme);
|
|
195
|
+
overlay.onClose = done;
|
|
196
|
+
overlay.requestRender = requestRender;
|
|
197
|
+
return overlay;
|
|
198
|
+
}, "80%");
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
pi.registerCommand("toolkit:bark-test", {
|
|
203
|
+
description: "Send a Bark test notification",
|
|
204
|
+
handler: async (_args, ctx) => {
|
|
205
|
+
try {
|
|
206
|
+
await send(ctx, "Pi Toolkit — Test", "Bark notifications are working.", { force: true });
|
|
207
|
+
ctx.ui.notify("Bark test notification sent.", "info");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
pi.registerCommand("toolkit:bark-urgent", {
|
|
215
|
+
description: "Toggle continuous ringing for the next completed task",
|
|
216
|
+
handler: async (_args, ctx) => {
|
|
217
|
+
urgentNextTask = !urgentNextTask;
|
|
218
|
+
ctx.ui.setStatus("pi-toolkit-urgent", urgentNextTask ? "☎ next task: 30s ring" : undefined);
|
|
219
|
+
ctx.ui.notify(
|
|
220
|
+
urgentNextTask
|
|
221
|
+
? "Urgent mode armed: the next completed task will ring for 30 seconds."
|
|
222
|
+
: "Urgent mode cancelled.",
|
|
223
|
+
urgentNextTask ? "warning" : "info",
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
pi.registerCommand("toolkit:bark-recap-model", {
|
|
229
|
+
description: "Select the model used for Bark recaps",
|
|
230
|
+
handler: async (_args, ctx) => {
|
|
231
|
+
if (!ctx.hasUI) return void ctx.ui.notify("Model selection requires an interactive UI.", "warning");
|
|
232
|
+
const settings = new BarkSettingsOverlay(ctx.cwd);
|
|
233
|
+
openOverlay(ctx, (done, requestRender, theme) => {
|
|
234
|
+
const config = loadResolvedConfig(ctx.cwd);
|
|
235
|
+
const selector = new RecapModelSelectorOverlay(registryModels(ctx), config.recap.model, (model) => {
|
|
236
|
+
settings.setRecapModel(model);
|
|
237
|
+
settings.handleInput("\r");
|
|
238
|
+
});
|
|
239
|
+
selector.setTheme(theme);
|
|
240
|
+
selector.onClose = done;
|
|
241
|
+
selector.requestRender = requestRender;
|
|
242
|
+
return selector;
|
|
243
|
+
}, "60%");
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
pi.registerCommand("toolkit:bark-notify-settings", {
|
|
248
|
+
description: "Configure Bark events, scopes, and LLM recaps",
|
|
249
|
+
handler: async (_args, ctx) => {
|
|
250
|
+
if (!ctx.hasUI) return void ctx.ui.notify("Notify settings require an interactive UI.", "warning");
|
|
251
|
+
openOverlay(ctx, (done, requestRender, theme) => {
|
|
252
|
+
const overlay = new BarkSettingsOverlay(ctx.cwd);
|
|
253
|
+
overlay.setTheme(theme);
|
|
254
|
+
overlay.onClose = done;
|
|
255
|
+
overlay.requestRender = requestRender;
|
|
256
|
+
overlay.onOpenModelSelector = (currentModel) => {
|
|
257
|
+
openOverlay(ctx, (selectorDone, selectorRender, selectorTheme) => {
|
|
258
|
+
const selector = new RecapModelSelectorOverlay(registryModels(ctx), currentModel, (model) => overlay.setRecapModel(model));
|
|
259
|
+
selector.setTheme(selectorTheme);
|
|
260
|
+
selector.onClose = selectorDone;
|
|
261
|
+
selector.requestRender = selectorRender;
|
|
262
|
+
return selector;
|
|
263
|
+
}, "60%");
|
|
264
|
+
};
|
|
265
|
+
return overlay;
|
|
266
|
+
}, "80%");
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
pi.registerTool({
|
|
271
|
+
name: "toolkit_notify",
|
|
272
|
+
label: "Toolkit Notify",
|
|
273
|
+
description: "Send an explicit Bark notification through the user's pi-toolkit configuration.",
|
|
274
|
+
parameters: Type.Object({
|
|
275
|
+
message: Type.String({ description: "Notification body" }),
|
|
276
|
+
title: Type.Optional(Type.String({ description: "Notification title" })),
|
|
277
|
+
priority: Type.Optional(Type.String({ enum: ["low", "normal", "high"] })),
|
|
278
|
+
}),
|
|
279
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
280
|
+
const values = params as { message: string; title?: string; priority?: NotifyPriority };
|
|
281
|
+
try {
|
|
282
|
+
await send(ctx, values.title || "Pi Toolkit", values.message, { priority: values.priority, sessionName: pi.getSessionName?.() });
|
|
283
|
+
return { content: [{ type: "text" as const, text: "Bark notification sent." }], details: { success: true } };
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
286
|
+
return { content: [{ type: "text" as const, text: `Bark notification failed: ${message}` }], details: { success: false, error: message }, isError: true };
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
pi.on("session_start", (_event, ctx) => {
|
|
292
|
+
sessionCtx = ctx;
|
|
293
|
+
urgentNextTask = false;
|
|
294
|
+
ctx.ui.setStatus("pi-toolkit-urgent", undefined);
|
|
295
|
+
for (const unsubscribe of eventUnsubscribers) unsubscribe();
|
|
296
|
+
eventUnsubscribers = [
|
|
297
|
+
pi.events.on(ASK_USER_PROMPT_EVENT, (payload) => notifyEvent("ask_user_prompt", "Pi Toolkit — Question", askUserMessage(payload))),
|
|
298
|
+
pi.events.on(PERMISSION_PROMPT_EVENT, (payload) => notifyEvent("permission_request", "Pi Toolkit — Permission", permissionMessage(payload))),
|
|
299
|
+
];
|
|
300
|
+
});
|
|
301
|
+
pi.on("agent_settled", (event) => notifyEvent("agent_settled", "Pi Toolkit — Task Complete", "Agent is complete.", event));
|
|
302
|
+
pi.on("agent_end", (event) => notifyEvent("agent_end", "Pi Toolkit — Agent Response", "Agent response finished.", event));
|
|
303
|
+
pi.on("session_shutdown", async () => {
|
|
304
|
+
const ctx = sessionCtx;
|
|
305
|
+
if (ctx) {
|
|
306
|
+
const config = loadResolvedConfig(ctx.cwd);
|
|
307
|
+
if (config.enabled && config.events.session_shutdown) {
|
|
308
|
+
await send(ctx, "Pi Toolkit — Session End", "Pi session is closing.", { sessionName: pi.getSessionName?.() }).catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const unsubscribe of eventUnsubscribers) unsubscribe();
|
|
312
|
+
eventUnsubscribers = [];
|
|
313
|
+
urgentNextTask = false;
|
|
314
|
+
sessionCtx?.ui.setStatus("pi-toolkit-urgent", undefined);
|
|
315
|
+
sessionCtx = undefined;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
3
|
+
import { redactSensitiveText } from "./redact.js";
|
|
4
|
+
|
|
5
|
+
const SYSTEM_PROMPT =
|
|
6
|
+
"Summarize the completed work in one concise sentence for a push notification. Never include passwords, keys, tokens, credentials, authorization headers, private keys, or other secrets; replace any such value with [REDACTED]. Reply with only the sanitized summary.";
|
|
7
|
+
const MAX_INPUT_CHARS = 2_000;
|
|
8
|
+
const FALLBACK_CHARS = 120;
|
|
9
|
+
|
|
10
|
+
export interface RecapCredentials {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
headers?: ProviderHeaders;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function fallbackSummary(text: string, exactSecrets: Array<string | undefined> = []): string {
|
|
16
|
+
const trimmed = redactSensitiveText(text, exactSecrets).trim();
|
|
17
|
+
return trimmed.length <= FALLBACK_CHARS ? trimmed : `${trimmed.slice(0, FALLBACK_CHARS)}...`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Summarize through Pi's provider-neutral model adapter. */
|
|
21
|
+
export async function summarizeForNotification(
|
|
22
|
+
text: string,
|
|
23
|
+
model: Model<Api>,
|
|
24
|
+
credentials: RecapCredentials,
|
|
25
|
+
exactSecrets: Array<string | undefined> = [],
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const secrets = [credentials.apiKey, ...exactSecrets];
|
|
28
|
+
const safeText = redactSensitiveText(text, secrets);
|
|
29
|
+
const input = safeText.length > MAX_INPUT_CHARS ? `${safeText.slice(0, MAX_INPUT_CHARS)}...` : safeText;
|
|
30
|
+
try {
|
|
31
|
+
const response = await completeSimple(
|
|
32
|
+
model,
|
|
33
|
+
{
|
|
34
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
35
|
+
messages: [{ role: "user", content: input, timestamp: Date.now() }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
apiKey: credentials.apiKey,
|
|
39
|
+
headers: credentials.headers,
|
|
40
|
+
maxTokens: 100,
|
|
41
|
+
timeoutMs: 10_000,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
const summary = response.content
|
|
45
|
+
.filter((block): block is Extract<(typeof response.content)[number], { type: "text" }> => block.type === "text")
|
|
46
|
+
.map((block) => block.text)
|
|
47
|
+
.join("")
|
|
48
|
+
.trim();
|
|
49
|
+
return summary ? redactSensitiveText(summary, secrets) : fallbackSummary(text, secrets);
|
|
50
|
+
} catch {
|
|
51
|
+
return fallbackSummary(text, secrets);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const REDACTED = "[REDACTED]";
|
|
2
|
+
|
|
3
|
+
/** Remove common credentials before text reaches either the recap model or Bark. */
|
|
4
|
+
export function redactSensitiveText(text: string, exactSecrets: Array<string | undefined> = []): string {
|
|
5
|
+
let safe = text;
|
|
6
|
+
for (const secret of exactSecrets) {
|
|
7
|
+
if (secret && secret.length >= 4) safe = safe.split(secret).join(REDACTED);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return safe
|
|
11
|
+
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/gi, REDACTED)
|
|
12
|
+
.replace(/\b(https?:\/\/)[^/\s:@]+:[^/\s@]+@/gi, `$1${REDACTED}@`)
|
|
13
|
+
.replace(/\b(https?:\/\/(?:api\.day\.app|[^/\s]+\/push)\/)([^/\s?]+)/gi, `$1${REDACTED}`)
|
|
14
|
+
.replace(/\b((?:authorization|proxy-authorization)\s*[:=]\s*)(?:bearer|basic)\s+[^\s,;]+/gi, `$1${REDACTED}`)
|
|
15
|
+
.replace(
|
|
16
|
+
/((?:\b(?:api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|device[_-]?key|client[_-]?secret|access[_-]?token|auth[_-]?token|refresh[_-]?token|password|passwd|pwd|secret|token|database[_-]?url|dsn)\b|密码|密钥|令牌|口令)\s*["']?\s*(?::|=|:|\bis\b|是)\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|`[^`\r\n]*`|[^\s,;]+)/gi,
|
|
17
|
+
`$1${REDACTED}`,
|
|
18
|
+
)
|
|
19
|
+
.replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[a-z]-[A-Za-z0-9-]{16,})\b/g, REDACTED)
|
|
20
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED);
|
|
21
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
import type { CachedModel } from "../types.js";
|
|
6
|
+
import { boxInnerWidth, OverlayTheme } from "./theme.js";
|
|
7
|
+
|
|
8
|
+
export class RecapModelSelectorOverlay implements Component {
|
|
9
|
+
private filtered: CachedModel[];
|
|
10
|
+
private selectedIndex = 0;
|
|
11
|
+
private filter = "";
|
|
12
|
+
private filterMode = false;
|
|
13
|
+
private readonly overlay = new OverlayTheme();
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
requestRender?: () => void;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly models: CachedModel[],
|
|
19
|
+
currentModel: string | undefined,
|
|
20
|
+
private readonly onSelect: (model: string) => void,
|
|
21
|
+
) {
|
|
22
|
+
this.filtered = [...models];
|
|
23
|
+
const current = this.filtered.findIndex((model) => `${model.provider}/${model.id}` === currentModel);
|
|
24
|
+
if (current >= 0) this.selectedIndex = current;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setTheme(theme: Theme): void {
|
|
28
|
+
this.overlay.setTheme(theme);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
invalidate(): void {}
|
|
32
|
+
|
|
33
|
+
handleInput(data: string): void {
|
|
34
|
+
if (matchesKey(data, "ctrl+c")) return this.onClose?.();
|
|
35
|
+
if (this.filterMode) {
|
|
36
|
+
if (matchesKey(data, "enter")) this.filterMode = false;
|
|
37
|
+
else if (matchesKey(data, "escape")) {
|
|
38
|
+
this.filter = "";
|
|
39
|
+
this.filterMode = false;
|
|
40
|
+
this.applyFilter();
|
|
41
|
+
} else if (matchesKey(data, "up")) this.move(-1);
|
|
42
|
+
else if (matchesKey(data, "down")) this.move(1);
|
|
43
|
+
else if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
|
|
44
|
+
this.filter = this.filter.slice(0, -1);
|
|
45
|
+
this.applyFilter();
|
|
46
|
+
} else if (data.length === 1 && data >= " ") {
|
|
47
|
+
this.filter += data;
|
|
48
|
+
this.applyFilter();
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (matchesKey(data, "escape")) return this.onClose?.();
|
|
53
|
+
if (matchesKey(data, "up") || data === "k") return this.move(-1);
|
|
54
|
+
if (matchesKey(data, "down") || data === "j") return this.move(1);
|
|
55
|
+
if (data === "/") {
|
|
56
|
+
this.filterMode = true;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (matchesKey(data, "enter")) {
|
|
60
|
+
const model = this.filtered[this.selectedIndex];
|
|
61
|
+
if (!model) return;
|
|
62
|
+
this.onSelect(`${model.provider}/${model.id}`);
|
|
63
|
+
this.onClose?.();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private move(delta: number): void {
|
|
68
|
+
this.selectedIndex = Math.max(0, Math.min(Math.max(0, this.filtered.length - 1), this.selectedIndex + delta));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private applyFilter(): void {
|
|
72
|
+
const query = this.filter.toLowerCase();
|
|
73
|
+
this.filtered = query
|
|
74
|
+
? this.models.filter((model) => model.id.toLowerCase().includes(query) || model.name?.toLowerCase().includes(query))
|
|
75
|
+
: [...this.models];
|
|
76
|
+
this.selectedIndex = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(width: number): string[] {
|
|
80
|
+
const inner = boxInnerWidth(width);
|
|
81
|
+
const lines = [
|
|
82
|
+
this.overlay.borderLine(inner, "top"),
|
|
83
|
+
this.overlay.frameLine(this.overlay.fg("accent", this.overlay.bold("🤖 Recap Model")), inner),
|
|
84
|
+
this.overlay.frameLine(this.overlay.fg("dim", this.filterMode ? `Filter: ${this.filter}█` : `${this.models.length} available models · / filters`), inner),
|
|
85
|
+
this.overlay.ruleLine(inner),
|
|
86
|
+
];
|
|
87
|
+
const maxRows = Math.max(5, (process.stdout.rows ?? 30) - 12);
|
|
88
|
+
const start = Math.max(0, this.selectedIndex - Math.floor(maxRows / 2));
|
|
89
|
+
for (let index = start; index < Math.min(this.filtered.length, start + maxRows); index++) {
|
|
90
|
+
const model = this.filtered[index];
|
|
91
|
+
const selected = index === this.selectedIndex;
|
|
92
|
+
const marker = selected ? this.overlay.fg("accent", "▸") : " ";
|
|
93
|
+
const label = `${model.name || model.id} ${this.overlay.fg("dim", `[${model.provider}]`)}`;
|
|
94
|
+
lines.push(this.overlay.frameLine(` ${marker} ${selected ? this.overlay.bold(label) : label}`, inner));
|
|
95
|
+
}
|
|
96
|
+
if (this.filtered.length === 0) lines.push(this.overlay.frameLine(this.overlay.fg("dim", " No matching models"), inner));
|
|
97
|
+
lines.push(this.overlay.ruleLine(inner));
|
|
98
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "↑↓ navigate · / filter · Enter select · Esc close"), inner));
|
|
99
|
+
lines.push(this.overlay.borderLine(inner, "bottom"));
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
}
|