@tekmidian/pai 0.8.4 → 0.9.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/ARCHITECTURE.md +121 -0
- package/FEATURE.md +5 -0
- package/README.md +54 -0
- package/dist/cli/index.mjs +11 -11
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon-nXyhvdzz.mjs → daemon-VIFoKc_z.mjs} +31 -6
- package/dist/daemon-VIFoKc_z.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +51 -0
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{factory-Ygqe_bVZ.mjs → factory-e0k1HWuc.mjs} +2 -2
- package/dist/{factory-Ygqe_bVZ.mjs.map → factory-e0k1HWuc.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs +276 -89
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/stop-hook.mjs +152 -2
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/{postgres-CKf-EDtS.mjs → postgres-DvEPooLO.mjs} +45 -10
- package/dist/postgres-DvEPooLO.mjs.map +1 -0
- package/dist/query-feedback-Dv43XKHM.mjs +76 -0
- package/dist/query-feedback-Dv43XKHM.mjs.map +1 -0
- package/dist/tools-C4SBZHga.mjs +1731 -0
- package/dist/tools-C4SBZHga.mjs.map +1 -0
- package/dist/{vault-indexer-Bi2cRmn7.mjs → vault-indexer-B-aJpRZC.mjs} +3 -2
- package/dist/{vault-indexer-Bi2cRmn7.mjs.map → vault-indexer-B-aJpRZC.mjs.map} +1 -1
- package/dist/{zettelkasten-cdajbnPr.mjs → zettelkasten-DhBKZQHF.mjs} +358 -3
- package/dist/zettelkasten-DhBKZQHF.mjs.map +1 -0
- package/package.json +1 -1
- package/src/hooks/ts/session-start/load-project-context.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +203 -1
- package/dist/daemon-nXyhvdzz.mjs.map +0 -1
- package/dist/postgres-CKf-EDtS.mjs.map +0 -1
- package/dist/tools-DcaJlYDN.mjs +0 -869
- package/dist/tools-DcaJlYDN.mjs.map +0 -1
- package/dist/zettelkasten-cdajbnPr.mjs.map +0 -1
|
@@ -15,7 +15,7 @@ async function createStorageBackend(config) {
|
|
|
15
15
|
}
|
|
16
16
|
async function tryPostgres(config) {
|
|
17
17
|
try {
|
|
18
|
-
const { PostgresBackend } = await import("./postgres-
|
|
18
|
+
const { PostgresBackend } = await import("./postgres-DvEPooLO.mjs");
|
|
19
19
|
const pgConfig = config.postgres ?? {};
|
|
20
20
|
await PostgresBackend.ensureDatabase(pgConfig);
|
|
21
21
|
const backend = new PostgresBackend(pgConfig);
|
|
@@ -41,4 +41,4 @@ async function createSQLiteBackend() {
|
|
|
41
41
|
|
|
42
42
|
//#endregion
|
|
43
43
|
export { factory_exports as n, createStorageBackend as t };
|
|
44
|
-
//# sourceMappingURL=factory-
|
|
44
|
+
//# sourceMappingURL=factory-e0k1HWuc.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-
|
|
1
|
+
{"version":3,"file":"factory-e0k1HWuc.mjs","names":[],"sources":["../src/storage/factory.ts"],"sourcesContent":["/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n * If Postgres is configured but unavailable, falls back to SQLite with\n * a warning log — the daemon never crashes due to a missing Postgres.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-fallback behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend if reachable, else SQLiteBackend\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await tryPostgres(config);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\nasync function tryPostgres(config: PaiDaemonConfig): Promise<StorageBackend> {\n try {\n const { PostgresBackend } = await import(\"./postgres.js\");\n const pgConfig = config.postgres ?? {};\n\n // Ensure the per-user database exists and has the schema applied\n await PostgresBackend.ensureDatabase(pgConfig);\n\n const backend = new PostgresBackend(pgConfig);\n\n const err = await backend.testConnection();\n if (err) {\n process.stderr.write(\n `[pai-daemon] Postgres unavailable (${err}). Falling back to SQLite.\\n`\n );\n await backend.close();\n return createSQLiteBackend();\n }\n\n process.stderr.write(\"[pai-daemon] Connected to PostgreSQL backend.\\n\");\n return backend;\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Postgres init error (${msg}). Falling back to SQLite.\\n`\n );\n return createSQLiteBackend();\n }\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const { openFederation } = await import(\"../memory/db.js\");\n const { SQLiteBackend } = await import(\"./sqlite.js\");\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n"],"mappings":";;;;;;;;;;;AAkBA,eAAsB,qBACpB,QACyB;AACzB,KAAI,OAAO,mBAAmB,WAC5B,QAAO,MAAM,YAAY,OAAO;AAIlC,QAAO,qBAAqB;;AAG9B,eAAe,YAAY,QAAkD;AAC3E,KAAI;EACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;EACzC,MAAM,WAAW,OAAO,YAAY,EAAE;AAGtC,QAAM,gBAAgB,eAAe,SAAS;EAE9C,MAAM,UAAU,IAAI,gBAAgB,SAAS;EAE7C,MAAM,MAAM,MAAM,QAAQ,gBAAgB;AAC1C,MAAI,KAAK;AACP,WAAQ,OAAO,MACb,sCAAsC,IAAI,8BAC3C;AACD,SAAM,QAAQ,OAAO;AACrB,UAAO,qBAAqB;;AAG9B,UAAQ,OAAO,MAAM,kDAAkD;AACvE,SAAO;UACA,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MACb,qCAAqC,IAAI,8BAC1C;AACD,SAAO,qBAAqB;;;AAIhC,eAAe,sBAA+C;CAC5D,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cADA,gBAAgB,CACC"}
|
|
@@ -1,28 +1,183 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/ts/session-start/load-project-context.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
6
|
-
import { homedir as
|
|
4
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync } from "fs";
|
|
5
|
+
import { join as join6, basename as basename4, dirname, resolve as resolve2 } from "path";
|
|
6
|
+
import { homedir as homedir4 } from "os";
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
8
|
|
|
9
|
+
// src/memory/wakeup.ts
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join, basename } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
var L1_TOKEN_BUDGET = 800;
|
|
14
|
+
var L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4;
|
|
15
|
+
var L1_MAX_NOTES = 10;
|
|
16
|
+
var EXTRACT_SECTIONS = [
|
|
17
|
+
"Work Done",
|
|
18
|
+
"Key Decisions",
|
|
19
|
+
"Next Steps",
|
|
20
|
+
"Checkpoint"
|
|
21
|
+
];
|
|
22
|
+
var IDENTITY_FILE = join(homedir(), ".pai", "identity.txt");
|
|
23
|
+
function loadL0Identity() {
|
|
24
|
+
if (!existsSync(IDENTITY_FILE)) return "";
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(IDENTITY_FILE, "utf-8").trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function findNotesDirForProject(rootPath) {
|
|
32
|
+
const localCandidates = [
|
|
33
|
+
join(rootPath, "Notes"),
|
|
34
|
+
join(rootPath, "notes"),
|
|
35
|
+
join(rootPath, ".claude", "Notes")
|
|
36
|
+
];
|
|
37
|
+
for (const p of localCandidates) {
|
|
38
|
+
if (existsSync(p)) return p;
|
|
39
|
+
}
|
|
40
|
+
const encoded = rootPath.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
41
|
+
const centralNotes = join(
|
|
42
|
+
homedir(),
|
|
43
|
+
".claude",
|
|
44
|
+
"projects",
|
|
45
|
+
encoded,
|
|
46
|
+
"Notes"
|
|
47
|
+
);
|
|
48
|
+
if (existsSync(centralNotes)) return centralNotes;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function findSessionNotes(notesDir) {
|
|
52
|
+
const result = [];
|
|
53
|
+
const scanDir = (dir) => {
|
|
54
|
+
if (!existsSync(dir)) return;
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const fullPath = join(dir, entry.name);
|
|
63
|
+
if (entry.isDir) {
|
|
64
|
+
scanDir(fullPath);
|
|
65
|
+
} else if (entry.name.match(/^\d{3,4}[\s_-].*\.md$/)) {
|
|
66
|
+
result.push(fullPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
scanDir(notesDir);
|
|
71
|
+
result.sort((a, b) => {
|
|
72
|
+
const numA = parseInt(basename(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
73
|
+
const numB = parseInt(basename(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
74
|
+
return numB - numA;
|
|
75
|
+
});
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function extractKeyLines(content, maxChars) {
|
|
79
|
+
const lines = content.split("\n");
|
|
80
|
+
const selected = [];
|
|
81
|
+
let inTargetSection = false;
|
|
82
|
+
let currentSection = "";
|
|
83
|
+
let charCount = 0;
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const h2Match = line.match(/^## (.+)$/);
|
|
86
|
+
const h3Match = line.match(/^### (.+)$/);
|
|
87
|
+
if (h2Match) {
|
|
88
|
+
currentSection = h2Match[1];
|
|
89
|
+
inTargetSection = EXTRACT_SECTIONS.some(
|
|
90
|
+
(s) => currentSection.toLowerCase().includes(s.toLowerCase())
|
|
91
|
+
);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (h3Match) {
|
|
95
|
+
if (inTargetSection) {
|
|
96
|
+
const label = `[${h3Match[1]}]`;
|
|
97
|
+
if (charCount + label.length < maxChars) {
|
|
98
|
+
selected.push(label);
|
|
99
|
+
charCount += label.length + 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!inTargetSection) continue;
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!trimmed || trimmed.startsWith("<!--") || trimmed === "---") continue;
|
|
107
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.match(/^\d+\./) || trimmed.startsWith("**")) {
|
|
108
|
+
if (charCount + trimmed.length + 1 > maxChars) break;
|
|
109
|
+
selected.push(trimmed);
|
|
110
|
+
charCount += trimmed.length + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return selected.join("\n");
|
|
114
|
+
}
|
|
115
|
+
function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
116
|
+
const charBudget = tokenBudget * 4;
|
|
117
|
+
const notesDir = findNotesDirForProject(rootPath);
|
|
118
|
+
if (!notesDir) return "";
|
|
119
|
+
const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);
|
|
120
|
+
if (noteFiles.length === 0) return "";
|
|
121
|
+
const sections = [];
|
|
122
|
+
let remaining = charBudget;
|
|
123
|
+
for (const noteFile of noteFiles) {
|
|
124
|
+
if (remaining <= 50) break;
|
|
125
|
+
let content;
|
|
126
|
+
try {
|
|
127
|
+
content = readFileSync(noteFile, "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const name = basename(noteFile);
|
|
132
|
+
const titleMatch = name.match(/^\d+ - (\d{4}-\d{2}-\d{2}) - (.+)\.md$/);
|
|
133
|
+
const dateLabel = titleMatch ? titleMatch[1] : "";
|
|
134
|
+
const titleLabel = titleMatch ? titleMatch[2] : name.replace(/^\d+ - /, "").replace(/\.md$/, "");
|
|
135
|
+
const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);
|
|
136
|
+
const extracted = extractKeyLines(content, perNoteChars);
|
|
137
|
+
if (!extracted) continue;
|
|
138
|
+
const noteBlock = `[${dateLabel} - ${titleLabel}]
|
|
139
|
+
${extracted}`;
|
|
140
|
+
sections.push(noteBlock);
|
|
141
|
+
remaining -= noteBlock.length + 1;
|
|
142
|
+
}
|
|
143
|
+
if (sections.length === 0) return "";
|
|
144
|
+
return sections.join("\n\n");
|
|
145
|
+
}
|
|
146
|
+
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
147
|
+
const identity = loadL0Identity();
|
|
148
|
+
const essentialStory = rootPath ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
149
|
+
if (!identity && !essentialStory) return "";
|
|
150
|
+
const parts = [];
|
|
151
|
+
if (identity) {
|
|
152
|
+
parts.push(`## L0 Identity
|
|
153
|
+
|
|
154
|
+
${identity}`);
|
|
155
|
+
}
|
|
156
|
+
if (essentialStory) {
|
|
157
|
+
parts.push(`## L1 Essential Story
|
|
158
|
+
|
|
159
|
+
${essentialStory}`);
|
|
160
|
+
}
|
|
161
|
+
return parts.join("\n\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
9
164
|
// src/hooks/ts/lib/project-utils/paths.ts
|
|
10
|
-
import { existsSync as
|
|
11
|
-
import { join as
|
|
165
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, renameSync } from "fs";
|
|
166
|
+
import { join as join3, basename as basename2 } from "path";
|
|
12
167
|
|
|
13
168
|
// src/hooks/ts/lib/pai-paths.ts
|
|
14
|
-
import { homedir } from "os";
|
|
15
|
-
import { resolve, join } from "path";
|
|
16
|
-
import { existsSync, readFileSync } from "fs";
|
|
169
|
+
import { homedir as homedir2 } from "os";
|
|
170
|
+
import { resolve, join as join2 } from "path";
|
|
171
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
17
172
|
function loadEnvFile() {
|
|
18
173
|
const possiblePaths = [
|
|
19
174
|
resolve(process.env.PAI_DIR || "", ".env"),
|
|
20
|
-
resolve(
|
|
175
|
+
resolve(homedir2(), ".claude", ".env")
|
|
21
176
|
];
|
|
22
177
|
for (const envPath of possiblePaths) {
|
|
23
|
-
if (
|
|
178
|
+
if (existsSync2(envPath)) {
|
|
24
179
|
try {
|
|
25
|
-
const content =
|
|
180
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
26
181
|
for (const line of content.split("\n")) {
|
|
27
182
|
const trimmed = line.trim();
|
|
28
183
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -33,8 +188,8 @@ function loadEnvFile() {
|
|
|
33
188
|
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
34
189
|
value = value.slice(1, -1);
|
|
35
190
|
}
|
|
36
|
-
value = value.replace(/\$HOME/g,
|
|
37
|
-
value = value.replace(/^~(?=\/|$)/,
|
|
191
|
+
value = value.replace(/\$HOME/g, homedir2());
|
|
192
|
+
value = value.replace(/^~(?=\/|$)/, homedir2());
|
|
38
193
|
if (process.env[key] === void 0) {
|
|
39
194
|
process.env[key] = value;
|
|
40
195
|
}
|
|
@@ -47,19 +202,19 @@ function loadEnvFile() {
|
|
|
47
202
|
}
|
|
48
203
|
}
|
|
49
204
|
loadEnvFile();
|
|
50
|
-
var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(
|
|
51
|
-
var HOOKS_DIR =
|
|
52
|
-
var SKILLS_DIR =
|
|
53
|
-
var AGENTS_DIR =
|
|
54
|
-
var HISTORY_DIR =
|
|
55
|
-
var COMMANDS_DIR =
|
|
205
|
+
var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(homedir2(), ".claude");
|
|
206
|
+
var HOOKS_DIR = join2(PAI_DIR, "Hooks");
|
|
207
|
+
var SKILLS_DIR = join2(PAI_DIR, "Skills");
|
|
208
|
+
var AGENTS_DIR = join2(PAI_DIR, "Agents");
|
|
209
|
+
var HISTORY_DIR = join2(PAI_DIR, "History");
|
|
210
|
+
var COMMANDS_DIR = join2(PAI_DIR, "Commands");
|
|
56
211
|
function validatePAIStructure() {
|
|
57
|
-
if (!
|
|
212
|
+
if (!existsSync2(PAI_DIR)) {
|
|
58
213
|
console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
|
|
59
214
|
console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
|
|
60
215
|
process.exit(1);
|
|
61
216
|
}
|
|
62
|
-
if (!
|
|
217
|
+
if (!existsSync2(HOOKS_DIR)) {
|
|
63
218
|
console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
|
|
64
219
|
console.error(` Your PAI_DIR may be misconfigured`);
|
|
65
220
|
console.error(` Current PAI_DIR: ${PAI_DIR}`);
|
|
@@ -69,7 +224,7 @@ function validatePAIStructure() {
|
|
|
69
224
|
validatePAIStructure();
|
|
70
225
|
|
|
71
226
|
// src/hooks/ts/lib/project-utils/paths.ts
|
|
72
|
-
var PROJECTS_DIR =
|
|
227
|
+
var PROJECTS_DIR = join3(PAI_DIR, "projects");
|
|
73
228
|
var PROBE_CWD_PATTERNS = [
|
|
74
229
|
"/CodexBar/ClaudeProbe",
|
|
75
230
|
"/ClaudeProbe"
|
|
@@ -83,23 +238,23 @@ function encodePath(path) {
|
|
|
83
238
|
}
|
|
84
239
|
function getProjectDir(cwd) {
|
|
85
240
|
const encoded = encodePath(cwd);
|
|
86
|
-
return
|
|
241
|
+
return join3(PROJECTS_DIR, encoded);
|
|
87
242
|
}
|
|
88
243
|
function getNotesDir(cwd) {
|
|
89
|
-
return
|
|
244
|
+
return join3(getProjectDir(cwd), "Notes");
|
|
90
245
|
}
|
|
91
246
|
function findNotesDir(cwd) {
|
|
92
|
-
const cwdBasename =
|
|
93
|
-
if (cwdBasename === "notes" &&
|
|
247
|
+
const cwdBasename = basename2(cwd).toLowerCase();
|
|
248
|
+
if (cwdBasename === "notes" && existsSync3(cwd)) {
|
|
94
249
|
return { path: cwd, isLocal: true };
|
|
95
250
|
}
|
|
96
251
|
const localPaths = [
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
252
|
+
join3(cwd, "Notes"),
|
|
253
|
+
join3(cwd, "notes"),
|
|
254
|
+
join3(cwd, ".claude", "Notes")
|
|
100
255
|
];
|
|
101
256
|
for (const path of localPaths) {
|
|
102
|
-
if (
|
|
257
|
+
if (existsSync3(path)) {
|
|
103
258
|
return { path, isLocal: true };
|
|
104
259
|
}
|
|
105
260
|
}
|
|
@@ -107,41 +262,41 @@ function findNotesDir(cwd) {
|
|
|
107
262
|
}
|
|
108
263
|
function findTodoPath(cwd) {
|
|
109
264
|
const localPaths = [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
265
|
+
join3(cwd, "TODO.md"),
|
|
266
|
+
join3(cwd, "notes", "TODO.md"),
|
|
267
|
+
join3(cwd, "Notes", "TODO.md"),
|
|
268
|
+
join3(cwd, ".claude", "TODO.md")
|
|
114
269
|
];
|
|
115
270
|
for (const path of localPaths) {
|
|
116
|
-
if (
|
|
271
|
+
if (existsSync3(path)) return path;
|
|
117
272
|
}
|
|
118
|
-
return
|
|
273
|
+
return join3(getNotesDir(cwd), "TODO.md");
|
|
119
274
|
}
|
|
120
275
|
function findAllClaudeMdPaths(cwd) {
|
|
121
276
|
const foundPaths = [];
|
|
122
277
|
const localPaths = [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
278
|
+
join3(cwd, ".claude", "CLAUDE.md"),
|
|
279
|
+
join3(cwd, "CLAUDE.md"),
|
|
280
|
+
join3(cwd, "Notes", "CLAUDE.md"),
|
|
281
|
+
join3(cwd, "notes", "CLAUDE.md"),
|
|
282
|
+
join3(cwd, "Prompts", "CLAUDE.md"),
|
|
283
|
+
join3(cwd, "prompts", "CLAUDE.md")
|
|
129
284
|
];
|
|
130
285
|
for (const path of localPaths) {
|
|
131
|
-
if (
|
|
286
|
+
if (existsSync3(path)) foundPaths.push(path);
|
|
132
287
|
}
|
|
133
288
|
return foundPaths;
|
|
134
289
|
}
|
|
135
290
|
|
|
136
291
|
// src/hooks/ts/lib/project-utils/notify.ts
|
|
137
|
-
import { existsSync as
|
|
138
|
-
import { join as
|
|
139
|
-
import { homedir as
|
|
292
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
293
|
+
import { join as join4 } from "path";
|
|
294
|
+
import { homedir as homedir3 } from "os";
|
|
140
295
|
function isWhatsAppEnabled() {
|
|
141
296
|
try {
|
|
142
|
-
const settingsPath =
|
|
143
|
-
if (!
|
|
144
|
-
const settings = JSON.parse(
|
|
297
|
+
const settingsPath = join4(homedir3(), ".claude", "settings.json");
|
|
298
|
+
if (!existsSync4(settingsPath)) return false;
|
|
299
|
+
const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
145
300
|
const enabled = settings.enabledMcpjsonServers || [];
|
|
146
301
|
return enabled.includes("aibroker") || enabled.includes("whazaa") || enabled.includes("telex");
|
|
147
302
|
} catch {
|
|
@@ -186,21 +341,21 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
186
341
|
}
|
|
187
342
|
|
|
188
343
|
// src/hooks/ts/lib/project-utils/session-notes.ts
|
|
189
|
-
import { existsSync as
|
|
190
|
-
import { join as
|
|
344
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync, renameSync as renameSync2 } from "fs";
|
|
345
|
+
import { join as join5, basename as basename3 } from "path";
|
|
191
346
|
function getMonthDir(notesDir) {
|
|
192
347
|
const now = /* @__PURE__ */ new Date();
|
|
193
348
|
const year = String(now.getFullYear());
|
|
194
349
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
195
|
-
const monthDir =
|
|
196
|
-
if (!
|
|
350
|
+
const monthDir = join5(notesDir, year, month);
|
|
351
|
+
if (!existsSync5(monthDir)) {
|
|
197
352
|
mkdirSync2(monthDir, { recursive: true });
|
|
198
353
|
}
|
|
199
354
|
return monthDir;
|
|
200
355
|
}
|
|
201
356
|
function getNextNoteNumber(notesDir) {
|
|
202
357
|
const monthDir = getMonthDir(notesDir);
|
|
203
|
-
const files =
|
|
358
|
+
const files = readdirSync3(monthDir).filter((f) => f.match(/^\d{3,4}[\s_-]/)).sort();
|
|
204
359
|
if (files.length === 0) return "0001";
|
|
205
360
|
let maxNumber = 0;
|
|
206
361
|
for (const file of files) {
|
|
@@ -213,27 +368,27 @@ function getNextNoteNumber(notesDir) {
|
|
|
213
368
|
return String(maxNumber + 1).padStart(4, "0");
|
|
214
369
|
}
|
|
215
370
|
function getCurrentNotePath(notesDir) {
|
|
216
|
-
if (!
|
|
371
|
+
if (!existsSync5(notesDir)) return null;
|
|
217
372
|
const findLatestIn = (dir) => {
|
|
218
|
-
if (!
|
|
219
|
-
const files =
|
|
373
|
+
if (!existsSync5(dir)) return null;
|
|
374
|
+
const files = readdirSync3(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
|
|
220
375
|
const numA = parseInt(a.match(/^(\d+)/)?.[1] || "0", 10);
|
|
221
376
|
const numB = parseInt(b.match(/^(\d+)/)?.[1] || "0", 10);
|
|
222
377
|
return numA - numB;
|
|
223
378
|
});
|
|
224
379
|
if (files.length === 0) return null;
|
|
225
|
-
return
|
|
380
|
+
return join5(dir, files[files.length - 1]);
|
|
226
381
|
};
|
|
227
382
|
const now = /* @__PURE__ */ new Date();
|
|
228
383
|
const year = String(now.getFullYear());
|
|
229
384
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
230
|
-
const currentMonthDir =
|
|
385
|
+
const currentMonthDir = join5(notesDir, year, month);
|
|
231
386
|
const found = findLatestIn(currentMonthDir);
|
|
232
387
|
if (found) return found;
|
|
233
388
|
const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
234
389
|
const prevYear = String(prevDate.getFullYear());
|
|
235
390
|
const prevMonth = String(prevDate.getMonth() + 1).padStart(2, "0");
|
|
236
|
-
const prevMonthDir =
|
|
391
|
+
const prevMonthDir = join5(notesDir, prevYear, prevMonth);
|
|
237
392
|
const prevFound = findLatestIn(prevMonthDir);
|
|
238
393
|
if (prevFound) return prevFound;
|
|
239
394
|
return findLatestIn(notesDir);
|
|
@@ -243,7 +398,7 @@ function createSessionNote(notesDir, description) {
|
|
|
243
398
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
244
399
|
const monthDir = getMonthDir(notesDir);
|
|
245
400
|
const filename = `${noteNumber} - ${date} - New Session.md`;
|
|
246
|
-
const filepath =
|
|
401
|
+
const filepath = join5(monthDir, filename);
|
|
247
402
|
const content = `# Session ${noteNumber}: ${description}
|
|
248
403
|
|
|
249
404
|
**Date:** ${date}
|
|
@@ -281,16 +436,16 @@ function findPaiBinary() {
|
|
|
281
436
|
`${process.env.HOME}/.local/bin/pai`
|
|
282
437
|
];
|
|
283
438
|
for (const p of fallbacks) {
|
|
284
|
-
if (
|
|
439
|
+
if (existsSync6(p)) return p;
|
|
285
440
|
}
|
|
286
441
|
}
|
|
287
442
|
return "pai";
|
|
288
443
|
}
|
|
289
444
|
function getRoutedNotesPath() {
|
|
290
|
-
const routingFile =
|
|
291
|
-
if (!
|
|
445
|
+
const routingFile = join6(PAI_DIR, "session-routing.json");
|
|
446
|
+
if (!existsSync6(routingFile)) return null;
|
|
292
447
|
try {
|
|
293
|
-
const routing = JSON.parse(
|
|
448
|
+
const routing = JSON.parse(readFileSync5(routingFile, "utf-8"));
|
|
294
449
|
const active = routing?.active_session;
|
|
295
450
|
if (active?.notes_path) {
|
|
296
451
|
return active.notes_path;
|
|
@@ -315,16 +470,16 @@ var PROJECT_SIGNALS = [
|
|
|
315
470
|
"CMakeLists.txt",
|
|
316
471
|
"tsconfig.json",
|
|
317
472
|
"CLAUDE.md",
|
|
318
|
-
|
|
473
|
+
join6("Notes", "PAI.md")
|
|
319
474
|
];
|
|
320
475
|
function hasProjectSignals(dir) {
|
|
321
476
|
for (const signal of PROJECT_SIGNALS) {
|
|
322
|
-
if (
|
|
477
|
+
if (existsSync6(join6(dir, signal))) return true;
|
|
323
478
|
}
|
|
324
479
|
return false;
|
|
325
480
|
}
|
|
326
481
|
function isGuardedPath(dir) {
|
|
327
|
-
const home =
|
|
482
|
+
const home = homedir4();
|
|
328
483
|
const resolved = resolve2(dir);
|
|
329
484
|
if (resolved === home) return true;
|
|
330
485
|
const parts = resolved.split("/").filter(Boolean);
|
|
@@ -355,9 +510,9 @@ async function main() {
|
|
|
355
510
|
console.error("Could not parse hook input, using process.cwd()");
|
|
356
511
|
}
|
|
357
512
|
const cwd = hookInput?.cwd || process.cwd();
|
|
358
|
-
let projectName =
|
|
513
|
+
let projectName = basename4(cwd);
|
|
359
514
|
if (projectName.toLowerCase() === "notes") {
|
|
360
|
-
projectName =
|
|
515
|
+
projectName = basename4(dirname(cwd));
|
|
361
516
|
}
|
|
362
517
|
console.error(`Working directory: ${cwd}`);
|
|
363
518
|
console.error(`Project: ${projectName}`);
|
|
@@ -373,7 +528,7 @@ async function main() {
|
|
|
373
528
|
for (const path of claudeMdPaths) {
|
|
374
529
|
console.error(` - ${path}`);
|
|
375
530
|
try {
|
|
376
|
-
const content =
|
|
531
|
+
const content = readFileSync5(path, "utf-8");
|
|
377
532
|
claudeMdContents.push({ path, content });
|
|
378
533
|
console.error(` Read ${content.length} chars`);
|
|
379
534
|
} catch (error) {
|
|
@@ -388,7 +543,7 @@ async function main() {
|
|
|
388
543
|
let notesDir;
|
|
389
544
|
if (routedPath) {
|
|
390
545
|
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
391
|
-
if (!
|
|
546
|
+
if (!existsSync6(routedPath)) {
|
|
392
547
|
mkdirSync3(routedPath, { recursive: true });
|
|
393
548
|
console.error(`Created routed Notes: ${routedPath}`);
|
|
394
549
|
} else {
|
|
@@ -401,7 +556,7 @@ async function main() {
|
|
|
401
556
|
notesDir = notesInfo.path;
|
|
402
557
|
console.error(`Notes directory: ${notesDir} (local)`);
|
|
403
558
|
} else {
|
|
404
|
-
if (!
|
|
559
|
+
if (!existsSync6(notesInfo.path)) {
|
|
405
560
|
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
406
561
|
mkdirSync3(notesInfo.path, { recursive: true });
|
|
407
562
|
console.error(`Created central Notes: ${notesInfo.path}`);
|
|
@@ -412,24 +567,24 @@ async function main() {
|
|
|
412
567
|
}
|
|
413
568
|
}
|
|
414
569
|
const projectDir = getProjectDir(cwd);
|
|
415
|
-
if (
|
|
570
|
+
if (existsSync6(projectDir)) {
|
|
416
571
|
try {
|
|
417
|
-
const files =
|
|
572
|
+
const files = readdirSync4(projectDir);
|
|
418
573
|
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => ({
|
|
419
574
|
name: f,
|
|
420
|
-
path:
|
|
421
|
-
mtime: statSync(
|
|
575
|
+
path: join6(projectDir, f),
|
|
576
|
+
mtime: statSync(join6(projectDir, f)).mtime.getTime()
|
|
422
577
|
})).sort((a, b) => b.mtime - a.mtime);
|
|
423
578
|
if (jsonlFiles.length > 1) {
|
|
424
579
|
const { mkdirSync: mkdirSync3, renameSync: renameSync3 } = await import("fs");
|
|
425
|
-
const sessionsDir =
|
|
426
|
-
if (!
|
|
580
|
+
const sessionsDir = join6(projectDir, "sessions");
|
|
581
|
+
if (!existsSync6(sessionsDir)) {
|
|
427
582
|
mkdirSync3(sessionsDir, { recursive: true });
|
|
428
583
|
}
|
|
429
584
|
for (let i = 1; i < jsonlFiles.length; i++) {
|
|
430
585
|
const file = jsonlFiles[i];
|
|
431
|
-
const destPath =
|
|
432
|
-
if (!
|
|
586
|
+
const destPath = join6(sessionsDir, file.name);
|
|
587
|
+
if (!existsSync6(destPath)) {
|
|
433
588
|
renameSync3(file.path, destPath);
|
|
434
589
|
console.error(`Moved old session: ${file.name} \u2192 sessions/`);
|
|
435
590
|
}
|
|
@@ -440,11 +595,11 @@ async function main() {
|
|
|
440
595
|
}
|
|
441
596
|
}
|
|
442
597
|
const todoPath = findTodoPath(cwd);
|
|
443
|
-
const hasTodo =
|
|
598
|
+
const hasTodo = existsSync6(todoPath);
|
|
444
599
|
if (hasTodo) {
|
|
445
600
|
console.error(`TODO.md: ${todoPath}`);
|
|
446
601
|
} else {
|
|
447
|
-
const newTodoPath =
|
|
602
|
+
const newTodoPath = join6(notesDir, "TODO.md");
|
|
448
603
|
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
449
604
|
writeFileSync2(newTodoPath, `# TODO
|
|
450
605
|
|
|
@@ -465,13 +620,13 @@ async function main() {
|
|
|
465
620
|
const safeProjectName = typeof projectName === "string" && projectName.trim().length > 0 ? projectName.trim() : "Untitled Session";
|
|
466
621
|
console.error("\nNo previous session notes found - creating new one");
|
|
467
622
|
activeNotePath = createSessionNote(notesDir, String(safeProjectName));
|
|
468
|
-
console.error(`Created: ${
|
|
623
|
+
console.error(`Created: ${basename4(activeNotePath)}`);
|
|
469
624
|
} else {
|
|
470
625
|
activeNotePath = currentNotePath;
|
|
471
626
|
console.error(`
|
|
472
|
-
Using existing session note: ${
|
|
627
|
+
Using existing session note: ${basename4(activeNotePath)}`);
|
|
473
628
|
try {
|
|
474
|
-
const content =
|
|
629
|
+
const content = readFileSync5(activeNotePath, "utf-8");
|
|
475
630
|
const lines = content.split("\n").slice(0, 12);
|
|
476
631
|
console.error("--- Current Note Preview ---");
|
|
477
632
|
for (const line of lines) {
|
|
@@ -482,9 +637,9 @@ Using existing session note: ${basename3(activeNotePath)}`);
|
|
|
482
637
|
}
|
|
483
638
|
}
|
|
484
639
|
}
|
|
485
|
-
if (
|
|
640
|
+
if (existsSync6(todoPath)) {
|
|
486
641
|
try {
|
|
487
|
-
const todoContent =
|
|
642
|
+
const todoContent = readFileSync5(todoPath, "utf-8");
|
|
488
643
|
const todoLines = todoContent.split("\n").filter((l) => l.includes("[ ]")).slice(0, 5);
|
|
489
644
|
if (todoLines.length > 0) {
|
|
490
645
|
console.error("\nOpen TODOs:");
|
|
@@ -583,7 +738,7 @@ Working Directory: ${cwd}
|
|
|
583
738
|
${notesDir ? `Notes Directory: ${notesDir}${routedPath ? " (routed via pai route)" : ""}` : "Notes: disabled (no local Notes/ directory)"}
|
|
584
739
|
${hasTodo ? `TODO: ${todoPath}` : "TODO: not found"}
|
|
585
740
|
${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(", ")}` : "No CLAUDE.md found"}
|
|
586
|
-
${activeNotePath ? `Active Note: ${
|
|
741
|
+
${activeNotePath ? `Active Note: ${basename4(activeNotePath)}` : ""}
|
|
587
742
|
${routedPath ? `
|
|
588
743
|
Note Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ""}
|
|
589
744
|
${paiProjectBlock ? `
|
|
@@ -611,6 +766,38 @@ THE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.
|
|
|
611
766
|
console.log(claudeMdReminder);
|
|
612
767
|
console.error(`Injected CLAUDE.md content from: ${path}`);
|
|
613
768
|
}
|
|
769
|
+
try {
|
|
770
|
+
let wakeupRootPath;
|
|
771
|
+
try {
|
|
772
|
+
const { execFileSync: efs } = await import("child_process");
|
|
773
|
+
const raw2 = efs(paiBin, ["project", "detect", "--json", cwd], {
|
|
774
|
+
encoding: "utf-8",
|
|
775
|
+
env: process.env
|
|
776
|
+
}).trim();
|
|
777
|
+
if (raw2) {
|
|
778
|
+
const det = JSON.parse(raw2);
|
|
779
|
+
if (det.root_path) wakeupRootPath = det.root_path;
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
782
|
+
wakeupRootPath = cwd;
|
|
783
|
+
}
|
|
784
|
+
const wakeupBlock = buildWakeupContext(wakeupRootPath);
|
|
785
|
+
if (wakeupBlock) {
|
|
786
|
+
const wakeupReminder = `
|
|
787
|
+
<system-reminder>
|
|
788
|
+
WAKEUP CONTEXT
|
|
789
|
+
|
|
790
|
+
${wakeupBlock}
|
|
791
|
+
</system-reminder>
|
|
792
|
+
`;
|
|
793
|
+
console.log(wakeupReminder);
|
|
794
|
+
console.error("Injected wake-up context (L0+L1)");
|
|
795
|
+
} else {
|
|
796
|
+
console.error("No wake-up context to inject (no identity file or session notes)");
|
|
797
|
+
}
|
|
798
|
+
} catch (wakeupError) {
|
|
799
|
+
console.error("Wake-up context injection failed:", wakeupError);
|
|
800
|
+
}
|
|
614
801
|
console.error("\nProject context setup complete\n");
|
|
615
802
|
process.exit(0);
|
|
616
803
|
}
|