@tekmidian/pai 0.6.4 → 0.6.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  "pai-daemon-mcp": "dist/daemon-mcp/index.mjs"
49
49
  },
50
50
  "scripts": {
51
- "build": "tsdown && node scripts/build-hooks.mjs && node scripts/build-skill-stubs.mjs --sync",
51
+ "build": "tsdown && node scripts/build-hooks.mjs --sync && node scripts/build-skill-stubs.mjs --sync",
52
52
  "dev": "tsdown --watch",
53
53
  "test": "vitest",
54
54
  "lint": "tsc --noEmit",
@@ -4,14 +4,33 @@
4
4
  *
5
5
  * Each hook is fully self-contained — lib/ dependencies are inlined.
6
6
  * Output: dist/hooks/<name>.mjs with #!/usr/bin/env node shebang.
7
+ *
8
+ * With --sync: also creates/updates symlinks (or copies on Windows) from
9
+ * ~/.claude/Hooks/ and ~/.claude/ to the built/source files. This ensures
10
+ * that `bun run build` is the only step needed to deploy hook updates.
7
11
  */
8
12
 
9
13
  import { buildSync } from "esbuild";
10
- import { readdirSync, statSync, chmodSync } from "fs";
11
- import { join, relative, basename } from "path";
14
+ import {
15
+ readdirSync,
16
+ statSync,
17
+ chmodSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ symlinkSync,
21
+ lstatSync,
22
+ readlinkSync,
23
+ unlinkSync,
24
+ copyFileSync,
25
+ readFileSync,
26
+ writeFileSync,
27
+ } from "fs";
28
+ import { join, resolve, basename } from "path";
29
+ import { homedir, platform } from "os";
12
30
 
13
31
  const HOOKS_SRC = "src/hooks/ts";
14
32
  const HOOKS_OUT = "dist/hooks";
33
+ const doSync = process.argv.includes("--sync");
15
34
 
16
35
  // Collect all .ts entry points (skip lib/ — those are bundled into each hook)
17
36
  function collectEntryPoints(dir) {
@@ -49,3 +68,97 @@ for (const entry of entryPoints) {
49
68
  }
50
69
 
51
70
  console.log(`✔ ${entryPoints.length} hooks built to ${HOOKS_OUT}/`);
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // --sync: Symlink (or copy on Windows) all deployable files to ~/.claude/
74
+ // ---------------------------------------------------------------------------
75
+
76
+ if (doSync) {
77
+ const useSymlinks = platform() !== "win32";
78
+ const claudeDir = join(homedir(), ".claude");
79
+ const hooksTarget = join(claudeDir, "Hooks");
80
+ mkdirSync(hooksTarget, { recursive: true });
81
+
82
+ let created = 0;
83
+ let updated = 0;
84
+ let current = 0;
85
+
86
+ /**
87
+ * Ensure `target` is a symlink (or copy on Windows) pointing to `source`.
88
+ * Replaces stale symlinks and plain-file copies with correct symlinks.
89
+ * Never overwrites non-symlink, non-PAI files (user's own scripts).
90
+ */
91
+ function syncFile(source, target) {
92
+ const absSource = resolve(source);
93
+
94
+ if (!existsSync(absSource)) {
95
+ console.warn(` ⚠ Source not found: ${source}`);
96
+ return;
97
+ }
98
+
99
+ // Check existing target (lstat doesn't follow symlinks)
100
+ let isUpdate = false;
101
+ try {
102
+ const stat = lstatSync(target);
103
+ if (stat.isSymbolicLink()) {
104
+ if (resolve(readlinkSync(target)) === absSource) {
105
+ current++;
106
+ return;
107
+ }
108
+ unlinkSync(target);
109
+ isUpdate = true;
110
+ } else if (stat.isFile()) {
111
+ unlinkSync(target);
112
+ isUpdate = true;
113
+ } else {
114
+ return; // Directory or something unexpected — don't touch
115
+ }
116
+ } catch {
117
+ // Target doesn't exist — fresh install
118
+ }
119
+
120
+ if (useSymlinks) {
121
+ symlinkSync(absSource, target);
122
+ } else {
123
+ copyFileSync(absSource, target);
124
+ chmodSync(target, 0o755);
125
+ }
126
+
127
+ if (isUpdate) {
128
+ updated++;
129
+ } else {
130
+ created++;
131
+ }
132
+ }
133
+
134
+ // 1. TypeScript hooks: dist/hooks/*.mjs → ~/.claude/Hooks/*.mjs
135
+ const mjsFiles = readdirSync(HOOKS_OUT).filter((f) => f.endsWith(".mjs"));
136
+ for (const filename of mjsFiles) {
137
+ syncFile(join(HOOKS_OUT, filename), join(hooksTarget, filename));
138
+ }
139
+
140
+ // 2. Shell hooks: src/hooks/*.sh → ~/.claude/Hooks/pai-*.sh
141
+ const shellHooks = [
142
+ ["src/hooks/pre-compact.sh", "pai-pre-compact.sh"],
143
+ ["src/hooks/session-stop.sh", "pai-session-stop.sh"],
144
+ ];
145
+ for (const [src, destName] of shellHooks) {
146
+ if (existsSync(src)) {
147
+ syncFile(src, join(hooksTarget, destName));
148
+ }
149
+ }
150
+
151
+ // 3. Root scripts: statusline + tab-color → ~/.claude/
152
+ const rootScripts = ["statusline-command.sh", "tab-color-command.sh"];
153
+ for (const script of rootScripts) {
154
+ if (existsSync(script)) {
155
+ syncFile(script, join(claudeDir, script));
156
+ }
157
+ }
158
+
159
+ const parts = [];
160
+ if (created > 0) parts.push(`${created} created`);
161
+ if (updated > 0) parts.push(`${updated} updated`);
162
+ if (current > 0) parts.push(`${current} current`);
163
+ console.log(`✔ Hook symlinks synced: ${parts.join(", ")}`);
164
+ }
@@ -406,11 +406,15 @@ if [ -f "$usage_cache" ]; then
406
406
  elapsed_secs=$((window_secs - remaining_secs))
407
407
  # Expected usage if spending linearly: elapsed/total * 100
408
408
  expected_pct=$(( elapsed_secs * 100 / window_secs ))
409
- # Daily pace: actual spend per day vs budget (100/7 ≈ 14%/day)
409
+ # Daily pace: actual spend/day vs dynamic budget
410
+ # Budget = remaining capacity / remaining days (not static 100/7)
410
411
  elapsed_days_x10=$((elapsed_secs * 10 / 86400))
411
412
  [ "$elapsed_days_x10" -lt 1 ] && elapsed_days_x10=1
412
413
  spend_per_day=$((seven_day_int * 10 / elapsed_days_x10))
413
- budget_per_day=14 # 100/7 ≈ 14%
414
+ remaining_days_x10=$((remaining_secs * 10 / 86400))
415
+ [ "$remaining_days_x10" -lt 1 ] && remaining_days_x10=1
416
+ remaining_budget=$((100 - seven_day_int))
417
+ budget_per_day=$((remaining_budget * 10 / remaining_days_x10))
414
418
  # Color: green = under budget, orange = near budget, red = over budget
415
419
  overspend=$((spend_per_day - budget_per_day))
416
420
  if [ "$overspend" -le -3 ] 2>/dev/null; then