klyro 0.1.1

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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/agent/anthropic-adapter.d.ts +75 -0
  4. package/dist/agent/anthropic-adapter.js +297 -0
  5. package/dist/agent/message.d.ts +40 -0
  6. package/dist/agent/message.js +21 -0
  7. package/dist/agent/observation.d.ts +47 -0
  8. package/dist/agent/observation.js +53 -0
  9. package/dist/agent/provider-adapter.d.ts +101 -0
  10. package/dist/agent/provider-adapter.js +254 -0
  11. package/dist/agent/registry.d.ts +42 -0
  12. package/dist/agent/registry.js +86 -0
  13. package/dist/agent/retry.d.ts +34 -0
  14. package/dist/agent/retry.js +91 -0
  15. package/dist/agent/runtime.d.ts +130 -0
  16. package/dist/agent/runtime.js +221 -0
  17. package/dist/agent/worker-spawner.d.ts +41 -0
  18. package/dist/agent/worker-spawner.js +76 -0
  19. package/dist/chat.d.ts +39 -0
  20. package/dist/chat.js +235 -0
  21. package/dist/chat.js.map +1 -0
  22. package/dist/cli/eval.d.ts +75 -0
  23. package/dist/cli/eval.js +190 -0
  24. package/dist/cli/repl.d.ts +15 -0
  25. package/dist/cli/repl.js +194 -0
  26. package/dist/cli/run.d.ts +67 -0
  27. package/dist/cli/run.js +198 -0
  28. package/dist/cli/slash/parser.d.ts +39 -0
  29. package/dist/cli/slash/parser.js +47 -0
  30. package/dist/context/level6.d.ts +45 -0
  31. package/dist/context/level6.js +231 -0
  32. package/dist/context/level7.d.ts +84 -0
  33. package/dist/context/level7.js +156 -0
  34. package/dist/context/project-map.d.ts +48 -0
  35. package/dist/context/project-map.js +438 -0
  36. package/dist/context/repo-map.d.ts +30 -0
  37. package/dist/context/repo-map.js +168 -0
  38. package/dist/context/selector.d.ts +22 -0
  39. package/dist/context/selector.js +37 -0
  40. package/dist/context/snippets.d.ts +17 -0
  41. package/dist/context/snippets.js +15 -0
  42. package/dist/context/tokenizer.d.ts +49 -0
  43. package/dist/context/tokenizer.js +97 -0
  44. package/dist/eval/harness.d.ts +48 -0
  45. package/dist/eval/harness.js +149 -0
  46. package/dist/eval/tasks.d.ts +6 -0
  47. package/dist/eval/tasks.js +98 -0
  48. package/dist/index.d.ts +14 -0
  49. package/dist/index.js +128 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/persistence/audit.d.ts +75 -0
  52. package/dist/persistence/audit.js +15 -0
  53. package/dist/persistence/store.d.ts +67 -0
  54. package/dist/persistence/store.js +106 -0
  55. package/dist/policy/approval.d.ts +35 -0
  56. package/dist/policy/approval.js +54 -0
  57. package/dist/policy/engine.d.ts +76 -0
  58. package/dist/policy/engine.js +163 -0
  59. package/dist/policy/path-guard.d.ts +30 -0
  60. package/dist/policy/path-guard.js +100 -0
  61. package/dist/policy/secret-redactor.d.ts +19 -0
  62. package/dist/policy/secret-redactor.js +52 -0
  63. package/dist/providers.d.ts +23 -0
  64. package/dist/providers.js +83 -0
  65. package/dist/repl.d.ts +9 -0
  66. package/dist/repl.js +170 -0
  67. package/dist/repl.js.map +1 -0
  68. package/dist/tools/fs/edit-file.d.ts +48 -0
  69. package/dist/tools/fs/edit-file.js +84 -0
  70. package/dist/tools/fs/list-dir.d.ts +40 -0
  71. package/dist/tools/fs/list-dir.js +72 -0
  72. package/dist/tools/fs/read-file.d.ts +66 -0
  73. package/dist/tools/fs/read-file.js +75 -0
  74. package/dist/tools/fs/write-file.d.ts +24 -0
  75. package/dist/tools/fs/write-file.js +48 -0
  76. package/dist/tools/git/git-diff.d.ts +35 -0
  77. package/dist/tools/git/git-diff.js +68 -0
  78. package/dist/tools/git/git-status.d.ts +29 -0
  79. package/dist/tools/git/git-status.js +57 -0
  80. package/dist/tools/normalize.d.ts +31 -0
  81. package/dist/tools/normalize.js +88 -0
  82. package/dist/tools/registry.d.ts +23 -0
  83. package/dist/tools/registry.js +87 -0
  84. package/dist/tools/schema.d.ts +29 -0
  85. package/dist/tools/schema.js +183 -0
  86. package/dist/tools/search/dependencies.d.ts +44 -0
  87. package/dist/tools/search/dependencies.js +188 -0
  88. package/dist/tools/search/glob.d.ts +26 -0
  89. package/dist/tools/search/glob.js +88 -0
  90. package/dist/tools/search/grep.d.ts +52 -0
  91. package/dist/tools/search/grep.js +125 -0
  92. package/dist/tools/search/recent-files.d.ts +33 -0
  93. package/dist/tools/search/recent-files.js +100 -0
  94. package/dist/tools/search/search-files.d.ts +39 -0
  95. package/dist/tools/search/search-files.js +110 -0
  96. package/dist/tools/shell/shell-exec.d.ts +60 -0
  97. package/dist/tools/shell/shell-exec.js +133 -0
  98. package/dist/tools/types.d.ts +54 -0
  99. package/dist/tools/types.js +15 -0
  100. package/dist/tools/verify/run-verify.d.ts +60 -0
  101. package/dist/tools/verify/run-verify.js +115 -0
  102. package/dist/tui/app.d.ts +27 -0
  103. package/dist/tui/app.js +79 -0
  104. package/dist/tui/app.test.d.ts +1 -0
  105. package/dist/tui/app.test.js +77 -0
  106. package/dist/tui/status.d.ts +21 -0
  107. package/dist/tui/status.js +13 -0
  108. package/dist/tui/status.test.d.ts +1 -0
  109. package/dist/tui/status.test.js +39 -0
  110. package/dist/tui/transcript.d.ts +40 -0
  111. package/dist/tui/transcript.js +28 -0
  112. package/dist/tui/transcript.test.d.ts +1 -0
  113. package/dist/tui/transcript.test.js +71 -0
  114. package/dist/verification/detect.d.ts +29 -0
  115. package/dist/verification/detect.js +85 -0
  116. package/dist/verification/engine.d.ts +25 -0
  117. package/dist/verification/engine.js +57 -0
  118. package/package.json +58 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * `klyro` (no args) — the TUI REPL.
