@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/dist/cli/index.mjs +55 -37
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +2 -2
- package/scripts/build-hooks.mjs +115 -2
- package/statusline-command.sh +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekmidian/pai",
|
|
3
|
-
"version": "0.6.
|
|
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",
|
package/scripts/build-hooks.mjs
CHANGED
|
@@ -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 {
|
|
11
|
-
|
|
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
|
+
}
|
package/statusline-command.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|