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