llm-cli-gateway 1.0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/approval-manager.d.ts +43 -0
  5. package/dist/approval-manager.js +156 -0
  6. package/dist/async-job-manager.d.ts +57 -0
  7. package/dist/async-job-manager.js +334 -0
  8. package/dist/claude-mcp-config.d.ts +8 -0
  9. package/dist/claude-mcp-config.js +161 -0
  10. package/dist/config.d.ts +35 -0
  11. package/dist/config.js +56 -0
  12. package/dist/db.d.ts +48 -0
  13. package/dist/db.js +170 -0
  14. package/dist/executor.d.ts +30 -0
  15. package/dist/executor.js +315 -0
  16. package/dist/health.d.ts +20 -0
  17. package/dist/health.js +32 -0
  18. package/dist/index.d.ts +67 -0
  19. package/dist/index.js +1503 -0
  20. package/dist/logger.d.ts +6 -0
  21. package/dist/logger.js +5 -0
  22. package/dist/metrics.d.ts +23 -0
  23. package/dist/metrics.js +57 -0
  24. package/dist/migrate-sessions.d.ts +12 -0
  25. package/dist/migrate-sessions.js +145 -0
  26. package/dist/migrate.d.ts +2 -0
  27. package/dist/migrate.js +100 -0
  28. package/dist/model-registry.d.ts +10 -0
  29. package/dist/model-registry.js +346 -0
  30. package/dist/optimizer.d.ts +3 -0
  31. package/dist/optimizer.js +183 -0
  32. package/dist/process-monitor.d.ts +54 -0
  33. package/dist/process-monitor.js +146 -0
  34. package/dist/request-helpers.d.ts +25 -0
  35. package/dist/request-helpers.js +32 -0
  36. package/dist/resources.d.ts +26 -0
  37. package/dist/resources.js +201 -0
  38. package/dist/retry.d.ts +72 -0
  39. package/dist/retry.js +146 -0
  40. package/dist/review-integrity.d.ts +50 -0
  41. package/dist/review-integrity.js +283 -0
  42. package/dist/session-manager-pg.d.ts +76 -0
  43. package/dist/session-manager-pg.js +383 -0
  44. package/dist/session-manager.d.ts +62 -0
  45. package/dist/session-manager.js +223 -0
  46. package/dist/stream-json-parser.d.ts +35 -0
  47. package/dist/stream-json-parser.js +94 -0
  48. package/package.json +90 -0
