@pithos-kit/squiggle 0.4.1 → 0.5.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/README.md +1 -1
- package/extensions/{squiggle.ts → index.ts} +21 -7
- package/extensions/logging.ts +152 -0
- package/index.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { complete, type UserMessage } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { createPithosLogger, errorMetadata, modelMetadata, usageMetadata, type PithosLogger } from "./logging.ts";
|
|
5
6
|
|
|
6
7
|
const SQUIGGLE_HELP = `Usage: /squiggle toggle
|
|
7
8
|
|
|
@@ -19,17 +20,21 @@ Options:
|
|
|
19
20
|
|
|
20
21
|
export function registerSquiggle(
|
|
21
22
|
pi: ExtensionAPI,
|
|
22
|
-
correctPrompt: (input: string, ctx: ExtensionContext, config: SquiggleConfig) => Promise<string | null> = correctWithModel,
|
|
23
|
+
correctPrompt: (input: string, ctx: ExtensionContext, config: SquiggleConfig, log?: PithosLogger) => Promise<string | null> = correctWithModel,
|
|
23
24
|
) {
|
|
25
|
+
const log = createPithosLogger();
|
|
26
|
+
log.info("extension.register");
|
|
24
27
|
let runtimeMode: SquiggleConfig["mode"] | undefined;
|
|
25
28
|
|
|
26
|
-
pi.on("session_start", async (
|
|
29
|
+
pi.on("session_start", async (event, ctx) => {
|
|
27
30
|
runtimeMode = restoreRuntimeMode(ctx);
|
|
31
|
+
log.info("session.start", { reason: event.reason, sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), runtimeMode });
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
pi.registerCommand("squiggle", {
|
|
31
35
|
description: "Toggle squiggle on/off",
|
|
32
36
|
handler: async (args, ctx) => {
|
|
37
|
+
log.info("command.squiggle", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), args: args.trim() });
|
|
33
38
|
if (isHelpRequest(args)) return emitHelp(ctx, SQUIGGLE_HELP);
|
|
34
39
|
|
|
35
40
|
const command = args.trim().toLowerCase();
|
|
@@ -48,6 +53,7 @@ export function registerSquiggle(
|
|
|
48
53
|
pi.registerCommand("squiggle-status", {
|
|
49
54
|
description: "Show whether squiggle is loaded",
|
|
50
55
|
handler: async (args, ctx) => {
|
|
56
|
+
log.info("command.squiggle-status", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.() });
|
|
51
57
|
if (isHelpRequest(args)) return emitHelp(ctx, SQUIGGLE_STATUS_HELP);
|
|
52
58
|
ctx.ui.notify(formatStatus(ctx, loadEffectiveConfig(ctx.cwd, runtimeMode)), "info");
|
|
53
59
|
},
|
|
@@ -61,7 +67,7 @@ export function registerSquiggle(
|
|
|
61
67
|
if (!event.text.trim()) return { action: "continue" };
|
|
62
68
|
|
|
63
69
|
const stopIndicator = startSquiggleIndicator(ctx);
|
|
64
|
-
const corrected = await correctPrompt(event.text, ctx, config).finally(stopIndicator);
|
|
70
|
+
const corrected = await correctPrompt(event.text, ctx, config, log).finally(stopIndicator);
|
|
65
71
|
if (!corrected || corrected === event.text) return { action: "continue" };
|
|
66
72
|
|
|
67
73
|
if (ctx.hasUI) ctx.ui.notify(formatColoredDiff(event.text, corrected), "info");
|
|
@@ -102,12 +108,14 @@ type SquiggleConfig = {
|
|
|
102
108
|
maxInputChars: number;
|
|
103
109
|
};
|
|
104
110
|
|
|
105
|
-
async function correctWithModel(input: string, ctx: ExtensionContext, config: SquiggleConfig): Promise<string | null> {
|
|
111
|
+
async function correctWithModel(input: string, ctx: ExtensionContext, config: SquiggleConfig, log = createPithosLogger()): Promise<string | null> {
|
|
106
112
|
const model = selectCorrectionModel(ctx, config);
|
|
107
113
|
if (!model) return null;
|
|
108
114
|
if (input.length > config.maxInputChars) return null;
|
|
109
115
|
|
|
110
116
|
try {
|
|
117
|
+
const started = Date.now();
|
|
118
|
+
log.info("model.correct.start", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), inputChars: input.length, ...modelMetadata(model) });
|
|
111
119
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
112
120
|
if (!auth.ok || !auth.apiKey) return null;
|
|
113
121
|
|
|
@@ -123,14 +131,20 @@ async function correctWithModel(input: string, ctx: ExtensionContext, config: Sq
|
|
|
123
131
|
{ apiKey: auth.apiKey, headers: auth.headers },
|
|
124
132
|
);
|
|
125
133
|
|
|
126
|
-
if (response.stopReason === "aborted")
|
|
134
|
+
if (response.stopReason === "aborted") {
|
|
135
|
+
log.warn("model.correct.aborted", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), durationMs: Date.now() - started, ...modelMetadata(model), usage: usageMetadata(response.usage) });
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
127
138
|
|
|
128
|
-
|
|
139
|
+
const corrected = response.content
|
|
129
140
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
130
141
|
.map((c) => c.text)
|
|
131
142
|
.join("\n")
|
|
132
143
|
.trim();
|
|
133
|
-
|
|
144
|
+
log.info("model.correct.complete", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), durationMs: Date.now() - started, inputChars: input.length, changed: corrected !== input, ...modelMetadata(model), usage: usageMetadata(response.usage) });
|
|
145
|
+
return corrected;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
log.warn("model.correct.error", { sessionId: (ctx.sessionManager as { getSessionId?: () => string } | undefined)?.getSessionId?.(), inputChars: input.length, ...modelMetadata(model), error: errorMetadata(error) });
|
|
134
148
|
return null;
|
|
135
149
|
}
|
|
136
150
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const PACKAGE_NAME = "@pithos-kit/squiggle";
|
|
2
|
+
|
|
3
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export type PithosLogLevel = "debug" | "info" | "warn" | "error" | "off";
|
|
7
|
+
export type PithosLogger = ReturnType<typeof createPithosLogger>;
|
|
8
|
+
|
|
9
|
+
const LEVELS: Record<PithosLogLevel, number> = {
|
|
10
|
+
debug: 10,
|
|
11
|
+
info: 20,
|
|
12
|
+
warn: 30,
|
|
13
|
+
error: 40,
|
|
14
|
+
off: Number.POSITIVE_INFINITY,
|
|
15
|
+
};
|
|
16
|
+
const MAX_STRING_LENGTH = 500;
|
|
17
|
+
const MAX_ARRAY_LENGTH = 20;
|
|
18
|
+
const MAX_OBJECT_KEYS = 30;
|
|
19
|
+
const MAX_DEPTH = 4;
|
|
20
|
+
const SECRET_KEY_RE = /(api[-_]?key|authorization|bearer|cookie|credential|header|password|secret|(^|[-_])token($|[-_]))/iu;
|
|
21
|
+
|
|
22
|
+
export function createPithosLogger(packageName = PACKAGE_NAME, env: NodeJS.ProcessEnv = process.env) {
|
|
23
|
+
const level = normalizeLevel(env.PITHOS_LOG_LEVEL);
|
|
24
|
+
const file = resolveLogFile(packageName, env);
|
|
25
|
+
const enabled = level !== "off" && file !== undefined;
|
|
26
|
+
|
|
27
|
+
function write(levelName: Exclude<PithosLogLevel, "off">, event: string, metadata?: Record<string, unknown>): void {
|
|
28
|
+
if (!enabled || LEVELS[levelName] < LEVELS[level]) return;
|
|
29
|
+
const entry = JSON.stringify({
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
level: levelName,
|
|
32
|
+
package: packageName,
|
|
33
|
+
event,
|
|
34
|
+
...(metadata ? { metadata: sanitize(metadata) } : {}),
|
|
35
|
+
}) + "\n";
|
|
36
|
+
void mkdir(dirname(file), { recursive: true })
|
|
37
|
+
.then(() => appendFile(file, entry, "utf8"))
|
|
38
|
+
.catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
enabled,
|
|
43
|
+
level,
|
|
44
|
+
file,
|
|
45
|
+
debug: (event: string, metadata?: Record<string, unknown>) => write("debug", event, metadata),
|
|
46
|
+
info: (event: string, metadata?: Record<string, unknown>) => write("info", event, metadata),
|
|
47
|
+
warn: (event: string, metadata?: Record<string, unknown>) => write("warn", event, metadata),
|
|
48
|
+
error: (event: string, metadata?: Record<string, unknown>) => write("error", event, metadata),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function errorMetadata(error: unknown): Record<string, unknown> {
|
|
53
|
+
if (error instanceof Error) {
|
|
54
|
+
return {
|
|
55
|
+
name: error.name,
|
|
56
|
+
message: error.message,
|
|
57
|
+
...("code" in error ? { code: (error as { code?: unknown }).code } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { message: String(error) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function modelMetadata(model: unknown): Record<string, unknown> | undefined {
|
|
64
|
+
if (!model || typeof model !== "object") return undefined;
|
|
65
|
+
const record = model as Record<string, unknown>;
|
|
66
|
+
return {
|
|
67
|
+
...(typeof record.provider === "string" ? { provider: record.provider } : {}),
|
|
68
|
+
...(typeof record.id === "string" ? { model: record.id } : {}),
|
|
69
|
+
...(typeof record.api === "string" ? { api: record.api } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function usageMetadata(usage: unknown): Record<string, unknown> | undefined {
|
|
74
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
75
|
+
const record = usage as Record<string, unknown>;
|
|
76
|
+
const inputTokens = numberField(record, "inputTokens") ?? numberField(record, "input");
|
|
77
|
+
const outputTokens = numberField(record, "outputTokens") ?? numberField(record, "output");
|
|
78
|
+
const cacheReadTokens = numberField(record, "cacheReadTokens") ?? numberField(record, "cacheRead");
|
|
79
|
+
const cacheWriteTokens = numberField(record, "cacheWriteTokens") ?? numberField(record, "cacheWrite");
|
|
80
|
+
const totalTokens = numberField(record, "totalTokens") ?? sumNumbers(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
81
|
+
const costValue = record.cost;
|
|
82
|
+
const costUsd = numberField(record, "costUsd") ?? numberField(record, "spendUsd") ?? numberField(record, "cost")
|
|
83
|
+
?? (costValue && typeof costValue === "object" ? numberField(costValue as Record<string, unknown>, "total") : undefined);
|
|
84
|
+
const turns = numberField(record, "turns");
|
|
85
|
+
const normalized = {
|
|
86
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
87
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
88
|
+
...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
|
|
89
|
+
...(cacheWriteTokens !== undefined ? { cacheWriteTokens } : {}),
|
|
90
|
+
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
91
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
92
|
+
...(turns !== undefined ? { turns } : {}),
|
|
93
|
+
...(typeof record.source === "string" ? { source: record.source } : {}),
|
|
94
|
+
...(typeof record.estimated === "boolean" ? { estimated: record.estimated } : {}),
|
|
95
|
+
};
|
|
96
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeLevel(value: unknown): PithosLogLevel {
|
|
100
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error" || value === "off" ? value : "off";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveLogFile(packageName: string, env: NodeJS.ProcessEnv): string | undefined {
|
|
104
|
+
if (env.PITHOS_LOG_FILE?.trim()) return resolve(env.PITHOS_LOG_FILE);
|
|
105
|
+
if (!env.PITHOS_LOG_DIR?.trim()) return undefined;
|
|
106
|
+
const fileName = packageName.replace(/^@pithos-kit\//u, "pithos-").replace(/[^a-z0-9.-]+/giu, "-");
|
|
107
|
+
return resolve(env.PITHOS_LOG_DIR, `${fileName}.jsonl`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function numberField(record: Record<string, unknown>, key: string): number | undefined {
|
|
111
|
+
const value = record[key];
|
|
112
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sumNumbers(...values: Array<number | undefined>): number | undefined {
|
|
116
|
+
let total = 0;
|
|
117
|
+
let seen = false;
|
|
118
|
+
for (const value of values) {
|
|
119
|
+
if (value === undefined) continue;
|
|
120
|
+
total += value;
|
|
121
|
+
seen = true;
|
|
122
|
+
}
|
|
123
|
+
return seen ? total : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sanitize(value: unknown, depth = 0, key = ""): unknown {
|
|
127
|
+
if (SECRET_KEY_RE.test(key)) return "[redacted]";
|
|
128
|
+
if (value === null || value === undefined || typeof value === "boolean" || typeof value === "number") return value;
|
|
129
|
+
if (typeof value === "bigint") return value.toString();
|
|
130
|
+
if (typeof value === "string") return boundString(value);
|
|
131
|
+
if (value instanceof Error) return sanitize(errorMetadata(value), depth, key);
|
|
132
|
+
if (depth >= MAX_DEPTH) return "[truncated]";
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
const items = value.slice(0, MAX_ARRAY_LENGTH).map((item) => sanitize(item, depth + 1));
|
|
135
|
+
if (value.length > MAX_ARRAY_LENGTH) items.push(`[${value.length - MAX_ARRAY_LENGTH} more items]`);
|
|
136
|
+
return items;
|
|
137
|
+
}
|
|
138
|
+
if (typeof value === "object") {
|
|
139
|
+
const result: Record<string, unknown> = {};
|
|
140
|
+
const entries = Object.entries(value as Record<string, unknown>).slice(0, MAX_OBJECT_KEYS);
|
|
141
|
+
for (const [entryKey, entryValue] of entries) result[entryKey] = sanitize(entryValue, depth + 1, entryKey);
|
|
142
|
+
const extra = Object.keys(value as Record<string, unknown>).length - entries.length;
|
|
143
|
+
if (extra > 0) result.__truncatedKeys = extra;
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
return String(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function boundString(value: string): string {
|
|
150
|
+
if (value.length <= MAX_STRING_LENGTH) return value;
|
|
151
|
+
return `${value.slice(0, MAX_STRING_LENGTH)}…[${value.length - MAX_STRING_LENGTH} more chars]`;
|
|
152
|
+
}
|
package/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from "./extensions/
|
|
1
|
+
export { default } from "./extensions/index.ts";
|