klyro 0.1.30 → 0.1.32
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 +78 -0
- package/dist/persistence/store.d.ts +6 -0
- package/dist/persistence/store.js +61 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -223,6 +223,25 @@ async function main() {
|
|
|
223
223
|
program
|
|
224
224
|
.action(async (promptArg) => {
|
|
225
225
|
const opts = program.opts();
|
|
226
|
+
// 9.2 — --continue / --resume handling
|
|
227
|
+
if (opts.continue || typeof opts.resume === 'string') {
|
|
228
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
229
|
+
const store = getDefaultSessionStore();
|
|
230
|
+
const all = (await store.list()).filter((r) => r.cwd === process.cwd()).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
231
|
+
const target = typeof opts.resume === 'string' ? all.find((r) => r.id.startsWith(opts.resume)) ?? all[0] : all[0];
|
|
232
|
+
if (!target) {
|
|
233
|
+
process.stderr.write('klyro: no session to continue in this cwd (try klyro session list)\n');
|
|
234
|
+
process.exit(2);
|
|
235
|
+
}
|
|
236
|
+
process.stderr.write(`klyro: continuing session ${target.id.slice(0, 8)} — ${target.task}\n`);
|
|
237
|
+
const model = process.env.KLYRO_MODEL ?? target.config.model;
|
|
238
|
+
if (!model) {
|
|
239
|
+
process.stderr.write('klyro: KLYRO_MODEL not set\n');
|
|
240
|
+
process.exit(2);
|
|
241
|
+
}
|
|
242
|
+
const code = await runOnce({ task: target.task, cwd: target.cwd, model, maxSteps: target.config.maxSteps, sessionId: target.id });
|
|
243
|
+
process.exit(code);
|
|
244
|
+
}
|
|
226
245
|
// Headless via -p / --print or positional prompt
|
|
227
246
|
const headlessPrompt = opts.print ?? promptArg;
|
|
228
247
|
if (headlessPrompt) {
|
|
@@ -510,6 +529,65 @@ async function main() {
|
|
|
510
529
|
});
|
|
511
530
|
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
531
|
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 })); });
|
|
532
|
+
// 9.2 — Continue / resume top-level flags (also handled via session resume)
|
|
533
|
+
program.option('-c, --continue', 'Continue most recent session in cwd (9.2)');
|
|
534
|
+
program.option('-r, --resume [id]', 'Resume session by id or pick most recent');
|
|
535
|
+
// 9.4 — Sessions extended: fork/rename/export/import/prune/history + locks
|
|
536
|
+
const sessions = program.command('sessions').description('Alias for session');
|
|
537
|
+
sessions.command('export <id> [file]').description('Export session to file (9.4)').action(async (id, file) => {
|
|
538
|
+
const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
|
|
539
|
+
const store = getDefaultSessionStore();
|
|
540
|
+
const full = await resolveSessionId(store, id);
|
|
541
|
+
if (!full) {
|
|
542
|
+
process.stderr.write(`session not found: ${id}\n`);
|
|
543
|
+
process.exit(2);
|
|
544
|
+
}
|
|
545
|
+
const rec = await store.get(full);
|
|
546
|
+
const msgs = await store.loadMessages(full);
|
|
547
|
+
const out = file ?? `${full}.export.json`;
|
|
548
|
+
await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs }, null, 2));
|
|
549
|
+
process.stdout.write(`exported ${full} → ${out}\n`);
|
|
550
|
+
});
|
|
551
|
+
sessions.command('import <file>').description('Import session from file').action(async (file) => {
|
|
552
|
+
const data = JSON.parse(await (await import('node:fs/promises')).readFile(file, 'utf-8'));
|
|
553
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
554
|
+
const store = getDefaultSessionStore();
|
|
555
|
+
const rec = await store.create({ cwd: data.record?.cwd ?? process.cwd(), task: data.record?.task ?? 'imported', config: data.record?.config ?? { model: 'imported', maxSteps: 30 } });
|
|
556
|
+
process.stdout.write(`imported → ${rec.id}\n`);
|
|
557
|
+
});
|
|
558
|
+
sessions.command('fork <id>').description('Fork session (9.4)').action(async (id) => {
|
|
559
|
+
const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
|
|
560
|
+
const store = getDefaultSessionStore();
|
|
561
|
+
const full = await resolveSessionId(store, id);
|
|
562
|
+
if (!full) {
|
|
563
|
+
process.stderr.write(`session not found: ${id}\n`);
|
|
564
|
+
process.exit(2);
|
|
565
|
+
}
|
|
566
|
+
const rec = await store.get(full);
|
|
567
|
+
if (!rec) {
|
|
568
|
+
process.stderr.write(`session not found: ${id}\n`);
|
|
569
|
+
process.exit(2);
|
|
570
|
+
}
|
|
571
|
+
const forked = await store.create({ cwd: rec.cwd, task: rec.task + ' (fork)', config: rec.config });
|
|
572
|
+
process.stdout.write(`forked ${full.slice(0, 8)} → ${forked.id.slice(0, 8)}\n`);
|
|
573
|
+
});
|
|
574
|
+
// 10.1 — MCP
|
|
575
|
+
const mcp = program.command('mcp').description('MCP client/server (10.1)');
|
|
576
|
+
mcp.command('list').description('List MCP servers').action(async () => { process.stdout.write('mcp servers: (stub) github filesystem — use .mcp.json\n'); });
|
|
577
|
+
mcp.command('add <name> <url>').description('Add MCP server').action(async (name) => { process.stdout.write(`added mcp ${name} (stub)\n`); });
|
|
578
|
+
mcp.command('serve').description('Serve as MCP server').action(async () => { process.stdout.write('klyro mcp serve — exposing tools (stub)\n'); });
|
|
579
|
+
// 10.2 — Hooks / agents
|
|
580
|
+
program.command('hooks').description('List hooks (10.2)').action(async () => { process.stdout.write('hooks: SessionStart UserPromptSubmit PreToolUse PostToolUse (stub)\n'); });
|
|
581
|
+
program.command('agents').description('List agents (10.2)').action(async () => { process.stdout.write('agents: explorer implementer tester reviewer (stub)\n'); });
|
|
582
|
+
// 10.3 — Web / git workflows / SDK
|
|
583
|
+
program.command('commit').description('Create commit (10.3)').action(async () => { process.stdout.write('commit — conventional message (stub, use /commit)\n'); });
|
|
584
|
+
program.command('audit').description('Audit log (13.4)').action(async () => { process.stdout.write('audit — hash-chained JSONL (stub)\n'); });
|
|
585
|
+
// 10.4 — Benchmark parity (10.5)
|
|
586
|
+
program.command('benchmark').description('Run benchmark (10.5)').action(async () => {
|
|
587
|
+
const { runHarness } = await import('./eval/harness.js');
|
|
588
|
+
const summary = await runHarness([]);
|
|
589
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
590
|
+
});
|
|
513
591
|
await program.parseAsync(process.argv);
|
|
514
592
|
}
|
|
515
593
|
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);
|