harnessly 1.0.0

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/build/tools.js ADDED
@@ -0,0 +1,142 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { execSync } from 'child_process';
6
+ import { checkPath, checkCommand, logToolCall, } from './harness';
7
+ const MAX_OUTPUT_LENGTH = 10000;
8
+ function truncate(text, limit = MAX_OUTPUT_LENGTH) {
9
+ if (text.length <= limit)
10
+ return text;
11
+ const half = Math.floor(limit / 2);
12
+ return text.slice(0, half) + `\n\n... [truncated ${text.length - limit} chars] ...\n\n` + text.slice(-half);
13
+ }
14
+ export function createTools() {
15
+ function timedExecute(toolName, input, fn) {
16
+ const start = Date.now();
17
+ const result = fn();
18
+ const durationMs = Date.now() - start;
19
+ logToolCall({
20
+ timestamp: new Date().toISOString(),
21
+ tool: toolName,
22
+ input,
23
+ result,
24
+ durationMs,
25
+ });
26
+ return result;
27
+ }
28
+ return {
29
+ readFile: tool({
30
+ description: 'Read the contents of a file at the given path',
31
+ inputSchema: z.object({
32
+ filePath: z.string().describe('Path to the file to read'),
33
+ }),
34
+ execute: async ({ filePath }) => {
35
+ const pathCheck = checkPath(filePath);
36
+ if (!pathCheck.allowed)
37
+ return pathCheck.reason;
38
+ return timedExecute('readFile', { filePath }, () => {
39
+ try {
40
+ const absolutePath = path.resolve(filePath);
41
+ const content = fs.readFileSync(absolutePath, 'utf-8');
42
+ return truncate(content);
43
+ }
44
+ catch (e) {
45
+ return `Error reading file: ${e.message}`;
46
+ }
47
+ });
48
+ },
49
+ }),
50
+ writeFile: tool({
51
+ description: 'Write content to a file. Creates the file and directories if they do not exist.',
52
+ inputSchema: z.object({
53
+ filePath: z.string().describe('Path to the file to write'),
54
+ content: z.string().describe('Content to write to the file'),
55
+ }),
56
+ execute: async ({ filePath, content }) => {
57
+ const pathCheck = checkPath(filePath);
58
+ if (!pathCheck.allowed)
59
+ return pathCheck.reason;
60
+ return timedExecute('writeFile', { filePath, content }, () => {
61
+ try {
62
+ const absolutePath = path.resolve(filePath);
63
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
64
+ fs.writeFileSync(absolutePath, content);
65
+ return `File written successfully: ${absolutePath}`;
66
+ }
67
+ catch (e) {
68
+ return `Error writing file: ${e.message}`;
69
+ }
70
+ });
71
+ },
72
+ }),
73
+ listDirectory: tool({
74
+ description: 'List files and directories at the given path',
75
+ inputSchema: z.object({
76
+ dirPath: z.string().describe('Path to the directory to list').default('.'),
77
+ }),
78
+ execute: async ({ dirPath }) => {
79
+ const pathCheck = checkPath(dirPath);
80
+ if (!pathCheck.allowed)
81
+ return pathCheck.reason;
82
+ return timedExecute('listDirectory', { dirPath }, () => {
83
+ try {
84
+ const absolutePath = path.resolve(dirPath);
85
+ const entries = fs.readdirSync(absolutePath, { withFileTypes: true });
86
+ return entries.map(e => `${e.isDirectory() ? '📁' : '📄'} ${e.name}`).join('\n');
87
+ }
88
+ catch (e) {
89
+ return `Error listing directory: ${e.message}`;
90
+ }
91
+ });
92
+ },
93
+ }),
94
+ runCommand: tool({
95
+ description: 'Run a shell command and return its output. Use this for git, npm, or any CLI command.',
96
+ inputSchema: z.object({
97
+ command: z.string().describe('The shell command to execute'),
98
+ }),
99
+ execute: async ({ command }) => {
100
+ const cmdCheck = checkCommand(command);
101
+ if (!cmdCheck.allowed)
102
+ return cmdCheck.reason;
103
+ return timedExecute('runCommand', { command }, () => {
104
+ try {
105
+ const output = execSync(command, {
106
+ encoding: 'utf-8',
107
+ timeout: 30000,
108
+ cwd: process.cwd(),
109
+ });
110
+ return truncate(output || '(no output)');
111
+ }
112
+ catch (e) {
113
+ return `Command failed: ${e.message}\n${e.stdout || ''}${e.stderr || ''}`;
114
+ }
115
+ });
116
+ },
117
+ }),
118
+ searchFiles: tool({
119
+ description: 'Search for a text pattern across files in a directory (like grep)',
120
+ inputSchema: z.object({
121
+ pattern: z.string().describe('Text or regex pattern to search for'),
122
+ dirPath: z.string().describe('Directory to search in').default('.'),
123
+ }),
124
+ execute: async ({ pattern, dirPath }) => {
125
+ const pathCheck = checkPath(dirPath);
126
+ if (!pathCheck.allowed)
127
+ return pathCheck.reason;
128
+ return timedExecute('searchFiles', { pattern, dirPath }, () => {
129
+ try {
130
+ const output = execSync(`findstr /s /n /i "${pattern}" "${path.resolve(dirPath)}\\*"`, { encoding: 'utf-8', timeout: 15000 });
131
+ return truncate(output || 'No matches found.');
132
+ }
133
+ catch (e) {
134
+ if (e.status === 1)
135
+ return 'No matches found.';
136
+ return `Search error: ${e.message}`;
137
+ }
138
+ });
139
+ },
140
+ }),
141
+ };
142
+ }
package/build/ui.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ declare const PROVIDERS: readonly ['OpenAI', 'Anthropic', 'Azure', 'Custom'];
2
+ type Provider = (typeof PROVIDERS)[number];
3
+ export interface ProviderConfig {
4
+ provider: Provider;
5
+ apiKey: string;
6
+ model: string;
7
+ resourceName?: string;
8
+ baseURL?: string;
9
+ }
10
+ export declare function Welcome(): void;
11
+ export declare function SelectProvider(): Promise<ProviderConfig>;
12
+ export {};
package/build/ui.js ADDED
@@ -0,0 +1,156 @@
1
+ // import chalk from 'chalk';
2
+ // import { select, text, isCancel, intro } from '@clack/prompts';
3
+ // const providers = [
4
+ // { value: 'OpenAI', label: 'OpenAI' },
5
+ // { value: 'Anthropic', label: 'Anthropic' },
6
+ // { value: 'Google', label: 'Google' },
7
+ // { value: 'Azure', label: 'Azure' },
8
+ // { value: 'Custom', label: 'Custom' },
9
+ // ];
10
+ // export interface ProviderConfig {
11
+ // provider: string;
12
+ // apiKey: string;
13
+ // model?: string;
14
+ // resourceName?: string;
15
+ // baseURL?: string;
16
+ // }
17
+ // export function Welcome() {
18
+ // console.log('');
19
+ // console.log(chalk.cyan.bold(' ╔══════════════════════════════════╗'));
20
+ // console.log(chalk.cyan.bold(' ║') + chalk.white.bold(' D E V R A ') + chalk.cyan.bold('║'));
21
+ // console.log(chalk.cyan.bold(' ╚══════════════════════════════════╝'));
22
+ // console.log(chalk.gray(' Your AI-powered coding agent'));
23
+ // console.log(chalk.dim(' Type "exit" to quit | Ctrl+C to cancel'));
24
+ // console.log('');
25
+ // }
26
+ // function prompt<T>(result: T | symbol): T {
27
+ // if (isCancel(result)) {
28
+ // console.log(chalk.dim('\nSetup cancelled.\n'));
29
+ // process.exit(0);
30
+ // }
31
+ // return result as T;
32
+ // }
33
+ // export async function SelectProvider(): Promise<ProviderConfig> {
34
+ // intro(chalk.bold('Setup'));
35
+ // const provider = prompt(await select({
36
+ // message: 'Select your AI provider',
37
+ // options: providers,
38
+ // }));
39
+ // const apiKey = prompt(await text({
40
+ // message: `Enter your ${provider} API Key`,
41
+ // placeholder: 'sk-...',
42
+ // validate: (val) => (val || '').length < 4 ? 'API key is too short' : undefined,
43
+ // }));
44
+ // const config: ProviderConfig = { provider, apiKey };
45
+ // if (provider === 'Azure') {
46
+ // config.resourceName = prompt(await text({
47
+ // message: 'Enter Azure Resource Name',
48
+ // placeholder: 'my-resource',
49
+ // }));
50
+ // }
51
+ // if (provider === 'Custom') {
52
+ // config.baseURL = prompt(await text({
53
+ // message: 'Enter base URL',
54
+ // placeholder: 'http://localhost:11434/v1',
55
+ // }));
56
+ // }
57
+ // const defaultModel = getDefaultModel(provider);
58
+ // const model = prompt(await text({
59
+ // message: 'Model name',
60
+ // placeholder: defaultModel,
61
+ // defaultValue: defaultModel,
62
+ // }));
63
+ // config.model = model || defaultModel;
64
+ // console.log('');
65
+ // console.log(chalk.green(' ✓ ') + chalk.bold('Ready!') + chalk.dim(` (${provider} / ${config.model})`));
66
+ // console.log('');
67
+ // console.log(chalk.dim(' Configurations ') + process.cwd());
68
+ // console.log(chalk.dim(''))
69
+ // console.log('');
70
+ // return config;
71
+ // }
72
+ // function getDefaultModel(provider: string): string {
73
+ // const defaults: Record<string, string> = {
74
+ // OpenAI: 'gpt-4o',
75
+ // Anthropic: 'claude-sonnet-4-20250514',
76
+ // Google: 'gemini-2.0-flash',
77
+ // Azure: 'gpt-4o',
78
+ // Custom: 'custom-model',
79
+ // };
80
+ // return defaults[provider] || 'gpt-4o';
81
+ // }
82
+ import chalk from 'chalk';
83
+ import { select, text, password, note, isCancel, intro, outro, cancel } from '@clack/prompts';
84
+ const PROVIDERS = ['OpenAI', 'Anthropic', 'Azure', 'Custom'];
85
+ const providerOptions = PROVIDERS.map((p) => ({ value: p, label: p }));
86
+ const DEFAULT_MODELS = {
87
+ OpenAI: 'gpt-4o',
88
+ Anthropic: 'claude-sonnet-4-5',
89
+ Azure: 'gpt-4o',
90
+ Custom: 'custom-model',
91
+ };
92
+ export function Welcome() {
93
+ console.log('');
94
+ console.log(chalk.cyan.bold(' ╔══════════════════════════════════╗'));
95
+ console.log(chalk.cyan.bold(' ║') + chalk.white.bold(' H A R N E S S L Y ') + chalk.cyan.bold('║'));
96
+ console.log(chalk.cyan.bold(' ╚══════════════════════════════════╝'));
97
+ console.log(chalk.gray(' Your harnessed AI coding agent'));
98
+ console.log(chalk.dim(' Type "exit" to quit | Ctrl+C to cancel'));
99
+ console.log('');
100
+ }
101
+ /** Unwraps a clack prompt result, exiting cleanly on Ctrl+C / cancel. */
102
+ function unwrap(result) {
103
+ if (isCancel(result)) {
104
+ cancel('Setup cancelled.');
105
+ process.exit(0);
106
+ }
107
+ return result;
108
+ }
109
+ function isValidUrl(value) {
110
+ try {
111
+ new URL(value);
112
+ return true;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ export async function SelectProvider() {
119
+ intro(chalk.bold('Setup'));
120
+ const provider = unwrap(await select({
121
+ message: 'Select your AI provider',
122
+ options: providerOptions,
123
+ }));
124
+ // password() masks input as it's typed — apiKey should never use text().
125
+ const apiKey = unwrap(await password({
126
+ message: `Enter your ${provider} API Key`,
127
+ validate: (val) => ((val || '').trim().length < 4 ? 'API key is too short' : undefined),
128
+ }));
129
+ const config = { provider, apiKey, model: DEFAULT_MODELS[provider] };
130
+ if (provider === 'Azure') {
131
+ config.resourceName = unwrap(await text({
132
+ message: 'Enter Azure Resource Name',
133
+ placeholder: 'my-resource',
134
+ validate: (val) => ((val || '').trim().length === 0 ? 'Resource name is required' : undefined),
135
+ }));
136
+ }
137
+ if (provider === 'Custom') {
138
+ config.baseURL = unwrap(await text({
139
+ message: 'Enter base URL',
140
+ placeholder: 'http://localhost:11434/v1',
141
+ validate: (val) => (!isValidUrl(val || '') ? 'Enter a valid URL' : undefined),
142
+ }));
143
+ }
144
+ const defaultModel = DEFAULT_MODELS[provider];
145
+ const model = unwrap(await text({
146
+ message: 'Model name',
147
+ placeholder: defaultModel,
148
+ defaultValue: defaultModel,
149
+ }));
150
+ config.model = model;
151
+ note(`${chalk.dim('Provider')} ${config.provider}\n` +
152
+ `${chalk.dim('Model')} ${config.model}\n` +
153
+ `${chalk.dim('Config')} ${process.cwd()}`, chalk.green('Ready'));
154
+ outro(chalk.dim('Starting session...'));
155
+ return config;
156
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "harnessly",
3
+ "version": "1.0.0",
4
+ "description": "A harnessed AI coding agent CLI with guardrails, approval flow, observability, and error recovery",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "bin": {
8
+ "harnessly": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/VomeshAtukuri/coding-agent.git"
19
+ },
20
+ "homepage": "https://github.com/VomeshAtukuri/coding-agent#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/VomeshAtukuri/coding-agent/issues"
23
+ },
24
+ "scripts": {
25
+ "dev": "tsx src/index.ts",
26
+ "build": "tsc",
27
+ "prepublishOnly": "npm run build",
28
+ "start": "node build/index.js",
29
+ "test": "echo \"Error: no test specified\" && exit 1"
30
+ },
31
+ "keywords": [
32
+ "coding",
33
+ "agent",
34
+ "cli",
35
+ "ai",
36
+ "harness",
37
+ "guardrails",
38
+ "approval",
39
+ "observability",
40
+ "llm",
41
+ "terminal"
42
+ ],
43
+ "author": "Vomesh",
44
+ "license": "MIT",
45
+ "type": "module",
46
+ "dependencies": {
47
+ "@ai-sdk/anthropic": "^4.0.42",
48
+ "@ai-sdk/azure": "^4.0.42",
49
+ "@ai-sdk/openai": "^4.0.41",
50
+ "@clack/prompts": "^1.7.0",
51
+ "ai": "^7.0.65",
52
+ "cac": "^7.0.0",
53
+ "chalk": "^6.0.0",
54
+ "ora": "^9.4.1",
55
+ "zod": "^4.4.3"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^26.2.0",
59
+ "nodemon": "^3.1.14",
60
+ "tsx": "^4.23.12",
61
+ "typescript": "^7.0.2"
62
+ }
63
+ }