3
+ *
4
+ * Loads config from env, builds the agent runtime deps, mounts the
5
+ * Ink app, and bridges RuntimeEvents to the app's transcript/status
6
+ * via the global hooks installed by App.useEffect.
7
+ */
8
+ import React from 'react';
9
+ import { render } from 'ink';
10
+ import { App } from '../tui/app.js';
11
+ import { httpChatAdapter } from '../agent/provider-adapter.js';
12
+ import { run } from '../agent/runtime.js';
13
+ import { builtinRegistry } from '../tools/registry.js';
14
+ import { builtinRules, DEFAULT_POLICY_CONFIG, PolicyEngine } from '../policy/engine.js';
15
+ import { buildLevel6Context } from '../context/level6.js';
16
+ import { DenyAllApprovalPrompt, StdinApprovalPrompt } from '../policy/approval.js';
17
+ function readEnv(name, fallback) {
18
+ const v = process.env[name];
19
+ return v && v.length > 0 ? v : fallback;
20
+ }
21
+ export async function startRepl(opts = {}) {
22
+ const baseUrl = readEnv('KLYRO_BASE_URL');
23
+ const apiKey = readEnv('KLYRO_API_KEY');
24
+ const model = opts.model ?? readEnv('KLYRO_MODEL');
25
+ if (!baseUrl || !apiKey || !model) {
26
+ process.stderr.write('klyro: KLYRO_BASE_URL, KLYRO_API_KEY, and KLYRO_MODEL must be set\n');
27
+ return 2;
28
+ }
29
+ const cwd = opts.cwd ?? process.cwd();
30
+ const registry = builtinRegistry();
31
+ const policy = new PolicyEngine(builtinRules(), DEFAULT_POLICY_CONFIG);
32
+ const adapter = httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: 60_000 });
33
+ const approval = opts.nonInteractive ? new DenyAllApprovalPrompt() : new StdinApprovalPrompt();
34
+ const ctxBlock = await buildLevel6Context({ cwd });
35
+ const ctxPrefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
36
+ const systemPromptFn = (_ctx) => {
37
+ const base = opts.systemPrompt ?? 'You are Klyro, an autonomous coding harness. Solve the user\'s task using the available tools.';
38
+ const t = _ctx.telemetry ? '\n\n' + _ctx.telemetry : '';
39
+ return base + ctxPrefix + t;
40
+ };
41
+ const ac = new AbortController();
42
+ process.on('SIGINT', () => ac.abort());
43
+ let inflight = null;
44
+ let transcriptRef = [];
45
+ let lastStatus = null;
46
+ const app = render(React.createElement(App, {
47
+ initialModel: model,
48
+ maxSteps: opts.maxSteps ?? 30,
49
+ initialStatus: { status: 'idle' },
50
+ onPrompt: async (text) => {
51
+ inflight = runWithBridge(text);
52
+ await inflight;
53
+ inflight = null;
54
+ },
55
+ onSlash: async (cmd) => {
56
+ await handleSlash(cmd);
57
+ },
58
+ }));
59
+ // Bridge: subscribes to global hooks installed by App.useEffect.
60
+ // Every time the runtime emits, we translate to a transcript item or
61
+ // a status update.
62
+ const appG = globalThis;
63
+ async function runWithBridge(text) {
64
+ appG.__klyroAppStatus?.({ status: 'running', step: 0 });
65
+ let textBuf = '';
66
+ let activeCallId = null;
67
+ let activeCallName = null;
68
+ let activeCallArgs = '';
69
+ try {
70
+ const result = await run({
71
+ task: text,
72
+ cwd,
73
+ model: model,
74
+ maxSteps: opts.maxSteps ?? 30,
75
+ signal: ac.signal,
76
+ nonInteractive: opts.nonInteractive ?? false,
77
+ onEvent: (ev) => {
78
+ if (ev.kind === 'step_start') {
79
+ appG.__klyroAppStatus?.({ step: ev.step });
80
+ }
81
+ else if (ev.kind === 'text_delta') {
82
+ textBuf += ev.text;
83
+ appG.__klyroAppAppend?.({ id: `text-${ev.kind}-${Date.now()}-${Math.random()}`, kind: 'text', text: ev.text, role: 'assistant' });
84
+ }
85
+ else if (ev.kind === 'tool_call_start') {
86
+ activeCallId = ev.id;
87
+ activeCallName = ev.name;
88
+ activeCallArgs = '';
89
+ }
90
+ else if (ev.kind === 'tool_call_delta') {
91
+ activeCallArgs += ev.argsJson;
92
+ }
93
+ else if (ev.kind === 'tool_call_end') {
94
+ appG.__klyroAppAppend?.({
95
+ id: `tool-${ev.id}-${Date.now()}`,
96
+ kind: 'tool',
97
+ name: ev.name,
98
+ id_call: ev.id,
99
+ args: JSON.stringify(ev.input, null, 2),
100
+ collapsed: true,
101
+ });
102
+ activeCallId = null;
103
+ activeCallName = null;
104
+ activeCallArgs = '';
105
+ }
106
+ else if (ev.kind === 'policy_decision') {
107
+ appG.__klyroAppAppend?.({
108
+ id: `pol-${ev.id}-${Date.now()}`,
109
+ kind: 'policy',
110
+ name: ev.name,
111
+ action: ev.action,
112
+ reason: ev.reason,
113
+ });
114
+ }
115
+ else if (ev.kind === 'tool_result') {
116
+ appG.__klyroAppAppend?.({
117
+ id: `tres-${ev.id}-${Date.now()}`,
118
+ kind: 'tool',
119
+ name: ev.name,
120
+ id_call: ev.id,
121
+ args: '',
122
+ result: typeof ev.output === 'string' ? ev.output : JSON.stringify(ev.output, null, 2),
123
+ isError: ev.isError,
124
+ latencyMs: ev.latencyMs,
125
+ collapsed: true,
126
+ });
127
+ }
128
+ else if (ev.kind === 'usage') {
129
+ appG.__klyroAppStatus?.({ usageInput: ev.input, usageOutput: ev.output });
130
+ }
131
+ else if (ev.kind === 'aborted') {
132
+ appG.__klyroAppStatus?.({ status: 'aborted' });
133
+ }
134
+ },
135
+ }, { adapter, registry, policy, approval, systemPrompt: systemPromptFn });
136
+ appG.__klyroAppStatus?.({ status: 'complete' === result.status ? 'done' : 'error', repairs: result.repairs ?? 0 });
137
+ }
138
+ catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ appG.__klyroAppAppend?.({ id: `err-${Date.now()}`, kind: 'error', message });
141
+ appG.__klyroAppStatus?.({ status: 'error', errorMessage: message });
142
+ }
143
+ }
144
+ async function handleSlash(cmd) {
145
+ switch (cmd.kind) {
146
+ case 'quit':
147
+ app.unmount();
148
+ process.exit(0);
149
+ return;
150
+ case 'clear':
151
+ // Bypass via the app's setTranscript by re-rendering is awkward;
152
+ // for MVP, append a marker and rely on the user to scroll.
153
+ // A real implementation would expose a clear() method.
154
+ appG.__klyroAppAppend?.({ id: `sep-${Date.now()}`, kind: 'text', text: '--- cleared ---', role: 'assistant' });
155
+ return;
156
+ case 'help': {
157
+ const helpText = 'commands: /clear /compact /model <id> /diff /status /quit';
158
+ appG.__klyroAppAppend?.({ id: `help-${Date.now()}`, kind: 'text', text: helpText, role: 'assistant' });
159
+ return;
160
+ }
161
+ case 'status': {
162
+ if (lastStatus) {
163
+ appG.__klyroAppAppend?.({
164
+ id: `stat-${Date.now()}`,
165
+ kind: 'text',
166
+ text: JSON.stringify(lastStatus, null, 2),
167
+ role: 'assistant',
168
+ });
169
+ }
170
+ return;
171
+ }
172
+ case 'compact':
173
+ case 'diff':
174
+ case 'model':
175
+ appG.__klyroAppAppend?.({
176
+ id: `stub-${Date.now()}`,
177
+ kind: 'text',
178
+ text: `/${cmd.kind} is a stub in this build. (${cmd.kind === 'model' ? `requested: ${cmd.model}` : 'persistence integration pending'})`,
179
+ role: 'assistant',
180
+ });
181
+ return;
182
+ case 'unknown':
183
+ appG.__klyroAppAppend?.({
184
+ id: `unk-${Date.now()}`,
185
+ kind: 'error',
186
+ message: `unknown command: ${cmd.raw} (try /help)`,
187
+ });
188
+ return;
189
+ }
190
+ }
191
+ return new Promise((resolve) => {
192
+ process.on('exit', () => resolve(0));
193
+ });
194
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `klyro run "<prompt>"` — one-shot autonomous task. Runs the agent loop
3
+ * to completion (or max_steps) and prints the final text. Non-interactive:
4
+ * policy denies `ask` decisions rather than prompting the user.
5
+ *
6
+ * Streams text deltas to stdout as they arrive so the user sees progress
7
+ * even on a long task. Tool calls and policy decisions go to stderr so
8
+ * they don't pollute the captured `finalText` if the user pipes.
9
+ */
10
+ import type { Message } from '../agent/message.js';
11
+ export interface RunCliOptions {
12
+ task: string;
13
+ cwd: string;
14
+ model: string;
15
+ maxSteps?: number;
16
+ maxTokens?: number;
17
+ temperature?: number;
18
+ systemPrompt?: (ctx: {
19
+ cwd: string;
20
+ telemetry?: string;
21
+ }) => string;
22
+ baseUrl?: string;
23
+ apiKey?: string;
24
+ timeoutMs?: number;
25
+ abortOnSigint?: boolean;
26
+ /** Inject a custom adapter (used by tests). If omitted, httpChatAdapter is used. */
27
+ adapter?: import('../agent/provider-adapter.js').ProviderAdapter;
28
+ /**
29
+ * Provider kind to use when no custom adapter is supplied. Default 'openai'.
30
+ * Set to 'anthropic' to use the Anthropic Messages API adapter; in that
31
+ * case `baseUrl` defaults to https://api.anthropic.com and the auth header
32
+ * is sent as `x-api-key` (or `Authorization: Bearer` if `authHeader: 'bearer'`).
33
+ */
34
+ provider?: 'openai' | 'anthropic';
35
+ /** When provider='anthropic', override the auth header. Default 'x-api-key'. */
36
+ authHeader?: 'x-api-key' | 'Authorization';
37
+ /**
38
+ * Output mode:
39
+ * - 'human' (default): text deltas on stdout, tool/policy on stderr
40
+ * - 'json': one JSON object per line per RuntimeEvent on stdout
41
+ * - 'silent': no streaming output; only the final text printed at the end
42
+ */
43
+ output?: 'human' | 'json' | 'silent';
44
+ /**
45
+ * Dry-run: don't actually call the model. Print the request that would
46
+ * be made (system prompt + tool definitions + first user message) and
47
+ * exit 0. Useful for inspecting the prompt assembly and verifying wiring.
48
+ */
49
+ dryRun?: boolean;
50
+ /**
51
+ * Path to a JSON file containing a saved transcript to resume from.
52
+ * The file must have shape `{transcript: Message[]}` and optionally
53
+ * `task?: string`. If `task` is present, it's used as the new user
54
+ * turn; if absent, `opts.task` is appended.
55
+ */
56
+ resumePath?: string;
57
+ }
58
+ export declare function runOnce(opts: RunCliOptions): Promise<number>;
59
+ /** Wrap a system-prompt fn to inject Level-6 context (project map etc.). */
60
+ export declare function makeRunSystemPrompt(cwd: string, base: (ctx: {
61
+ cwd: string;
62
+ telemetry?: string;
63
+ }) => string): Promise<(ctx: {
64
+ cwd: string;
65
+ telemetry?: string;
66
+ }) => string>;
67
+ export declare function loadTranscript(path: string): Message[];
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `klyro run "<prompt>"` — one-shot autonomous task. Runs the agent loop
3
+ * to completion (or max_steps) and prints the final text. Non-interactive:
4
+ * policy denies `ask` decisions rather than prompting the user.
5
+ *
6
+ * Streams text deltas to stdout as they arrive so the user sees progress
7
+ * even on a long task. Tool calls and policy decisions go to stderr so
8
+ * they don't pollute the captured `finalText` if the user pipes.
9
+ */
10
+ import { stdout, stderr } from 'node:process';
11
+ import { httpChatAdapter } from '../agent/provider-adapter.js';
12
+ import { anthropicAdapter } from '../agent/anthropic-adapter.js';
13
+ import { run } from '../agent/runtime.js';
14
+ import { builtinRegistry } from '../tools/registry.js';
15
+ import { builtinRules, DEFAULT_POLICY_CONFIG, PolicyEngine } from '../policy/engine.js';
16
+ import { DenyAllApprovalPrompt } from '../policy/approval.js';
17
+ import { buildLevel6Context } from '../context/level6.js';
18
+ ;
19
+ import * as fs from 'node:fs';
20
+ function readEnv(name, fallback) {
21
+ const v = process.env[name];
22
+ return v && v.length > 0 ? v : fallback;
23
+ }
24
+ export async function runOnce(opts) {
25
+ const output = opts.output ?? 'human';
26
+ if (opts.dryRun) {
27
+ return dryRunReport(opts);
28
+ }
29
+ let adapter = opts.adapter;
30
+ if (!adapter) {
31
+ const provider = opts.provider ?? 'openai';
32
+ const baseUrl = opts.baseUrl ?? readEnv('KLYRO_BASE_URL');
33
+ const apiKey = opts.apiKey ?? readEnv('KLYRO_API_KEY');
34
+ if (!apiKey) {
35
+ stderr.write('klyro: KLYRO_API_KEY is not set (or pass --api-key)\n');
36
+ return 2;
37
+ }
38
+ if (provider === 'anthropic') {
39
+ adapter = anthropicAdapter({
40
+ baseURL: baseUrl,
41
+ apiKey,
42
+ timeoutMs: opts.timeoutMs ?? 60_000,
43
+ authHeader: opts.authHeader,
44
+ });
45
+ }
46
+ else {
47
+ if (!baseUrl) {
48
+ stderr.write('klyro: KLYRO_BASE_URL is not set (or pass --base-url)\n');
49
+ return 2;
50
+ }
51
+ adapter = httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: opts.timeoutMs ?? 60_000 });
52
+ }
53
+ }
54
+ const registry = builtinRegistry();
55
+ const policy = new PolicyEngine(builtinRules(), DEFAULT_POLICY_CONFIG);
56
+ const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
57
+ const ac = new AbortController();
58
+ if (opts.abortOnSigint !== false) {
59
+ process.on('SIGINT', () => {
60
+ stderr.write('\nklyro: SIGINT — aborting\n');
61
+ ac.abort();
62
+ });
63
+ }
64
+ const result = await run({
65
+ task: opts.task,
66
+ cwd: opts.cwd,
67
+ model: opts.model,
68
+ maxSteps: opts.maxSteps,
69
+ maxTokens: opts.maxTokens,
70
+ temperature: opts.temperature,
71
+ signal: ac.signal,
72
+ nonInteractive: true,
73
+ initialTranscript: opts.resumePath ? loadTranscript(opts.resumePath) : undefined,
74
+ onEvent: (ev) => {
75
+ if (output === 'json') {
76
+ stdout.write(JSON.stringify(ev) + '\n');
77
+ return;
78
+ }
79
+ if (output === 'silent') {
80
+ return;
81
+ }
82
+ // human mode
83
+ if (ev.kind === 'text_delta')
84
+ stdout.write(ev.text);
85
+ else if (ev.kind === 'tool_call_start') {
86
+ stderr.write(`\n[tool] ${ev.name} {\n`);
87
+ }
88
+ else if (ev.kind === 'tool_call_end') {
89
+ stderr.write(`\n}\n`);
90
+ }
91
+ else if (ev.kind === 'tool_result') {
92
+ const summary = typeof ev.output === 'string' ? ev.output.slice(0, 200) : JSON.stringify(ev.output).slice(0, 200);
93
+ stderr.write(` -> ${ev.isError ? 'ERR ' : 'ok '}(${ev.latencyMs}ms) ${summary}\n`);
94
+ }
95
+ else if (ev.kind === 'policy_decision' && ev.action !== 'allow') {
96
+ stderr.write(` policy: ${ev.action}${ev.reason ? ` — ${ev.reason}` : ''}\n`);
97
+ }
98
+ else if (ev.kind === 'step_start') {
99
+ stderr.write(`\n[step ${ev.step}]\n`);
100
+ }
101
+ },
102
+ }, { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt });
103
+ if (output !== 'json')
104
+ stdout.write('\n');
105
+ if (result.status === 'max_steps') {
106
+ if (output === 'json')
107
+ stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
108
+ else
109
+ stderr.write(`klyro: hit max steps (${result.steps}); consider raising --max-steps\n`);
110
+ return 3;
111
+ }
112
+ if (result.status === 'aborted') {
113
+ return 130;
114
+ }
115
+ if (result.status === 'no_final') {
116
+ if (output !== 'json')
117
+ stderr.write('klyro: provider error — no final answer\n');
118
+ return 4;
119
+ }
120
+ if (output === 'json')
121
+ stdout.write(JSON.stringify({ kind: 'final', status: 'ok', text: result.finalText }) + '\n');
122
+ return 0;
123
+ }
124
+ function dryRunReport(opts) {
125
+ const systemPrompt = (opts.systemPrompt ?? defaultRunSystemPrompt)({ cwd: opts.cwd });
126
+ const registry = builtinRegistry();
127
+ const rules = builtinRules();
128
+ const report = {
129
+ kind: 'dry_run',
130
+ cwd: opts.cwd,
131
+ model: opts.model,
132
+ maxSteps: opts.maxSteps,
133
+ maxTokens: opts.maxTokens,
134
+ temperature: opts.temperature,
135
+ systemPrompt,
136
+ task: opts.task,
137
+ toolCount: registry.list().length,
138
+ toolNames: registry.list().map((t) => t.name),
139
+ policyRules: rules.map((r) => r.name),
140
+ };
141
+ stdout.write(JSON.stringify(report, null, 2) + '\n');
142
+ return 0;
143
+ }
144
+ function defaultRunSystemPrompt(_ctx) {
145
+ const base = [
146
+ 'You are Klyro running in one-shot mode. The user has given you a single task.',
147
+ 'Solve it by calling tools as needed. When done, produce a short final text answer.',
148
+ 'Do not invent file paths. Do not call tools outside the working directory.',
149
+ ].join(' ');
150
+ return _ctx.telemetry ? base + '\n\n' + _ctx.telemetry : base;
151
+ }
152
+ /** Wrap a system-prompt fn to inject Level-6 context (project map etc.). */
153
+ export async function makeRunSystemPrompt(cwd, base) {
154
+ const ctxBlock = await buildLevel6Context({ cwd });
155
+ const prefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
156
+ return (ctx) => {
157
+ const t = ctx.telemetry ? '\n\n' + ctx.telemetry : '';
158
+ return base(ctx) + prefix + t;
159
+ };
160
+ }
161
+ export function loadTranscript(path) {
162
+ let raw;
163
+ try {
164
+ raw = fs.readFileSync(path, 'utf8');
165
+ }
166
+ catch (err) {
167
+ const msg = err instanceof Error ? err.message : String(err);
168
+ throw new Error(`cannot read transcript file '${path}': ${msg}`);
169
+ }
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(raw);
173
+ }
174
+ catch (err) {
175
+ const msg = err instanceof Error ? err.message : String(err);
176
+ throw new Error(`transcript file '${path}' is not valid JSON: ${msg}`);
177
+ }
178
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
179
+ throw new Error(`transcript file '${path}' must be a JSON object with a 'transcript' field`);
180
+ }
181
+ const obj = parsed;
182
+ if (!Array.isArray(obj.transcript)) {
183
+ throw new Error(`transcript file '${path}' is missing required field 'transcript' (array of Message)`);
184
+ }
185
+ // Light shape check — full validation would re-implement message.ts. We
186
+ // trust the shape from a previous run; failures at runtime will surface
187
+ // clearly from the provider adapter.
188
+ for (const m of obj.transcript) {
189
+ if (typeof m !== 'object' || m === null) {
190
+ throw new Error(`transcript file '${path}' has non-object message: ${JSON.stringify(m).slice(0, 80)}`);
191
+ }
192
+ const mm = m;
193
+ if (typeof mm.role !== 'string' || !Array.isArray(mm.content)) {
194
+ throw new Error(`transcript file '${path}' has malformed message (need role:string + content:[]): ${JSON.stringify(m).slice(0, 80)}`);
195
+ }
196
+ }
197
+ return obj.transcript;
198
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Slash command parser — turns "/model foo" into a typed Command.
3
+ *
4
+ * Commands supported in MVP:
5
+ * /clear — clear the visible transcript (not the agent's)
6
+ * /compact — ask the agent to compact its own context
7
+ * /model <id> — switch the active model mid-session
8
+ * /diff — show working-tree diff (git diff)
9
+ * /status — show session status (model, steps, usage)
10
+ * /quit — exit the REPL
11
+ * /help — list available commands
12
+ *
13
+ * Anything not starting with "/" is a regular prompt and yields
14
+ * { kind: 'prompt', text }.
15
+ */
16
+ export type SlashCommand = {
17
+ kind: 'clear';
18
+ } | {
19
+ kind: 'compact';
20
+ } | {
21
+ kind: 'model';
22
+ model: string;
23
+ } | {
24
+ kind: 'diff';
25
+ } | {
26
+ kind: 'status';
27
+ } | {
28
+ kind: 'quit';
29
+ } | {
30
+ kind: 'help';
31
+ } | {
32
+ kind: 'prompt';
33
+ text: string;
34
+ } | {
35
+ kind: 'unknown';
36
+ raw: string;
37
+ };
38
+ export declare function parse(input: string): SlashCommand;
39
+ export declare function listCommands(): string[];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Slash command parser — turns "/model foo" into a typed Command.
3
+ *
4
+ * Commands supported in MVP:
5
+ * /clear — clear the visible transcript (not the agent's)
6
+ * /compact — ask the agent to compact its own context
7
+ * /model <id> — switch the active model mid-session
8
+ * /diff — show working-tree diff (git diff)
9
+ * /status — show session status (model, steps, usage)
10
+ * /quit — exit the REPL
11
+ * /help — list available commands
12
+ *
13
+ * Anything not starting with "/" is a regular prompt and yields
14
+ * { kind: 'prompt', text }.
15
+ */
16
+ const KNOWN = ['clear', 'compact', 'model', 'diff', 'status', 'quit', 'help'];
17
+ export function parse(input) {
18
+ const trimmed = input.trim();
19
+ if (!trimmed.startsWith('/')) {
20
+ return { kind: 'prompt', text: trimmed };
21
+ }
22
+ const space = trimmed.indexOf(' ');
23
+ const name = (space === -1 ? trimmed.slice(1) : trimmed.slice(1, space)).toLowerCase();
24
+ const rest = space === -1 ? '' : trimmed.slice(space + 1).trim();
25
+ switch (name) {
26
+ case 'clear': return { kind: 'clear' };
27
+ case 'compact': return { kind: 'compact' };
28
+ case 'diff': return { kind: 'diff' };
29
+ case 'status': return { kind: 'status' };
30
+ case 'quit':
31
+ case 'exit':
32
+ case 'q': return { kind: 'quit' };
33
+ case 'help':
34
+ case '?': return { kind: 'help' };
35
+ case 'model':
36
+ case 'm': {
37
+ if (!rest)
38
+ return { kind: 'unknown', raw: trimmed };
39
+ return { kind: 'model', model: rest };
40
+ }
41
+ default:
42
+ return { kind: 'unknown', raw: trimmed };
43
+ }
44
+ }
45
+ export function listCommands() {
46
+ return [...KNOWN];
47
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Level-6 context: project map + repo map + recent files + dependencies.
3
+ *
4
+ * Computed once per run, formatted compactly, and prepended to the system
5
+ * prompt. Each sub-block is independently optional — if a sub-step errors
6
+ * (e.g. unreadable package.json), we omit that block rather than failing
7
+ * the whole context load.
8
+ *
9
+ * Token budget: we cap each sub-block so the total added context is bounded
10
+ * (≤ ~6 KB). The exact budget is exposed via Level6Options.
11
+ *
12
+ * Static counterpart to Level-7 (`./level7.ts`), which injects a *live*
13
+ * runtime-telemetry snapshot (step count, recent tool calls, errors) into
14
+ * every model call. Together they give the model a static picture of the
15
+ * project (L6) and a live picture of its own run (L7).
16
+ */
17
+ export interface Level6Options {
18
+ cwd: string;
19
+ /** Cap on the formatted total (in characters). */
20
+ maxTotalChars?: number;
21
+ /** Skip the project map block. */
22
+ skipProjectMap?: boolean;
23
+ /** Skip the repo map block. */
24
+ skipRepoMap?: boolean;
25
+ /** Skip the recent files block. */
26
+ skipRecentFiles?: boolean;
27
+ /** Skip the dependencies block. */
28
+ skipDeps?: boolean;
29
+ /** Hours window for "recent". */
30
+ recentHours?: number;
31
+ }
32
+ export interface Level6Context {
33
+ formatted: string;
34
+ hasProjectMap: boolean;
35
+ hasRepoMap: boolean;
36
+ hasRecentFiles: boolean;
37
+ hasDeps: boolean;
38
+ /** Number of bytes saved to disk cache (0 if cache disabled). */
39
+ cacheBytes?: number;
40
+ }
41
+ /** Build a Level-6 context block for the given working directory. */
42
+ export declare function buildLevel6Context(opts: Level6Options): Promise<Level6Context>;
43
+ /** Persist the Level-6 context to a JSON cache under .klyro/. */
44
+ export declare function writeLevel6Cache(cwd: string, ctx: Level6Context): Promise<string | null>;
45
+ export declare function readLevel6Cache(cwd: string): Promise<Level6Context | null>;