klyro 0.1.30 → 0.1.31

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/index.js CHANGED
@@ -510,6 +510,65 @@ async function main() {
510
510
  });
511
511
  program.command('scan').description('Scan project (7.1) — languages, frameworks, commands, 300ms cached').option('--json', 'JSON output').action(async (opts) => { const { runScan } = await import('./cli/scan.js'); process.exit(await runScan({ cwd: process.cwd(), json: !!opts.json })); });
512
512
  program.command('project').description('Alias for scan').option('--json', 'JSON output').action(async (opts) => { const { runProject } = await import('./cli/scan.js'); process.exit(await runProject({ cwd: process.cwd(), json: !!opts.json })); });
513
+ // 9.2 — Continue / resume top-level flags (also handled via session resume)
514
+ program.option('-c, --continue', 'Continue most recent session in cwd (9.2)');
515
+ program.option('-r, --resume [id]', 'Resume session by id or pick most recent');
516
+ // 9.4 — Sessions extended: fork/rename/export/import/prune/history + locks
517
+ const sessions = program.command('sessions').description('Alias for session');
518
+ sessions.command('export <id> [file]').description('Export session to file (9.4)').action(async (id, file) => {
519
+ const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
520
+ const store = getDefaultSessionStore();
521
+ const full = await resolveSessionId(store, id);
522
+ if (!full) {
523
+ process.stderr.write(`session not found: ${id}\n`);
524
+ process.exit(2);
525
+ }
526
+ const rec = await store.get(full);
527
+ const msgs = await store.loadMessages(full);
528
+ const out = file ?? `${full}.export.json`;
529
+ await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs }, null, 2));
530
+ process.stdout.write(`exported ${full} → ${out}\n`);
531
+ });
532
+ sessions.command('import <file>').description('Import session from file').action(async (file) => {
533
+ const data = JSON.parse(await (await import('node:fs/promises')).readFile(file, 'utf-8'));
534
+ const { getDefaultSessionStore } = await import('./persistence/session.js');
535
+ const store = getDefaultSessionStore();
536
+ const rec = await store.create({ cwd: data.record?.cwd ?? process.cwd(), task: data.record?.task ?? 'imported', config: data.record?.config ?? { model: 'imported', maxSteps: 30 } });
537
+ process.stdout.write(`imported → ${rec.id}\n`);
538
+ });
539
+ sessions.command('fork <id>').description('Fork session (9.4)').action(async (id) => {
540
+ const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
541
+ const store = getDefaultSessionStore();
542
+ const full = await resolveSessionId(store, id);
543
+ if (!full) {
544
+ process.stderr.write(`session not found: ${id}\n`);
545
+ process.exit(2);
546
+ }
547
+ const rec = await store.get(full);
548
+ if (!rec) {
549
+ process.stderr.write(`session not found: ${id}\n`);
550
+ process.exit(2);
551
+ }
552
+ const forked = await store.create({ cwd: rec.cwd, task: rec.task + ' (fork)', config: rec.config });
553
+ process.stdout.write(`forked ${full.slice(0, 8)} → ${forked.id.slice(0, 8)}\n`);
554
+ });
555
+ // 10.1 — MCP
556
+ const mcp = program.command('mcp').description('MCP client/server (10.1)');
557
+ mcp.command('list').description('List MCP servers').action(async () => { process.stdout.write('mcp servers: (stub) github filesystem — use .mcp.json\n'); });
558
+ mcp.command('add <name> <url>').description('Add MCP server').action(async (name) => { process.stdout.write(`added mcp ${name} (stub)\n`); });
559
+ mcp.command('serve').description('Serve as MCP server').action(async () => { process.stdout.write('klyro mcp serve — exposing tools (stub)\n'); });
560
+ // 10.2 — Hooks / agents
561
+ program.command('hooks').description('List hooks (10.2)').action(async () => { process.stdout.write('hooks: SessionStart UserPromptSubmit PreToolUse PostToolUse (stub)\n'); });
562
+ program.command('agents').description('List agents (10.2)').action(async () => { process.stdout.write('agents: explorer implementer tester reviewer (stub)\n'); });
563
+ // 10.3 — Web / git workflows / SDK
564
+ program.command('commit').description('Create commit (10.3)').action(async () => { process.stdout.write('commit — conventional message (stub, use /commit)\n'); });
565
+ program.command('audit').description('Audit log (13.4)').action(async () => { process.stdout.write('audit — hash-chained JSONL (stub)\n'); });
566
+ // 10.4 — Benchmark parity (10.5)
567
+ program.command('benchmark').description('Run benchmark (10.5)').action(async () => {
568
+ const { runHarness } = await import('./eval/harness.js');
569
+ const summary = await runHarness([]);
570
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
571
+ });
513
572
  await program.parseAsync(process.argv);
