klyro 0.1.60 → 0.1.62

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/READ.md CHANGED
@@ -22,7 +22,7 @@
22
22
  |-------|--------|-----|
23
23
  | Language | TypeScript 5.5, Node 20+ | Strict, `NodeNext`, `tsc` → `dist/` |
24
24
  | CLI | `commander 12.1` | Stable, `InvalidArgumentError` for `exit 2` |
25
- | TUI | `ink 7.1` + `react 19` + `ink-spinner 5` | React for terminal, proven by Claude Code/Gemini |
25
+ | TUI | `ink 7.1` + `react 19` + `ink-spinner 5` | React renderer for the terminal |
26
26
  | Schema | `zod 4.5` | Tool input validation + config schema |
27
27
  | Test | `vitest 4.1` `fileParallelism:false` `10s timeout` | `node` env, deterministic mocks |
28
28
  | Build | `tsc` (not `tsup`) | `tsc --noEmit` `typecheck`, `tsc` `build` |
@@ -465,3 +465,71 @@ Global flags `src/index.ts:60` `--cwd/--config/--debug/--verbose/--quiet/--json/
465
465
 
466
466
  Run: `pnpm build && pnpm test && node dist/index.js --help` → `klyro -p "fix login test" --output json` → `klyro doctor`.
467
467
 
468
+ ---
469
+
470
+ ## 20. Session Ledger v0.1.39 → v0.1.61 (what was done, what failed)
471
+
472
+ All work below was done live against user-reported production issues, verified with
473
+ `typecheck 0` + full `vitest` + built-`dist` smoke tests before every publish.
474
+ Current: **`klyro@0.1.61` `latest`, 51 test files, 442/442 tests, commits as
475
+ `siddu-lingampelli`** (history rewritten from `Mythicai-lab`, tags moved, §20.6).
476
+
477
+ ### 20.1 Release history
478
+
479
+ | Version | Commit | What shipped |
480
+ |---|---|---|
481
+ | 0.1.39 | base | Single-column Klyro TUI, true `#E8843C` orange via 24-bit, hello Direct-LLM (no tools) |
482
+ | 0.1.40 | `ca85d56` | True `#FF6B1A` orange, clean single-column, no `g`-letter corruption |
483
+ | 0.1.41 | `1884807` | `useChatScroll` hook (pin/unpin, snap-to-message, new-messages pill) |
484
+ | 0.1.42 | `50c84dc` | Audit batch: git-blame argv+path validation, `NODE_OPTIONS`/`LD_PRELOAD` block, ordered transcript, configurable `rerunOnce` timeout, model-aware `estimateCost`, Buffer-based (non-O(n²)) verify collectors, deleted empty `packages/cli+core`, `snippets.ts`, `util/log.ts`, `stuckCount`; TUI ellipsis `…`→`...`, mojibake purge |
485
+ | 0.1.43 | `04bd24e` | Slash groundwork: `/provider` `/effort` `/model` `/compact` `/plan` `/login` `/logout` `/init` handlers |
486
+ | 0.1.44 | `81d9627` | **Priority 1 per `commands.md`** (~39 cmds): session/model/project/perms/auth groups; TUI orphan-file reconciliation (deleted `app.inline/banner/input-box/activity-line/thinking-block/components/Header`) |
487
+ | 0.1.45 | `feb30ff` | **Priority 2 per `commands.md`** (~70 cmds): review/build/git/files/agents/config/misc; `/` autocomplete popup + Tab |
488
+ | 0.1.46 | `b8947d1` | **`scroll.md` S1–S9 Ink adaptation**: `measure.ts` (display-width/wrap/cache/line-index), `scroll-model.ts` (anchor reducer), input history + contextual ↑/↓, run-length activity merge, console-patch, exit transcript replay |
489
+ | 0.1.47 | `c440bc3` | **Dead scroll trap fix** (directional epsilon — line/wheel up from bottom could never escape) + mouse-wheel capture + `Ctrl+F/B` pager keys |
490
+ | 0.1.48 | `1e7f85c` | **Set-once auth**: persisted provider resolution (env → config+credentials → probe), first-run setup, `/model` `/provider` persist, `/reload` rebuilds adapter |
491
+ | 0.1.49 | `f0e840d` + `262b8fc` | Plain-HTTP endpoints explain+confirm with persisted `allowInsecure` opt-in, loud rejections (`lastProviderError`), prefilled setup, keep-saved-key; (+ separate commit preserving a concurrent working-tree `scroll.md` rewrite) |
492
+ | 0.1.50 | `5b3d3ee` | **`design.md` conformance**: half-page keys, Esc/Ctrl+C/Shift+Enter semantics, full Markdown renderer, header/statusbar/plan restyle |
493
+ | 0.1.51 | `6ba47de` | **`scroll.md` command architecture**: `transcript-commands.ts` keybind mapper + 4-command handle, submit-key settle effect |
494
+ | 0.1.52 | `db09e5f` | **Markdown columns bug**: `MarkdownText` fragment rendered lines as side-by-side columns — back to single `<Text>` |
495
+ | 0.1.53 | `5f9fa84` | Root `overflow:hidden` I1 guard (input can never be pushed out), tool results patch start-items in place (no forever-`running` spinners), narrow-terminal hints |
496
+ | 0.1.54 | `1ec3d0c` | **Full audit application** (P0+P1, §20.3) |
497
+ | 0.1.55 | `eb8776f` | Live working spinner (Thinking row, running tool groups, status bar) |
498
+ | 0.1.56 | `cf1a94d` | **Thinking channel**: `reasoning_content`/Anthropic-thinking capture → dim display → cleared on response |
499
+ | 0.1.57 | `f6fd749` | Composer audit: `flexShrink={0}` chrome, position/multiline/aggregation tests |
500
+ | 0.1.58 | `a57d526` | Abort clears thinking, `/clear` resets scroll anchor, badge overlap fix |
501
+ | 0.1.59 | `bbe9ba6` | Scroll position readout (`⇅ top/max`) in status bar |
502
+ | 0.1.60 | `6627b5b` | **Wheel tap fix**: intercept `stdin.read` (Ink7 paused mode), not `data` events that never fire |
503
+ | 0.1.61 | `5a1a97f` | Brand sweep: Claude naming removed, `CLAUDE.md` fallback dropped (Klyro-only) |
504
+
505
+ ### 20.2 What was built (by area)
506
+
507
+ - **Slash commands (113, all smoke-tested on built `dist`)**: session (`/new` `/resume` `/sessions` `/rename` `/fork` `/branch` `/export` `/copy`), model (`/model` `/models` `/provider` `/effort` `/fast`), project (`/init` `/status` `/context` `/diff` `/plan` `/todos` `/memory`), perms (`/permissions` `/mode` `/sandbox` `/approve` `/deny`), app (`/login` `/logout` `/auth` `/version` `/update` `/cancel` `/shell` `!` `/mention` `@` `/tools`), workflow (`/review` `/code-review` `/security-review` `/simplify` `/test` `/lint` `/build` `/run` `/fix` `/explain` `/format` `/ask`), checkpoints (`/undo` `/redo`→honest-noop `/rewind` `/checkpoint` `/accept` `/reject`), activity (`/details` `/verbose` `/raw` `/activity` `/tasks` `/ps` `/stop` `/queue` `/retry` `/kill`), agents+MCP, files (`/attach`→in-context `/drop` `/files` `/image` `/paste` `/ls` `/tree` `/search` `/web` `/read` `/map` `/tokens`), git (`/commit` via temp file `/push` `/pull` `/pr` `/issue`), config UI (`/editor` argv-spawn `/keymap` `/vim` `/theme` `/statusline` `/output-style`), diagnostics (`/doctor` `/debug` `/whoami` `/reload` `/reset` `/bug` `/changelog`), library (`/prompt` `/alias`→auto-expand `/commands` `/env` (injection keys blocked) `/deps` `/install`).
508
+ - **Scroll system** (`src/tui/scroll-model.ts` + `measure.ts` + `mouse.ts` + `transcript-commands.ts`): anchor `{itemId,lineInItem}` (never raw rows), measured wrapped display lines (CJK=2/emoji=2/combining=0), `MeasureCache`, follow-at-bottom / freeze-when-pinned / `↓ N new` badge, `End`/`Ctrl+G`/empty-Enter dismiss, wheel ±3, PageUp/Dn half-page, Home/End first/last, exit transcript replay, degraded tiny-terminal mode.
509
+ - **Auth persistence**: `~/.klyro/settings.json` + `credentials.json` (0600); resolution env → config → stored keys → local probe; `KLYRO_CONFIG`/`KLYRO_CONFIG_DIR`/`KLYRO_CREDENTIALS_FILE` overrides (also used by tests); persisted `allowInsecure` replaces per-terminal `KLYRO_ALLOW_INSECURE=1`.
510
+ - **Verification**: denylist (destructive-only; metachars allowed since commands are user/config-sourced), sanity checks fail-fast *before* the suite, scoped→full, flaky rerun with configured timeout, `hasEdits` drives `--require-verify` (exit 8 only when edits exist).
511
+ - **Safety**: `resolveAndFollowSymlinks` everywhere incl. checkpoints (traversal-contained undo that also restores deletions), `wasRead` guards on all four mutators with normalized + per-task history, `readFileSizeRule` stats real files, `clonePolicyConfig` per engine, `setByPath` proto-pollution guard, shell full-output caps + POSIX process-group kill, `startBackgroundArgv` (shell:false), secret redaction before model/summary paths.
512
+
513
+ ### 20.3 Audit application (the big diagnostic)
514
+
515
+ Every claim was verified against code before touching. **Confirmed + fixed (P0)**: ApprovalModal never rendered (+ single-listener bridge deafened it — also the flake root cause); Anthropic `role:tool` flattened to empty turns; duplicate `message_end`; edit-file fuzzy rewriting whole files (plus a dead line-window tier); apply-patch append-only stub → real hunk applier; `require()` in ESM (`run-verify`, later all touched files → static imports). **Confirmed + fixed (P1)**: shared policy config; `inferFileChanged` gaps; tokenizer pair orphans; klyro-md import escape + no caps; verify-order waste + `$()` overblock; unbounded full buffers; `/web` caps; checkpoint traversal/undo; store locking + `delete()`/`fork()` with messages; SIGINT-once; silent-mode newline; retry sleep ignores abort; read-history normalization; dotfile flag; `read_fileBody` shape bug in `/attach` `/mention` `/read` `/review` `/explain`; Ctrl+C-safe login/setup; `isToolErrorCode` swallowing custom codes; read-size rule via stat; multi-edit guards; `last.diff` writer; dry-run real prompt; debug.log rotation; `/cd` cwd reset; cost/context single source; headless URL policy. **Deliberately not changed**: `isSimpleChat` fast-path (intended UX); Ink-vs-OpenTUI architecture (approved); eval/MCP/L10–L20 (roadmap); `$EDITOR`/user-typed shell passthrough (user's own intent); trace paths; `isToolErrorCode`-adjacent churn that worked.
516
+
517
+ ### 20.4 What FAILED along the way (honest log)
518
+
519
+ - **Markdown columns regression (shipped 0.1.50, caught by user screenshot, fixed 0.1.52)**: fragment of `<Text>` in a row `Box` laid lines out as columns. Lesson: frame-level regression test added (fails on old code, passes on new).
520
+ - **Dead scroll trap (mine)**: symmetric follow-epsilon made line/wheel-up from bottom re-stick forever. Found by tracing, fixed directional, regression-tested.
521
+ - **Deaf wheel tap (mine)**: intercepted `data` events; Ink7 only uses `readable`+`read()`. Found by reading installed Ink source, rewrote + tested the wrapper contract.
522
+ - **Silent setup loop (mine)**: saved-but-rejected base URLs were swallowed → infinite first-run. Fixed with recorded reasons + prefilled recovery.
523
+ - **`require()` in ESM**: would have thrown `ReferenceError` in production `dist` while passing under `tsx`/`vitest` shims. Converted everything touched to static imports.
524
+ - **My own test-math errors**: indent tier (4sp→2 tabs), wheel maxTop, badge line counts, half-page expectations — each caught by running tests, fixed, and (for scroll-reset) proven discriminating by noop-check.
525
+ - **Infra friction**: npm `E409` staging + `E404` propagation delays (60–150s waits per publish); PowerShell mangling inline `python`/`node` quoting (rule learned: script files only); `git add -A` twice scooped a concurrent agent's doc rewrites (split into separate commits; own files only since); vitest load flakes under parallel sessions (fixed sleeps → poll-with-timeout; one residual publish-gate flake at 0.1.59, green on rerun).
526
+ - **Authorship**: 49 commits + 10 tags rewritten to `siddu-lingampelli` (force-with-lease, trees verified identical); repo-local git identity set.
527
+
528
+ ### 20.5 Environment notes for future sessions
529
+
530
+ - Windows PowerShell 5.1: chain with `;`, never `&&`; quote paths; never inline `python -c` with brackets/quotes (write temp scripts under `C:\Users\L.Siddhartha\AppData\Local\Temp\opencode`).
531
+ - Never `git add -A` blindly — another agent rewrites `design.md`/`scroll.md`/`TUI_DESIGN.md`/`commands.md` concurrently; stage own files explicitly.
532
+ - `npm publish` runs `typecheck && test && build`; expect CDN delays; `latest` flips ~1–3 min after `+ klyro@x`.
533
+ - Test isolation for anything reading `~/.klyro`: set `KLYRO_CONFIG` + `KLYRO_CONFIG_DIR` + `KLYRO_CREDENTIALS_FILE` to temp dirs.
534
+ - `ink-testing-library` has fixed terminal geometry (no resize tests) and normalizes keys like production Ink; production-only paths (stdin tap, alt-screen) need contract tests, not frame tests.
535
+
@@ -106,7 +106,7 @@ async function* streamAnthropic(req, opts) {
106
106
  }
107
107
  yield { kind: 'message_start' };
108
108
  // Stream SSE: lines are `event: <type>\ndata: <json>\n\n`.
109
- // We use a simple incremental parser; Claude's API guarantees
109
+ // We use a simple incremental parser; Anthropic's API guarantees
110
110
  // well-formed SSE.
111
111
  const reader = resp.body.getReader();
112
112
  const decoder = new TextDecoder('utf-8');
package/dist/cli/repl.js CHANGED
@@ -21,6 +21,7 @@ import { TuiApprovalBridge } from '../tui/approval.js';
21
21
  import { parseUnifiedDiff } from '../tui/diff-parser.js';
22
22
  import { parse } from './slash/parser.js';
23
23
  import { resolveProvider, providerHelp, lastProviderError } from '../providers.js';
24
+ import { readVersion } from '../version.js';
24
25
  import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, createReadWrapper } from '../tui/mouse.js';
