@tekmidian/pai 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ARCHITECTURE.md CHANGED
@@ -103,14 +103,15 @@ The interactive wizard walks through 14 steps:
103
103
  4. CLAUDE.md template installation
104
104
  5. PAI skill installation
105
105
  6. Steering rules installation
106
- 7. Hook system deployment
107
- 8. TypeScript hook compilation
108
- 9. Claude Code settings configuration
109
- 10. Daemon installation
110
- 11. MCP server registration
111
- 12. Directory creation
112
- 13. Initial indexing
113
- 14. Verification
106
+ 7. MCP skill stub symlinks
107
+ 8. Hook system deployment
108
+ 9. TypeScript hook compilation
109
+ 10. Claude Code settings configuration
110
+ 11. Daemon installation
111
+ 12. MCP server registration
112
+ 13. Directory creation
113
+ 14. Initial indexing
114
+ 15. Verification
114
115
 
115
116
  ### 4. Install the Daemon
116
117
 
@@ -642,6 +643,68 @@ The build script compiles each `.ts` hook to a self-contained `.mjs` module usin
642
643
 
643
644
  ---
644
645
 
646
+ ## Skill Stub System
647
+
648
+ PAI's 18 MCP prompts are exposed to Claude Code as discoverable skills via auto-generated SKILL.md files. This bridges the gap between MCP prompts (protocol-level, invoked via `prompts/get`) and Claude Code's skill scanner (filesystem-based, scans `~/.claude/skills/`).
649
+
650
+ ### How It Works
651
+
652
+ ```
653
+ Source (TypeScript) Build Claude Code
654
+ ───────────────── ───── ──────────
655
+ src/daemon-mcp/prompts/*.ts → dist/skills/<Name>/ → ~/.claude/skills/<Name>/
656
+ src/daemon-mcp/prompts/ SKILL.md (symlink)
657
+ custom/*.ts (gitignored)
658
+ ```
659
+
660
+ 1. **Source of truth**: TypeScript files in `src/daemon-mcp/prompts/`. Each exports `{ description, content }`.
661
+ 2. **Build**: `node scripts/build-skill-stubs.mjs --sync` extracts content and generates `dist/skills/<TitleCase>/SKILL.md` with YAML frontmatter.
662
+ 3. **Sync**: The `--sync` flag creates/updates symlinks in `~/.claude/skills/`. Runs automatically on every `bun run build`.
663
+ 4. **Discovery**: Claude Code scans `~/.claude/skills/<Name>/SKILL.md` at session start, loads descriptions, and auto-invokes matching skills.
664
+
665
+ **Important**: Skills MUST be at `~/.claude/skills/<Name>/SKILL.md` (one level deep). Subdirectories like `~/.claude/skills/user/<Name>/` are NOT discovered by Claude Code's scanner.
666
+
667
+ ### Adding a New Skill
668
+
669
+ **Built-in (shipped with PAI):**
670
+
671
+ 1. Create `src/daemon-mcp/prompts/my-skill.ts`:
672
+ ```typescript
673
+ export const mySkill = {
674
+ description: "What the skill does",
675
+ content: `## My Skill
676
+
677
+ USE WHEN user says 'trigger phrase', 'another trigger', ...
678
+
679
+ ### Instructions
680
+ ...your skill content here...`,
681
+ };
682
+ ```
683
+ 2. Add export to `src/daemon-mcp/prompts/index.ts`:
684
+ ```typescript
685
+ export { mySkill } from "./my-skill.js";
686
+ ```
687
+ 3. Run `bun run build` — generates the stub AND syncs the symlink.
688
+
689
+ **Custom (user-created, survives `git pull`):**
690
+
691
+ 1. Create `src/daemon-mcp/prompts/custom/my-local-skill.ts` (same format as above).
692
+ 2. Run `bun run build` — custom prompts are picked up automatically.
693
+
694
+ The `custom/` directory is gitignored (only `.gitkeep` is tracked), so your local skills survive PAI updates.
695
+
696
+ ### Updating After Changes
697
+
698
+ Symlinks point to `dist/skills/`, so any `bun run build` automatically updates what Claude Code sees. No manual steps needed.
699
+
700
+ If you reorganize prompts (rename, delete, add), the build script regenerates all stubs and the `--sync` flag updates symlinks accordingly. Stale symlinks pointing to removed stubs are cleaned up automatically.
701
+
702
+ ### Setup Integration
703
+
704
+ `pai setup` (Step 7) runs the same symlink logic interactively, asking before creating symlinks. It also cleans up legacy symlinks from the old `~/.claude/skills/user/` location.
705
+
706
+ ---
707
+
645
708
  ## Templates
646
709
 
647
710
  PAI ships three templates used during setup and customizable for your workflow.
@@ -854,6 +917,7 @@ bun run lint # tsc --noEmit
854
917
  | `dist/daemon/index.mjs` | Daemon server |
855
918
  | `dist/daemon-mcp/index.mjs` | MCP shim (stdio → daemon socket) |
856
919
  | `dist/hooks/*.mjs` | Compiled lifecycle hooks |
