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,72 @@
1
+ /**
2
+ * list_directory — list entries inside a directory.
3
+ *
4
+ * Refuses paths outside cwd. Skips node_modules and .git by default.
5
+ */
6
+ import * as fs from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+ import { z } from 'zod';
9
+ import { defineTool } from '../types.js';
10
+ import { resolveAndFollowSymlinks } from '../../policy/path-guard.js';
11
+ import { TOOL_ERROR_CODES, safe } from '../normalize.js';
12
+ const DEFAULT_SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.turbo', 'coverage']);
13
+ const InputSchema = z.object({
14
+ path: z.string().min(1).default('.').describe('Directory path relative to cwd. Defaults to cwd.'),
15
+ maxDepth: z.number().int().min(1).max(5).optional().describe('How deep to recurse (default: 1 = top level only).'),
16
+ skip: z.array(z.string()).optional().describe('Directory names to skip (default: node_modules, .git, etc.)'),
17
+ });
18
+ export const listDirTool = defineTool({
19
+ name: 'list_directory',
20
+ description: 'List entries inside a directory. Skips node_modules, .git, and common build dirs by default.',
21
+ inputSchema: InputSchema,
22
+ execute: async (input, ctx) => {
23
+ return safe(async () => {
24
+ const { resolved } = await resolveAndFollowSymlinks(ctx.cwd, input.path);
25
+ const stat = await fs.stat(resolved);
26
+ if (!stat.isDirectory()) {
27
+ return {
28
+ ok: false,
29
+ error: { code: TOOL_ERROR_CODES.INVALID_INPUT, message: `Not a directory: ${input.path}` },
30
+ };
31
+ }
32
+ const skip = new Set([...DEFAULT_SKIP, ...(input.skip ?? [])]);
33
+ const maxDepth = input.maxDepth ?? 1;
34
+ const entries = [];
35
+ await walk(resolved, resolved, 0, maxDepth, skip, entries);
36
+ return { path: input.path, entries };
37
+ });
38
+ },
39
+ });
40
+ async function walk(root, dir, depth, maxDepth, skip, out) {
41
+ const names = await fs.readdir(dir).catch(() => []);
42
+ for (const name of names) {
43
+ if (skip.has(name))
44
+ continue;
45
+ if (name.startsWith('.') && name !== '.' && name !== '..')
46
+ continue;
47
+ const full = path.join(dir, name);
48
+ let entry;
49
+ try {
50
+ const s = await fs.lstat(full);
51
+ if (s.isSymbolicLink()) {
52
+ entry = { name: path.relative(root, full), type: 'symlink', size: s.size };
53
+ }
54
+ else if (s.isDirectory()) {
55
+ entry = { name: path.relative(root, full), type: 'directory', size: s.size };
56
+ }
57
+ else if (s.isFile()) {
58
+ entry = { name: path.relative(root, full), type: 'file', size: s.size };
59
+ }
60
+ else {
61
+ entry = { name: path.relative(root, full), type: 'other', size: s.size };
62
+ }
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ out.push(entry);
68
+ if (entry.type === 'directory' && depth + 1 < maxDepth) {
69
+ await walk(root, full, depth + 1, maxDepth, skip, out);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * read_file — read a file (or a slice of it) from the workspace.
3
+ *
4
+ * Inputs:
5
+ * - path: required, relative to cwd or absolute
6
+ * - startLine / endLine: optional 1-indexed inclusive window
7
+ * - maxBytes: optional cap (default 1 MiB)
8
+ *
9
+ * Behavior:
10
+ * - Refuses to read outside cwd
11
+ * - Refuses files > maxBytes unless line window keeps it small
12
+ * - On missing file returns NOT_FOUND, not throw
13
+ * - UTF-8 decode with a single TextDecoder
14
+ */
15
+ import { z } from 'zod';
16
+ declare const InputSchema: z.ZodObject<{
17
+ path: z.ZodString;
18
+ startLine: z.ZodOptional<z.ZodNumber>;
19
+ endLine: z.ZodOptional<z.ZodNumber>;
20
+ maxBytes: z.ZodOptional<z.ZodNumber>;
21
+ }, z.core.$strip>;
22
+ export interface ReadFileOutput {
23
+ path: string;
24
+ totalLines: number;
25
+ lines: string[];
26
+ bytesRead: number;
27
+ truncated: boolean;
28
+ }
29
+ export declare const readFileTool: import("../types.js").Tool<{
30
+ path: string;
31
+ startLine?: number | undefined;
32
+ endLine?: number | undefined;
33
+ maxBytes?: number | undefined;
34
+ }, {
35
+ readonly ok: false;
36
+ readonly error: {
37
+ readonly code: "INVALID_INPUT";
38
+ readonly message: `Not a regular file: ${string}`;
39
+ };
40
+ path?: undefined;
41
+ totalLines?: undefined;
42
+ lines?: undefined;
43
+ bytesRead?: undefined;
44
+ truncated?: undefined;
45
+ } | {
46
+ readonly ok: false;
47
+ readonly error: {
48
+ readonly code: "INVALID_INPUT";
49
+ readonly message: `File too large (${number} bytes). Use startLine/endLine or raise maxBytes.`;
50
+ };
51
+ path?: undefined;
52
+ totalLines?: undefined;
53
+ lines?: undefined;
54
+ bytesRead?: undefined;
55
+ truncated?: undefined;
56
+ } | {
57
+ path: string;
58
+ totalLines: number;
59
+ lines: string[];
60
+ bytesRead: number;
61
+ truncated: boolean;
62
+ readonly ok?: undefined;
63
+ readonly error?: undefined;
64
+ }>;
65
+ export type ReadFileInput = z.infer<typeof InputSchema>;
66
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * read_file — read a file (or a slice of it) from the workspace.
3
+ *
4
+ * Inputs:
5
+ * - path: required, relative to cwd or absolute
6
+ * - startLine / endLine: optional 1-indexed inclusive window
7
+ * - maxBytes: optional cap (default 1 MiB)
8
+ *
9
+ * Behavior:
10
+ * - Refuses to read outside cwd
11
+ * - Refuses files > maxBytes unless line window keeps it small
12
+ * - On missing file returns NOT_FOUND, not throw
13
+ * - UTF-8 decode with a single TextDecoder
14
+ */
15
+ import * as fs from 'node:fs/promises';
16
+ import { z } from 'zod';
17
+ import { defineTool } from '../types.js';
18
+ import { resolveAndFollowSymlinks } from '../../policy/path-guard.js';
19
+ import { TOOL_ERROR_CODES, safe } from '../normalize.js';
20
+ const InputSchema = z.object({
21
+ path: z.string().min(1).describe('Path relative to cwd, or absolute path inside cwd'),
22
+ startLine: z.number().int().min(1).optional().describe('1-indexed inclusive start line'),
23
+ endLine: z.number().int().min(1).optional().describe('1-indexed inclusive end line'),
24
+ maxBytes: z.number().int().min(1).max(16 * 1024 * 1024).optional().describe('Max bytes to read (default 1 MiB)'),
25
+ });
26
+ const DEFAULT_MAX_BYTES = 1024 * 1024; // 1 MiB
27
+ export const readFileTool = defineTool({
28
+ name: 'read_file',
29
+ description: 'Read a file from the workspace. Returns the lines and total line count. Use startLine/endLine to read a slice without pulling the whole file.',
30
+ inputSchema: InputSchema,
31
+ execute: async (input, ctx) => {
32
+ return safe(async () => {
33
+ const { resolved } = await resolveAndFollowSymlinks(ctx.cwd, input.path);
34
+ const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
35
+ const stat = await fs.stat(resolved);
36
+ if (!stat.isFile()) {
37
+ return {
38
+ ok: false,
39
+ error: { code: TOOL_ERROR_CODES.INVALID_INPUT, message: `Not a regular file: ${input.path}` },
40
+ };
41
+ }
42
+ if (stat.size > maxBytes * 4 && !input.startLine && !input.endLine) {
43
+ return {
44
+ ok: false,
45
+ error: {
46
+ code: TOOL_ERROR_CODES.INVALID_INPUT,
47
+ message: `File too large (${stat.size} bytes). Use startLine/endLine or raise maxBytes.`,
48
+ },
49
+ };
50
+ }
51
+ const fh = await fs.open(resolved, 'r');
52
+ try {
53
+ const buf = Buffer.alloc(Math.min(maxBytes, stat.size));
54
+ const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
55
+ const text = new TextDecoder('utf-8').decode(buf.subarray(0, bytesRead));
56
+ const allLines = text.split(/\r\n|\r|\n/);
57
+ const truncated = bytesRead < stat.size;
58
+ const totalLines = truncated ? -1 : allLines.length;
59
+ const lines = input.startLine !== undefined || input.endLine !== undefined
60
+ ? allLines.slice((input.startLine ?? 1) - 1, input.endLine ?? allLines.length)
61
+ : allLines;
62
+ return {
63
+ path: input.path,
64
+ totalLines,
65
+ lines,
66
+ bytesRead,
67
+ truncated,
68
+ };
69
+ }
70
+ finally {
71
+ await fh.close().catch(() => undefined);
72
+ }
73
+ });
74
+ },
75
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * write_file — write content to a file in the workspace.
3
+ *
4
+ * Atomic write via tmp + rename. Refuses paths outside cwd. Creates parent
5
+ * directories if needed.
6
+ */
7
+ import { z } from 'zod';
8
+ declare const InputSchema: z.ZodObject<{
9
+ path: z.ZodString;
10
+ content: z.ZodString;
11
+ }, z.core.$strip>;
12
+ export interface WriteFileOutput {
13
+ path: string;
14
+ bytesWritten: number;
15
+ }
16
+ export declare const writeFileTool: import("../types.js").Tool<{
17
+ path: string;
18
+ content: string;
19
+ }, {
20
+ path: string;
21
+ bytesWritten: number;
22
+ }>;
23
+ export type WriteFileInput = z.infer<typeof InputSchema>;
24
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * write_file — write content to a file in the workspace.
3
+ *
4
+ * Atomic write via tmp + rename. Refuses paths outside cwd. Creates parent
5
+ * directories if needed.
6
+ */
7
+ import * as fs from 'node:fs/promises';
8
+ import * as path from 'node:path';
9
+ import * as crypto from 'node:crypto';
10
+ import { z } from 'zod';
11
+ import { defineTool } from '../types.js';
12
+ import { resolveWithinCwd } from '../../policy/path-guard.js';
13
+ import { safe } from '../normalize.js';
14
+ const InputSchema = z.object({
15
+ path: z.string().min(1).describe('Path relative to cwd or absolute (must be inside cwd)'),
16
+ content: z.string().describe('File content. Use empty string to truncate.'),
17
+ });
18
+ export const writeFileTool = defineTool({
19
+ name: 'write_file',
20
+ description: 'Write content to a file. Atomic: writes to a temp file then renames. Creates parent directories as needed.',
21
+ inputSchema: InputSchema,
22
+ execute: async (input, ctx) => {
23
+ return safe(async () => {
24
+ const { resolved } = resolveWithinCwd(ctx.cwd, input.path);
25
+ const parent = path.dirname(resolved);
26
+ await fs.mkdir(parent, { recursive: true });
27
+ const suffix = crypto.randomBytes(8).toString('hex');
28
+ const tmp = path.join(parent, `.klyro-write-${process.pid}-${suffix}.tmp`);
29
+ const data = Buffer.from(input.content, 'utf-8');
30
+ const fh = await fs.open(tmp, 'w');
31
+ try {
32
+ await fh.writeFile(data);
33
+ await fh.sync();
34
+ }
35
+ finally {
36
+ await fh.close().catch(() => undefined);
37
+ }
38
+ try {
39
+ await fs.rename(tmp, resolved);
40
+ }
41
+ catch (err) {
42
+ await fs.unlink(tmp).catch(() => undefined);
43
+ throw err;
44
+ }
45
+ return { path: input.path, bytesWritten: data.byteLength };
46
+ });
47
+ },
48
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * git_diff — show unstaged (`git diff`) or staged (`git diff --cached`)
3
+ * changes plus a compact per-file stat. Read-only.
4
+ */
5
+ import { z } from 'zod';
6
+ declare const InputSchema: z.ZodObject<{
7
+ staged: z.ZodOptional<z.ZodBoolean>;
8
+ path: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export interface GitDiffOutput {
11
+ diff: string;
12
+ stat: string;
13
+ patchedFiles: string[];
14
+ }
15
+ export declare const gitDiffTool: import("../types.js").Tool<{
16
+ staged?: boolean | undefined;
17
+ path?: string | undefined;
18
+ }, {
19
+ readonly ok: false;
20
+ readonly error: {
21
+ readonly code: "IO_ERROR";
22
+ readonly message: `git diff failed: ${string}`;
23
+ };
24
+ diff?: undefined;
25
+ stat?: undefined;
26
+ patchedFiles?: undefined;
27
+ } | {
28
+ diff: string;
29
+ stat: string;
30
+ patchedFiles: string[];
31
+ readonly ok?: undefined;
32
+ readonly error?: undefined;
33
+ }>;
34
+ export type GitDiffInput = z.infer<typeof InputSchema>;
35
+ export {};
@@ -0,0 +1,68 @@
1
+ /**
2
+ * git_diff — show unstaged (`git diff`) or staged (`git diff --cached`)
3
+ * changes plus a compact per-file stat. Read-only.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { z } from 'zod';
7
+ import { defineTool } from '../types.js';
8
+ import { safe, TOOL_ERROR_CODES } from '../normalize.js';
9
+ const InputSchema = z.object({
10
+ staged: z.boolean().optional().describe('Show staged changes instead of unstaged (default false)'),
11
+ path: z.string().optional().describe('Limit to a specific path'),
12
+ });
13
+ function run(args, cwd, timeoutMs = 15_000) {
14
+ return new Promise((resolve) => {
15
+ const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: false });
16
+ const out = [];
17
+ const err = [];
18
+ const timer = setTimeout(() => { try {
19
+ child.kill('SIGKILL');
20
+ }
21
+ catch { /* noop */ } }, timeoutMs);
22
+ child.stdout.on('data', (b) => out.push(b));
23
+ child.stderr.on('data', (b) => err.push(b));
24
+ child.on('close', (code) => {
25
+ clearTimeout(timer);
26
+ resolve({ code: code ?? 1, out: Buffer.concat(out).toString('utf-8'), err: Buffer.concat(err).toString('utf-8') });
27
+ });
28
+ });
29
+ }
30
+ export const gitDiffTool = defineTool({
31
+ name: 'git_diff',
32
+ description: 'Show the unified diff of working-tree changes (unstaged by default) or staged changes. Read-only. Returns the diff text, a per-file stat, and the list of changed file paths.',
33
+ inputSchema: InputSchema,
34
+ execute: async (input, ctx) => {
35
+ return safe(async () => {
36
+ const base = input.staged ? ['diff', '--cached'] : ['diff'];
37
+ const args = [...base, '--no-color'];
38
+ if (input.path)
39
+ args.push('--', input.path);
40
+ const diff = await run(args, ctx.cwd);
41
+ if (diff.code !== 0) {
42
+ return {
43
+ ok: false,
44
+ error: { code: TOOL_ERROR_CODES.IO_ERROR, message: `git diff failed: ${diff.err.trim() || diff.out.trim()}` },
45
+ };
46
+ }
47
+ const statArgs = [...base, '--stat', '--no-color'];
48
+ if (input.path)
49
+ statArgs.push('--', input.path);
50
+ const stat = await run(statArgs, ctx.cwd);
51
+ // Parse changed file paths from `diff --git a/... b/...` lines.
52
+ const patchedFiles = [];
53
+ const newline = /\r?\n/;
54
+ const re = /^diff --git a\/(.*?) b\/(.*\S)\s*$/;
55
+ for (const line of diff.out.split(newline)) {
56
+ const m = re.exec(line);
57
+ const target = m?.[2];
58
+ if (target)
59
+ patchedFiles.push(target.replace(/\/$/, ''));
60
+ }
61
+ return {
62
+ diff: diff.out.trim(),
63
+ stat: stat.out.trim(),
64
+ patchedFiles,
65
+ };
66
+ });
67
+ },
68
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * git_status — one or two commands that both read the repo:
3
+ * - `git status --porcelain --untracked-files=all`
4
+ * - `git log -n 10 --oneline` for recent history
5
+ * The agent needs working-tree state AND recent history to plan edits.
6
+ */
7
+ import { z } from 'zod';
8
+ declare const InputSchema: z.ZodObject<{
9
+ showLog: z.ZodOptional<z.ZodBoolean>;
10
+ }, z.core.$strip>;
11
+ export interface GitStatusOutput {
12
+ porcelain: string;
13
+ recentCommits?: string;
14
+ branch?: string;
15
+ }
16
+ export declare const gitStatusTool: import("../types.js").Tool<{
17
+ showLog?: boolean | undefined;
18
+ }, GitStatusOutput | {
19
+ readonly ok: false;
20
+ readonly error: {
21
+ readonly code: "NOT_A_GIT_REPO";
22
+ readonly message: string;
23
+ readonly details: {
24
+ readonly code: number;
25
+ };
26
+ };
27
+ }>;
28
+ export type GitStatusInput = z.infer<typeof InputSchema>;
29
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * git_status — one or two commands that both read the repo:
3
+ * - `git status --porcelain --untracked-files=all`
4
+ * - `git log -n 10 --oneline` for recent history
5
+ * The agent needs working-tree state AND recent history to plan edits.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { z } from 'zod';
9
+ import { defineTool } from '../types.js';
10
+ import { safe } from '../normalize.js';
11
+ const InputSchema = z.object({
12
+ showLog: z.boolean().optional().describe('Also include the last 10 commit messages (default true)'),
13
+ });
14
+ function run(args, cwd, timeoutMs = 15_000) {
15
+ return new Promise((resolve) => {
16
+ const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: false });
17
+ const out = [];
18
+ const err = [];
19
+ const timer = setTimeout(() => { try {
20
+ child.kill('SIGKILL');
21
+ }
22
+ catch { /* noop */ } }, timeoutMs);
23
+ child.stdout.on('data', (b) => out.push(b));
24
+ child.stderr.on('data', (b) => err.push(b));
25
+ child.on('close', (code) => {
26
+ clearTimeout(timer);
27
+ resolve({ code: code ?? 1, out: Buffer.concat(out).toString('utf-8'), err: Buffer.concat(err).toString('utf-8') });
28
+ });
29
+ });
30
+ }
31
+ export const gitStatusTool = defineTool({
32
+ name: 'git_status',
33
+ description: 'Show working-tree status (porcelain) and, optionally, the last 10 commit messages and current branch. Read-only.',
34
+ inputSchema: InputSchema,
35
+ execute: async (input, ctx) => {
36
+ return safe(async () => {
37
+ const status = await run(['status', '--porcelain', '--untracked-files=all'], ctx.cwd);
38
+ if (status.code !== 0) {
39
+ // Not a git repo or git unavailable
40
+ return {
41
+ ok: false,
42
+ error: { code: 'NOT_A_GIT_REPO', message: status.err.trim() || 'git status failed', details: { code: status.code } },
43
+ };
44
+ }
45
+ const result = { porcelain: status.out || '(clean)' };
46
+ const branch = await run(['branch', '--show-current'], ctx.cwd);
47
+ if (branch.code === 0 && branch.out.trim())
48
+ result.branch = branch.out.trim();
49
+ if (input.showLog !== false) {
50
+ const log = await run(['log', '-n', '10', '--oneline', '--no-show-signature'], ctx.cwd);
51
+ if (log.code === 0 && log.out.trim())
52
+ result.recentCommits = log.out.trim();
53
+ }
54
+ return result;
55
+ });
56
+ },
57
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Normalize any thrown value or system error into a ToolResult error.
3
+ * Centralizing this means tools can `throw` and the harness sees structured
4
+ * errors without each tool hand-coding the shape.
5
+ */
6
+ import type { ToolResult } from './types.js';
7
+ /** Standard tool error codes. Tools may add their own. */
8
+ export declare const TOOL_ERROR_CODES: {
9
+ readonly NOT_FOUND: "NOT_FOUND";
10
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
11
+ readonly INVALID_INPUT: "INVALID_INPUT";
12
+ readonly IO_ERROR: "IO_ERROR";
13
+ readonly TIMEOUT: "TIMEOUT";
14
+ readonly ABORTED: "ABORTED";
15
+ readonly PATH_ESCAPE: "PATH_ESCAPE";
16
+ readonly COMMAND_DENIED: "COMMAND_DENIED";
17
+ readonly COMMAND_NOT_FOUND: "COMMAND_NOT_FOUND";
18
+ readonly EXIT_NONZERO: "EXIT_NONZERO";
19
+ readonly INTERNAL: "INTERNAL";
20
+ };
21
+ export type ToolErrorCode = (typeof TOOL_ERROR_CODES)[keyof typeof TOOL_ERROR_CODES] | string;
22
+ export declare function toToolError(err: unknown, fallbackCode?: ToolErrorCode): {
23
+ code: ToolErrorCode;
24
+ message: string;
25
+ details?: unknown;
26
+ };
27
+ /** Convenience: wrap a function in try/catch and return a ToolResult.
28
+ * If the function returns a ToolResult (already-shaped), it is passed through
29
+ * unwrapped. Otherwise the function's return value becomes `value` on success.
30
+ */
31
+ export declare function safe<T>(fn: () => Promise<T>): Promise<ToolResult<T>>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Normalize any thrown value or system error into a ToolResult error.
3
+ * Centralizing this means tools can `throw` and the harness sees structured
4
+ * errors without each tool hand-coding the shape.
5
+ */
6
+ /** Standard tool error codes. Tools may add their own. */
7
+ export const TOOL_ERROR_CODES = {
8
+ NOT_FOUND: 'NOT_FOUND',
9
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
10
+ INVALID_INPUT: 'INVALID_INPUT',
11
+ IO_ERROR: 'IO_ERROR',
12
+ TIMEOUT: 'TIMEOUT',
13
+ ABORTED: 'ABORTED',
14
+ PATH_ESCAPE: 'PATH_ESCAPE',
15
+ COMMAND_DENIED: 'COMMAND_DENIED',
16
+ COMMAND_NOT_FOUND: 'COMMAND_NOT_FOUND',
17
+ EXIT_NONZERO: 'EXIT_NONZERO',
18
+ INTERNAL: 'INTERNAL',
19
+ };
20
+ export function toToolError(err, fallbackCode = TOOL_ERROR_CODES.INTERNAL) {
21
+ if (err === null || err === undefined) {
22
+ return { code: fallbackCode, message: 'Unknown error' };
23
+ }
24
+ if (err instanceof Error) {
25
+ // If the error already declares a tool-level `code` (e.g. PathGuardError),
26
+ // honor it instead of mapping through Node errno.
27
+ const maybeCode = err.code;
28
+ if (typeof maybeCode === 'string' && isToolErrorCode(maybeCode)) {
29
+ return { code: maybeCode, message: err.message };
30
+ }
31
+ if (typeof maybeCode === 'string') {
32
+ // Looks like a Node errno (e.g. 'ENOENT'); map it.
33
+ return {
34
+ code: mapNodeErrorCode(maybeCode),
35
+ message: err.message,
36
+ details: { syscall: err.syscall, path: err.path },
37
+ };
38
+ }
39
+ return { code: fallbackCode, message: err.message, details: err.stack };
40
+ }
41
+ return { code: fallbackCode, message: String(err) };
42
+ }
43
+ function isToolErrorCode(code) {
44
+ return Object.values(TOOL_ERROR_CODES).includes(code) || code === 'MATCH_NOT_FOUND' || code === 'MATCH_AMBIGUOUS' || code === 'UNKNOWN_TOOL' || code === 'INVALID_INPUT';
45
+ }
46
+ function mapNodeErrorCode(code) {
47
+ switch (code) {
48
+ case 'ENOENT':
49
+ return TOOL_ERROR_CODES.NOT_FOUND;
50
+ case 'EACCES':
51
+ case 'EPERM':
52
+ return TOOL_ERROR_CODES.PERMISSION_DENIED;
53
+ case 'EISDIR':
54
+ case 'ENOTDIR':
55
+ return TOOL_ERROR_CODES.IO_ERROR;
56
+ case 'ETIMEDOUT':
57
+ case 'ESOCKETTIMEDOUT':
58
+ return TOOL_ERROR_CODES.TIMEOUT;
59
+ case 'ECANCELED':
60
+ case 'ABORT_ERR':
61
+ return TOOL_ERROR_CODES.ABORTED;
62
+ default:
63
+ return TOOL_ERROR_CODES.IO_ERROR;
64
+ }
65
+ }
66
+ /** Convenience: wrap a function in try/catch and return a ToolResult.
67
+ * If the function returns a ToolResult (already-shaped), it is passed through
68
+ * unwrapped. Otherwise the function's return value becomes `value` on success.
69
+ */
70
+ export async function safe(fn) {
71
+ try {
72
+ const result = await fn();
73
+ // Only pass through true tool-level errors: ok:false WITH an error
74
+ // object. Tools that return a value with an `ok` field of their own
75
+ // (e.g. VerifyOutput.ok = "did the verify command succeed") get
76
+ // wrapped so the harness sees ToolResult<T>, not a bare T.
77
+ if (result &&
78
+ typeof result === 'object' &&
79
+ result.ok === false &&
80
+ result.error) {
81
+ return result;
82
+ }
83
+ return { ok: true, value: result };
84
+ }
85
+ catch (err) {
86
+ return { ok: false, error: toToolError(err) };
87
+ }
88
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool registry — single source of truth for tools the model can use.
3
+ */
4
+ import type { Tool, ToolContext, ToolResult } from './types.js';
5
+ export declare class ToolRegistry {
6
+ private readonly tools;
7
+ register<TIn, TOut>(tool: Tool<TIn, TOut>): this;
8
+ get(name: string): Tool<unknown, unknown> | undefined;
9
+ list(): Tool<unknown, unknown>[];
10
+ /** Tools in OpenAI-style tool definition format. */
11
+ toOpenAITools(): Array<{
12
+ type: 'function';
13
+ function: {
14
+ name: string;
15
+ description: string;
16
+ parameters: unknown;
17
+ };
18
+ }>;
19
+ /** Cached JSON Schemas keyed by tool name (for adapter consumers). */
20
+ jsonSchemas(): Record<string, unknown>;
21
+ execute(name: string, rawInput: unknown, ctx: ToolContext): Promise<ToolResult<unknown>>;
22
+ }
23
+ export declare const builtinRegistry: () => ToolRegistry;