25
26
  import { inferProviderFromBaseURL } from '../agent/registry.js';
26
27
  import { getDefaultSessionStore } from '../persistence/session.js';
@@ -464,6 +465,7 @@ export async function startRepl(opts = {}) {
464
465
  initialModel: model,
465
466
  maxSteps: currentMaxSteps,
466
467
  cwd,
468
+ version: readVersion(),
467
469
  initialStatus: { status: 'idle' },
468
470
  approvalBridge: tuiBridge,
469
471
  isFullscreen: isAltScreen,
@@ -533,7 +535,7 @@ export async function startRepl(opts = {}) {
533
535
  const rec = await tuiStore.create({ cwd, task: taskText, config: { model, maxSteps: currentMaxSteps } });
534
536
  sessionId = rec.id;
535
537
  tuiSessionId = rec.id;
536
- // Session info goes to status bar, not transcript (clean like Claude Code)
538
+ // Session info goes to status bar, not transcript (clean Klyro transcript)
537
539
  queuedStatus({ status: 'running', step: 0, model });
538
540
  }
539
541
  catch {
@@ -793,17 +795,7 @@ export async function startRepl(opts = {}) {
793
795
  return;
794
796
  }
795
797
  case 'version': {
796
- const { readFileSync } = await import('node:fs');
797
- const { resolve, dirname } = await import('node:path');
798
- const { fileURLToPath } = await import('node:url');
799
- try {
800
- const here = dirname(fileURLToPath(import.meta.url));
801
- const pkg = JSON.parse(readFileSync(resolve(here, '../../package.json'), 'utf-8'));
802
- queuedAppend({ id: `ver-${Date.now()}`, kind: 'text', text: `klyro ${pkg.version ?? '0.0.0'}`, role: 'assistant' });
803
- }
804
- catch {
805
- queuedAppend({ id: `ver-${Date.now()}`, kind: 'text', text: 'klyro (version unknown)', role: 'assistant' });
806
- }
798
+ queuedAppend({ id: `ver-${Date.now()}`, kind: 'text', text: `klyro ${readVersion()}`, role: 'assistant' });
807
799
  return;
808
800
  }
809
801
  case 'cost': {
@@ -37,7 +37,7 @@ export async function loadKlyroMd(cwd) {
37
37
  }
38
38
  }
39
39
  // Root (imports resolved relative to each file, contained to cwd)
40
- for (const name of ['KLYRO.md', 'KLYRO.local.md', 'AGENTS.md', 'CLAUDE.md', '.cursorrules']) {
40
+ for (const name of ['KLYRO.md', 'KLYRO.local.md', 'AGENTS.md', '.cursorrules']) {
41
41
  const p = path.join(cwd, name);
42
42
  try {
43
43
  const t = await fs.readFile(p, 'utf-8');
package/dist/index.js CHANGED
@@ -12,9 +12,6 @@
12
12
  * Configure via env: KLYRO_BASE_URL, KLYRO_API_KEY, KLYRO_MODEL.
13
13
  */
14
14
  import { Command, InvalidArgumentError } from 'commander';
15
- import { readFileSync } from 'node:fs';
16
- import { fileURLToPath } from 'node:url';
17
- import { dirname, resolve } from 'node:path';
18
15
  import { chat } from './chat.js';
19
16
  import { repl } from './repl.js';
20
17
  import { startRepl } from './cli/repl.js';
@@ -25,46 +22,7 @@ import { runDoctor } from './cli/doctor.js';
25
22
  import { runCompletion } from './cli/completion.js';
26
23
  import { runUpdate } from './cli/update.js';
27
24
  import { runLogin, runLogout } from './cli/auth.js';
28
- // Read version from package.json so `klyro --version` always matches the
29
- // published version. Walks up from this file (src/index.ts) to find the
30
- // nearest package.json.
31
- function readVersion() {
32
- const here = dirname(fileURLToPath(import.meta.url));
33
- const candidates = [
34
- resolve(here, '..', 'package.json'),
35
- resolve(here, '../..', 'package.json'),
36
- resolve(here, '../../package.json'),
37
- ];
38
- for (const pkgPath of candidates) {
39
- try {
40
- const raw = readFileSync(pkgPath, 'utf8');
41
- const pkg = JSON.parse(raw);
42
- if (typeof pkg.version === 'string' && pkg.version.length > 0)
43
- return pkg.version;
44
- }
45
- catch (err) {
46
- const e = err;
47
- // Only ignore missing file; surface JSON parse errors
48
- if (e.code === 'ENOENT')
49
- continue;
50
- // Malformed JSON — warn but don't crash version output
51
- process.stderr.write(`klyro: warning: malformed ${pkgPath}: ${e.message}\n`);
52
- continue;
53
- }
54
- }
55
- // Fallback: try require-style resolution
56
- try {
57
- const pkgPath = resolve(process.cwd(), 'package.json');
58
- const raw = readFileSync(pkgPath, 'utf8');
59
- const pkg = JSON.parse(raw);
60
- if (pkg.name === 'klyro' && typeof pkg.version === 'string')
61
- return pkg.version;
62
- }
63
- catch {
64
- // ignore
65
- }
66
- return '0.0.0';
67
- }
25
+ import { readVersion } from './version.js';
68
26
  const VERSION = readVersion();
69
27
  function parsePositiveInt(name, v) {
70
28
  const n = Number(v);
package/dist/tui/app.js CHANGED
@@ -13,6 +13,7 @@ import { tokens, g } from './tokens.js';
13
13
  import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, } from './scroll-model.js';
14
14
  import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
15
15
  import { renderMarkdownLines } from './markdown.js';
16
+ import { readVersion } from '../version.js';
16
17
  import { getTranscriptCommand, } from './transcript-commands.js';
17
18
  import { estimateCost, getModelInfo } from '../providers/model-info.js';
18
19
  let _id = 0;
@@ -674,7 +675,8 @@ export function App(props) {
674
675
  setHistIdx(null);
675
676
  }
676
677
  });
677
- const ver = props.version ?? '0.1.27';
678
+ // Single source of truth: package.json via version.ts never hardcoded.
679
+ const ver = props.version ?? readVersion();
678
680
  const rule = g('rule').repeat(Math.max(10, width - 2));
679
681
  // Single cost/context source: model-aware registry rates + window.
680
682
  const cost = estimateCost(status.model, status.usageInput, status.usageOutput);
@@ -6,8 +6,8 @@ describe('App', () => {
6
6
  it('shows the empty-state hint and the status line', () => {
7
7
  const { lastFrame } = render(_jsx(App, { initialModel: "mock", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
8
8
  const out = lastFrame();
9
- // design.md: top bar shows claude-code + sessions, center shows Message Klyro placeholder
10
- expect(out).toMatch(/claude-code|Sessions|Message Klyro/i);
9
+ // Top bar shows KLYRO + sessions, center shows Message Klyro placeholder
10
+ expect(out).toMatch(/Sessions|Message Klyro/i);
11
11
  expect(out).toMatch(/Message Klyro|Type a message/i);
12
12
  });
13
13
  it('renders initial transcript items', () => {
@@ -565,6 +565,16 @@ describe('App', () => {
565
565
  const top = await waitForMatch(lastFrame, /⇅ 0\/\d+/);
566
566
  expect(top).toMatch(/MSG-00-tag/);
567
567
  });
568
+ it('header shows the passed version, defaulting to package.json (never stale)', async () => {
569
+ const { readVersion } = await import('../version.js');
570
+ const { lastFrame: f1 } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, version: "9.9.9-test" }));
571
+ await new Promise((r) => setTimeout(r, 50));
572
+ expect(f1() ?? '').toContain('9.9.9-test');
573
+ const { lastFrame: f2 } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
574
+ await new Promise((r) => setTimeout(r, 50));
575
+ expect(f2() ?? '').toContain(readVersion());
576
+ expect(f2() ?? '').not.toContain('0.1.27');
577
+ });
568
578
  it('Shift+Up / Shift+Down scroll by one line', async () => {
569
579
  const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
570
580
  await new Promise((r) => setTimeout(r, 50));
@@ -16,11 +16,11 @@ describe('App visual snapshot', () => {
16
16
  it('renders header + statusline + transcript + input at idle', () => {
17
17
  const { lastFrame } = render(_jsx(App, { ...DEFAULT_PROPS, initialModel: "gpt-4o-mini", maxSteps: 30, initialStatus: { status: 'idle', model: 'gpt-4o-mini', step: 0, maxSteps: 30, usageInput: 0, usageOutput: 0, repairs: 0 } }));
18
18
  const frame = lastFrame();
19
- // design.md: top bar shows ▌ claude-code, not KLYRO, and sessions/files
20
- expect(frame).toMatch(/claude-code|KLYRO/i);
19
+ // Top bar shows KLYRO, transcript shows sessions/files context
20
+ expect(frame).toMatch(/KLYRO/i);
21
21
  expect(frame).toMatch(/demo|Sessions|Files/i);
22
22
  expect(frame).toMatch(/shift\+tab|for history|Message Klyro|Type a message/i);
23
- expect(frame).toMatch(/Message Klyro|claude-code|>/i);
23
+ expect(frame).toMatch(/Message Klyro|>/i);
24
24
  });
25
25
  it('renders a transcript with assistant text', () => {
26
26
  const { lastFrame } = render(_jsx(App, { ...DEFAULT_PROPS, initialModel: "gpt-4o-mini", maxSteps: 30, initialStatus: { status: 'idle', model: 'gpt-4o-mini', step: 0, maxSteps: 30, usageInput: 0, usageOutput: 0, repairs: 0 }, initialTranscript: [
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Single source of truth for the Klyro version (read from package.json).
3
+ *
4
+ * Every surface — `klyro --version`, the TUI header, `/version` — reads
5
+ * through here, so a version bump in package.json shows up everywhere with
6
+ * no hardcoded fallbacks rotting behind.
7
+ */
8
+ export declare function readVersion(): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Single source of truth for the Klyro version (read from package.json).
3
+ *
4
+ * Every surface — `klyro --version`, the TUI header, `/version` — reads
5
+ * through here, so a version bump in package.json shows up everywhere with
6
+ * no hardcoded fallbacks rotting behind.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, resolve } from 'node:path';
11
+ export function readVersion() {
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const candidates = [
14
+ resolve(here, '..', 'package.json'),
15
+ resolve(here, '../..', 'package.json'),
16
+ resolve(here, '../../package.json'),
17
+ ];
18
+ for (const pkgPath of candidates) {
19
+ try {
20
+ const raw = readFileSync(pkgPath, 'utf8');
21
+ const pkg = JSON.parse(raw);
22
+ if (typeof pkg.version === 'string' && pkg.version.length > 0)
23
+ return pkg.version;
24
+ }
25
+ catch (err) {
26
+ const e = err;
27
+ // Only ignore missing file; surface JSON parse errors
28
+ if (e.code === 'ENOENT')
29
+ continue;
30
+ // Malformed JSON — warn but don't crash version output
31
+ process.stderr.write(`klyro: warning: malformed ${pkgPath}: ${e.message}\n`);
32
+ continue;
33
+ }
34
+ }
35
+ // Fallback: try require-style resolution
36
+ try {
37
+ const pkgPath = resolve(process.cwd(), 'package.json');
38
+ const raw = readFileSync(pkgPath, 'utf8');
39
+ const pkg = JSON.parse(raw);
40
+ if (pkg.name === 'klyro' && typeof pkg.version === 'string')
41
+ return pkg.version;
42
+ }
43
+ catch {
44
+ // ignore
45
+ }
46
+ return '0.0.0';
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Klyro \u2014 autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",