karajan-code 3.12.1 → 3.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -5
  3. package/packages/hu-board/package.json +1 -1
  4. package/packages/hu-board/src/plan-mutations.js +4 -4
  5. package/packages/hu-board/src/routes/api.js +1 -1
  6. package/packages/hu-board/src/routes/rag.js +1 -1
  7. package/packages/hu-board/src/routes/wiki.js +1 -1
  8. package/packages/hu-board/src/run-tracker.js +1 -1
  9. package/packages/hu-board/src/server.js +2 -2
  10. package/packages/hu-board/src/sync.js +1 -1
  11. package/scripts/build-sea.mjs +23 -1
  12. package/scripts/esbuild-sea.config.mjs +22 -1
  13. package/scripts/verify-pack.mjs +38 -6
  14. package/src/audit/osv-findings.js +1 -1
  15. package/src/brain/standby-scheduler.js +2 -2
  16. package/src/brain/standby-store.js +2 -2
  17. package/src/commands/init.js +4 -3
  18. package/src/commands/onboard.js +2 -1
  19. package/src/git/hu-snapshot.js +2 -2
  20. package/src/orchestrator/config-init.js +2 -1
  21. package/src/plan/plan-hu-ops.js +2 -2
  22. package/src/plan/plan-id.js +2 -2
  23. package/src/plan/plan-validation.js +2 -2
  24. package/src/prompts/prompt-resolver.js +2 -9
  25. package/src/rag/vec-store.js +2 -2
  26. package/src/utils/atomic-write.js +3 -3
  27. package/src/utils/install-hints.js +5 -1
  28. package/src/utils/paths.js +2 -2
  29. package/src/utils/port-check.js +3 -3
  30. package/src/utils/process.js +2 -2
  31. package/src/utils/run-registry.js +2 -2
  32. package/src/utils/shared-paths.js +3 -3
  33. package/src/utils/templates-root.js +61 -0
  34. package/src/utils/update-check.js +78 -15
  35. package/node_modules/@karajan/core/README.md +0 -42
  36. package/node_modules/@karajan/core/package.json +0 -49
  37. package/node_modules/@karajan/core/src/atomic-write.js +0 -43
  38. package/node_modules/@karajan/core/src/cost/index.js +0 -102
  39. package/node_modules/@karajan/core/src/hu-snapshot.js +0 -105
  40. package/node_modules/@karajan/core/src/index.js +0 -18
  41. package/node_modules/@karajan/core/src/paths.js +0 -123
  42. package/node_modules/@karajan/core/src/plan-hu-ops.js +0 -338
  43. package/node_modules/@karajan/core/src/plan-id.js +0 -45
  44. package/node_modules/@karajan/core/src/plan-validation.js +0 -82
  45. package/node_modules/@karajan/core/src/port-check.js +0 -45
  46. package/node_modules/@karajan/core/src/pricing/index.js +0 -64
  47. package/node_modules/@karajan/core/src/pricing/model-pricing.json +0 -28
  48. package/node_modules/@karajan/core/src/process.js +0 -188
  49. package/node_modules/@karajan/core/src/run-registry.js +0 -115
  50. package/node_modules/@karajan/core/src/shared-paths.js +0 -41
  51. package/node_modules/@karajan/core/src/standby-scheduler.js +0 -92
  52. package/node_modules/@karajan/core/src/standby-store.js +0 -168
  53. package/node_modules/@karajan/core/src/vec-store.js +0 -253
  54. package/node_modules/@karajan/core/tests/atomic-write.test.js +0 -30
  55. package/node_modules/@karajan/core/tests/cost/index.test.js +0 -89
  56. package/node_modules/@karajan/core/tests/hu-snapshot.test.js +0 -39
  57. package/node_modules/@karajan/core/tests/paths.test.js +0 -51
  58. package/node_modules/@karajan/core/tests/plan-hu-ops.test.js +0 -68
  59. package/node_modules/@karajan/core/tests/plan-id.test.js +0 -21
  60. package/node_modules/@karajan/core/tests/plan-validation.test.js +0 -39
  61. package/node_modules/@karajan/core/tests/port-check.test.js +0 -47
  62. package/node_modules/@karajan/core/tests/pricing/index.test.js +0 -95
  63. package/node_modules/@karajan/core/tests/process.test.js +0 -22
  64. package/node_modules/@karajan/core/tests/run-registry.test.js +0 -71
  65. package/node_modules/@karajan/core/tests/shared-paths.test.js +0 -30
  66. package/node_modules/@karajan/core/tests/standby.test.js +0 -66
  67. package/node_modules/@karajan/core/tests/vec-store.test.js +0 -98
  68. package/node_modules/@karajan/core/vitest.config.js +0 -12
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { isSea, getAsset } from "node:sea";
5
+ import { getKarajanHome } from "./paths.js";
6
+
7
+ // npm install / source tree: the real templates/ directory next to src/.
8
+ const SOURCE_ROOT = path.resolve(
9
+ path.dirname(fileURLToPath(import.meta.url)),
10
+ "..",
11
+ "..",
12
+ "templates",
13
+ );
14
+
15
+ const INDEX_ASSET = "templates-index.json";
16
+
17
+ /**
18
+ * Root directory of the built-in templates (KJC-BUG-0104).
19
+ *
20
+ * The SEA binary ships only the JS bundle — templates/ never travels as
21
+ * files, and `import.meta.dirname`-relative resolution lands on the
22
+ * binary's own directory (e.g. `$HOME/templates/skills` → ENOENT). In SEA
23
+ * builds the templates travel as SEA assets (see scripts/build-sea.mjs);
24
+ * they are extracted once per version into ~/.karajan/embedded-templates/
25
+ * and served from there, so every fs-based consumer keeps working.
26
+ */
27
+ export function getTemplatesRoot() {
28
+ if (!isSeaRuntime()) return SOURCE_ROOT;
29
+ const index = JSON.parse(getAsset(INDEX_ASSET, "utf8"));
30
+ const destRoot = path.join(getKarajanHome(), "embedded-templates", index.version);
31
+ return extractEmbeddedTemplates({
32
+ destRoot,
33
+ files: index.files,
34
+ readAsset: (rel) => getAsset(`templates/${rel}`, "utf8"),
35
+ });
36
+ }
37
+
38
+ function isSeaRuntime() {
39
+ try {
40
+ return isSea();
41
+ } catch {
42
+ // isSea() unexpectedly threw ⇒ treat as npm / source tree, never block.
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Extract the embedded template files into destRoot (idempotent: a
49
+ * .complete marker skips re-extraction). Exported for unit testing.
50
+ */
51
+ export function extractEmbeddedTemplates({ destRoot, files, readAsset }) {
52
+ const marker = path.join(destRoot, ".complete");
53
+ if (fs.existsSync(marker)) return destRoot;
54
+ for (const rel of files) {
55
+ const dest = path.join(destRoot, rel);
56
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
57
+ fs.writeFileSync(dest, readAsset(rel));
58
+ }
59
+ fs.writeFileSync(marker, "");
60
+ return destRoot;
61
+ }
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { isSea } from "node:sea";
4
5
  import { getKarajanHome } from "./paths.js";
@@ -104,30 +105,46 @@ export async function printUpdateNotice(currentVersion) {
104
105
  }
105
106
 
106
107
  /**
107
- * Run the npm-channel self-update (`kj update`). Captures npm's output instead
108
- * of streaming it: a successful update shows only the progress + result line,
109
- * so npm's deprecation / allow-scripts / funding noise — build plumbing, not
110
- * actionable for whoever runs the command — never reaches the user. On failure
111
- * the captured stdout/stderr IS surfaced, so real errors (native build,
112
- * permissions) stay diagnosable — never a silent failure.
108
+ * Run the self-update (`kj update`), channel-aware (KJC-BUG-0106).
109
+ *
110
+ * npm channel: `npm install -g` with CAPTURED output — success shows only
111
+ * progress + result; npm's deprecation / allow-scripts / funding noise never
112
+ * reaches the user. On failure the captured output IS surfaced.
113
+ *
114
+ * sea channel (standalone binary): npm-installing would update a copy the
115
+ * PATH never resolves — the running binary stays old while `kj update`
116
+ * reports success. Instead the binary installer is re-run (downloaded to a
117
+ * temp file first, never piped from the network straight into a shell).
118
+ *
119
+ * Both channels end with a PATH probe: `kj --version` must report the new
120
+ * version, otherwise a stale copy shadows the update and the command fails
121
+ * LOUDLY naming the likely cause — never a silent "updated" that isn't.
113
122
  *
114
123
  * @param {Object} opts
115
124
  * @param {string} opts.currentVersion - version of the running kj
116
125
  * @param {(cmd: string, args: string[]) => Promise<{stdout: string, stderr: string}>} [opts.exec] - injectable runner (defaults to execa)
117
126
  * @param {Console} [opts.logger]
118
- * @returns {Promise<{ ok: boolean, alreadyLatest?: boolean, latest?: string }>}
127
+ * @param {"npm"|"sea"} [opts.channel] - injectable channel (defaults to detectInstallChannel())
128
+ * @param {typeof fetch} [opts.fetchFn] - injectable fetch (registry lookup + installer download)
129
+ * @returns {Promise<{ ok: boolean, alreadyLatest?: boolean, latest?: string, manual?: boolean }>}
119
130
  */
120
- export async function performSelfUpdate({ currentVersion, exec, logger = console } = {}) {
131
+ export async function performSelfUpdate({ currentVersion, exec, logger = console, channel, fetchFn = fetch } = {}) {
121
132
  const run = exec || (async (cmd, args) => (await import("execa")).execa(cmd, args));
133
+ const ch = channel || detectInstallChannel();
122
134
  logger.log(`Current version: ${currentVersion}`);
123
135
  logger.log("Checking for updates...");
124
136
 
125
137
  let latest;
126
138
  try {
127
- const { stdout } = await run("npm", ["view", PACKAGE_NAME, "version"]);
128
- latest = stdout.trim();
139
+ // Registry over HTTP, not `npm view` — standalone-binary machines may
140
+ // not have npm at all.
141
+ const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
142
+ headers: { Accept: "application/json" },
143
+ });
144
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
145
+ latest = (await res.json()).version;
129
146
  } catch (err) {
130
- logger.error(`Update failed: ${err.shortMessage || err.message}`);
147
+ logger.error(`Update failed: could not check the registry (${err.message})`);
131
148
  return { ok: false };
132
149
  }
133
150
 
@@ -136,18 +153,64 @@ export async function performSelfUpdate({ currentVersion, exec, logger = console
136
153
  return { ok: true, alreadyLatest: true, latest };
137
154
  }
138
155
 
156
+ if (ch === "sea" && process.platform === "win32") {
157
+ // Executing a downloaded .ps1 from inside the binary is not supported
158
+ // yet — hand the user the exact command instead of half-updating.
159
+ logger.log(`Update available: ${currentVersion} → ${latest} (standalone binary).`);
160
+ logger.log(`${updateInstruction({ channel: "sea" })}`);
161
+ return { ok: true, latest, manual: true };
162
+ }
163
+
139
164
  logger.log(`Updating ${currentVersion} → ${latest}... (this can take a few minutes)`);
140
165
  try {
141
- // No stdio:inherit — capture and drop npm's warnings on the success path.
142
- await run("npm", ["install", "-g", `${PACKAGE_NAME}@latest`]);
143
- logger.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
144
- return { ok: true, latest };
166
+ if (ch === "sea") {
167
+ // Re-run the binary installer: download to a temp file, then execute —
168
+ // never pipe the network straight into a shell.
169
+ const res = await fetchFn(INSTALL_SH_URL);
170
+ if (!res.ok) throw new Error(`installer download failed (${res.status})`);
171
+ const tmpScript = path.join(os.tmpdir(), `kj-install-${process.pid}.sh`);
172
+ await fs.writeFile(tmpScript, await res.text(), { mode: 0o700 });
173
+ try {
174
+ await run("sh", [tmpScript]);
175
+ } finally {
176
+ await fs.rm(tmpScript, { force: true });
177
+ }
178
+ } else {
179
+ // No stdio:inherit — capture and drop npm's warnings on the success path.
180
+ await run("npm", ["install", "-g", `${PACKAGE_NAME}@latest`]);
181
+ }
145
182
  } catch (err) {
146
183
  if (err.stdout) logger.error(err.stdout);
147
184
  if (err.stderr) logger.error(err.stderr);
148
185
  logger.error(`Update failed: ${err.shortMessage || err.message}`);
149
186
  return { ok: false };
150
187
  }
188
+
189
+ // The install succeeded — but is the kj the USER runs actually the new one?
190
+ const onPath = await verifyKjOnPath({ run, latest, logger });
191
+ if (!onPath) return { ok: false };
192
+ logger.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
193
+ return { ok: true, latest };
194
+ }
195
+
196
+ /**
197
+ * Probe `kj --version` through the PATH and confirm it reports `latest`.
198
+ * A mismatch means a stale copy shadows the freshly installed one (e.g. a
199
+ * standalone binary in ~/.local/bin in front of the npm global prefix).
200
+ */
201
+ async function verifyKjOnPath({ run, latest, logger }) {
202
+ let reported;
203
+ try {
204
+ const { stdout } = await run("kj", ["--version"]);
205
+ reported = String(stdout).trim();
206
+ } catch (err) {
207
+ logger.error(`Update installed ${latest}, but running \`kj --version\` failed: ${err.shortMessage || err.message}`);
208
+ return false;
209
+ }
210
+ if (reported.includes(latest)) return true;
211
+ logger.error(`Update installed ${latest}, but the \`kj\` on your PATH still reports ${reported}.`);
212
+ logger.error("A stale copy is shadowing the update. Check `which -a kj` and remove the old one, or update through the channel that owns the first entry.");
213
+ return false;
151
214
  }
152
215
 
153
216
  /** Simple semver compare: returns >0 if a > b, <0 if a < b, 0 if equal */
@@ -1,42 +0,0 @@
1
- # @karajan/core
2
-
3
- Shared modules for the Karajan Code CLI and `@karajan/hu-board`. Private
4
- workspace package — not published to npm.
5
-
6
- ## Scope
7
-
8
- `@karajan/core` is the extraction destination for code that both the CLI
9
- package (`karajan-code`) and the HU Board package (`@karajan/hu-board`)
10
- need. Today it ships the atomic JSON writer; subsequent commits of
11
- KJC-TSK-0511 will land the shared paths, port-check, run-registry,
12
- process runner, plan validation, vector store, and plan/HU ops modules
13
- here.
14
-
15
- ## Exports
16
-
17
- | Subpath | Module |
18
- | --- | --- |
19
- | `@karajan/core/atomic-write` | `writeJsonAtomic`, `writeJsonAtomicSync` |
20
- | `@karajan/core/shared-paths` | `SHARED_DIR_NAME`, `getSharedRoot`, `getSharedPlansDir`, `getSharedHuStoriesDir`, `isSharedPath` |
21
- | `@karajan/core/port-check` | `isPortAvailable`, `findAvailablePort` |
22
- | `@karajan/core/paths` | `resolveHome`, `getKarajanHome`, `getSessionRoot`, `getSonarComposePath`, `getOllamaComposePath`, `getWebperfDir`, `getRunsDir`, `getPromptsDir`, `__resetKjHomeWarningForTests` |
23
- | `@karajan/core/run-registry` | `runsDir`, `registerRun`, `unregisterRun`, `listActiveRuns` |
24
- | `@karajan/core/vec-store` | `dbPath`, `openVecStore`, `insertChunk`, `searchSimilar`, `searchBM25`, `projectSlug`, `deleteChunksBySource`, `findChunkByHash`, `getEmbeddingsByIds`, `getLastIndexedCommit`, `setLastIndexedCommit`, `countChunks` |
25
- | `@karajan/core/plan-id` | `generatePlanId`, `generateHuId`, `normaliseAlias` |
26
- | `@karajan/core/plan-hu-ops` | `addHu`, `removeHu`, `updateHu`, `updateHuStatus`, `setHuOutcome`, `setPlanOutcome`, `autoCertifyPendingHus`, `assertPlanRunnable`, `computePlanOutcome`, `certifyAllHus`, `reorderHus` |
27
- | `@karajan/core/plan-validation` | `validateBlockedByChange` |
28
- | `@karajan/core/process` | `runCommand` (execa wrapper with output streaming, silence/total timeouts, ENOENT enrichment) |
29
- | `@karajan/core/hu-snapshot` | `snapshotRefForHu`, `createHuSnapshot`, `hasHuSnapshot`, `restoreHuSnapshot`, `removeHuSnapshot` |
30
- | `@karajan/core/standby-store` | `persistStandby`, `loadStandby`, `listPendingStandby`, `markStandbyDone`, `acquireStandbyLock`, `buildStandbyState`, `standbyDir`, `standbyDoneDir` |
31
- | `@karajan/core/standby-scheduler` | `scheduleResume`, `cancelScheduled`, `reconcileAll`, `acquireStandbyLock`, `markStandbyDone`, `_resetScheduledForTests` |
32
-
33
- The root export (`@karajan/core`) re-exports everything via the barrel
34
- in `src/index.js`, but prefer the subpath form so the bundler can
35
- tree-shake unused modules.
36
-
37
- ## Status
38
-
39
- KJC-TSK-0511 PR8 — `standby-store` + `standby-scheduler` extracted.
40
- With this PR `@karajan/hu-board` no longer reaches into `../../../src`
41
- from any static **or dynamic** import. PR9 will re-add
42
- `packages/hu-board` to the root `workspaces` array.
@@ -1,49 +0,0 @@
1
- {
2
- "name": "@karajan/core",
3
- "version": "0.1.0",
4
- "description": "Shared modules for Karajan Code CLI and @karajan/hu-board (atomic-write, paths, RAG store, plan ops, process runner).",
5
- "type": "module",
6
- "private": true,
7
- "engines": {
8
- "node": ">=22.22.1"
9
- },
10
- "exports": {
11
- "./atomic-write": "./src/atomic-write.js",
12
- "./shared-paths": "./src/shared-paths.js",
13
- "./port-check": "./src/port-check.js",
14
- "./paths": "./src/paths.js",
15
- "./run-registry": "./src/run-registry.js",
16
- "./vec-store": "./src/vec-store.js",
17
- "./plan-id": "./src/plan-id.js",
18
- "./plan-hu-ops": "./src/plan-hu-ops.js",
19
- "./plan-validation": "./src/plan-validation.js",
20
- "./process": "./src/process.js",
21
- "./hu-snapshot": "./src/hu-snapshot.js",
22
- "./standby-store": "./src/standby-store.js",
23
- "./standby-scheduler": "./src/standby-scheduler.js",
24
- "./pricing": "./src/pricing/index.js",
25
- "./cost": "./src/cost/index.js"
26
- },
27
- "scripts": {
28
- "test": "vitest run"
29
- },
30
- "peerDependencies": {
31
- "better-sqlite3": "^12.10.0",
32
- "execa": "^9.6.0",
33
- "sqlite-vec": "^0.1.9"
34
- },
35
- "peerDependenciesMeta": {
36
- "better-sqlite3": {
37
- "optional": true
38
- },
39
- "execa": {
40
- "optional": true
41
- },
42
- "sqlite-vec": {
43
- "optional": true
44
- }
45
- },
46
- "devDependencies": {
47
- "vitest": "^4.0.0"
48
- }
49
- }
@@ -1,43 +0,0 @@
1
- // Atomic JSON writes for persistent state (resilience audit, Phase 2).
2
- //
3
- // A plain `writeFile(path, JSON.stringify(...))` truncates the live file
4
- // before the new content lands. If the process is killed mid-write
5
- // (SIGKILL, crash, power loss) the file is left truncated and the only
6
- // good copy is already gone — the plan / session / standby JSON
7
- // silently corrupts.
8
- //
9
- // write-temp + rename fixes it: `rename` is atomic on the same
10
- // filesystem, so a reader sees either the old file or the complete new
11
- // one, never a half-written one. The temp file sits next to the target
12
- // (same dir → same FS) and is uniquely named so concurrent writers do
13
- // not clobber each other's temp. A crash before the rename leaves an
14
- // orphan `.tmp` but never a corrupt target.
15
-
16
- import { writeFileSync, renameSync } from "node:fs";
17
- import { writeFile, rename } from "node:fs/promises";
18
-
19
- function tempPath(target) {
20
- return `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
21
- }
22
-
23
- /**
24
- * Atomically write `obj` as pretty JSON to `target` (synchronous).
25
- * @param {string} target - destination path; its directory must exist
26
- * @param {unknown} obj
27
- */
28
- export function writeJsonAtomicSync(target, obj) {
29
- const tmp = tempPath(target);
30
- writeFileSync(tmp, JSON.stringify(obj, null, 2), "utf-8");
31
- renameSync(tmp, target);
32
- }
33
-
34
- /**
35
- * Atomically write `obj` as pretty JSON to `target` (async).
36
- * @param {string} target - destination path; its directory must exist
37
- * @param {unknown} obj
38
- */
39
- export async function writeJsonAtomic(target, obj) {
40
- const tmp = tempPath(target);
41
- await writeFile(tmp, JSON.stringify(obj, null, 2), "utf-8");
42
- await rename(tmp, target);
43
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * Cost aggregator (Cost B — KJC-TSK-0513).
3
- *
4
- * Turns a list of token-usage events emitted by the orchestrator into
5
- * a USD breakdown for a single run / HU. The pipeline calls this once
6
- * when a HU finishes; the result is what Cost C will persist in
7
- * board.db and Cost E will expose as `/api/projects/:id/cost`.
8
- *
9
- * Inputs are deliberately permissive: any extra field on an event is
10
- * ignored. We only require `role`, `model`, `tokensIn`, `tokensOut`.
11
- *
12
- * Unknown models do NOT throw. Their token counts accumulate into
13
- * `unknownModelTokens` and the function returns a flag so the caller
14
- * can surface it in the UI (orange badge instead of `$X.XXXX`).
15
- */
16
-
17
- import { getModelPricing } from "../pricing/index.js";
18
-
19
- const DEFAULT_ROUND_DECIMALS = 4;
20
-
21
- function round(n, decimals = DEFAULT_ROUND_DECIMALS) {
22
- const f = 10 ** decimals;
23
- return Math.round(n * f) / f;
24
- }
25
-
26
- function priceTokens(pricing, tokensIn, tokensOut) {
27
- const inUsd = (tokensIn / 1_000_000) * pricing.inputPerMTok;
28
- const outUsd = (tokensOut / 1_000_000) * pricing.outputPerMTok;
29
- return inUsd + outUsd;
30
- }
31
-
32
- /**
33
- * Aggregate cost of a run.
34
- *
35
- * @param {object} args
36
- * @param {Array<{role:string, model:string, tokensIn:number, tokensOut:number}>} args.events
37
- * @param {(level:"warn"|"info", msg:string, ctx?:object)=>void} [args.logger]
38
- * Optional structured logger. When a model is unknown we call
39
- * logger("warn", ...) so the user sees it once per run.
40
- * @returns {{
41
- * totalUsd:number,
42
- * byRole:Record<string,number>,
43
- * byModel:Record<string,number>,
44
- * unknownModelTokens:{ tokensIn:number, tokensOut:number, models:string[] },
45
- * hasUnknown:boolean,
46
- * currency:string
47
- * }}
48
- */
49
- export function aggregateRunCost({ events, logger } = {}) {
50
- const safeEvents = Array.isArray(events) ? events : [];
51
- const byRole = {};
52
- const byModel = {};
53
- const unknownModels = new Set();
54
- let totalUsd = 0;
55
- let unknownIn = 0;
56
- let unknownOut = 0;
57
-
58
- for (const ev of safeEvents) {
59
- if (!ev || typeof ev !== "object") continue;
60
- const role = typeof ev.role === "string" && ev.role.length > 0 ? ev.role : "unknown";
61
- const model = typeof ev.model === "string" ? ev.model : "";
62
- const tokensIn = Number.isFinite(ev.tokensIn) ? ev.tokensIn : 0;
63
- const tokensOut = Number.isFinite(ev.tokensOut) ? ev.tokensOut : 0;
64
- if (tokensIn === 0 && tokensOut === 0) continue;
65
-
66
- const pricing = getModelPricing(model);
67
- if (!pricing) {
68
- unknownIn += tokensIn;
69
- unknownOut += tokensOut;
70
- if (model.length > 0) unknownModels.add(model);
71
- if (typeof logger === "function") {
72
- logger("warn", "cost-aggregator: unknown model pricing", { model, role, tokensIn, tokensOut });
73
- }
74
- continue;
75
- }
76
-
77
- const cost = priceTokens(pricing, tokensIn, tokensOut);
78
- totalUsd += cost;
79
- byRole[role] = (byRole[role] || 0) + cost;
80
- byModel[model] = (byModel[model] || 0) + cost;
81
- }
82
-
83
- const roundedByRole = Object.fromEntries(
84
- Object.entries(byRole).map(([k, v]) => [k, round(v)])
85
- );
86
- const roundedByModel = Object.fromEntries(
87
- Object.entries(byModel).map(([k, v]) => [k, round(v)])
88
- );
89
-
90
- return {
91
- totalUsd: round(totalUsd),
92
- byRole: roundedByRole,
93
- byModel: roundedByModel,
94
- unknownModelTokens: {
95
- tokensIn: unknownIn,
96
- tokensOut: unknownOut,
97
- models: Array.from(unknownModels).sort(),
98
- },
99
- hasUnknown: unknownIn > 0 || unknownOut > 0,
100
- currency: "USD",
101
- };
102
- }
@@ -1,105 +0,0 @@
1
- // KJC-TSK-0408: snapshots de ficheros por HU para soportar Undo.
2
- //
3
- // Antes de ejecutar una HU, guardamos un ref git apuntando al HEAD
4
- // actual (`refs/kj-snapshots/<huId>`). Si el usuario hace Undo después,
5
- // hacemos `git reset --hard <ref>` para volver al estado pre-run.
6
- //
7
- // Por qué refs en lugar de stash/tarball:
8
- // - Refs son objetos git nativos. No mueven blobs (todos compartidos
9
- // con HEAD), así que crear el snapshot es O(1).
10
- // - El snapshot persiste a través de checkouts y rebases.
11
- // - Si el ref ya apunta al SHA correcto, restore es no-op.
12
- //
13
- // El runner es inyectable para que los tests no necesiten un repo git real.
14
-
15
- import { runCommand } from "./process.js";
16
-
17
- const REF_PREFIX = "refs/kj-snapshots/";
18
-
19
- /**
20
- * Sanitiza un huId para uso como nombre de ref. Git rechaza:
21
- * - `..`, `:`, `~`, `^`, `?`, `*`, `[`, `\`, espacios, control chars
22
- * - segmentos que empiecen por `.` o terminen en `.lock`
23
- * Reemplazamos cualquier no [a-zA-Z0-9_-] por `_`.
24
- */
25
- export function snapshotRefForHu(huId) {
26
- if (!huId) throw new Error("huId requerido");
27
- const safe = String(huId).replaceAll(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
28
- return REF_PREFIX + safe;
29
- }
30
-
31
- async function defaultRunner(args, opts = {}) {
32
- return runCommand("git", args, opts);
33
- }
34
-
35
- /**
36
- * Crea (o actualiza) el ref del snapshot apuntando al HEAD actual.
37
- *
38
- * @param {object} args
39
- * @param {string} args.projectDir
40
- * @param {string} args.huId
41
- * @param {Function} [args.runner] - sustituye git CLI en tests
42
- * @returns {Promise<{ ok: boolean, ref?: string, sha?: string, error?: string }>}
43
- */
44
- export async function createHuSnapshot({ projectDir, huId, runner }) {
45
- const run = runner || defaultRunner;
46
- const ref = snapshotRefForHu(huId);
47
- const head = await run(["rev-parse", "HEAD"], { cwd: projectDir });
48
- if (head.exitCode !== 0) {
49
- return { ok: false, error: `rev-parse HEAD failed: ${(head.stderr || "").trim()}` };
50
- }
51
- const sha = (head.stdout || "").trim();
52
- if (!sha) return { ok: false, error: "HEAD vacío" };
53
- const update = await run(["update-ref", ref, sha], { cwd: projectDir });
54
- if (update.exitCode !== 0) {
55
- return { ok: false, error: `update-ref falló: ${(update.stderr || "").trim()}` };
56
- }
57
- return { ok: true, ref, sha };
58
- }
59
-
60
- /**
61
- * @param {object} args
62
- * @param {string} args.projectDir
63
- * @param {string} args.huId
64
- * @param {Function} [args.runner]
65
- * @returns {Promise<{ ok: boolean, sha?: string }>}
66
- */
67
- export async function hasHuSnapshot({ projectDir, huId, runner }) {
68
- const run = runner || defaultRunner;
69
- const ref = snapshotRefForHu(huId);
70
- const r = await run(["rev-parse", "--verify", "--quiet", ref], { cwd: projectDir });
71
- if (r.exitCode !== 0) return { ok: false };
72
- return { ok: true, sha: (r.stdout || "").trim() };
73
- }
74
-
75
- /**
76
- * Restaura el workspace al snapshot. DESTRUCTIVO: hace `git reset --hard`,
77
- * lo que descarta cambios no committeados. Caller debe avisar al usuario.
78
- *
79
- * @returns {Promise<{ ok: boolean, sha?: string, error?: string }>}
80
- */
81
- export async function restoreHuSnapshot({ projectDir, huId, runner }) {
82
- const run = runner || defaultRunner;
83
- const ref = snapshotRefForHu(huId);
84
- const has = await hasHuSnapshot({ projectDir, huId, runner: run });
85
- if (!has.ok) return { ok: false, error: `snapshot no encontrado para HU ${huId}` };
86
- const r = await run(["reset", "--hard", ref], { cwd: projectDir });
87
- if (r.exitCode !== 0) {
88
- return { ok: false, error: `reset --hard falló: ${(r.stderr || "").trim()}` };
89
- }
90
- return { ok: true, sha: has.sha };
91
- }
92
-
93
- /**
94
- * Borra el ref del snapshot. Idempotente: si no existe, devuelve ok:true.
95
- */
96
- export async function removeHuSnapshot({ projectDir, huId, runner }) {
97
- const run = runner || defaultRunner;
98
- const ref = snapshotRefForHu(huId);
99
- const r = await run(["update-ref", "-d", ref], { cwd: projectDir });
100
- // git update-ref -d devuelve 0 incluso si el ref no existía.
101
- if (r.exitCode !== 0) {
102
- return { ok: false, error: `update-ref -d falló: ${(r.stderr || "").trim()}` };
103
- }
104
- return { ok: true };
105
- }
@@ -1,18 +0,0 @@
1
- // Barrel export for @karajan/core. Subpath imports
2
- // (e.g. `@karajan/core/atomic-write`) are the preferred shape — this
3
- // barrel only exists so a single `import { writeJsonAtomic } from
4
- // "@karajan/core"` keeps working for callers that want the whole API.
5
-
6
- export * from "./atomic-write.js";
7
- export * from "./shared-paths.js";
8
- export * from "./port-check.js";
9
- export * from "./paths.js";
10
- export * from "./run-registry.js";
11
- export * from "./vec-store.js";
12
- export * from "./plan-id.js";
13
- export * from "./plan-hu-ops.js";
14
- export * from "./plan-validation.js";
15
- export * from "./process.js";
16
- export * from "./hu-snapshot.js";
17
- export * from "./standby-store.js";
18
- export * from "./standby-scheduler.js";