920
+ | `dist/skills/<Name>/SKILL.md` | Generated skill stubs (symlinked to ~/.claude/skills/) |
857
921
 
858
922
  ### Source Structure
859
923
 
@@ -885,6 +949,7 @@ src/
885
949
  ├── daemon-mcp/
886
950
  │ ├── instructions.ts # MCP server instructions (~1.5KB routing table)
887
951
  │ ├── prompts/ # 18 on-demand skill prompts
952
+ │ │ └── custom/ # User-created prompts (gitignored)
888
953
  │ ├── resources/ # 11 reference resources (pai:// URIs)
889
954
  │ └── index.ts # MCP shim entry point (stdio → socket)
890
955
  ├── hooks/
@@ -15,7 +15,7 @@ import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
15
15
  import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, t as CONFIG_DIR } from "../config-BuhHWyOK.mjs";
16
16
  import { t as createStorageBackend } from "../factory-Ygqe_bVZ.mjs";
17
17
  import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
18
- import { homedir, tmpdir } from "node:os";
18
+ import { homedir, platform, tmpdir } from "node:os";
19
19
  import { basename, dirname, join, relative, resolve } from "node:path";
20
20
  import chalk from "chalk";
21
21
  import { Command } from "commander";
@@ -4310,6 +4310,18 @@ function getDistHooksDir() {
4310
4310
  for (const candidate of candidates) if (existsSync(join(candidate, "stop-hook.mjs"))) return candidate;
4311
4311
  return fromModule;
4312
4312
  }
