klyro 0.1.4 → 0.1.6

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 (78) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +1 -1
  2. package/dist/agent/anthropic-adapter.js +22 -20
  3. package/dist/agent/provider-adapter.js +30 -7
  4. package/dist/agent/registry.js +24 -7
  5. package/dist/agent/retry.js +54 -9
  6. package/dist/agent/runtime.d.ts +45 -1
  7. package/dist/agent/runtime.js +236 -23
  8. package/dist/chat.d.ts +3 -3
  9. package/dist/chat.js +137 -39
  10. package/dist/cli/auth.d.ts +9 -0
  11. package/dist/cli/auth.js +84 -0
  12. package/dist/cli/completion.d.ts +6 -0
  13. package/dist/cli/completion.js +66 -0
  14. package/dist/cli/config.d.ts +44 -0
  15. package/dist/cli/config.js +428 -0
  16. package/dist/cli/doctor.d.ts +8 -0
  17. package/dist/cli/doctor.js +136 -0
  18. package/dist/cli/errors.d.ts +5 -0
  19. package/dist/cli/errors.js +37 -0
  20. package/dist/cli/markdown.d.ts +9 -0
  21. package/dist/cli/markdown.js +77 -0
  22. package/dist/cli/repl.js +113 -5
  23. package/dist/cli/run.d.ts +9 -0
  24. package/dist/cli/run.js +97 -2
  25. package/dist/cli/slash/parser.d.ts +10 -0
  26. package/dist/cli/slash/parser.js +6 -1
  27. package/dist/cli/trace.d.ts +7 -0
  28. package/dist/cli/trace.js +63 -0
  29. package/dist/cli/update.d.ts +6 -0
  30. package/dist/cli/update.js +70 -0
  31. package/dist/context/system-prompt.d.ts +14 -0
  32. package/dist/context/system-prompt.js +47 -0
  33. package/dist/events/bus.d.ts +16 -0
  34. package/dist/events/bus.js +28 -0
  35. package/dist/events/catalog.d.ts +136 -0
  36. package/dist/events/catalog.js +5 -0
  37. package/dist/index.js +335 -22
  38. package/dist/persistence/session.d.ts +12 -0
  39. package/dist/persistence/session.js +45 -0
  40. package/dist/policy/engine.d.ts +11 -1
  41. package/dist/policy/engine.js +85 -2
  42. package/dist/providers/model-info.d.ts +18 -0
  43. package/dist/providers/model-info.js +18 -0
  44. package/dist/providers.js +3 -2
  45. package/dist/renderers/json.d.ts +11 -0
  46. package/dist/renderers/json.js +14 -0
  47. package/dist/renderers/terminal.d.ts +9 -0
  48. package/dist/renderers/terminal.js +41 -0
  49. package/dist/repl.js +52 -15
  50. package/dist/shared/errors.d.ts +18 -0
  51. package/dist/shared/errors.js +33 -0
  52. package/dist/shared/index.d.ts +2 -0
  53. package/dist/shared/index.js +2 -0
  54. package/dist/shared/types.d.ts +19 -0
  55. package/dist/shared/types.js +11 -0
  56. package/dist/tools/fs/read-file.d.ts +1 -31
  57. package/dist/tools/fs/read-file.js +45 -17
  58. package/dist/tools/fs/read-history.d.ts +3 -0
  59. package/dist/tools/fs/read-history.js +13 -0
  60. package/dist/tools/fs/write-file.d.ts +1 -4
  61. package/dist/tools/fs/write-file.js +35 -1
  62. package/dist/tools/normalize.d.ts +1 -1
  63. package/dist/tools/shell/shell-exec.d.ts +1 -26
  64. package/dist/tools/shell/shell-exec.js +121 -17
  65. package/dist/tools/types.d.ts +25 -0
  66. package/dist/trace/writer.d.ts +13 -0
  67. package/dist/trace/writer.js +53 -0
  68. package/dist/tui/app.d.ts +1 -15
  69. package/dist/tui/app.js +203 -22
  70. package/dist/tui/approval.js +25 -4
  71. package/dist/tui/approval.test.js +1 -1
  72. package/dist/tui/snapshot.test.js +2 -2
  73. package/dist/tui/status.js +5 -1
  74. package/dist/util/log.d.ts +12 -0
  75. package/dist/util/log.js +75 -0
  76. package/dist/verification/auto.d.ts +8 -0
  77. package/dist/verification/auto.js +52 -0
  78. package/package.json +3 -2
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 2.2 — klyro login / logout / aliases
3
+ * Stores masked key → ~/.klyro/credentials.json 0600
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import * as readline from 'node:readline/promises';
9
+ import { stdin, stdout } from 'node:process';
10
+ function credPath() {
11
+ const home = os.homedir() || process.cwd();
12
+ return path.join(home, '.klyro', 'credentials.json');
13
+ }
14
+ export async function runLogin() {
15
+ const rl = readline.createInterface({ input: stdin, output: stdout });
16
+ try {
17
+ const provider = (await rl.question('Provider (anthropic/openai) [openai]: ')) || 'openai';
18
+ const key = await rl.question('API key (input hidden, paste): ');
19
+ if (!key.trim()) {
20
+ process.stderr.write('No key provided\n');
21
+ return 2;
22
+ }
23
+ const creds = {};
24
+ try {
25
+ const raw = await fs.readFile(credPath(), 'utf-8');
26
+ Object.assign(creds, JSON.parse(raw));
27
+ }
28
+ catch { /* ignore */ }
29
+ creds[provider] = key.trim();
30
+ await fs.mkdir(path.dirname(credPath()), { recursive: true });
31
+ await fs.writeFile(credPath(), JSON.stringify(creds, null, 2), { mode: 0o600 });
32
+ try {
33
+ await fs.chmod(credPath(), 0o600);
34
+ }
35
+ catch { /* ignore on Windows */ }
36
+ process.stdout.write(`Saved ${provider} key to ${credPath()} (0600)\n`);
37
+ return 0;
38
+ }
39
+ finally {
40
+ rl.close();
41
+ }
42
+ }
43
+ export async function runLogout(provider) {
44
+ try {
45
+ const raw = await fs.readFile(credPath(), 'utf-8');
46
+ const creds = JSON.parse(raw);
47
+ if (provider) {
48
+ delete creds[provider];
49
+ await fs.writeFile(credPath(), JSON.stringify(creds, null, 2), 'utf-8');
50
+ process.stdout.write(`Removed ${provider} key\n`);
51
+ }
52
+ else {
53
+ await fs.unlink(credPath());
54
+ process.stdout.write('Removed all credentials\n');
55
+ }
56
+ return 0;
57
+ }
58
+ catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ process.stderr.write(`No credentials to remove: ${msg}\n`);
61
+ return 1;
62
+ }
63
+ }
64
+ export function getStoredKey(provider) {
65
+ try {
66
+ const raw = require('node:fs').readFileSync(credPath(), 'utf-8');
67
+ const creds = JSON.parse(raw);
68
+ return creds[provider];
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ // Model aliases 2.2
75
+ export const MODEL_ALIASES = {
76
+ sonnet: 'claude-3-5-sonnet-20240620',
77
+ opus: 'claude-3-opus-20240229',
78
+ haiku: 'claude-3-haiku-20240307',
79
+ gpt: 'gpt-4o',
80
+ local: 'llama3.2',
81
+ };
82
+ export function resolveAlias(model) {
83
+ return MODEL_ALIASES[model] ?? model;
84
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 1.2 — klyro completion
3
+ * Generates shell completion scripts for bash/zsh/fish/powershell.
4
+ */
5
+ export declare function getCompletionScript(shell: string): string | null;
6
+ export declare function runCompletion(shell?: string): Promise<number>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 1.2 — klyro completion
3
+ * Generates shell completion scripts for bash/zsh/fish/powershell.
4
+ */
5
+ const COMMANDS = ['tui', 'run', 'chat', 'config', 'doctor', 'completion', 'update', 'eval', 'session', 'resume', 'help', 'version'];
6
+ function bashScript() {
7
+ return `# klyro bash completion
8
+ _klyro_complete() {
9
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
10
+ local cmds="${COMMANDS.join(' ')}"
11
+ COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
12
+ }
13
+ complete -F _klyro_complete klyro
14
+ complete -F _klyro_complete ky
15
+ `;
16
+ }
17
+ function zshScript() {
18
+ return `#compdef klyro ky
19
+ _klyro() {
20
+ local -a completions
21
+ completions=(${COMMANDS.map((c) => `'${c}'`).join(' ')})
22
+ _describe 'klyro commands' completions
23
+ }
24
+ compdef _klyro klyro ky
25
+ `;
26
+ }
27
+ function fishScript() {
28
+ return `# klyro fish completion
29
+ ${COMMANDS.map((c) => `complete -c klyro -f -a ${c}`).join('\n')}
30
+ complete -c ky -f -a "${COMMANDS.join(' ')}"
31
+ `;
32
+ }
33
+ function powershellScript() {
34
+ return `# klyro powershell completion
35
+ Register-ArgumentCompleter -Native -CommandName klyro,ky -ScriptBlock {
36
+ param($wordToComplete, $commandAst, $cursorPosition)
37
+ $cmds = @(${COMMANDS.map((c) => `'${c}'`).join(', ')})
38
+ $cmds | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
39
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
40
+ }
41
+ }
42
+ `;
43
+ }
44
+ export function getCompletionScript(shell) {
45
+ switch (shell) {
46
+ case 'bash': return bashScript();
47
+ case 'zsh': return zshScript();
48
+ case 'fish': return fishScript();
49
+ case 'powershell':
50
+ case 'pwsh': return powershellScript();
51
+ default: return null;
52
+ }
53
+ }
54
+ export async function runCompletion(shell) {
55
+ if (!shell) {
56
+ process.stderr.write('Usage: klyro completion <bash|zsh|fish|powershell>\n');
57
+ return 2;
58
+ }
59
+ const script = getCompletionScript(shell);
60
+ if (!script) {
61
+ process.stderr.write(`Unknown shell: ${shell} (expected bash|zsh|fish|powershell)\n`);
62
+ return 2;
63
+ }
64
+ process.stdout.write(script);
65
+ return 0;
66
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * LEVEL 1 — klyro config (1.3)
3
+ * Precedence: flags → env → .klyro/settings.local.json → .klyro/settings.json → ~/.klyro/settings.json → defaults
4
+ * JSONC-tolerant, Zod schema, exit 3 on invalid.
5
+ */
6
+ import { z } from 'zod';
7
+ export declare const ConfigSchema: z.ZodObject<{
8
+ model: z.ZodOptional<z.ZodString>;
9
+ provider: z.ZodOptional<z.ZodEnum<{
10
+ anthropic: "anthropic";
11
+ openai: "openai";
12
+ }>>;
13
+ baseUrl: z.ZodOptional<z.ZodString>;
14
+ apiKey: z.ZodOptional<z.ZodString>;
15
+ baseURL: z.ZodOptional<z.ZodString>;
16
+ api_key: z.ZodOptional<z.ZodString>;
17
+ 'model.default': z.ZodOptional<z.ZodString>;
18
+ 'model.small': z.ZodOptional<z.ZodString>;
19
+ }, z.core.$loose>;
20
+ export type KlyroConfig = z.infer<typeof ConfigSchema>;
21
+ export declare function getConfigDir(): string;
22
+ export declare function getConfigPath(): string;
23
+ export declare function getConfigSearchPaths(cwd?: string): Array<{
24
+ path: string;
25
+ layer: string;
26
+ exists: boolean;
27
+ }>;
28
+ declare function stripJsonComments(text: string): string;
29
+ declare function getByPath(obj: Record<string, unknown>, dotted: string): unknown;
30
+ declare function setByPath(obj: Record<string, unknown>, dotted: string, value: unknown): void;
31
+ declare function deleteByPath(obj: Record<string, unknown>, dotted: string): boolean;
32
+ declare function parseValue(raw: string): unknown;
33
+ export declare function loadConfig(): Promise<Record<string, unknown>>;
34
+ export declare function loadMergedConfig(cwd?: string, flags?: Record<string, unknown>): Promise<Record<string, unknown>>;
35
+ export declare function saveConfig(obj: Record<string, unknown>): Promise<void>;
36
+ export declare function runConfig(args: string[]): Promise<number>;
37
+ export declare const _helpers: {
38
+ getByPath: typeof getByPath;
39
+ setByPath: typeof setByPath;
40
+ deleteByPath: typeof deleteByPath;
41
+ parseValue: typeof parseValue;
42
+ stripJsonComments: typeof stripJsonComments;
43
+ };
44
+ export {};
@@ -0,0 +1,428 @@
1
+ /**
2
+ * LEVEL 1 — klyro config (1.3)
3
+ * Precedence: flags → env → .klyro/settings.local.json → .klyro/settings.json → ~/.klyro/settings.json → defaults
4
+ * JSONC-tolerant, Zod schema, exit 3 on invalid.
5
+ */
6
+ import * as fs from 'node:fs/promises';
7
+ import * as fsSync from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as os from 'node:os';
10
+ import { z } from 'zod';
11
+ import { KlyroError } from '../shared/errors.js';
12
+ // --- Zod schema (Appendix A shape, L1 keys active, passthrough for future) ---
13
+ export const ConfigSchema = z
14
+ .object({
15
+ model: z.string().optional(),
16
+ provider: z.enum(['openai', 'anthropic']).optional(),
17
+ baseUrl: z.string().optional(),
18
+ apiKey: z.string().optional(),
19
+ baseURL: z.string().optional(),
20
+ api_key: z.string().optional(),
21
+ // Global flags persisted
22
+ 'model.default': z.string().optional(),
23
+ 'model.small': z.string().optional(),
24
+ })
25
+ .passthrough();
26
+ // --- Paths ---
27
+ export function getConfigDir() {
28
+ if (process.env.KLYRO_CONFIG_DIR)
29
+ return process.env.KLYRO_CONFIG_DIR;
30
+ const home = os.homedir();
31
+ if (home)
32
+ return path.join(home, '.klyro');
33
+ return path.join(process.cwd(), '.klyro');
34
+ }
35
+ export function getConfigPath() {
36
+ if (process.env.KLYRO_CONFIG)
37
+ return process.env.KLYRO_CONFIG;
38
+ // New canonical path per spec
39
+ return path.join(getConfigDir(), 'settings.json');
40
+ }
41
+ function getLegacyConfigPath() {
42
+ if (process.env.KLYRO_CONFIG)
43
+ return process.env.KLYRO_CONFIG;
44
+ const home = os.homedir();
45
+ if (home)
46
+ return path.join(home, '.klyro', 'config.json');
47
+ return path.join(process.cwd(), '.klyro', 'config.json');
48
+ }
49
+ export function getConfigSearchPaths(cwd = process.cwd()) {
50
+ const homeSettings = path.join(getConfigDir(), 'settings.json');
51
+ const projSettings = path.join(cwd, '.klyro', 'settings.json');
52
+ const projLocal = path.join(cwd, '.klyro', 'settings.local.json');
53
+ const legacy = getLegacyConfigPath();
54
+ const paths = [
55
+ { path: homeSettings, layer: '~/.klyro/settings.json' },
56
+ { path: legacy, layer: '~/.klyro/config.json (legacy)' },
57
+ { path: projSettings, layer: '.klyro/settings.json' },
58
+ { path: projLocal, layer: '.klyro/settings.local.json' },
59
+ ];
60
+ return paths.map((p) => ({ ...p, exists: fsSync.existsSync(p.path) }));
61
+ }
62
+ // --- JSONC ---
63
+ function stripJsonComments(text) {
64
+ let out = '';
65
+ let inString = false;
66
+ let escaped = false;
67
+ let inSingleLineComment = false;
68
+ let inMultiLineComment = false;
69
+ for (let i = 0; i < text.length; i++) {
70
+ const ch = text[i];
71
+ const nxt = text[i + 1];
72
+ if (inSingleLineComment) {
73
+ if (ch === '\n') {
74
+ inSingleLineComment = false;
75
+ out += ch;
76
+ }
77
+ continue;
78
+ }
79
+ if (inMultiLineComment) {
80
+ if (ch === '*' && nxt === '/') {
81
+ inMultiLineComment = false;
82
+ i++;
83
+ }
84
+ continue;
85
+ }
86
+ if (inString) {
87
+ out += ch;
88
+ if (escaped)
89
+ escaped = false;
90
+ else if (ch === '\\')
91
+ escaped = true;
92
+ else if (ch === '"')
93
+ inString = false;
94
+ continue;
95
+ }
96
+ if (ch === '"') {
97
+ inString = true;
98
+ out += ch;
99
+ continue;
100
+ }
101
+ if (ch === '/' && nxt === '/') {
102
+ inSingleLineComment = true;
103
+ i++;
104
+ continue;
105
+ }
106
+ if (ch === '/' && nxt === '*') {
107
+ inMultiLineComment = true;
108
+ i++;
109
+ continue;
110
+ }
111
+ out += ch;
112
+ }
113
+ return out;
114
+ }
115
+ function parseJsonc(raw, filePath) {
116
+ const stripped = stripJsonComments(raw);
117
+ try {
118
+ const parsed = JSON.parse(stripped);
119
+ if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
120
+ throw new KlyroError('CONFIG_INVALID', `config is not a JSON object: ${filePath}`, {
121
+ hint: 'Fix the file to be a JSON object, e.g. {"model":"gpt-4o-mini"}',
122
+ exitCode: 3,
123
+ });
124
+ }
125
+ return parsed;
126
+ }
127
+ catch (err) {
128
+ if (err instanceof KlyroError)
129
+ throw err;
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ throw new KlyroError('CONFIG_INVALID', `invalid JSON in ${filePath}: ${msg}`, {
132
+ hint: `Check ${filePath} for trailing commas or comments — JSONC is allowed but must be valid`,
133
+ exitCode: 3,
134
+ });
135
+ }
136
+ }
137
+ function validateConfig(obj, filePath) {
138
+ const res = ConfigSchema.safeParse(obj);
139
+ if (!res.success) {
140
+ const issues = res.error.issues.map((iss) => `${iss.path.join('.')}: ${iss.message}`).join('; ');
141
+ throw new KlyroError('CONFIG_INVALID', `invalid config ${filePath}: ${issues}`, {
142
+ hint: 'Fix the key per expected type, e.g. "model": "gpt-4o-mini"',
143
+ exitCode: 3,
144
+ });
145
+ }
146
+ return res.data;
147
+ }
148
+ // --- Helpers for dotted paths (unchanged) ---
149
+ function getByPath(obj, dotted) {
150
+ const parts = dotted.split('.');
151
+ let cur = obj;
152
+ for (const p of parts) {
153
+ if (cur == null || typeof cur !== 'object')
154
+ return undefined;
155
+ cur = cur[p];
156
+ }
157
+ return cur;
158
+ }
159
+ function setByPath(obj, dotted, value) {
160
+ const parts = dotted.split('.');
161
+ let cur = obj;
162
+ for (let i = 0; i < parts.length - 1; i++) {
163
+ const p = parts[i];
164
+ const nxt = cur[p];
165
+ if (nxt == null || typeof nxt !== 'object' || Array.isArray(nxt))
166
+ cur[p] = {};
167
+ cur = cur[p];
168
+ }
169
+ cur[parts[parts.length - 1]] = value;
170
+ }
171
+ function deleteByPath(obj, dotted) {
172
+ const parts = dotted.split('.');
173
+ let cur = obj;
174
+ for (let i = 0; i < parts.length - 1; i++) {
175
+ const p = parts[i];
176
+ const nxt = cur[p];
177
+ if (nxt == null || typeof nxt !== 'object')
178
+ return false;
179
+ cur = nxt;
180
+ }
181
+ const last = parts[parts.length - 1];
182
+ if (!(last in cur))
183
+ return false;
184
+ delete cur[last];
185
+ return true;
186
+ }
187
+ function parseValue(raw) {
188
+ const trimmed = raw.trim();
189
+ if (trimmed === '')
190
+ return '';
191
+ try {
192
+ return JSON.parse(trimmed);
193
+ }
194
+ catch {
195
+ return raw;
196
+ }
197
+ }
198
+ // --- Single-file load (backward compat) ---
199
+ export async function loadConfig() {
200
+ const p = getConfigPath();
201
+ const legacy = getLegacyConfigPath();
202
+ // Try canonical first, then legacy
203
+ for (const file of [p, legacy]) {
204
+ try {
205
+ const raw = await fs.readFile(file, 'utf-8');
206
+ const obj = parseJsonc(raw, file);
207
+ return validateConfig(obj, file);
208
+ }
209
+ catch (err) {
210
+ const e = err;
211
+ if (e.code === 'ENOENT')
212
+ continue;
213
+ if (err instanceof KlyroError)
214
+ throw err;
215
+ throw new KlyroError('CONFIG_INVALID', `cannot read ${file}: ${e.message}`, { exitCode: 3 });
216
+ }
217
+ }
218
+ return {};
219
+ }
220
+ // --- Merged load with 5-layer precedence ---
221
+ export async function loadMergedConfig(cwd = process.cwd(), flags = {}) {
222
+ const layers = [];
223
+ // 1) defaults (empty)
224
+ layers.push({});
225
+ // 2) ~/.klyro/settings.json
226
+ for (const file of [path.join(getConfigDir(), 'settings.json'), getLegacyConfigPath()]) {
227
+ try {
228
+ const raw = await fs.readFile(file, 'utf-8');
229
+ const obj = parseJsonc(raw, file);
230
+ layers.push(validateConfig(obj, file));
231
+ }
232
+ catch (err) {
233
+ const e = err;
234
+ if (e.code === 'ENOENT')
235
+ continue;
236
+ throw err;
237
+ }
238
+ }
239
+ // 3) .klyro/settings.json
240
+ try {
241
+ const raw = await fs.readFile(path.join(cwd, '.klyro', 'settings.json'), 'utf-8');
242
+ const obj = parseJsonc(raw, path.join(cwd, '.klyro/settings.json'));
243
+ layers.push(validateConfig(obj, path.join(cwd, '.klyro/settings.json')));
244
+ }
245
+ catch (err) {
246
+ const e = err;
247
+ if (e.code !== 'ENOENT')
248
+ throw err;
249
+ }
250
+ // 4) .klyro/settings.local.json
251
+ try {
252
+ const raw = await fs.readFile(path.join(cwd, '.klyro', 'settings.local.json'), 'utf-8');
253
+ const obj = parseJsonc(raw, path.join(cwd, '.klyro/settings.local.json'));
254
+ layers.push(validateConfig(obj, path.join(cwd, '.klyro/settings.local.json')));
255
+ }
256
+ catch (err) {
257
+ const e = err;
258
+ if (e.code !== 'ENOENT')
259
+ throw err;
260
+ }
261
+ // 5) env
262
+ const envLayer = {};
263
+ if (process.env.KLYRO_MODEL)
264
+ envLayer.model = process.env.KLYRO_MODEL;
265
+ if (process.env.KLYRO_PROVIDER)
266
+ envLayer.provider = process.env.KLYRO_PROVIDER;
267
+ if (process.env.KLYRO_BASE_URL)
268
+ envLayer.baseUrl = process.env.KLYRO_BASE_URL;
269
+ if (process.env.KLYRO_API_KEY)
270
+ envLayer.apiKey = process.env.KLYRO_API_KEY;
271
+ if (process.env.OPENAI_API_KEY)
272
+ envLayer.apiKey = process.env.OPENAI_API_KEY;
273
+ if (process.env.ANTHROPIC_API_KEY)
274
+ envLayer.apiKey = process.env.ANTHROPIC_API_KEY;
275
+ layers.push(envLayer);
276
+ // 6) flags (highest)
277
+ layers.push(flags);
278
+ // Merge shallow (dotted keys are already nested via setByPath, but we treat flat)
279
+ const merged = {};
280
+ for (const layer of layers) {
281
+ for (const [k, v] of Object.entries(layer)) {
282
+ if (v !== undefined)
283
+ setByPath(merged, k, v);
284
+ }
285
+ }
286
+ return merged;
287
+ }
288
+ export async function saveConfig(obj) {
289
+ const p = getConfigPath();
290
+ await fs.mkdir(path.dirname(p), { recursive: true });
291
+ const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
292
+ const data = JSON.stringify(validateConfig(obj, p), null, 2) + '\n';
293
+ await fs.writeFile(tmp, data, { mode: 0o600 });
294
+ try {
295
+ await fs.rename(tmp, p);
296
+ }
297
+ catch (err) {
298
+ await fs.unlink(tmp).catch(() => undefined);
299
+ throw err;
300
+ }
301
+ try {
302
+ await fs.chmod(p, 0o600);
303
+ }
304
+ catch { /* ignore on Windows */ }
305
+ }
306
+ export async function runConfig(args) {
307
+ const [cmd, ...rest] = args;
308
+ const cfgPath = getConfigPath();
309
+ if (!cmd || cmd === 'list') {
310
+ try {
311
+ const cfg = await loadMergedConfig(process.cwd(), {});
312
+ if (Object.keys(cfg).length === 0) {
313
+ process.stdout.write(`# ${cfgPath} (empty)\n# No config yet. Use: klyro config set <key> <value>\n`);
314
+ }
315
+ else {
316
+ process.stdout.write(JSON.stringify(cfg, null, 2) + '\n');
317
+ }
318
+ return 0;
319
+ }
320
+ catch (err) {
321
+ if (err instanceof KlyroError) {
322
+ process.stderr.write(`✖ ${err.message}\n hint: ${err.hint ?? ''}\n`);
323
+ return err.exitCode;
324
+ }
325
+ throw err;
326
+ }
327
+ }
328
+ if (cmd === 'path') {
329
+ const paths = getConfigSearchPaths(process.cwd());
330
+ for (const p of paths) {
331
+ const status = p.exists ? 'exists' : 'missing';
332
+ process.stdout.write(`${p.layer}: ${p.path} [${status}]\n`);
333
+ }
334
+ // Also show env
335
+ process.stdout.write(`env: KLYRO_MODEL=${process.env.KLYRO_MODEL ?? '(not set)'} KLYRO_PROVIDER=${process.env.KLYRO_PROVIDER ?? '(not set)'}\n`);
336
+ return 0;
337
+ }
338
+ if (cmd === 'edit') {
339
+ const p = getConfigPath();
340
+ // Try to open $EDITOR
341
+ const editor = process.env.EDITOR || process.env.VISUAL;
342
+ if (editor) {
343
+ const { spawn } = await import('node:child_process');
344
+ const child = spawn(editor, [p], { stdio: 'inherit', shell: true });
345
+ const code = await new Promise((res) => child.on('close', (c) => res(typeof c === 'number' ? c : 1)));
346
+ return code;
347
+ }
348
+ process.stderr.write('klyro: config edit — open ' + p + ' in $EDITOR\n');
349
+ process.stdout.write(p + '\n');
350
+ return 0;
351
+ }
352
+ if (cmd === 'get') {
353
+ const key = rest[0];
354
+ if (!key) {
355
+ process.stderr.write('klyro: config get <key> — missing key\n');
356
+ return 2;
357
+ }
358
+ try {
359
+ const cfg = await loadMergedConfig(process.cwd(), {});
360
+ const val = getByPath(cfg, key);
361
+ if (val === undefined) {
362
+ process.stderr.write(`klyro: key not found: ${key}\n`);
363
+ return 1;
364
+ }
365
+ if (typeof val === 'string')
366
+ process.stdout.write(val + '\n');
367
+ else
368
+ process.stdout.write(JSON.stringify(val, null, 2) + '\n');
369
+ return 0;
370
+ }
371
+ catch (err) {
372
+ if (err instanceof KlyroError) {
373
+ process.stderr.write(`✖ ${err.message}\n hint: ${err.hint ?? ''}\n`);
374
+ return err.exitCode;
375
+ }
376
+ throw err;
377
+ }
378
+ }
379
+ if (cmd === 'set') {
380
+ const key = rest[0];
381
+ const rawVal = rest.slice(1).join(' ');
382
+ if (!key) {
383
+ process.stderr.write('klyro: config set <key> <value> — missing key\n');
384
+ return 2;
385
+ }
386
+ if (rawVal === '' && rest.length < 2) {
387
+ process.stderr.write('klyro: config set <key> <value> — missing value\n');
388
+ return 2;
389
+ }
390
+ try {
391
+ const cfg = await loadConfig();
392
+ const val = parseValue(rawVal);
393
+ setByPath(cfg, key, val);
394
+ // Validate before save
395
+ validateConfig(cfg, cfgPath);
396
+ await saveConfig(cfg);
397
+ process.stdout.write(`set ${key} = ${typeof val === 'string' ? val : JSON.stringify(val)}\n`);
398
+ return 0;
399
+ }
400
+ catch (err) {
401
+ if (err instanceof KlyroError) {
402
+ process.stderr.write(`✖ ${err.message}\n hint: ${err.hint ?? ''}\n`);
403
+ return err.exitCode;
404
+ }
405
+ throw err;
406
+ }
407
+ }
408
+ if (cmd === 'unset' || cmd === 'delete' || cmd === 'rm') {
409
+ const key = rest[0];
410
+ if (!key) {
411
+ process.stderr.write(`klyro: config ${cmd} <key> — missing key\n`);
412
+ return 2;
413
+ }
414
+ const cfg = await loadConfig();
415
+ const ok = deleteByPath(cfg, key);
416
+ if (!ok) {
417
+ process.stderr.write(`klyro: key not found: ${key}\n`);
418
+ return 1;
419
+ }
420
+ await saveConfig(cfg);
421
+ process.stdout.write(`unset ${key}\n`);
422
+ return 0;
423
+ }
424
+ process.stderr.write(`klyro: unknown config command: ${cmd}\n`);
425
+ process.stderr.write('Usage: klyro config [list|get <key>|set <key> <value>|unset <key>|path|edit]\n');
426
+ return 2;
427
+ }
428
+ export const _helpers = { getByPath, setByPath, deleteByPath, parseValue, stripJsonComments: stripJsonComments };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * LEVEL 1 — klyro doctor
3
+ * Runs quick diagnostics: node version, config, provider reachability,
4
+ * persistence dir, git, tools.
5
+ */
6
+ export declare function runDoctor(opts?: {
7
+ json?: boolean;
8
+ }): Promise<number>;