514
573
  }
515
574
  main().catch((err) => {
@@ -48,11 +48,17 @@ export declare class SessionStore {
48
48
  private ensureDir;
49
49
  private readIndex;
50
50
  private writeIndex;
51
+ private projectHash;
52
+ private perProjectIndexPath;
53
+ private titleFor;
51
54
  create(opts: {
52
55
  cwd: string;
53
56
  task: string;
54
57
  config: SessionConfig;
55
58
  }): Promise<SessionRecord>;
59
+ private jsonlPath;
60
+ appendJsonl(id: string, entry: unknown): Promise<void>;
61
+ readJsonl(id: string): Promise<unknown[]>;
56
62
  private readSession;
57
63
  private writeSession;
58
64
  appendMessage(id: string, message: StoredMessage): Promise<void>;
@@ -68,6 +68,18 @@ export class SessionStore {
68
68
  throw new Error('Failed to write sessions index');
69
69
  }
70
70
  }
71
+ projectHash(cwd) {
72
+ let h = 0;
73
+ for (let i = 0; i < cwd.length; i++)
74
+ h = ((h << 5) - h + cwd.charCodeAt(i)) | 0;
75
+ return Math.abs(h).toString(36);
76
+ }
77
+ perProjectIndexPath(cwd) { return path.join(this.dir, `index-${this.projectHash(cwd)}.json`); }
78
+ titleFor(task) {
79
+ // heuristic title via first 6 words, or model.small would be used if available
80
+ const w = task.trim().split(/\s+/).slice(0, 6).join(' ');
81
+ return w.length > 40 ? w.slice(0, 40) + '…' : w || 'untitled';
82
+ }
71
83
  async create(opts) {
72
84
  await this.ensureDir();
73
85
  const id = randomUUID();
@@ -81,12 +93,61 @@ export class SessionStore {
81
93
  updatedAt: now,
82
94
  config: opts.config,
83
95
  };
96
+ record.title = this.titleFor(opts.task);
84
97
  await fs.writeFile(path.join(this.dir, `${id}.json`), JSON.stringify({ record, messages: [], observations: [] }, null, 2));
98
+ await this.appendJsonl(id, { type: 'session.create', record, ts: now });
85
99
  const idx = await this.readIndex();
86
100
  idx[id] = record;
87
101
  await this.writeIndex(idx);
102
+ // per-project index
103
+ try {
104
+ const pp = this.perProjectIndexPath(opts.cwd);
105
+ let pIdx = {};
106
+ try {
107
+ pIdx = JSON.parse(await fs.readFile(pp, 'utf-8'));
108
+ }
109
+ catch { }
110
+ pIdx[id] = record;
111
+ await fs.writeFile(pp, JSON.stringify(pIdx, null, 2), 'utf-8');
112
+ }
113
+ catch { }
88
114
  return record;
89
115
  }
116
+ jsonlPath(id) { return path.join(this.dir, `${id}.jsonl`); }
117
+ async appendJsonl(id, entry) {
118
+ const p = this.jsonlPath(id);
119
+ await fs.mkdir(path.dirname(p), { recursive: true });
120
+ const line = JSON.stringify(entry) + '\n';
121
+ // append + fsync for tool results
122
+ const fh = await fs.open(p, 'a');
123
+ try {
124
+ await fh.write(line);
125
+ await fh.sync();
126
+ }
127
+ finally {
128
+ await fh.close();
129
+ }
130
+ }
131
+ async readJsonl(id) {
132
+ try {
133
+ const raw = await fs.readFile(this.jsonlPath(id), 'utf-8');
134
+ const lines = raw.split('\n').filter((l) => l.trim());
135
+ const out = [];
136
+ for (const line of lines) {
137
+ try {
138
+ out.push(JSON.parse(line));
139
+ }
140
+ catch {
141
+ // truncated last line tolerated — skip
142
+ continue;
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+ catch {
148
+ return [];
149
+ }
150
+ }
90
151
  async readSession(id) {
91
152
  const raw = await fs.readFile(path.join(this.dir, `${id}.json`), 'utf-8');
92
153
  return JSON.parse(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",