@@ -0,0 +1,346 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
2
+ import { homedir } from "os";
3
+ import path from "path";
4
+ const FALLBACK_INFO = {
5
+ claude: {
6
+ description: "Anthropic's Claude Code CLI - best for code generation, analysis, and agentic coding tasks",
7
+ models: {
8
+ opus: "Most capable model. Best for: complex reasoning, nuanced analysis, difficult problems, research",
9
+ sonnet: "Balanced performance. Best for: everyday coding, code review, general tasks (default)",
10
+ haiku: "Fastest model. Best for: simple queries, quick answers, high-volume tasks, cost-sensitive use"
11
+ },
12
+ defaultModel: "sonnet",
13
+ modelOrder: ["opus", "sonnet", "haiku"]
14
+ },
15
+ codex: {
16
+ description: "OpenAI's Codex CLI - best for code execution in sandboxed environments",
17
+ models: {
18
+ "gpt-5.4": "Frontier coding and professional-work model. Best for: most Codex tasks, long-running agentic work",
19
+ "gpt-5.3-codex": "Specialized Codex model. Best for: agentic coding workflows with Codex-tuned behavior",
20
+ "gpt-5.2": "Strong general-purpose GPT-5 model. Best for: broad coding and reasoning tasks",
21
+ "gpt-5-pro": "Highest-capability GPT-5 model. Best for: deep reasoning and difficult professional workflows"
22
+ },
23
+ defaultModel: "gpt-5.4",
24
+ modelOrder: ["gpt-5.4", "gpt-5.3-codex", "gpt-5.2", "gpt-5-pro"]
25
+ },
26
+ gemini: {
27
+ description: "Google's Gemini CLI - best for multimodal tasks and Google ecosystem integration",
28
+ models: {
29
+ "gemini-2.5-pro": "Most capable model. Best for: complex reasoning, long context, multimodal",
30
+ "gemini-2.5-flash": "Fast model. Best for: quick responses, high throughput, cost-sensitive use"
31
+ }
32
+ }
33
+ };
34
+ const MODEL_CACHE_TTL_MS = 2 * 60 * 1000;
35
+ let cachedInfo = null;
36
+ export function getCliInfo(forceRefresh = false) {
37
+ if (!forceRefresh && cachedInfo && Date.now() - cachedInfo.loadedAt < MODEL_CACHE_TTL_MS) {
38
+ return cachedInfo.info;
39
+ }
40
+ const info = buildCliInfo();
41
+ cachedInfo = { loadedAt: Date.now(), info };
42
+ return info;
43
+ }
44
+ export function resolveModelAlias(cli, model, info) {
45
+ if (!model) {
46
+ return undefined;
47
+ }
48
+ const trimmed = model.trim();
49
+ if (!trimmed) {
50
+ return undefined;
51
+ }
52
+ const normalized = trimmed.toLowerCase();
53
+ const cliInfo = info[cli];
54
+ if (normalized === "default" || normalized === "latest") {
55
+ return cliInfo.defaultModel ?? trimmed;
56
+ }
57
+ if (cli === "gemini") {
58
+ if (normalized === "flash" || normalized === "pro") {
59
+ const picked = pickLatestMatching(cliInfo, normalized);
60
+ return picked ?? trimmed;
61
+ }
62
+ }
63
+ return trimmed;
64
+ }
65
+ function buildCliInfo() {
66
+ const info = {
67
+ claude: cloneInfo(FALLBACK_INFO.claude),
68
+ codex: cloneInfo(FALLBACK_INFO.codex),
69
+ gemini: cloneInfo(FALLBACK_INFO.gemini)
70
+ };
71
+ applyClaudeOverrides(info.claude);
72
+ applyCodexOverrides(info.codex);
73
+ applyGeminiOverrides(info.gemini);
74
+ return info;
75
+ }
76
+ function cloneInfo(source) {
77
+ return {
78
+ description: source.description,
79
+ models: { ...source.models },
80
+ defaultModel: source.defaultModel,
81
+ modelOrder: source.modelOrder ? [...source.modelOrder] : undefined
82
+ };
83
+ }
84
+ function applyClaudeOverrides(info) {
85
+ const settingsPath = path.join(homedir(), ".claude", "settings.json");
86
+ const settingsLocalPath = path.join(homedir(), ".claude", "settings.local.json");
87
+ const envDefault = process.env.CLAUDE_DEFAULT_MODEL;
88
+ const localDefault = readJsonValue(settingsLocalPath, "model");
89
+ const settingsDefault = readJsonValue(settingsPath, "model");
90
+ const defaultModel = envDefault || localDefault || settingsDefault;
91
+ const defaultSource = envDefault
92
+ ? "CLAUDE_DEFAULT_MODEL"
93
+ : localDefault
94
+ ? settingsLocalPath
95
+ : settingsPath;
96
+ if (defaultModel && typeof defaultModel === "string") {
97
+ if (!info.models[defaultModel]) {
98
+ info.models[defaultModel] = `Configured default from ${defaultSource}`;
99
+ }
100
+ info.defaultModel = defaultModel;
101
+ }
102
+ const envModels = parseEnvModels(process.env.CLAUDE_MODELS);
103
+ if (envModels.length > 0) {
104
+ envModels.forEach(model => {
105
+ if (!info.models[model]) {
106
+ info.models[model] = "Configured via CLAUDE_MODELS";
107
+ }
108
+ });
109
+ }
110
+ info.modelOrder = buildOrder(info, info.defaultModel);
111
+ }
112
+ function applyCodexOverrides(info) {
113
+ const configPath = process.env.CODEX_CONFIG_PATH || path.join(homedir(), ".codex", "config.toml");
114
+ const envDefault = process.env.CODEX_DEFAULT_MODEL;
115
+ const envModels = parseEnvModels(process.env.CODEX_MODELS);
116
+ const detectedModels = {};
117
+ let defaultModel;
118
+ if (existsSync(configPath)) {
119
+ const content = readFileSync(configPath, "utf-8");
120
+ const model = extractTomlString(content, "model");
121
+ if (model) {
122
+ detectedModels[model] = `Default from ${configPath}`;
123
+ defaultModel = model;
124
+ }
125
+ const migrations = extractTomlTableMap(content, "notice.model_migrations");
126
+ Object.entries(migrations).forEach(([from, to]) => {
127
+ if (!detectedModels[to]) {
128
+ detectedModels[to] = `Migrated from ${from}`;
129
+ }
130
+ if (!detectedModels[from]) {
131
+ detectedModels[from] = `Legacy model (migrates to ${to})`;
132
+ }
133
+ });
134
+ }
135
+ envModels.forEach(model => {
136
+ if (!detectedModels[model]) {
137
+ detectedModels[model] = "Configured via CODEX_MODELS";
138
+ }
139
+ });
140
+ if (envDefault) {
141
+ detectedModels[envDefault] = detectedModels[envDefault] || "Default from CODEX_DEFAULT_MODEL";
142
+ defaultModel = envDefault;
143
+ }
144
+ if (Object.keys(detectedModels).length > 0) {
145
+ info.models = detectedModels;
146
+ info.defaultModel = defaultModel;
147
+ }
148
+ else {
149
+ info.defaultModel = defaultModel;
150
+ }
151
+ info.modelOrder = buildOrder(info, info.defaultModel);
152
+ }
153
+ function applyGeminiOverrides(info) {
154
+ const envDefault = process.env.GEMINI_DEFAULT_MODEL;
155
+ const envModels = parseEnvModels(process.env.GEMINI_MODELS);
156
+ const observed = collectGeminiModels();
157
+ if (Object.keys(observed.models).length > 0) {
158
+ info.models = observed.models;
159
+ info.modelOrder = observed.order;
160
+ info.defaultModel = observed.order[0];
161
+ }
162
+ envModels.forEach(model => {
163
+ if (!info.models[model]) {
164
+ info.models[model] = "Configured via GEMINI_MODELS";
165
+ }
166
+ });
167
+ if (envDefault) {
168
+ info.models[envDefault] = info.models[envDefault] || "Default from GEMINI_DEFAULT_MODEL";
169
+ info.defaultModel = envDefault;
170
+ }
171
+ info.modelOrder = buildOrder(info, info.defaultModel);
172
+ }
173
+ function readJsonValue(filePath, key) {
174
+ if (!existsSync(filePath)) {
175
+ return undefined;
176
+ }
177
+ try {
178
+ const content = readFileSync(filePath, "utf-8");
179
+ const parsed = JSON.parse(content);
180
+ const value = parsed?.[key];
181
+ return typeof value === "string" ? value : undefined;
182
+ }
183
+ catch {
184
+ return undefined;
185
+ }
186
+ }
187
+ function parseEnvModels(value) {
188
+ if (!value) {
189
+ return [];
190
+ }
191
+ return value
192
+ .split(/[,\n]/)
193
+ .map(entry => entry.trim())
194
+ .filter(entry => entry.length > 0);
195
+ }
196
+ function extractTomlString(content, key) {
197
+ const regex = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*\"([^\"]+)\"`, "m");
198
+ const match = content.match(regex);
199
+ return match ? match[1] : undefined;
200
+ }
201
+ function extractTomlTableMap(content, tableName) {
202
+ const tableRegex = new RegExp(`^\\s*\\[${escapeRegex(tableName)}\\]\\s*$`, "m");
203
+ const tableMatch = content.match(tableRegex);
204
+ if (!tableMatch || tableMatch.index === undefined) {
205
+ return {};
206
+ }
207
+ const startIndex = tableMatch.index + tableMatch[0].length;
208
+ const rest = content.slice(startIndex);
209
+ const nextTable = rest.search(/^\s*\[[^\]]+\]\s*$/m);
210
+ const block = nextTable >= 0 ? rest.slice(0, nextTable) : rest;
211
+ const map = {};
212
+ const lineRegex = /"([^"]+)"\s*=\s*"([^"]+)"/g;
213
+ let match;
214
+ while ((match = lineRegex.exec(block)) !== null) {
215
+ map[match[1]] = match[2];
216
+ }
217
+ return map;
218
+ }
219
+ function escapeRegex(value) {
220
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
221
+ }
222
+ function collectGeminiModels() {
223
+ const root = path.join(homedir(), ".gemini", "tmp");
224
+ if (!existsSync(root)) {
225
+ return { models: {}, order: [] };
226
+ }
227
+ const candidates = [];
228
+ const roots = safeReadDir(root);
229
+ roots.forEach(entry => {
230
+ if (!entry.isDirectory()) {
231
+ return;
232
+ }
233
+ const chatsPath = path.join(root, entry.name, "chats");
234
+ if (!existsSync(chatsPath)) {
235
+ return;
236
+ }
237
+ safeReadDir(chatsPath).forEach(file => {
238
+ if (!file.isFile() || !file.name.endsWith(".json")) {
239
+ return;
240
+ }
241
+ const filePath = path.join(chatsPath, file.name);
242
+ try {
243
+ const stat = statSync(filePath);
244
+ candidates.push({ filePath, mtimeMs: stat.mtimeMs });
245
+ }
246
+ catch {
247
+ return;
248
+ }
249
+ });
250
+ });
251
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
252
+ const maxFiles = 200;
253
+ const recent = candidates.slice(0, maxFiles);
254
+ const models = {};
255
+ for (const candidate of recent) {
256
+ const model = extractGeminiModel(candidate.filePath);
257
+ if (!model) {
258
+ continue;
259
+ }
260
+ const existing = models[model]?.lastSeen ?? 0;
261
+ if (candidate.mtimeMs > existing) {
262
+ models[model] = { lastSeen: candidate.mtimeMs };
263
+ }
264
+ }
265
+ const order = Object.entries(models)
266
+ .sort((a, b) => {
267
+ const versionDiff = extractModelVersion(b[0]) - extractModelVersion(a[0]);
268
+ if (versionDiff !== 0) {
269
+ return versionDiff;
270
+ }
271
+ return b[1].lastSeen - a[1].lastSeen;
272
+ })
273
+ .map(([name]) => name);
274
+ const describedModels = {};
275
+ order.forEach(model => {
276
+ describedModels[model] = `Observed in local Gemini sessions (last seen ${formatDate(models[model].lastSeen)})`;
277
+ });
278
+ return { models: describedModels, order };
279
+ }
280
+ function extractGeminiModel(filePath) {
281
+ try {
282
+ const content = readFileSync(filePath, "utf-8");
283
+ const match = content.match(/"model"\s*:\s*"([^"]+)"/);
284
+ return match ? match[1] : undefined;
285
+ }
286
+ catch {
287
+ return undefined;
288
+ }
289
+ }
290
+ function formatDate(timestampMs) {
291
+ try {
292
+ return new Date(timestampMs).toISOString().slice(0, 10);
293
+ }
294
+ catch {
295
+ return "unknown";
296
+ }
297
+ }
298
+ function extractModelVersion(model) {
299
+ const match = model.match(/(\d+(?:\.\d+)?)/);
300
+ if (!match) {
301
+ return 0;
302
+ }
303
+ const parsed = Number.parseFloat(match[1]);
304
+ return Number.isFinite(parsed) ? parsed : 0;
305
+ }
306
+ function safeReadDir(dirPath) {
307
+ try {
308
+ return readdirSync(dirPath, { withFileTypes: true, encoding: "utf8" });
309
+ }
310
+ catch {
311
+ return [];
312
+ }
313
+ }
314
+ function buildOrder(info, preferred) {
315
+ const order = [];
316
+ const seen = new Set();
317
+ if (preferred && info.models[preferred]) {
318
+ order.push(preferred);
319
+ seen.add(preferred);
320
+ }
321
+ if (info.modelOrder) {
322
+ info.modelOrder.forEach(model => {
323
+ if (!seen.has(model) && info.models[model]) {
324
+ order.push(model);
325
+ seen.add(model);
326
+ }
327
+ });
328
+ }
329
+ Object.keys(info.models).forEach(model => {
330
+ if (!seen.has(model)) {
331
+ order.push(model);
332
+ seen.add(model);
333
+ }
334
+ });
335
+ return order;
336
+ }
337
+ function pickLatestMatching(info, token) {
338
+ const normalized = token.toLowerCase();
339
+ const order = info.modelOrder ?? Object.keys(info.models);
340
+ for (const model of order) {
341
+ if (model.toLowerCase().includes(normalized)) {
342
+ return model;
343
+ }
344
+ }
345
+ return undefined;
346
+ }
@@ -0,0 +1,3 @@
1
+ export declare function estimateTokens(text: string): number;
2
+ export declare function optimizePrompt(text: string): string;
3
+ export declare function optimizeResponse(text: string): string;
@@ -0,0 +1,183 @@
1
+ const COURTESY_PATTERNS = [
2
+ /\bPlease\b[:,]?\s*/gi,
3
+ /\bCould you\b\s*/gi,
4
+ /\bI would like you to\b\s*/gi,
5
+ /\bI would like\b\s*/gi,
6
+ /\bI need you to\b\s*/gi,
7
+ /\bI just implemented\b\s*/gi,
8
+ /\bPlease do the following:?\s*/gi
9
+ ];
10
+ const ADJECTIVE_PATTERNS = [
11
+ /\bcomprehensive\b/gi,
12
+ /\bdetailed\b/gi,
13
+ /\bthorough\b/gi,
14
+ /\boverall\b/gi,
15
+ /\bcritical\b/gi
16
+ ];
17
+ const TASK_PREFIXES = [
18
+ /^(First|Then|After that|Finally),?\s*/i,
19
+ /^(First|Then|After that|Finally),?\s*you should\s*/i
20
+ ];
21
+ export function estimateTokens(text) {
22
+ const trimmed = text.trim();
23
+ if (!trimmed)
24
+ return 0;
25
+ const words = trimmed.split(/\s+/).length;
26
+ return Math.ceil(words * 1.3);
27
+ }
28
+ export function optimizePrompt(text) {
29
+ return optimizeText(text, "prompt");
30
+ }
31
+ export function optimizeResponse(text) {
32
+ return optimizeText(text, "response");
33
+ }
34
+ function optimizeText(text, mode) {
35
+ if (!text.trim())
36
+ return text;
37
+ const parts = [];
38
+ const codeBlockRegex = /```[\s\S]*?```/g;
39
+ let lastIndex = 0;
40
+ let match;
41
+ while ((match = codeBlockRegex.exec(text)) !== null) {
42
+ parts.push(optimizeSegment(text.slice(lastIndex, match.index), mode));
43
+ parts.push(match[0]);
44
+ lastIndex = match.index + match[0].length;
45
+ }
46
+ parts.push(optimizeSegment(text.slice(lastIndex), mode));
47
+ return parts.join("");
48
+ }
49
+ function optimizeSegment(segment, mode) {
50
+ const inlineParts = segment.split(/(`[^`]*`)/g);
51
+ return inlineParts
52
+ .map((part) => (part.startsWith("`") ? part : optimizePlain(part, mode)))
53
+ .join("");
54
+ }
55
+ function optimizePlain(text, mode) {
56
+ let output = text;
57
+ COURTESY_PATTERNS.forEach((pattern) => {
58
+ output = output.replace(pattern, "");
59
+ });
60
+ ADJECTIVE_PATTERNS.forEach((pattern) => {
61
+ output = output.replace(pattern, "");
62
+ });
63
+ output = output.replace(/\bfound in the [^:.\n]+/gi, "");
64
+ output = output.replace(/\bthat we implemented\b/gi, "");
65
+ output = output.replace(/\bwe implemented\b/gi, "");
66
+ output = output.replace(/\bthe\s+([a-z][\w\s-]+?)\s+system\b/gi, "$1");
67
+ output = output.replace(/\bthe\s+([a-z][\w\s-]+?)\s+feature\b/gi, "$1");
68
+ output = output.replace(/^\s*Problem:\s*/gim, "");
69
+ output = inlineFileReferences(output);
70
+ output = compactTypes(output);
71
+ output = output.replace(/\bAre there any\s+([^?]+)\?/gi, "$1?");
72
+ output = compressTaskLists(output);
73
+ output = applyArrowNotation(output);
74
+ output = applySlashNotation(output);
75
+ if (mode === "response") {
76
+ output = output.replace(/\bIn conclusion\b[:,]?\s*/gi, "");
77
+ output = output.replace(/\bOverall\b[:,]?\s*/gi, "");
78
+ }
79
+ output = output.replace(/[ \t]+/g, " ");
80
+ output = output.replace(/\n{3,}/g, "\n\n");
81
+ return output.trimEnd();
82
+ }
83
+ function inlineFileReferences(text) {
84
+ const replaceLines = (lines) => {
85
+ const matches = lines.match(/\d+/g);
86
+ if (!matches)
87
+ return lines.trim();
88
+ return matches.join(",");
89
+ };
90
+ let output = text.replace(/\b(?:in (?:the )?file\s+)?(\S+)\s+(?:on|at)\s+lines?\s+([0-9 ,and~]+)/gi, (_match, file, lines) => `${file}:${replaceLines(lines)}`);
91
+ output = output.replace(/\b(?:in (?:the )?file\s+)?(\S+)\s+on line\s+~?(\d+)/gi, (_match, file, line) => `${file}:${line}`);
92
+ output = output.replace(/\b(\S+)\s+line\s+~?(\d+)/gi, (_match, file, line) => `${file}:${line}`);
93
+ output = output.replace(/\b(?:in the )?([\w./-]+)\s+file\s+at\s+lines?\s+([0-9 ,and~]+)/gi, (_match, file, lines) => `${file}:${replaceLines(lines)}`);
94
+ output = output.replace(/\bin the\s+([\w./-]+)\s+file:(\d+(?:,\d+)*)/gi, "$1:$2");
95
+ return output;
96
+ }
97
+ function compactTypes(text) {
98
+ let output = text;
99
+ output = output.replace(/\ban? optional boolean\s*\(default\s*(true|false)\)/gi, "bool=$1");
100
+ output = output.replace(/\boptional boolean\b/gi, "bool?");
101
+ output = output.replace(/\bboolean\b/gi, "bool");
102
+ output = output.replace(/\ban? required integer\b/gi, "int!");
103
+ output = output.replace(/\ban? integer\b/gi, "int");
104
+ output = output.replace(/\ban? array of strings\b/gi, "str[]");
105
+ output = output.replace(/\bstring enum of\s*(\[[^\]]+\])/gi, "enum$1");
106
+ output = output.replace(/\bstring enum\b/gi, "enum");
107
+ output = output.replace(/\bstring parameter\b/gi, "param:str");
108
+ output = output.replace(/\bstring\b/gi, "str");
109
+ return output;
110
+ }
111
+ function compressTaskLists(text) {
112
+ const lines = text.split("\n");
113
+ const output = [];
114
+ for (let i = 0; i < lines.length; i += 1) {
115
+ const line = lines[i];
116
+ if (/^\s*\d+\.\s+/.test(line)) {
117
+ const items = [];
118
+ let j = i;
119
+ while (j < lines.length && /^\s*\d+\.\s+/.test(lines[j])) {
120
+ const item = lines[j].replace(/^\s*\d+\.\s+/, "");
121
+ items.push(cleanTaskItem(item));
122
+ j += 1;
123
+ }
124
+ if (items.length > 1) {
125
+ output.push(`Tasks: ${items.join(" → ")}`);
126
+ i = j - 1;
127
+ continue;
128
+ }
129
+ }
130
+ if (/^\s*-\s+/.test(line)) {
131
+ const items = [];
132
+ let j = i;
133
+ while (j < lines.length && /^\s*-\s+/.test(lines[j])) {
134
+ const item = lines[j].replace(/^\s*-\s+/, "");
135
+ items.push(cleanTaskItem(item));
136
+ j += 1;
137
+ }
138
+ if (items.length > 1) {
139
+ output.push(items.join(" → "));
140
+ i = j - 1;
141
+ continue;
142
+ }
143
+ }
144
+ output.push(line);
145
+ }
146
+ return output.join("\n");
147
+ }
148
+ function cleanTaskItem(item) {
149
+ let cleaned = item.trim();
150
+ TASK_PREFIXES.forEach((pattern) => {
151
+ cleaned = cleaned.replace(pattern, "");
152
+ });
153
+ cleaned = cleaned.replace(/^you should\s*/i, "");
154
+ cleaned = cleaned.replace(/^\bPlease\b\s*/i, "");
155
+ cleaned = cleaned.replace(/[.:;]+$/g, "");
156
+ return cleaned;
157
+ }
158
+ function applyArrowNotation(text) {
159
+ const lines = text.split("\n");
160
+ const output = lines.map((line) => {
161
+ let updated = line;
162
+ updated = updated.replace(/\bChange\s+(?:the\s+)?([A-Za-z][\w-]*)\s+to\s+(?:a\s+|an\s+)?([A-Za-z][\w-]*)([.!?]|$)/gi, (_m, from, to, end) => {
163
+ return `${from.trim()} → ${to.trim()}${end || ""}`;
164
+ });
165
+ updated = updated.replace(/\bConvert\s+(?:the\s+)?([A-Za-z][\w-]*)\s+to\s+(?:a\s+|an\s+)?([A-Za-z][\w-]*)([.!?]|$)/gi, (_m, from, to, end) => {
166
+ return `${from.trim()} → ${to.trim()}${end || ""}`;
167
+ });
168
+ updated = updated.replace(/\b(\w+)\s+should be\s+an?\s+(\w+)/gi, "$1: $2");
169
+ return updated;
170
+ });
171
+ return output.join("\n");
172
+ }
173
+ function applySlashNotation(text) {
174
+ const lines = text.split("\n");
175
+ const output = lines.map((line) => {
176
+ const trimmed = line.trim();
177
+ if (trimmed.length < 50 && /^[A-Za-z0-9][A-Za-z0-9\s/&-]+$/.test(trimmed)) {
178
+ return line.replace(/\s+and\s+/, "/");
179
+ }
180
+ return line;
181
+ });
182
+ return output.join("\n");
183
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * On-demand process health monitoring via /proc (Linux).
3
+ * Gracefully degrades on non-Linux platforms.
4
+ */
5
+ import type { Logger } from "./logger.js";
6
+ export interface ProcessHealth {
7
+ pid: number;
8
+ alive: boolean;
9
+ state: string | null;
10
+ cpuPercent: number | null;
11
+ memoryRssKb: number | null;
12
+ sampledAt: string;
13
+ }
14
+ export interface JobHealth {
15
+ jobId: string;
16
+ cli: string;
17
+ status: string;
18
+ processHealth: ProcessHealth | null;
19
+ isDead: boolean;
20
+ isZombie: boolean;
21
+ runningForMs: number;
22
+ }
23
+ /**
24
+ * Parse /proc/[pid]/stat safely.
25
+ * The `comm` field (field 2) is in parentheses and may contain spaces,
26
+ * so we find the LAST ')' and parse remaining fields from there.
27
+ */
28
+ export declare function parseProcStat(content: string): {
29
+ state: string;
30
+ utime: number;
31
+ stime: number;
32
+ } | null;
33
+ /**
34
+ * Parse VmRSS from /proc/[pid]/status.
35
+ * Returns RSS in kilobytes (already in kB in /proc/[pid]/status).
36
+ */
37
+ export declare function parseVmRss(content: string): number | null;
38
+ export declare class ProcessMonitor {
39
+ private logger;
40
+ private prevSamples;
41
+ constructor(logger?: Logger);
42
+ /** Clear all cached CPU samples */
43
+ reset(): void;
44
+ sampleProcess(pid: number): ProcessHealth;
45
+ checkJobHealth(jobs: {
46
+ jobId: string;
47
+ cli: string;
48
+ status: string;
49
+ pid: number | null;
50
+ startedAt: string;
51
+ }[]): JobHealth[];
52
+ /** Clean up stale samples for PIDs that no longer exist */
53
+ cleanupSamples(activePids: Set<number>): void;
54
+ }