@oscharko-dev/keiko-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +103 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +108 -0
- package/dist/evaluate.d.ts +8 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +270 -0
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +129 -0
- package/dist/gateway-config.d.ts +12 -0
- package/dist/gateway-config.d.ts.map +1 -0
- package/dist/gateway-config.js +19 -0
- package/dist/gen-tests.d.ts +8 -0
- package/dist/gen-tests.d.ts.map +1 -0
- package/dist/gen-tests.js +216 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/init.d.ts +9 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +122 -0
- package/dist/install-layout.d.ts +19 -0
- package/dist/install-layout.d.ts.map +1 -0
- package/dist/install-layout.js +76 -0
- package/dist/investigate.d.ts +9 -0
- package/dist/investigate.d.ts.map +1 -0
- package/dist/investigate.js +249 -0
- package/dist/launcher-paths.d.ts +4 -0
- package/dist/launcher-paths.d.ts.map +1 -0
- package/dist/launcher-paths.js +69 -0
- package/dist/launcher-platforms.d.ts +25 -0
- package/dist/launcher-platforms.d.ts.map +1 -0
- package/dist/launcher-platforms.js +131 -0
- package/dist/launcher-state.d.ts +25 -0
- package/dist/launcher-state.d.ts.map +1 -0
- package/dist/launcher-state.js +228 -0
- package/dist/launcher.d.ts +21 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +439 -0
- package/dist/lifecycle.d.ts +22 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +425 -0
- package/dist/memory.d.ts +14 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +290 -0
- package/dist/models.d.ts +4 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +62 -0
- package/dist/prompt-enhancer.d.ts +13 -0
- package/dist/prompt-enhancer.d.ts.map +1 -0
- package/dist/prompt-enhancer.js +261 -0
- package/dist/repair.d.ts +10 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +402 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +269 -0
- package/dist/runner.d.ts +7 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +108 -0
- package/dist/state-paths.d.ts +43 -0
- package/dist/state-paths.d.ts.map +1 -0
- package/dist/state-paths.js +396 -0
- package/dist/ui.d.ts +39 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +450 -0
- package/dist/uninstall.d.ts +10 -0
- package/dist/uninstall.d.ts.map +1 -0
- package/dist/uninstall.js +345 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +108 -0
- package/package.json +42 -0
package/dist/memory.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// `keiko memory` — operator surface for the governed memory vault (Epic #204).
|
|
2
|
+
//
|
|
3
|
+
// maintain Run one bounded maintenance pass IN-PROCESS (consolidate + decay + reinforce +
|
|
4
|
+
// forget) against the local vault and print the applied counts. Reuses the exact same
|
|
5
|
+
// `runMemoryMaintenance` core the BFF route uses, so the CLI and UI never drift.
|
|
6
|
+
// stats Print memory counts by status, by scope kind, and the total.
|
|
7
|
+
// diagnostics
|
|
8
|
+
// Print a redacted body-free diagnostics snapshot for local support.
|
|
9
|
+
//
|
|
10
|
+
// The vault is opened at the resolved memory dir (default $KEIKO_MEMORY_DIR or the platform state
|
|
11
|
+
// dir; override with --memory-dir). Tests inject a vault via deps so no disk is touched. Exit 0 on
|
|
12
|
+
// success, 1 on a runtime error (vault open / maintenance fault), 2 on usage (unknown/missing
|
|
13
|
+
// subcommand).
|
|
14
|
+
import { createMemoryVault } from "@oscharko-dev/keiko-memory-vault";
|
|
15
|
+
import { createMemoryEmbedder, exportMemoryDiagnostics, runMemoryMaintenance, } from "@oscharko-dev/keiko-server";
|
|
16
|
+
import { createAuditRedactor, createNodeEvidenceStore, resolveEvidenceDir, } from "@oscharko-dev/keiko-evidence";
|
|
17
|
+
import { requestOpenAIEmbedding, GatewayError, } from "@oscharko-dev/keiko-model-gateway";
|
|
18
|
+
import { loadGatewayConfigFromFile } from "./gateway-config.js";
|
|
19
|
+
const USAGE = `Usage:
|
|
20
|
+
keiko memory maintain [--memory-dir PATH] [--evidence-dir PATH]
|
|
21
|
+
Run a bounded consolidate + archive + forget pass.
|
|
22
|
+
keiko memory stats [--memory-dir PATH] Print memory counts by status and scope.
|
|
23
|
+
keiko memory diagnostics [--memory-dir PATH] [--evidence-dir PATH] [--last N]
|
|
24
|
+
Print redacted local diagnostics JSON.
|
|
25
|
+
keiko memory reembed [--memory-dir PATH] [--limit N] [--config PATH]
|
|
26
|
+
Backfill embeddings for accepted memories lacking one.
|
|
27
|
+
|
|
28
|
+
Opens the local memory vault (default $KEIKO_MEMORY_DIR or the platform state dir; override with
|
|
29
|
+
--memory-dir). \`maintain\` promotes strong proposals, archives faded memories, forgets
|
|
30
|
+
expired/very-faint ones, and reports unresolved consolidation review items (memory strength itself
|
|
31
|
+
is derived live at retrieval; this pass never rewrites confidence). \`diagnostics\`
|
|
32
|
+
prints schema version, generated time, scope/status counts, redacted storage path, and a bounded
|
|
33
|
+
audit tail without memory body or payload content. \`reembed\` computes the embedding for each
|
|
34
|
+
accepted memory that has none (bounded by --limit, default 200), so pre-existing memories become
|
|
35
|
+
semantically retrievable; it is gated on an embedding model being configured (via --config /
|
|
36
|
+
$KEIKO_CONFIG_FILE) and is best-effort.
|
|
37
|
+
`;
|
|
38
|
+
const DEFAULT_REEMBED_LIMIT = 200;
|
|
39
|
+
function flagValue(args, name) {
|
|
40
|
+
const i = args.indexOf(name);
|
|
41
|
+
if (i === -1)
|
|
42
|
+
return undefined;
|
|
43
|
+
const value = args[i + 1];
|
|
44
|
+
return value === undefined || value.startsWith("--") ? undefined : value;
|
|
45
|
+
}
|
|
46
|
+
function resolveVault(args, env, deps) {
|
|
47
|
+
if (deps.vault !== undefined)
|
|
48
|
+
return deps.vault;
|
|
49
|
+
const memoryDir = flagValue(args, "--memory-dir");
|
|
50
|
+
if (deps.openVault !== undefined)
|
|
51
|
+
return deps.openVault(memoryDir, env);
|
|
52
|
+
return createMemoryVault({
|
|
53
|
+
...(memoryDir !== undefined ? { memoryDir } : {}),
|
|
54
|
+
env,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function scopeKindOf(scope) {
|
|
58
|
+
return scope.kind;
|
|
59
|
+
}
|
|
60
|
+
function scopeKey(scope) {
|
|
61
|
+
switch (scope.kind) {
|
|
62
|
+
case "user":
|
|
63
|
+
return `user:${scope.userId}`;
|
|
64
|
+
case "workspace":
|
|
65
|
+
return `workspace:${scope.workspaceId}`;
|
|
66
|
+
case "project":
|
|
67
|
+
return `project:${scope.projectId}`;
|
|
68
|
+
case "workflow":
|
|
69
|
+
return `workflow:${scope.workflowDefinitionId}`;
|
|
70
|
+
case "global":
|
|
71
|
+
return "global";
|
|
72
|
+
default: {
|
|
73
|
+
const never = scope;
|
|
74
|
+
return never;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function uniqueRecordScopes(records) {
|
|
79
|
+
const scopes = new Map();
|
|
80
|
+
for (const record of records) {
|
|
81
|
+
scopes.set(scopeKey(record.scope), record.scope);
|
|
82
|
+
}
|
|
83
|
+
return scopes.size === 0 ? [{ kind: "global" }] : [...scopes.values()];
|
|
84
|
+
}
|
|
85
|
+
function tallyBy(records, keyOf) {
|
|
86
|
+
const counts = new Map();
|
|
87
|
+
for (const record of records) {
|
|
88
|
+
const key = keyOf(record);
|
|
89
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
90
|
+
}
|
|
91
|
+
return counts;
|
|
92
|
+
}
|
|
93
|
+
function renderTally(title, counts) {
|
|
94
|
+
const rows = [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
95
|
+
if (rows.length === 0)
|
|
96
|
+
return `${title}:\n (none)\n`;
|
|
97
|
+
const body = rows.map(([key, n]) => ` ${key}: ${String(n)}`).join("\n");
|
|
98
|
+
return `${title}:\n${body}\n`;
|
|
99
|
+
}
|
|
100
|
+
function renderStats(records) {
|
|
101
|
+
const byStatus = tallyBy(records, (record) => record.status);
|
|
102
|
+
const byScope = tallyBy(records, (record) => scopeKindOf(record.scope));
|
|
103
|
+
return (renderTally("By status", byStatus) +
|
|
104
|
+
renderTally("By scope", byScope) +
|
|
105
|
+
`Total: ${String(records.length)}\n`);
|
|
106
|
+
}
|
|
107
|
+
function renderMaintenanceReport(counts) {
|
|
108
|
+
return [
|
|
109
|
+
"Memory maintenance complete.",
|
|
110
|
+
` promoted: ${String(counts.promoted)}`,
|
|
111
|
+
` archived: ${String(counts.archived)}`,
|
|
112
|
+
` forgotten: ${String(counts.forgotten)}`,
|
|
113
|
+
` superseded: ${String(counts.superseded)}`,
|
|
114
|
+
` edgesCreated: ${String(counts.edgesCreated)}`,
|
|
115
|
+
` clustersInspected: ${String(counts.clustersInspected)}`,
|
|
116
|
+
` reviewItems: ${String(counts.reviewItemsCreated)}`,
|
|
117
|
+
"",
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
function runStats(args, io, env, deps) {
|
|
121
|
+
const vault = resolveVault(args, env, deps);
|
|
122
|
+
try {
|
|
123
|
+
const records = vault.listMemories({ includeExpired: true });
|
|
124
|
+
io.out(renderStats(records));
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
if (deps.vault === undefined)
|
|
129
|
+
vault.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function parseLastAuditEvents(args) {
|
|
133
|
+
const raw = flagValue(args, "--last");
|
|
134
|
+
if (raw === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
const parsed = Number.parseInt(raw, 10);
|
|
137
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
138
|
+
}
|
|
139
|
+
function runDiagnostics(args, io, env, deps) {
|
|
140
|
+
const vault = resolveVault(args, env, deps);
|
|
141
|
+
const evidenceDir = resolveEvidenceDir(flagValue(args, "--evidence-dir"), env);
|
|
142
|
+
const evidenceStore = deps.evidenceStore ?? createNodeEvidenceStore(evidenceDir);
|
|
143
|
+
const redactString = deps.redactString ?? createAuditRedactor({}, env);
|
|
144
|
+
try {
|
|
145
|
+
const records = vault.listMemories({ includeExpired: true });
|
|
146
|
+
const lastNAuditEvents = parseLastAuditEvents(args);
|
|
147
|
+
const diagnostics = exportMemoryDiagnostics({
|
|
148
|
+
vault,
|
|
149
|
+
scopes: uniqueRecordScopes(records),
|
|
150
|
+
evidenceStore,
|
|
151
|
+
redactString,
|
|
152
|
+
evidenceDir,
|
|
153
|
+
...(lastNAuditEvents === undefined ? {} : { lastNAuditEvents }),
|
|
154
|
+
});
|
|
155
|
+
io.out(`${JSON.stringify(diagnostics, null, 2)}\n`);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (deps.vault === undefined)
|
|
160
|
+
vault.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function runMaintain(args, io, env, deps) {
|
|
164
|
+
const vault = resolveVault(args, env, deps);
|
|
165
|
+
const evidenceDir = resolveEvidenceDir(flagValue(args, "--evidence-dir"), env);
|
|
166
|
+
const evidenceStore = deps.evidenceStore ?? createNodeEvidenceStore(evidenceDir);
|
|
167
|
+
try {
|
|
168
|
+
const counts = runMemoryMaintenance(vault, evidenceStore);
|
|
169
|
+
io.out(renderMaintenanceReport(counts));
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (deps.vault === undefined)
|
|
174
|
+
vault.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Resolves the production embedder from the gateway config (--config / $KEIKO_CONFIG_FILE), or
|
|
178
|
+
// null when no config source is available, the config cannot be loaded, or no embedding-capable
|
|
179
|
+
// model is configured. The test seam (deps.embedText) short-circuits this entirely. A GatewayError
|
|
180
|
+
// is treated as "no model" (best-effort backfill never hard-fails on a config problem).
|
|
181
|
+
function resolveEmbedder(args, env, deps) {
|
|
182
|
+
if (deps.embedText !== undefined)
|
|
183
|
+
return deps.embedText;
|
|
184
|
+
const configPath = flagValue(args, "--config") ?? env.KEIKO_CONFIG_FILE;
|
|
185
|
+
if (configPath === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
try {
|
|
188
|
+
return createMemoryEmbedder(loadGatewayConfigFromFile(configPath, env), requestOpenAIEmbedding);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error instanceof GatewayError)
|
|
192
|
+
return null;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function parseLimit(args) {
|
|
197
|
+
const raw = flagValue(args, "--limit");
|
|
198
|
+
if (raw === undefined)
|
|
199
|
+
return DEFAULT_REEMBED_LIMIT;
|
|
200
|
+
const parsed = Number.parseInt(raw, 10);
|
|
201
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_REEMBED_LIMIT;
|
|
202
|
+
}
|
|
203
|
+
async function backfillEmbeddings(vault, embed, limit) {
|
|
204
|
+
const accepted = vault.listMemories({ status: ["accepted"], includeExpired: true, limit });
|
|
205
|
+
const counts = { embedded: 0, skipped: 0, failed: 0 };
|
|
206
|
+
for (const record of accepted) {
|
|
207
|
+
if (vault.getEmbedding(record.id) !== undefined) {
|
|
208
|
+
counts.skipped += 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const input = await embed(record.body);
|
|
212
|
+
if (input === null) {
|
|
213
|
+
counts.failed += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
vault.upsertEmbedding(record.id, input);
|
|
218
|
+
counts.embedded += 1;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
counts.failed += 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return counts;
|
|
225
|
+
}
|
|
226
|
+
function renderReembedReport(counts) {
|
|
227
|
+
return [
|
|
228
|
+
"Memory re-embedding complete.",
|
|
229
|
+
` embedded: ${String(counts.embedded)}`,
|
|
230
|
+
` skipped: ${String(counts.skipped)}`,
|
|
231
|
+
` failed: ${String(counts.failed)}`,
|
|
232
|
+
"",
|
|
233
|
+
].join("\n");
|
|
234
|
+
}
|
|
235
|
+
async function reembed(args, io, env, deps) {
|
|
236
|
+
const embed = resolveEmbedder(args, env, deps);
|
|
237
|
+
if (embed === null) {
|
|
238
|
+
io.out("No embedding model is configured — skipping re-embedding. " +
|
|
239
|
+
"Provide a gateway config with --config PATH or $KEIKO_CONFIG_FILE.\n");
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
const vault = resolveVault(args, env, deps);
|
|
243
|
+
try {
|
|
244
|
+
const counts = await backfillEmbeddings(vault, embed, parseLimit(args));
|
|
245
|
+
io.out(renderReembedReport(counts));
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (deps.vault === undefined)
|
|
250
|
+
vault.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// async wrapper so a sync-or-async failure surfaces as exit 1 (the sync subcommands rely on
|
|
254
|
+
// dispatchSubcommand's try/catch, which cannot catch a rejected Promise).
|
|
255
|
+
async function runReembed(args, io, env, deps) {
|
|
256
|
+
try {
|
|
257
|
+
return await reembed(args, io, env, deps);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
io.err(`keiko memory: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function dispatchSubcommand(sub, args, io, env, deps) {
|
|
265
|
+
try {
|
|
266
|
+
if (sub === "maintain")
|
|
267
|
+
return runMaintain(args, io, env, deps);
|
|
268
|
+
if (sub === "stats")
|
|
269
|
+
return runStats(args, io, env, deps);
|
|
270
|
+
if (sub === "diagnostics")
|
|
271
|
+
return runDiagnostics(args, io, env, deps);
|
|
272
|
+
if (sub === "reembed")
|
|
273
|
+
return runReembed(args, io, env, deps);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
io.err(`keiko memory: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
io.err(`keiko memory: unknown subcommand: ${sub}\n`);
|
|
280
|
+
io.err(USAGE);
|
|
281
|
+
return 2;
|
|
282
|
+
}
|
|
283
|
+
export function runMemoryCli(rest, io, env = {}, deps = {}) {
|
|
284
|
+
const sub = rest[0];
|
|
285
|
+
if (sub === undefined || sub === "--help" || sub === "-h") {
|
|
286
|
+
io.out(USAGE);
|
|
287
|
+
return sub === undefined ? 2 : 0;
|
|
288
|
+
}
|
|
289
|
+
return dispatchSubcommand(sub, rest.slice(1), io, env, deps);
|
|
290
|
+
}
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,SAAS,EAEf,MAAM,mCAAmC,CAAC;AAE3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAkDzC,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,GAAG,MAAM,CAevF"}
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `keiko models` CLI handler. Synchronous by design: `list` reads built-in
|
|
2
|
+
// capability metadata; `validate` reads a config file with readFileSync and runs
|
|
3
|
+
// the hand-rolled validator. Neither path needs a live async Gateway, so the
|
|
4
|
+
// existing `process.exit(runCli(...))` shim stays synchronous. No credential value
|
|
5
|
+
// is ever written to stdout or stderr.
|
|
6
|
+
import { listCapabilities, GatewayError, } from "@oscharko-dev/keiko-model-gateway";
|
|
7
|
+
import { loadGatewayConfigFromFile, resolveConfigPathFromArgs } from "./gateway-config.js";
|
|
8
|
+
const USAGE = `Usage:
|
|
9
|
+
keiko models list List registered model capabilities.
|
|
10
|
+
keiko models validate [--config PATH] Validate gateway configuration.
|
|
11
|
+
`;
|
|
12
|
+
function formatUseCases(useCases) {
|
|
13
|
+
return useCases.map((useCase) => useCase.toLowerCase().replace(/\s+/g, "-")).join(",");
|
|
14
|
+
}
|
|
15
|
+
function listModels(io) {
|
|
16
|
+
io.out("ID\tKIND\tCOST\tLATENCY\tTOOLS\tSTRUCT\tUSE-CASES\n");
|
|
17
|
+
for (const cap of listCapabilities()) {
|
|
18
|
+
const tools = cap.toolCalling ? "yes" : "no";
|
|
19
|
+
const struct = cap.structuredOutput ? "yes" : "no";
|
|
20
|
+
io.out(`${cap.id}\t${cap.kind}\t${cap.costClass}\t${cap.latencyClass}\t${tools}\t${struct}\t${formatUseCases(cap.preferredUseCases)}\n`);
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
function validateConfig(args, io, env) {
|
|
25
|
+
const resolution = resolveConfigPathFromArgs(args, env);
|
|
26
|
+
if (resolution.kind === "missing-value") {
|
|
27
|
+
io.err("Error: --config requires a path argument.\n");
|
|
28
|
+
return 2;
|
|
29
|
+
}
|
|
30
|
+
if (resolution.kind === "not-configured") {
|
|
31
|
+
io.err("Error [GATEWAY_CONFIG_INVALID]: no config source; pass --config PATH or set KEIKO_CONFIG_FILE.\n");
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const config = loadGatewayConfigFromFile(resolution.path, env);
|
|
36
|
+
io.out(`Gateway config valid. ${String(config.providers.length)} model providers configured.\n`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof GatewayError) {
|
|
41
|
+
io.err(`Error [${error.code}]: ${error.message}\n`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function runModelsCli(args, io, env) {
|
|
48
|
+
const sub = args[0];
|
|
49
|
+
if (sub === "list") {
|
|
50
|
+
return listModels(io);
|
|
51
|
+
}
|
|
52
|
+
if (sub === "validate") {
|
|
53
|
+
return validateConfig(args.slice(1), io, env);
|
|
54
|
+
}
|
|
55
|
+
if (sub === undefined) {
|
|
56
|
+
io.err(USAGE);
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
io.err(`keiko models: unknown sub-command: ${sub}\n`);
|
|
60
|
+
io.err(USAGE);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type EnvSource, type GatewayConfig } from "@oscharko-dev/keiko-model-gateway";
|
|
2
|
+
import { runPromptEnhancement } from "@oscharko-dev/keiko-workflows";
|
|
3
|
+
import type { CliIo } from "./runner.js";
|
|
4
|
+
export interface PromptEnhancerCliDeps {
|
|
5
|
+
readonly run?: typeof runPromptEnhancement;
|
|
6
|
+
readonly loadConfig?: (path: string, env: EnvSource) => GatewayConfig;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `keiko prompt-enhancer` entrypoint. Returns 0 on success, 1 on a runtime/config error, 2 on usage
|
|
10
|
+
* error. Pure apart from optional config-file reads; never dispatches a model.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runPromptEnhancerCli(args: readonly string[], io: CliIo, env: EnvSource, deps?: PromptEnhancerCliDeps): number;
|
|
13
|
+
//# sourceMappingURL=prompt-enhancer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-enhancer.d.ts","sourceRoot":"","sources":["../src/prompt-enhancer.ts"],"names":[],"mappings":"AAQA,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAIL,oBAAoB,EACrB,MAAM,+BAA+B,CAAC;AAYvC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIzC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,oBAAoB,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,KAAK,aAAa,CAAC;CACvE;AA4PD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,GAAG,EAAE,SAAS,EACd,IAAI,GAAE,qBAA0B,GAC/B,MAAM,CAyCR"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// `keiko prompt-enhancer` — the developer-facing surface for the Prompt Enhancer (Epic #1307, Issue
|
|
2
|
+
// #1314; ADR-0044 §1 "CLI command"). It drives the EXACT SAME deterministic workflow as the BFF
|
|
3
|
+
// route (`runPromptEnhancement` from keiko-workflows), so the product and developer surfaces produce
|
|
4
|
+
// byte-identical enhancements (AC1). The command never dispatches a model: enhancement is deterministic
|
|
5
|
+
// and provider-neutral; an optional `--model` only resolves dispatch readiness through the Model
|
|
6
|
+
// Gateway (AC3). All printed output is redacted (AC4); `--evidence` emits a redact-by-construction
|
|
7
|
+
// audit manifest reusing the #1313 keiko-evidence promptEnhancement store.
|
|
8
|
+
import { ConfigInvalidError, GatewayError, loadConfigFromFile, redact, } from "@oscharko-dev/keiko-model-gateway";
|
|
9
|
+
import { PromptEnhancementInputError, buildPromptEnhancementRecordInput, promptEnhancementGatewayRoutingConfig, runPromptEnhancement, } from "@oscharko-dev/keiko-workflows";
|
|
10
|
+
import { validatePromptEnhancementWireRequest, } from "@oscharko-dev/keiko-contracts";
|
|
11
|
+
import { buildPromptEnhancementEvidenceManifest, createAuditRedactor, deepRedactStrings, } from "@oscharko-dev/keiko-evidence";
|
|
12
|
+
import { keikoApiKeySecretValues } from "@oscharko-dev/keiko-security";
|
|
13
|
+
const USAGE = `Usage: keiko prompt-enhancer --input "<raw prompt>" [OPTIONS]
|
|
14
|
+
|
|
15
|
+
Generate a governed, reviewable Enhanced Prompt from a raw draft. Deterministic and provider-neutral;
|
|
16
|
+
the result is data for review, never executed.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--input TEXT The raw prompt draft to enhance (required).
|
|
20
|
+
--profile ID Preferred profile: fast | precise | research | creative | technical |
|
|
21
|
+
safety-critical | agentic. Safety criticality may still escalate it.
|
|
22
|
+
--strategy MODE How to handle missing information: clarify (default) | assume.
|
|
23
|
+
--candidates N Number of candidate variants to score (1-7).
|
|
24
|
+
--model ID Resolve dispatch readiness for this model through the Model Gateway. Requires a
|
|
25
|
+
gateway config (--config PATH or KEIKO_CONFIG_FILE).
|
|
26
|
+
--config PATH Gateway config file (also honored via KEIKO_CONFIG_FILE).
|
|
27
|
+
--evidence Also print a redacted, content-light audit evidence manifest.
|
|
28
|
+
--json Print the full enhancement result as redacted JSON.
|
|
29
|
+
`;
|
|
30
|
+
const VALUE_FLAGS = [
|
|
31
|
+
"--input",
|
|
32
|
+
"--profile",
|
|
33
|
+
"--strategy",
|
|
34
|
+
"--candidates",
|
|
35
|
+
"--model",
|
|
36
|
+
"--config",
|
|
37
|
+
];
|
|
38
|
+
const BOOLEAN_FLAG_KEYS = {
|
|
39
|
+
"--json": "json",
|
|
40
|
+
"--evidence": "evidence",
|
|
41
|
+
"--help": "help",
|
|
42
|
+
"-h": "help",
|
|
43
|
+
};
|
|
44
|
+
const isValueFlag = (value) => VALUE_FLAGS.includes(value);
|
|
45
|
+
function parseArgs(args) {
|
|
46
|
+
const values = {};
|
|
47
|
+
const bools = { json: false, evidence: false, help: false };
|
|
48
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
49
|
+
const arg = args[i];
|
|
50
|
+
if (arg === undefined)
|
|
51
|
+
continue;
|
|
52
|
+
if (isValueFlag(arg)) {
|
|
53
|
+
const value = args[i + 1];
|
|
54
|
+
if (value === undefined || (arg !== "--input" && value.startsWith("--"))) {
|
|
55
|
+
return { ok: false, error: `missing value for ${arg}` };
|
|
56
|
+
}
|
|
57
|
+
values[arg] = value;
|
|
58
|
+
i += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const boolKey = BOOLEAN_FLAG_KEYS[arg];
|
|
62
|
+
if (boolKey !== undefined) {
|
|
63
|
+
bools[boolKey] = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: arg.startsWith("--") ? `unknown flag ${arg}` : `unexpected argument ${arg}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, flags: { values, ...bools } };
|
|
72
|
+
}
|
|
73
|
+
// Build the wire request from flags and validate it with the same validator the BFF route uses, so the
|
|
74
|
+
// CLI and API reject identical malformed inputs. Numeric/enumerated flags are coerced before validation.
|
|
75
|
+
function buildWireRequest(flags) {
|
|
76
|
+
const candidate = { text: flags.values["--input"] };
|
|
77
|
+
if (flags.values["--profile"] !== undefined)
|
|
78
|
+
candidate.profilePreference = flags.values["--profile"];
|
|
79
|
+
if (flags.values["--strategy"] !== undefined) {
|
|
80
|
+
candidate.missingInformationStrategy = flags.values["--strategy"];
|
|
81
|
+
}
|
|
82
|
+
if (flags.values["--model"] !== undefined)
|
|
83
|
+
candidate.modelId = flags.values["--model"];
|
|
84
|
+
if (flags.values["--candidates"] !== undefined) {
|
|
85
|
+
const parsed = Number(flags.values["--candidates"]);
|
|
86
|
+
candidate.candidateCount = Number.isNaN(parsed) ? flags.values["--candidates"] : parsed;
|
|
87
|
+
}
|
|
88
|
+
const validation = validatePromptEnhancementWireRequest(candidate);
|
|
89
|
+
if (!validation.ok) {
|
|
90
|
+
return { ok: false, errors: validation.errors };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, request: validation.value };
|
|
93
|
+
}
|
|
94
|
+
function resolveGatewayConfig(flags, env, io, deps) {
|
|
95
|
+
// A model is only checked for downstream-dispatch readiness when --model is given. Without it,
|
|
96
|
+
// enhancement is fully deterministic and needs no config.
|
|
97
|
+
if (flags.values["--model"] === undefined) {
|
|
98
|
+
return { ok: true, config: undefined };
|
|
99
|
+
}
|
|
100
|
+
const path = flags.values["--config"] ?? env.KEIKO_CONFIG_FILE;
|
|
101
|
+
if (path === undefined) {
|
|
102
|
+
io.err("Error: --model requires a gateway config to resolve readiness; pass --config PATH or set KEIKO_CONFIG_FILE.\n");
|
|
103
|
+
return { ok: false, code: 1 };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const load = deps.loadConfig ?? loadConfigFromFile;
|
|
107
|
+
return { ok: true, config: load(path, env) };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof GatewayError || error instanceof ConfigInvalidError) {
|
|
111
|
+
io.err(`Error: model gateway configuration problem — ${redact(error.message)}\n` +
|
|
112
|
+
"Provide a valid gateway config with --config PATH or KEIKO_CONFIG_FILE.\n");
|
|
113
|
+
return { ok: false, code: 1 };
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function buildEvidenceManifest(rawInput, result, env, config) {
|
|
119
|
+
const record = buildPromptEnhancementRecordInput({
|
|
120
|
+
rawInput,
|
|
121
|
+
result,
|
|
122
|
+
recordedAt: new Date().toISOString(),
|
|
123
|
+
});
|
|
124
|
+
// The builder fingerprints + redacts by construction, so the returned manifest carries no raw secret.
|
|
125
|
+
// Topology values are literal-redacted; configured credentials only trigger full string redaction so
|
|
126
|
+
// the raw credential values never approach the evidence hashing boundary.
|
|
127
|
+
return buildPromptEnhancementEvidenceManifest(record, {
|
|
128
|
+
additionalSecrets: promptEnhancerTopologyRedactionSecrets(config),
|
|
129
|
+
redactAllStrings: promptEnhancerHasOpaqueRedactionSecret(env, config),
|
|
130
|
+
}).manifest;
|
|
131
|
+
}
|
|
132
|
+
function egressSecretValues(egress) {
|
|
133
|
+
if (egress === undefined)
|
|
134
|
+
return [];
|
|
135
|
+
return [egress.httpProxy, egress.httpsProxy, egress.caBundlePath].filter((value) => value !== undefined && value.length > 0);
|
|
136
|
+
}
|
|
137
|
+
function configTopologyValues(config) {
|
|
138
|
+
if (config === undefined)
|
|
139
|
+
return [];
|
|
140
|
+
const out = [...egressSecretValues(config.egress)];
|
|
141
|
+
for (const provider of config.providers) {
|
|
142
|
+
out.push(provider.baseUrl, ...egressSecretValues(provider.egress));
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function configOpaqueSecretValues(config) {
|
|
147
|
+
if (config === undefined)
|
|
148
|
+
return [];
|
|
149
|
+
const out = [];
|
|
150
|
+
if (config.figma?.accessToken !== undefined) {
|
|
151
|
+
out.push(config.figma.accessToken);
|
|
152
|
+
}
|
|
153
|
+
for (const provider of config.providers) {
|
|
154
|
+
out.push(provider.apiKey);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function figmaEnvSecretValues(env) {
|
|
159
|
+
const token = env.FIGMA_ACCESS_TOKEN;
|
|
160
|
+
return token !== undefined && token.length > 0 ? [token] : [];
|
|
161
|
+
}
|
|
162
|
+
function promptEnhancerRedactionSecrets(env, config) {
|
|
163
|
+
return Array.from(new Set([
|
|
164
|
+
...keikoApiKeySecretValues(env),
|
|
165
|
+
...figmaEnvSecretValues(env),
|
|
166
|
+
...configTopologyValues(config),
|
|
167
|
+
...configOpaqueSecretValues(config),
|
|
168
|
+
]));
|
|
169
|
+
}
|
|
170
|
+
function promptEnhancerTopologyRedactionSecrets(config) {
|
|
171
|
+
return Array.from(new Set(configTopologyValues(config)));
|
|
172
|
+
}
|
|
173
|
+
function promptEnhancerHasOpaqueRedactionSecret(env, config) {
|
|
174
|
+
return (keikoApiKeySecretValues(env).length > 0 ||
|
|
175
|
+
figmaEnvSecretValues(env).length > 0 ||
|
|
176
|
+
configOpaqueSecretValues(config).length > 0);
|
|
177
|
+
}
|
|
178
|
+
function renderHuman(result, io) {
|
|
179
|
+
const winner = result.candidates.scorecards.find((scorecard) => scorecard.candidateId === result.candidates.winnerCandidateId);
|
|
180
|
+
io.out(`Enhanced prompt (profile: ${winner?.profile ?? "n/a"}, id: ${result.promptId})\n`);
|
|
181
|
+
io.out(`Task class: ${result.analysis.taskClass} · domain: ${result.analysis.domain} · criticality: ${result.analysis.criticality}\n`);
|
|
182
|
+
io.out(`Model routing: ${result.modelRouting.availability} (${result.modelRouting.reason})` +
|
|
183
|
+
(result.modelRouting.resolvedModelId === undefined
|
|
184
|
+
? "\n"
|
|
185
|
+
: ` → ${result.modelRouting.resolvedModelId}\n`));
|
|
186
|
+
io.out(`Safety: ${result.safety.decision} · verification: ${result.safety.verificationStatus}` +
|
|
187
|
+
` · human review: ${result.safety.requiresHumanReview ? "required" : "not required"}\n`);
|
|
188
|
+
if (result.safety.findings.length > 0) {
|
|
189
|
+
io.out(`Safety findings: ${result.safety.findings.map((f) => f.code).join(", ")}\n`);
|
|
190
|
+
}
|
|
191
|
+
io.out("\nCandidate scorecards (winner first):\n");
|
|
192
|
+
for (const scorecard of result.candidates.scorecards) {
|
|
193
|
+
const marker = scorecard.candidateId === result.candidates.winnerCandidateId ? "*" : " ";
|
|
194
|
+
io.out(` ${marker} ${scorecard.profile.padEnd(16)} score ${scorecard.aggregateScore.toFixed(3)} · ~${String(scorecard.estimatedTokens)} tokens\n`);
|
|
195
|
+
}
|
|
196
|
+
io.out("\n--- Enhanced prompt (review before any downstream use) ---\n");
|
|
197
|
+
io.out(`${result.renderedPrompt}\n`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* `keiko prompt-enhancer` entrypoint. Returns 0 on success, 1 on a runtime/config error, 2 on usage
|
|
201
|
+
* error. Pure apart from optional config-file reads; never dispatches a model.
|
|
202
|
+
*/
|
|
203
|
+
export function runPromptEnhancerCli(args, io, env, deps = {}) {
|
|
204
|
+
const parsed = parseArgs(args);
|
|
205
|
+
if (!parsed.ok) {
|
|
206
|
+
io.err(`Error: ${parsed.error}\n\n${USAGE}`);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
if (parsed.flags.help) {
|
|
210
|
+
io.out(USAGE);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
const rawInput = parsed.flags.values["--input"];
|
|
214
|
+
if (rawInput === undefined) {
|
|
215
|
+
io.err(`Error: --input is required.\n\n${USAGE}`);
|
|
216
|
+
return 2;
|
|
217
|
+
}
|
|
218
|
+
const wire = buildWireRequest(parsed.flags);
|
|
219
|
+
if (!wire.ok) {
|
|
220
|
+
io.err(`Error: invalid request:\n - ${wire.errors.join("\n - ")}\n`);
|
|
221
|
+
return 2;
|
|
222
|
+
}
|
|
223
|
+
const config = resolveGatewayConfig(parsed.flags, env, io, deps);
|
|
224
|
+
if (!config.ok) {
|
|
225
|
+
return config.code;
|
|
226
|
+
}
|
|
227
|
+
const run = deps.run ?? runPromptEnhancement;
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = run(wire.request, {
|
|
231
|
+
gatewayRoutingConfig: promptEnhancementGatewayRoutingConfig(config.config),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof PromptEnhancementInputError) {
|
|
236
|
+
io.err(`Error: enhancement rejected the request:\n - ${error.errors.join("\n - ")}\n`);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
emitResult(parsed.flags, result, rawInput, env, config.config, io);
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
// Redact and print the enhancement result. AC4: deep-redact every string leaf before printing so a
|
|
245
|
+
// secret-shaped substring a user pasted into their own draft is scrubbed from the echoed input /
|
|
246
|
+
// rendered prompt. The evidence manifest is built from the ORIGINAL result (the evidence store redacts
|
|
247
|
+
// by construction) and is always integrity-hashed and content-light.
|
|
248
|
+
function emitResult(flags, result, rawInput, env, config, io) {
|
|
249
|
+
const redactFn = createAuditRedactor({ additionalSecrets: promptEnhancerRedactionSecrets(env, config) }, env);
|
|
250
|
+
const redactedResult = deepRedactStrings(result, redactFn);
|
|
251
|
+
if (flags.json) {
|
|
252
|
+
io.out(`${JSON.stringify(redactedResult, null, 2)}\n`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
renderHuman(redactedResult, io);
|
|
256
|
+
}
|
|
257
|
+
if (flags.evidence) {
|
|
258
|
+
const manifest = buildEvidenceManifest(rawInput, result, env, config);
|
|
259
|
+
io.out(`\n--- Redacted evidence manifest ---\n${JSON.stringify(manifest, null, 2)}\n`);
|
|
260
|
+
}
|
|
261
|
+
}
|
package/dist/repair.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EnvSource } from "@oscharko-dev/keiko-model-gateway";
|
|
2
|
+
import type { CliIo } from "./runner.js";
|
|
3
|
+
export interface RepairCliDeps {
|
|
4
|
+
readonly cwd?: string | undefined;
|
|
5
|
+
readonly argv?: readonly string[] | undefined;
|
|
6
|
+
readonly homedir?: () => string;
|
|
7
|
+
readonly isProcessAlive?: (pid: number) => boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function runRepairCli(args: readonly string[], io: CliIo, env: EnvSource, deps?: RepairCliDeps): number;
|
|
10
|
+
//# sourceMappingURL=repair.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repair.d.ts","sourceRoot":"","sources":["../src/repair.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA8EzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACpD;AA0XD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,GAAG,EAAE,SAAS,EACd,IAAI,GAAE,aAAkB,GACvB,MAAM,CAmCR"}
|