@tekmidian/pai 0.48.0 → 0.50.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/{aibroker-client-D5NT0S1N.mjs → aibroker-client-Bu72yPKj.mjs} +1 -1
- package/dist/{aibroker-client-Bn9SnGMa.mjs → aibroker-client-CDIXmq-e.mjs} +1 -1
- package/dist/{aibroker-client-Bn9SnGMa.mjs.map → aibroker-client-CDIXmq-e.mjs.map} +1 -1
- package/dist/{chain-CNSa0A3G.mjs → chain-BBbVbWVv.mjs} +4 -4
- package/dist/{chain-CNSa0A3G.mjs.map → chain-BBbVbWVv.mjs.map} +1 -1
- package/dist/cli/index.mjs +14 -14
- package/dist/cli/program.mjs +14 -14
- package/dist/config-Bn9jpblO.mjs +653 -0
- package/dist/config-Bn9jpblO.mjs.map +1 -0
- package/dist/{config-DxMZjpvY.mjs → config-DYXARgk8.mjs} +180 -151
- package/dist/config-DYXARgk8.mjs.map +1 -0
- package/dist/config-RjeVGJ7K.mjs +3 -0
- package/dist/{context-handover-cache-B9NKSjwR.mjs → context-handover-cache-yOuK9RIk.mjs} +5 -5
- package/dist/{context-handover-cache-B9NKSjwR.mjs.map → context-handover-cache-yOuK9RIk.mjs.map} +1 -1
- package/dist/daemon/index.mjs +13 -12
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-DZ4Q0jrs.mjs → daemon-CBTQYtHF.mjs} +49 -30
- package/dist/daemon-CBTQYtHF.mjs.map +1 -0
- package/dist/daemon-Vf7pHnjR.mjs +25 -0
- package/dist/daemon-mcp/index.mjs +186 -23
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/factory-Ba8j9K7q.mjs +3 -0
- package/dist/{factory-DNYAqjH0.mjs → factory-Bt7TWQje.mjs} +3 -3
- package/dist/{factory-DNYAqjH0.mjs.map → factory-Bt7TWQje.mjs.map} +1 -1
- package/dist/{fallback-IeR2FrFn.mjs → fallback-CIGspCSi.mjs} +260 -10
- package/dist/fallback-CIGspCSi.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +80 -8
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +54 -39
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/post-compact-inject.mjs +54 -39
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +204 -109
- package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
- package/dist/hooks/whisper-rules.mjs +7613 -9
- package/dist/hooks/whisper-rules.mjs.map +4 -4
- package/dist/hooks/worker-proxy.mjs +204 -109
- package/dist/hooks/worker-proxy.mjs.map +4 -4
- package/dist/hooks/worker-status-line.mjs +211 -116
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/hooks/worker-supervision.mjs +208 -113
- package/dist/hooks/worker-supervision.mjs.map +4 -4
- package/dist/{kg-entity-BSjM5xZh.mjs → kg-entity-BSW7ra8i.mjs} +1 -1
- package/dist/{kg-entity-BSjM5xZh.mjs.map → kg-entity-BSW7ra8i.mjs.map} +1 -1
- package/dist/main-resolver-BTastpp4.mjs +7 -0
- package/dist/{main-resolver-DvYu9jbl.mjs → main-resolver-BmZPCmgj.mjs} +3 -3
- package/dist/{main-resolver-DvYu9jbl.mjs.map → main-resolver-BmZPCmgj.mjs.map} +1 -1
- package/dist/{planner-Cn8HmT7l.mjs → planner-BiH9msqq.mjs} +6 -6
- package/dist/{planner-Cn8HmT7l.mjs.map → planner-BiH9msqq.mjs.map} +1 -1
- package/dist/{program-Bgv1lFFA.mjs → program-BX67-4I2.mjs} +536 -260
- package/dist/program-BX67-4I2.mjs.map +1 -0
- package/dist/query-feedback-Bm6XvCmg.mjs +3 -0
- package/dist/{query-feedback-CYyOGi3y.mjs → query-feedback-CTs_jx6i.mjs} +1 -1
- package/dist/{query-feedback-CYyOGi3y.mjs.map → query-feedback-CTs_jx6i.mjs.map} +1 -1
- package/dist/{run-Cls0YGgV.mjs → run-TChOnMO_.mjs} +1388 -1183
- package/dist/run-TChOnMO_.mjs.map +1 -0
- package/dist/{server-Ch99xt5K.mjs → server-R9UkCn3v.mjs} +42 -3
- package/dist/server-R9UkCn3v.mjs.map +1 -0
- package/dist/session-keepalive-BuG3wvik.mjs +579 -0
- package/dist/session-keepalive-BuG3wvik.mjs.map +1 -0
- package/dist/skills/VaultContext/SKILL.md +1 -1
- package/dist/{state-DnYGRmS7.mjs → state-Ca0ltLgI.mjs} +1 -1
- package/dist/{state-PJrgfDLW.mjs → state-DwrsOoMi.mjs} +1 -1
- package/dist/{state-PJrgfDLW.mjs.map → state-DwrsOoMi.mjs.map} +1 -1
- package/dist/{tools-B77WHeVZ.mjs → tools-BEqiJt_j.mjs} +13 -13
- package/dist/{tools-B77WHeVZ.mjs.map → tools-BEqiJt_j.mjs.map} +1 -1
- package/dist/{tools-NnR_n-Ja.mjs → tools-BwsOTmjh.mjs} +2 -2
- package/dist/{wakeup-UrhiW-KU.mjs → wakeup-Yk5lQ0jb.mjs} +1 -1
- package/dist/{wakeup-UrhiW-KU.mjs.map → wakeup-Yk5lQ0jb.mjs.map} +1 -1
- package/dist/{work-queue-worker-BA5IDBwQ.mjs → work-queue-worker-B9zVWImP.mjs} +3 -3
- package/dist/{work-queue-worker-BA5IDBwQ.mjs.map → work-queue-worker-B9zVWImP.mjs.map} +1 -1
- package/dist/work-queue-worker-D-DnQ7oh.mjs +14 -0
- package/dist/{zettelkasten-BBjA7GcP.mjs → zettelkasten-DezbXghl.mjs} +2 -2
- package/dist/{zettelkasten-BBjA7GcP.mjs.map → zettelkasten-DezbXghl.mjs.map} +1 -1
- package/docs/cache-keepalive.md +138 -0
- package/docs/commands/README.md +7 -0
- package/docs/commands/audit.md +1 -0
- package/docs/commands/config.md +84 -0
- package/docs/commands/daemon.md +6 -0
- package/docs/commands/worker.md +37 -2
- package/docs/config.md +171 -0
- package/docs/model-catalogue.md +502 -0
- package/docs/provider-independence-details.md +1 -1
- package/docs/task-bus.md +1 -1
- package/docs/worker.md +33 -5
- package/docs/workers-config.md +105 -12
- package/package.json +1 -1
- package/src/hooks/ts/capture-all-events.test.ts +71 -0
- package/src/hooks/ts/capture-all-events.ts +10 -3
- package/src/hooks/ts/user-prompt/whisper-rules.test.ts +56 -1
- package/src/hooks/ts/user-prompt/whisper-rules.ts +24 -0
- package/statusline-command.sh +81 -6
- package/dist/config-DCu8THKi.mjs +0 -228
- package/dist/config-DCu8THKi.mjs.map +0 -1
- package/dist/config-DeqFqa1N.mjs +0 -3
- package/dist/config-DxMZjpvY.mjs.map +0 -1
- package/dist/daemon-CYpVy_Dw.mjs +0 -24
- package/dist/daemon-DZ4Q0jrs.mjs.map +0 -1
- package/dist/factory-47Qf_df7.mjs +0 -3
- package/dist/fallback-IeR2FrFn.mjs.map +0 -1
- package/dist/indexer-C5Le7XE7.mjs +0 -1
- package/dist/main-resolver-CnfnWa8C.mjs +0 -7
- package/dist/program-Bgv1lFFA.mjs.map +0 -1
- package/dist/query-feedback-C7rSfiXU.mjs +0 -3
- package/dist/run-Cls0YGgV.mjs.map +0 -1
- package/dist/server-Ch99xt5K.mjs.map +0 -1
- package/dist/work-queue-worker-DLM2wlqM.mjs +0 -14
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { a as paiHomePath, o as resolvePaiFile, r as migratePaiFile } from "./pai-home-ChAOupZc.mjs";
|
|
2
|
+
import { i as paiSocketPath } from "./runtime-paths-B0P1TvUr.mjs";
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, userInfo } from "node:os";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { Document, YAMLMap, parseDocument } from "yaml";
|
|
7
|
+
|
|
8
|
+
//#region src/notifications/types.ts
|
|
9
|
+
const DEFAULT_ROUTING = {
|
|
10
|
+
error: [
|
|
11
|
+
"whatsapp",
|
|
12
|
+
"macos",
|
|
13
|
+
"ntfy",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
completion: [
|
|
17
|
+
"whatsapp",
|
|
18
|
+
"macos",
|
|
19
|
+
"ntfy",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
info: ["cli"],
|
|
23
|
+
progress: ["cli"],
|
|
24
|
+
debug: []
|
|
25
|
+
};
|
|
26
|
+
const DEFAULT_CHANNELS = {
|
|
27
|
+
ntfy: {
|
|
28
|
+
enabled: false,
|
|
29
|
+
url: void 0,
|
|
30
|
+
priority: "default"
|
|
31
|
+
},
|
|
32
|
+
whatsapp: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
recipient: void 0
|
|
35
|
+
},
|
|
36
|
+
macos: { enabled: true },
|
|
37
|
+
voice: {
|
|
38
|
+
enabled: false,
|
|
39
|
+
voiceName: "bm_george"
|
|
40
|
+
},
|
|
41
|
+
cli: { enabled: true }
|
|
42
|
+
};
|
|
43
|
+
const DEFAULT_NOTIFICATION_CONFIG = {
|
|
44
|
+
mode: "auto",
|
|
45
|
+
channels: DEFAULT_CHANNELS,
|
|
46
|
+
routing: DEFAULT_ROUTING
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/tasks/types.ts
|
|
51
|
+
/**
|
|
52
|
+
* Prefix marking a tracker label as a PAI ownership assertion.
|
|
53
|
+
* A task labelled `pai:acme-api` is owned by the `acme-api` project.
|
|
54
|
+
*/
|
|
55
|
+
const OWNER_LABEL_PREFIX = "pai:";
|
|
56
|
+
/** An unresolved owner. UNROUTED is a normal state, not an error. */
|
|
57
|
+
const UNROUTED = {
|
|
58
|
+
project: null,
|
|
59
|
+
rootPath: null,
|
|
60
|
+
source: "none"
|
|
61
|
+
};
|
|
62
|
+
const DEFAULT_TASK_CONFIG = {
|
|
63
|
+
enabled: false,
|
|
64
|
+
providers: { todoist: { enabled: false } },
|
|
65
|
+
autoDispatch: false
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/config/json-store.ts
|
|
70
|
+
/**
|
|
71
|
+
* json-store.ts — read/write JSON config files without destroying them
|
|
72
|
+
*
|
|
73
|
+
* The failure this exists to prevent:
|
|
74
|
+
*
|
|
75
|
+
* try { return JSON.parse(read(path)); } catch { return {}; }
|
|
76
|
+
* ... later ...
|
|
77
|
+
* write(path, JSON.stringify(ourData));
|
|
78
|
+
*
|
|
79
|
+
* An unreadable file becomes an empty object, and the next write makes that
|
|
80
|
+
* permanent. Silently, exit code 0. This shape appeared three times in this
|
|
81
|
+
* repo against three different files, and twice in AIBroker.
|
|
82
|
+
*
|
|
83
|
+
* The distinction that matters is between *missing* and *unreadable*:
|
|
84
|
+
*
|
|
85
|
+
* missing — legitimate first run. Start fresh; writing is safe.
|
|
86
|
+
* unreadable — the file exists and we could not parse it. Those bytes are
|
|
87
|
+
* the only copy of something. Never overwrite them.
|
|
88
|
+
*
|
|
89
|
+
* Collapsing the second into the first is the bug.
|
|
90
|
+
*
|
|
91
|
+
* NOT everything deserves this guard. For a transient buffer — an undelivered
|
|
92
|
+
* message queue, a cache — starting fresh IS the correct recovery, and
|
|
93
|
+
* refusing to write would disable the feature permanently. Use `writeJsonAtomic`
|
|
94
|
+
* alone there: it still prevents a crash from truncating a good file, without
|
|
95
|
+
* blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* Read a JSON file, distinguishing "absent" from "damaged".
|
|
99
|
+
*
|
|
100
|
+
* @param path file to read
|
|
101
|
+
* @param label how to name it to the user, e.g. "~/.claude.json"
|
|
102
|
+
* @throws if the file exists but cannot be read or parsed
|
|
103
|
+
*/
|
|
104
|
+
function readJsonStrict(path, label = path) {
|
|
105
|
+
if (!existsSync(path)) return {};
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = readFileSync(path, "utf8");
|
|
109
|
+
} catch (e) {
|
|
110
|
+
throw new Error(`Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — writing now would replace its contents with ours alone.`);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(raw);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
throw new Error(`${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — overwriting it would destroy whatever it holds.\nRepair the file, or move it aside and re-run this command.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Leaf values that mean "type word", not data.
|
|
120
|
+
*
|
|
121
|
+
* A config whose top-level keys map to bare type words is a serialized schema,
|
|
122
|
+
* not a configuration — the exact shape found over the live config on
|
|
123
|
+
* 2026-09-18 (399 bytes: `socketPath: string`, `indexIntervalSecs: int`, …).
|
|
124
|
+
* No real config carries three of these at its top level; a dump carries
|
|
125
|
+
* nothing else.
|
|
126
|
+
*/
|
|
127
|
+
const SCHEMA_TYPE_WORDS = new Set([
|
|
128
|
+
"string",
|
|
129
|
+
"int",
|
|
130
|
+
"integer",
|
|
131
|
+
"number",
|
|
132
|
+
"bool",
|
|
133
|
+
"boolean",
|
|
134
|
+
"float",
|
|
135
|
+
"double",
|
|
136
|
+
"object",
|
|
137
|
+
"array",
|
|
138
|
+
"any",
|
|
139
|
+
"unknown"
|
|
140
|
+
]);
|
|
141
|
+
function schemaDumpHits(data) {
|
|
142
|
+
let hits = 0;
|
|
143
|
+
for (const v of Object.values(data)) if (typeof v === "string" && SCHEMA_TYPE_WORDS.has(v.trim())) hits += 1;
|
|
144
|
+
return hits;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Write JSON without risking the existing file.
|
|
148
|
+
*
|
|
149
|
+
* Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file
|
|
150
|
+
* and renames. Rename is atomic within a filesystem, so a crash mid-write
|
|
151
|
+
* leaves the original intact rather than truncated — which is how these files
|
|
152
|
+
* become corrupt in the first place.
|
|
153
|
+
*/
|
|
154
|
+
function writeJsonAtomic(path, data, opts = {}) {
|
|
155
|
+
const { backup = true, label = path } = opts;
|
|
156
|
+
if (schemaDumpHits(data) >= 3) throw new Error(`${label}: refusing to write — the object looks like a type schema, not values (several top-level keys map to bare type words). That is the corruption signature of 2026-09-18: the caller serialized the schema instead of the config. Nothing was written.`);
|
|
157
|
+
const serialized = JSON.stringify(data, null, 2) + "\n";
|
|
158
|
+
const dir = dirname(path);
|
|
159
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
160
|
+
if (backup && existsSync(path)) try {
|
|
161
|
+
copyFileSync(path, `${path}.bak-pai`);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
throw new Error(`Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
|
|
164
|
+
}
|
|
165
|
+
const tmp = `${path}.tmp-pai-${process.pid}`;
|
|
166
|
+
try {
|
|
167
|
+
writeFileSync(tmp, serialized, "utf8");
|
|
168
|
+
renameSync(tmp, path);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
try {
|
|
171
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
172
|
+
} catch {}
|
|
173
|
+
throw new Error(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\nThe original is unchanged.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/config/yaml-store.ts
|
|
179
|
+
/**
|
|
180
|
+
* yaml-store.ts — generic, comment-preserving YAML file helpers shared by
|
|
181
|
+
* every PAI config file that has a YAML form (config.yaml, voices.yaml;
|
|
182
|
+
* workers.yaml keeps its own writer in workers-config.ts because it needs
|
|
183
|
+
* provider-specific key-quoting, but its atomic-write tail is this module's
|
|
184
|
+
* writeYamlFileAtomic).
|
|
185
|
+
*/
|
|
186
|
+
var YamlStoreError = class extends Error {};
|
|
187
|
+
/**
|
|
188
|
+
* Validate `text` parses as YAML, back up any existing file to `.bak-pai`,
|
|
189
|
+
* then write via temp file + rename (atomic within a filesystem) at mode
|
|
190
|
+
* 0600 — every PAI-owned YAML file can carry secrets, key or no key.
|
|
191
|
+
*/
|
|
192
|
+
function writeYamlFileAtomic(path, text, opts = {}) {
|
|
193
|
+
const label = opts.label ?? path;
|
|
194
|
+
const parsed = parseDocument(text);
|
|
195
|
+
if (parsed.errors.length) throw new YamlStoreError(`refusing to write ${label}: ${parsed.errors[0].message} (previous file left unchanged)`);
|
|
196
|
+
const dir = dirname(path);
|
|
197
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
198
|
+
if (existsSync(path)) try {
|
|
199
|
+
copyFileSync(path, `${path}.bak-pai`);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
throw new YamlStoreError(`Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
|
|
202
|
+
}
|
|
203
|
+
const tmp = `${path}.tmp-pai-${process.pid}`;
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(tmp, text, {
|
|
206
|
+
encoding: "utf8",
|
|
207
|
+
mode: 384
|
|
208
|
+
});
|
|
209
|
+
renameSync(tmp, path);
|
|
210
|
+
chmodSync(path, 384);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
try {
|
|
213
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
214
|
+
} catch {}
|
|
215
|
+
throw new YamlStoreError(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Deep-equal for plain JSON-shaped values (objects/arrays/scalars). */
|
|
219
|
+
function deepEqualJson(a, b) {
|
|
220
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
221
|
+
}
|
|
222
|
+
function isPlainObject(v) {
|
|
223
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Apply the diff between `before` and `after` (arbitrary nested plain JSON
|
|
227
|
+
* objects) onto a YAML Document, touching only the leaves/keys that actually
|
|
228
|
+
* changed — this is what lets hand-written comments elsewhere in the file
|
|
229
|
+
* survive a `set`/`unset`. Arrays and non-object values are replaced
|
|
230
|
+
* wholesale when they differ; only plain objects are diffed key by key.
|
|
231
|
+
*/
|
|
232
|
+
function syncPlainObjectIntoYamlDoc(doc, before, after, path = []) {
|
|
233
|
+
if (isPlainObject(after) && isPlainObject(before)) {
|
|
234
|
+
for (const key of Object.keys(after)) syncPlainObjectIntoYamlDoc(doc, before[key], after[key], [...path, key]);
|
|
235
|
+
for (const key of Object.keys(before)) if (!(key in after)) doc.deleteIn([...path, key]);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (after === void 0) {
|
|
239
|
+
if (before !== void 0) doc.deleteIn(path);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!deepEqualJson(before, after)) doc.setIn(path, after);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Attach a `# text` comment directly above the key at `path` inside a YAML
|
|
246
|
+
* Document — used for the annotated section headers `pai config yaml`
|
|
247
|
+
* writes. No-op if the path does not resolve to a mapping entry (e.g. the
|
|
248
|
+
* key was omitted from the document).
|
|
249
|
+
*/
|
|
250
|
+
function setYamlKeyComment(doc, path, text) {
|
|
251
|
+
const parentPath = path.slice(0, -1);
|
|
252
|
+
const lastKey = path[path.length - 1];
|
|
253
|
+
const parent = parentPath.length ? doc.getIn(parentPath, true) : doc.contents;
|
|
254
|
+
if (!(parent instanceof YAMLMap)) return;
|
|
255
|
+
const pair = parent.items.find((p) => p.key?.value === lastKey);
|
|
256
|
+
if (pair) pair.key.commentBefore = ` ${text}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/config/main-config.ts
|
|
261
|
+
/**
|
|
262
|
+
* main-config.ts — the dual-format (config.yaml / config.json) engine behind
|
|
263
|
+
* the main PAI config file. Deliberately has NO dependency on
|
|
264
|
+
* src/daemon/config.ts (which owns path resolution, PaiDaemonConfig and
|
|
265
|
+
* DEFAULTS) so that module can import this one for loadConfig() without a
|
|
266
|
+
* cycle; every path this module touches is passed in explicitly.
|
|
267
|
+
*
|
|
268
|
+
* Precedence, same shape as workers.yaml/workers/config.ts: config.yaml wins
|
|
269
|
+
* when it exists, config.json otherwise. `pai config yaml` (see
|
|
270
|
+
* src/cli/commands/config.ts) performs the one-time JSON→YAML conversion;
|
|
271
|
+
* every writer that goes through readMainConfigRaw/writeMainConfigRaw (see
|
|
272
|
+
* src/daemon/config.ts) keeps working unmodified either way.
|
|
273
|
+
*/
|
|
274
|
+
var MainConfigError = class extends Error {};
|
|
275
|
+
/** The config.yaml sibling of a given config.json path (same directory). */
|
|
276
|
+
function yamlSiblingPath(jsonPath) {
|
|
277
|
+
return join(dirname(jsonPath), "config.yaml");
|
|
278
|
+
}
|
|
279
|
+
/** The path a user should be pointed at: config.yaml when it exists (same
|
|
280
|
+
* precedence as readDualFormatConfigRaw), else config.json. */
|
|
281
|
+
function resolvedMainConfigPath(jsonPath, yamlPath = yamlSiblingPath(jsonPath)) {
|
|
282
|
+
return existsSync(yamlPath) ? yamlPath : jsonPath;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Read the raw main config: config.yaml if it exists, else config.json
|
|
286
|
+
* (readJsonStrict — missing is `{}`, damaged throws), matching
|
|
287
|
+
* readWorkersSection's precedence for workers.yaml/JSON.
|
|
288
|
+
*/
|
|
289
|
+
function readDualFormatConfigRaw(jsonPath, yamlPath = yamlSiblingPath(jsonPath)) {
|
|
290
|
+
if (existsSync(yamlPath)) {
|
|
291
|
+
let text;
|
|
292
|
+
try {
|
|
293
|
+
text = readFileSync(yamlPath, "utf8");
|
|
294
|
+
} catch (e) {
|
|
295
|
+
throw new MainConfigError(`Could not read ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
296
|
+
}
|
|
297
|
+
const doc = parseDocument(text);
|
|
298
|
+
if (doc.errors.length) throw new MainConfigError(`${yamlPath}: ${doc.errors[0].message}`);
|
|
299
|
+
const parsed = doc.toJS();
|
|
300
|
+
if (parsed === null || parsed === void 0) return {};
|
|
301
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) throw new MainConfigError(`${yamlPath}: top level must be a mapping`);
|
|
302
|
+
return parsed;
|
|
303
|
+
}
|
|
304
|
+
return readJsonStrict(jsonPath, jsonPath);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Write the raw main config back: comment-preserving diff-sync into
|
|
308
|
+
* config.yaml when it exists, else plain writeJsonAtomic to config.json —
|
|
309
|
+
* this is the one seam every writer of the main config (CLI commands,
|
|
310
|
+
* identity, notifications, setup, workers) routes through, so a config.yaml
|
|
311
|
+
* on disk is honored no matter which of them made the change.
|
|
312
|
+
*/
|
|
313
|
+
function writeDualFormatConfigRaw(jsonPath, raw, yamlPath = yamlSiblingPath(jsonPath)) {
|
|
314
|
+
if (existsSync(yamlPath)) {
|
|
315
|
+
let text;
|
|
316
|
+
try {
|
|
317
|
+
text = readFileSync(yamlPath, "utf8");
|
|
318
|
+
} catch (e) {
|
|
319
|
+
throw new MainConfigError(`Could not read ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
320
|
+
}
|
|
321
|
+
const doc = parseDocument(text);
|
|
322
|
+
if (doc.errors.length) throw new MainConfigError(`${yamlPath}: ${doc.errors[0].message}`);
|
|
323
|
+
syncPlainObjectIntoYamlDoc(doc, doc.toJS() ?? {}, raw);
|
|
324
|
+
try {
|
|
325
|
+
writeYamlFileAtomic(yamlPath, String(doc), { label: yamlPath });
|
|
326
|
+
} catch (e) {
|
|
327
|
+
throw new MainConfigError(e instanceof Error ? e.message : String(e));
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
writeJsonAtomic(jsonPath, raw, { label: jsonPath });
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Short annotation for every top-level PaiDaemonConfig field, shown as a `#`
|
|
335
|
+
* comment above the field in the generated config.yaml. Mirrors (and must be
|
|
336
|
+
* kept in sync with) the doc comments on PaiDaemonConfig in
|
|
337
|
+
* src/daemon/config.ts — kept as a separate short-form table rather than
|
|
338
|
+
* parsed from the TS source at runtime.
|
|
339
|
+
*/
|
|
340
|
+
const MAIN_CONFIG_SECTION_COMMENTS = {
|
|
341
|
+
socketPath: "Unix Domain Socket path for daemon IPC.",
|
|
342
|
+
indexIntervalSecs: "How often the daemon re-indexes changed files, in seconds.",
|
|
343
|
+
embedIntervalSecs: "How often the daemon runs the embedding pass, in seconds.",
|
|
344
|
+
embedOnStartup: "Run an embed pass 60s after daemon start. Off by default: with a large backlog it makes every restart a CPU storm.",
|
|
345
|
+
maintenanceHour: "Local hour (0-23) to anchor the recurring index/embed cycle to, so maintenance runs in a fixed window.",
|
|
346
|
+
storageBackend: "Storage backend: \"sqlite\" (default) or \"postgres\".",
|
|
347
|
+
postgres: "PostgreSQL connection settings, used when storageBackend is \"postgres\".",
|
|
348
|
+
embeddingModel: "Embedding model name, used for semantic/hybrid search.",
|
|
349
|
+
logLevel: "Daemon log level: debug, info, warn, or error.",
|
|
350
|
+
vaultPath: "Obsidian vault root path for zettelkasten indexing, if any.",
|
|
351
|
+
vaultProjectId: "Registry project_id used for vault chunks in memory_chunks. Default: auto-detected.",
|
|
352
|
+
notifications: "Notification subsystem configuration.",
|
|
353
|
+
search: "Search defaults, applied when an MCP tool or CLI call doesn't specify one.",
|
|
354
|
+
tasks: "Task bus — optional external tracker for cross-session work.",
|
|
355
|
+
identity: "Who \"me\" is — addresses that count as the user's own. Empty by default and never guessed: nothing is self-addressed until this is set.",
|
|
356
|
+
workers: "Non-provider worker settings (pane, routing, tree, cache keepalive, fallback). Providers/classes/mcp_sets live in workers.yaml, not here."
|
|
357
|
+
};
|
|
358
|
+
/** Keys whose leading-underscore string value is a JSON stand-in for a
|
|
359
|
+
* comment (e.g. `"_comment": "..."`, `"_deliverToNote": "..."`). Stripped
|
|
360
|
+
* from the generated YAML and re-attached as a real `#` comment above the
|
|
361
|
+
* nearest remaining sibling key in the same object. */
|
|
362
|
+
function isCommentKey(key) {
|
|
363
|
+
return key.startsWith("_");
|
|
364
|
+
}
|
|
365
|
+
/** Recursively strip `_`-prefixed comment keys out of a plain JSON value,
|
|
366
|
+
* returning the cleaned value plus where each stripped comment belongs. */
|
|
367
|
+
function stripUnderscoreComments(value, path = []) {
|
|
368
|
+
if (Array.isArray(value)) {
|
|
369
|
+
const comments = [];
|
|
370
|
+
return {
|
|
371
|
+
cleaned: value.map((v, i) => {
|
|
372
|
+
const r = stripUnderscoreComments(v, [...path, i]);
|
|
373
|
+
comments.push(...r.comments);
|
|
374
|
+
return r.cleaned;
|
|
375
|
+
}),
|
|
376
|
+
comments
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (typeof value === "object" && value !== null) {
|
|
380
|
+
const obj = value;
|
|
381
|
+
const cleaned = {};
|
|
382
|
+
const comments = [];
|
|
383
|
+
const localComments = [];
|
|
384
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
385
|
+
if (isCommentKey(k)) {
|
|
386
|
+
if (typeof v === "string" && v.trim()) localComments.push(v.trim());
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const r = stripUnderscoreComments(v, [...path, k]);
|
|
390
|
+
cleaned[k] = r.cleaned;
|
|
391
|
+
comments.push(...r.comments);
|
|
392
|
+
}
|
|
393
|
+
const firstKey = Object.keys(cleaned)[0];
|
|
394
|
+
if (localComments.length && firstKey !== void 0) comments.unshift({
|
|
395
|
+
path: [...path, firstKey],
|
|
396
|
+
text: localComments.join(" ")
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
cleaned,
|
|
400
|
+
comments
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
cleaned: value,
|
|
405
|
+
comments: []
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* `pai config yaml`: convert config.json to config.yaml. Strips `_`-prefixed
|
|
410
|
+
* JSON-comment-workaround keys into real `#` comments, adds a short
|
|
411
|
+
* annotation above every top-level section (MAIN_CONFIG_SECTION_COMMENTS),
|
|
412
|
+
* verifies the generated YAML re-parses to the exact same data before
|
|
413
|
+
* writing anything, then renames the JSON aside to
|
|
414
|
+
* `config.json.migrated-<YYYY-MM-DD>` (never deleted). Refuses when
|
|
415
|
+
* config.yaml already exists unless `force`. `dryRun` computes and returns
|
|
416
|
+
* the would-be YAML text without touching either file.
|
|
417
|
+
*/
|
|
418
|
+
function migrateMainConfigToYaml(jsonPath, opts = {}) {
|
|
419
|
+
const yamlPath = opts.yamlPath ?? yamlSiblingPath(jsonPath);
|
|
420
|
+
if (existsSync(yamlPath) && !opts.force) throw new MainConfigError(`${yamlPath} already exists — edit it directly, or pass --force to regenerate it from ${jsonPath}`);
|
|
421
|
+
if (!existsSync(jsonPath)) throw new MainConfigError(`${jsonPath} does not exist — nothing to migrate`);
|
|
422
|
+
const { cleaned, comments } = stripUnderscoreComments(readJsonStrict(jsonPath, jsonPath));
|
|
423
|
+
const cleanedObj = cleaned;
|
|
424
|
+
const doc = new Document(cleanedObj);
|
|
425
|
+
const sectionComments = opts.sectionComments ?? MAIN_CONFIG_SECTION_COMMENTS;
|
|
426
|
+
for (const [key, text] of Object.entries(sectionComments)) if (key in cleanedObj) setYamlKeyComment(doc, [key], text);
|
|
427
|
+
for (const c of comments) setYamlKeyComment(doc, c.path, c.text);
|
|
428
|
+
const yamlText = String(doc);
|
|
429
|
+
if (!deepEqualJson(parseDocument(yamlText).toJS(), cleanedObj)) throw new MainConfigError(`${yamlPath}: generated YAML does not re-parse to the same data as ${jsonPath} — aborting, nothing written`);
|
|
430
|
+
if (opts.dryRun) return {
|
|
431
|
+
yamlPath,
|
|
432
|
+
yamlText,
|
|
433
|
+
backupPath: null,
|
|
434
|
+
dryRun: true
|
|
435
|
+
};
|
|
436
|
+
writeYamlFileAtomic(yamlPath, yamlText, { label: yamlPath });
|
|
437
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
438
|
+
const backupPath = join(dirname(jsonPath), `${basename(jsonPath)}.migrated-${stamp}`);
|
|
439
|
+
renameSync(jsonPath, backupPath);
|
|
440
|
+
return {
|
|
441
|
+
yamlPath,
|
|
442
|
+
yamlText,
|
|
443
|
+
backupPath,
|
|
444
|
+
dryRun: false
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/daemon/config.ts
|
|
450
|
+
/**
|
|
451
|
+
* config.ts — Configuration loader for PAI Daemon
|
|
452
|
+
*
|
|
453
|
+
* Loads config from ~/.claude/pai/config.json (the pre-2026-09-19 location
|
|
454
|
+
* was ~/.config/pai/config.json, briefly ~/.claude/pai.json in between —
|
|
455
|
+
* see paiConfigFilePath/migrateConfigFile).
|
|
456
|
+
* Deep-merges with defaults so partial configs work fine.
|
|
457
|
+
* Expands ~ in path values at runtime.
|
|
458
|
+
*/
|
|
459
|
+
/** Derive a per-user Postgres database name: pai_<username> */
|
|
460
|
+
function perUserDbName() {
|
|
461
|
+
return `pai_${userInfo().username.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase()}`;
|
|
462
|
+
}
|
|
463
|
+
/** Derive the per-user connection string */
|
|
464
|
+
function perUserConnectionString() {
|
|
465
|
+
return `postgresql://pai:pai@localhost:5432/${perUserDbName()}`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* idleMinutes=50 sits under the 60-minute ephemeral-1h TTL with margin for
|
|
469
|
+
* scheduler jitter; maxBeats=6 (~5h of coverage at one beat per idle stretch)
|
|
470
|
+
* and activeHours 08:00-22:00 keep an unattended overnight machine from
|
|
471
|
+
* beating all night for a session nobody will return to before the cache
|
|
472
|
+
* would have expired anyway.
|
|
473
|
+
*/
|
|
474
|
+
const DEFAULT_SESSIONS_CACHE_KEEPALIVE = {
|
|
475
|
+
enabled: false,
|
|
476
|
+
idleMinutes: 50,
|
|
477
|
+
maxBeats: 6,
|
|
478
|
+
activeHours: "08:00-22:00",
|
|
479
|
+
minContextTokens: 2e4,
|
|
480
|
+
prompt: "keepalive"
|
|
481
|
+
};
|
|
482
|
+
const DEFAULTS = {
|
|
483
|
+
socketPath: paiSocketPath(),
|
|
484
|
+
indexIntervalSecs: 300,
|
|
485
|
+
embedIntervalSecs: 600,
|
|
486
|
+
embedOnStartup: false,
|
|
487
|
+
storageBackend: "sqlite",
|
|
488
|
+
postgres: {
|
|
489
|
+
connectionString: perUserConnectionString(),
|
|
490
|
+
maxConnections: 5,
|
|
491
|
+
connectionTimeoutMs: 5e3
|
|
492
|
+
},
|
|
493
|
+
embeddingModel: "Snowflake/snowflake-arctic-embed-m-v1.5",
|
|
494
|
+
logLevel: "info",
|
|
495
|
+
notifications: DEFAULT_NOTIFICATION_CONFIG,
|
|
496
|
+
tasks: DEFAULT_TASK_CONFIG,
|
|
497
|
+
identity: { selfEmails: [] },
|
|
498
|
+
sessions: { cacheKeepalive: { ...DEFAULT_SESSIONS_CACHE_KEEPALIVE } },
|
|
499
|
+
search: {
|
|
500
|
+
mode: "keyword",
|
|
501
|
+
rerank: true,
|
|
502
|
+
recencyBoostDays: 90,
|
|
503
|
+
defaultLimit: 10,
|
|
504
|
+
snippetLength: 200
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
/** Config template — generated at runtime so the DB name is per-user */
|
|
508
|
+
function configTemplate() {
|
|
509
|
+
return `{
|
|
510
|
+
"socketPath": "/tmp/pai.sock",
|
|
511
|
+
"indexIntervalSecs": 300,
|
|
512
|
+
"embedIntervalSecs": 600,
|
|
513
|
+
"storageBackend": "sqlite",
|
|
514
|
+
"postgres": {
|
|
515
|
+
"connectionString": "${perUserConnectionString()}",
|
|
516
|
+
"maxConnections": 5,
|
|
517
|
+
"connectionTimeoutMs": 5000
|
|
518
|
+
},
|
|
519
|
+
"embeddingModel": "Snowflake/snowflake-arctic-embed-m-v1.5",
|
|
520
|
+
"logLevel": "info",
|
|
521
|
+
"vaultPath": "",
|
|
522
|
+
"vaultProjectId": 0,
|
|
523
|
+
"search": {
|
|
524
|
+
"mode": "keyword",
|
|
525
|
+
"rerank": true,
|
|
526
|
+
"recencyBoostDays": 90,
|
|
527
|
+
"defaultLimit": 10,
|
|
528
|
+
"snippetLength": 200
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
`;
|
|
532
|
+
}
|
|
533
|
+
/** Expand a leading ~ to the real home directory */
|
|
534
|
+
function expandHome(p) {
|
|
535
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(1));
|
|
536
|
+
return p;
|
|
537
|
+
}
|
|
538
|
+
/** Canonical location since 2026-09-19: under the PAI_HOME namespace dir,
|
|
539
|
+
* so nothing PAI writes can collide with a file Claude Code itself owns. */
|
|
540
|
+
const NEW_CONFIG_FILE = paiHomePath("config.json");
|
|
541
|
+
/** Briefly the canonical location between 2026-09-19's two migrations —
|
|
542
|
+
* read during the transition, never written to again. */
|
|
543
|
+
const OLD_CONFIG_FILE = join(homedir(), ".claude", "pai.json");
|
|
544
|
+
/** Where the config lived before 2026-09-19 — read during the transition,
|
|
545
|
+
* never written to once NEW_CONFIG_FILE exists (see `pai config migrate`). */
|
|
546
|
+
const LEGACY_CONFIG_FILE = join(homedir(), ".config", "pai", "config.json");
|
|
547
|
+
/**
|
|
548
|
+
* The path any read/write of the PAI config actually uses: PAI_CONFIG_FILE
|
|
549
|
+
* (tests, power users) first, else the new PAI_HOME location if it exists,
|
|
550
|
+
* else the most recent old location that is actually on disk (printing a
|
|
551
|
+
* one-time notice), else the new location (the target a first write creates).
|
|
552
|
+
*/
|
|
553
|
+
function paiConfigFilePath() {
|
|
554
|
+
const override = process.env.PAI_CONFIG_FILE;
|
|
555
|
+
if (override) return override;
|
|
556
|
+
return resolvePaiFile(NEW_CONFIG_FILE, [OLD_CONFIG_FILE, LEGACY_CONFIG_FILE], "pai config migrate");
|
|
557
|
+
}
|
|
558
|
+
const CONFIG_FILE = paiConfigFilePath();
|
|
559
|
+
const CONFIG_DIR = dirname(CONFIG_FILE);
|
|
560
|
+
/** config.yaml — the canonical main config once `pai config yaml` has run,
|
|
561
|
+
* read/written by readMainConfigRaw/writeMainConfigRaw instead of
|
|
562
|
+
* CONFIG_FILE whenever it exists (see src/config/main-config.ts). */
|
|
563
|
+
function paiConfigYamlFilePath() {
|
|
564
|
+
return yamlSiblingPath(CONFIG_FILE);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Read the raw main config object exactly as every non-typed writer
|
|
568
|
+
* (identity, notifications, obsidian, setup, workers, memory settings) needs
|
|
569
|
+
* it: config.yaml when it exists, else CONFIG_FILE. `path` overrides the
|
|
570
|
+
* JSON location (tests, or workers/config.ts's parametrized CONFIG_FILE) —
|
|
571
|
+
* its YAML sibling (same dir, `config.yaml`) is what gets preferred.
|
|
572
|
+
*/
|
|
573
|
+
function readMainConfigRaw(path = CONFIG_FILE) {
|
|
574
|
+
try {
|
|
575
|
+
return readDualFormatConfigRaw(path);
|
|
576
|
+
} catch (e) {
|
|
577
|
+
throw e instanceof MainConfigError ? new Error(e.message) : e;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Write the raw main config object back through the same seam: a
|
|
582
|
+
* comment-preserving sync into config.yaml when it exists, else a plain
|
|
583
|
+
* writeJsonAtomic to `path`. Every writer of the main config must call this
|
|
584
|
+
* instead of touching CONFIG_FILE directly, so a config.yaml on disk is
|
|
585
|
+
* honored no matter which of them made the change.
|
|
586
|
+
*/
|
|
587
|
+
function writeMainConfigRaw(raw, path = CONFIG_FILE) {
|
|
588
|
+
try {
|
|
589
|
+
writeDualFormatConfigRaw(path, raw);
|
|
590
|
+
} catch (e) {
|
|
591
|
+
throw e instanceof MainConfigError ? new Error(e.message) : e;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* `pai config migrate`: move config.json (from ~/.claude/pai.json or
|
|
596
|
+
* ~/.config/pai/config.json, whichever is found) to ~/.claude/pai/config.json
|
|
597
|
+
* byte-for-byte, verify the copy, then rename the old file aside as
|
|
598
|
+
* config.json.migrated-<YYYYMMDD> (never deleted).
|
|
599
|
+
*/
|
|
600
|
+
function migrateConfigFile(opts = {}) {
|
|
601
|
+
return migratePaiFile(NEW_CONFIG_FILE, [OLD_CONFIG_FILE, LEGACY_CONFIG_FILE], opts);
|
|
602
|
+
}
|
|
603
|
+
function deepMerge(target, source) {
|
|
604
|
+
const result = { ...target };
|
|
605
|
+
for (const key of Object.keys(source)) {
|
|
606
|
+
const srcVal = source[key];
|
|
607
|
+
if (srcVal === void 0 || srcVal === null) continue;
|
|
608
|
+
const tgtVal = target[key];
|
|
609
|
+
if (typeof srcVal === "object" && !Array.isArray(srcVal) && typeof tgtVal === "object" && tgtVal !== null && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
|
|
610
|
+
else result[key] = srcVal;
|
|
611
|
+
}
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Load configuration: config.yaml (see paiConfigYamlFilePath) if it exists,
|
|
616
|
+
* else CONFIG_FILE (see paiConfigFilePath), else defaults. Returns defaults
|
|
617
|
+
* deep-merged with any values found in the file.
|
|
618
|
+
*/
|
|
619
|
+
function loadConfig() {
|
|
620
|
+
if (!existsSync(CONFIG_FILE) && !existsSync(paiConfigYamlFilePath())) return { ...DEFAULTS };
|
|
621
|
+
let parsed;
|
|
622
|
+
try {
|
|
623
|
+
parsed = readMainConfigRaw();
|
|
624
|
+
} catch (e) {
|
|
625
|
+
process.stderr.write(`[pai-daemon] Could not read config: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
626
|
+
return { ...DEFAULTS };
|
|
627
|
+
}
|
|
628
|
+
if (parsed.obsidianVaultPath && !parsed.vaultPath) {
|
|
629
|
+
parsed.vaultPath = parsed.obsidianVaultPath;
|
|
630
|
+
process.stderr.write(`[pai-daemon] Config: mapped obsidianVaultPath → vaultPath (${parsed.vaultPath})\n`);
|
|
631
|
+
}
|
|
632
|
+
return deepMerge(DEFAULTS, parsed);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Ensure CONFIG_DIR exists and write a default config template to CONFIG_FILE
|
|
636
|
+
* if none exists yet. Call this only from the `serve` command.
|
|
637
|
+
*/
|
|
638
|
+
function ensureConfigDir() {
|
|
639
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
640
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
641
|
+
process.stderr.write(`[pai-daemon] Created config directory: ${CONFIG_DIR}\n`);
|
|
642
|
+
}
|
|
643
|
+
if (!existsSync(CONFIG_FILE) && !existsSync(paiConfigYamlFilePath())) try {
|
|
644
|
+
writeFileSync(CONFIG_FILE, configTemplate(), "utf-8");
|
|
645
|
+
process.stderr.write(`[pai-daemon] Wrote default config to: ${CONFIG_FILE}\n`);
|
|
646
|
+
} catch (e) {
|
|
647
|
+
process.stderr.write(`[pai-daemon] Could not write default config: ${e}\n`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
//#endregion
|
|
652
|
+
export { DEFAULT_NOTIFICATION_CONFIG as S, writeYamlFileAtomic as _, ensureConfigDir as a, OWNER_LABEL_PREFIX as b, migrateConfigFile as c, readMainConfigRaw as d, writeMainConfigRaw as f, YamlStoreError as g, resolvedMainConfigPath as h, DEFAULT_SESSIONS_CACHE_KEEPALIVE as i, paiConfigFilePath as l, migrateMainConfigToYaml as m, CONFIG_FILE as n, expandHome as o, MainConfigError as p, DEFAULTS as r, loadConfig as s, CONFIG_DIR as t, paiConfigYamlFilePath as u, readJsonStrict as v, UNROUTED as x, writeJsonAtomic as y };
|
|
653
|
+
//# sourceMappingURL=config-Bn9jpblO.mjs.map
|