promptpilot 0.1.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/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +1543 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +1270 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var InvalidPromptError = class extends Error {
|
|
8
|
+
constructor(message = "Prompt must be a non-empty string.") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "InvalidPromptError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var OllamaUnavailableError = class extends Error {
|
|
14
|
+
constructor(message = "Ollama is unavailable or returned an invalid response.") {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "OllamaUnavailableError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ContextStoreError = class extends Error {
|
|
20
|
+
constructor(message = "Context store operation failed.") {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ContextStoreError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var TokenBudgetExceededError = class extends Error {
|
|
26
|
+
constructor(message = "Final prompt could not be reduced to fit within the configured token budget.") {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "TokenBudgetExceededError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/storage/fileSessionStore.ts
|
|
33
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
34
|
+
import { homedir } from "os";
|
|
35
|
+
import { dirname, join } from "path";
|
|
36
|
+
|
|
37
|
+
// src/utils/validation.ts
|
|
38
|
+
function normalizeWhitespace(value) {
|
|
39
|
+
return value.replace(/\r\n/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
40
|
+
}
|
|
41
|
+
function validatePrompt(prompt) {
|
|
42
|
+
if (typeof prompt !== "string" || normalizeWhitespace(prompt).length === 0) {
|
|
43
|
+
throw new InvalidPromptError();
|
|
44
|
+
}
|
|
45
|
+
return normalizeWhitespace(prompt);
|
|
46
|
+
}
|
|
47
|
+
function sanitizeSessionId(sessionId) {
|
|
48
|
+
return sessionId.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/storage/fileSessionStore.ts
|
|
52
|
+
var FileSessionStore = class {
|
|
53
|
+
rootDir;
|
|
54
|
+
constructor(rootDir = join(homedir(), ".promptpilot", "sessions")) {
|
|
55
|
+
this.rootDir = rootDir;
|
|
56
|
+
}
|
|
57
|
+
async loadSession(sessionId) {
|
|
58
|
+
const filePath = this.getFilePath(sessionId);
|
|
59
|
+
try {
|
|
60
|
+
const contents = await readFile(filePath, "utf8");
|
|
61
|
+
const parsed = JSON.parse(contents);
|
|
62
|
+
return parsed;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const code = error.code;
|
|
65
|
+
if (code === "ENOENT") {
|
|
66
|
+
return this.createEmptySession(sessionId);
|
|
67
|
+
}
|
|
68
|
+
throw new ContextStoreError(`Failed to load session "${sessionId}".`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async saveSession(session) {
|
|
72
|
+
const filePath = this.getFilePath(session.sessionId);
|
|
73
|
+
try {
|
|
74
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
75
|
+
await writeFile(filePath, JSON.stringify(session, null, 2), "utf8");
|
|
76
|
+
} catch {
|
|
77
|
+
throw new ContextStoreError(`Failed to save session "${session.sessionId}".`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async clearSession(sessionId) {
|
|
81
|
+
const filePath = this.getFilePath(sessionId);
|
|
82
|
+
try {
|
|
83
|
+
await rm(filePath, { force: true });
|
|
84
|
+
} catch {
|
|
85
|
+
throw new ContextStoreError(`Failed to clear session "${sessionId}".`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
getFilePath(sessionId) {
|
|
89
|
+
return join(this.rootDir, `${sanitizeSessionId(sessionId)}.json`);
|
|
90
|
+
}
|
|
91
|
+
createEmptySession(sessionId) {
|
|
92
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
93
|
+
return {
|
|
94
|
+
sessionId,
|
|
95
|
+
entries: [],
|
|
96
|
+
summaries: [],
|
|
97
|
+
createdAt: now,
|
|
98
|
+
updatedAt: now
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/storage/sqliteSessionStore.ts
|
|
104
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
105
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
106
|
+
var SQLiteSessionStore = class {
|
|
107
|
+
constructor(dbPath = join2(process.cwd(), ".promptpilot", "promptpilot.db")) {
|
|
108
|
+
this.dbPath = dbPath;
|
|
109
|
+
this.dbPromise = this.openDatabase();
|
|
110
|
+
}
|
|
111
|
+
dbPath;
|
|
112
|
+
dbPromise;
|
|
113
|
+
async loadSession(sessionId) {
|
|
114
|
+
const db = await this.dbPromise;
|
|
115
|
+
const row = db.prepare("SELECT data FROM sessions WHERE id = ?").get(sessionId);
|
|
116
|
+
if (!row?.data) {
|
|
117
|
+
return createEmptySession(sessionId);
|
|
118
|
+
}
|
|
119
|
+
return JSON.parse(row.data);
|
|
120
|
+
}
|
|
121
|
+
async saveSession(session) {
|
|
122
|
+
const db = await this.dbPromise;
|
|
123
|
+
db.prepare("INSERT OR REPLACE INTO sessions (id, data) VALUES (?, ?)").run(
|
|
124
|
+
session.sessionId,
|
|
125
|
+
JSON.stringify(session)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
async clearSession(sessionId) {
|
|
129
|
+
const db = await this.dbPromise;
|
|
130
|
+
db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
|
|
131
|
+
}
|
|
132
|
+
async openDatabase() {
|
|
133
|
+
try {
|
|
134
|
+
await mkdir2(dirname2(this.dbPath), { recursive: true });
|
|
135
|
+
const nodeSqliteName = "node:sqlite";
|
|
136
|
+
const nodeSqlite = await import(nodeSqliteName).catch(() => null);
|
|
137
|
+
if (nodeSqlite?.DatabaseSync) {
|
|
138
|
+
const db = new nodeSqlite.DatabaseSync(this.dbPath);
|
|
139
|
+
db.exec("CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, data TEXT NOT NULL)");
|
|
140
|
+
return db;
|
|
141
|
+
}
|
|
142
|
+
const betterSqliteName = "better-sqlite3";
|
|
143
|
+
const betterSqlite = await import(betterSqliteName).catch(() => null);
|
|
144
|
+
if (betterSqlite?.default) {
|
|
145
|
+
const db = new betterSqlite.default(this.dbPath);
|
|
146
|
+
db.exec("CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, data TEXT NOT NULL)");
|
|
147
|
+
return db;
|
|
148
|
+
}
|
|
149
|
+
throw new ContextStoreError(
|
|
150
|
+
"SQLite store requires node:sqlite support or the better-sqlite3 package."
|
|
151
|
+
);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof ContextStoreError) {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
throw new ContextStoreError("Failed to initialize SQLite session store.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function createEmptySession(sessionId) {
|
|
161
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
162
|
+
return {
|
|
163
|
+
sessionId,
|
|
164
|
+
entries: [],
|
|
165
|
+
summaries: [],
|
|
166
|
+
createdAt: now,
|
|
167
|
+
updatedAt: now
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/utils/logger.ts
|
|
172
|
+
var noopLogger = {
|
|
173
|
+
debug: () => void 0,
|
|
174
|
+
info: () => void 0,
|
|
175
|
+
warn: () => void 0,
|
|
176
|
+
error: () => void 0
|
|
177
|
+
};
|
|
178
|
+
function createLogger(debugEnabled = false) {
|
|
179
|
+
return {
|
|
180
|
+
debug(message, meta) {
|
|
181
|
+
if (debugEnabled) {
|
|
182
|
+
console.debug(`[promptpilot] ${message}`, meta ?? "");
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
info(message, meta) {
|
|
186
|
+
if (debugEnabled) {
|
|
187
|
+
console.info(`[promptpilot] ${message}`, meta ?? "");
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
warn(message, meta) {
|
|
191
|
+
if (debugEnabled) {
|
|
192
|
+
console.warn(`[promptpilot] ${message}`, meta ?? "");
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
error(message, meta) {
|
|
196
|
+
if (debugEnabled) {
|
|
197
|
+
console.error(`[promptpilot] ${message}`, meta ?? "");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/utils/json.ts
|
|
204
|
+
function safeJsonParse(value) {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(value);
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function extractFirstJsonObject(value) {
|
|
212
|
+
const start = value.indexOf("{");
|
|
213
|
+
if (start === -1) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
let depth = 0;
|
|
217
|
+
let inString = false;
|
|
218
|
+
let escaped = false;
|
|
219
|
+
for (let index = start; index < value.length; index += 1) {
|
|
220
|
+
const char = value[index];
|
|
221
|
+
if (escaped) {
|
|
222
|
+
escaped = false;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === "\\") {
|
|
226
|
+
escaped = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (char === '"') {
|
|
230
|
+
inString = !inString;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (inString) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (char === "{") {
|
|
237
|
+
depth += 1;
|
|
238
|
+
} else if (char === "}") {
|
|
239
|
+
depth -= 1;
|
|
240
|
+
if (depth === 0) {
|
|
241
|
+
return value.slice(start, index + 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function toPrettyJson(value) {
|
|
248
|
+
return JSON.stringify(value, null, 2);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/core/ollamaClient.ts
|
|
252
|
+
var OllamaClient = class {
|
|
253
|
+
host;
|
|
254
|
+
model;
|
|
255
|
+
timeoutMs;
|
|
256
|
+
temperature;
|
|
257
|
+
logger;
|
|
258
|
+
constructor(config = {}) {
|
|
259
|
+
this.host = config.host ?? "http://localhost:11434";
|
|
260
|
+
this.model = config.model ?? "qwen2.5:3b";
|
|
261
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
262
|
+
this.temperature = config.temperature ?? 0.1;
|
|
263
|
+
this.logger = config.logger ?? noopLogger;
|
|
264
|
+
}
|
|
265
|
+
async isAvailable() {
|
|
266
|
+
try {
|
|
267
|
+
const response = await fetch(new URL("/api/tags", this.host), {
|
|
268
|
+
method: "GET"
|
|
269
|
+
});
|
|
270
|
+
return response.ok;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async listModels() {
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(new URL("/api/tags", this.host), {
|
|
278
|
+
method: "GET"
|
|
279
|
+
});
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
throw new OllamaUnavailableError(`Ollama tags request failed with status ${response.status}.`);
|
|
282
|
+
}
|
|
283
|
+
const data = await response.json();
|
|
284
|
+
return (data.models ?? []).filter((model) => typeof model.name === "string" && model.name.length > 0).map((model) => ({
|
|
285
|
+
name: model.name,
|
|
286
|
+
sizeBytes: model.size,
|
|
287
|
+
family: model.details?.family,
|
|
288
|
+
parameterSize: model.details?.parameter_size,
|
|
289
|
+
modifiedAt: model.modified_at
|
|
290
|
+
}));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : "Unknown Ollama tags error.";
|
|
293
|
+
this.logger.warn("ollama.list_models.failed", { message });
|
|
294
|
+
throw new OllamaUnavailableError(message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async generate(options) {
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? this.timeoutMs);
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(new URL("/api/generate", this.host), {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: {
|
|
304
|
+
"content-type": "application/json"
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
model: options.model ?? this.model,
|
|
308
|
+
system: options.systemPrompt,
|
|
309
|
+
prompt: options.prompt,
|
|
310
|
+
stream: false,
|
|
311
|
+
format: options.format === "json" ? "json" : void 0,
|
|
312
|
+
options: {
|
|
313
|
+
temperature: options.temperature ?? this.temperature
|
|
314
|
+
}
|
|
315
|
+
}),
|
|
316
|
+
signal: controller.signal
|
|
317
|
+
});
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new OllamaUnavailableError(`Ollama request failed with status ${response.status}.`);
|
|
320
|
+
}
|
|
321
|
+
const data = await response.json();
|
|
322
|
+
if (!data.response || typeof data.response !== "string") {
|
|
323
|
+
throw new OllamaUnavailableError("Ollama did not return a text response.");
|
|
324
|
+
}
|
|
325
|
+
return data.response.trim();
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof OllamaUnavailableError) {
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
const message = error instanceof Error ? error.message : "Unknown Ollama error.";
|
|
331
|
+
this.logger.warn("ollama.generate.failed", { message });
|
|
332
|
+
throw new OllamaUnavailableError(message);
|
|
333
|
+
} finally {
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async generateJson(options) {
|
|
338
|
+
const raw = await this.generate({
|
|
339
|
+
...options,
|
|
340
|
+
format: "json"
|
|
341
|
+
});
|
|
342
|
+
const direct = safeJsonParse(raw);
|
|
343
|
+
if (direct) {
|
|
344
|
+
return direct;
|
|
345
|
+
}
|
|
346
|
+
const extracted = extractFirstJsonObject(raw);
|
|
347
|
+
if (extracted) {
|
|
348
|
+
const parsed = safeJsonParse(extracted);
|
|
349
|
+
if (parsed) {
|
|
350
|
+
return parsed;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
throw new OllamaUnavailableError("Ollama returned JSON that could not be parsed.");
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/core/systemPrompt.ts
|
|
358
|
+
var modeGuidance = {
|
|
359
|
+
clarity: "Improve clarity, remove ambiguity, and keep the request easy for a downstream model to follow.",
|
|
360
|
+
concise: "Minimize token count while preserving user intent, constraints, and expected output.",
|
|
361
|
+
detailed: "Make the request explicit and complete, including structure and success criteria.",
|
|
362
|
+
structured: "Organize the request into clean sections with compact headings and bullet points where helpful.",
|
|
363
|
+
persuasive: "Refine wording so the request is compelling and likely to elicit a thoughtful response.",
|
|
364
|
+
compress: "Aggressively compress redundant wording while preserving the meaning and critical constraints.",
|
|
365
|
+
claude_cli: "Optimize specifically for Claude CLI: compact sections, direct instructions, and minimal boilerplate."
|
|
366
|
+
};
|
|
367
|
+
var presetGuidance = {
|
|
368
|
+
code: "Favor precise technical requirements, edge cases, and expected output format for code tasks.",
|
|
369
|
+
email: "Preserve the sender's goal, tone, and audience; aim for a realistic and usable writing request.",
|
|
370
|
+
essay: "Preserve thesis, structure, and voice guidance while making the prompt clearer.",
|
|
371
|
+
support: "Favor concise issue context, user impact, and desired resolution details.",
|
|
372
|
+
summarization: "Favor strong compression while retaining key facts, structure, and takeaways.",
|
|
373
|
+
chat: "Keep the prompt natural and conversational while preserving instructions and context."
|
|
374
|
+
};
|
|
375
|
+
function getOptimizationSystemPrompt(mode, preset) {
|
|
376
|
+
return [
|
|
377
|
+
"You are PromptPilot, a local prompt optimizer for downstream LLM workflows.",
|
|
378
|
+
"Return strict JSON only with this shape:",
|
|
379
|
+
'{"optimizedPrompt":"string","constraints":["string"],"changes":["string"],"warnings":["string"]}',
|
|
380
|
+
"Rules:",
|
|
381
|
+
"- Do not change the user's intent.",
|
|
382
|
+
"- Do not invent new requirements.",
|
|
383
|
+
"- Preserve critical constraints and task goals.",
|
|
384
|
+
"- Improve clarity, structure, and downstream usefulness.",
|
|
385
|
+
"- Keep the result compact when the mode requests compression.",
|
|
386
|
+
`Mode guidance: ${modeGuidance[mode]}`,
|
|
387
|
+
preset ? `Preset guidance: ${presetGuidance[preset]}` : "Preset guidance: none"
|
|
388
|
+
].join("\n");
|
|
389
|
+
}
|
|
390
|
+
function buildOptimizationPrompt(input, relevantContext, extractedConstraints) {
|
|
391
|
+
const payload = {
|
|
392
|
+
prompt: input.prompt,
|
|
393
|
+
task: input.task ?? null,
|
|
394
|
+
tone: input.tone ?? null,
|
|
395
|
+
targetModel: input.targetModel ?? "claude",
|
|
396
|
+
outputFormat: input.outputFormat ?? null,
|
|
397
|
+
maxLength: input.maxLength ?? null,
|
|
398
|
+
mode: input.mode ?? "claude_cli",
|
|
399
|
+
preset: input.preset ?? null,
|
|
400
|
+
pinnedConstraints: input.pinnedConstraints ?? [],
|
|
401
|
+
extractedConstraints,
|
|
402
|
+
relevantContext
|
|
403
|
+
};
|
|
404
|
+
return `Optimize this prompt payload for a downstream LLM.
|
|
405
|
+
${JSON.stringify(payload, null, 2)}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/core/tokenEstimator.ts
|
|
409
|
+
var TokenEstimator = class {
|
|
410
|
+
estimateText(text) {
|
|
411
|
+
const normalized = normalizeWhitespace(text);
|
|
412
|
+
if (!normalized) {
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
const wordCount = normalized.split(/\s+/).length;
|
|
416
|
+
const charCount = normalized.length;
|
|
417
|
+
return Math.max(1, Math.ceil(wordCount * 1.3), Math.ceil(charCount / 4));
|
|
418
|
+
}
|
|
419
|
+
estimateUsage(input) {
|
|
420
|
+
const prompt = this.estimateText(input.prompt);
|
|
421
|
+
const context = this.estimateText(input.context ?? "");
|
|
422
|
+
return {
|
|
423
|
+
prompt,
|
|
424
|
+
context,
|
|
425
|
+
total: prompt + context
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
truncateToBudget(text, tokenBudget) {
|
|
429
|
+
const normalized = normalizeWhitespace(text);
|
|
430
|
+
if (tokenBudget <= 0 || !normalized) {
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
if (this.estimateText(normalized) <= tokenBudget) {
|
|
434
|
+
return normalized;
|
|
435
|
+
}
|
|
436
|
+
const words = normalized.split(/\s+/);
|
|
437
|
+
const selected = [];
|
|
438
|
+
for (const word of words) {
|
|
439
|
+
const candidate = [...selected, word].join(" ");
|
|
440
|
+
if (this.estimateText(candidate) > tokenBudget) {
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
selected.push(word);
|
|
444
|
+
}
|
|
445
|
+
return selected.join(" ").trim();
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/core/contextCompressor.ts
|
|
450
|
+
import { randomUUID } from "crypto";
|
|
451
|
+
var ContextCompressor = class {
|
|
452
|
+
constructor(estimator, client, logger = noopLogger) {
|
|
453
|
+
this.estimator = estimator;
|
|
454
|
+
this.client = client;
|
|
455
|
+
this.logger = logger;
|
|
456
|
+
}
|
|
457
|
+
estimator;
|
|
458
|
+
client;
|
|
459
|
+
logger;
|
|
460
|
+
async summarizeEntries(options) {
|
|
461
|
+
if (options.entries.length === 0 || options.budgetTokens <= 0) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const ollamaSummary = await this.tryOllamaSummary(options);
|
|
465
|
+
if (ollamaSummary) {
|
|
466
|
+
return ollamaSummary;
|
|
467
|
+
}
|
|
468
|
+
return this.heuristicSummary(options);
|
|
469
|
+
}
|
|
470
|
+
async tryOllamaSummary(options) {
|
|
471
|
+
if (!this.client || options.entries.length < 3) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const source = options.entries.slice(-8).map((entry) => [
|
|
476
|
+
`Timestamp: ${entry.timestamp}`,
|
|
477
|
+
entry.task ? `Task: ${entry.task}` : "",
|
|
478
|
+
entry.constraints?.length ? `Constraints: ${entry.constraints.join("; ")}` : "",
|
|
479
|
+
`Prompt: ${entry.rawPrompt ?? entry.text}`
|
|
480
|
+
].filter(Boolean).join("\n")).join("\n\n");
|
|
481
|
+
const response = await this.client.generateJson({
|
|
482
|
+
systemPrompt: getOptimizationSystemPrompt("compress"),
|
|
483
|
+
prompt: `Summarize this stored prompt context for reuse in later turns.
|
|
484
|
+
Prompt: ${options.prompt}
|
|
485
|
+
Task: ${options.task ?? "unknown"}
|
|
486
|
+
Budget tokens: ${options.budgetTokens}
|
|
487
|
+
|
|
488
|
+
${source}`,
|
|
489
|
+
timeoutMs: options.timeoutMs,
|
|
490
|
+
format: "json"
|
|
491
|
+
});
|
|
492
|
+
const summaryText = this.estimator.truncateToBudget(response.optimizedPrompt ?? "", options.budgetTokens);
|
|
493
|
+
if (!summaryText) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
id: randomUUID(),
|
|
498
|
+
sessionId: options.sessionId,
|
|
499
|
+
text: summaryText,
|
|
500
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
501
|
+
sourceEntryIds: options.entries.map((entry) => entry.id),
|
|
502
|
+
tokenEstimate: this.estimator.estimateText(summaryText),
|
|
503
|
+
kind: "ollama"
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
const message = error instanceof Error ? error.message : "Unknown summary failure";
|
|
507
|
+
this.logger.debug("context.summary.ollama_fallback", { message });
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
heuristicSummary(options) {
|
|
512
|
+
const constraints = Array.from(
|
|
513
|
+
new Set(options.entries.flatMap((entry) => entry.constraints ?? []).filter(Boolean))
|
|
514
|
+
).slice(0, 6);
|
|
515
|
+
const recentPrompts = options.entries.slice(-4).map((entry) => `- ${truncateLine(entry.rawPrompt ?? entry.text, 120)}`);
|
|
516
|
+
const lines = [
|
|
517
|
+
constraints.length ? `Key constraints: ${constraints.join("; ")}` : "",
|
|
518
|
+
recentPrompts.length ? `Recent focus:
|
|
519
|
+
${recentPrompts.join("\n")}` : ""
|
|
520
|
+
].filter(Boolean);
|
|
521
|
+
const summaryText = this.estimator.truncateToBudget(lines.join("\n\n"), options.budgetTokens);
|
|
522
|
+
return {
|
|
523
|
+
id: randomUUID(),
|
|
524
|
+
sessionId: options.sessionId,
|
|
525
|
+
text: summaryText,
|
|
526
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
527
|
+
sourceEntryIds: options.entries.map((entry) => entry.id),
|
|
528
|
+
tokenEstimate: this.estimator.estimateText(summaryText),
|
|
529
|
+
kind: "heuristic"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
function truncateLine(value, maxLength) {
|
|
534
|
+
if (value.length <= maxLength) {
|
|
535
|
+
return value;
|
|
536
|
+
}
|
|
537
|
+
return `${value.slice(0, maxLength - 3).trim()}...`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/core/contextManager.ts
|
|
541
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
542
|
+
var ContextManager = class {
|
|
543
|
+
constructor(store, estimator, compressor, logger = noopLogger) {
|
|
544
|
+
this.store = store;
|
|
545
|
+
this.estimator = estimator;
|
|
546
|
+
this.compressor = compressor;
|
|
547
|
+
this.logger = logger;
|
|
548
|
+
}
|
|
549
|
+
store;
|
|
550
|
+
estimator;
|
|
551
|
+
compressor;
|
|
552
|
+
logger;
|
|
553
|
+
async loadContext(sessionId) {
|
|
554
|
+
return this.store.loadSession(sessionId);
|
|
555
|
+
}
|
|
556
|
+
async clearContext(sessionId) {
|
|
557
|
+
await this.store.clearSession(sessionId);
|
|
558
|
+
}
|
|
559
|
+
async saveContext(options) {
|
|
560
|
+
const session = await this.store.loadSession(options.sessionId);
|
|
561
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
562
|
+
const constraints = Array.from(
|
|
563
|
+
/* @__PURE__ */ new Set([
|
|
564
|
+
...extractConstraints(options.input.prompt),
|
|
565
|
+
...options.input.pinnedConstraints ?? []
|
|
566
|
+
])
|
|
567
|
+
);
|
|
568
|
+
const entry = {
|
|
569
|
+
id: randomUUID2(),
|
|
570
|
+
sessionId: options.sessionId,
|
|
571
|
+
text: options.optimizedPrompt,
|
|
572
|
+
rawPrompt: options.input.prompt,
|
|
573
|
+
optimizedPrompt: options.optimizedPrompt,
|
|
574
|
+
finalPrompt: options.finalPrompt,
|
|
575
|
+
task: options.input.task,
|
|
576
|
+
tone: options.input.tone,
|
|
577
|
+
tags: options.input.tags ?? [],
|
|
578
|
+
constraints,
|
|
579
|
+
entities: extractEntities(options.input.prompt),
|
|
580
|
+
pinned: (options.input.pinnedConstraints?.length ?? 0) > 0,
|
|
581
|
+
timestamp,
|
|
582
|
+
tokenEstimate: this.estimator.estimateText(options.finalPrompt)
|
|
583
|
+
};
|
|
584
|
+
session.entries.push(entry);
|
|
585
|
+
if (session.entries.length > 100) {
|
|
586
|
+
session.entries = session.entries.slice(-100);
|
|
587
|
+
}
|
|
588
|
+
if (options.contextSummary) {
|
|
589
|
+
session.summaries.push(options.contextSummary);
|
|
590
|
+
if (session.summaries.length > 10) {
|
|
591
|
+
session.summaries = session.summaries.slice(-10);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
session.updatedAt = timestamp;
|
|
595
|
+
await this.store.saveSession(session);
|
|
596
|
+
}
|
|
597
|
+
async summarizeContext(sessionId, prompt, task, budgetTokens, timeoutMs) {
|
|
598
|
+
const session = await this.store.loadSession(sessionId);
|
|
599
|
+
if (session.entries.length === 0) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return this.compressor.summarizeEntries({
|
|
603
|
+
sessionId,
|
|
604
|
+
entries: session.entries,
|
|
605
|
+
prompt,
|
|
606
|
+
task,
|
|
607
|
+
budgetTokens,
|
|
608
|
+
timeoutMs
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async getRelevantContext(options) {
|
|
612
|
+
const session = await this.store.loadSession(options.sessionId);
|
|
613
|
+
if (session.entries.length === 0 && session.summaries.length === 0) {
|
|
614
|
+
return {
|
|
615
|
+
usedEntries: [],
|
|
616
|
+
summary: null,
|
|
617
|
+
warnings: [],
|
|
618
|
+
debugInfo: { scores: [] }
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const scores = session.entries.map((entry, index) => ({
|
|
622
|
+
entry,
|
|
623
|
+
score: scoreEntry({
|
|
624
|
+
entry,
|
|
625
|
+
prompt: options.prompt,
|
|
626
|
+
task: options.task,
|
|
627
|
+
tags: options.tags,
|
|
628
|
+
pinnedConstraints: options.pinnedConstraints,
|
|
629
|
+
index,
|
|
630
|
+
total: session.entries.length
|
|
631
|
+
})
|
|
632
|
+
}));
|
|
633
|
+
const selected = [];
|
|
634
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
635
|
+
let selectedTokens = 0;
|
|
636
|
+
for (const scored of scores.sort((left, right) => right.score - left.score)) {
|
|
637
|
+
if (selected.find((entry) => entry.id === scored.entry.id)) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const fingerprint = createFingerprint(scored.entry);
|
|
641
|
+
if (seenFingerprints.has(fingerprint)) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (selectedTokens + scored.entry.tokenEstimate > options.maxContextTokens) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
selected.push(scored.entry);
|
|
648
|
+
seenFingerprints.add(fingerprint);
|
|
649
|
+
selectedTokens += scored.entry.tokenEstimate;
|
|
650
|
+
}
|
|
651
|
+
const remainder = session.entries.filter(
|
|
652
|
+
(entry) => !selected.find((selectedEntry) => selectedEntry.id === entry.id)
|
|
653
|
+
);
|
|
654
|
+
let summary = null;
|
|
655
|
+
if (remainder.length > 1 && selectedTokens < Math.floor(options.maxContextTokens * 0.8)) {
|
|
656
|
+
summary = await this.compressor.summarizeEntries({
|
|
657
|
+
sessionId: options.sessionId,
|
|
658
|
+
entries: remainder,
|
|
659
|
+
prompt: options.prompt,
|
|
660
|
+
task: options.task,
|
|
661
|
+
budgetTokens: Math.max(80, options.maxContextTokens - selectedTokens),
|
|
662
|
+
timeoutMs: options.timeoutMs
|
|
663
|
+
});
|
|
664
|
+
} else if (session.summaries.length > 0) {
|
|
665
|
+
const latest = session.summaries.at(-1) ?? null;
|
|
666
|
+
if (latest && latest.tokenEstimate <= options.maxContextTokens - selectedTokens) {
|
|
667
|
+
summary = latest;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const warnings = [];
|
|
671
|
+
if (remainder.length > 0) {
|
|
672
|
+
warnings.push(`Dropped ${remainder.length} lower-value context item(s) to fit the token budget.`);
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
usedEntries: selected.sort((left, right) => left.timestamp.localeCompare(right.timestamp)),
|
|
676
|
+
summary,
|
|
677
|
+
warnings,
|
|
678
|
+
debugInfo: {
|
|
679
|
+
scores: scores.sort((left, right) => right.score - left.score).map(({ entry, score }) => ({ id: entry.id, score, task: entry.task, timestamp: entry.timestamp }))
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
function scoreEntry(input) {
|
|
685
|
+
const promptTerms = tokenize(input.prompt);
|
|
686
|
+
const promptEntities = new Set(extractEntities(input.prompt).map((entity) => entity.toLowerCase()));
|
|
687
|
+
const entryTerms = tokenize(
|
|
688
|
+
[input.entry.rawPrompt, input.entry.optimizedPrompt, input.entry.constraints?.join(" ")].filter(Boolean).join(" ")
|
|
689
|
+
);
|
|
690
|
+
const entryEntities = new Set((input.entry.entities ?? []).map((entity) => entity.toLowerCase()));
|
|
691
|
+
const overlap = Array.from(promptTerms).filter((term) => entryTerms.has(term)).length;
|
|
692
|
+
const entityOverlap = Array.from(promptEntities).filter((entity) => entryEntities.has(entity)).length;
|
|
693
|
+
const taskMatch = input.task && input.entry.task === input.task ? 2 : 0;
|
|
694
|
+
const pinned = input.entry.pinned ? 3 : 0;
|
|
695
|
+
const pinnedConstraintMatch = Array.from(new Set(input.pinnedConstraints ?? [])).filter(
|
|
696
|
+
(constraint) => (input.entry.constraints ?? []).includes(constraint)
|
|
697
|
+
).length;
|
|
698
|
+
const tagOverlap = Array.from(new Set(input.tags ?? [])).filter((tag) => (input.entry.tags ?? []).includes(tag)).length;
|
|
699
|
+
const constraintBonus = (input.entry.constraints?.length ?? 0) > 0 ? 0.75 : 0;
|
|
700
|
+
const recency = (input.index + 1) / input.total;
|
|
701
|
+
return overlap * 0.8 + entityOverlap * 1.2 + taskMatch + pinned + pinnedConstraintMatch * 2 + tagOverlap + constraintBonus + recency;
|
|
702
|
+
}
|
|
703
|
+
function tokenize(value) {
|
|
704
|
+
return new Set(
|
|
705
|
+
value.toLowerCase().split(/[^a-z0-9]+/i).filter((token) => token.length > 2)
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
function extractConstraints(value) {
|
|
709
|
+
return value.split(/\n+/).map((line) => line.trim()).filter((line) => /(must|should|avoid|do not|don't|never|exactly|at most|under|limit|max)/i.test(line)).slice(0, 8);
|
|
710
|
+
}
|
|
711
|
+
function extractEntities(value) {
|
|
712
|
+
return Array.from(
|
|
713
|
+
new Set(value.match(/\b[A-Z][a-zA-Z0-9._-]+\b/g) ?? [])
|
|
714
|
+
).slice(0, 12);
|
|
715
|
+
}
|
|
716
|
+
function createFingerprint(entry) {
|
|
717
|
+
return [entry.task ?? "", entry.rawPrompt ?? entry.text].join("::").toLowerCase().replace(/\s+/g, " ").trim();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/core/modelSelector.ts
|
|
721
|
+
var DEFAULT_SMALL_MODEL_PREFERENCES = [
|
|
722
|
+
"qwen2.5:3b",
|
|
723
|
+
"phi3:mini",
|
|
724
|
+
"llama3.2:3b",
|
|
725
|
+
"qwen2.5:1.5b"
|
|
726
|
+
];
|
|
727
|
+
function getDefaultPreferredModels() {
|
|
728
|
+
return [...DEFAULT_SMALL_MODEL_PREFERENCES];
|
|
729
|
+
}
|
|
730
|
+
function selectOllamaModel(input) {
|
|
731
|
+
const preferred = buildPreferredOrder(input);
|
|
732
|
+
const smallCandidates = input.installedModels.filter((model) => isSuitableSmallModel(model));
|
|
733
|
+
const preferredMatch = findPreferredMatch(smallCandidates, preferred);
|
|
734
|
+
if (preferredMatch) {
|
|
735
|
+
return {
|
|
736
|
+
model: preferredMatch,
|
|
737
|
+
reason: `Selected installed model "${preferredMatch}" from the preferred low-memory order.`,
|
|
738
|
+
suitableForAutoUse: true
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
const ranked = [...smallCandidates].filter((model) => isUsefulGenerationModel(model.name)).map((model) => ({ model, score: scoreModel(model.name, input.preset, input.mode, input.task) })).sort((left, right) => right.score - left.score);
|
|
742
|
+
if (ranked[0]) {
|
|
743
|
+
return {
|
|
744
|
+
model: ranked[0].model.name,
|
|
745
|
+
reason: `Selected installed model "${ranked[0].model.name}" using task-aware ranking.`,
|
|
746
|
+
suitableForAutoUse: true
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const oversizedRanked = [...input.installedModels].filter((model) => isUsefulGenerationModel(model.name)).map((model) => ({ model, score: scoreModel(model.name, input.preset, input.mode, input.task) })).sort((left, right) => right.score - left.score);
|
|
750
|
+
if (oversizedRanked[0]) {
|
|
751
|
+
return {
|
|
752
|
+
model: oversizedRanked[0].model.name,
|
|
753
|
+
reason: `Installed model "${oversizedRanked[0].model.name}" was detected, but it is larger than the preferred low-memory range for auto-use.`,
|
|
754
|
+
suitableForAutoUse: false
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
model: preferred[0] ?? "qwen2.5:3b",
|
|
759
|
+
reason: "No installed Ollama models were discovered, so the default small-model preference was used.",
|
|
760
|
+
suitableForAutoUse: false
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function buildPreferredOrder(input) {
|
|
764
|
+
const taskContext = `${input.task ?? ""} ${input.preset} ${input.mode}`.toLowerCase();
|
|
765
|
+
const configured = (input.preferredModels ?? []).map((model) => model.toLowerCase());
|
|
766
|
+
if (taskContext.includes("code")) {
|
|
767
|
+
return uniqueModels([
|
|
768
|
+
...configured,
|
|
769
|
+
"qwen2.5-coder:3b",
|
|
770
|
+
"qwen2.5:3b",
|
|
771
|
+
"phi3:mini",
|
|
772
|
+
"llama3.2:3b",
|
|
773
|
+
"qwen2.5:1.5b"
|
|
774
|
+
]);
|
|
775
|
+
}
|
|
776
|
+
if (taskContext.includes("compress") || taskContext.includes("summar")) {
|
|
777
|
+
return uniqueModels([
|
|
778
|
+
...configured,
|
|
779
|
+
"qwen2.5:3b",
|
|
780
|
+
"qwen2.5:1.5b",
|
|
781
|
+
"phi3:mini",
|
|
782
|
+
"llama3.2:3b"
|
|
783
|
+
]);
|
|
784
|
+
}
|
|
785
|
+
return uniqueModels([...configured, ...DEFAULT_SMALL_MODEL_PREFERENCES]);
|
|
786
|
+
}
|
|
787
|
+
function uniqueModels(models) {
|
|
788
|
+
return Array.from(new Set(models));
|
|
789
|
+
}
|
|
790
|
+
function findPreferredMatch(installedModels, preferred) {
|
|
791
|
+
const installedNames = installedModels.map((model) => model.name);
|
|
792
|
+
for (const preferredName of preferred) {
|
|
793
|
+
const direct = installedNames.find((name) => name.toLowerCase() === preferredName);
|
|
794
|
+
if (direct) {
|
|
795
|
+
return direct;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
function scoreModel(modelName, preset, mode, task) {
|
|
801
|
+
const lower = modelName.toLowerCase();
|
|
802
|
+
let score = 0;
|
|
803
|
+
if (!isUsefulGenerationModel(lower)) {
|
|
804
|
+
return -100;
|
|
805
|
+
}
|
|
806
|
+
if (lower.includes("qwen2.5")) {
|
|
807
|
+
score += 4;
|
|
808
|
+
} else if (lower.includes("phi3")) {
|
|
809
|
+
score += 3.5;
|
|
810
|
+
} else if (lower.includes("llama3.2")) {
|
|
811
|
+
score += 3;
|
|
812
|
+
} else if (lower.includes("mistral")) {
|
|
813
|
+
score += 2;
|
|
814
|
+
}
|
|
815
|
+
const parameterSize = extractBillions(lower);
|
|
816
|
+
if (parameterSize !== null) {
|
|
817
|
+
if (parameterSize <= 4) {
|
|
818
|
+
score += 4;
|
|
819
|
+
} else if (parameterSize <= 8) {
|
|
820
|
+
score += 1;
|
|
821
|
+
} else {
|
|
822
|
+
score -= 4;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (lower.includes("mini")) {
|
|
826
|
+
score += 2;
|
|
827
|
+
}
|
|
828
|
+
if (lower.includes("instruct") || lower.includes("chat")) {
|
|
829
|
+
score += 1;
|
|
830
|
+
}
|
|
831
|
+
const taskContext = `${task ?? ""} ${preset} ${mode}`.toLowerCase();
|
|
832
|
+
if (taskContext.includes("code") && lower.includes("coder")) {
|
|
833
|
+
score += 3;
|
|
834
|
+
}
|
|
835
|
+
if ((taskContext.includes("compress") || taskContext.includes("summar")) && lower.includes("qwen2.5")) {
|
|
836
|
+
score += 1;
|
|
837
|
+
}
|
|
838
|
+
return score;
|
|
839
|
+
}
|
|
840
|
+
function extractBillions(modelName) {
|
|
841
|
+
const match = modelName.match(/(\d+(?:\.\d+)?)b/);
|
|
842
|
+
if (!match) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
return Number.parseFloat(match[1]);
|
|
846
|
+
}
|
|
847
|
+
function isUsefulGenerationModel(modelName) {
|
|
848
|
+
const lower = modelName.toLowerCase();
|
|
849
|
+
return ![
|
|
850
|
+
"embed",
|
|
851
|
+
"embedding",
|
|
852
|
+
"whisper",
|
|
853
|
+
"vision",
|
|
854
|
+
"diffusion",
|
|
855
|
+
"tts",
|
|
856
|
+
"bge",
|
|
857
|
+
"nomic-embed"
|
|
858
|
+
].some((blocked) => lower.includes(blocked));
|
|
859
|
+
}
|
|
860
|
+
function isSuitableSmallModel(model) {
|
|
861
|
+
if (!isUsefulGenerationModel(model.name)) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
const byParameter = extractBillions(`${model.parameterSize ?? ""} ${model.name}`);
|
|
865
|
+
if (byParameter !== null) {
|
|
866
|
+
return byParameter <= 4;
|
|
867
|
+
}
|
|
868
|
+
if (typeof model.sizeBytes === "number") {
|
|
869
|
+
return model.sizeBytes <= 55e8;
|
|
870
|
+
}
|
|
871
|
+
return /mini|1\.5b|2b|3b|4b/i.test(model.name);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/core/optimizer.ts
|
|
875
|
+
var DEFAULT_MODE = "claude_cli";
|
|
876
|
+
var DEFAULT_PRESET = "chat";
|
|
877
|
+
var DEFAULT_PROVIDER = "ollama";
|
|
878
|
+
var DEFAULT_MAX_INPUT_TOKENS = 1200;
|
|
879
|
+
var DEFAULT_MAX_CONTEXT_TOKENS = 800;
|
|
880
|
+
var DEFAULT_MAX_TOTAL_TOKENS = 2200;
|
|
881
|
+
var PromptOptimizer = class {
|
|
882
|
+
config;
|
|
883
|
+
logger;
|
|
884
|
+
estimator;
|
|
885
|
+
client;
|
|
886
|
+
compressor;
|
|
887
|
+
contextManager;
|
|
888
|
+
constructor(config = {}) {
|
|
889
|
+
this.logger = config.logger ?? noopLogger;
|
|
890
|
+
this.estimator = new TokenEstimator();
|
|
891
|
+
this.client = config.ollamaClient ?? new OllamaClient({
|
|
892
|
+
host: config.host,
|
|
893
|
+
model: config.ollamaModel,
|
|
894
|
+
timeoutMs: config.timeoutMs,
|
|
895
|
+
temperature: config.temperature,
|
|
896
|
+
logger: this.logger
|
|
897
|
+
});
|
|
898
|
+
const store = resolveSessionStore(config);
|
|
899
|
+
this.compressor = new ContextCompressor(this.estimator, this.client, this.logger);
|
|
900
|
+
this.contextManager = new ContextManager(store, this.estimator, this.compressor, this.logger);
|
|
901
|
+
this.config = {
|
|
902
|
+
...config,
|
|
903
|
+
host: config.host ?? "http://localhost:11434",
|
|
904
|
+
ollamaModel: config.ollamaModel,
|
|
905
|
+
preferredModels: config.preferredModels ?? getDefaultPreferredModels(),
|
|
906
|
+
timeoutMs: config.timeoutMs ?? 3e4,
|
|
907
|
+
temperature: config.temperature ?? 0.1
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async optimize(input) {
|
|
911
|
+
const originalPrompt = validatePrompt(input.prompt);
|
|
912
|
+
const mode = input.mode ?? this.config.defaultMode ?? DEFAULT_MODE;
|
|
913
|
+
const preset = input.preset ?? this.config.defaultPreset ?? DEFAULT_PRESET;
|
|
914
|
+
const maxInputTokens = input.maxInputTokens ?? this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
915
|
+
const maxContextTokens = input.maxContextTokens ?? this.config.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
|
|
916
|
+
const maxTotalTokens = input.maxTotalTokens ?? this.config.maxTotalTokens ?? DEFAULT_MAX_TOTAL_TOKENS;
|
|
917
|
+
const warnings = [];
|
|
918
|
+
const changes = [];
|
|
919
|
+
const useContext = input.useContext !== false && Boolean(input.sessionId);
|
|
920
|
+
const saveContext = input.saveContext ?? Boolean(input.sessionId);
|
|
921
|
+
const relevantContext = useContext && input.sessionId ? await this.contextManager.getRelevantContext({
|
|
922
|
+
sessionId: input.sessionId,
|
|
923
|
+
prompt: originalPrompt,
|
|
924
|
+
task: input.task,
|
|
925
|
+
tags: input.tags,
|
|
926
|
+
pinnedConstraints: input.pinnedConstraints,
|
|
927
|
+
maxContextTokens,
|
|
928
|
+
timeoutMs: input.timeoutMs ?? this.config.timeoutMs
|
|
929
|
+
}) : emptyRelevantContext();
|
|
930
|
+
warnings.push(...relevantContext.warnings);
|
|
931
|
+
const contextBlock = formatContextBlock(relevantContext);
|
|
932
|
+
const estimatedTokensBefore = this.estimator.estimateUsage({
|
|
933
|
+
prompt: originalPrompt,
|
|
934
|
+
context: contextBlock
|
|
935
|
+
});
|
|
936
|
+
if (estimatedTokensBefore.prompt > maxInputTokens) {
|
|
937
|
+
warnings.push(
|
|
938
|
+
`Raw prompt estimate (${estimatedTokensBefore.prompt}) exceeds maxInputTokens (${maxInputTokens}).`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const extractedConstraints = Array.from(
|
|
942
|
+
/* @__PURE__ */ new Set([...extractConstraints(originalPrompt), ...input.pinnedConstraints ?? []])
|
|
943
|
+
);
|
|
944
|
+
let provider = input.bypassOptimization ? "heuristic" : this.config.provider ?? DEFAULT_PROVIDER;
|
|
945
|
+
let model = provider === "ollama" ? this.config.ollamaModel ?? "auto" : "heuristic";
|
|
946
|
+
let optimizedPrompt = originalPrompt;
|
|
947
|
+
let providerWarnings = [];
|
|
948
|
+
let providerChanges = [];
|
|
949
|
+
if (provider === "ollama") {
|
|
950
|
+
const modelSelection = await this.resolveOllamaModel({
|
|
951
|
+
mode,
|
|
952
|
+
preset,
|
|
953
|
+
task: input.task
|
|
954
|
+
});
|
|
955
|
+
model = modelSelection.model;
|
|
956
|
+
providerWarnings.push(...modelSelection.warnings);
|
|
957
|
+
if (input.debug) {
|
|
958
|
+
changes.push(modelSelection.reason);
|
|
959
|
+
}
|
|
960
|
+
if (modelSelection.forceHeuristic) {
|
|
961
|
+
provider = "heuristic";
|
|
962
|
+
model = "heuristic";
|
|
963
|
+
}
|
|
964
|
+
const ollamaResult = provider === "ollama" ? await this.tryOllamaOptimization({
|
|
965
|
+
input: {
|
|
966
|
+
...input,
|
|
967
|
+
prompt: originalPrompt,
|
|
968
|
+
mode,
|
|
969
|
+
preset
|
|
970
|
+
},
|
|
971
|
+
model,
|
|
972
|
+
extractedConstraints,
|
|
973
|
+
relevantContext: contextBlock
|
|
974
|
+
}) : null;
|
|
975
|
+
if (ollamaResult) {
|
|
976
|
+
optimizedPrompt = ollamaResult.optimizedPrompt;
|
|
977
|
+
providerWarnings = ollamaResult.warnings;
|
|
978
|
+
providerChanges = ollamaResult.changes;
|
|
979
|
+
} else if (provider === "ollama") {
|
|
980
|
+
provider = "heuristic";
|
|
981
|
+
model = "heuristic";
|
|
982
|
+
providerWarnings = [
|
|
983
|
+
`Ollama was unavailable. Falling back to deterministic local prompt shaping.`
|
|
984
|
+
];
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (provider === "heuristic") {
|
|
988
|
+
const fallback = this.heuristicOptimize({
|
|
989
|
+
input: {
|
|
990
|
+
...input,
|
|
991
|
+
prompt: originalPrompt,
|
|
992
|
+
mode,
|
|
993
|
+
preset
|
|
994
|
+
},
|
|
995
|
+
context: relevantContext,
|
|
996
|
+
constraints: extractedConstraints
|
|
997
|
+
});
|
|
998
|
+
optimizedPrompt = fallback.optimizedPrompt;
|
|
999
|
+
providerChanges = [...providerChanges, ...fallback.changes];
|
|
1000
|
+
providerWarnings = [...providerWarnings, ...fallback.warnings];
|
|
1001
|
+
}
|
|
1002
|
+
warnings.push(...providerWarnings);
|
|
1003
|
+
changes.push(...providerChanges);
|
|
1004
|
+
let finalPrompt = composeFinalPrompt({
|
|
1005
|
+
optimizedPrompt,
|
|
1006
|
+
input: {
|
|
1007
|
+
...input,
|
|
1008
|
+
prompt: originalPrompt,
|
|
1009
|
+
mode,
|
|
1010
|
+
preset
|
|
1011
|
+
},
|
|
1012
|
+
context: relevantContext
|
|
1013
|
+
});
|
|
1014
|
+
let estimatedTokensAfter = {
|
|
1015
|
+
prompt: this.estimator.estimateText(optimizedPrompt),
|
|
1016
|
+
context: this.estimator.estimateText(formatContextBlock(relevantContext)),
|
|
1017
|
+
total: this.estimator.estimateText(finalPrompt)
|
|
1018
|
+
};
|
|
1019
|
+
if (estimatedTokensAfter.total > maxTotalTokens) {
|
|
1020
|
+
const reduced = await this.reduceToBudget({
|
|
1021
|
+
input: {
|
|
1022
|
+
...input,
|
|
1023
|
+
prompt: originalPrompt,
|
|
1024
|
+
mode,
|
|
1025
|
+
preset
|
|
1026
|
+
},
|
|
1027
|
+
optimizedPrompt,
|
|
1028
|
+
context: relevantContext,
|
|
1029
|
+
maxTotalTokens
|
|
1030
|
+
});
|
|
1031
|
+
finalPrompt = reduced.finalPrompt;
|
|
1032
|
+
estimatedTokensAfter = reduced.estimatedTokensAfter;
|
|
1033
|
+
if (reduced.warning) {
|
|
1034
|
+
warnings.push(reduced.warning);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (estimatedTokensAfter.total > maxTotalTokens) {
|
|
1038
|
+
throw new TokenBudgetExceededError();
|
|
1039
|
+
}
|
|
1040
|
+
if (saveContext && input.sessionId) {
|
|
1041
|
+
await this.contextManager.saveContext({
|
|
1042
|
+
sessionId: input.sessionId,
|
|
1043
|
+
input: {
|
|
1044
|
+
...input,
|
|
1045
|
+
prompt: originalPrompt
|
|
1046
|
+
},
|
|
1047
|
+
optimizedPrompt,
|
|
1048
|
+
finalPrompt,
|
|
1049
|
+
contextSummary: relevantContext.summary
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
originalPrompt,
|
|
1054
|
+
optimizedPrompt,
|
|
1055
|
+
finalPrompt,
|
|
1056
|
+
usedContext: relevantContext.usedEntries,
|
|
1057
|
+
contextSummary: relevantContext.summary,
|
|
1058
|
+
estimatedTokensBefore,
|
|
1059
|
+
estimatedTokensAfter,
|
|
1060
|
+
tokenSavings: Math.max(0, estimatedTokensBefore.total - estimatedTokensAfter.total),
|
|
1061
|
+
mode,
|
|
1062
|
+
provider,
|
|
1063
|
+
model,
|
|
1064
|
+
warnings,
|
|
1065
|
+
changes,
|
|
1066
|
+
debugInfo: input.debug ? {
|
|
1067
|
+
context: relevantContext.debugInfo,
|
|
1068
|
+
estimatedTokensBefore,
|
|
1069
|
+
estimatedTokensAfter,
|
|
1070
|
+
extractedConstraints,
|
|
1071
|
+
preset,
|
|
1072
|
+
selectedModel: model
|
|
1073
|
+
} : void 0
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async clearContext(sessionId) {
|
|
1077
|
+
await this.contextManager.clearContext(sessionId);
|
|
1078
|
+
}
|
|
1079
|
+
async loadContext(sessionId) {
|
|
1080
|
+
return this.contextManager.loadContext(sessionId);
|
|
1081
|
+
}
|
|
1082
|
+
async summarizeContext(sessionId, prompt, task, budgetTokens = 200) {
|
|
1083
|
+
return this.contextManager.summarizeContext(sessionId, prompt, task, budgetTokens, this.config.timeoutMs);
|
|
1084
|
+
}
|
|
1085
|
+
async getRelevantContext(sessionId, prompt, task, maxContextTokens) {
|
|
1086
|
+
return this.contextManager.getRelevantContext({
|
|
1087
|
+
sessionId,
|
|
1088
|
+
prompt,
|
|
1089
|
+
task,
|
|
1090
|
+
maxContextTokens: maxContextTokens ?? this.config.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS,
|
|
1091
|
+
timeoutMs: this.config.timeoutMs
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
async tryOllamaOptimization(options) {
|
|
1095
|
+
try {
|
|
1096
|
+
if (!await this.client.isAvailable()) {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
const response = await this.client.generateJson({
|
|
1100
|
+
systemPrompt: getOptimizationSystemPrompt(options.input.mode, options.input.preset),
|
|
1101
|
+
prompt: buildOptimizationPrompt(options.input, options.relevantContext, options.extractedConstraints),
|
|
1102
|
+
timeoutMs: options.input.timeoutMs ?? this.config.timeoutMs,
|
|
1103
|
+
model: options.model,
|
|
1104
|
+
temperature: this.config.temperature,
|
|
1105
|
+
format: "json"
|
|
1106
|
+
});
|
|
1107
|
+
const optimizedPrompt = normalizeWhitespace(response.optimizedPrompt ?? "");
|
|
1108
|
+
if (!optimizedPrompt) {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
optimizedPrompt,
|
|
1113
|
+
changes: response.changes ?? [`Applied Ollama optimization with ${options.model}.`],
|
|
1114
|
+
warnings: response.warnings ?? []
|
|
1115
|
+
};
|
|
1116
|
+
} catch {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async resolveOllamaModel(options) {
|
|
1121
|
+
if (this.config.ollamaModel) {
|
|
1122
|
+
return {
|
|
1123
|
+
model: this.config.ollamaModel,
|
|
1124
|
+
warnings: [],
|
|
1125
|
+
reason: `Using explicitly configured model "${this.config.ollamaModel}".`,
|
|
1126
|
+
forceHeuristic: false
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
if (!this.client.listModels) {
|
|
1130
|
+
const fallback = this.config.preferredModels[0] ?? "qwen2.5:3b";
|
|
1131
|
+
return {
|
|
1132
|
+
model: fallback,
|
|
1133
|
+
warnings: [`Model auto-selection is unavailable in the current Ollama client, so "${fallback}" was assumed.`],
|
|
1134
|
+
reason: `Assumed default model "${fallback}" because model discovery is unsupported.`,
|
|
1135
|
+
forceHeuristic: false
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
try {
|
|
1139
|
+
const installedModels = await this.client.listModels();
|
|
1140
|
+
const selection = selectOllamaModel({
|
|
1141
|
+
installedModels,
|
|
1142
|
+
mode: options.mode,
|
|
1143
|
+
preset: options.preset,
|
|
1144
|
+
task: options.task,
|
|
1145
|
+
preferredModels: this.config.preferredModels
|
|
1146
|
+
});
|
|
1147
|
+
if (!selection.suitableForAutoUse) {
|
|
1148
|
+
return {
|
|
1149
|
+
model: selection.model,
|
|
1150
|
+
warnings: [
|
|
1151
|
+
`No suitable small Ollama model was found for auto-use. Falling back to heuristic optimization. Install one of: ${this.config.preferredModels.join(", ")}.`,
|
|
1152
|
+
selection.reason
|
|
1153
|
+
],
|
|
1154
|
+
reason: selection.reason,
|
|
1155
|
+
forceHeuristic: true
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
model: selection.model,
|
|
1160
|
+
warnings: installedModels.length === 0 ? [`No installed Ollama models were reported, so "${selection.model}" was chosen as the default preference.`] : [],
|
|
1161
|
+
reason: selection.reason,
|
|
1162
|
+
forceHeuristic: false
|
|
1163
|
+
};
|
|
1164
|
+
} catch {
|
|
1165
|
+
const fallback = this.config.preferredModels[0] ?? "qwen2.5:3b";
|
|
1166
|
+
return {
|
|
1167
|
+
model: fallback,
|
|
1168
|
+
warnings: [`Failed to inspect local Ollama models, so "${fallback}" was chosen as the default preference.`],
|
|
1169
|
+
reason: `Fell back to default model "${fallback}" because model discovery failed.`,
|
|
1170
|
+
forceHeuristic: false
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
heuristicOptimize(options) {
|
|
1175
|
+
const lines = [
|
|
1176
|
+
`Request: ${options.input.prompt}`,
|
|
1177
|
+
options.input.task ? `Task type: ${options.input.task}` : "",
|
|
1178
|
+
options.input.tone ? `Tone: ${options.input.tone}` : "",
|
|
1179
|
+
options.input.outputFormat ? `Output format: ${options.input.outputFormat}` : "",
|
|
1180
|
+
options.input.maxLength ? `Maximum length: ${options.input.maxLength}` : "",
|
|
1181
|
+
options.constraints.length ? `Critical constraints: ${options.constraints.join("; ")}` : ""
|
|
1182
|
+
].filter(Boolean);
|
|
1183
|
+
const optimizedPrompt = lines.join("\n");
|
|
1184
|
+
const changes = ["Normalized prompt structure for downstream model consumption."];
|
|
1185
|
+
if (options.input.mode === "compress" || options.input.mode === "concise") {
|
|
1186
|
+
changes.push("Applied concise formatting to reduce token usage.");
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
optimizedPrompt,
|
|
1190
|
+
changes,
|
|
1191
|
+
warnings: []
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
async reduceToBudget(options) {
|
|
1195
|
+
const summaryBudget = Math.max(80, Math.floor(options.maxTotalTokens * 0.2));
|
|
1196
|
+
const summary = options.input.sessionId ? await this.summarizeContext(options.input.sessionId, options.input.prompt, options.input.task, summaryBudget) : null;
|
|
1197
|
+
const compactContext = {
|
|
1198
|
+
...options.context,
|
|
1199
|
+
usedEntries: [],
|
|
1200
|
+
summary
|
|
1201
|
+
};
|
|
1202
|
+
const finalPrompt = composeFinalPrompt({
|
|
1203
|
+
optimizedPrompt: this.estimator.truncateToBudget(options.optimizedPrompt, Math.floor(options.maxTotalTokens * 0.5)),
|
|
1204
|
+
input: options.input,
|
|
1205
|
+
context: compactContext
|
|
1206
|
+
});
|
|
1207
|
+
return {
|
|
1208
|
+
finalPrompt,
|
|
1209
|
+
estimatedTokensAfter: {
|
|
1210
|
+
prompt: this.estimator.estimateText(options.optimizedPrompt),
|
|
1211
|
+
context: this.estimator.estimateText(formatContextBlock(compactContext)),
|
|
1212
|
+
total: this.estimator.estimateText(finalPrompt)
|
|
1213
|
+
},
|
|
1214
|
+
warning: "Summarized stored context further to stay within the total token budget."
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
function resolveSessionStore(config) {
|
|
1219
|
+
if (typeof config.contextStore === "object" && config.contextStore !== null) {
|
|
1220
|
+
return config.contextStore;
|
|
1221
|
+
}
|
|
1222
|
+
if (config.contextStore === "sqlite") {
|
|
1223
|
+
return new SQLiteSessionStore(config.sqlitePath);
|
|
1224
|
+
}
|
|
1225
|
+
return new FileSessionStore(config.storageDir);
|
|
1226
|
+
}
|
|
1227
|
+
function composeFinalPrompt(input) {
|
|
1228
|
+
const sections = [];
|
|
1229
|
+
sections.push(`Task:
|
|
1230
|
+
${input.optimizedPrompt}`);
|
|
1231
|
+
const contextBlock = formatContextBlock(input.context);
|
|
1232
|
+
if (contextBlock) {
|
|
1233
|
+
sections.push(`Relevant context:
|
|
1234
|
+
${contextBlock}`);
|
|
1235
|
+
}
|
|
1236
|
+
const constraints = Array.from(
|
|
1237
|
+
new Set([
|
|
1238
|
+
...input.input.pinnedConstraints ?? [],
|
|
1239
|
+
...extractConstraints(input.input.prompt),
|
|
1240
|
+
input.input.maxLength ? `Keep the response within ${input.input.maxLength} units if possible.` : "",
|
|
1241
|
+
input.input.tone ? `Maintain a ${input.input.tone} tone.` : "",
|
|
1242
|
+
input.input.outputFormat ? `Return the output as ${input.input.outputFormat}.` : ""
|
|
1243
|
+
].filter(Boolean))
|
|
1244
|
+
);
|
|
1245
|
+
if (constraints.length > 0) {
|
|
1246
|
+
sections.push(`Constraints:
|
|
1247
|
+
- ${constraints.join("\n- ")}`);
|
|
1248
|
+
}
|
|
1249
|
+
const desiredOutput = [
|
|
1250
|
+
input.input.targetModel ? `Target model: ${input.input.targetModel}` : "Target model: claude",
|
|
1251
|
+
`Mode: ${input.input.mode}`,
|
|
1252
|
+
`Preset: ${input.input.preset}`
|
|
1253
|
+
];
|
|
1254
|
+
sections.push(`Desired output:
|
|
1255
|
+
- ${desiredOutput.join("\n- ")}`);
|
|
1256
|
+
return sections.join("\n\n").trim();
|
|
1257
|
+
}
|
|
1258
|
+
function formatContextBlock(context) {
|
|
1259
|
+
const lines = [];
|
|
1260
|
+
if (context.summary?.text) {
|
|
1261
|
+
lines.push(`Summary: ${context.summary.text}`);
|
|
1262
|
+
}
|
|
1263
|
+
for (const entry of context.usedEntries.slice(-4)) {
|
|
1264
|
+
lines.push(`- ${entry.optimizedPrompt ?? entry.rawPrompt ?? entry.text}`);
|
|
1265
|
+
}
|
|
1266
|
+
return normalizeWhitespace(lines.join("\n"));
|
|
1267
|
+
}
|
|
1268
|
+
function emptyRelevantContext() {
|
|
1269
|
+
return {
|
|
1270
|
+
usedEntries: [],
|
|
1271
|
+
summary: null,
|
|
1272
|
+
warnings: [],
|
|
1273
|
+
debugInfo: {}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/index.ts
|
|
1278
|
+
function createOptimizer(config = {}) {
|
|
1279
|
+
return new PromptOptimizer(config);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/cli.ts
|
|
1283
|
+
async function runCli(argv, io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }, dependencies = { createOptimizer, readStdin }) {
|
|
1284
|
+
const [command, ...rest] = argv;
|
|
1285
|
+
if (!command || command === "--help" || command === "-h") {
|
|
1286
|
+
io.stdout.write(`${getHelpText()}
|
|
1287
|
+
`);
|
|
1288
|
+
return 0;
|
|
1289
|
+
}
|
|
1290
|
+
if (command !== "optimize") {
|
|
1291
|
+
io.stderr.write(`Unknown command: ${command}
|
|
1292
|
+
`);
|
|
1293
|
+
io.stderr.write(`${getHelpText()}
|
|
1294
|
+
`);
|
|
1295
|
+
return 1;
|
|
1296
|
+
}
|
|
1297
|
+
const parsed = parseOptimizeArgs(rest);
|
|
1298
|
+
if (parsed.help) {
|
|
1299
|
+
io.stdout.write(`${getHelpText()}
|
|
1300
|
+
`);
|
|
1301
|
+
return 0;
|
|
1302
|
+
}
|
|
1303
|
+
if (!parsed.sessionId && parsed.clearSession) {
|
|
1304
|
+
io.stderr.write("--clear-session requires --session <id>.\n");
|
|
1305
|
+
return 1;
|
|
1306
|
+
}
|
|
1307
|
+
const optimizer = dependencies.createOptimizer({
|
|
1308
|
+
provider: "ollama",
|
|
1309
|
+
ollamaModel: parsed.model,
|
|
1310
|
+
host: parsed.host,
|
|
1311
|
+
contextStore: parsed.contextStore,
|
|
1312
|
+
storageDir: parsed.storageDir,
|
|
1313
|
+
sqlitePath: parsed.sqlitePath,
|
|
1314
|
+
timeoutMs: parsed.timeoutMs,
|
|
1315
|
+
logger: createLogger(parsed.debug)
|
|
1316
|
+
});
|
|
1317
|
+
if (parsed.clearSession && parsed.sessionId) {
|
|
1318
|
+
await optimizer.clearContext(parsed.sessionId);
|
|
1319
|
+
if (!parsed.prompt) {
|
|
1320
|
+
io.stdout.write(`Cleared session ${parsed.sessionId}
|
|
1321
|
+
`);
|
|
1322
|
+
return 0;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (!parsed.prompt) {
|
|
1326
|
+
const stdinPrompt = await dependencies.readStdin(io.stdin);
|
|
1327
|
+
parsed.prompt = stdinPrompt.trim() || void 0;
|
|
1328
|
+
}
|
|
1329
|
+
if (!parsed.prompt) {
|
|
1330
|
+
io.stderr.write("A prompt is required.\n");
|
|
1331
|
+
return 1;
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
const result = await optimizer.optimize({
|
|
1335
|
+
prompt: parsed.prompt,
|
|
1336
|
+
task: parsed.task,
|
|
1337
|
+
tone: parsed.tone,
|
|
1338
|
+
mode: parsed.mode,
|
|
1339
|
+
preset: parsed.preset,
|
|
1340
|
+
sessionId: parsed.sessionId,
|
|
1341
|
+
saveContext: parsed.saveContext,
|
|
1342
|
+
useContext: parsed.useContext,
|
|
1343
|
+
targetModel: parsed.targetModel ?? "claude",
|
|
1344
|
+
outputFormat: parsed.outputFormat,
|
|
1345
|
+
maxLength: parsed.maxLength,
|
|
1346
|
+
tags: parsed.tags,
|
|
1347
|
+
pinnedConstraints: parsed.pinnedConstraints,
|
|
1348
|
+
debug: parsed.debug,
|
|
1349
|
+
plainOutput: parsed.plain,
|
|
1350
|
+
maxTotalTokens: parsed.maxTotalTokens,
|
|
1351
|
+
maxContextTokens: parsed.maxContextTokens,
|
|
1352
|
+
maxInputTokens: parsed.maxInputTokens,
|
|
1353
|
+
timeoutMs: parsed.timeoutMs,
|
|
1354
|
+
bypassOptimization: parsed.bypassOptimization
|
|
1355
|
+
});
|
|
1356
|
+
if (parsed.json) {
|
|
1357
|
+
io.stdout.write(`${toPrettyJson(result)}
|
|
1358
|
+
`);
|
|
1359
|
+
return 0;
|
|
1360
|
+
}
|
|
1361
|
+
if (parsed.plain) {
|
|
1362
|
+
io.stdout.write(`${result.finalPrompt}
|
|
1363
|
+
`);
|
|
1364
|
+
return 0;
|
|
1365
|
+
}
|
|
1366
|
+
io.stdout.write(`${result.finalPrompt}
|
|
1367
|
+
|
|
1368
|
+
`);
|
|
1369
|
+
io.stdout.write(`provider=${result.provider} model=${result.model} tokens=${result.estimatedTokensAfter.total} savings=${result.tokenSavings}
|
|
1370
|
+
`);
|
|
1371
|
+
if (result.warnings.length > 0) {
|
|
1372
|
+
io.stdout.write(`warnings=${result.warnings.join(" | ")}
|
|
1373
|
+
`);
|
|
1374
|
+
}
|
|
1375
|
+
return 0;
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
const message = error instanceof Error ? error.message : "Unknown CLI error.";
|
|
1378
|
+
io.stderr.write(`${message}
|
|
1379
|
+
`);
|
|
1380
|
+
return 1;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
function parseOptimizeArgs(args) {
|
|
1384
|
+
const parsed = {
|
|
1385
|
+
plain: false,
|
|
1386
|
+
json: false,
|
|
1387
|
+
debug: false,
|
|
1388
|
+
clearSession: false,
|
|
1389
|
+
useContext: true,
|
|
1390
|
+
bypassOptimization: false,
|
|
1391
|
+
help: false,
|
|
1392
|
+
tags: [],
|
|
1393
|
+
pinnedConstraints: []
|
|
1394
|
+
};
|
|
1395
|
+
const positionals = [];
|
|
1396
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1397
|
+
const arg = args[index];
|
|
1398
|
+
switch (arg) {
|
|
1399
|
+
case "--session":
|
|
1400
|
+
parsed.sessionId = args[++index];
|
|
1401
|
+
break;
|
|
1402
|
+
case "--model":
|
|
1403
|
+
parsed.model = args[++index];
|
|
1404
|
+
break;
|
|
1405
|
+
case "--mode":
|
|
1406
|
+
parsed.mode = args[++index];
|
|
1407
|
+
break;
|
|
1408
|
+
case "--task":
|
|
1409
|
+
parsed.task = args[++index];
|
|
1410
|
+
break;
|
|
1411
|
+
case "--tone":
|
|
1412
|
+
parsed.tone = args[++index];
|
|
1413
|
+
break;
|
|
1414
|
+
case "--preset":
|
|
1415
|
+
parsed.preset = args[++index];
|
|
1416
|
+
break;
|
|
1417
|
+
case "--target-model":
|
|
1418
|
+
parsed.targetModel = args[++index];
|
|
1419
|
+
break;
|
|
1420
|
+
case "--output-format":
|
|
1421
|
+
parsed.outputFormat = args[++index];
|
|
1422
|
+
break;
|
|
1423
|
+
case "--max-length":
|
|
1424
|
+
parsed.maxLength = Number(args[++index]);
|
|
1425
|
+
break;
|
|
1426
|
+
case "--tag":
|
|
1427
|
+
parsed.tags.push(args[++index]);
|
|
1428
|
+
break;
|
|
1429
|
+
case "--pin-constraint":
|
|
1430
|
+
parsed.pinnedConstraints.push(args[++index]);
|
|
1431
|
+
break;
|
|
1432
|
+
case "--host":
|
|
1433
|
+
parsed.host = args[++index];
|
|
1434
|
+
break;
|
|
1435
|
+
case "--store":
|
|
1436
|
+
parsed.contextStore = args[++index];
|
|
1437
|
+
break;
|
|
1438
|
+
case "--storage-dir":
|
|
1439
|
+
parsed.storageDir = args[++index];
|
|
1440
|
+
break;
|
|
1441
|
+
case "--sqlite-path":
|
|
1442
|
+
parsed.sqlitePath = args[++index];
|
|
1443
|
+
break;
|
|
1444
|
+
case "--plain":
|
|
1445
|
+
parsed.plain = true;
|
|
1446
|
+
break;
|
|
1447
|
+
case "--json":
|
|
1448
|
+
parsed.json = true;
|
|
1449
|
+
break;
|
|
1450
|
+
case "--debug":
|
|
1451
|
+
parsed.debug = true;
|
|
1452
|
+
break;
|
|
1453
|
+
case "--save-context":
|
|
1454
|
+
parsed.saveContext = true;
|
|
1455
|
+
break;
|
|
1456
|
+
case "--no-context":
|
|
1457
|
+
parsed.useContext = false;
|
|
1458
|
+
break;
|
|
1459
|
+
case "--clear-session":
|
|
1460
|
+
parsed.clearSession = true;
|
|
1461
|
+
break;
|
|
1462
|
+
case "--max-total-tokens":
|
|
1463
|
+
parsed.maxTotalTokens = Number(args[++index]);
|
|
1464
|
+
break;
|
|
1465
|
+
case "--max-context-tokens":
|
|
1466
|
+
parsed.maxContextTokens = Number(args[++index]);
|
|
1467
|
+
break;
|
|
1468
|
+
case "--max-input-tokens":
|
|
1469
|
+
parsed.maxInputTokens = Number(args[++index]);
|
|
1470
|
+
break;
|
|
1471
|
+
case "--timeout":
|
|
1472
|
+
parsed.timeoutMs = Number(args[++index]);
|
|
1473
|
+
break;
|
|
1474
|
+
case "--bypass-optimization":
|
|
1475
|
+
parsed.bypassOptimization = true;
|
|
1476
|
+
break;
|
|
1477
|
+
case "--help":
|
|
1478
|
+
case "-h":
|
|
1479
|
+
parsed.help = true;
|
|
1480
|
+
break;
|
|
1481
|
+
default:
|
|
1482
|
+
positionals.push(arg);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
parsed.prompt = positionals.join(" ").trim() || void 0;
|
|
1486
|
+
return parsed;
|
|
1487
|
+
}
|
|
1488
|
+
function getHelpText() {
|
|
1489
|
+
return [
|
|
1490
|
+
"promptpilot optimize <prompt> [options]",
|
|
1491
|
+
"",
|
|
1492
|
+
"Options:",
|
|
1493
|
+
" --session <id>",
|
|
1494
|
+
" --model <name> Override auto-selected local Ollama model",
|
|
1495
|
+
" --mode <mode>",
|
|
1496
|
+
" --task <task>",
|
|
1497
|
+
" --tone <tone>",
|
|
1498
|
+
" --preset <preset>",
|
|
1499
|
+
" --target-model <name>",
|
|
1500
|
+
" --output-format <text>",
|
|
1501
|
+
" --max-length <n>",
|
|
1502
|
+
" --tag <value> Repeatable",
|
|
1503
|
+
" --pin-constraint <text> Repeatable",
|
|
1504
|
+
" --host <url>",
|
|
1505
|
+
" --store <local|sqlite>",
|
|
1506
|
+
" --storage-dir <path>",
|
|
1507
|
+
" --sqlite-path <path>",
|
|
1508
|
+
" --plain",
|
|
1509
|
+
" --json",
|
|
1510
|
+
" --debug",
|
|
1511
|
+
" --save-context",
|
|
1512
|
+
" --no-context",
|
|
1513
|
+
" --clear-session",
|
|
1514
|
+
" --max-total-tokens <n>",
|
|
1515
|
+
" --max-context-tokens <n>",
|
|
1516
|
+
" --max-input-tokens <n>",
|
|
1517
|
+
" --timeout <ms>",
|
|
1518
|
+
" --bypass-optimization"
|
|
1519
|
+
].join("\n");
|
|
1520
|
+
}
|
|
1521
|
+
async function readStdin(stdin = process.stdin) {
|
|
1522
|
+
if (!stdin || stdin.isTTY) {
|
|
1523
|
+
return "";
|
|
1524
|
+
}
|
|
1525
|
+
return new Promise((resolve, reject) => {
|
|
1526
|
+
let data = "";
|
|
1527
|
+
stdin.setEncoding("utf8");
|
|
1528
|
+
stdin.on("data", (chunk) => {
|
|
1529
|
+
data += chunk;
|
|
1530
|
+
});
|
|
1531
|
+
stdin.on("end", () => resolve(data));
|
|
1532
|
+
stdin.on("error", reject);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1536
|
+
runCli(process.argv.slice(2)).then((code) => {
|
|
1537
|
+
process.exit(code);
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
export {
|
|
1541
|
+
runCli
|
|
1542
|
+
};
|
|
1543
|
+
//# sourceMappingURL=cli.js.map
|