@oh-my-pi/pi-utils 14.5.12 → 14.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/frontmatter.ts +17 -7
- package/src/logger.ts +217 -65
- package/src/prompt.ts +10 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "14.5.
|
|
4
|
+
"version": "14.5.13",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/bun": "^1.3",
|
|
41
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
41
|
+
"@oh-my-pi/pi-natives": "14.5.13"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"bun": ">=1.3.7"
|
package/src/frontmatter.ts
CHANGED
|
@@ -8,23 +8,33 @@ function stripHtmlComments(content: string): string {
|
|
|
8
8
|
|
|
9
9
|
/** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */
|
|
10
10
|
function kebabToCamel(key: string): string {
|
|
11
|
+
if (!key.includes("-")) return key;
|
|
11
12
|
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/** Recursively normalize object keys from kebab-case to camelCase */
|
|
15
16
|
function normalizeKeys<T>(obj: T): T {
|
|
16
|
-
if (obj === null || typeof obj !== "object")
|
|
17
|
-
return obj;
|
|
18
|
-
}
|
|
17
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
19
18
|
if (Array.isArray(obj)) {
|
|
20
|
-
|
|
19
|
+
let changed = false;
|
|
20
|
+
const out: unknown[] = new Array(obj.length);
|
|
21
|
+
for (let i = 0; i < obj.length; i++) {
|
|
22
|
+
const v = obj[i];
|
|
23
|
+
const nv = normalizeKeys(v);
|
|
24
|
+
out[i] = nv;
|
|
25
|
+
if (nv !== v) changed = true;
|
|
26
|
+
}
|
|
27
|
+
return (changed ? (out as unknown) : obj) as T;
|
|
21
28
|
}
|
|
29
|
+
let changed = false;
|
|
22
30
|
const result: Record<string, unknown> = {};
|
|
23
31
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
24
|
-
const
|
|
25
|
-
|
|
32
|
+
const nk = key.includes("-") ? kebabToCamel(key) : key;
|
|
33
|
+
const nv = normalizeKeys(value);
|
|
34
|
+
result[nk] = nv;
|
|
35
|
+
if (nk !== key || nv !== value) changed = true;
|
|
26
36
|
}
|
|
27
|
-
return result as T;
|
|
37
|
+
return (changed ? result : obj) as T;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export class FrontmatterError extends Error {
|
package/src/logger.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Logs to ~/.omp/logs/ with size-based rotation, supporting concurrent omp instances.
|
|
5
5
|
* Each log entry includes process.pid for traceability.
|
|
6
6
|
*/
|
|
7
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
import * as fs from "node:fs";
|
|
8
9
|
import winston from "winston";
|
|
9
10
|
import DailyRotateFile from "winston-daily-rotate-file";
|
|
@@ -96,109 +97,260 @@ export function debug(message: string, context?: Record<string, unknown>): void
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
const LOGGED_TIMING_THRESHOLD_MS = 5;
|
|
100
|
+
const LOGGED_TIMING_THRESHOLD_MS = 0.5;
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
interface Span {
|
|
103
|
+
op: string;
|
|
104
|
+
start: number;
|
|
105
|
+
end?: number;
|
|
106
|
+
parent?: Span;
|
|
107
|
+
children: Span[];
|
|
108
|
+
/** Marker / point event without a duration. */
|
|
109
|
+
point?: boolean;
|
|
110
|
+
}
|
|
106
111
|
|
|
107
|
-
|
|
112
|
+
const spanStorage = new AsyncLocalStorage<Span>();
|
|
113
|
+
let gRootSpan: Span | undefined;
|
|
108
114
|
let gRecordTimings = false;
|
|
109
115
|
|
|
110
116
|
/**
|
|
111
|
-
* Print collected timings
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* for those awaits instead.
|
|
117
|
+
* Print collected timings as an indented tree.
|
|
118
|
+
* Each span shows wall duration; parents with children also show "(self)" for unattributed time.
|
|
119
|
+
* Sibling spans are sorted by start time. Spans whose intervals overlap with siblings ran in parallel.
|
|
115
120
|
*/
|
|
116
121
|
export function printTimings(): void {
|
|
117
|
-
if (!gRecordTimings ||
|
|
122
|
+
if (!gRecordTimings || !gRootSpan) {
|
|
118
123
|
console.error("\n--- Startup Timings ---\n(no markers)\n");
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (dur > LOGGED_TIMING_THRESHOLD_MS) {
|
|
132
|
-
console.error(` ${op}: ${dur}ms`);
|
|
133
|
-
}
|
|
127
|
+
gRootSpan.end = performance.now();
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push("--- Startup timings (hierarchical) ---");
|
|
131
|
+
const work: Span[] = [];
|
|
132
|
+
const loads: Span[] = [];
|
|
133
|
+
for (const child of gRootSpan.children) {
|
|
134
|
+
if (isModuleLoadSpan(child)) loads.push(child);
|
|
135
|
+
else work.push(child);
|
|
134
136
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (gAsyncSpans.length > 0) {
|
|
138
|
-
console.error("\n--- Async (await-accurate; parallel spans may overlap) ---");
|
|
139
|
-
for (const [op, dur] of gAsyncSpans) {
|
|
140
|
-
if (dur > LOGGED_TIMING_THRESHOLD_MS) {
|
|
141
|
-
console.error(` ${op}: ${dur}ms`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
137
|
+
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
138
|
+
printSpan(child, 0, lines);
|
|
144
139
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
if (loads.length > 0) {
|
|
141
|
+
printModuleLoadSummary(loads, 0, lines);
|
|
142
|
+
}
|
|
143
|
+
const totalMs = (gRootSpan.end - gRootSpan.start).toFixed(1);
|
|
144
|
+
lines.push(`Total: ${totalMs}ms`);
|
|
145
|
+
lines.push("--------------------------------------");
|
|
146
|
+
lines.push("");
|
|
147
|
+
console.error(lines.join("\n"));
|
|
148
|
+
gRootSpan.end = undefined;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
* Begin recording startup timings
|
|
152
|
+
* Begin recording startup timings under a new root span.
|
|
153
|
+
* Idempotent: a second call while already recording is a no-op so that side-effect
|
|
154
|
+
* starters (see module-timer.ts) and explicit starters (main.ts) can coexist.
|
|
153
155
|
*/
|
|
154
156
|
export function startTiming(): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
if (gRecordTimings) return;
|
|
158
|
+
gRootSpan = {
|
|
159
|
+
op: "(root)",
|
|
160
|
+
start: performance.now(),
|
|
161
|
+
parent: undefined,
|
|
162
|
+
children: [],
|
|
163
|
+
};
|
|
157
164
|
gRecordTimings = true;
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Record an externally-measured span as a leaf child of the active span (or root
|
|
169
|
+
* when no span is active). Used by the module-load timing plugin to splice load
|
|
170
|
+
* events into the tree retroactively.
|
|
171
|
+
*/
|
|
172
|
+
export function recordModuleLoadSpan(path: string, start: number, durationMs: number): void {
|
|
173
|
+
if (!gRecordTimings || !gRootSpan) return;
|
|
174
|
+
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
175
|
+
const span: Span = {
|
|
176
|
+
op: `load:${shortenLoadPath(path)}`,
|
|
177
|
+
start,
|
|
178
|
+
end: start + durationMs,
|
|
179
|
+
parent,
|
|
180
|
+
children: [],
|
|
181
|
+
};
|
|
182
|
+
parent.children.push(span);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function shortenLoadPath(p: string): string {
|
|
186
|
+
const cwd = process.cwd();
|
|
187
|
+
if (p.startsWith(`${cwd}/`)) return p.slice(cwd.length + 1);
|
|
188
|
+
const home = process.env.HOME;
|
|
189
|
+
if (home && p.startsWith(`${home}/`)) return `~/${p.slice(home.length + 1)}`;
|
|
190
|
+
return p;
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
/**
|
|
161
194
|
* End timing window and clear buffers.
|
|
162
195
|
*/
|
|
163
196
|
export function endTiming(): void {
|
|
164
|
-
|
|
165
|
-
gAsyncSpans = [];
|
|
197
|
+
gRootSpan = undefined;
|
|
166
198
|
gRecordTimings = false;
|
|
167
199
|
}
|
|
168
200
|
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
function durationOf(span: Span): number {
|
|
202
|
+
if (span.point || span.end === undefined) return 0;
|
|
203
|
+
return span.end - span.start;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Self time = total - union of child intervals (handles parallel children correctly). */
|
|
207
|
+
function selfTimeOf(span: Span): number {
|
|
208
|
+
const dur = durationOf(span);
|
|
209
|
+
if (span.children.length === 0 || span.point) return dur;
|
|
210
|
+
const intervals = span.children
|
|
211
|
+
.filter(c => !c.point && c.end !== undefined)
|
|
212
|
+
.map(c => [c.start, c.end as number] as const)
|
|
213
|
+
.sort((a, b) => a[0] - b[0]);
|
|
214
|
+
if (intervals.length === 0) return dur;
|
|
215
|
+
let union = 0;
|
|
216
|
+
let curStart = intervals[0][0];
|
|
217
|
+
let curEnd = intervals[0][1];
|
|
218
|
+
for (let i = 1; i < intervals.length; i++) {
|
|
219
|
+
const [s, e] = intervals[i];
|
|
220
|
+
if (s > curEnd) {
|
|
221
|
+
union += curEnd - curStart;
|
|
222
|
+
curStart = s;
|
|
223
|
+
curEnd = e;
|
|
224
|
+
} else if (e > curEnd) {
|
|
225
|
+
curEnd = e;
|
|
226
|
+
}
|
|
173
227
|
}
|
|
228
|
+
union += curEnd - curStart;
|
|
229
|
+
return Math.max(0, dur - union);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fmtMs(ms: number): string {
|
|
233
|
+
if (ms < 1) return `${ms.toFixed(2)}ms`;
|
|
234
|
+
if (ms < 100) return `${ms.toFixed(1)}ms`;
|
|
235
|
+
return `${ms.toFixed(0)}ms`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const MODULE_LOAD_PREFIX = "load:";
|
|
239
|
+
const MODULE_LOAD_VERBOSE_TOP = 10;
|
|
240
|
+
|
|
241
|
+
function isModuleLoadSpan(span: Span): boolean {
|
|
242
|
+
return span.op.startsWith(MODULE_LOAD_PREFIX);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function printSpan(span: Span, depth: number, lines: string[]): void {
|
|
246
|
+
const indent = " ".repeat(depth);
|
|
247
|
+
if (span.point) {
|
|
248
|
+
lines.push(`${indent}• ${span.op}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const dur = durationOf(span);
|
|
252
|
+
if (dur < LOGGED_TIMING_THRESHOLD_MS && span.children.length === 0) return;
|
|
253
|
+
const parallel = isParallel(span);
|
|
254
|
+
const tag = parallel ? " [parallel]" : "";
|
|
255
|
+
const self = selfTimeOf(span);
|
|
256
|
+
const selfStr = span.children.length > 0 && self > LOGGED_TIMING_THRESHOLD_MS ? ` (self ${fmtMs(self)})` : "";
|
|
257
|
+
lines.push(`${indent}${span.op}: ${fmtMs(dur)}${selfStr}${tag}`);
|
|
258
|
+
|
|
259
|
+
// Split children into work spans and module-load spans for summarization.
|
|
260
|
+
const work: Span[] = [];
|
|
261
|
+
const loads: Span[] = [];
|
|
262
|
+
for (const child of span.children) {
|
|
263
|
+
if (isModuleLoadSpan(child)) loads.push(child);
|
|
264
|
+
else work.push(child);
|
|
265
|
+
}
|
|
266
|
+
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
267
|
+
printSpan(child, depth + 1, lines);
|
|
268
|
+
}
|
|
269
|
+
if (loads.length > 0) {
|
|
270
|
+
printModuleLoadSummary(loads, depth + 1, lines);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Collapse the (typically hundreds of) module-load spans into one summary line. */
|
|
275
|
+
function printModuleLoadSummary(loads: Span[], depth: number, lines: string[]): void {
|
|
276
|
+
const childIndent = " ".repeat(depth);
|
|
277
|
+
const grandIndent = " ".repeat(depth + 1);
|
|
278
|
+
let unionStart = Number.POSITIVE_INFINITY;
|
|
279
|
+
let unionEnd = 0;
|
|
280
|
+
let totalSelf = 0;
|
|
281
|
+
for (const span of loads) {
|
|
282
|
+
if (span.end === undefined) continue;
|
|
283
|
+
if (span.start < unionStart) unionStart = span.start;
|
|
284
|
+
if (span.end > unionEnd) unionEnd = span.end;
|
|
285
|
+
totalSelf += span.end - span.start;
|
|
286
|
+
}
|
|
287
|
+
const wall = unionEnd > unionStart ? unionEnd - unionStart : 0;
|
|
288
|
+
lines.push(`${childIndent}(modules): ${loads.length} loaded, wall ${fmtMs(wall)}, sum ${fmtMs(totalSelf)}`);
|
|
289
|
+
const showAll = process.env.PI_TIMING === "full";
|
|
290
|
+
const sorted = [...loads].sort((a, b) => durationOf(b) - durationOf(a));
|
|
291
|
+
const visible = showAll ? sorted : sorted.slice(0, MODULE_LOAD_VERBOSE_TOP);
|
|
292
|
+
for (const span of visible) {
|
|
293
|
+
const dur = durationOf(span);
|
|
294
|
+
if (dur < LOGGED_TIMING_THRESHOLD_MS) break;
|
|
295
|
+
const tag = isParallel(span) ? " [parallel]" : "";
|
|
296
|
+
lines.push(`${grandIndent}${span.op}: ${fmtMs(dur)}${tag}`);
|
|
297
|
+
}
|
|
298
|
+
if (!showAll && sorted.length > MODULE_LOAD_VERBOSE_TOP) {
|
|
299
|
+
lines.push(`${grandIndent}… ${sorted.length - MODULE_LOAD_VERBOSE_TOP} more (PI_TIMING=full to show all)`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** A span is parallel if it overlaps a sibling that started before it. */
|
|
304
|
+
function isParallel(span: Span): boolean {
|
|
305
|
+
const parent = span.parent;
|
|
306
|
+
if (!parent || span.end === undefined) return false;
|
|
307
|
+
for (const sibling of parent.children) {
|
|
308
|
+
if (sibling === span || sibling.end === undefined || sibling.point) continue;
|
|
309
|
+
// Overlap test: A overlaps B iff A.start < B.end && B.start < A.end
|
|
310
|
+
if (sibling.start < span.end && span.start < sibling.end) return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
174
313
|
}
|
|
175
314
|
|
|
176
315
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
316
|
+
* Time a span. Three forms:
|
|
317
|
+
* time(op) — point event (zero-duration breadcrumb)
|
|
318
|
+
* time(op, fn, ...args) — wrap fn in a span; returns fn's return value (sync or Promise)
|
|
319
|
+
*
|
|
320
|
+
* Spans nest hierarchically via AsyncLocalStorage: a child started inside another span's fn
|
|
321
|
+
* (even across awaits) becomes that span's child. Parallel children are recorded as siblings
|
|
322
|
+
* with overlapping intervals.
|
|
179
323
|
*/
|
|
180
324
|
export function time(op: string): void;
|
|
181
325
|
export function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T;
|
|
182
326
|
export function time<T, A extends unknown[]>(op: string, fn?: (...args: A) => T, ...args: A): T | undefined {
|
|
327
|
+
if (!gRecordTimings || !gRootSpan) {
|
|
328
|
+
if (fn === undefined) return undefined as T;
|
|
329
|
+
return fn(...args);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
333
|
+
const span: Span = { op, start: performance.now(), parent, children: [] };
|
|
334
|
+
parent.children.push(span);
|
|
335
|
+
|
|
183
336
|
if (fn === undefined) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
337
|
+
span.end = span.start;
|
|
338
|
+
span.point = true;
|
|
187
339
|
return undefined as T;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return result;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
recordAsyncSpan(op, start);
|
|
199
|
-
throw error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const finish = (): void => {
|
|
343
|
+
span.end = performance.now();
|
|
344
|
+
};
|
|
345
|
+
try {
|
|
346
|
+
const result = spanStorage.run(span, () => fn(...args));
|
|
347
|
+
if (result instanceof Promise) {
|
|
348
|
+
return result.finally(finish) as T;
|
|
200
349
|
}
|
|
201
|
-
|
|
202
|
-
return
|
|
350
|
+
finish();
|
|
351
|
+
return result;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
finish();
|
|
354
|
+
throw error;
|
|
203
355
|
}
|
|
204
356
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -418,8 +418,17 @@ function disambiguateClosingBraces(template: string): string {
|
|
|
418
418
|
return template.replace(/\}\}(\}+)/g, "}}{{!---}}$1");
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
const compiledTemplateCache = new Map<string, (context: TemplateContext) => string>();
|
|
422
|
+
|
|
421
423
|
export function compile(template: string): (context: TemplateContext) => string {
|
|
422
|
-
|
|
424
|
+
const disambiguated = disambiguateClosingBraces(template);
|
|
425
|
+
const cached = compiledTemplateCache.get(disambiguated);
|
|
426
|
+
if (cached) return cached;
|
|
427
|
+
const compiled = handlebars.compile(disambiguated, { noEscape: true, strict: false }) as (
|
|
428
|
+
context: TemplateContext,
|
|
429
|
+
) => string;
|
|
430
|
+
compiledTemplateCache.set(disambiguated, compiled);
|
|
431
|
+
return compiled;
|
|
423
432
|
}
|
|
424
433
|
|
|
425
434
|
export function render(template: string, context: TemplateContext = {}): string {
|