my-pi 0.0.12 → 0.0.13

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.
@@ -2279,2197 +2279,2197 @@ async function mcp(pi) {
2279
2279
  });
2280
2280
  }
2281
2281
  //#endregion
2282
- //#region src/extensions/telemetry-config.ts
2283
- const DEFAULT_CONFIG$1 = {
2284
- version: 1,
2285
- enabled: false
2282
+ //#region src/extensions/prompt-presets.ts
2283
+ const PRESET_STATE_TYPE = "prompt-preset-state";
2284
+ const ENABLED$1 = "[x]";
2285
+ const DISABLED$1 = "[ ]";
2286
+ const SELECTED = "(x)";
2287
+ const UNSELECTED = "( )";
2288
+ const NONE_BASE_ID = "__base_none__";
2289
+ const DEFAULT_PROMPT_PRESETS = {
2290
+ terse: {
2291
+ kind: "base",
2292
+ description: "Short, direct, no fluff",
2293
+ instructions: "Be concise and direct. Default to the shortest response that fully solves the user's request. No purple prose, no filler, no repetitive caveats. Prefer a short paragraph or a few bullets. Only include extra detail when it materially affects the decision, implementation, or next step."
2294
+ },
2295
+ standard: {
2296
+ kind: "base",
2297
+ description: "Clear and concise with key context",
2298
+ instructions: "Be clear, direct, and concise. Include only the reasoning and implementation details that matter. Avoid filler, grandstanding, and ornamental language. Use bullets when they improve scanability."
2299
+ },
2300
+ detailed: {
2301
+ kind: "base",
2302
+ description: "More explanation when nuance matters",
2303
+ instructions: "Be thorough when the task is complex or tradeoffs matter, but stay practical. Explain only the details that help the user decide, verify, or implement. Avoid purple prose and unnecessary scene-setting."
2304
+ },
2305
+ "no-purple-prose": {
2306
+ kind: "layer",
2307
+ description: "Strip out ornamental language",
2308
+ instructions: "Do not use purple prose, flourish, motivational filler, or theatrical transitions. Prefer plain language and concrete statements."
2309
+ },
2310
+ bullets: {
2311
+ kind: "layer",
2312
+ description: "Prefer short bullets when useful",
2313
+ instructions: "When presenting options, findings, or steps, prefer short bullet lists over long paragraphs."
2314
+ },
2315
+ "clarify-first": {
2316
+ kind: "layer",
2317
+ description: "Ask brief clarifying questions when requirements are ambiguous",
2318
+ instructions: "If the request is materially ambiguous, ask the minimum clarifying question(s) needed before proceeding. Do not ask unnecessary questions."
2319
+ },
2320
+ "include-risks": {
2321
+ kind: "layer",
2322
+ description: "Call out notable risks or tradeoffs",
2323
+ instructions: "When making a recommendation or implementation plan, briefly mention the key risk, tradeoff, or caveat if one materially matters."
2324
+ }
2286
2325
  };
2287
- function get_telemetry_config_path() {
2288
- return join(getAgentDir(), "telemetry.json");
2326
+ function normalize_prompt_presets(input) {
2327
+ if (!input || typeof input !== "object") return {};
2328
+ const normalized = {};
2329
+ for (const [raw_name, raw_value] of Object.entries(input)) {
2330
+ const name = raw_name.trim();
2331
+ if (!name) continue;
2332
+ if (typeof raw_value === "string") {
2333
+ normalized[name] = {
2334
+ kind: "base",
2335
+ instructions: raw_value
2336
+ };
2337
+ continue;
2338
+ }
2339
+ if (!raw_value || typeof raw_value !== "object") continue;
2340
+ const candidate = raw_value;
2341
+ if (typeof candidate.instructions !== "string") continue;
2342
+ normalized[name] = {
2343
+ instructions: candidate.instructions,
2344
+ ...candidate.kind === "layer" ? { kind: "layer" } : {},
2345
+ ...typeof candidate.description === "string" ? { description: candidate.description } : {}
2346
+ };
2347
+ }
2348
+ return normalized;
2289
2349
  }
2290
- function get_default_telemetry_db_path() {
2291
- return join(getAgentDir(), "telemetry.db");
2350
+ function to_loaded_prompt_presets(presets, source) {
2351
+ return Object.fromEntries(Object.entries(presets).map(([name, preset]) => [name, {
2352
+ name,
2353
+ kind: preset.kind === "layer" ? "layer" : "base",
2354
+ source,
2355
+ ...preset
2356
+ }]));
2292
2357
  }
2293
- function resolve_telemetry_db_path(cwd, override_path) {
2294
- if (!override_path) return get_default_telemetry_db_path();
2295
- return resolve(cwd, override_path);
2358
+ function get_global_presets_path() {
2359
+ return join(getAgentDir(), "presets.json");
2296
2360
  }
2297
- function load_telemetry_config() {
2298
- const path = get_telemetry_config_path();
2299
- if (!existsSync(path)) return { ...DEFAULT_CONFIG$1 };
2361
+ function get_project_presets_path(cwd) {
2362
+ return join(cwd, ".pi", "presets.json");
2363
+ }
2364
+ function get_persisted_prompt_state_path() {
2365
+ return join(getAgentDir(), "prompt-preset-state.json");
2366
+ }
2367
+ function read_prompt_presets_file(path) {
2368
+ if (!existsSync(path)) return {};
2300
2369
  try {
2301
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
2302
- return {
2303
- version: typeof parsed.version === "number" ? parsed.version : DEFAULT_CONFIG$1.version,
2304
- enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : DEFAULT_CONFIG$1.enabled
2305
- };
2370
+ return normalize_prompt_presets(JSON.parse(readFileSync(path, "utf-8")));
2306
2371
  } catch {
2307
- return { ...DEFAULT_CONFIG$1 };
2372
+ return {};
2308
2373
  }
2309
2374
  }
2310
- function save_telemetry_config(config) {
2311
- const path = get_telemetry_config_path();
2375
+ function load_prompt_presets(cwd) {
2376
+ return Object.assign({}, to_loaded_prompt_presets(DEFAULT_PROMPT_PRESETS, "builtin"), to_loaded_prompt_presets(read_prompt_presets_file(get_global_presets_path()), "user"), to_loaded_prompt_presets(read_prompt_presets_file(get_project_presets_path(cwd)), "project"));
2377
+ }
2378
+ function sort_prompt_presets(presets) {
2379
+ return Object.fromEntries(Object.entries(presets).sort(([a], [b]) => a.localeCompare(b)));
2380
+ }
2381
+ function save_project_prompt_presets(cwd, presets) {
2382
+ const path = get_project_presets_path(cwd);
2312
2383
  const dir = dirname(path);
2313
2384
  if (!existsSync(dir)) mkdirSync(dir, {
2314
2385
  recursive: true,
2315
2386
  mode: 448
2316
2387
  });
2317
2388
  const tmp = `${path}.tmp-${Date.now()}`;
2318
- writeFileSync(tmp, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
2389
+ writeFileSync(tmp, JSON.stringify(sort_prompt_presets(presets), null, " ") + "\n", { mode: 384 });
2319
2390
  renameSync(tmp, path);
2391
+ return path;
2320
2392
  }
2321
- function resolve_telemetry_enabled(config = load_telemetry_config(), override) {
2322
- return override ?? config.enabled;
2323
- }
2324
- //#endregion
2325
- //#region src/extensions/otel.ts
2326
- const COMMANDS = [
2327
- "status",
2328
- "stats",
2329
- "query",
2330
- "export",
2331
- "on",
2332
- "off",
2333
- "path"
2334
- ];
2335
- const DEFAULT_QUERY_LIMIT = 20;
2336
- function parse_int(value) {
2337
- if (!value) return null;
2338
- const parsed = Number.parseInt(value, 10);
2339
- return Number.isFinite(parsed) ? parsed : null;
2340
- }
2341
- function get_eval_metadata() {
2393
+ function remove_project_prompt_preset(cwd, name) {
2394
+ const path = get_project_presets_path(cwd);
2395
+ const project_presets = read_prompt_presets_file(path);
2396
+ if (!(name in project_presets)) return {
2397
+ removed: false,
2398
+ path,
2399
+ remaining: Object.keys(project_presets).length
2400
+ };
2401
+ delete project_presets[name];
2402
+ const remaining = Object.keys(project_presets).length;
2403
+ if (remaining === 0) {
2404
+ if (existsSync(path)) unlinkSync(path);
2405
+ return {
2406
+ removed: true,
2407
+ path,
2408
+ remaining
2409
+ };
2410
+ }
2411
+ save_project_prompt_presets(cwd, project_presets);
2342
2412
  return {
2343
- run_id: process.env.MY_PI_EVAL_RUN_ID ?? null,
2344
- case_id: process.env.MY_PI_EVAL_CASE_ID ?? null,
2345
- attempt: parse_int(process.env.MY_PI_EVAL_ATTEMPT),
2346
- suite: process.env.MY_PI_EVAL_SUITE ?? null
2413
+ removed: true,
2414
+ path,
2415
+ remaining
2347
2416
  };
2348
2417
  }
2349
- function get_model_identity(model) {
2350
- if (!model) return {
2351
- provider: null,
2352
- id: null
2353
- };
2418
+ function normalize_prompt_preset_state(input) {
2419
+ if (!input || typeof input !== "object") return void 0;
2420
+ const candidate = input;
2354
2421
  return {
2355
- provider: typeof model.provider === "string" ? model.provider : null,
2356
- id: typeof model.id === "string" ? model.id : null
2422
+ base_name: typeof candidate.base_name === "string" && candidate.base_name.trim() ? candidate.base_name.trim() : null,
2423
+ layer_names: Array.isArray(candidate.layer_names) ? [...new Set(candidate.layer_names.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim()))].sort() : []
2357
2424
  };
2358
2425
  }
2359
- function get_session_file(ctx) {
2360
- return ctx.sessionManager.getSessionFile?.() ?? null;
2361
- }
2362
- function safe_json_stringify(value) {
2363
- if (value === void 0) return null;
2426
+ function read_persisted_prompt_states(path = get_persisted_prompt_state_path()) {
2427
+ if (!existsSync(path)) return {
2428
+ version: 1,
2429
+ projects: {}
2430
+ };
2364
2431
  try {
2365
- return JSON.stringify(value);
2432
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
2433
+ const raw_projects = parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {};
2434
+ const projects = {};
2435
+ for (const [cwd, value] of Object.entries(raw_projects)) {
2436
+ const normalized = normalize_prompt_preset_state(value);
2437
+ if (!normalized) continue;
2438
+ projects[cwd] = normalized;
2439
+ }
2440
+ return {
2441
+ version: typeof parsed.version === "number" ? parsed.version : 1,
2442
+ projects
2443
+ };
2366
2444
  } catch {
2367
- return JSON.stringify({
2368
- type: typeof value,
2369
- unserializable: true
2370
- });
2445
+ return {
2446
+ version: 1,
2447
+ projects: {}
2448
+ };
2371
2449
  }
2372
2450
  }
2373
- function summarize_value(value, depth = 0) {
2374
- if (value == null) return null;
2375
- if (typeof value === "string") return {
2376
- type: "string",
2377
- bytes: Buffer.byteLength(value, "utf-8"),
2378
- lines: value === "" ? 0 : value.split(/\r?\n/).length
2379
- };
2380
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
2381
- if (Array.isArray(value)) return {
2382
- type: "array",
2383
- length: value.length,
2384
- items: depth >= 1 ? void 0 : value.slice(0, 5).map((item) => summarize_value(item, depth + 1))
2451
+ function load_persisted_prompt_state(cwd, path = get_persisted_prompt_state_path()) {
2452
+ return read_persisted_prompt_states(path).projects[cwd];
2453
+ }
2454
+ function save_persisted_prompt_state(cwd, state, path = get_persisted_prompt_state_path()) {
2455
+ const persisted = read_persisted_prompt_states(path);
2456
+ persisted.projects[cwd] = normalize_prompt_preset_state(state) ?? {
2457
+ base_name: null,
2458
+ layer_names: []
2385
2459
  };
2386
- if (typeof value === "object") {
2387
- const entries = Object.entries(value);
2388
- const summary = {
2389
- type: "object",
2390
- keys: entries.map(([key]) => key).slice(0, 20)
2391
- };
2392
- if (depth < 1) for (const [key, child] of entries.slice(0, 10)) {
2393
- if (key === "oldText" || key === "newText" || key === "content" || key === "text") {
2394
- summary[`${key}_summary`] = summarize_value(child, depth + 1);
2395
- continue;
2396
- }
2397
- summary[key] = summarize_value(child, depth + 1);
2398
- }
2399
- return summary;
2400
- }
2401
- return { type: typeof value };
2460
+ const dir = dirname(path);
2461
+ if (!existsSync(dir)) mkdirSync(dir, {
2462
+ recursive: true,
2463
+ mode: 448
2464
+ });
2465
+ const tmp = `${path}.tmp-${Date.now()}`;
2466
+ writeFileSync(tmp, JSON.stringify({
2467
+ version: 1,
2468
+ projects: Object.fromEntries(Object.entries(persisted.projects).sort(([a], [b]) => a.localeCompare(b)))
2469
+ }, null, " ") + "\n", { mode: 384 });
2470
+ renameSync(tmp, path);
2471
+ return path;
2402
2472
  }
2403
- function summarize_tool_args(tool_name, args) {
2404
- if (!args || typeof args !== "object") return safe_json_stringify(summarize_value(args));
2405
- const input = args;
2406
- switch (tool_name) {
2407
- case "bash": return safe_json_stringify({
2408
- tool: tool_name,
2409
- timeout: input.timeout ?? null,
2410
- command: summarize_value(input.command)
2411
- });
2412
- case "read":
2413
- case "write":
2414
- case "edit": return safe_json_stringify({
2415
- tool: tool_name,
2416
- path: typeof input.path === "string" ? input.path : null,
2417
- offset: typeof input.offset === "number" ? input.offset : null,
2418
- limit: typeof input.limit === "number" ? input.limit : null,
2419
- content: summarize_value(input.content),
2420
- edits: summarize_value(input.edits)
2421
- });
2422
- default: return safe_json_stringify({
2423
- tool: tool_name,
2424
- summary: summarize_value(args)
2425
- });
2473
+ function get_last_preset_state(ctx) {
2474
+ const entries = ctx.sessionManager.getEntries();
2475
+ for (let i = entries.length - 1; i >= 0; i--) {
2476
+ const entry = entries[i];
2477
+ if (entry.type === "custom" && entry.customType === PRESET_STATE_TYPE && entry.data) return entry.data;
2426
2478
  }
2427
2479
  }
2428
- function summarize_tool_result(result) {
2429
- return safe_json_stringify(summarize_value(result));
2430
- }
2431
- function summarize_headers(headers) {
2432
- return safe_json_stringify({
2433
- keys: Object.keys(headers).slice(0, 20),
2434
- count: Object.keys(headers).length
2435
- });
2436
- }
2437
- function summarize_provider_payload(payload) {
2438
- return safe_json_stringify(summarize_value(payload));
2439
- }
2440
- function get_stop_reason(message) {
2441
- if (!message || typeof message !== "object") return null;
2442
- const stop_reason = message.stopReason;
2443
- return typeof stop_reason === "string" ? stop_reason : null;
2444
- }
2445
- function get_error_message(message) {
2446
- if (!message || typeof message !== "object") return null;
2447
- const error_message = message.errorMessage;
2448
- return typeof error_message === "string" ? error_message : null;
2449
- }
2450
- function infer_run_outcome(event) {
2451
- const last_assistant = event.messages.filter((message) => message.role === "assistant").at(-1);
2452
- const stop_reason = get_stop_reason(last_assistant);
2453
- if (stop_reason === "error") return {
2454
- success: false,
2455
- error_message: get_error_message(last_assistant) ?? "agent error"
2456
- };
2457
- if (stop_reason === "aborted") return {
2458
- success: false,
2459
- error_message: get_error_message(last_assistant) ?? "agent aborted"
2460
- };
2461
- return {
2462
- success: true,
2463
- error_message: null
2464
- };
2465
- }
2466
- function format_telemetry_status(options) {
2467
- const override_label = options.override === void 0 ? "none" : options.override ? "--telemetry" : "--no-telemetry";
2468
- return [
2469
- `telemetry ${options.effective_enabled ? "enabled" : "disabled"} now`,
2470
- `default ${options.saved_enabled ? "enabled" : "disabled"}`,
2471
- `override ${override_label}`,
2472
- `db ${options.db_path}`
2473
- ].join("\n");
2474
- }
2475
- function format_bytes(bytes) {
2476
- if (bytes < 1024) return `${bytes} B`;
2477
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
2478
- return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
2479
- }
2480
- function format_timestamp(timestamp) {
2481
- return new Date(timestamp).toISOString();
2480
+ function sets_equal$1(a, b) {
2481
+ if (a.size !== b.size) return false;
2482
+ for (const value of a) if (!b.has(value)) return false;
2483
+ return true;
2482
2484
  }
2483
- function format_duration(duration_ms) {
2484
- if (duration_ms === null) return "open";
2485
- if (duration_ms < 1e3) return `${duration_ms}ms`;
2486
- if (duration_ms < 6e4) return `${(duration_ms / 1e3).toFixed(1)}s`;
2487
- return `${(duration_ms / 6e4).toFixed(1)}m`;
2485
+ function get_prompt_source_label(source) {
2486
+ switch (source) {
2487
+ case "builtin": return "built-in";
2488
+ case "user": return "user";
2489
+ case "project": return "project";
2490
+ }
2488
2491
  }
2489
- function format_success(value) {
2490
- if (value === true) return "success";
2491
- if (value === false) return "failure";
2492
- return "unknown";
2492
+ function list_base_presets(presets) {
2493
+ return Object.values(presets).filter((preset) => preset.kind === "base").sort((a, b) => a.name.localeCompare(b.name));
2493
2494
  }
2494
- function tokenize_command_args(input) {
2495
- return (input.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S+/g) ?? []).map((token) => {
2496
- if (token.startsWith("\"") && token.endsWith("\"") || token.startsWith("'") && token.endsWith("'")) return token.slice(1, -1);
2497
- return token;
2498
- });
2495
+ function list_layer_presets(presets) {
2496
+ return Object.values(presets).filter((preset) => preset.kind === "layer").sort((a, b) => a.name.localeCompare(b.name));
2499
2497
  }
2500
- function parse_telemetry_command(input) {
2501
- const tokens = tokenize_command_args(input.trim());
2502
- const subcommand = tokens[0] ?? "status";
2503
- const filters = {};
2504
- let export_path = null;
2505
- const errors = [];
2506
- for (const token of tokens.slice(1)) {
2507
- const equals_index = token.indexOf("=");
2508
- if (equals_index === -1) {
2509
- if (subcommand === "export" && export_path === null) export_path = token;
2510
- else errors.push(`Unexpected argument: ${token}`);
2511
- continue;
2512
- }
2513
- const key = token.slice(0, equals_index);
2514
- const value = token.slice(equals_index + 1);
2515
- switch (key) {
2516
- case "eval_run_id":
2517
- case "run":
2518
- filters.eval_run_id = value;
2519
- break;
2520
- case "eval_case_id":
2521
- case "case":
2522
- filters.eval_case_id = value;
2523
- break;
2524
- case "eval_suite":
2525
- case "suite":
2526
- filters.eval_suite = value;
2527
- break;
2528
- case "success":
2529
- if (value === "true") filters.success = true;
2530
- else if (value === "false") filters.success = false;
2531
- else if (value === "null") filters.success = null;
2532
- else errors.push(`Invalid success value: ${value}. Use true, false, or null`);
2533
- break;
2534
- case "limit": {
2535
- const parsed = Number.parseInt(value, 10);
2536
- if (!Number.isFinite(parsed) || parsed <= 0) errors.push(`Invalid limit value: ${value}. Use a positive integer`);
2537
- else filters.limit = parsed;
2538
- break;
2539
- }
2540
- default: errors.push(`Unknown filter: ${key}`);
2498
+ function format_summary(active_base_name, active_layers, presets) {
2499
+ const lines = [`Base: ${active_base_name ?? "(none)"}`];
2500
+ const layer_names = [...active_layers].sort();
2501
+ if (layer_names.length === 0) lines.push("Layers: (none)");
2502
+ else {
2503
+ lines.push("Layers:");
2504
+ for (const name of layer_names) {
2505
+ const preset = presets[name];
2506
+ const description = preset?.description ? ` — ${preset.description}` : "";
2507
+ lines.push(`- ${name}${description}`);
2541
2508
  }
2542
2509
  }
2543
- if (subcommand === "query" && filters.limit === void 0) filters.limit = DEFAULT_QUERY_LIMIT;
2544
- return {
2545
- subcommand,
2546
- export_path,
2547
- filters,
2548
- errors
2549
- };
2510
+ return lines.join("\n");
2550
2511
  }
2551
- function format_filter_summary(filters) {
2512
+ function format_active_details(active_base_name, active_layers, presets) {
2552
2513
  const parts = [];
2553
- if (filters.eval_run_id !== void 0) parts.push(`eval_run_id=${filters.eval_run_id}`);
2554
- if (filters.eval_case_id !== void 0) parts.push(`eval_case_id=${filters.eval_case_id}`);
2555
- if (filters.eval_suite !== void 0) parts.push(`eval_suite=${filters.eval_suite}`);
2556
- if (filters.success !== void 0) parts.push(`success=${String(filters.success)}`);
2557
- if (filters.limit !== void 0) parts.push(`limit=${filters.limit}`);
2558
- return parts.length > 0 ? parts.join(" ") : "none";
2514
+ if (active_base_name) {
2515
+ const base = presets[active_base_name];
2516
+ if (base) {
2517
+ parts.push(`Base: ${base.name}`);
2518
+ if (base.description) parts.push(`Description: ${base.description}`);
2519
+ parts.push(`Source: ${get_prompt_source_label(base.source)}`);
2520
+ parts.push("", base.instructions.trim());
2521
+ }
2522
+ }
2523
+ const layer_names = [...active_layers].sort();
2524
+ if (layer_names.length > 0) {
2525
+ if (parts.length > 0) parts.push("", "---", "");
2526
+ parts.push("Layers:");
2527
+ for (const name of layer_names) {
2528
+ const layer = presets[name];
2529
+ if (!layer) continue;
2530
+ parts.push(`- ${layer.name} (${get_prompt_source_label(layer.source)})`);
2531
+ if (layer.description) parts.push(` ${layer.description}`);
2532
+ }
2533
+ }
2534
+ return parts.join("\n") || "No preset or layers active";
2559
2535
  }
2560
- function format_telemetry_stats(options) {
2561
- return [
2562
- `db ${options.db_path}`,
2563
- `schema v${options.stats.schema_version}`,
2564
- `runs ${options.stats.runs}`,
2565
- `turns ${options.stats.turns}`,
2566
- `tool_calls ${options.stats.tool_calls}`,
2567
- `provider_requests ${options.stats.provider_requests}`,
2568
- `db_bytes ${format_bytes(options.stats.db_bytes)}`,
2569
- `wal_bytes ${format_bytes(options.stats.wal_bytes)}`,
2570
- `total_bytes ${format_bytes(options.stats.total_bytes)}`
2571
- ].join("\n");
2536
+ function get_footer_prompt_status(active_base_name, active_layers) {
2537
+ if (!active_base_name && active_layers.size === 0) return;
2538
+ return `prompt:${active_base_name ?? "none"}${active_layers.size > 0 ? ` +${active_layers.size}` : ""}`;
2572
2539
  }
2573
- function format_telemetry_query_results(options) {
2574
- if (options.runs.length === 0) return [
2575
- `db ${options.db_path}`,
2576
- `filters ${format_filter_summary(options.filters)}`,
2577
- "no matching runs"
2578
- ].join("\n");
2579
- return [
2580
- `db ${options.db_path}`,
2581
- `filters ${format_filter_summary(options.filters)}`,
2582
- ...options.runs.map((run) => [
2583
- `${format_timestamp(run.started_at)} ${run.id}`,
2584
- `status=${format_success(run.success)}`,
2585
- `duration=${format_duration(run.duration_ms)}`,
2586
- `turns=${run.turn_count}`,
2587
- `tools=${run.tool_call_count}`,
2588
- `tool_errors=${run.tool_error_count}`,
2589
- `provider_requests=${run.provider_request_count}`,
2590
- run.eval_run_id ? `eval_run_id=${run.eval_run_id}` : null,
2591
- run.eval_case_id ? `eval_case_id=${run.eval_case_id}` : null,
2592
- run.eval_suite ? `eval_suite=${run.eval_suite}` : null
2593
- ].filter(Boolean).join(" "))
2594
- ].join("\n");
2540
+ function sanitize_status_text(text) {
2541
+ return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
2595
2542
  }
2596
- function get_default_telemetry_export_path(cwd) {
2597
- return resolve(cwd, `telemetry-export-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2543
+ function format_token_count(count) {
2544
+ if (count < 1e3) return count.toString();
2545
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
2546
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
2547
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
2548
+ return `${Math.round(count / 1e6)}M`;
2598
2549
  }
2599
- async function default_load_store(db_path) {
2600
- const { TelemetryDatabase } = await import("./telemetry-db-BnenoOSj.js");
2601
- return TelemetryDatabase.open(db_path);
2550
+ function get_current_thinking_level(ctx) {
2551
+ const entries = ctx.sessionManager.getEntries();
2552
+ for (let i = entries.length - 1; i >= 0; i--) {
2553
+ const entry = entries[i];
2554
+ if (entry.type === "thinking_level_change" && typeof entry.thinkingLevel === "string") return entry.thinkingLevel;
2555
+ }
2556
+ return ctx.model?.reasoning ? "high" : "off";
2602
2557
  }
2603
- function create_telemetry_extension(options = {}) {
2604
- return async function telemetry(pi) {
2605
- const now = options.now ?? (() => Date.now());
2606
- const load_store = options.load_store ?? default_load_store;
2607
- const cwd = options.cwd ?? process.cwd();
2608
- const db_path = resolve_telemetry_db_path(cwd, options.db_path);
2609
- let config = load_telemetry_config();
2610
- let store = null;
2611
- let effective_enabled = resolve_telemetry_enabled(config, options.enabled);
2612
- let current_model = {
2613
- provider: null,
2614
- id: null
2558
+ function render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers) {
2559
+ let total_input = 0;
2560
+ let total_output = 0;
2561
+ let total_cache_read = 0;
2562
+ let total_cache_write = 0;
2563
+ let total_cost = 0;
2564
+ for (const entry of ctx.sessionManager.getEntries()) if (entry.type === "message" && entry.message.role === "assistant") {
2565
+ total_input += entry.message.usage.input;
2566
+ total_output += entry.message.usage.output;
2567
+ total_cache_read += entry.message.usage.cacheRead;
2568
+ total_cache_write += entry.message.usage.cacheWrite;
2569
+ total_cost += entry.message.usage.cost.total;
2570
+ }
2571
+ const context_usage = ctx.getContextUsage();
2572
+ const context_window = context_usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
2573
+ const context_percent_value = context_usage?.percent ?? 0;
2574
+ const context_percent = context_usage?.percent !== null ? context_percent_value.toFixed(1) : "?";
2575
+ let pwd = ctx.cwd;
2576
+ const home = process.env.HOME || process.env.USERPROFILE;
2577
+ if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
2578
+ const branch = footer_data.getGitBranch();
2579
+ if (branch) pwd = `${pwd} (${branch})`;
2580
+ const session_name = ctx.sessionManager.getSessionName();
2581
+ if (session_name) pwd = `${pwd} • ${session_name}`;
2582
+ const stats_parts = [];
2583
+ if (total_input) stats_parts.push(`↑${format_token_count(total_input)}`);
2584
+ if (total_output) stats_parts.push(`↓${format_token_count(total_output)}`);
2585
+ if (total_cache_read) stats_parts.push(`R${format_token_count(total_cache_read)}`);
2586
+ if (total_cache_write) stats_parts.push(`W${format_token_count(total_cache_write)}`);
2587
+ const using_subscription = ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
2588
+ if (total_cost || using_subscription) stats_parts.push(`$${total_cost.toFixed(3)}${using_subscription ? " (sub)" : ""}`);
2589
+ const context_percent_display = context_percent === "?" ? `?/${format_token_count(context_window)}` : `${context_percent}%/${format_token_count(context_window)}`;
2590
+ let context_percent_str = context_percent_display;
2591
+ if (context_percent_value > 90) context_percent_str = theme.fg("error", context_percent_display);
2592
+ else if (context_percent_value > 70) context_percent_str = theme.fg("warning", context_percent_display);
2593
+ stats_parts.push(context_percent_str);
2594
+ let stats_left = stats_parts.join(" ");
2595
+ let stats_left_width = visibleWidth(stats_left);
2596
+ if (stats_left_width > width) {
2597
+ stats_left = truncateToWidth(stats_left, width, "...");
2598
+ stats_left_width = visibleWidth(stats_left);
2599
+ }
2600
+ const model_name = ctx.model?.id || "no-model";
2601
+ const thinking_level = get_current_thinking_level(ctx);
2602
+ let right_side_without_provider = model_name;
2603
+ if (ctx.model?.reasoning) right_side_without_provider = thinking_level === "off" ? `${model_name} • thinking off` : `${model_name} • ${thinking_level}`;
2604
+ let right_side = right_side_without_provider;
2605
+ if (footer_data.getAvailableProviderCount() > 1 && ctx.model) {
2606
+ right_side = `(${ctx.model.provider}) ${right_side_without_provider}`;
2607
+ if (stats_left_width + 2 + visibleWidth(right_side) > width) right_side = right_side_without_provider;
2608
+ }
2609
+ const right_side_width = visibleWidth(right_side);
2610
+ const total_needed = stats_left_width + 2 + right_side_width;
2611
+ let stats_line;
2612
+ if (total_needed <= width) {
2613
+ const padding = " ".repeat(width - stats_left_width - right_side_width);
2614
+ stats_line = stats_left + padding + right_side;
2615
+ } else {
2616
+ const available_for_right = width - stats_left_width - 2;
2617
+ if (available_for_right > 0) {
2618
+ const truncated_right = truncateToWidth(right_side, available_for_right, "");
2619
+ const truncated_right_width = visibleWidth(truncated_right);
2620
+ const padding = " ".repeat(Math.max(0, width - stats_left_width - truncated_right_width));
2621
+ stats_line = stats_left + padding + truncated_right;
2622
+ } else stats_line = stats_left;
2623
+ }
2624
+ const dim_stats_left = theme.fg("dim", stats_left);
2625
+ const remainder = stats_line.slice(stats_left.length);
2626
+ const dim_remainder = theme.fg("dim", remainder);
2627
+ const lines = [truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")), dim_stats_left + dim_remainder];
2628
+ const prompt_status = get_footer_prompt_status(active_base_name, active_layers);
2629
+ if (prompt_status) {
2630
+ const themed_status = theme.fg("dim", prompt_status);
2631
+ const status_width = visibleWidth(themed_status);
2632
+ const aligned_status = status_width >= width ? truncateToWidth(themed_status, width, theme.fg("dim", "...")) : `${" ".repeat(width - status_width)}${themed_status}`;
2633
+ lines.push(aligned_status);
2634
+ }
2635
+ const other_statuses = Array.from(footer_data.getExtensionStatuses().entries()).filter(([key]) => key !== "preset").sort(([a], [b]) => a.localeCompare(b)).map(([, text]) => sanitize_status_text(text));
2636
+ if (other_statuses.length > 0) lines.push(truncateToWidth(other_statuses.join(" "), width, theme.fg("dim", "...")));
2637
+ return lines;
2638
+ }
2639
+ function set_status(ctx, active_base_name, active_layers) {
2640
+ ctx.ui.setStatus("preset", void 0);
2641
+ if (!ctx.hasUI) return;
2642
+ ctx.ui.setFooter((tui, theme, footer_data) => {
2643
+ return {
2644
+ dispose: footer_data.onBranchChange(() => tui.requestRender()),
2645
+ invalidate() {},
2646
+ render(width) {
2647
+ return render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers);
2648
+ }
2615
2649
  };
2616
- let active_run = null;
2617
- const active_turns = /* @__PURE__ */ new Map();
2618
- const provider_request_ids = [];
2619
- async function ensure_store() {
2620
- if (!effective_enabled) return null;
2621
- if (!store) store = await load_store(db_path);
2622
- return store;
2623
- }
2624
- function finish_active_run_on_disable(reason) {
2625
- if (!store || !active_run) return;
2626
- store.finish_run({
2627
- id: active_run.id,
2628
- ended_at: now(),
2629
- success: null,
2630
- error_message: reason
2650
+ });
2651
+ }
2652
+ function persist_state(pi, ctx, active_base_name, active_layers) {
2653
+ const state = {
2654
+ base_name: active_base_name ?? null,
2655
+ layer_names: [...active_layers].sort()
2656
+ };
2657
+ pi.appendEntry(PRESET_STATE_TYPE, state);
2658
+ save_persisted_prompt_state(ctx.cwd, state);
2659
+ }
2660
+ function normalize_active_state(presets, active_base_name, active_layers) {
2661
+ return {
2662
+ active_base_name: active_base_name && presets[active_base_name]?.kind === "base" ? active_base_name : void 0,
2663
+ active_layers: new Set([...active_layers].filter((name) => presets[name]?.kind === "layer"))
2664
+ };
2665
+ }
2666
+ function parse_preset_flag(flag) {
2667
+ return flag.split(",").map((item) => item.trim()).filter(Boolean);
2668
+ }
2669
+ function is_subcommand(command) {
2670
+ return [
2671
+ "list",
2672
+ "show",
2673
+ "clear",
2674
+ "edit",
2675
+ "delete",
2676
+ "reset",
2677
+ "reload",
2678
+ "base",
2679
+ "enable",
2680
+ "disable",
2681
+ "toggle"
2682
+ ].includes(command);
2683
+ }
2684
+ async function prompt_presets(pi) {
2685
+ let presets = {};
2686
+ let active_base_name;
2687
+ let active_layers = /* @__PURE__ */ new Set();
2688
+ function get_base(name) {
2689
+ return name ? presets[name] : void 0;
2690
+ }
2691
+ function get_layer(name) {
2692
+ const preset = presets[name];
2693
+ return preset?.kind === "layer" ? preset : void 0;
2694
+ }
2695
+ function commit_state(ctx, next_base_name, next_layers, options) {
2696
+ active_base_name = next_base_name;
2697
+ active_layers = new Set(next_layers);
2698
+ set_status(ctx, active_base_name, active_layers);
2699
+ if (options?.persist !== false) persist_state(pi, ctx, active_base_name, active_layers);
2700
+ if (options?.notify) ctx.ui.notify(options.notify, "info");
2701
+ }
2702
+ function activate_base(name, ctx, options) {
2703
+ if (!name) {
2704
+ commit_state(ctx, void 0, active_layers, {
2705
+ persist: options?.persist,
2706
+ notify: "Base preset cleared"
2631
2707
  });
2632
- active_run = null;
2633
- active_turns.clear();
2634
- provider_request_ids.length = 0;
2708
+ return true;
2635
2709
  }
2636
- function close_store() {
2637
- if (!store) return;
2638
- store.close();
2639
- store = null;
2710
+ const preset = get_base(name);
2711
+ if (!preset) {
2712
+ ctx.ui.notify(`Unknown base preset: ${name}`, "warning");
2713
+ return false;
2640
2714
  }
2641
- function command_message(ctx, message) {
2642
- if (ctx.hasUI) ctx.ui.notify(message);
2643
- else console.error(message);
2715
+ commit_state(ctx, preset.name, active_layers, {
2716
+ persist: options?.persist,
2717
+ notify: `Base preset "${preset.name}" activated`
2718
+ });
2719
+ return true;
2720
+ }
2721
+ function set_layer_enabled(name, enabled, ctx, options) {
2722
+ const preset = get_layer(name);
2723
+ if (!preset) {
2724
+ ctx.ui.notify(`Unknown prompt layer: ${name}`, "warning");
2725
+ return false;
2644
2726
  }
2645
- pi.registerCommand("telemetry", {
2646
- description: "Manage local SQLite telemetry for evals and debugging",
2647
- getArgumentCompletions: (prefix) => {
2648
- const first_token = prefix.trim().split(/\s+/, 1)[0] ?? "";
2649
- return COMMANDS.filter((command) => command.startsWith(first_token)).map((command) => ({
2650
- value: command,
2651
- label: command
2652
- }));
2653
- },
2654
- handler: async (args, ctx) => {
2655
- const parsed = parse_telemetry_command(args);
2656
- const subcommand = parsed.subcommand;
2657
- if (!COMMANDS.includes(subcommand)) {
2658
- command_message(ctx, `Unknown telemetry command: ${subcommand}. Use: ${COMMANDS.join(", ")}`);
2659
- return;
2660
- }
2661
- if (parsed.errors.length > 0) {
2662
- command_message(ctx, parsed.errors.join("\n"));
2663
- return;
2664
- }
2665
- if (subcommand === "status") {
2666
- command_message(ctx, format_telemetry_status({
2667
- saved_enabled: config.enabled,
2668
- effective_enabled,
2669
- override: options.enabled,
2670
- db_path
2671
- }));
2672
- return;
2673
- }
2674
- if (subcommand === "stats") {
2675
- if (!existsSync(db_path)) {
2676
- command_message(ctx, `No telemetry database at ${db_path}`);
2677
- return;
2678
- }
2679
- const stats_store = store ?? await load_store(db_path);
2680
- const should_close_after = stats_store !== store;
2681
- try {
2682
- command_message(ctx, format_telemetry_stats({
2683
- db_path,
2684
- stats: stats_store.get_stats()
2685
- }));
2686
- } finally {
2687
- if (should_close_after) stats_store.close();
2688
- }
2689
- return;
2690
- }
2691
- if (subcommand === "query" || subcommand === "export") {
2692
- if (!existsSync(db_path)) {
2693
- command_message(ctx, `No telemetry database at ${db_path}`);
2694
- return;
2695
- }
2696
- const query_store = store ?? await load_store(db_path);
2697
- const should_close_after = query_store !== store;
2698
- try {
2699
- const runs = query_store.query_runs(parsed.filters);
2700
- if (subcommand === "query") {
2701
- command_message(ctx, format_telemetry_query_results({
2702
- db_path,
2703
- filters: parsed.filters,
2704
- runs
2705
- }));
2706
- return;
2707
- }
2708
- const export_path = resolve(cwd, parsed.export_path ?? get_default_telemetry_export_path(cwd));
2709
- mkdirSync(dirname(export_path), { recursive: true });
2710
- writeFileSync(export_path, JSON.stringify({
2711
- exported_at: (/* @__PURE__ */ new Date()).toISOString(),
2712
- db_path,
2713
- schema_version: query_store.get_stats().schema_version,
2714
- filters: parsed.filters,
2715
- runs
2716
- }, null, 2), "utf-8");
2717
- command_message(ctx, `Exported ${runs.length} telemetry run${runs.length === 1 ? "" : "s"} to ${export_path}`);
2718
- return;
2719
- } finally {
2720
- if (should_close_after) query_store.close();
2721
- }
2722
- }
2723
- if (subcommand === "path") {
2724
- command_message(ctx, db_path);
2725
- return;
2726
- }
2727
- const next_enabled = subcommand === "on";
2728
- config = {
2729
- ...config,
2730
- enabled: next_enabled
2731
- };
2732
- save_telemetry_config(config);
2733
- if (options.enabled !== void 0) {
2734
- command_message(ctx, [`Saved default telemetry ${next_enabled ? "enabled" : "disabled"}.`, `Current process still uses ${options.enabled ? "--telemetry" : "--no-telemetry"}.`].join(" "));
2735
- return;
2736
- }
2737
- effective_enabled = next_enabled;
2738
- if (effective_enabled) {
2739
- await ensure_store();
2740
- command_message(ctx, `Telemetry enabled. Writing to ${db_path}`);
2741
- return;
2742
- }
2743
- finish_active_run_on_disable("telemetry disabled");
2744
- close_store();
2745
- command_message(ctx, "Telemetry disabled.");
2727
+ const next_layers = new Set(active_layers);
2728
+ if (enabled) next_layers.add(preset.name);
2729
+ else next_layers.delete(preset.name);
2730
+ commit_state(ctx, active_base_name, next_layers, {
2731
+ persist: options?.persist,
2732
+ notify: enabled ? `Layer "${preset.name}" enabled` : `Layer "${preset.name}" disabled`
2733
+ });
2734
+ return true;
2735
+ }
2736
+ function toggle_layer(name, ctx, options) {
2737
+ return set_layer_enabled(name, !active_layers.has(name), ctx, options);
2738
+ }
2739
+ async function edit_preset(name, ctx) {
2740
+ const existing = presets[name];
2741
+ const kind_choice = await ctx.ui.select("Preset kind", [existing?.kind === "layer" ? "layer (current)" : "base (current)", existing?.kind === "layer" ? "base" : "layer"]);
2742
+ if (!kind_choice) return;
2743
+ const kind = kind_choice.startsWith("layer") ? "layer" : "base";
2744
+ const description = await ctx.ui.input(`Description for ${name}`, existing?.description ?? "");
2745
+ if (description === void 0) return;
2746
+ const instructions = await ctx.ui.editor(`Edit ${kind} preset: ${name}`, existing?.instructions ?? "");
2747
+ if (instructions === void 0) return;
2748
+ save_project_prompt_presets(ctx.cwd, {
2749
+ ...read_prompt_presets_file(get_project_presets_path(ctx.cwd)),
2750
+ [name]: {
2751
+ kind,
2752
+ instructions,
2753
+ ...description.trim() ? { description: description.trim() } : {}
2746
2754
  }
2747
2755
  });
2748
- pi.on("model_select", async (event) => {
2749
- current_model = get_model_identity(event.model);
2756
+ presets = load_prompt_presets(ctx.cwd);
2757
+ const normalized = normalize_active_state(presets, active_base_name, active_layers);
2758
+ active_base_name = normalized.active_base_name;
2759
+ active_layers = normalized.active_layers;
2760
+ if (kind === "base") activate_base(name, ctx);
2761
+ else set_layer_enabled(name, true, ctx);
2762
+ ctx.ui.notify(`Saved preset "${name}" to ${get_project_presets_path(ctx.cwd)}`, "info");
2763
+ }
2764
+ function remove_custom_preset(name, ctx, mode) {
2765
+ const result = remove_project_prompt_preset(ctx.cwd, name);
2766
+ if (!result.removed) {
2767
+ ctx.ui.notify(`No project-local preset named "${name}" to ${mode}`, "warning");
2768
+ return;
2769
+ }
2770
+ presets = load_prompt_presets(ctx.cwd);
2771
+ const normalized = normalize_active_state(presets, active_base_name, active_layers);
2772
+ active_base_name = normalized.active_base_name;
2773
+ active_layers = normalized.active_layers;
2774
+ set_status(ctx, active_base_name, active_layers);
2775
+ persist_state(pi, ctx, active_base_name, active_layers);
2776
+ const fallback = presets[name];
2777
+ if (mode === "reset" && fallback) {
2778
+ ctx.ui.notify(`Reset "${name}" to ${get_prompt_source_label(fallback.source)} preset`, "info");
2779
+ return;
2780
+ }
2781
+ ctx.ui.notify(result.remaining === 0 ? `Removed "${name}" and deleted ${result.path}` : `Removed "${name}" from ${result.path}`, "info");
2782
+ }
2783
+ async function show_manager(ctx) {
2784
+ const base_presets = list_base_presets(presets);
2785
+ const layer_presets = list_layer_presets(presets);
2786
+ if (base_presets.length === 0 && layer_presets.length === 0) {
2787
+ ctx.ui.notify("No prompt presets available", "warning");
2788
+ return;
2789
+ }
2790
+ const initial_base = active_base_name;
2791
+ const initial_layers = new Set(active_layers);
2792
+ let selected_base = active_base_name;
2793
+ const enabled_layers = new Set(active_layers);
2794
+ const items = [];
2795
+ const base_ids = /* @__PURE__ */ new Set();
2796
+ const layer_ids = /* @__PURE__ */ new Set();
2797
+ items.push({
2798
+ id: "__header_base__",
2799
+ label: `── Base presets (${base_presets.length + 1}) ──`,
2800
+ description: "",
2801
+ currentValue: ""
2750
2802
  });
2751
- pi.on("agent_start", async (_event, ctx) => {
2752
- const active_store = await ensure_store();
2753
- if (!active_store) return;
2754
- const run_id = randomUUID();
2755
- const eval_metadata = get_eval_metadata();
2756
- const model_identity = ctx.model ? get_model_identity(ctx.model) : current_model;
2757
- active_store.insert_run({
2758
- id: run_id,
2759
- session_file: get_session_file(ctx),
2760
- cwd: ctx.cwd,
2761
- started_at: now(),
2762
- model_provider: model_identity.provider,
2763
- model_id: model_identity.id,
2764
- eval_run_id: eval_metadata.run_id,
2765
- eval_case_id: eval_metadata.case_id,
2766
- eval_attempt: eval_metadata.attempt,
2767
- eval_suite: eval_metadata.suite
2768
- });
2769
- active_run = { id: run_id };
2770
- active_turns.clear();
2771
- provider_request_ids.length = 0;
2803
+ items.push({
2804
+ id: NONE_BASE_ID,
2805
+ label: "(none)",
2806
+ description: "No active base preset",
2807
+ currentValue: UNSELECTED,
2808
+ values: [SELECTED, UNSELECTED]
2772
2809
  });
2773
- pi.on("agent_end", async (event) => {
2774
- if (!store || !active_run) return;
2775
- const outcome = infer_run_outcome(event);
2776
- store.finish_run({
2777
- id: active_run.id,
2778
- ended_at: now(),
2779
- success: outcome.success,
2780
- error_message: outcome.error_message
2810
+ base_ids.add(NONE_BASE_ID);
2811
+ for (const preset of base_presets) {
2812
+ items.push({
2813
+ id: preset.name,
2814
+ label: preset.name,
2815
+ description: [`${get_prompt_source_label(preset.source)} • ${preset.description ?? "base preset"}`].join("\n"),
2816
+ currentValue: UNSELECTED,
2817
+ values: [SELECTED, UNSELECTED]
2781
2818
  });
2782
- active_run = null;
2783
- active_turns.clear();
2784
- provider_request_ids.length = 0;
2819
+ base_ids.add(preset.name);
2820
+ }
2821
+ items.push({
2822
+ id: "__header_layers__",
2823
+ label: `── Prompt layers (${layer_presets.length}) ──`,
2824
+ description: "",
2825
+ currentValue: ""
2785
2826
  });
2786
- pi.on("turn_start", async (event) => {
2787
- if (!store || !active_run) return;
2788
- const turn_id = `${active_run.id}:turn:${event.turnIndex}`;
2789
- active_turns.set(event.turnIndex, { id: turn_id });
2790
- store.insert_turn({
2791
- id: turn_id,
2792
- run_id: active_run.id,
2793
- turn_index: event.turnIndex,
2794
- started_at: event.timestamp
2827
+ for (const preset of layer_presets) {
2828
+ items.push({
2829
+ id: preset.name,
2830
+ label: preset.name,
2831
+ description: [`${get_prompt_source_label(preset.source)} • ${preset.description ?? "layer"}`].join("\n"),
2832
+ currentValue: DISABLED$1,
2833
+ values: [ENABLED$1, DISABLED$1]
2795
2834
  });
2796
- });
2797
- pi.on("turn_end", async (event) => {
2798
- const active_turn = active_turns.get(event.turnIndex);
2799
- if (!store || !active_turn) return;
2800
- store.finish_turn({
2801
- id: active_turn.id,
2802
- ended_at: now(),
2803
- tool_result_count: event.toolResults.length,
2804
- stop_reason: get_stop_reason(event.message)
2835
+ layer_ids.add(preset.name);
2836
+ }
2837
+ function sync_values() {
2838
+ for (const item of items) if (base_ids.has(item.id)) item.currentValue = item.id === NONE_BASE_ID && !selected_base || item.id === selected_base ? SELECTED : UNSELECTED;
2839
+ else if (layer_ids.has(item.id)) item.currentValue = enabled_layers.has(item.id) ? ENABLED$1 : DISABLED$1;
2840
+ }
2841
+ sync_values();
2842
+ await ctx.ui.custom((tui, theme, _kb, done) => {
2843
+ const list = new SettingsList(items, Math.min(Math.max(items.length + 4, 8), 24), {
2844
+ cursor: theme.fg("accent", "›"),
2845
+ label: (text, selected) => {
2846
+ if (text.startsWith("──") && text.endsWith("──")) return theme.fg("dim", theme.bold(text));
2847
+ return selected ? theme.fg("accent", text) : text;
2848
+ },
2849
+ value: (text, selected) => {
2850
+ const color = text === ENABLED$1 || text === SELECTED ? "success" : "dim";
2851
+ const rendered = theme.fg(color, text);
2852
+ return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
2853
+ },
2854
+ description: (text) => theme.fg("muted", text),
2855
+ hint: (text) => theme.fg("dim", text)
2856
+ }, (id, new_value) => {
2857
+ if (id.startsWith("__header_")) return;
2858
+ if (base_ids.has(id)) {
2859
+ selected_base = new_value === SELECTED && id !== NONE_BASE_ID ? id : void 0;
2860
+ sync_values();
2861
+ return;
2862
+ }
2863
+ if (layer_ids.has(id)) {
2864
+ if (new_value === ENABLED$1) enabled_layers.add(id);
2865
+ else enabled_layers.delete(id);
2866
+ sync_values();
2867
+ }
2868
+ }, () => done(void 0), { enableSearch: true });
2869
+ const container = new Container();
2870
+ container.addChild({
2871
+ render: () => [
2872
+ theme.fg("accent", theme.bold("Prompt presets")),
2873
+ theme.fg("muted", `base: ${selected_base ?? "(none)"} • ${enabled_layers.size} layer(s) enabled`),
2874
+ ""
2875
+ ],
2876
+ invalidate: () => {}
2805
2877
  });
2806
- active_turns.delete(event.turnIndex);
2807
- });
2808
- pi.on("tool_execution_start", async (event) => {
2809
- if (!store || !active_run) return;
2810
- const current_turn = [...active_turns.values()].at(-1);
2811
- store.insert_tool_call({
2812
- tool_call_id: event.toolCallId,
2813
- run_id: active_run.id,
2814
- turn_id: current_turn?.id ?? null,
2815
- tool_name: event.toolName,
2816
- started_at: now(),
2817
- args_summary_json: summarize_tool_args(event.toolName, event.args)
2878
+ container.addChild({
2879
+ render(width) {
2880
+ return list.render(width);
2881
+ },
2882
+ invalidate() {
2883
+ list.invalidate();
2884
+ }
2818
2885
  });
2886
+ container.addChild(new Text(theme.fg("dim", "search filters • enter toggles • esc close"), 0, 1));
2887
+ return {
2888
+ render(width) {
2889
+ return container.render(width);
2890
+ },
2891
+ invalidate() {
2892
+ container.invalidate();
2893
+ },
2894
+ handleInput(data) {
2895
+ list.handleInput(data);
2896
+ tui.requestRender();
2897
+ }
2898
+ };
2819
2899
  });
2820
- pi.on("tool_execution_update", async (event) => {
2821
- if (!store || !active_run) return;
2822
- store.note_tool_update(event.toolCallId);
2823
- });
2824
- pi.on("tool_execution_end", async (event) => {
2825
- if (!store || !active_run) return;
2826
- store.finish_tool_call({
2827
- tool_call_id: event.toolCallId,
2828
- ended_at: now(),
2829
- is_error: event.isError,
2830
- result_summary_json: summarize_tool_result(event.result),
2831
- error_message: event.isError && event.result != null ? safe_json_stringify(summarize_value(event.result)) : null
2832
- });
2833
- });
2834
- pi.on("before_provider_request", async (event) => {
2835
- if (!store || !active_run) return;
2836
- const request_id = randomUUID();
2837
- const current_turn = [...active_turns.values()].at(-1);
2838
- store.insert_provider_request({
2839
- id: request_id,
2840
- run_id: active_run.id,
2841
- turn_id: current_turn?.id ?? null,
2842
- started_at: now(),
2843
- payload_summary_json: summarize_provider_payload(event.payload)
2844
- });
2845
- provider_request_ids.push(request_id);
2846
- });
2847
- pi.on("after_provider_response", async (event) => {
2848
- if (!store || !active_run) return;
2849
- const request_id = provider_request_ids.shift();
2850
- if (!request_id) return;
2851
- store.finish_provider_request({
2852
- id: request_id,
2853
- ended_at: now(),
2854
- status_code: event.status,
2855
- headers_json: summarize_headers(event.headers)
2856
- });
2857
- });
2858
- pi.on("session_shutdown", async () => {
2859
- if (store && active_run) store.finish_run({
2860
- id: active_run.id,
2861
- ended_at: now(),
2862
- success: null,
2863
- error_message: "session shutdown"
2864
- });
2865
- close_store();
2866
- active_run = null;
2867
- active_turns.clear();
2868
- provider_request_ids.length = 0;
2869
- });
2870
- };
2871
- }
2872
- create_telemetry_extension();
2873
- //#endregion
2874
- //#region src/extensions/prompt-presets.ts
2875
- const PRESET_STATE_TYPE = "prompt-preset-state";
2876
- const ENABLED$1 = "[x]";
2877
- const DISABLED$1 = "[ ]";
2878
- const SELECTED = "(x)";
2879
- const UNSELECTED = "( )";
2880
- const NONE_BASE_ID = "__base_none__";
2881
- const DEFAULT_PROMPT_PRESETS = {
2882
- terse: {
2883
- kind: "base",
2884
- description: "Short, direct, no fluff",
2885
- instructions: "Be concise and direct. Default to the shortest response that fully solves the user's request. No purple prose, no filler, no repetitive caveats. Prefer a short paragraph or a few bullets. Only include extra detail when it materially affects the decision, implementation, or next step."
2886
- },
2887
- standard: {
2888
- kind: "base",
2889
- description: "Clear and concise with key context",
2890
- instructions: "Be clear, direct, and concise. Include only the reasoning and implementation details that matter. Avoid filler, grandstanding, and ornamental language. Use bullets when they improve scanability."
2891
- },
2892
- detailed: {
2893
- kind: "base",
2894
- description: "More explanation when nuance matters",
2895
- instructions: "Be thorough when the task is complex or tradeoffs matter, but stay practical. Explain only the details that help the user decide, verify, or implement. Avoid purple prose and unnecessary scene-setting."
2896
- },
2897
- "no-purple-prose": {
2898
- kind: "layer",
2899
- description: "Strip out ornamental language",
2900
- instructions: "Do not use purple prose, flourish, motivational filler, or theatrical transitions. Prefer plain language and concrete statements."
2901
- },
2902
- bullets: {
2903
- kind: "layer",
2904
- description: "Prefer short bullets when useful",
2905
- instructions: "When presenting options, findings, or steps, prefer short bullet lists over long paragraphs."
2906
- },
2907
- "clarify-first": {
2908
- kind: "layer",
2909
- description: "Ask brief clarifying questions when requirements are ambiguous",
2910
- instructions: "If the request is materially ambiguous, ask the minimum clarifying question(s) needed before proceeding. Do not ask unnecessary questions."
2911
- },
2912
- "include-risks": {
2913
- kind: "layer",
2914
- description: "Call out notable risks or tradeoffs",
2915
- instructions: "When making a recommendation or implementation plan, briefly mention the key risk, tradeoff, or caveat if one materially matters."
2900
+ if (selected_base !== initial_base || !sets_equal$1(initial_layers, enabled_layers)) commit_state(ctx, selected_base, enabled_layers, { notify: "Updated prompt preset selection" });
2916
2901
  }
2917
- };
2918
- function normalize_prompt_presets(input) {
2919
- if (!input || typeof input !== "object") return {};
2920
- const normalized = {};
2921
- for (const [raw_name, raw_value] of Object.entries(input)) {
2922
- const name = raw_name.trim();
2923
- if (!name) continue;
2924
- if (typeof raw_value === "string") {
2925
- normalized[name] = {
2926
- kind: "base",
2927
- instructions: raw_value
2928
- };
2929
- continue;
2902
+ pi.registerFlag("preset", {
2903
+ description: "Activate prompt config on startup. Accepts a base preset or comma-separated preset/layer names.",
2904
+ type: "string"
2905
+ });
2906
+ pi.registerCommand("preset", {
2907
+ description: "Manage base prompt presets and prompt layers",
2908
+ getArgumentCompletions: (prefix) => {
2909
+ const trimmed = prefix.trim();
2910
+ const parts = trimmed ? trimmed.split(/\s+/) : [];
2911
+ const base_names = list_base_presets(presets).map((preset) => preset.name);
2912
+ const layer_names = list_layer_presets(presets).map((preset) => preset.name);
2913
+ const all_names = [...base_names, ...layer_names];
2914
+ if (parts.length <= 1) {
2915
+ const query = parts[0] ?? "";
2916
+ return [...[
2917
+ "list",
2918
+ "show",
2919
+ "clear",
2920
+ "edit",
2921
+ "delete",
2922
+ "reset",
2923
+ "reload",
2924
+ "base",
2925
+ "enable",
2926
+ "disable",
2927
+ "toggle"
2928
+ ].filter((item) => item.startsWith(query)).map((item) => ({
2929
+ value: item,
2930
+ label: item
2931
+ })), ...all_names.filter((item) => item.startsWith(query)).map((item) => ({
2932
+ value: item,
2933
+ label: item
2934
+ }))];
2935
+ }
2936
+ const command = parts[0];
2937
+ const query = parts.slice(1).join(" ");
2938
+ if (command === "base") return base_names.filter((item) => item.startsWith(query)).map((item) => ({
2939
+ value: `base ${item}`,
2940
+ label: item
2941
+ }));
2942
+ if ([
2943
+ "enable",
2944
+ "disable",
2945
+ "toggle"
2946
+ ].includes(command)) return layer_names.filter((item) => item.startsWith(query)).map((item) => ({
2947
+ value: `${command} ${item}`,
2948
+ label: item
2949
+ }));
2950
+ if (command === "edit") return all_names.filter((item) => item.startsWith(query)).map((item) => ({
2951
+ value: `edit ${item}`,
2952
+ label: item
2953
+ }));
2954
+ if (["delete", "reset"].includes(command)) return all_names.filter((item) => item.startsWith(query)).map((item) => ({
2955
+ value: `${command} ${item}`,
2956
+ label: item
2957
+ }));
2958
+ return null;
2959
+ },
2960
+ handler: async (args, ctx) => {
2961
+ const trimmed = args.trim();
2962
+ if (!trimmed) {
2963
+ if (ctx.hasUI) {
2964
+ await show_manager(ctx);
2965
+ return;
2966
+ }
2967
+ ctx.ui.notify(format_summary(active_base_name, active_layers, presets), "info");
2968
+ return;
2969
+ }
2970
+ const [first, ...rest] = trimmed.split(/\s+/);
2971
+ const arg = rest.join(" ").trim();
2972
+ switch (first) {
2973
+ case "list":
2974
+ ctx.ui.notify(format_summary(active_base_name, active_layers, presets), "info");
2975
+ return;
2976
+ case "show":
2977
+ ctx.ui.notify(format_active_details(active_base_name, active_layers, presets), "info");
2978
+ return;
2979
+ case "clear":
2980
+ commit_state(ctx, void 0, /* @__PURE__ */ new Set(), { notify: "Cleared base preset and prompt layers" });
2981
+ return;
2982
+ case "reload": {
2983
+ presets = load_prompt_presets(ctx.cwd);
2984
+ const normalized = normalize_active_state(presets, active_base_name, active_layers);
2985
+ active_base_name = normalized.active_base_name;
2986
+ active_layers = normalized.active_layers;
2987
+ set_status(ctx, active_base_name, active_layers);
2988
+ ctx.ui.notify("Reloaded prompt presets", "info");
2989
+ return;
2990
+ }
2991
+ case "base":
2992
+ if (!arg) {
2993
+ ctx.ui.notify("Usage: /preset base <name>", "warning");
2994
+ return;
2995
+ }
2996
+ activate_base(arg, ctx);
2997
+ return;
2998
+ case "enable":
2999
+ if (!arg) {
3000
+ ctx.ui.notify("Usage: /preset enable <layer>", "warning");
3001
+ return;
3002
+ }
3003
+ set_layer_enabled(arg, true, ctx);
3004
+ return;
3005
+ case "disable":
3006
+ if (!arg) {
3007
+ ctx.ui.notify("Usage: /preset disable <layer>", "warning");
3008
+ return;
3009
+ }
3010
+ set_layer_enabled(arg, false, ctx);
3011
+ return;
3012
+ case "toggle":
3013
+ if (!arg) {
3014
+ ctx.ui.notify("Usage: /preset toggle <layer>", "warning");
3015
+ return;
3016
+ }
3017
+ toggle_layer(arg, ctx);
3018
+ return;
3019
+ case "edit":
3020
+ if (!arg) {
3021
+ ctx.ui.notify("Usage: /preset edit <name>", "warning");
3022
+ return;
3023
+ }
3024
+ await edit_preset(arg, ctx);
3025
+ return;
3026
+ case "delete":
3027
+ if (!arg) {
3028
+ ctx.ui.notify("Usage: /preset delete <name>", "warning");
3029
+ return;
3030
+ }
3031
+ remove_custom_preset(arg, ctx, "delete");
3032
+ return;
3033
+ case "reset":
3034
+ if (!arg) {
3035
+ ctx.ui.notify("Usage: /preset reset <name>", "warning");
3036
+ return;
3037
+ }
3038
+ remove_custom_preset(arg, ctx, "reset");
3039
+ return;
3040
+ }
3041
+ if (is_subcommand(first)) {
3042
+ ctx.ui.notify(`Unsupported preset command: ${first}`, "warning");
3043
+ return;
3044
+ }
3045
+ const preset = presets[trimmed];
3046
+ if (!preset) {
3047
+ ctx.ui.notify(`Unknown preset or layer: ${trimmed}`, "warning");
3048
+ return;
3049
+ }
3050
+ if (preset.kind === "base") activate_base(preset.name, ctx);
3051
+ else toggle_layer(preset.name, ctx);
2930
3052
  }
2931
- if (!raw_value || typeof raw_value !== "object") continue;
2932
- const candidate = raw_value;
2933
- if (typeof candidate.instructions !== "string") continue;
2934
- normalized[name] = {
2935
- instructions: candidate.instructions,
2936
- ...candidate.kind === "layer" ? { kind: "layer" } : {},
2937
- ...typeof candidate.description === "string" ? { description: candidate.description } : {}
2938
- };
2939
- }
2940
- return normalized;
2941
- }
2942
- function to_loaded_prompt_presets(presets, source) {
2943
- return Object.fromEntries(Object.entries(presets).map(([name, preset]) => [name, {
2944
- name,
2945
- kind: preset.kind === "layer" ? "layer" : "base",
2946
- source,
2947
- ...preset
2948
- }]));
2949
- }
2950
- function get_global_presets_path() {
2951
- return join(getAgentDir(), "presets.json");
2952
- }
2953
- function get_project_presets_path(cwd) {
2954
- return join(cwd, ".pi", "presets.json");
2955
- }
2956
- function get_persisted_prompt_state_path() {
2957
- return join(getAgentDir(), "prompt-preset-state.json");
3053
+ });
3054
+ pi.on("session_start", async (_event, ctx) => {
3055
+ presets = load_prompt_presets(ctx.cwd);
3056
+ active_base_name = void 0;
3057
+ active_layers = /* @__PURE__ */ new Set();
3058
+ const preset_flag = pi.getFlag("preset");
3059
+ if (typeof preset_flag === "string" && preset_flag.trim()) {
3060
+ for (const name of parse_preset_flag(preset_flag)) {
3061
+ const preset = presets[name];
3062
+ if (!preset) continue;
3063
+ if (preset.kind === "base") active_base_name = name;
3064
+ else active_layers.add(name);
3065
+ }
3066
+ const normalized = normalize_active_state(presets, active_base_name, active_layers);
3067
+ active_base_name = normalized.active_base_name;
3068
+ active_layers = normalized.active_layers;
3069
+ set_status(ctx, active_base_name, active_layers);
3070
+ return;
3071
+ }
3072
+ const restored = get_last_preset_state(ctx) ?? load_persisted_prompt_state(ctx.cwd);
3073
+ if (restored) {
3074
+ active_base_name = restored.base_name ?? void 0;
3075
+ active_layers = new Set(restored.layer_names ?? []);
3076
+ }
3077
+ const normalized = normalize_active_state(presets, active_base_name, active_layers);
3078
+ active_base_name = normalized.active_base_name;
3079
+ active_layers = normalized.active_layers;
3080
+ set_status(ctx, active_base_name, active_layers);
3081
+ });
3082
+ pi.on("before_agent_start", async (event) => {
3083
+ const blocks = [];
3084
+ const base = get_base(active_base_name);
3085
+ if (base?.instructions.trim()) blocks.push(`## Active Base Prompt: ${base.name}\n${base.instructions.trim()}`);
3086
+ const layer_blocks = [...active_layers].sort().map((name) => presets[name]).filter((preset) => Boolean(preset?.instructions.trim())).map((preset) => `### ${preset.name}\n${preset.instructions.trim()}`);
3087
+ if (layer_blocks.length > 0) blocks.push(`## Active Prompt Layers\n\n${layer_blocks.join("\n\n")}`);
3088
+ if (blocks.length === 0) return;
3089
+ return { systemPrompt: `${event.systemPrompt}\n\n${blocks.join("\n\n")}` };
3090
+ });
3091
+ pi.on("session_shutdown", async (_event, ctx) => {
3092
+ ctx.ui.setStatus("preset", void 0);
3093
+ ctx.ui.setFooter(void 0);
3094
+ });
2958
3095
  }
2959
- function read_prompt_presets_file(path) {
2960
- if (!existsSync(path)) return {};
3096
+ //#endregion
3097
+ //#region src/extensions/recall.ts
3098
+ const DEFAULT_DB_PATH = join(process.env.HOME, ".pi", "pirecall.db");
3099
+ function sync_recall_db_in_background() {
3100
+ if (!existsSync(DEFAULT_DB_PATH)) return;
2961
3101
  try {
2962
- return normalize_prompt_presets(JSON.parse(readFileSync(path, "utf-8")));
2963
- } catch {
2964
- return {};
2965
- }
2966
- }
2967
- function load_prompt_presets(cwd) {
2968
- return Object.assign({}, to_loaded_prompt_presets(DEFAULT_PROMPT_PRESETS, "builtin"), to_loaded_prompt_presets(read_prompt_presets_file(get_global_presets_path()), "user"), to_loaded_prompt_presets(read_prompt_presets_file(get_project_presets_path(cwd)), "project"));
2969
- }
2970
- function sort_prompt_presets(presets) {
2971
- return Object.fromEntries(Object.entries(presets).sort(([a], [b]) => a.localeCompare(b)));
3102
+ spawn("npx", [
3103
+ "pirecall",
3104
+ "sync",
3105
+ "--json"
3106
+ ], { stdio: "ignore" }).unref();
3107
+ } catch {}
2972
3108
  }
2973
- function save_project_prompt_presets(cwd, presets) {
2974
- const path = get_project_presets_path(cwd);
2975
- const dir = dirname(path);
2976
- if (!existsSync(dir)) mkdirSync(dir, {
2977
- recursive: true,
2978
- mode: 448
3109
+ async function recall(pi) {
3110
+ pi.on("session_start", async () => {
3111
+ sync_recall_db_in_background();
3112
+ });
3113
+ pi.on("before_agent_start", async (event) => {
3114
+ return { systemPrompt: event.systemPrompt + `
3115
+
3116
+ ## Session Recall
3117
+
3118
+ You have access to past Pi session history via \`npx pirecall\`. Use it when:
3119
+ - The user references prior work ("what did we do", "last time", "remember when")
3120
+ - You need context from a previous session about this project
3121
+ - You want to avoid repeating work already done
3122
+
3123
+ Quick reference:
3124
+ - \`npx pirecall recall "<query>" --json\` — LLM-optimised context retrieval with surrounding messages
3125
+ - \`npx pirecall search "<query>" --json\` — full-text search (supports FTS5: AND, OR, NOT, "phrase", prefix*)
3126
+ - \`npx pirecall search "<query>" --json --project my-pi\` — filter by project
3127
+ - \`npx pirecall search "<query>" --json --after 2026-04-10\` — filter by date
3128
+ - \`npx pirecall sessions --json\` — list recent sessions
3129
+ - \`npx pirecall stats --json\` — database statistics
3130
+
3131
+ Always pass \`--json\` for structured output.` };
2979
3132
  });
2980
- const tmp = `${path}.tmp-${Date.now()}`;
2981
- writeFileSync(tmp, JSON.stringify(sort_prompt_presets(presets), null, " ") + "\n", { mode: 384 });
2982
- renameSync(tmp, path);
2983
- return path;
2984
- }
2985
- function remove_project_prompt_preset(cwd, name) {
2986
- const path = get_project_presets_path(cwd);
2987
- const project_presets = read_prompt_presets_file(path);
2988
- if (!(name in project_presets)) return {
2989
- removed: false,
2990
- path,
2991
- remaining: Object.keys(project_presets).length
2992
- };
2993
- delete project_presets[name];
2994
- const remaining = Object.keys(project_presets).length;
2995
- if (remaining === 0) {
2996
- if (existsSync(path)) unlinkSync(path);
2997
- return {
2998
- removed: true,
2999
- path,
3000
- remaining
3001
- };
3002
- }
3003
- save_project_prompt_presets(cwd, project_presets);
3004
- return {
3005
- removed: true,
3006
- path,
3007
- remaining
3008
- };
3009
3133
  }
3010
- function normalize_prompt_preset_state(input) {
3011
- if (!input || typeof input !== "object") return void 0;
3012
- const candidate = input;
3013
- return {
3014
- base_name: typeof candidate.base_name === "string" && candidate.base_name.trim() ? candidate.base_name.trim() : null,
3015
- layer_names: Array.isArray(candidate.layer_names) ? [...new Set(candidate.layer_names.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim()))].sort() : []
3016
- };
3134
+ //#endregion
3135
+ //#region src/skills/config.ts
3136
+ const DEFAULT_CONFIG$1 = {
3137
+ version: 1,
3138
+ enabled: {},
3139
+ defaults: "all-disabled"
3140
+ };
3141
+ function get_config_path() {
3142
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "my-pi", "skills.json");
3017
3143
  }
3018
- function read_persisted_prompt_states(path = get_persisted_prompt_state_path()) {
3019
- if (!existsSync(path)) return {
3020
- version: 1,
3021
- projects: {}
3022
- };
3144
+ function load_skills_config() {
3145
+ const path = get_config_path();
3146
+ if (!existsSync(path)) return { ...DEFAULT_CONFIG$1 };
3023
3147
  try {
3024
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
3025
- const raw_projects = parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {};
3026
- const projects = {};
3027
- for (const [cwd, value] of Object.entries(raw_projects)) {
3028
- const normalized = normalize_prompt_preset_state(value);
3029
- if (!normalized) continue;
3030
- projects[cwd] = normalized;
3031
- }
3148
+ const raw = readFileSync(path, "utf-8");
3149
+ const parsed = JSON.parse(raw);
3032
3150
  return {
3033
- version: typeof parsed.version === "number" ? parsed.version : 1,
3034
- projects
3151
+ version: parsed.version ?? 1,
3152
+ enabled: parsed.enabled ?? {},
3153
+ defaults: parsed.defaults ?? "all-enabled"
3035
3154
  };
3036
3155
  } catch {
3037
- return {
3038
- version: 1,
3039
- projects: {}
3040
- };
3156
+ return { ...DEFAULT_CONFIG$1 };
3041
3157
  }
3042
3158
  }
3043
- function load_persisted_prompt_state(cwd, path = get_persisted_prompt_state_path()) {
3044
- return read_persisted_prompt_states(path).projects[cwd];
3045
- }
3046
- function save_persisted_prompt_state(cwd, state, path = get_persisted_prompt_state_path()) {
3047
- const persisted = read_persisted_prompt_states(path);
3048
- persisted.projects[cwd] = normalize_prompt_preset_state(state) ?? {
3049
- base_name: null,
3050
- layer_names: []
3051
- };
3159
+ function save_skills_config(config) {
3160
+ const path = get_config_path();
3052
3161
  const dir = dirname(path);
3053
3162
  if (!existsSync(dir)) mkdirSync(dir, {
3054
3163
  recursive: true,
3055
3164
  mode: 448
3056
3165
  });
3057
3166
  const tmp = `${path}.tmp-${Date.now()}`;
3058
- writeFileSync(tmp, JSON.stringify({
3059
- version: 1,
3060
- projects: Object.fromEntries(Object.entries(persisted.projects).sort(([a], [b]) => a.localeCompare(b)))
3061
- }, null, " ") + "\n", { mode: 384 });
3167
+ writeFileSync(tmp, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
3062
3168
  renameSync(tmp, path);
3063
- return path;
3064
3169
  }
3065
- function get_last_preset_state(ctx) {
3066
- const entries = ctx.sessionManager.getEntries();
3067
- for (let i = entries.length - 1; i >= 0; i--) {
3068
- const entry = entries[i];
3069
- if (entry.type === "custom" && entry.customType === PRESET_STATE_TYPE && entry.data) return entry.data;
3070
- }
3170
+ function make_skill_key(name, source) {
3171
+ return `${name}@${source}`;
3071
3172
  }
3072
- function sets_equal$1(a, b) {
3073
- if (a.size !== b.size) return false;
3074
- for (const value of a) if (!b.has(value)) return false;
3075
- return true;
3173
+ function is_skill_enabled(config, key) {
3174
+ if (key in config.enabled) return config.enabled[key];
3175
+ return config.defaults === "all-enabled";
3076
3176
  }
3077
- function get_prompt_source_label(source) {
3078
- switch (source) {
3079
- case "builtin": return "built-in";
3080
- case "user": return "user";
3081
- case "project": return "project";
3177
+ //#endregion
3178
+ //#region src/skills/scanner.ts
3179
+ const IMPORT_METADATA_FILE = ".my-pi-source.json";
3180
+ function read_installed_plugins() {
3181
+ const path = join(homedir(), ".claude", "plugins", "installed_plugins.json");
3182
+ if (!existsSync(path)) return null;
3183
+ try {
3184
+ return JSON.parse(readFileSync(path, "utf-8"));
3185
+ } catch {
3186
+ return null;
3082
3187
  }
3083
3188
  }
3084
- function list_base_presets(presets) {
3085
- return Object.values(presets).filter((preset) => preset.kind === "base").sort((a, b) => a.name.localeCompare(b.name));
3189
+ function parse_skill_md(skill_path) {
3190
+ try {
3191
+ const { frontmatter } = parseFrontmatter(readFileSync(skill_path, "utf-8"));
3192
+ const description = frontmatter?.description;
3193
+ if (!description) return null;
3194
+ return {
3195
+ name: frontmatter?.name || basename(dirname(skill_path)),
3196
+ description: description.trim()
3197
+ };
3198
+ } catch {
3199
+ return null;
3200
+ }
3086
3201
  }
3087
- function list_layer_presets(presets) {
3088
- return Object.values(presets).filter((preset) => preset.kind === "layer").sort((a, b) => a.name.localeCompare(b.name));
3202
+ function read_import_metadata(base_dir) {
3203
+ const metadata_path = join(base_dir, IMPORT_METADATA_FILE);
3204
+ if (!existsSync(metadata_path)) return void 0;
3205
+ try {
3206
+ return JSON.parse(readFileSync(metadata_path, "utf-8"));
3207
+ } catch {
3208
+ return;
3209
+ }
3089
3210
  }
3090
- function format_summary(active_base_name, active_layers, presets) {
3091
- const lines = [`Base: ${active_base_name ?? "(none)"}`];
3092
- const layer_names = [...active_layers].sort();
3093
- if (layer_names.length === 0) lines.push("Layers: (none)");
3094
- else {
3095
- lines.push("Layers:");
3096
- for (const name of layer_names) {
3097
- const preset = presets[name];
3098
- const description = preset?.description ? ` — ${preset.description}` : "";
3099
- lines.push(`- ${name}${description}`);
3211
+ function scan_dir_for_skills(dir, options) {
3212
+ if (!existsSync(dir)) return [];
3213
+ const results = [];
3214
+ const direct = join(dir, "SKILL.md");
3215
+ if ((options.include_direct_root_skill ?? true) && existsSync(direct)) {
3216
+ const parsed = parse_skill_md(direct);
3217
+ if (parsed) results.push({
3218
+ ...parsed,
3219
+ skillPath: direct,
3220
+ baseDir: dir,
3221
+ source: options.source,
3222
+ kind: options.kind,
3223
+ plugin: options.plugin,
3224
+ import_meta: options.kind === "managed" ? read_import_metadata(dir) : void 0
3225
+ });
3226
+ return results;
3227
+ }
3228
+ try {
3229
+ const matches = globSync("*/SKILL.md", { cwd: dir });
3230
+ for (const match of matches) {
3231
+ const full_path = resolve(dir, match);
3232
+ const parsed = parse_skill_md(full_path);
3233
+ if (parsed) {
3234
+ const base_dir = dirname(full_path);
3235
+ results.push({
3236
+ ...parsed,
3237
+ skillPath: full_path,
3238
+ baseDir: base_dir,
3239
+ source: options.source,
3240
+ kind: options.kind,
3241
+ plugin: options.plugin,
3242
+ import_meta: options.kind === "managed" ? read_import_metadata(base_dir) : void 0
3243
+ });
3244
+ }
3100
3245
  }
3246
+ } catch {}
3247
+ return results;
3248
+ }
3249
+ function dedupe_by_skill_path(skills) {
3250
+ const seen = /* @__PURE__ */ new Set();
3251
+ const deduped = [];
3252
+ for (const skill of skills) {
3253
+ if (seen.has(skill.skillPath)) continue;
3254
+ seen.add(skill.skillPath);
3255
+ deduped.push(skill);
3101
3256
  }
3102
- return lines.join("\n");
3257
+ return deduped;
3103
3258
  }
3104
- function format_active_details(active_base_name, active_layers, presets) {
3105
- const parts = [];
3106
- if (active_base_name) {
3107
- const base = presets[active_base_name];
3108
- if (base) {
3109
- parts.push(`Base: ${base.name}`);
3110
- if (base.description) parts.push(`Description: ${base.description}`);
3111
- parts.push(`Source: ${get_prompt_source_label(base.source)}`);
3112
- parts.push("", base.instructions.trim());
3259
+ function scan_managed_skills() {
3260
+ const skills = [];
3261
+ for (const skill of scan_dir_for_skills(join(homedir(), ".claude", "skills"), {
3262
+ source: "user-local",
3263
+ kind: "managed"
3264
+ })) skills.push(skill);
3265
+ for (const skill of scan_dir_for_skills(join(homedir(), ".pi", "agent", "skills"), {
3266
+ source: "pi-native",
3267
+ kind: "managed",
3268
+ include_direct_root_skill: false
3269
+ })) skills.push(skill);
3270
+ return dedupe_by_skill_path(skills);
3271
+ }
3272
+ function scan_importable_skills() {
3273
+ const skills = [];
3274
+ const plugins = read_installed_plugins();
3275
+ if (!plugins?.plugins) return skills;
3276
+ for (const [plugin_id, entries] of Object.entries(plugins.plugins)) {
3277
+ const entry = entries[0];
3278
+ if (!entry?.installPath || !existsSync(entry.installPath)) continue;
3279
+ const source = `plugin:${plugin_id}`;
3280
+ const plugin = {
3281
+ pluginId: plugin_id,
3282
+ installPath: entry.installPath,
3283
+ version: entry.version,
3284
+ gitCommitSha: entry.gitCommitSha
3285
+ };
3286
+ for (const skill of scan_dir_for_skills(join(entry.installPath, "skills"), {
3287
+ source,
3288
+ kind: "external",
3289
+ plugin
3290
+ })) skills.push(skill);
3291
+ for (const skill of scan_dir_for_skills(join(entry.installPath, ".pi", "skills"), {
3292
+ source,
3293
+ kind: "external",
3294
+ plugin
3295
+ })) skills.push(skill);
3296
+ const direct_root_skill = join(entry.installPath, "SKILL.md");
3297
+ if (existsSync(direct_root_skill)) {
3298
+ const parsed = parse_skill_md(direct_root_skill);
3299
+ if (parsed) skills.push({
3300
+ ...parsed,
3301
+ skillPath: direct_root_skill,
3302
+ baseDir: entry.installPath,
3303
+ source,
3304
+ kind: "external",
3305
+ plugin
3306
+ });
3113
3307
  }
3114
3308
  }
3115
- const layer_names = [...active_layers].sort();
3116
- if (layer_names.length > 0) {
3117
- if (parts.length > 0) parts.push("", "---", "");
3118
- parts.push("Layers:");
3119
- for (const name of layer_names) {
3120
- const layer = presets[name];
3121
- if (!layer) continue;
3122
- parts.push(`- ${layer.name} (${get_prompt_source_label(layer.source)})`);
3123
- if (layer.description) parts.push(` ${layer.description}`);
3309
+ return dedupe_by_skill_path(skills);
3310
+ }
3311
+ //#endregion
3312
+ //#region src/skills/importer.ts
3313
+ const IMPORT_METADATA_VERSION = 1;
3314
+ function get_managed_skills_dir() {
3315
+ return join(homedir(), ".pi", "agent", "skills");
3316
+ }
3317
+ function ensure_dir(path) {
3318
+ mkdirSync(path, {
3319
+ recursive: true,
3320
+ mode: 448
3321
+ });
3322
+ }
3323
+ function list_files_recursively(dir) {
3324
+ const files = [];
3325
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
3326
+ const full_path = join(dir, entry.name);
3327
+ if (entry.name === ".my-pi-source.json") continue;
3328
+ if (entry.isDirectory()) {
3329
+ files.push(...list_files_recursively(full_path));
3330
+ continue;
3124
3331
  }
3332
+ if (entry.isFile()) files.push(full_path);
3333
+ }
3334
+ return files.sort((a, b) => a.localeCompare(b));
3335
+ }
3336
+ function hash_directory(dir) {
3337
+ const hash = createHash("sha256");
3338
+ for (const file of list_files_recursively(dir)) {
3339
+ hash.update(relative(dir, file));
3340
+ hash.update("\0");
3341
+ hash.update(readFileSync(file));
3342
+ hash.update("\0");
3125
3343
  }
3126
- return parts.join("\n") || "No preset or layers active";
3344
+ return hash.digest("hex");
3127
3345
  }
3128
- function get_footer_prompt_status(active_base_name, active_layers) {
3129
- if (!active_base_name && active_layers.size === 0) return;
3130
- return `prompt:${active_base_name ?? "none"}${active_layers.size > 0 ? ` +${active_layers.size}` : ""}`;
3346
+ function read_metadata(base_dir) {
3347
+ const path = join(base_dir, IMPORT_METADATA_FILE);
3348
+ if (!existsSync(path)) return void 0;
3349
+ try {
3350
+ return JSON.parse(readFileSync(path, "utf-8"));
3351
+ } catch {
3352
+ return;
3353
+ }
3131
3354
  }
3132
- function sanitize_status_text(text) {
3133
- return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
3355
+ function write_metadata(base_dir, metadata) {
3356
+ writeFileSync(join(base_dir, IMPORT_METADATA_FILE), JSON.stringify(metadata, null, " ") + "\n", { mode: 384 });
3134
3357
  }
3135
- function format_token_count(count) {
3136
- if (count < 1e3) return count.toString();
3137
- if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
3138
- if (count < 1e6) return `${Math.round(count / 1e3)}k`;
3139
- if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
3140
- return `${Math.round(count / 1e6)}M`;
3358
+ function replace_directory(source_dir, dest_dir) {
3359
+ const parent_dir = dirname(dest_dir);
3360
+ ensure_dir(parent_dir);
3361
+ const tmp_dir = join(parent_dir, `.${resolve(dest_dir).split("/").pop()}.tmp-${Date.now()}`);
3362
+ rmSync(tmp_dir, {
3363
+ recursive: true,
3364
+ force: true
3365
+ });
3366
+ cpSync(source_dir, tmp_dir, {
3367
+ recursive: true,
3368
+ preserveTimestamps: true,
3369
+ verbatimSymlinks: false
3370
+ });
3371
+ rmSync(dest_dir, {
3372
+ recursive: true,
3373
+ force: true
3374
+ });
3375
+ cpSync(tmp_dir, dest_dir, {
3376
+ recursive: true,
3377
+ preserveTimestamps: true,
3378
+ verbatimSymlinks: false
3379
+ });
3380
+ rmSync(tmp_dir, {
3381
+ recursive: true,
3382
+ force: true
3383
+ });
3141
3384
  }
3142
- function get_current_thinking_level(ctx) {
3143
- const entries = ctx.sessionManager.getEntries();
3144
- for (let i = entries.length - 1; i >= 0; i--) {
3145
- const entry = entries[i];
3146
- if (entry.type === "thinking_level_change" && typeof entry.thinkingLevel === "string") return entry.thinkingLevel;
3385
+ function import_external_skill(skill) {
3386
+ if (skill.kind !== "external") throw new Error(`Skill ${skill.name} is not importable`);
3387
+ const managed_root = get_managed_skills_dir();
3388
+ ensure_dir(managed_root);
3389
+ const skill_dir = join(managed_root, skill.name);
3390
+ if (existsSync(skill_dir)) {
3391
+ if (!statSync(skill_dir).isDirectory()) throw new Error(`${skill_dir} exists and is not a directory`);
3392
+ if (!read_metadata(skill_dir)) throw new Error(`Refusing to overwrite existing unmanaged skill at ${skill_dir}`);
3147
3393
  }
3148
- return ctx.model?.reasoning ? "high" : "off";
3394
+ replace_directory(skill.baseDir, skill_dir);
3395
+ const upstream_hash = hash_directory(skill.baseDir);
3396
+ const imported_hash = hash_directory(skill_dir);
3397
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3398
+ const metadata = {
3399
+ version: IMPORT_METADATA_VERSION,
3400
+ source: skill.source,
3401
+ upstream_skill_path: skill.skillPath,
3402
+ upstream_base_dir: skill.baseDir,
3403
+ upstream_install_path: skill.plugin?.installPath,
3404
+ upstream_version: skill.plugin?.version,
3405
+ upstream_git_commit_sha: skill.plugin?.gitCommitSha,
3406
+ imported_at: now,
3407
+ last_synced_at: now,
3408
+ imported_hash,
3409
+ upstream_hash
3410
+ };
3411
+ write_metadata(skill_dir, metadata);
3412
+ return {
3413
+ skillDir: skill_dir,
3414
+ metadata
3415
+ };
3149
3416
  }
3150
- function render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers) {
3151
- let total_input = 0;
3152
- let total_output = 0;
3153
- let total_cache_read = 0;
3154
- let total_cache_write = 0;
3155
- let total_cost = 0;
3156
- for (const entry of ctx.sessionManager.getEntries()) if (entry.type === "message" && entry.message.role === "assistant") {
3157
- total_input += entry.message.usage.input;
3158
- total_output += entry.message.usage.output;
3159
- total_cache_read += entry.message.usage.cacheRead;
3160
- total_cache_write += entry.message.usage.cacheWrite;
3161
- total_cost += entry.message.usage.cost.total;
3162
- }
3163
- const context_usage = ctx.getContextUsage();
3164
- const context_window = context_usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
3165
- const context_percent_value = context_usage?.percent ?? 0;
3166
- const context_percent = context_usage?.percent !== null ? context_percent_value.toFixed(1) : "?";
3167
- let pwd = ctx.cwd;
3168
- const home = process.env.HOME || process.env.USERPROFILE;
3169
- if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
3170
- const branch = footer_data.getGitBranch();
3171
- if (branch) pwd = `${pwd} (${branch})`;
3172
- const session_name = ctx.sessionManager.getSessionName();
3173
- if (session_name) pwd = `${pwd} • ${session_name}`;
3174
- const stats_parts = [];
3175
- if (total_input) stats_parts.push(`↑${format_token_count(total_input)}`);
3176
- if (total_output) stats_parts.push(`↓${format_token_count(total_output)}`);
3177
- if (total_cache_read) stats_parts.push(`R${format_token_count(total_cache_read)}`);
3178
- if (total_cache_write) stats_parts.push(`W${format_token_count(total_cache_write)}`);
3179
- const using_subscription = ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
3180
- if (total_cost || using_subscription) stats_parts.push(`$${total_cost.toFixed(3)}${using_subscription ? " (sub)" : ""}`);
3181
- const context_percent_display = context_percent === "?" ? `?/${format_token_count(context_window)}` : `${context_percent}%/${format_token_count(context_window)}`;
3182
- let context_percent_str = context_percent_display;
3183
- if (context_percent_value > 90) context_percent_str = theme.fg("error", context_percent_display);
3184
- else if (context_percent_value > 70) context_percent_str = theme.fg("warning", context_percent_display);
3185
- stats_parts.push(context_percent_str);
3186
- let stats_left = stats_parts.join(" ");
3187
- let stats_left_width = visibleWidth(stats_left);
3188
- if (stats_left_width > width) {
3189
- stats_left = truncateToWidth(stats_left, width, "...");
3190
- stats_left_width = visibleWidth(stats_left);
3417
+ function sync_imported_skill(skill) {
3418
+ if (skill.kind !== "managed" || !skill.import_meta) throw new Error(`Skill ${skill.name} is not managed by my-pi sync`);
3419
+ const metadata = skill.import_meta;
3420
+ if (!existsSync(metadata.upstream_base_dir)) throw new Error(`Upstream source no longer exists: ${metadata.upstream_base_dir}`);
3421
+ if (hash_directory(skill.baseDir) !== metadata.imported_hash) throw new Error(`Refusing to sync ${skill.name}; local changes detected in ${skill.baseDir}`);
3422
+ const upstream_hash = hash_directory(metadata.upstream_base_dir);
3423
+ if (upstream_hash === metadata.upstream_hash) return {
3424
+ skillDir: skill.baseDir,
3425
+ metadata,
3426
+ changed: false
3427
+ };
3428
+ replace_directory(metadata.upstream_base_dir, skill.baseDir);
3429
+ const imported_hash = hash_directory(skill.baseDir);
3430
+ const updated = {
3431
+ ...metadata,
3432
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
3433
+ imported_hash,
3434
+ upstream_hash
3435
+ };
3436
+ write_metadata(skill.baseDir, updated);
3437
+ return {
3438
+ skillDir: skill.baseDir,
3439
+ metadata: updated,
3440
+ changed: true
3441
+ };
3442
+ }
3443
+ //#endregion
3444
+ //#region src/skills/manager.ts
3445
+ function resolve_skill_key(skill) {
3446
+ return make_skill_key(skill.name, skill.source);
3447
+ }
3448
+ function match_skill_by_key_or_name(skills, key_or_name) {
3449
+ const exact_key = skills.find((skill) => resolve_skill_key(skill) === key_or_name);
3450
+ if (exact_key) return exact_key;
3451
+ const by_name = skills.filter((skill) => skill.name === key_or_name);
3452
+ if (by_name.length === 1) return by_name[0];
3453
+ if (by_name.length > 1) throw new Error(`Multiple skills named ${key_or_name}. Use an exact key instead.`);
3454
+ throw new Error(`Unknown skill: ${key_or_name}`);
3455
+ }
3456
+ function create_skills_manager() {
3457
+ let config = load_skills_config();
3458
+ let managed_cache = null;
3459
+ let importable_cache = null;
3460
+ function get_managed() {
3461
+ if (!managed_cache) managed_cache = scan_managed_skills();
3462
+ return managed_cache;
3191
3463
  }
3192
- const model_name = ctx.model?.id || "no-model";
3193
- const thinking_level = get_current_thinking_level(ctx);
3194
- let right_side_without_provider = model_name;
3195
- if (ctx.model?.reasoning) right_side_without_provider = thinking_level === "off" ? `${model_name} • thinking off` : `${model_name} • ${thinking_level}`;
3196
- let right_side = right_side_without_provider;
3197
- if (footer_data.getAvailableProviderCount() > 1 && ctx.model) {
3198
- right_side = `(${ctx.model.provider}) ${right_side_without_provider}`;
3199
- if (stats_left_width + 2 + visibleWidth(right_side) > width) right_side = right_side_without_provider;
3464
+ function get_importable() {
3465
+ if (!importable_cache) importable_cache = scan_importable_skills();
3466
+ return importable_cache;
3200
3467
  }
3201
- const right_side_width = visibleWidth(right_side);
3202
- const total_needed = stats_left_width + 2 + right_side_width;
3203
- let stats_line;
3204
- if (total_needed <= width) {
3205
- const padding = " ".repeat(width - stats_left_width - right_side_width);
3206
- stats_line = stats_left + padding + right_side;
3207
- } else {
3208
- const available_for_right = width - stats_left_width - 2;
3209
- if (available_for_right > 0) {
3210
- const truncated_right = truncateToWidth(right_side, available_for_right, "");
3211
- const truncated_right_width = visibleWidth(truncated_right);
3212
- const padding = " ".repeat(Math.max(0, width - stats_left_width - truncated_right_width));
3213
- stats_line = stats_left + padding + truncated_right;
3214
- } else stats_line = stats_left;
3468
+ function to_managed(skill) {
3469
+ const key = resolve_skill_key(skill);
3470
+ return {
3471
+ ...skill,
3472
+ key,
3473
+ enabled: skill.kind === "managed" ? is_skill_enabled(config, key) : false
3474
+ };
3215
3475
  }
3216
- const dim_stats_left = theme.fg("dim", stats_left);
3217
- const remainder = stats_line.slice(stats_left.length);
3218
- const dim_remainder = theme.fg("dim", remainder);
3219
- const lines = [truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")), dim_stats_left + dim_remainder];
3220
- const prompt_status = get_footer_prompt_status(active_base_name, active_layers);
3221
- if (prompt_status) {
3222
- const themed_status = theme.fg("dim", prompt_status);
3223
- const status_width = visibleWidth(themed_status);
3224
- const aligned_status = status_width >= width ? truncateToWidth(themed_status, width, theme.fg("dim", "...")) : `${" ".repeat(width - status_width)}${themed_status}`;
3225
- lines.push(aligned_status);
3476
+ function get_enabled_managed_skills() {
3477
+ return get_managed().filter((skill) => is_skill_enabled(config, resolve_skill_key(skill))).map(to_managed);
3226
3478
  }
3227
- const other_statuses = Array.from(footer_data.getExtensionStatuses().entries()).filter(([key]) => key !== "preset").sort(([a], [b]) => a.localeCompare(b)).map(([, text]) => sanitize_status_text(text));
3228
- if (other_statuses.length > 0) lines.push(truncateToWidth(other_statuses.join(" "), width, theme.fg("dim", "...")));
3229
- return lines;
3479
+ return {
3480
+ discover() {
3481
+ return get_managed().map(to_managed);
3482
+ },
3483
+ discover_importable() {
3484
+ return get_importable().map(to_managed);
3485
+ },
3486
+ is_enabled_by_skill(name, filePath) {
3487
+ const discovered = get_managed();
3488
+ const match = discovered.find((s) => s.skillPath === filePath);
3489
+ if (match) return is_skill_enabled(config, resolve_skill_key(match));
3490
+ const by_name = discovered.find((s) => s.name === name);
3491
+ if (by_name) return is_skill_enabled(config, resolve_skill_key(by_name));
3492
+ return true;
3493
+ },
3494
+ get_enabled_skill_paths() {
3495
+ return get_enabled_managed_skills().map((skill) => skill.baseDir);
3496
+ },
3497
+ enable(key) {
3498
+ config.enabled[key] = true;
3499
+ save_skills_config(config);
3500
+ return true;
3501
+ },
3502
+ disable(key) {
3503
+ config.enabled[key] = false;
3504
+ save_skills_config(config);
3505
+ return false;
3506
+ },
3507
+ toggle(key) {
3508
+ const current = is_skill_enabled(config, key);
3509
+ config.enabled[key] = !current;
3510
+ save_skills_config(config);
3511
+ return !current;
3512
+ },
3513
+ search(query) {
3514
+ const q = query.toLowerCase();
3515
+ return this.discover().filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.source.toLowerCase().includes(q));
3516
+ },
3517
+ search_importable(query) {
3518
+ const q = query.toLowerCase();
3519
+ return this.discover_importable().filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.source.toLowerCase().includes(q));
3520
+ },
3521
+ set_defaults(policy) {
3522
+ config.defaults = policy;
3523
+ save_skills_config(config);
3524
+ },
3525
+ import_skill(key_or_name) {
3526
+ const skill = match_skill_by_key_or_name(get_importable(), key_or_name);
3527
+ const result = import_external_skill(skill);
3528
+ const managed_key = make_skill_key(skill.name, "pi-native");
3529
+ config.enabled[managed_key] = true;
3530
+ save_skills_config(config);
3531
+ this.refresh();
3532
+ return {
3533
+ ...result,
3534
+ key: managed_key
3535
+ };
3536
+ },
3537
+ sync_skill(key_or_name) {
3538
+ const skill = match_skill_by_key_or_name(get_managed(), key_or_name);
3539
+ const result = sync_imported_skill(skill);
3540
+ this.refresh();
3541
+ return {
3542
+ ...result,
3543
+ key: resolve_skill_key(skill)
3544
+ };
3545
+ },
3546
+ refresh() {
3547
+ managed_cache = null;
3548
+ importable_cache = null;
3549
+ config = load_skills_config();
3550
+ }
3551
+ };
3230
3552
  }
3231
- function set_status(ctx, active_base_name, active_layers) {
3232
- ctx.ui.setStatus("preset", void 0);
3233
- if (!ctx.hasUI) return;
3234
- ctx.ui.setFooter((tui, theme, footer_data) => {
3235
- return {
3236
- dispose: footer_data.onBranchChange(() => tui.requestRender()),
3237
- invalidate() {},
3238
- render(width) {
3239
- return render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers);
3240
- }
3241
- };
3553
+ //#endregion
3554
+ //#region src/extensions/skills.ts
3555
+ const ENABLED = "[x]";
3556
+ const DISABLED = "[ ]";
3557
+ const SYNC = "[~]";
3558
+ const IMPORTED_LABEL = "[=]";
3559
+ function sort_skills(skills) {
3560
+ return [...skills].sort((a, b) => {
3561
+ const by_name = a.name.localeCompare(b.name);
3562
+ if (by_name !== 0) return by_name;
3563
+ const by_source = a.source.localeCompare(b.source);
3564
+ if (by_source !== 0) return by_source;
3565
+ return a.key.localeCompare(b.key);
3242
3566
  });
3243
3567
  }
3244
- function persist_state(pi, ctx, active_base_name, active_layers) {
3245
- const state = {
3246
- base_name: active_base_name ?? null,
3247
- layer_names: [...active_layers].sort()
3568
+ function find_matching_imported_skill(managed_skills, skill) {
3569
+ const exact_match = managed_skills.find((candidate) => candidate.import_meta?.source === skill.source && (candidate.import_meta.upstream_skill_path === skill.skillPath || candidate.import_meta.upstream_base_dir === skill.baseDir));
3570
+ if (exact_match) return exact_match;
3571
+ return managed_skills.find((candidate) => candidate.import_meta?.source === skill.source && candidate.name === skill.name);
3572
+ }
3573
+ function get_importable_state(managed_skills, skill) {
3574
+ const imported = find_matching_imported_skill(managed_skills, skill);
3575
+ if (imported?.import_meta) {
3576
+ const version_changed = Boolean(skill.plugin?.version && imported.import_meta.upstream_version && skill.plugin.version !== imported.import_meta.upstream_version);
3577
+ const sha_changed = Boolean(skill.plugin?.gitCommitSha && imported.import_meta.upstream_git_commit_sha && skill.plugin.gitCommitSha !== imported.import_meta.upstream_git_commit_sha);
3578
+ if (version_changed || sha_changed) return {
3579
+ label: "sync",
3580
+ detail: "Press Enter to sync the imported copy and reload",
3581
+ action: "sync"
3582
+ };
3583
+ return {
3584
+ label: "imported",
3585
+ detail: `Already imported to ${imported.baseDir}`,
3586
+ action: null
3587
+ };
3588
+ }
3589
+ const managed_conflict = managed_skills.find((candidate) => candidate.name === skill.name);
3590
+ if (managed_conflict) return {
3591
+ label: "managed",
3592
+ detail: `Already managed at ${managed_conflict.baseDir}`,
3593
+ action: null
3594
+ };
3595
+ return {
3596
+ label: "import",
3597
+ detail: "Press Enter to import into pi-native skills and reload",
3598
+ action: "import"
3248
3599
  };
3249
- pi.appendEntry(PRESET_STATE_TYPE, state);
3250
- save_persisted_prompt_state(ctx.cwd, state);
3251
3600
  }
3252
- function normalize_active_state(presets, active_base_name, active_layers) {
3601
+ function to_setting_item(skill) {
3602
+ const detail_lines = [
3603
+ `${skill.source} • ${skill.key}`,
3604
+ skill.description,
3605
+ skill.baseDir
3606
+ ];
3607
+ if (skill.import_meta?.upstream_version) detail_lines.push(`upstream: ${skill.import_meta.upstream_version}${skill.import_meta.upstream_git_commit_sha ? ` • ${skill.import_meta.upstream_git_commit_sha.slice(0, 12)}` : ""}`);
3253
3608
  return {
3254
- active_base_name: active_base_name && presets[active_base_name]?.kind === "base" ? active_base_name : void 0,
3255
- active_layers: new Set([...active_layers].filter((name) => presets[name]?.kind === "layer"))
3609
+ id: skill.key,
3610
+ label: skill.name,
3611
+ description: detail_lines.join("\n"),
3612
+ currentValue: skill.enabled ? ENABLED : DISABLED,
3613
+ values: [ENABLED, DISABLED]
3256
3614
  };
3257
3615
  }
3258
- function parse_preset_flag(flag) {
3259
- return flag.split(",").map((item) => item.trim()).filter(Boolean);
3616
+ function to_importable_setting_item(managed_skills, skill) {
3617
+ const state = get_importable_state(managed_skills, skill);
3618
+ const detail_lines = [
3619
+ `${skill.source} • ${skill.key}`,
3620
+ skill.description,
3621
+ skill.baseDir
3622
+ ];
3623
+ if (skill.plugin?.version) detail_lines.push(`plugin: ${skill.plugin.version}${skill.plugin.gitCommitSha ? ` • ${skill.plugin.gitCommitSha.slice(0, 12)}` : ""}`);
3624
+ if (state.action === "import") return {
3625
+ id: skill.key,
3626
+ label: skill.name,
3627
+ description: detail_lines.join("\n"),
3628
+ currentValue: DISABLED,
3629
+ values: [ENABLED, DISABLED]
3630
+ };
3631
+ if (state.action === "sync") {
3632
+ detail_lines.push("enter to sync");
3633
+ return {
3634
+ id: skill.key,
3635
+ label: skill.name,
3636
+ description: detail_lines.join("\n"),
3637
+ currentValue: SYNC,
3638
+ values: [SYNC]
3639
+ };
3640
+ }
3641
+ detail_lines.push(state.detail);
3642
+ return {
3643
+ id: skill.key,
3644
+ label: skill.name,
3645
+ description: detail_lines.join("\n"),
3646
+ currentValue: IMPORTED_LABEL
3647
+ };
3260
3648
  }
3261
- function is_subcommand(command) {
3262
- return [
3263
- "list",
3264
- "show",
3265
- "clear",
3266
- "edit",
3267
- "delete",
3268
- "reset",
3269
- "reload",
3270
- "base",
3271
- "enable",
3272
- "disable",
3273
- "toggle"
3274
- ].includes(command);
3649
+ function sets_equal(a, b) {
3650
+ if (a.size !== b.size) return false;
3651
+ for (const value of a) if (!b.has(value)) return false;
3652
+ return true;
3275
3653
  }
3276
- async function prompt_presets(pi) {
3277
- let presets = {};
3278
- let active_base_name;
3279
- let active_layers = /* @__PURE__ */ new Set();
3280
- function get_base(name) {
3281
- return name ? presets[name] : void 0;
3282
- }
3283
- function get_layer(name) {
3284
- const preset = presets[name];
3285
- return preset?.kind === "layer" ? preset : void 0;
3286
- }
3287
- function commit_state(ctx, next_base_name, next_layers, options) {
3288
- active_base_name = next_base_name;
3289
- active_layers = new Set(next_layers);
3290
- set_status(ctx, active_base_name, active_layers);
3291
- if (options?.persist !== false) persist_state(pi, ctx, active_base_name, active_layers);
3292
- if (options?.notify) ctx.ui.notify(options.notify, "info");
3293
- }
3294
- function activate_base(name, ctx, options) {
3295
- if (!name) {
3296
- commit_state(ctx, void 0, active_layers, {
3297
- persist: options?.persist,
3298
- notify: "Base preset cleared"
3299
- });
3300
- return true;
3301
- }
3302
- const preset = get_base(name);
3303
- if (!preset) {
3304
- ctx.ui.notify(`Unknown base preset: ${name}`, "warning");
3305
- return false;
3306
- }
3307
- commit_state(ctx, preset.name, active_layers, {
3308
- persist: options?.persist,
3309
- notify: `Base preset "${preset.name}" activated`
3310
- });
3311
- return true;
3312
- }
3313
- function set_layer_enabled(name, enabled, ctx, options) {
3314
- const preset = get_layer(name);
3315
- if (!preset) {
3316
- ctx.ui.notify(`Unknown prompt layer: ${name}`, "warning");
3317
- return false;
3318
- }
3319
- const next_layers = new Set(active_layers);
3320
- if (enabled) next_layers.add(preset.name);
3321
- else next_layers.delete(preset.name);
3322
- commit_state(ctx, active_base_name, next_layers, {
3323
- persist: options?.persist,
3324
- notify: enabled ? `Layer "${preset.name}" enabled` : `Layer "${preset.name}" disabled`
3325
- });
3326
- return true;
3327
- }
3328
- function toggle_layer(name, ctx, options) {
3329
- return set_layer_enabled(name, !active_layers.has(name), ctx, options);
3330
- }
3331
- async function edit_preset(name, ctx) {
3332
- const existing = presets[name];
3333
- const kind_choice = await ctx.ui.select("Preset kind", [existing?.kind === "layer" ? "layer (current)" : "base (current)", existing?.kind === "layer" ? "base" : "layer"]);
3334
- if (!kind_choice) return;
3335
- const kind = kind_choice.startsWith("layer") ? "layer" : "base";
3336
- const description = await ctx.ui.input(`Description for ${name}`, existing?.description ?? "");
3337
- if (description === void 0) return;
3338
- const instructions = await ctx.ui.editor(`Edit ${kind} preset: ${name}`, existing?.instructions ?? "");
3339
- if (instructions === void 0) return;
3340
- save_project_prompt_presets(ctx.cwd, {
3341
- ...read_prompt_presets_file(get_project_presets_path(ctx.cwd)),
3342
- [name]: {
3343
- kind,
3344
- instructions,
3345
- ...description.trim() ? { description: description.trim() } : {}
3654
+ async function skills(pi) {
3655
+ const mgr = create_skills_manager();
3656
+ const subs = [
3657
+ "import",
3658
+ "sync",
3659
+ "refresh",
3660
+ "defaults"
3661
+ ];
3662
+ pi.registerCommand("skills", {
3663
+ description: "Manage pi-native skills and import external skills",
3664
+ getArgumentCompletions: (prefix) => {
3665
+ const parts = prefix.trim().split(/\s+/);
3666
+ if (parts.length <= 1) return subs.filter((s) => s.startsWith(parts[0] || "")).map((s) => ({
3667
+ value: s,
3668
+ label: s
3669
+ }));
3670
+ if (parts[0] === "import") {
3671
+ const q = parts.slice(1).join(" ").toLowerCase();
3672
+ return sort_skills(mgr.discover_importable()).filter((s) => s.key.toLowerCase().includes(q) || s.name.toLowerCase().includes(q)).slice(0, 20).map((s) => ({
3673
+ value: `${parts[0]} ${s.key}`,
3674
+ label: s.key
3675
+ }));
3346
3676
  }
3347
- });
3348
- presets = load_prompt_presets(ctx.cwd);
3349
- const normalized = normalize_active_state(presets, active_base_name, active_layers);
3350
- active_base_name = normalized.active_base_name;
3351
- active_layers = normalized.active_layers;
3352
- if (kind === "base") activate_base(name, ctx);
3353
- else set_layer_enabled(name, true, ctx);
3354
- ctx.ui.notify(`Saved preset "${name}" to ${get_project_presets_path(ctx.cwd)}`, "info");
3355
- }
3356
- function remove_custom_preset(name, ctx, mode) {
3357
- const result = remove_project_prompt_preset(ctx.cwd, name);
3358
- if (!result.removed) {
3359
- ctx.ui.notify(`No project-local preset named "${name}" to ${mode}`, "warning");
3360
- return;
3361
- }
3362
- presets = load_prompt_presets(ctx.cwd);
3363
- const normalized = normalize_active_state(presets, active_base_name, active_layers);
3364
- active_base_name = normalized.active_base_name;
3365
- active_layers = normalized.active_layers;
3366
- set_status(ctx, active_base_name, active_layers);
3367
- persist_state(pi, ctx, active_base_name, active_layers);
3368
- const fallback = presets[name];
3369
- if (mode === "reset" && fallback) {
3370
- ctx.ui.notify(`Reset "${name}" to ${get_prompt_source_label(fallback.source)} preset`, "info");
3371
- return;
3372
- }
3373
- ctx.ui.notify(result.remaining === 0 ? `Removed "${name}" and deleted ${result.path}` : `Removed "${name}" from ${result.path}`, "info");
3374
- }
3375
- async function show_manager(ctx) {
3376
- const base_presets = list_base_presets(presets);
3377
- const layer_presets = list_layer_presets(presets);
3378
- if (base_presets.length === 0 && layer_presets.length === 0) {
3379
- ctx.ui.notify("No prompt presets available", "warning");
3380
- return;
3381
- }
3382
- const initial_base = active_base_name;
3383
- const initial_layers = new Set(active_layers);
3384
- let selected_base = active_base_name;
3385
- const enabled_layers = new Set(active_layers);
3386
- const items = [];
3387
- const base_ids = /* @__PURE__ */ new Set();
3388
- const layer_ids = /* @__PURE__ */ new Set();
3389
- items.push({
3390
- id: "__header_base__",
3391
- label: `── Base presets (${base_presets.length + 1}) ──`,
3392
- description: "",
3393
- currentValue: ""
3394
- });
3395
- items.push({
3396
- id: NONE_BASE_ID,
3397
- label: "(none)",
3398
- description: "No active base preset",
3399
- currentValue: UNSELECTED,
3400
- values: [SELECTED, UNSELECTED]
3401
- });
3402
- base_ids.add(NONE_BASE_ID);
3403
- for (const preset of base_presets) {
3404
- items.push({
3405
- id: preset.name,
3406
- label: preset.name,
3407
- description: [`${get_prompt_source_label(preset.source)} • ${preset.description ?? "base preset"}`].join("\n"),
3408
- currentValue: UNSELECTED,
3409
- values: [SELECTED, UNSELECTED]
3410
- });
3411
- base_ids.add(preset.name);
3412
- }
3413
- items.push({
3414
- id: "__header_layers__",
3415
- label: `── Prompt layers (${layer_presets.length}) ──`,
3416
- description: "",
3417
- currentValue: ""
3418
- });
3419
- for (const preset of layer_presets) {
3420
- items.push({
3421
- id: preset.name,
3422
- label: preset.name,
3423
- description: [`${get_prompt_source_label(preset.source)} • ${preset.description ?? "layer"}`].join("\n"),
3424
- currentValue: DISABLED$1,
3425
- values: [ENABLED$1, DISABLED$1]
3426
- });
3427
- layer_ids.add(preset.name);
3428
- }
3429
- function sync_values() {
3430
- for (const item of items) if (base_ids.has(item.id)) item.currentValue = item.id === NONE_BASE_ID && !selected_base || item.id === selected_base ? SELECTED : UNSELECTED;
3431
- else if (layer_ids.has(item.id)) item.currentValue = enabled_layers.has(item.id) ? ENABLED$1 : DISABLED$1;
3432
- }
3433
- sync_values();
3434
- await ctx.ui.custom((tui, theme, _kb, done) => {
3435
- const list = new SettingsList(items, Math.min(Math.max(items.length + 4, 8), 24), {
3436
- cursor: theme.fg("accent", "›"),
3437
- label: (text, selected) => {
3438
- if (text.startsWith("──") && text.endsWith("──")) return theme.fg("dim", theme.bold(text));
3439
- return selected ? theme.fg("accent", text) : text;
3440
- },
3441
- value: (text, selected) => {
3442
- const color = text === ENABLED$1 || text === SELECTED ? "success" : "dim";
3443
- const rendered = theme.fg(color, text);
3444
- return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
3445
- },
3446
- description: (text) => theme.fg("muted", text),
3447
- hint: (text) => theme.fg("dim", text)
3448
- }, (id, new_value) => {
3449
- if (id.startsWith("__header_")) return;
3450
- if (base_ids.has(id)) {
3451
- selected_base = new_value === SELECTED && id !== NONE_BASE_ID ? id : void 0;
3452
- sync_values();
3677
+ if (parts[0] === "sync") {
3678
+ const q = parts.slice(1).join(" ").toLowerCase();
3679
+ return sort_skills(mgr.discover().filter((skill) => Boolean(skill.import_meta))).filter((s) => s.key.toLowerCase().includes(q) || s.name.toLowerCase().includes(q)).slice(0, 20).map((s) => ({
3680
+ value: `${parts[0]} ${s.key}`,
3681
+ label: s.key
3682
+ }));
3683
+ }
3684
+ return null;
3685
+ },
3686
+ handler: async (args, ctx) => {
3687
+ const trimmed = args.trim();
3688
+ if (!trimmed && ctx.hasUI) {
3689
+ const discovered = sort_skills(mgr.discover());
3690
+ const importable = sort_skills(mgr.discover_importable());
3691
+ if (discovered.length === 0 && importable.length === 0) {
3692
+ ctx.ui.notify("No managed or importable skills found");
3453
3693
  return;
3454
3694
  }
3455
- if (layer_ids.has(id)) {
3456
- if (new_value === ENABLED$1) enabled_layers.add(id);
3457
- else enabled_layers.delete(id);
3458
- sync_values();
3695
+ const initial_enabled = new Set(discovered.filter((skill) => skill.enabled).map((skill) => skill.key));
3696
+ const current_enabled = new Set(initial_enabled);
3697
+ const queued_imports = /* @__PURE__ */ new Set();
3698
+ let reload_notice = null;
3699
+ const managed_items = discovered.map(to_setting_item);
3700
+ const importable_items = importable.map((skill) => to_importable_setting_item(discovered, skill));
3701
+ const all_items = [];
3702
+ if (managed_items.length > 0) {
3703
+ all_items.push({
3704
+ id: "__header_managed__",
3705
+ label: `── Managed (${managed_items.length}) ──`,
3706
+ description: "",
3707
+ currentValue: ""
3708
+ });
3709
+ all_items.push(...managed_items);
3459
3710
  }
3460
- }, () => done(void 0), { enableSearch: true });
3461
- const container = new Container();
3462
- container.addChild({
3463
- render: () => [
3464
- theme.fg("accent", theme.bold("Prompt presets")),
3465
- theme.fg("muted", `base: ${selected_base ?? "(none)"} • ${enabled_layers.size} layer(s) enabled`),
3466
- ""
3467
- ],
3468
- invalidate: () => {}
3469
- });
3470
- container.addChild({
3471
- render(width) {
3472
- return list.render(width);
3473
- },
3474
- invalidate() {
3475
- list.invalidate();
3711
+ if (importable_items.length > 0) {
3712
+ all_items.push({
3713
+ id: "__header_importable__",
3714
+ label: `── Importable (${importable_items.length}) ──`,
3715
+ description: "",
3716
+ currentValue: ""
3717
+ });
3718
+ all_items.push(...importable_items);
3476
3719
  }
3477
- });
3478
- container.addChild(new Text(theme.fg("dim", "search filters • enter toggles • esc close"), 0, 1));
3479
- return {
3480
- render(width) {
3481
- return container.render(width);
3482
- },
3483
- invalidate() {
3484
- container.invalidate();
3485
- },
3486
- handleInput(data) {
3487
- list.handleInput(data);
3488
- tui.requestRender();
3720
+ const managed_keys = new Set(discovered.map((s) => s.key));
3721
+ const importable_map = new Map(importable.map((s) => [s.key, s]));
3722
+ await ctx.ui.custom((tui, theme, _kb, done) => {
3723
+ const list = new SettingsList(all_items, Math.min(Math.max(all_items.length + 4, 8), 22), {
3724
+ cursor: theme.fg("accent", "›"),
3725
+ label: (text, selected) => {
3726
+ if (text.startsWith("──") && text.endsWith("──")) return theme.fg("dim", theme.bold(text));
3727
+ return selected ? theme.fg("accent", text) : text;
3728
+ },
3729
+ value: (text, selected) => {
3730
+ const color = text === ENABLED ? "success" : text === SYNC ? "warning" : text === IMPORTED_LABEL ? "success" : "dim";
3731
+ const rendered = theme.fg(color, text);
3732
+ return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
3733
+ },
3734
+ description: (text) => theme.fg("muted", text),
3735
+ hint: (text) => theme.fg("dim", text)
3736
+ }, (id, new_value) => {
3737
+ if (id.startsWith("__header_")) return;
3738
+ if (managed_keys.has(id)) {
3739
+ if (new_value === ENABLED) {
3740
+ current_enabled.add(id);
3741
+ mgr.enable(id);
3742
+ } else {
3743
+ current_enabled.delete(id);
3744
+ mgr.disable(id);
3745
+ }
3746
+ return;
3747
+ }
3748
+ const import_skill = importable_map.get(id);
3749
+ if (!import_skill) return;
3750
+ const state = get_importable_state(discovered, import_skill);
3751
+ if (state.action === "import") {
3752
+ if (new_value === ENABLED) queued_imports.add(id);
3753
+ else queued_imports.delete(id);
3754
+ return;
3755
+ }
3756
+ if (state.action === "sync") {
3757
+ const imported_skill = find_matching_imported_skill(discovered, import_skill);
3758
+ if (!imported_skill) {
3759
+ ctx.ui.notify(`Imported copy for ${import_skill.name} was not found`, "warning");
3760
+ return;
3761
+ }
3762
+ try {
3763
+ if (mgr.sync_skill(imported_skill.key).changed) {
3764
+ reload_notice = `Synced ${import_skill.name}. Reloading...`;
3765
+ done(void 0);
3766
+ } else ctx.ui.notify(`${import_skill.name} is already up to date.`, "info");
3767
+ } catch (error) {
3768
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
3769
+ }
3770
+ }
3771
+ }, () => done(void 0), { enableSearch: true });
3772
+ const container = new Container();
3773
+ container.addChild({
3774
+ render: () => {
3775
+ const enabled = current_enabled.size;
3776
+ const disabled = discovered.length - enabled;
3777
+ const queued = queued_imports.size;
3778
+ const parts = [`${enabled} enabled`, `${disabled} disabled`];
3779
+ if (importable.length > 0) parts.push(`${importable.length} importable`);
3780
+ if (queued > 0) parts.push(`${queued} queued for import`);
3781
+ return [
3782
+ theme.fg("accent", theme.bold("Skills")),
3783
+ theme.fg("muted", parts.join(" • ")),
3784
+ ""
3785
+ ];
3786
+ },
3787
+ invalidate: () => {}
3788
+ });
3789
+ container.addChild({
3790
+ render(width) {
3791
+ return list.render(width);
3792
+ },
3793
+ invalidate() {
3794
+ list.invalidate();
3795
+ }
3796
+ });
3797
+ container.addChild(new Text(theme.fg("dim", "search filters • enter toggles • esc close"), 0, 1));
3798
+ return {
3799
+ render(width) {
3800
+ return container.render(width);
3801
+ },
3802
+ invalidate() {
3803
+ container.invalidate();
3804
+ },
3805
+ handleInput(data) {
3806
+ list.handleInput(data);
3807
+ tui.requestRender();
3808
+ }
3809
+ };
3810
+ });
3811
+ if (queued_imports.size > 0) {
3812
+ const imported_names = [];
3813
+ for (const key of queued_imports) try {
3814
+ mgr.import_skill(key);
3815
+ imported_names.push(key);
3816
+ } catch (error) {
3817
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
3818
+ }
3819
+ if (imported_names.length > 0) reload_notice = `Imported ${imported_names.length} skill(s). Reloading...`;
3489
3820
  }
3490
- };
3491
- });
3492
- if (selected_base !== initial_base || !sets_equal$1(initial_layers, enabled_layers)) commit_state(ctx, selected_base, enabled_layers, { notify: "Updated prompt preset selection" });
3493
- }
3494
- pi.registerFlag("preset", {
3495
- description: "Activate prompt config on startup. Accepts a base preset or comma-separated preset/layer names.",
3496
- type: "string"
3497
- });
3498
- pi.registerCommand("preset", {
3499
- description: "Manage base prompt presets and prompt layers",
3500
- getArgumentCompletions: (prefix) => {
3501
- const trimmed = prefix.trim();
3502
- const parts = trimmed ? trimmed.split(/\s+/) : [];
3503
- const base_names = list_base_presets(presets).map((preset) => preset.name);
3504
- const layer_names = list_layer_presets(presets).map((preset) => preset.name);
3505
- const all_names = [...base_names, ...layer_names];
3506
- if (parts.length <= 1) {
3507
- const query = parts[0] ?? "";
3508
- return [...[
3509
- "list",
3510
- "show",
3511
- "clear",
3512
- "edit",
3513
- "delete",
3514
- "reset",
3515
- "reload",
3516
- "base",
3517
- "enable",
3518
- "disable",
3519
- "toggle"
3520
- ].filter((item) => item.startsWith(query)).map((item) => ({
3521
- value: item,
3522
- label: item
3523
- })), ...all_names.filter((item) => item.startsWith(query)).map((item) => ({
3524
- value: item,
3525
- label: item
3526
- }))];
3527
- }
3528
- const command = parts[0];
3529
- const query = parts.slice(1).join(" ");
3530
- if (command === "base") return base_names.filter((item) => item.startsWith(query)).map((item) => ({
3531
- value: `base ${item}`,
3532
- label: item
3533
- }));
3534
- if ([
3535
- "enable",
3536
- "disable",
3537
- "toggle"
3538
- ].includes(command)) return layer_names.filter((item) => item.startsWith(query)).map((item) => ({
3539
- value: `${command} ${item}`,
3540
- label: item
3541
- }));
3542
- if (command === "edit") return all_names.filter((item) => item.startsWith(query)).map((item) => ({
3543
- value: `edit ${item}`,
3544
- label: item
3545
- }));
3546
- if (["delete", "reset"].includes(command)) return all_names.filter((item) => item.startsWith(query)).map((item) => ({
3547
- value: `${command} ${item}`,
3548
- label: item
3549
- }));
3550
- return null;
3551
- },
3552
- handler: async (args, ctx) => {
3553
- const trimmed = args.trim();
3554
- if (!trimmed) {
3555
- if (ctx.hasUI) {
3556
- await show_manager(ctx);
3821
+ if (reload_notice) {
3822
+ ctx.ui.notify(reload_notice, "info");
3823
+ await ctx.reload();
3557
3824
  return;
3558
3825
  }
3559
- ctx.ui.notify(format_summary(active_base_name, active_layers, presets), "info");
3560
- return;
3561
- }
3562
- const [first, ...rest] = trimmed.split(/\s+/);
3563
- const arg = rest.join(" ").trim();
3564
- switch (first) {
3565
- case "list":
3566
- ctx.ui.notify(format_summary(active_base_name, active_layers, presets), "info");
3567
- return;
3568
- case "show":
3569
- ctx.ui.notify(format_active_details(active_base_name, active_layers, presets), "info");
3570
- return;
3571
- case "clear":
3572
- commit_state(ctx, void 0, /* @__PURE__ */ new Set(), { notify: "Cleared base preset and prompt layers" });
3573
- return;
3574
- case "reload": {
3575
- presets = load_prompt_presets(ctx.cwd);
3576
- const normalized = normalize_active_state(presets, active_base_name, active_layers);
3577
- active_base_name = normalized.active_base_name;
3578
- active_layers = normalized.active_layers;
3579
- set_status(ctx, active_base_name, active_layers);
3580
- ctx.ui.notify("Reloaded prompt presets", "info");
3826
+ if (!sets_equal(initial_enabled, current_enabled)) {
3827
+ ctx.ui.notify("Reloading to apply updated skills...", "info");
3828
+ await ctx.reload();
3581
3829
  return;
3582
3830
  }
3583
- case "base":
3584
- if (!arg) {
3585
- ctx.ui.notify("Usage: /preset base <name>", "warning");
3586
- return;
3587
- }
3588
- activate_base(arg, ctx);
3589
- return;
3590
- case "enable":
3591
- if (!arg) {
3592
- ctx.ui.notify("Usage: /preset enable <layer>", "warning");
3593
- return;
3594
- }
3595
- set_layer_enabled(arg, true, ctx);
3596
- return;
3597
- case "disable":
3598
- if (!arg) {
3599
- ctx.ui.notify("Usage: /preset disable <layer>", "warning");
3600
- return;
3601
- }
3602
- set_layer_enabled(arg, false, ctx);
3603
- return;
3604
- case "toggle":
3605
- if (!arg) {
3606
- ctx.ui.notify("Usage: /preset toggle <layer>", "warning");
3607
- return;
3608
- }
3609
- toggle_layer(arg, ctx);
3610
- return;
3611
- case "edit":
3612
- if (!arg) {
3613
- ctx.ui.notify("Usage: /preset edit <name>", "warning");
3614
- return;
3615
- }
3616
- await edit_preset(arg, ctx);
3617
- return;
3618
- case "delete":
3619
- if (!arg) {
3620
- ctx.ui.notify("Usage: /preset delete <name>", "warning");
3621
- return;
3622
- }
3623
- remove_custom_preset(arg, ctx, "delete");
3624
- return;
3625
- case "reset":
3626
- if (!arg) {
3627
- ctx.ui.notify("Usage: /preset reset <name>", "warning");
3628
- return;
3629
- }
3630
- remove_custom_preset(arg, ctx, "reset");
3631
- return;
3632
- }
3633
- if (is_subcommand(first)) {
3634
- ctx.ui.notify(`Unsupported preset command: ${first}`, "warning");
3635
- return;
3636
- }
3637
- const preset = presets[trimmed];
3638
- if (!preset) {
3639
- ctx.ui.notify(`Unknown preset or layer: ${trimmed}`, "warning");
3640
3831
  return;
3641
3832
  }
3642
- if (preset.kind === "base") activate_base(preset.name, ctx);
3643
- else toggle_layer(preset.name, ctx);
3644
- }
3645
- });
3646
- pi.on("session_start", async (_event, ctx) => {
3647
- presets = load_prompt_presets(ctx.cwd);
3648
- active_base_name = void 0;
3649
- active_layers = /* @__PURE__ */ new Set();
3650
- const preset_flag = pi.getFlag("preset");
3651
- if (typeof preset_flag === "string" && preset_flag.trim()) {
3652
- for (const name of parse_preset_flag(preset_flag)) {
3653
- const preset = presets[name];
3654
- if (!preset) continue;
3655
- if (preset.kind === "base") active_base_name = name;
3656
- else active_layers.add(name);
3657
- }
3658
- const normalized = normalize_active_state(presets, active_base_name, active_layers);
3659
- active_base_name = normalized.active_base_name;
3660
- active_layers = normalized.active_layers;
3661
- set_status(ctx, active_base_name, active_layers);
3662
- return;
3663
- }
3664
- const restored = get_last_preset_state(ctx) ?? load_persisted_prompt_state(ctx.cwd);
3665
- if (restored) {
3666
- active_base_name = restored.base_name ?? void 0;
3667
- active_layers = new Set(restored.layer_names ?? []);
3668
- }
3669
- const normalized = normalize_active_state(presets, active_base_name, active_layers);
3670
- active_base_name = normalized.active_base_name;
3671
- active_layers = normalized.active_layers;
3672
- set_status(ctx, active_base_name, active_layers);
3673
- });
3674
- pi.on("before_agent_start", async (event) => {
3675
- const blocks = [];
3676
- const base = get_base(active_base_name);
3677
- if (base?.instructions.trim()) blocks.push(`## Active Base Prompt: ${base.name}\n${base.instructions.trim()}`);
3678
- const layer_blocks = [...active_layers].sort().map((name) => presets[name]).filter((preset) => Boolean(preset?.instructions.trim())).map((preset) => `### ${preset.name}\n${preset.instructions.trim()}`);
3679
- if (layer_blocks.length > 0) blocks.push(`## Active Prompt Layers\n\n${layer_blocks.join("\n\n")}`);
3680
- if (blocks.length === 0) return;
3681
- return { systemPrompt: `${event.systemPrompt}\n\n${blocks.join("\n\n")}` };
3682
- });
3683
- pi.on("session_shutdown", async (_event, ctx) => {
3684
- ctx.ui.setStatus("preset", void 0);
3685
- ctx.ui.setFooter(void 0);
3686
- });
3687
- }
3688
- //#endregion
3689
- //#region src/extensions/recall.ts
3690
- const DEFAULT_DB_PATH = join(process.env.HOME, ".pi", "pirecall.db");
3691
- function sync_recall_db_in_background() {
3692
- if (!existsSync(DEFAULT_DB_PATH)) return;
3693
- try {
3694
- spawn("npx", [
3695
- "pirecall",
3696
- "sync",
3697
- "--json"
3698
- ], { stdio: "ignore" }).unref();
3699
- } catch {}
3700
- }
3701
- async function recall(pi) {
3702
- pi.on("session_start", async () => {
3703
- sync_recall_db_in_background();
3704
- });
3705
- pi.on("before_agent_start", async (event) => {
3706
- return { systemPrompt: event.systemPrompt + `
3707
-
3708
- ## Session Recall
3709
-
3710
- You have access to past Pi session history via \`npx pirecall\`. Use it when:
3711
- - The user references prior work ("what did we do", "last time", "remember when")
3712
- - You need context from a previous session about this project
3713
- - You want to avoid repeating work already done
3714
-
3715
- Quick reference:
3716
- - \`npx pirecall recall "<query>" --json\` — LLM-optimised context retrieval with surrounding messages
3717
- - \`npx pirecall search "<query>" --json\` — full-text search (supports FTS5: AND, OR, NOT, "phrase", prefix*)
3718
- - \`npx pirecall search "<query>" --json --project my-pi\` — filter by project
3719
- - \`npx pirecall search "<query>" --json --after 2026-04-10\` — filter by date
3720
- - \`npx pirecall sessions --json\` — list recent sessions
3721
- - \`npx pirecall stats --json\` — database statistics
3722
-
3723
- Always pass \`--json\` for structured output.` };
3833
+ const [sub, ...rest] = (trimmed || "list").split(/\s+/);
3834
+ const arg = rest.join(" ");
3835
+ switch (sub) {
3836
+ case "import":
3837
+ if (!arg) {
3838
+ ctx.ui.notify("Usage: /skills import <key|name>", "warning");
3839
+ return;
3840
+ }
3841
+ try {
3842
+ const result = mgr.import_skill(arg);
3843
+ ctx.ui.notify(`Imported ${arg} to ${result.skillDir}. Reloading...`, "info");
3844
+ await ctx.reload();
3845
+ return;
3846
+ } catch (error) {
3847
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
3848
+ return;
3849
+ }
3850
+ case "sync":
3851
+ if (!arg) {
3852
+ ctx.ui.notify("Usage: /skills sync <key|name>", "warning");
3853
+ return;
3854
+ }
3855
+ try {
3856
+ const result = mgr.sync_skill(arg);
3857
+ ctx.ui.notify(result.changed ? `Synced ${arg}. Reloading...` : `${arg} is already up to date.`, "info");
3858
+ if (result.changed) await ctx.reload();
3859
+ return;
3860
+ } catch (error) {
3861
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
3862
+ return;
3863
+ }
3864
+ case "refresh":
3865
+ mgr.refresh();
3866
+ ctx.ui.notify(`Rescanned: ${mgr.discover().length} managed skills, ${mgr.discover_importable().length} importable skills found`);
3867
+ break;
3868
+ case "defaults":
3869
+ if (arg !== "all-enabled" && arg !== "all-disabled") {
3870
+ ctx.ui.notify("Usage: /skills defaults <all-enabled|all-disabled>", "warning");
3871
+ return;
3872
+ }
3873
+ mgr.set_defaults(arg);
3874
+ ctx.ui.notify(`Default policy: ${arg}`);
3875
+ break;
3876
+ default: ctx.ui.notify(`Unknown: ${sub}. Use: ${subs.join(", ")}`, "warning");
3877
+ }
3878
+ }
3724
3879
  });
3725
3880
  }
3726
3881
  //#endregion
3727
- //#region src/skills/config.ts
3882
+ //#region src/extensions/telemetry-config.ts
3728
3883
  const DEFAULT_CONFIG = {
3729
3884
  version: 1,
3730
- enabled: {},
3731
- defaults: "all-disabled"
3885
+ enabled: false
3732
3886
  };
3733
- function get_config_path() {
3734
- return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "my-pi", "skills.json");
3735
- }
3736
- function load_skills_config() {
3737
- const path = get_config_path();
3738
- if (!existsSync(path)) return { ...DEFAULT_CONFIG };
3739
- try {
3740
- const raw = readFileSync(path, "utf-8");
3741
- const parsed = JSON.parse(raw);
3742
- return {
3743
- version: parsed.version ?? 1,
3744
- enabled: parsed.enabled ?? {},
3745
- defaults: parsed.defaults ?? "all-enabled"
3746
- };
3747
- } catch {
3748
- return { ...DEFAULT_CONFIG };
3749
- }
3750
- }
3751
- function save_skills_config(config) {
3752
- const path = get_config_path();
3753
- const dir = dirname(path);
3754
- if (!existsSync(dir)) mkdirSync(dir, {
3755
- recursive: true,
3756
- mode: 448
3757
- });
3758
- const tmp = `${path}.tmp-${Date.now()}`;
3759
- writeFileSync(tmp, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
3760
- renameSync(tmp, path);
3761
- }
3762
- function make_skill_key(name, source) {
3763
- return `${name}@${source}`;
3764
- }
3765
- function is_skill_enabled(config, key) {
3766
- if (key in config.enabled) return config.enabled[key];
3767
- return config.defaults === "all-enabled";
3768
- }
3769
- //#endregion
3770
- //#region src/skills/scanner.ts
3771
- const IMPORT_METADATA_FILE = ".my-pi-source.json";
3772
- function read_installed_plugins() {
3773
- const path = join(homedir(), ".claude", "plugins", "installed_plugins.json");
3774
- if (!existsSync(path)) return null;
3775
- try {
3776
- return JSON.parse(readFileSync(path, "utf-8"));
3777
- } catch {
3778
- return null;
3779
- }
3780
- }
3781
- function parse_skill_md(skill_path) {
3782
- try {
3783
- const { frontmatter } = parseFrontmatter(readFileSync(skill_path, "utf-8"));
3784
- const description = frontmatter?.description;
3785
- if (!description) return null;
3786
- return {
3787
- name: frontmatter?.name || basename(dirname(skill_path)),
3788
- description: description.trim()
3789
- };
3790
- } catch {
3791
- return null;
3792
- }
3793
- }
3794
- function read_import_metadata(base_dir) {
3795
- const metadata_path = join(base_dir, IMPORT_METADATA_FILE);
3796
- if (!existsSync(metadata_path)) return void 0;
3797
- try {
3798
- return JSON.parse(readFileSync(metadata_path, "utf-8"));
3799
- } catch {
3800
- return;
3801
- }
3802
- }
3803
- function scan_dir_for_skills(dir, options) {
3804
- if (!existsSync(dir)) return [];
3805
- const results = [];
3806
- const direct = join(dir, "SKILL.md");
3807
- if ((options.include_direct_root_skill ?? true) && existsSync(direct)) {
3808
- const parsed = parse_skill_md(direct);
3809
- if (parsed) results.push({
3810
- ...parsed,
3811
- skillPath: direct,
3812
- baseDir: dir,
3813
- source: options.source,
3814
- kind: options.kind,
3815
- plugin: options.plugin,
3816
- import_meta: options.kind === "managed" ? read_import_metadata(dir) : void 0
3817
- });
3818
- return results;
3819
- }
3820
- try {
3821
- const matches = globSync("*/SKILL.md", { cwd: dir });
3822
- for (const match of matches) {
3823
- const full_path = resolve(dir, match);
3824
- const parsed = parse_skill_md(full_path);
3825
- if (parsed) {
3826
- const base_dir = dirname(full_path);
3827
- results.push({
3828
- ...parsed,
3829
- skillPath: full_path,
3830
- baseDir: base_dir,
3831
- source: options.source,
3832
- kind: options.kind,
3833
- plugin: options.plugin,
3834
- import_meta: options.kind === "managed" ? read_import_metadata(base_dir) : void 0
3835
- });
3836
- }
3837
- }
3838
- } catch {}
3839
- return results;
3840
- }
3841
- function dedupe_by_skill_path(skills) {
3842
- const seen = /* @__PURE__ */ new Set();
3843
- const deduped = [];
3844
- for (const skill of skills) {
3845
- if (seen.has(skill.skillPath)) continue;
3846
- seen.add(skill.skillPath);
3847
- deduped.push(skill);
3848
- }
3849
- return deduped;
3850
- }
3851
- function scan_managed_skills() {
3852
- const skills = [];
3853
- for (const skill of scan_dir_for_skills(join(homedir(), ".claude", "skills"), {
3854
- source: "user-local",
3855
- kind: "managed"
3856
- })) skills.push(skill);
3857
- for (const skill of scan_dir_for_skills(join(homedir(), ".pi", "agent", "skills"), {
3858
- source: "pi-native",
3859
- kind: "managed",
3860
- include_direct_root_skill: false
3861
- })) skills.push(skill);
3862
- return dedupe_by_skill_path(skills);
3863
- }
3864
- function scan_importable_skills() {
3865
- const skills = [];
3866
- const plugins = read_installed_plugins();
3867
- if (!plugins?.plugins) return skills;
3868
- for (const [plugin_id, entries] of Object.entries(plugins.plugins)) {
3869
- const entry = entries[0];
3870
- if (!entry?.installPath || !existsSync(entry.installPath)) continue;
3871
- const source = `plugin:${plugin_id}`;
3872
- const plugin = {
3873
- pluginId: plugin_id,
3874
- installPath: entry.installPath,
3875
- version: entry.version,
3876
- gitCommitSha: entry.gitCommitSha
3877
- };
3878
- for (const skill of scan_dir_for_skills(join(entry.installPath, "skills"), {
3879
- source,
3880
- kind: "external",
3881
- plugin
3882
- })) skills.push(skill);
3883
- for (const skill of scan_dir_for_skills(join(entry.installPath, ".pi", "skills"), {
3884
- source,
3885
- kind: "external",
3886
- plugin
3887
- })) skills.push(skill);
3888
- const direct_root_skill = join(entry.installPath, "SKILL.md");
3889
- if (existsSync(direct_root_skill)) {
3890
- const parsed = parse_skill_md(direct_root_skill);
3891
- if (parsed) skills.push({
3892
- ...parsed,
3893
- skillPath: direct_root_skill,
3894
- baseDir: entry.installPath,
3895
- source,
3896
- kind: "external",
3897
- plugin
3898
- });
3899
- }
3900
- }
3901
- return dedupe_by_skill_path(skills);
3902
- }
3903
- //#endregion
3904
- //#region src/skills/importer.ts
3905
- const IMPORT_METADATA_VERSION = 1;
3906
- function get_managed_skills_dir() {
3907
- return join(homedir(), ".pi", "agent", "skills");
3908
- }
3909
- function ensure_dir(path) {
3910
- mkdirSync(path, {
3911
- recursive: true,
3912
- mode: 448
3913
- });
3914
- }
3915
- function list_files_recursively(dir) {
3916
- const files = [];
3917
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
3918
- const full_path = join(dir, entry.name);
3919
- if (entry.name === ".my-pi-source.json") continue;
3920
- if (entry.isDirectory()) {
3921
- files.push(...list_files_recursively(full_path));
3922
- continue;
3923
- }
3924
- if (entry.isFile()) files.push(full_path);
3925
- }
3926
- return files.sort((a, b) => a.localeCompare(b));
3927
- }
3928
- function hash_directory(dir) {
3929
- const hash = createHash("sha256");
3930
- for (const file of list_files_recursively(dir)) {
3931
- hash.update(relative(dir, file));
3932
- hash.update("\0");
3933
- hash.update(readFileSync(file));
3934
- hash.update("\0");
3935
- }
3936
- return hash.digest("hex");
3887
+ function get_telemetry_config_path() {
3888
+ return join(getAgentDir(), "telemetry.json");
3937
3889
  }
3938
- function read_metadata(base_dir) {
3939
- const path = join(base_dir, IMPORT_METADATA_FILE);
3940
- if (!existsSync(path)) return void 0;
3890
+ function get_default_telemetry_db_path() {
3891
+ return join(getAgentDir(), "telemetry.db");
3892
+ }
3893
+ function resolve_telemetry_db_path(cwd, override_path) {
3894
+ if (!override_path) return get_default_telemetry_db_path();
3895
+ return resolve(cwd, override_path);
3896
+ }
3897
+ function load_telemetry_config() {
3898
+ const path = get_telemetry_config_path();
3899
+ if (!existsSync(path)) return { ...DEFAULT_CONFIG };
3941
3900
  try {
3942
- return JSON.parse(readFileSync(path, "utf-8"));
3901
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
3902
+ return {
3903
+ version: typeof parsed.version === "number" ? parsed.version : DEFAULT_CONFIG.version,
3904
+ enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : DEFAULT_CONFIG.enabled
3905
+ };
3943
3906
  } catch {
3944
- return;
3907
+ return { ...DEFAULT_CONFIG };
3945
3908
  }
3946
3909
  }
3947
- function write_metadata(base_dir, metadata) {
3948
- writeFileSync(join(base_dir, IMPORT_METADATA_FILE), JSON.stringify(metadata, null, " ") + "\n", { mode: 384 });
3949
- }
3950
- function replace_directory(source_dir, dest_dir) {
3951
- const parent_dir = dirname(dest_dir);
3952
- ensure_dir(parent_dir);
3953
- const tmp_dir = join(parent_dir, `.${resolve(dest_dir).split("/").pop()}.tmp-${Date.now()}`);
3954
- rmSync(tmp_dir, {
3955
- recursive: true,
3956
- force: true
3957
- });
3958
- cpSync(source_dir, tmp_dir, {
3959
- recursive: true,
3960
- preserveTimestamps: true,
3961
- verbatimSymlinks: false
3962
- });
3963
- rmSync(dest_dir, {
3964
- recursive: true,
3965
- force: true
3966
- });
3967
- cpSync(tmp_dir, dest_dir, {
3968
- recursive: true,
3969
- preserveTimestamps: true,
3970
- verbatimSymlinks: false
3971
- });
3972
- rmSync(tmp_dir, {
3910
+ function save_telemetry_config(config) {
3911
+ const path = get_telemetry_config_path();
3912
+ const dir = dirname(path);
3913
+ if (!existsSync(dir)) mkdirSync(dir, {
3973
3914
  recursive: true,
3974
- force: true
3915
+ mode: 448
3975
3916
  });
3917
+ const tmp = `${path}.tmp-${Date.now()}`;
3918
+ writeFileSync(tmp, JSON.stringify(config, null, " ") + "\n", { mode: 384 });
3919
+ renameSync(tmp, path);
3976
3920
  }
3977
- function import_external_skill(skill) {
3978
- if (skill.kind !== "external") throw new Error(`Skill ${skill.name} is not importable`);
3979
- const managed_root = get_managed_skills_dir();
3980
- ensure_dir(managed_root);
3981
- const skill_dir = join(managed_root, skill.name);
3982
- if (existsSync(skill_dir)) {
3983
- if (!statSync(skill_dir).isDirectory()) throw new Error(`${skill_dir} exists and is not a directory`);
3984
- if (!read_metadata(skill_dir)) throw new Error(`Refusing to overwrite existing unmanaged skill at ${skill_dir}`);
3985
- }
3986
- replace_directory(skill.baseDir, skill_dir);
3987
- const upstream_hash = hash_directory(skill.baseDir);
3988
- const imported_hash = hash_directory(skill_dir);
3989
- const now = (/* @__PURE__ */ new Date()).toISOString();
3990
- const metadata = {
3991
- version: IMPORT_METADATA_VERSION,
3992
- source: skill.source,
3993
- upstream_skill_path: skill.skillPath,
3994
- upstream_base_dir: skill.baseDir,
3995
- upstream_install_path: skill.plugin?.installPath,
3996
- upstream_version: skill.plugin?.version,
3997
- upstream_git_commit_sha: skill.plugin?.gitCommitSha,
3998
- imported_at: now,
3999
- last_synced_at: now,
4000
- imported_hash,
4001
- upstream_hash
4002
- };
4003
- write_metadata(skill_dir, metadata);
3921
+ function resolve_telemetry_enabled(config = load_telemetry_config(), override) {
3922
+ return override ?? config.enabled;
3923
+ }
3924
+ //#endregion
3925
+ //#region src/extensions/telemetry.ts
3926
+ const COMMANDS = [
3927
+ "status",
3928
+ "stats",
3929
+ "query",
3930
+ "export",
3931
+ "on",
3932
+ "off",
3933
+ "path"
3934
+ ];
3935
+ const DEFAULT_QUERY_LIMIT = 20;
3936
+ function parse_int(value) {
3937
+ if (!value) return null;
3938
+ const parsed = Number.parseInt(value, 10);
3939
+ return Number.isFinite(parsed) ? parsed : null;
3940
+ }
3941
+ function get_eval_metadata() {
4004
3942
  return {
4005
- skillDir: skill_dir,
4006
- metadata
3943
+ run_id: process.env.MY_PI_EVAL_RUN_ID ?? null,
3944
+ case_id: process.env.MY_PI_EVAL_CASE_ID ?? null,
3945
+ attempt: parse_int(process.env.MY_PI_EVAL_ATTEMPT),
3946
+ suite: process.env.MY_PI_EVAL_SUITE ?? null
4007
3947
  };
4008
3948
  }
4009
- function sync_imported_skill(skill) {
4010
- if (skill.kind !== "managed" || !skill.import_meta) throw new Error(`Skill ${skill.name} is not managed by my-pi sync`);
4011
- const metadata = skill.import_meta;
4012
- if (!existsSync(metadata.upstream_base_dir)) throw new Error(`Upstream source no longer exists: ${metadata.upstream_base_dir}`);
4013
- if (hash_directory(skill.baseDir) !== metadata.imported_hash) throw new Error(`Refusing to sync ${skill.name}; local changes detected in ${skill.baseDir}`);
4014
- const upstream_hash = hash_directory(metadata.upstream_base_dir);
4015
- if (upstream_hash === metadata.upstream_hash) return {
4016
- skillDir: skill.baseDir,
4017
- metadata,
4018
- changed: false
4019
- };
4020
- replace_directory(metadata.upstream_base_dir, skill.baseDir);
4021
- const imported_hash = hash_directory(skill.baseDir);
4022
- const updated = {
4023
- ...metadata,
4024
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
4025
- imported_hash,
4026
- upstream_hash
3949
+ function get_model_identity(model) {
3950
+ if (!model) return {
3951
+ provider: null,
3952
+ id: null
4027
3953
  };
4028
- write_metadata(skill.baseDir, updated);
4029
3954
  return {
4030
- skillDir: skill.baseDir,
4031
- metadata: updated,
4032
- changed: true
3955
+ provider: typeof model.provider === "string" ? model.provider : null,
3956
+ id: typeof model.id === "string" ? model.id : null
4033
3957
  };
4034
3958
  }
4035
- //#endregion
4036
- //#region src/skills/manager.ts
4037
- function resolve_skill_key(skill) {
4038
- return make_skill_key(skill.name, skill.source);
4039
- }
4040
- function match_skill_by_key_or_name(skills, key_or_name) {
4041
- const exact_key = skills.find((skill) => resolve_skill_key(skill) === key_or_name);
4042
- if (exact_key) return exact_key;
4043
- const by_name = skills.filter((skill) => skill.name === key_or_name);
4044
- if (by_name.length === 1) return by_name[0];
4045
- if (by_name.length > 1) throw new Error(`Multiple skills named ${key_or_name}. Use an exact key instead.`);
4046
- throw new Error(`Unknown skill: ${key_or_name}`);
3959
+ function get_session_file(ctx) {
3960
+ return ctx.sessionManager.getSessionFile?.() ?? null;
4047
3961
  }
4048
- function create_skills_manager() {
4049
- let config = load_skills_config();
4050
- let managed_cache = null;
4051
- let importable_cache = null;
4052
- function get_managed() {
4053
- if (!managed_cache) managed_cache = scan_managed_skills();
4054
- return managed_cache;
4055
- }
4056
- function get_importable() {
4057
- if (!importable_cache) importable_cache = scan_importable_skills();
4058
- return importable_cache;
4059
- }
4060
- function to_managed(skill) {
4061
- const key = resolve_skill_key(skill);
4062
- return {
4063
- ...skill,
4064
- key,
4065
- enabled: skill.kind === "managed" ? is_skill_enabled(config, key) : false
4066
- };
4067
- }
4068
- function get_enabled_managed_skills() {
4069
- return get_managed().filter((skill) => is_skill_enabled(config, resolve_skill_key(skill))).map(to_managed);
3962
+ function safe_json_stringify(value) {
3963
+ if (value === void 0) return null;
3964
+ try {
3965
+ return JSON.stringify(value);
3966
+ } catch {
3967
+ return JSON.stringify({
3968
+ type: typeof value,
3969
+ unserializable: true
3970
+ });
4070
3971
  }
4071
- return {
4072
- discover() {
4073
- return get_managed().map(to_managed);
4074
- },
4075
- discover_importable() {
4076
- return get_importable().map(to_managed);
4077
- },
4078
- is_enabled_by_skill(name, filePath) {
4079
- const discovered = get_managed();
4080
- const match = discovered.find((s) => s.skillPath === filePath);
4081
- if (match) return is_skill_enabled(config, resolve_skill_key(match));
4082
- const by_name = discovered.find((s) => s.name === name);
4083
- if (by_name) return is_skill_enabled(config, resolve_skill_key(by_name));
4084
- return true;
4085
- },
4086
- get_enabled_skill_paths() {
4087
- return get_enabled_managed_skills().map((skill) => skill.baseDir);
4088
- },
4089
- enable(key) {
4090
- config.enabled[key] = true;
4091
- save_skills_config(config);
4092
- return true;
4093
- },
4094
- disable(key) {
4095
- config.enabled[key] = false;
4096
- save_skills_config(config);
4097
- return false;
4098
- },
4099
- toggle(key) {
4100
- const current = is_skill_enabled(config, key);
4101
- config.enabled[key] = !current;
4102
- save_skills_config(config);
4103
- return !current;
4104
- },
4105
- search(query) {
4106
- const q = query.toLowerCase();
4107
- return this.discover().filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.source.toLowerCase().includes(q));
4108
- },
4109
- search_importable(query) {
4110
- const q = query.toLowerCase();
4111
- return this.discover_importable().filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.source.toLowerCase().includes(q));
4112
- },
4113
- set_defaults(policy) {
4114
- config.defaults = policy;
4115
- save_skills_config(config);
4116
- },
4117
- import_skill(key_or_name) {
4118
- const skill = match_skill_by_key_or_name(get_importable(), key_or_name);
4119
- const result = import_external_skill(skill);
4120
- const managed_key = make_skill_key(skill.name, "pi-native");
4121
- config.enabled[managed_key] = true;
4122
- save_skills_config(config);
4123
- this.refresh();
4124
- return {
4125
- ...result,
4126
- key: managed_key
4127
- };
4128
- },
4129
- sync_skill(key_or_name) {
4130
- const skill = match_skill_by_key_or_name(get_managed(), key_or_name);
4131
- const result = sync_imported_skill(skill);
4132
- this.refresh();
4133
- return {
4134
- ...result,
4135
- key: resolve_skill_key(skill)
4136
- };
4137
- },
4138
- refresh() {
4139
- managed_cache = null;
4140
- importable_cache = null;
4141
- config = load_skills_config();
4142
- }
3972
+ }
3973
+ function summarize_value(value, depth = 0) {
3974
+ if (value == null) return null;
3975
+ if (typeof value === "string") return {
3976
+ type: "string",
3977
+ bytes: Buffer.byteLength(value, "utf-8"),
3978
+ lines: value === "" ? 0 : value.split(/\r?\n/).length
3979
+ };
3980
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
3981
+ if (Array.isArray(value)) return {
3982
+ type: "array",
3983
+ length: value.length,
3984
+ items: depth >= 1 ? void 0 : value.slice(0, 5).map((item) => summarize_value(item, depth + 1))
4143
3985
  };
3986
+ if (typeof value === "object") {
3987
+ const entries = Object.entries(value);
3988
+ const summary = {
3989
+ type: "object",
3990
+ keys: entries.map(([key]) => key).slice(0, 20)
3991
+ };
3992
+ if (depth < 1) for (const [key, child] of entries.slice(0, 10)) {
3993
+ if (key === "oldText" || key === "newText" || key === "content" || key === "text") {
3994
+ summary[`${key}_summary`] = summarize_value(child, depth + 1);
3995
+ continue;
3996
+ }
3997
+ summary[key] = summarize_value(child, depth + 1);
3998
+ }
3999
+ return summary;
4000
+ }
4001
+ return { type: typeof value };
4144
4002
  }
4145
- //#endregion
4146
- //#region src/extensions/skills.ts
4147
- const ENABLED = "[x]";
4148
- const DISABLED = "[ ]";
4149
- const SYNC = "[~]";
4150
- const IMPORTED_LABEL = "[=]";
4151
- function sort_skills(skills) {
4152
- return [...skills].sort((a, b) => {
4153
- const by_name = a.name.localeCompare(b.name);
4154
- if (by_name !== 0) return by_name;
4155
- const by_source = a.source.localeCompare(b.source);
4156
- if (by_source !== 0) return by_source;
4157
- return a.key.localeCompare(b.key);
4003
+ function summarize_tool_args(tool_name, args) {
4004
+ if (!args || typeof args !== "object") return safe_json_stringify(summarize_value(args));
4005
+ const input = args;
4006
+ switch (tool_name) {
4007
+ case "bash": return safe_json_stringify({
4008
+ tool: tool_name,
4009
+ timeout: input.timeout ?? null,
4010
+ command: summarize_value(input.command)
4011
+ });
4012
+ case "read":
4013
+ case "write":
4014
+ case "edit": return safe_json_stringify({
4015
+ tool: tool_name,
4016
+ path: typeof input.path === "string" ? input.path : null,
4017
+ offset: typeof input.offset === "number" ? input.offset : null,
4018
+ limit: typeof input.limit === "number" ? input.limit : null,
4019
+ content: summarize_value(input.content),
4020
+ edits: summarize_value(input.edits)
4021
+ });
4022
+ default: return safe_json_stringify({
4023
+ tool: tool_name,
4024
+ summary: summarize_value(args)
4025
+ });
4026
+ }
4027
+ }
4028
+ function summarize_tool_result(result) {
4029
+ return safe_json_stringify(summarize_value(result));
4030
+ }
4031
+ function summarize_headers(headers) {
4032
+ return safe_json_stringify({
4033
+ keys: Object.keys(headers).slice(0, 20),
4034
+ count: Object.keys(headers).length
4158
4035
  });
4159
4036
  }
4160
- function find_matching_imported_skill(managed_skills, skill) {
4161
- const exact_match = managed_skills.find((candidate) => candidate.import_meta?.source === skill.source && (candidate.import_meta.upstream_skill_path === skill.skillPath || candidate.import_meta.upstream_base_dir === skill.baseDir));
4162
- if (exact_match) return exact_match;
4163
- return managed_skills.find((candidate) => candidate.import_meta?.source === skill.source && candidate.name === skill.name);
4037
+ function summarize_provider_payload(payload) {
4038
+ return safe_json_stringify(summarize_value(payload));
4164
4039
  }
4165
- function get_importable_state(managed_skills, skill) {
4166
- const imported = find_matching_imported_skill(managed_skills, skill);
4167
- if (imported?.import_meta) {
4168
- const version_changed = Boolean(skill.plugin?.version && imported.import_meta.upstream_version && skill.plugin.version !== imported.import_meta.upstream_version);
4169
- const sha_changed = Boolean(skill.plugin?.gitCommitSha && imported.import_meta.upstream_git_commit_sha && skill.plugin.gitCommitSha !== imported.import_meta.upstream_git_commit_sha);
4170
- if (version_changed || sha_changed) return {
4171
- label: "sync",
4172
- detail: "Press Enter to sync the imported copy and reload",
4173
- action: "sync"
4174
- };
4175
- return {
4176
- label: "imported",
4177
- detail: `Already imported to ${imported.baseDir}`,
4178
- action: null
4179
- };
4180
- }
4181
- const managed_conflict = managed_skills.find((candidate) => candidate.name === skill.name);
4182
- if (managed_conflict) return {
4183
- label: "managed",
4184
- detail: `Already managed at ${managed_conflict.baseDir}`,
4185
- action: null
4040
+ function get_stop_reason(message) {
4041
+ if (!message || typeof message !== "object") return null;
4042
+ const stop_reason = message.stopReason;
4043
+ return typeof stop_reason === "string" ? stop_reason : null;
4044
+ }
4045
+ function get_error_message(message) {
4046
+ if (!message || typeof message !== "object") return null;
4047
+ const error_message = message.errorMessage;
4048
+ return typeof error_message === "string" ? error_message : null;
4049
+ }
4050
+ function infer_run_outcome(event) {
4051
+ const last_assistant = event.messages.filter((message) => message.role === "assistant").at(-1);
4052
+ const stop_reason = get_stop_reason(last_assistant);
4053
+ if (stop_reason === "error") return {
4054
+ success: false,
4055
+ error_message: get_error_message(last_assistant) ?? "agent error"
4056
+ };
4057
+ if (stop_reason === "aborted") return {
4058
+ success: false,
4059
+ error_message: get_error_message(last_assistant) ?? "agent aborted"
4186
4060
  };
4187
4061
  return {
4188
- label: "import",
4189
- detail: "Press Enter to import into pi-native skills and reload",
4190
- action: "import"
4062
+ success: true,
4063
+ error_message: null
4191
4064
  };
4192
4065
  }
4193
- function to_setting_item(skill) {
4194
- const detail_lines = [
4195
- `${skill.source} • ${skill.key}`,
4196
- skill.description,
4197
- skill.baseDir
4198
- ];
4199
- if (skill.import_meta?.upstream_version) detail_lines.push(`upstream: ${skill.import_meta.upstream_version}${skill.import_meta.upstream_git_commit_sha ? ` • ${skill.import_meta.upstream_git_commit_sha.slice(0, 12)}` : ""}`);
4066
+ function format_telemetry_status(options) {
4067
+ const override_label = options.override === void 0 ? "none" : options.override ? "--telemetry" : "--no-telemetry";
4068
+ return [
4069
+ `telemetry ${options.effective_enabled ? "enabled" : "disabled"} now`,
4070
+ `default ${options.saved_enabled ? "enabled" : "disabled"}`,
4071
+ `override ${override_label}`,
4072
+ `db ${options.db_path}`
4073
+ ].join("\n");
4074
+ }
4075
+ function format_bytes(bytes) {
4076
+ if (bytes < 1024) return `${bytes} B`;
4077
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
4078
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
4079
+ }
4080
+ function format_timestamp(timestamp) {
4081
+ return new Date(timestamp).toISOString();
4082
+ }
4083
+ function format_duration(duration_ms) {
4084
+ if (duration_ms === null) return "open";
4085
+ if (duration_ms < 1e3) return `${duration_ms}ms`;
4086
+ if (duration_ms < 6e4) return `${(duration_ms / 1e3).toFixed(1)}s`;
4087
+ return `${(duration_ms / 6e4).toFixed(1)}m`;
4088
+ }
4089
+ function format_success(value) {
4090
+ if (value === true) return "success";
4091
+ if (value === false) return "failure";
4092
+ return "unknown";
4093
+ }
4094
+ function tokenize_command_args(input) {
4095
+ return (input.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S+/g) ?? []).map((token) => {
4096
+ if (token.startsWith("\"") && token.endsWith("\"") || token.startsWith("'") && token.endsWith("'")) return token.slice(1, -1);
4097
+ return token;
4098
+ });
4099
+ }
4100
+ function parse_telemetry_command(input) {
4101
+ const tokens = tokenize_command_args(input.trim());
4102
+ const subcommand = tokens[0] ?? "status";
4103
+ const filters = {};
4104
+ let export_path = null;
4105
+ const errors = [];
4106
+ for (const token of tokens.slice(1)) {
4107
+ const equals_index = token.indexOf("=");
4108
+ if (equals_index === -1) {
4109
+ if (subcommand === "export" && export_path === null) export_path = token;
4110
+ else errors.push(`Unexpected argument: ${token}`);
4111
+ continue;
4112
+ }
4113
+ const key = token.slice(0, equals_index);
4114
+ const value = token.slice(equals_index + 1);
4115
+ switch (key) {
4116
+ case "eval_run_id":
4117
+ case "run":
4118
+ filters.eval_run_id = value;
4119
+ break;
4120
+ case "eval_case_id":
4121
+ case "case":
4122
+ filters.eval_case_id = value;
4123
+ break;
4124
+ case "eval_suite":
4125
+ case "suite":
4126
+ filters.eval_suite = value;
4127
+ break;
4128
+ case "success":
4129
+ if (value === "true") filters.success = true;
4130
+ else if (value === "false") filters.success = false;
4131
+ else if (value === "null") filters.success = null;
4132
+ else errors.push(`Invalid success value: ${value}. Use true, false, or null`);
4133
+ break;
4134
+ case "limit": {
4135
+ const parsed = Number.parseInt(value, 10);
4136
+ if (!Number.isFinite(parsed) || parsed <= 0) errors.push(`Invalid limit value: ${value}. Use a positive integer`);
4137
+ else filters.limit = parsed;
4138
+ break;
4139
+ }
4140
+ default: errors.push(`Unknown filter: ${key}`);
4141
+ }
4142
+ }
4143
+ if (subcommand === "query" && filters.limit === void 0) filters.limit = DEFAULT_QUERY_LIMIT;
4200
4144
  return {
4201
- id: skill.key,
4202
- label: skill.name,
4203
- description: detail_lines.join("\n"),
4204
- currentValue: skill.enabled ? ENABLED : DISABLED,
4205
- values: [ENABLED, DISABLED]
4145
+ subcommand,
4146
+ export_path,
4147
+ filters,
4148
+ errors
4206
4149
  };
4207
4150
  }
4208
- function to_importable_setting_item(managed_skills, skill) {
4209
- const state = get_importable_state(managed_skills, skill);
4210
- const detail_lines = [
4211
- `${skill.source} ${skill.key}`,
4212
- skill.description,
4213
- skill.baseDir
4214
- ];
4215
- if (skill.plugin?.version) detail_lines.push(`plugin: ${skill.plugin.version}${skill.plugin.gitCommitSha ? ` • ${skill.plugin.gitCommitSha.slice(0, 12)}` : ""}`);
4216
- if (state.action === "import") return {
4217
- id: skill.key,
4218
- label: skill.name,
4219
- description: detail_lines.join("\n"),
4220
- currentValue: DISABLED,
4221
- values: [ENABLED, DISABLED]
4222
- };
4223
- if (state.action === "sync") {
4224
- detail_lines.push("enter to sync");
4225
- return {
4226
- id: skill.key,
4227
- label: skill.name,
4228
- description: detail_lines.join("\n"),
4229
- currentValue: SYNC,
4230
- values: [SYNC]
4231
- };
4232
- }
4233
- detail_lines.push(state.detail);
4234
- return {
4235
- id: skill.key,
4236
- label: skill.name,
4237
- description: detail_lines.join("\n"),
4238
- currentValue: IMPORTED_LABEL
4239
- };
4151
+ function format_filter_summary(filters) {
4152
+ const parts = [];
4153
+ if (filters.eval_run_id !== void 0) parts.push(`eval_run_id=${filters.eval_run_id}`);
4154
+ if (filters.eval_case_id !== void 0) parts.push(`eval_case_id=${filters.eval_case_id}`);
4155
+ if (filters.eval_suite !== void 0) parts.push(`eval_suite=${filters.eval_suite}`);
4156
+ if (filters.success !== void 0) parts.push(`success=${String(filters.success)}`);
4157
+ if (filters.limit !== void 0) parts.push(`limit=${filters.limit}`);
4158
+ return parts.length > 0 ? parts.join(" ") : "none";
4159
+ }
4160
+ function format_telemetry_stats(options) {
4161
+ return [
4162
+ `db ${options.db_path}`,
4163
+ `schema v${options.stats.schema_version}`,
4164
+ `runs ${options.stats.runs}`,
4165
+ `turns ${options.stats.turns}`,
4166
+ `tool_calls ${options.stats.tool_calls}`,
4167
+ `provider_requests ${options.stats.provider_requests}`,
4168
+ `db_bytes ${format_bytes(options.stats.db_bytes)}`,
4169
+ `wal_bytes ${format_bytes(options.stats.wal_bytes)}`,
4170
+ `total_bytes ${format_bytes(options.stats.total_bytes)}`
4171
+ ].join("\n");
4172
+ }
4173
+ function format_telemetry_query_results(options) {
4174
+ if (options.runs.length === 0) return [
4175
+ `db ${options.db_path}`,
4176
+ `filters ${format_filter_summary(options.filters)}`,
4177
+ "no matching runs"
4178
+ ].join("\n");
4179
+ return [
4180
+ `db ${options.db_path}`,
4181
+ `filters ${format_filter_summary(options.filters)}`,
4182
+ ...options.runs.map((run) => [
4183
+ `${format_timestamp(run.started_at)} ${run.id}`,
4184
+ `status=${format_success(run.success)}`,
4185
+ `duration=${format_duration(run.duration_ms)}`,
4186
+ `turns=${run.turn_count}`,
4187
+ `tools=${run.tool_call_count}`,
4188
+ `tool_errors=${run.tool_error_count}`,
4189
+ `provider_requests=${run.provider_request_count}`,
4190
+ run.eval_run_id ? `eval_run_id=${run.eval_run_id}` : null,
4191
+ run.eval_case_id ? `eval_case_id=${run.eval_case_id}` : null,
4192
+ run.eval_suite ? `eval_suite=${run.eval_suite}` : null
4193
+ ].filter(Boolean).join(" "))
4194
+ ].join("\n");
4195
+ }
4196
+ function get_default_telemetry_export_path(cwd) {
4197
+ return resolve(cwd, `telemetry-export-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
4240
4198
  }
4241
- function sets_equal(a, b) {
4242
- if (a.size !== b.size) return false;
4243
- for (const value of a) if (!b.has(value)) return false;
4244
- return true;
4199
+ async function default_load_store(db_path) {
4200
+ const { TelemetryDatabase } = await import("./telemetry-db-BnenoOSj.js");
4201
+ return TelemetryDatabase.open(db_path);
4245
4202
  }
4246
- async function skills(pi) {
4247
- const mgr = create_skills_manager();
4248
- const subs = [
4249
- "import",
4250
- "sync",
4251
- "refresh",
4252
- "defaults"
4253
- ];
4254
- pi.registerCommand("skills", {
4255
- description: "Manage pi-native skills and import external skills",
4256
- getArgumentCompletions: (prefix) => {
4257
- const parts = prefix.trim().split(/\s+/);
4258
- if (parts.length <= 1) return subs.filter((s) => s.startsWith(parts[0] || "")).map((s) => ({
4259
- value: s,
4260
- label: s
4261
- }));
4262
- if (parts[0] === "import") {
4263
- const q = parts.slice(1).join(" ").toLowerCase();
4264
- return sort_skills(mgr.discover_importable()).filter((s) => s.key.toLowerCase().includes(q) || s.name.toLowerCase().includes(q)).slice(0, 20).map((s) => ({
4265
- value: `${parts[0]} ${s.key}`,
4266
- label: s.key
4267
- }));
4268
- }
4269
- if (parts[0] === "sync") {
4270
- const q = parts.slice(1).join(" ").toLowerCase();
4271
- return sort_skills(mgr.discover().filter((skill) => Boolean(skill.import_meta))).filter((s) => s.key.toLowerCase().includes(q) || s.name.toLowerCase().includes(q)).slice(0, 20).map((s) => ({
4272
- value: `${parts[0]} ${s.key}`,
4273
- label: s.key
4274
- }));
4275
- }
4276
- return null;
4277
- },
4278
- handler: async (args, ctx) => {
4279
- const trimmed = args.trim();
4280
- if (!trimmed && ctx.hasUI) {
4281
- const discovered = sort_skills(mgr.discover());
4282
- const importable = sort_skills(mgr.discover_importable());
4283
- if (discovered.length === 0 && importable.length === 0) {
4284
- ctx.ui.notify("No managed or importable skills found");
4285
- return;
4286
- }
4287
- const initial_enabled = new Set(discovered.filter((skill) => skill.enabled).map((skill) => skill.key));
4288
- const current_enabled = new Set(initial_enabled);
4289
- const queued_imports = /* @__PURE__ */ new Set();
4290
- let reload_notice = null;
4291
- const managed_items = discovered.map(to_setting_item);
4292
- const importable_items = importable.map((skill) => to_importable_setting_item(discovered, skill));
4293
- const all_items = [];
4294
- if (managed_items.length > 0) {
4295
- all_items.push({
4296
- id: "__header_managed__",
4297
- label: `── Managed (${managed_items.length}) ──`,
4298
- description: "",
4299
- currentValue: ""
4300
- });
4301
- all_items.push(...managed_items);
4302
- }
4303
- if (importable_items.length > 0) {
4304
- all_items.push({
4305
- id: "__header_importable__",
4306
- label: `── Importable (${importable_items.length}) ──`,
4307
- description: "",
4308
- currentValue: ""
4309
- });
4310
- all_items.push(...importable_items);
4311
- }
4312
- const managed_keys = new Set(discovered.map((s) => s.key));
4313
- const importable_map = new Map(importable.map((s) => [s.key, s]));
4314
- await ctx.ui.custom((tui, theme, _kb, done) => {
4315
- const list = new SettingsList(all_items, Math.min(Math.max(all_items.length + 4, 8), 22), {
4316
- cursor: theme.fg("accent", "›"),
4317
- label: (text, selected) => {
4318
- if (text.startsWith("──") && text.endsWith("──")) return theme.fg("dim", theme.bold(text));
4319
- return selected ? theme.fg("accent", text) : text;
4320
- },
4321
- value: (text, selected) => {
4322
- const color = text === ENABLED ? "success" : text === SYNC ? "warning" : text === IMPORTED_LABEL ? "success" : "dim";
4323
- const rendered = theme.fg(color, text);
4324
- return selected ? theme.bold(theme.fg("accent", rendered)) : rendered;
4325
- },
4326
- description: (text) => theme.fg("muted", text),
4327
- hint: (text) => theme.fg("dim", text)
4328
- }, (id, new_value) => {
4329
- if (id.startsWith("__header_")) return;
4330
- if (managed_keys.has(id)) {
4331
- if (new_value === ENABLED) {
4332
- current_enabled.add(id);
4333
- mgr.enable(id);
4334
- } else {
4335
- current_enabled.delete(id);
4336
- mgr.disable(id);
4337
- }
4338
- return;
4339
- }
4340
- const import_skill = importable_map.get(id);
4341
- if (!import_skill) return;
4342
- const state = get_importable_state(discovered, import_skill);
4343
- if (state.action === "import") {
4344
- if (new_value === ENABLED) queued_imports.add(id);
4345
- else queued_imports.delete(id);
4346
- return;
4347
- }
4348
- if (state.action === "sync") {
4349
- const imported_skill = find_matching_imported_skill(discovered, import_skill);
4350
- if (!imported_skill) {
4351
- ctx.ui.notify(`Imported copy for ${import_skill.name} was not found`, "warning");
4352
- return;
4353
- }
4354
- try {
4355
- if (mgr.sync_skill(imported_skill.key).changed) {
4356
- reload_notice = `Synced ${import_skill.name}. Reloading...`;
4357
- done(void 0);
4358
- } else ctx.ui.notify(`${import_skill.name} is already up to date.`, "info");
4359
- } catch (error) {
4360
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
4361
- }
4362
- }
4363
- }, () => done(void 0), { enableSearch: true });
4364
- const container = new Container();
4365
- container.addChild({
4366
- render: () => {
4367
- const enabled = current_enabled.size;
4368
- const disabled = discovered.length - enabled;
4369
- const queued = queued_imports.size;
4370
- const parts = [`${enabled} enabled`, `${disabled} disabled`];
4371
- if (importable.length > 0) parts.push(`${importable.length} importable`);
4372
- if (queued > 0) parts.push(`${queued} queued for import`);
4373
- return [
4374
- theme.fg("accent", theme.bold("Skills")),
4375
- theme.fg("muted", parts.join(" • ")),
4376
- ""
4377
- ];
4378
- },
4379
- invalidate: () => {}
4380
- });
4381
- container.addChild({
4382
- render(width) {
4383
- return list.render(width);
4384
- },
4385
- invalidate() {
4386
- list.invalidate();
4387
- }
4388
- });
4389
- container.addChild(new Text(theme.fg("dim", "search filters • enter toggles • esc close"), 0, 1));
4390
- return {
4391
- render(width) {
4392
- return container.render(width);
4393
- },
4394
- invalidate() {
4395
- container.invalidate();
4396
- },
4397
- handleInput(data) {
4398
- list.handleInput(data);
4399
- tui.requestRender();
4400
- }
4401
- };
4402
- });
4403
- if (queued_imports.size > 0) {
4404
- const imported_names = [];
4405
- for (const key of queued_imports) try {
4406
- mgr.import_skill(key);
4407
- imported_names.push(key);
4408
- } catch (error) {
4409
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
4410
- }
4411
- if (imported_names.length > 0) reload_notice = `Imported ${imported_names.length} skill(s). Reloading...`;
4203
+ function create_telemetry_extension(options = {}) {
4204
+ return async function telemetry(pi) {
4205
+ const now = options.now ?? (() => Date.now());
4206
+ const load_store = options.load_store ?? default_load_store;
4207
+ const cwd = options.cwd ?? process.cwd();
4208
+ const db_path = resolve_telemetry_db_path(cwd, options.db_path);
4209
+ let config = load_telemetry_config();
4210
+ let store = null;
4211
+ let effective_enabled = resolve_telemetry_enabled(config, options.enabled);
4212
+ let current_model = {
4213
+ provider: null,
4214
+ id: null
4215
+ };
4216
+ let active_run = null;
4217
+ const active_turns = /* @__PURE__ */ new Map();
4218
+ const provider_request_ids = [];
4219
+ async function ensure_store() {
4220
+ if (!effective_enabled) return null;
4221
+ if (!store) store = await load_store(db_path);
4222
+ return store;
4223
+ }
4224
+ function finish_active_run_on_disable(reason) {
4225
+ if (!store || !active_run) return;
4226
+ store.finish_run({
4227
+ id: active_run.id,
4228
+ ended_at: now(),
4229
+ success: null,
4230
+ error_message: reason
4231
+ });
4232
+ active_run = null;
4233
+ active_turns.clear();
4234
+ provider_request_ids.length = 0;
4235
+ }
4236
+ function close_store() {
4237
+ if (!store) return;
4238
+ store.close();
4239
+ store = null;
4240
+ }
4241
+ function command_message(ctx, message) {
4242
+ if (ctx.hasUI) ctx.ui.notify(message);
4243
+ else console.error(message);
4244
+ }
4245
+ pi.registerCommand("telemetry", {
4246
+ description: "Manage local SQLite telemetry for evals and debugging",
4247
+ getArgumentCompletions: (prefix) => {
4248
+ const first_token = prefix.trim().split(/\s+/, 1)[0] ?? "";
4249
+ return COMMANDS.filter((command) => command.startsWith(first_token)).map((command) => ({
4250
+ value: command,
4251
+ label: command
4252
+ }));
4253
+ },
4254
+ handler: async (args, ctx) => {
4255
+ const parsed = parse_telemetry_command(args);
4256
+ const subcommand = parsed.subcommand;
4257
+ if (!COMMANDS.includes(subcommand)) {
4258
+ command_message(ctx, `Unknown telemetry command: ${subcommand}. Use: ${COMMANDS.join(", ")}`);
4259
+ return;
4412
4260
  }
4413
- if (reload_notice) {
4414
- ctx.ui.notify(reload_notice, "info");
4415
- await ctx.reload();
4261
+ if (parsed.errors.length > 0) {
4262
+ command_message(ctx, parsed.errors.join("\n"));
4416
4263
  return;
4417
4264
  }
4418
- if (!sets_equal(initial_enabled, current_enabled)) {
4419
- ctx.ui.notify("Reloading to apply updated skills...", "info");
4420
- await ctx.reload();
4265
+ if (subcommand === "status") {
4266
+ command_message(ctx, format_telemetry_status({
4267
+ saved_enabled: config.enabled,
4268
+ effective_enabled,
4269
+ override: options.enabled,
4270
+ db_path
4271
+ }));
4421
4272
  return;
4422
4273
  }
4423
- return;
4424
- }
4425
- const [sub, ...rest] = (trimmed || "list").split(/\s+/);
4426
- const arg = rest.join(" ");
4427
- switch (sub) {
4428
- case "import":
4429
- if (!arg) {
4430
- ctx.ui.notify("Usage: /skills import <key|name>", "warning");
4274
+ if (subcommand === "stats") {
4275
+ if (!existsSync(db_path)) {
4276
+ command_message(ctx, `No telemetry database at ${db_path}`);
4431
4277
  return;
4432
4278
  }
4279
+ const stats_store = store ?? await load_store(db_path);
4280
+ const should_close_after = stats_store !== store;
4433
4281
  try {
4434
- const result = mgr.import_skill(arg);
4435
- ctx.ui.notify(`Imported ${arg} to ${result.skillDir}. Reloading...`, "info");
4436
- await ctx.reload();
4437
- return;
4438
- } catch (error) {
4439
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
4440
- return;
4282
+ command_message(ctx, format_telemetry_stats({
4283
+ db_path,
4284
+ stats: stats_store.get_stats()
4285
+ }));
4286
+ } finally {
4287
+ if (should_close_after) stats_store.close();
4441
4288
  }
4442
- case "sync":
4443
- if (!arg) {
4444
- ctx.ui.notify("Usage: /skills sync <key|name>", "warning");
4289
+ return;
4290
+ }
4291
+ if (subcommand === "query" || subcommand === "export") {
4292
+ if (!existsSync(db_path)) {
4293
+ command_message(ctx, `No telemetry database at ${db_path}`);
4445
4294
  return;
4446
4295
  }
4296
+ const query_store = store ?? await load_store(db_path);
4297
+ const should_close_after = query_store !== store;
4447
4298
  try {
4448
- const result = mgr.sync_skill(arg);
4449
- ctx.ui.notify(result.changed ? `Synced ${arg}. Reloading...` : `${arg} is already up to date.`, "info");
4450
- if (result.changed) await ctx.reload();
4451
- return;
4452
- } catch (error) {
4453
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
4454
- return;
4455
- }
4456
- case "refresh":
4457
- mgr.refresh();
4458
- ctx.ui.notify(`Rescanned: ${mgr.discover().length} managed skills, ${mgr.discover_importable().length} importable skills found`);
4459
- break;
4460
- case "defaults":
4461
- if (arg !== "all-enabled" && arg !== "all-disabled") {
4462
- ctx.ui.notify("Usage: /skills defaults <all-enabled|all-disabled>", "warning");
4299
+ const runs = query_store.query_runs(parsed.filters);
4300
+ if (subcommand === "query") {
4301
+ command_message(ctx, format_telemetry_query_results({
4302
+ db_path,
4303
+ filters: parsed.filters,
4304
+ runs
4305
+ }));
4306
+ return;
4307
+ }
4308
+ const export_path = resolve(cwd, parsed.export_path ?? get_default_telemetry_export_path(cwd));
4309
+ mkdirSync(dirname(export_path), { recursive: true });
4310
+ writeFileSync(export_path, JSON.stringify({
4311
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
4312
+ db_path,
4313
+ schema_version: query_store.get_stats().schema_version,
4314
+ filters: parsed.filters,
4315
+ runs
4316
+ }, null, 2), "utf-8");
4317
+ command_message(ctx, `Exported ${runs.length} telemetry run${runs.length === 1 ? "" : "s"} to ${export_path}`);
4463
4318
  return;
4319
+ } finally {
4320
+ if (should_close_after) query_store.close();
4464
4321
  }
4465
- mgr.set_defaults(arg);
4466
- ctx.ui.notify(`Default policy: ${arg}`);
4467
- break;
4468
- default: ctx.ui.notify(`Unknown: ${sub}. Use: ${subs.join(", ")}`, "warning");
4322
+ }
4323
+ if (subcommand === "path") {
4324
+ command_message(ctx, db_path);
4325
+ return;
4326
+ }
4327
+ const next_enabled = subcommand === "on";
4328
+ config = {
4329
+ ...config,
4330
+ enabled: next_enabled
4331
+ };
4332
+ save_telemetry_config(config);
4333
+ if (options.enabled !== void 0) {
4334
+ command_message(ctx, [`Saved default telemetry ${next_enabled ? "enabled" : "disabled"}.`, `Current process still uses ${options.enabled ? "--telemetry" : "--no-telemetry"}.`].join(" "));
4335
+ return;
4336
+ }
4337
+ effective_enabled = next_enabled;
4338
+ if (effective_enabled) {
4339
+ await ensure_store();
4340
+ command_message(ctx, `Telemetry enabled. Writing to ${db_path}`);
4341
+ return;
4342
+ }
4343
+ finish_active_run_on_disable("telemetry disabled");
4344
+ close_store();
4345
+ command_message(ctx, "Telemetry disabled.");
4469
4346
  }
4470
- }
4471
- });
4347
+ });
4348
+ pi.on("model_select", async (event) => {
4349
+ current_model = get_model_identity(event.model);
4350
+ });
4351
+ pi.on("agent_start", async (_event, ctx) => {
4352
+ const active_store = await ensure_store();
4353
+ if (!active_store) return;
4354
+ const run_id = randomUUID();
4355
+ const eval_metadata = get_eval_metadata();
4356
+ const model_identity = ctx.model ? get_model_identity(ctx.model) : current_model;
4357
+ active_store.insert_run({
4358
+ id: run_id,
4359
+ session_file: get_session_file(ctx),
4360
+ cwd: ctx.cwd,
4361
+ started_at: now(),
4362
+ model_provider: model_identity.provider,
4363
+ model_id: model_identity.id,
4364
+ eval_run_id: eval_metadata.run_id,
4365
+ eval_case_id: eval_metadata.case_id,
4366
+ eval_attempt: eval_metadata.attempt,
4367
+ eval_suite: eval_metadata.suite
4368
+ });
4369
+ active_run = { id: run_id };
4370
+ active_turns.clear();
4371
+ provider_request_ids.length = 0;
4372
+ });
4373
+ pi.on("agent_end", async (event) => {
4374
+ if (!store || !active_run) return;
4375
+ const outcome = infer_run_outcome(event);
4376
+ store.finish_run({
4377
+ id: active_run.id,
4378
+ ended_at: now(),
4379
+ success: outcome.success,
4380
+ error_message: outcome.error_message
4381
+ });
4382
+ active_run = null;
4383
+ active_turns.clear();
4384
+ provider_request_ids.length = 0;
4385
+ });
4386
+ pi.on("turn_start", async (event) => {
4387
+ if (!store || !active_run) return;
4388
+ const turn_id = `${active_run.id}:turn:${event.turnIndex}`;
4389
+ active_turns.set(event.turnIndex, { id: turn_id });
4390
+ store.insert_turn({
4391
+ id: turn_id,
4392
+ run_id: active_run.id,
4393
+ turn_index: event.turnIndex,
4394
+ started_at: event.timestamp
4395
+ });
4396
+ });
4397
+ pi.on("turn_end", async (event) => {
4398
+ const active_turn = active_turns.get(event.turnIndex);
4399
+ if (!store || !active_turn) return;
4400
+ store.finish_turn({
4401
+ id: active_turn.id,
4402
+ ended_at: now(),
4403
+ tool_result_count: event.toolResults.length,
4404
+ stop_reason: get_stop_reason(event.message)
4405
+ });
4406
+ active_turns.delete(event.turnIndex);
4407
+ });
4408
+ pi.on("tool_execution_start", async (event) => {
4409
+ if (!store || !active_run) return;
4410
+ const current_turn = [...active_turns.values()].at(-1);
4411
+ store.insert_tool_call({
4412
+ tool_call_id: event.toolCallId,
4413
+ run_id: active_run.id,
4414
+ turn_id: current_turn?.id ?? null,
4415
+ tool_name: event.toolName,
4416
+ started_at: now(),
4417
+ args_summary_json: summarize_tool_args(event.toolName, event.args)
4418
+ });
4419
+ });
4420
+ pi.on("tool_execution_update", async (event) => {
4421
+ if (!store || !active_run) return;
4422
+ store.note_tool_update(event.toolCallId);
4423
+ });
4424
+ pi.on("tool_execution_end", async (event) => {
4425
+ if (!store || !active_run) return;
4426
+ store.finish_tool_call({
4427
+ tool_call_id: event.toolCallId,
4428
+ ended_at: now(),
4429
+ is_error: event.isError,
4430
+ result_summary_json: summarize_tool_result(event.result),
4431
+ error_message: event.isError && event.result != null ? safe_json_stringify(summarize_value(event.result)) : null
4432
+ });
4433
+ });
4434
+ pi.on("before_provider_request", async (event) => {
4435
+ if (!store || !active_run) return;
4436
+ const request_id = randomUUID();
4437
+ const current_turn = [...active_turns.values()].at(-1);
4438
+ store.insert_provider_request({
4439
+ id: request_id,
4440
+ run_id: active_run.id,
4441
+ turn_id: current_turn?.id ?? null,
4442
+ started_at: now(),
4443
+ payload_summary_json: summarize_provider_payload(event.payload)
4444
+ });
4445
+ provider_request_ids.push(request_id);
4446
+ });
4447
+ pi.on("after_provider_response", async (event) => {
4448
+ if (!store || !active_run) return;
4449
+ const request_id = provider_request_ids.shift();
4450
+ if (!request_id) return;
4451
+ store.finish_provider_request({
4452
+ id: request_id,
4453
+ ended_at: now(),
4454
+ status_code: event.status,
4455
+ headers_json: summarize_headers(event.headers)
4456
+ });
4457
+ });
4458
+ pi.on("session_shutdown", async () => {
4459
+ if (store && active_run) store.finish_run({
4460
+ id: active_run.id,
4461
+ ended_at: now(),
4462
+ success: null,
4463
+ error_message: "session shutdown"
4464
+ });
4465
+ close_store();
4466
+ active_run = null;
4467
+ active_turns.clear();
4468
+ provider_request_ids.length = 0;
4469
+ });
4470
+ };
4472
4471
  }
4472
+ create_telemetry_extension();
4473
4473
  //#endregion
4474
4474
  //#region src/api.ts
4475
4475
  const BUILTIN_EXTENSION_FACTORIES = {
@@ -4588,4 +4588,4 @@ async function create_my_pi(options = {}) {
4588
4588
  //#endregion
4589
4589
  export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
4590
4590
 
4591
- //# sourceMappingURL=api-CMc8zzEG.js.map
4591
+ //# sourceMappingURL=api-CWEizv2k.js.map