4313
+ function getDistDir() {
4314
+ const moduleDir = new URL(".", import.meta.url).pathname;
4315
+ const fromModule = join(moduleDir, "..", "..", "..");
4316
+ const candidates = [
4317
+ fromModule,
4318
+ join(process.cwd(), "dist"),
4319
+ join(homedir(), "dev", "ai", "PAI", "dist"),
4320
+ join("/", "usr", "local", "lib", "node_modules", "@tekmidian", "pai", "dist")
4321
+ ];
4322
+ for (const candidate of candidates) if (existsSync(join(candidate, "skills"))) return candidate;
4323
+ return fromModule;
4324
+ }
4313
4325
  function getStatuslineScript() {
4314
4326
  const candidates = [
4315
4327
  join(process.cwd(), "statusline-command.sh"),
@@ -4685,6 +4697,89 @@ async function stepAiSteeringRules(rl) {
4685
4697
  return true;
4686
4698
  }
4687
4699
 
4700
+ //#endregion
4701
+ //#region src/cli/commands/setup/steps/07-skill-stubs.ts
4702
+ /**
4703
+ * Step 7: Install PAI MCP skill stubs to ~/.claude/skills/.
4704
+ *
4705
+ * Each PAI MCP prompt has a SKILL.md stub generated at build time
4706
+ * (dist/skills/<Name>/SKILL.md). This step symlinks (macOS/Linux) or
4707
+ * copies (Windows) them into Claude Code's skill discovery directory.
4708
+ *
4709
+ * Skills MUST live at ~/.claude/skills/<Name>/SKILL.md (top level),
4710
+ * NOT under a user/ subdirectory — Claude Code only scans one level deep.
4711
+ *
4712
+ * Symlinks keep the stubs in sync — rebuilding PAI updates them automatically.
4713
+ * Existing non-symlink directories with the same name are never overwritten.
4714
+ */
4715
+ async function stepSkillStubs(rl) {
4716
+ section("Step 7: MCP Skill Stubs");
4717
+ line$1();
4718
+ line$1(" PAI MCP prompts need SKILL.md stubs so Claude Code can discover them.");
4719
+ line$1(" Stubs are symlinked from dist/skills/ — rebuilding PAI keeps them in sync.");
4720
+ line$1();
4721
+ const distSkills = join(getDistDir(), "skills");
4722
+ if (!existsSync(distSkills)) {
4723
+ console.log(c.warn("dist/skills/ not found — run `bun run build` first."));
4724
+ return false;
4725
+ }
4726
+ const skillNames = readdirSync(distSkills).filter((name) => {
4727
+ return existsSync(join(join(distSkills, name), "SKILL.md"));
4728
+ });
4729
+ if (skillNames.length === 0) {
4730
+ console.log(c.warn("No skill stubs found in dist/skills/."));
4731
+ return false;
4732
+ }
4733
+ console.log(c.dim(` Found ${skillNames.length} skill stubs to install.`));
4734
+ line$1();
4735
+ if (!await promptYesNo(rl, `Symlink ${skillNames.length} PAI skill stubs to ~/.claude/skills/?`, true)) {
4736
+ console.log(c.dim(" Skipping skill stub installation."));
4737
+ return false;
4738
+ }
4739
+ const targetBase = join(homedir(), ".claude", "skills");
4740
+ mkdirSync(targetBase, { recursive: true });
4741
+ const oldUserBase = join(targetBase, "user");
4742
+ if (existsSync(oldUserBase)) for (const name of skillNames) {
4743
+ const oldTarget = join(oldUserBase, name);
4744
+ if (existsSync(oldTarget) && lstatSync(oldTarget).isSymbolicLink()) unlinkSync(oldTarget);
4745
+ }
4746
+ const useSymlinks = platform() !== "win32";
4747
+ let installed = 0;
4748
+ let skipped = 0;
4749
+ let updated = 0;
4750
+ for (const name of skillNames) {
4751
+ const source = resolve(join(distSkills, name));
4752
+ const target = join(targetBase, name);
4753
+ if (existsSync(target) && !lstatSync(target).isSymbolicLink()) {
4754
+ console.log(c.dim(` SKIP ${name} (existing user skill, not a symlink)`));
4755
+ skipped++;
4756
+ continue;
4757
+ }
4758
+ if (existsSync(target) && lstatSync(target).isSymbolicLink()) {
4759
+ if (resolve(readlinkSync(target)) === source) {
4760
+ installed++;
4761
+ continue;
4762
+ }
4763
+ unlinkSync(target);
4764
+ updated++;
4765
+ }
4766
+ if (useSymlinks) symlinkSync(source, target);
4767
+ else {
4768
+ mkdirSync(target, { recursive: true });
4769
+ copyFileSync(join(source, "SKILL.md"), join(target, "SKILL.md"));
4770
+ }
4771
+ installed++;
4772
+ }
4773
+ line$1();
4774
+ const method = useSymlinks ? "symlinked" : "copied";
4775
+ if (updated > 0) console.log(c.ok(`${installed} stubs ${method}, ${updated} updated, ${skipped} skipped (user skills).`));
4776
+ else {
4777
+ console.log(c.ok(`${installed} skill stubs ${method} to ~/.claude/skills/.`));
4778
+ if (skipped > 0) console.log(c.dim(` ${skipped} skipped (existing user skills not managed by PAI).`));
4779
+ }
4780
+ return true;
4781
+ }
4782
+
4688
4783
  //#endregion
4689
4784
  //#region src/cli/commands/setup/steps/08-hooks.ts
4690
4785
  /** Step 7: Shell lifecycle hooks installation (pre-compact, session-stop, statusline). */
@@ -5317,7 +5412,7 @@ async function stepInitialIndex(rl) {
5317
5412
  //#endregion
5318
5413
  //#region src/cli/commands/setup/steps/15-verify.ts
5319
5414
  /** Step 13: Setup summary — displays all configuration choices made during setup. */
5320
- function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered) {
5415
+ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered) {
5321
5416
  section("Setup Complete");
5322
5417
  line$1();
5323
5418
  console.log(chalk.green(" PAI Knowledge OS is configured!"));
@@ -5331,6 +5426,7 @@ function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, aiStee
5331
5426
  console.log(chalk.dim(" CLAUDE.md: ") + chalk.cyan(claudeMdGenerated ? "~/.claude/CLAUDE.md (generated)" : "(unchanged)"));
5332
5427
  console.log(chalk.dim(" PAI skill: ") + chalk.cyan(paiSkillInstalled ? "~/.claude/skills/PAI/SKILL.md (installed)" : "(unchanged)"));
5333
5428
  console.log(chalk.dim(" Steering rules: ") + chalk.cyan(aiSteeringRulesInstalled ? "~/.claude/skills/PAI/AI-STEERING-RULES.md (installed)" : "(unchanged)"));
5429
+ console.log(chalk.dim(" Skill stubs: ") + chalk.cyan(skillStubsInstalled ? "18 MCP prompt stubs symlinked to ~/.claude/skills/" : "(unchanged)"));
5334
5430
  console.log(chalk.dim(" Hooks (shell): ") + chalk.cyan(hooksInstalled ? "pai-pre-compact.sh, pai-session-stop.sh (installed)" : "(unchanged)"));
5335
5431
  console.log(chalk.dim(" Hooks (TS): ") + chalk.cyan(tsHooksInstalled ? "14 .mjs hooks installed to ~/.claude/Hooks/" : "(unchanged)"));
5336
5432
  console.log(chalk.dim(" Assistant name: ") + chalk.cyan(daName));
@@ -5394,6 +5490,7 @@ async function runSetup() {
5394
5490
  const claudeMdGenerated = await stepClaudeMd(rl);
5395
5491
  const paiSkillInstalled = await stepPaiSkill(rl);
5396
5492
  const aiSteeringRulesInstalled = await stepAiSteeringRules(rl);
5493
+ const skillStubsInstalled = await stepSkillStubs(rl);
5397
5494
  const hooksInstalled = await stepHooks(rl);
5398
5495
  const tsHooksInstalled = await stepTsHooks(rl);
5399
5496
  const daName = await stepDaName(rl);
@@ -5409,7 +5506,7 @@ async function runSetup() {
5409
5506
  line$1();
5410
5507
  console.log(chalk.green(" Configuration saved."));
5411
5508
  await stepInitialIndex(rl);
5412
- stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered);
5509
+ stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, aiSteeringRulesInstalled, skillStubsInstalled, hooksInstalled, tsHooksInstalled, settingsPatched, daName, daemonInstalled, mcpRegistered);
5413
5510
  } finally {
5414
5511
  rl.close();
5415
5512
  }