@tekmidian/pai 0.3.2 → 0.4.0
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 +279 -21
- package/dist/cli/index.mjs.map +1 -1
- package/dist/hooks/capture-all-events.mjs +238 -0
- package/dist/hooks/capture-all-events.mjs.map +7 -0
- package/dist/hooks/capture-session-summary.mjs +198 -0
- package/dist/hooks/capture-session-summary.mjs.map +7 -0
- package/dist/hooks/capture-tool-output.mjs +105 -0
- package/dist/hooks/capture-tool-output.mjs.map +7 -0
- package/dist/hooks/cleanup-session-files.mjs +129 -0
- package/dist/hooks/cleanup-session-files.mjs.map +7 -0
- package/dist/hooks/context-compression-hook.mjs +283 -0
- package/dist/hooks/context-compression-hook.mjs.map +7 -0
- package/dist/hooks/initialize-session.mjs +206 -0
- package/dist/hooks/initialize-session.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +110 -0
- package/dist/hooks/load-core-context.mjs.map +7 -0
- package/dist/hooks/load-project-context.mjs +548 -0
- package/dist/hooks/load-project-context.mjs.map +7 -0
- package/dist/hooks/security-validator.mjs +159 -0
- package/dist/hooks/security-validator.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +625 -0
- package/dist/hooks/stop-hook.mjs.map +7 -0
- package/dist/hooks/subagent-stop-hook.mjs +152 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
- package/dist/hooks/sync-todo-to-md.mjs +322 -0
- package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
- package/dist/hooks/update-tab-on-action.mjs +90 -0
- package/dist/hooks/update-tab-on-action.mjs.map +7 -0
- package/dist/hooks/update-tab-titles.mjs +55 -0
- package/dist/hooks/update-tab-titles.mjs.map +7 -0
- package/package.json +4 -2
- package/scripts/build-hooks.mjs +51 -0
- package/src/hooks/ts/capture-all-events.ts +179 -0
- package/src/hooks/ts/lib/detect-environment.ts +53 -0
- package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
- package/src/hooks/ts/lib/pai-paths.ts +124 -0
- package/src/hooks/ts/lib/project-utils.ts +914 -0
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
- package/src/hooks/ts/session-start/initialize-session.ts +155 -0
- package/src/hooks/ts/session-start/load-core-context.ts +104 -0
- package/src/hooks/ts/session-start/load-project-context.ts +394 -0
- package/src/hooks/ts/stop/stop-hook.ts +407 -0
- package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
- package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
- package/tab-color-command.sh +24 -0
- package/templates/skills/createskill-skill.template.md +78 -0
- package/templates/skills/history-system.template.md +371 -0
- package/templates/skills/hook-system.template.md +913 -0
- package/templates/skills/sessions-skill.template.md +102 -0
- package/templates/skills/skill-system.template.md +214 -0
- package/templates/skills/terminal-tabs.template.md +120 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/hooks/ts/session-start/load-project-context.ts
|
|
10
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
|
|
11
|
+
import { join as join3, basename as basename2, dirname } from "path";
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
|
|
14
|
+
// src/hooks/ts/lib/project-utils.ts
|
|
15
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
16
|
+
import { join as join2, basename } from "path";
|
|
17
|
+
|
|
18
|
+
// src/hooks/ts/lib/pai-paths.ts
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { resolve, join } from "path";
|
|
21
|
+
import { existsSync, readFileSync } from "fs";
|
|
22
|
+
function loadEnvFile() {
|
|
23
|
+
const possiblePaths = [
|
|
24
|
+
resolve(process.env.PAI_DIR || "", ".env"),
|
|
25
|
+
resolve(homedir(), ".claude", ".env")
|
|
26
|
+
];
|
|
27
|
+
for (const envPath of possiblePaths) {
|
|
28
|
+
if (existsSync(envPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const content = readFileSync(envPath, "utf-8");
|
|
31
|
+
for (const line of content.split("\n")) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
34
|
+
const eqIndex = trimmed.indexOf("=");
|
|
35
|
+
if (eqIndex > 0) {
|
|
36
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
37
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
38
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
39
|
+
value = value.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
value = value.replace(/\$HOME/g, homedir());
|
|
42
|
+
value = value.replace(/^~(?=\/|$)/, homedir());
|
|
43
|
+
if (process.env[key] === void 0) {
|
|
44
|
+
process.env[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
loadEnvFile();
|
|
55
|
+
var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(homedir(), ".claude");
|
|
56
|
+
var HOOKS_DIR = join(PAI_DIR, "Hooks");
|
|
57
|
+
var SKILLS_DIR = join(PAI_DIR, "Skills");
|
|
58
|
+
var AGENTS_DIR = join(PAI_DIR, "Agents");
|
|
59
|
+
var HISTORY_DIR = join(PAI_DIR, "History");
|
|
60
|
+
var COMMANDS_DIR = join(PAI_DIR, "Commands");
|
|
61
|
+
function validatePAIStructure() {
|
|
62
|
+
if (!existsSync(PAI_DIR)) {
|
|
63
|
+
console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
|
|
64
|
+
console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
68
|
+
console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
|
|
69
|
+
console.error(` Your PAI_DIR may be misconfigured`);
|
|
70
|
+
console.error(` Current PAI_DIR: ${PAI_DIR}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
validatePAIStructure();
|
|
75
|
+
|
|
76
|
+
// src/hooks/ts/lib/project-utils.ts
|
|
77
|
+
var PROJECTS_DIR = join2(PAI_DIR, "projects");
|
|
78
|
+
function encodePath(path) {
|
|
79
|
+
return path.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
80
|
+
}
|
|
81
|
+
function getProjectDir(cwd) {
|
|
82
|
+
const encoded = encodePath(cwd);
|
|
83
|
+
return join2(PROJECTS_DIR, encoded);
|
|
84
|
+
}
|
|
85
|
+
function getNotesDir(cwd) {
|
|
86
|
+
return join2(getProjectDir(cwd), "Notes");
|
|
87
|
+
}
|
|
88
|
+
function findNotesDir(cwd) {
|
|
89
|
+
const cwdBasename = basename(cwd).toLowerCase();
|
|
90
|
+
if (cwdBasename === "notes" && existsSync2(cwd)) {
|
|
91
|
+
return { path: cwd, isLocal: true };
|
|
92
|
+
}
|
|
93
|
+
const localPaths = [
|
|
94
|
+
join2(cwd, "Notes"),
|
|
95
|
+
join2(cwd, "notes"),
|
|
96
|
+
join2(cwd, ".claude", "Notes")
|
|
97
|
+
];
|
|
98
|
+
for (const path of localPaths) {
|
|
99
|
+
if (existsSync2(path)) {
|
|
100
|
+
return { path, isLocal: true };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { path: getNotesDir(cwd), isLocal: false };
|
|
104
|
+
}
|
|
105
|
+
function isWhatsAppEnabled() {
|
|
106
|
+
try {
|
|
107
|
+
const { homedir: homedir2 } = __require("os");
|
|
108
|
+
const settingsPath = join2(homedir2(), ".claude", "settings.json");
|
|
109
|
+
if (!existsSync2(settingsPath)) return false;
|
|
110
|
+
const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
111
|
+
const enabled = settings.enabledMcpjsonServers || [];
|
|
112
|
+
return enabled.includes("whazaa");
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function sendNtfyNotification(message, retries = 2) {
|
|
118
|
+
if (isWhatsAppEnabled()) {
|
|
119
|
+
console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const topic = process.env.NTFY_TOPIC;
|
|
123
|
+
if (!topic) {
|
|
124
|
+
console.error("NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled");
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch(`https://ntfy.sh/${topic}`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: message,
|
|
132
|
+
headers: {
|
|
133
|
+
"Title": "Claude Code",
|
|
134
|
+
"Priority": "default"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (response.ok) {
|
|
138
|
+
console.error(`ntfy.sh notification sent (WhatsApp inactive): "${message}"`);
|
|
139
|
+
return true;
|
|
140
|
+
} else {
|
|
141
|
+
console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);
|
|
145
|
+
}
|
|
146
|
+
if (attempt < retries) {
|
|
147
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
console.error("ntfy.sh notification failed after all retries");
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function getMonthDir(notesDir) {
|
|
154
|
+
const now = /* @__PURE__ */ new Date();
|
|
155
|
+
const year = String(now.getFullYear());
|
|
156
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
157
|
+
const monthDir = join2(notesDir, year, month);
|
|
158
|
+
if (!existsSync2(monthDir)) {
|
|
159
|
+
mkdirSync(monthDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
return monthDir;
|
|
162
|
+
}
|
|
163
|
+
function getNextNoteNumber(notesDir) {
|
|
164
|
+
const monthDir = getMonthDir(notesDir);
|
|
165
|
+
const files = readdirSync(monthDir).filter((f) => f.match(/^\d{3,4}[\s_-]/)).filter((f) => f.endsWith(".md")).sort();
|
|
166
|
+
if (files.length === 0) {
|
|
167
|
+
return "0001";
|
|
168
|
+
}
|
|
169
|
+
let maxNumber = 0;
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const digitMatch = file.match(/^(\d+)/);
|
|
172
|
+
if (digitMatch) {
|
|
173
|
+
const num = parseInt(digitMatch[1], 10);
|
|
174
|
+
if (num > maxNumber) maxNumber = num;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return String(maxNumber + 1).padStart(4, "0");
|
|
178
|
+
}
|
|
179
|
+
function getCurrentNotePath(notesDir) {
|
|
180
|
+
if (!existsSync2(notesDir)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const findLatestIn = (dir) => {
|
|
184
|
+
if (!existsSync2(dir)) return null;
|
|
185
|
+
const files = readdirSync(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
|
|
186
|
+
const numA = parseInt(a.match(/^(\d+)/)?.[1] || "0", 10);
|
|
187
|
+
const numB = parseInt(b.match(/^(\d+)/)?.[1] || "0", 10);
|
|
188
|
+
return numA - numB;
|
|
189
|
+
});
|
|
190
|
+
if (files.length === 0) return null;
|
|
191
|
+
return join2(dir, files[files.length - 1]);
|
|
192
|
+
};
|
|
193
|
+
const now = /* @__PURE__ */ new Date();
|
|
194
|
+
const year = String(now.getFullYear());
|
|
195
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
196
|
+
const currentMonthDir = join2(notesDir, year, month);
|
|
197
|
+
const found = findLatestIn(currentMonthDir);
|
|
198
|
+
if (found) return found;
|
|
199
|
+
const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
200
|
+
const prevYear = String(prevDate.getFullYear());
|
|
201
|
+
const prevMonth = String(prevDate.getMonth() + 1).padStart(2, "0");
|
|
202
|
+
const prevMonthDir = join2(notesDir, prevYear, prevMonth);
|
|
203
|
+
const prevFound = findLatestIn(prevMonthDir);
|
|
204
|
+
if (prevFound) return prevFound;
|
|
205
|
+
return findLatestIn(notesDir);
|
|
206
|
+
}
|
|
207
|
+
function createSessionNote(notesDir, description) {
|
|
208
|
+
const noteNumber = getNextNoteNumber(notesDir);
|
|
209
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
210
|
+
const safeDescription = "New Session";
|
|
211
|
+
const monthDir = getMonthDir(notesDir);
|
|
212
|
+
const filename = `${noteNumber} - ${date} - ${safeDescription}.md`;
|
|
213
|
+
const filepath = join2(monthDir, filename);
|
|
214
|
+
const content = `# Session ${noteNumber}: ${description}
|
|
215
|
+
|
|
216
|
+
**Date:** ${date}
|
|
217
|
+
**Status:** In Progress
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Work Done
|
|
222
|
+
|
|
223
|
+
<!-- PAI will add completed work here during session -->
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Next Steps
|
|
228
|
+
|
|
229
|
+
<!-- To be filled at session end -->
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
**Tags:** #Session
|
|
234
|
+
`;
|
|
235
|
+
writeFileSync(filepath, content);
|
|
236
|
+
console.error(`Created session note: ${filename}`);
|
|
237
|
+
return filepath;
|
|
238
|
+
}
|
|
239
|
+
function findTodoPath(cwd) {
|
|
240
|
+
const localPaths = [
|
|
241
|
+
join2(cwd, "TODO.md"),
|
|
242
|
+
join2(cwd, "notes", "TODO.md"),
|
|
243
|
+
join2(cwd, "Notes", "TODO.md"),
|
|
244
|
+
join2(cwd, ".claude", "TODO.md")
|
|
245
|
+
];
|
|
246
|
+
for (const path of localPaths) {
|
|
247
|
+
if (existsSync2(path)) {
|
|
248
|
+
return path;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return join2(getNotesDir(cwd), "TODO.md");
|
|
252
|
+
}
|
|
253
|
+
function findAllClaudeMdPaths(cwd) {
|
|
254
|
+
const foundPaths = [];
|
|
255
|
+
const localPaths = [
|
|
256
|
+
join2(cwd, ".claude", "CLAUDE.md"),
|
|
257
|
+
join2(cwd, "CLAUDE.md"),
|
|
258
|
+
join2(cwd, "Notes", "CLAUDE.md"),
|
|
259
|
+
join2(cwd, "notes", "CLAUDE.md"),
|
|
260
|
+
join2(cwd, "Prompts", "CLAUDE.md"),
|
|
261
|
+
join2(cwd, "prompts", "CLAUDE.md")
|
|
262
|
+
];
|
|
263
|
+
for (const path of localPaths) {
|
|
264
|
+
if (existsSync2(path)) {
|
|
265
|
+
foundPaths.push(path);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return foundPaths;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/hooks/ts/session-start/load-project-context.ts
|
|
272
|
+
function findPaiBinary() {
|
|
273
|
+
try {
|
|
274
|
+
return execSync("which pai", { encoding: "utf-8" }).trim();
|
|
275
|
+
} catch {
|
|
276
|
+
const fallbacks = [
|
|
277
|
+
"/usr/local/bin/pai",
|
|
278
|
+
"/opt/homebrew/bin/pai",
|
|
279
|
+
`${process.env.HOME}/.local/bin/pai`
|
|
280
|
+
];
|
|
281
|
+
for (const p of fallbacks) {
|
|
282
|
+
if (existsSync3(p)) return p;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return "pai";
|
|
286
|
+
}
|
|
287
|
+
function getRoutedNotesPath() {
|
|
288
|
+
const routingFile = join3(PAI_DIR, "session-routing.json");
|
|
289
|
+
if (!existsSync3(routingFile)) return null;
|
|
290
|
+
try {
|
|
291
|
+
const routing = JSON.parse(readFileSync3(routingFile, "utf-8"));
|
|
292
|
+
const active = routing?.active_session;
|
|
293
|
+
if (active?.notes_path) {
|
|
294
|
+
return active.notes_path;
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
async function main() {
|
|
301
|
+
console.error("\nload-project-context.ts starting...");
|
|
302
|
+
let hookInput = null;
|
|
303
|
+
try {
|
|
304
|
+
const chunks = [];
|
|
305
|
+
for await (const chunk of process.stdin) {
|
|
306
|
+
chunks.push(chunk);
|
|
307
|
+
}
|
|
308
|
+
const input = Buffer.concat(chunks).toString("utf-8");
|
|
309
|
+
if (input.trim()) {
|
|
310
|
+
hookInput = JSON.parse(input);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error("Could not parse hook input, using process.cwd()");
|
|
314
|
+
}
|
|
315
|
+
const cwd = hookInput?.cwd || process.cwd();
|
|
316
|
+
let projectName = basename2(cwd);
|
|
317
|
+
if (projectName.toLowerCase() === "notes") {
|
|
318
|
+
projectName = basename2(dirname(cwd));
|
|
319
|
+
}
|
|
320
|
+
console.error(`Working directory: ${cwd}`);
|
|
321
|
+
console.error(`Project: ${projectName}`);
|
|
322
|
+
const isSubagent = process.env.CLAUDE_AGENT_TYPE !== void 0 || (process.env.CLAUDE_PROJECT_DIR || "").includes("/.claude/agents/");
|
|
323
|
+
if (isSubagent) {
|
|
324
|
+
console.error("Subagent session - skipping project context setup");
|
|
325
|
+
process.exit(0);
|
|
326
|
+
}
|
|
327
|
+
const claudeMdPaths = findAllClaudeMdPaths(cwd);
|
|
328
|
+
const claudeMdContents = [];
|
|
329
|
+
if (claudeMdPaths.length > 0) {
|
|
330
|
+
console.error(`Found ${claudeMdPaths.length} CLAUDE.md file(s):`);
|
|
331
|
+
for (const path of claudeMdPaths) {
|
|
332
|
+
console.error(` - ${path}`);
|
|
333
|
+
try {
|
|
334
|
+
const content = readFileSync3(path, "utf-8");
|
|
335
|
+
claudeMdContents.push({ path, content });
|
|
336
|
+
console.error(` Read ${content.length} chars`);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error(` Could not read: ${error}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
console.error("No CLAUDE.md found in project");
|
|
343
|
+
console.error(" Consider creating one at ./CLAUDE.md or ./.claude/CLAUDE.md");
|
|
344
|
+
}
|
|
345
|
+
const routedPath = getRoutedNotesPath();
|
|
346
|
+
let notesDir;
|
|
347
|
+
if (routedPath) {
|
|
348
|
+
const { mkdirSync: mkdirSync2 } = await import("fs");
|
|
349
|
+
if (!existsSync3(routedPath)) {
|
|
350
|
+
mkdirSync2(routedPath, { recursive: true });
|
|
351
|
+
console.error(`Created routed Notes: ${routedPath}`);
|
|
352
|
+
} else {
|
|
353
|
+
console.error(`Notes directory: ${routedPath} (routed via pai route)`);
|
|
354
|
+
}
|
|
355
|
+
notesDir = routedPath;
|
|
356
|
+
} else {
|
|
357
|
+
const notesInfo = findNotesDir(cwd);
|
|
358
|
+
if (notesInfo.isLocal) {
|
|
359
|
+
notesDir = notesInfo.path;
|
|
360
|
+
console.error(`Notes directory: ${notesDir} (local)`);
|
|
361
|
+
} else {
|
|
362
|
+
if (!existsSync3(notesInfo.path)) {
|
|
363
|
+
const { mkdirSync: mkdirSync2 } = await import("fs");
|
|
364
|
+
mkdirSync2(notesInfo.path, { recursive: true });
|
|
365
|
+
console.error(`Created central Notes: ${notesInfo.path}`);
|
|
366
|
+
} else {
|
|
367
|
+
console.error(`Notes directory: ${notesInfo.path} (central)`);
|
|
368
|
+
}
|
|
369
|
+
notesDir = notesInfo.path;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const projectDir = getProjectDir(cwd);
|
|
373
|
+
if (existsSync3(projectDir)) {
|
|
374
|
+
try {
|
|
375
|
+
const files = readdirSync2(projectDir);
|
|
376
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => ({
|
|
377
|
+
name: f,
|
|
378
|
+
path: join3(projectDir, f),
|
|
379
|
+
mtime: statSync(join3(projectDir, f)).mtime.getTime()
|
|
380
|
+
})).sort((a, b) => b.mtime - a.mtime);
|
|
381
|
+
if (jsonlFiles.length > 1) {
|
|
382
|
+
const { mkdirSync: mkdirSync2, renameSync: renameSync2 } = await import("fs");
|
|
383
|
+
const sessionsDir = join3(projectDir, "sessions");
|
|
384
|
+
if (!existsSync3(sessionsDir)) {
|
|
385
|
+
mkdirSync2(sessionsDir, { recursive: true });
|
|
386
|
+
}
|
|
387
|
+
for (let i = 1; i < jsonlFiles.length; i++) {
|
|
388
|
+
const file = jsonlFiles[i];
|
|
389
|
+
const destPath = join3(sessionsDir, file.name);
|
|
390
|
+
if (!existsSync3(destPath)) {
|
|
391
|
+
renameSync2(file.path, destPath);
|
|
392
|
+
console.error(`Moved old session: ${file.name} \u2192 sessions/`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error(`Could not cleanup old .jsonl files: ${error}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const todoPath = findTodoPath(cwd);
|
|
401
|
+
const hasTodo = existsSync3(todoPath);
|
|
402
|
+
if (hasTodo) {
|
|
403
|
+
console.error(`TODO.md: ${todoPath}`);
|
|
404
|
+
} else {
|
|
405
|
+
const newTodoPath = join3(notesDir, "TODO.md");
|
|
406
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
407
|
+
writeFileSync2(newTodoPath, `# TODO
|
|
408
|
+
|
|
409
|
+
## Offen
|
|
410
|
+
|
|
411
|
+
- [ ]
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
*Created: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
416
|
+
`);
|
|
417
|
+
console.error(`Created TODO.md: ${newTodoPath}`);
|
|
418
|
+
}
|
|
419
|
+
let activeNotePath = null;
|
|
420
|
+
if (notesDir) {
|
|
421
|
+
const currentNotePath = getCurrentNotePath(notesDir);
|
|
422
|
+
let needsNewNote = false;
|
|
423
|
+
if (!currentNotePath) {
|
|
424
|
+
needsNewNote = true;
|
|
425
|
+
console.error("\nNo previous session notes found - creating new one");
|
|
426
|
+
} else {
|
|
427
|
+
try {
|
|
428
|
+
const content = readFileSync3(currentNotePath, "utf-8");
|
|
429
|
+
if (content.includes("**Status:** Completed") || content.includes("**Completed:**")) {
|
|
430
|
+
needsNewNote = true;
|
|
431
|
+
console.error(`
|
|
432
|
+
Previous note completed - creating new one`);
|
|
433
|
+
const summaryMatch = content.match(/## Next Steps\n\n([^\n]+)/);
|
|
434
|
+
if (summaryMatch) {
|
|
435
|
+
console.error(` Previous: ${summaryMatch[1].substring(0, 60)}...`);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
console.error(`
|
|
439
|
+
Continuing session note: ${basename2(currentNotePath)}`);
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
needsNewNote = true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (needsNewNote) {
|
|
446
|
+
activeNotePath = createSessionNote(notesDir, projectName);
|
|
447
|
+
console.error(`Created: ${basename2(activeNotePath)}`);
|
|
448
|
+
} else {
|
|
449
|
+
activeNotePath = currentNotePath;
|
|
450
|
+
try {
|
|
451
|
+
const content = readFileSync3(activeNotePath, "utf-8");
|
|
452
|
+
const lines = content.split("\n").slice(0, 12);
|
|
453
|
+
console.error("--- Current Note Preview ---");
|
|
454
|
+
for (const line of lines) {
|
|
455
|
+
console.error(line);
|
|
456
|
+
}
|
|
457
|
+
console.error("--- End Preview ---\n");
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (existsSync3(todoPath)) {
|
|
463
|
+
try {
|
|
464
|
+
const todoContent = readFileSync3(todoPath, "utf-8");
|
|
465
|
+
const todoLines = todoContent.split("\n").filter((l) => l.includes("[ ]")).slice(0, 5);
|
|
466
|
+
if (todoLines.length > 0) {
|
|
467
|
+
console.error("\nOpen TODOs:");
|
|
468
|
+
for (const line of todoLines) {
|
|
469
|
+
console.error(` ${line.trim()}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
await sendNtfyNotification(`Session started in ${projectName}`);
|
|
476
|
+
const paiBin = findPaiBinary();
|
|
477
|
+
let paiProjectBlock = "";
|
|
478
|
+
try {
|
|
479
|
+
const { execFileSync } = await import("child_process");
|
|
480
|
+
const raw = execFileSync(paiBin, ["project", "detect", "--json", cwd], {
|
|
481
|
+
encoding: "utf-8",
|
|
482
|
+
env: process.env
|
|
483
|
+
}).trim();
|
|
484
|
+
if (raw) {
|
|
485
|
+
const detected = JSON.parse(raw);
|
|
486
|
+
if (detected.error === "no_match") {
|
|
487
|
+
paiProjectBlock = `PAI Project Registry: No registered project matches this directory.
|
|
488
|
+
Run "pai project add ." to register this project, or use /route to tag the session.`;
|
|
489
|
+
console.error("PAI detect: no match for", cwd);
|
|
490
|
+
} else if (detected.slug) {
|
|
491
|
+
const name = detected.display_name || detected.slug;
|
|
492
|
+
const nameSlug = ` (slug: ${detected.slug})`;
|
|
493
|
+
const matchDesc = detected.match_type === "exact" ? "exact" : `parent (+${detected.relative_path ?? ""})`;
|
|
494
|
+
const statusFlag = detected.status && detected.status !== "active" ? ` [${detected.status.toUpperCase()}]` : "";
|
|
495
|
+
paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}
|
|
496
|
+
Match: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== "active" ? `
|
|
497
|
+
WARNING: Project status is "${detected.status}". Run: pai project health --fix` : ""}`;
|
|
498
|
+
console.error(`PAI detect: matched "${detected.slug}" (${detected.match_type})`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (e) {
|
|
502
|
+
console.error("pai project detect failed:", e);
|
|
503
|
+
}
|
|
504
|
+
const reminder = `
|
|
505
|
+
<system-reminder>
|
|
506
|
+
PROJECT CONTEXT LOADED
|
|
507
|
+
|
|
508
|
+
Project: ${projectName}
|
|
509
|
+
Working Directory: ${cwd}
|
|
510
|
+
${notesDir ? `Notes Directory: ${notesDir}${routedPath ? " (routed via pai route)" : ""}` : "Notes: disabled (no local Notes/ directory)"}
|
|
511
|
+
${hasTodo ? `TODO: ${todoPath}` : "TODO: not found"}
|
|
512
|
+
${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(", ")}` : "No CLAUDE.md found"}
|
|
513
|
+
${activeNotePath ? `Active Note: ${basename2(activeNotePath)}` : ""}
|
|
514
|
+
${routedPath ? `
|
|
515
|
+
Note Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ""}
|
|
516
|
+
${paiProjectBlock ? `
|
|
517
|
+
${paiProjectBlock}` : ""}
|
|
518
|
+
Session Commands:
|
|
519
|
+
- "pause session" \u2192 Save checkpoint, update TODO, exit (no compact)
|
|
520
|
+
- "end session" \u2192 Finalize note, commit if needed, start fresh next time
|
|
521
|
+
- "pai route clear" \u2192 Clear note routing (in a new session)
|
|
522
|
+
</system-reminder>
|
|
523
|
+
`;
|
|
524
|
+
console.log(reminder);
|
|
525
|
+
for (const { path, content } of claudeMdContents) {
|
|
526
|
+
const claudeMdReminder = `
|
|
527
|
+
<system-reminder>
|
|
528
|
+
LOCAL CLAUDE.md LOADED (MANDATORY - READ AND FOLLOW)
|
|
529
|
+
|
|
530
|
+
Source: ${path}
|
|
531
|
+
|
|
532
|
+
${content}
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
THE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.
|
|
536
|
+
</system-reminder>
|
|
537
|
+
`;
|
|
538
|
+
console.log(claudeMdReminder);
|
|
539
|
+
console.error(`Injected CLAUDE.md content from: ${path}`);
|
|
540
|
+
}
|
|
541
|
+
console.error("\nProject context setup complete\n");
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
main().catch((error) => {
|
|
545
|
+
console.error("load-project-context.ts error:", error);
|
|
546
|
+
process.exit(0);
|
|
547
|
+
});
|
|
548
|
+
//# sourceMappingURL=load-project-context.mjs.map
|