@denizokcu/haze 0.0.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 (69) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/bin/haze.js +2 -0
  5. package/dist/cli/commands/chat.d.ts +6 -0
  6. package/dist/cli/commands/chat.js +101 -0
  7. package/dist/cli/commands/commands.d.ts +15 -0
  8. package/dist/cli/commands/commands.js +46 -0
  9. package/dist/cli/commands/formatters.d.ts +8 -0
  10. package/dist/cli/commands/formatters.js +46 -0
  11. package/dist/cli/commands/skills.d.ts +4 -0
  12. package/dist/cli/commands/skills.js +35 -0
  13. package/dist/cli/commands/streaming.d.ts +19 -0
  14. package/dist/cli/commands/streaming.js +101 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +31 -0
  17. package/dist/config/contextFiles.d.ts +5 -0
  18. package/dist/config/contextFiles.js +59 -0
  19. package/dist/config/inputHistory.d.ts +4 -0
  20. package/dist/config/inputHistory.js +29 -0
  21. package/dist/config/paths.d.ts +3 -0
  22. package/dist/config/paths.js +5 -0
  23. package/dist/config/settings.d.ts +10 -0
  24. package/dist/config/settings.js +16 -0
  25. package/dist/llm/client.d.ts +1 -0
  26. package/dist/llm/client.js +11 -0
  27. package/dist/llm/hazeTools.d.ts +82 -0
  28. package/dist/llm/hazeTools.js +226 -0
  29. package/dist/llm/initPrompt.d.ts +1 -0
  30. package/dist/llm/initPrompt.js +19 -0
  31. package/dist/llm/systemPrompt.d.ts +2 -0
  32. package/dist/llm/systemPrompt.js +33 -0
  33. package/dist/skills/SkillLoader.d.ts +2 -0
  34. package/dist/skills/SkillLoader.js +22 -0
  35. package/dist/skills/SkillRegistry.d.ts +6 -0
  36. package/dist/skills/SkillRegistry.js +28 -0
  37. package/dist/skills/builder/SkillBuilder.d.ts +1 -0
  38. package/dist/skills/builder/SkillBuilder.js +25 -0
  39. package/dist/skills/installer/SkillInstaller.d.ts +1 -0
  40. package/dist/skills/installer/SkillInstaller.js +48 -0
  41. package/dist/skills/manifestSchema.d.ts +31 -0
  42. package/dist/skills/manifestSchema.js +23 -0
  43. package/dist/skills/types.d.ts +60 -0
  44. package/dist/skills/types.js +1 -0
  45. package/dist/tools/ToolExecutor.d.ts +3 -0
  46. package/dist/tools/ToolExecutor.js +15 -0
  47. package/dist/tools/types.d.ts +9 -0
  48. package/dist/tools/types.js +1 -0
  49. package/dist/ui/components/ErrorView.d.ts +3 -0
  50. package/dist/ui/components/ErrorView.js +7 -0
  51. package/dist/ui/components/Header.d.ts +3 -0
  52. package/dist/ui/components/Header.js +6 -0
  53. package/dist/ui/components/MarkdownText.d.ts +3 -0
  54. package/dist/ui/components/MarkdownText.js +108 -0
  55. package/dist/ui/components/TextInput.d.ts +9 -0
  56. package/dist/ui/components/TextInput.js +120 -0
  57. package/dist/ui/theme.d.ts +11 -0
  58. package/dist/ui/theme.js +11 -0
  59. package/dist/utils/fs.d.ts +14 -0
  60. package/dist/utils/fs.js +36 -0
  61. package/dist/utils/path.d.ts +3 -0
  62. package/dist/utils/path.js +16 -0
  63. package/dist/utils/yaml.d.ts +2 -0
  64. package/dist/utils/yaml.js +8 -0
  65. package/examples/skills/files/prompts/file_tasks.md +1 -0
  66. package/examples/skills/files/skill.yaml +28 -0
  67. package/examples/skills/files/tools/list_files.ts +21 -0
  68. package/examples/skills/files/tools/read_file.ts +12 -0
  69. package/package.json +71 -0
@@ -0,0 +1,59 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { HAZE_DIR } from './paths.js';
5
+ const CONTEXT_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md'];
6
+ const MAX_CONTEXT_FILE_CHARS = 20_000;
7
+ function uniqueExistingAncestors(fromDir) {
8
+ const dirs = [];
9
+ let current = path.resolve(fromDir);
10
+ while (true) {
11
+ dirs.push(current);
12
+ const parent = path.dirname(current);
13
+ if (parent === current)
14
+ break;
15
+ current = parent;
16
+ }
17
+ return dirs.reverse();
18
+ }
19
+ function displayPath(filePath) {
20
+ const home = os.homedir();
21
+ const cwd = process.cwd();
22
+ if (filePath.startsWith(cwd + path.sep) || filePath === cwd)
23
+ return path.relative(cwd, filePath) || path.basename(filePath);
24
+ if (filePath.startsWith(home + path.sep))
25
+ return `~/${path.relative(home, filePath)}`;
26
+ return filePath;
27
+ }
28
+ export async function readContextFiles(cwd = process.cwd()) {
29
+ const candidates = [];
30
+ for (const name of CONTEXT_FILE_NAMES) {
31
+ candidates.push(path.join(HAZE_DIR, name));
32
+ }
33
+ for (const dir of uniqueExistingAncestors(cwd)) {
34
+ for (const name of CONTEXT_FILE_NAMES) {
35
+ candidates.push(path.join(dir, name));
36
+ }
37
+ }
38
+ const seen = new Set();
39
+ const contextFiles = [];
40
+ for (const candidate of candidates) {
41
+ const absolute = path.resolve(candidate);
42
+ if (seen.has(absolute))
43
+ continue;
44
+ seen.add(absolute);
45
+ if (!await fs.pathExists(absolute))
46
+ continue;
47
+ const stat = await fs.stat(absolute).catch(() => null);
48
+ if (!stat?.isFile())
49
+ continue;
50
+ const content = await fs.readFile(absolute, 'utf8');
51
+ contextFiles.push({
52
+ path: displayPath(absolute),
53
+ content: content.length > MAX_CONTEXT_FILE_CHARS
54
+ ? `${content.slice(0, MAX_CONTEXT_FILE_CHARS)}\n\n[Context file truncated: ${content.length - MAX_CONTEXT_FILE_CHARS} characters omitted]`
55
+ : content,
56
+ });
57
+ }
58
+ return contextFiles;
59
+ }
@@ -0,0 +1,4 @@
1
+ export declare const INPUT_HISTORY_FILE: string;
2
+ export declare function readInputHistory(): Promise<string[]>;
3
+ export declare function writeInputHistory(history: string[]): Promise<void>;
4
+ export declare function addInputHistoryItem(item: string): Promise<string[]>;
@@ -0,0 +1,29 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { HAZE_DIR } from './paths.js';
4
+ const HISTORY_DIR = path.join(HAZE_DIR, 'history');
5
+ export const INPUT_HISTORY_FILE = path.join(HISTORY_DIR, 'input-history.json');
6
+ const MAX_HISTORY_ITEMS = 500;
7
+ function normalizeHistory(value) {
8
+ if (!Array.isArray(value))
9
+ return [];
10
+ return value.filter((item) => typeof item === 'string' && item.trim().length > 0);
11
+ }
12
+ export async function readInputHistory() {
13
+ const data = await fs.readJson(INPUT_HISTORY_FILE).catch(() => []);
14
+ return normalizeHistory(data).slice(-MAX_HISTORY_ITEMS);
15
+ }
16
+ export async function writeInputHistory(history) {
17
+ const normalized = normalizeHistory(history).slice(-MAX_HISTORY_ITEMS);
18
+ await fs.ensureDir(HISTORY_DIR);
19
+ await fs.writeJson(INPUT_HISTORY_FILE, normalized, { spaces: 2 });
20
+ }
21
+ export async function addInputHistoryItem(item) {
22
+ const trimmed = item.trim();
23
+ if (!trimmed)
24
+ return readInputHistory();
25
+ const current = await readInputHistory();
26
+ const next = current[current.length - 1] === trimmed ? current : [...current, trimmed];
27
+ await writeInputHistory(next);
28
+ return next.slice(-MAX_HISTORY_ITEMS);
29
+ }
@@ -0,0 +1,3 @@
1
+ export declare const HAZE_DIR: string;
2
+ export declare const GLOBAL_SKILLS_DIR: string;
3
+ export declare const LOCAL_SKILLS_DIR: string;
@@ -0,0 +1,5 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export const HAZE_DIR = path.join(os.homedir(), '.haze');
4
+ export const GLOBAL_SKILLS_DIR = path.join(HAZE_DIR, 'skills');
5
+ export const LOCAL_SKILLS_DIR = path.join(process.cwd(), '.haze', 'skills');
@@ -0,0 +1,10 @@
1
+ export interface HazeSettings {
2
+ provider?: 'openrouter';
3
+ apiKey?: string;
4
+ baseURL?: string;
5
+ model?: string;
6
+ }
7
+ export declare const SETTINGS_FILE: string;
8
+ export declare function readSettings(): Promise<HazeSettings>;
9
+ export declare function writeSettings(settings: HazeSettings): Promise<void>;
10
+ export declare function updateSettings(patch: HazeSettings): Promise<HazeSettings>;
@@ -0,0 +1,16 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { HAZE_DIR } from './paths.js';
4
+ export const SETTINGS_FILE = path.join(HAZE_DIR, 'settings.json');
5
+ export async function readSettings() {
6
+ return fs.readJson(SETTINGS_FILE).catch(() => ({}));
7
+ }
8
+ export async function writeSettings(settings) {
9
+ await fs.ensureDir(HAZE_DIR);
10
+ await fs.writeJson(SETTINGS_FILE, settings, { spaces: 2 });
11
+ }
12
+ export async function updateSettings(patch) {
13
+ const next = { ...await readSettings(), ...patch };
14
+ await writeSettings(next);
15
+ return next;
16
+ }
@@ -0,0 +1 @@
1
+ export declare function model(): Promise<import("@ai-sdk/provider").LanguageModelV3 | null>;
@@ -0,0 +1,11 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import { readSettings } from '../config/settings.js';
3
+ export async function model() {
4
+ const settings = await readSettings();
5
+ const baseURL = process.env.OPENAI_BASE_URL ?? settings.baseURL;
6
+ const apiKey = process.env.OPENAI_API_KEY ?? settings.apiKey;
7
+ const name = process.env.HAZE_MODEL ?? settings.model ?? 'openai/gpt-4o-mini';
8
+ if (!apiKey)
9
+ return null;
10
+ return createOpenAI({ apiKey, baseURL })(name);
11
+ }
@@ -0,0 +1,82 @@
1
+ export declare const hazeTools: {
2
+ listFiles: import("ai").Tool<{
3
+ path: string;
4
+ recursive: boolean;
5
+ maxEntries: number;
6
+ includeIgnored: boolean;
7
+ }, {
8
+ path: string;
9
+ recursive: boolean;
10
+ includeIgnored: boolean;
11
+ ignoredSkipped: number;
12
+ entries: {
13
+ path: string;
14
+ type: "file" | "directory";
15
+ size?: number;
16
+ }[];
17
+ truncated: boolean;
18
+ }>;
19
+ readFile: import("ai").Tool<{
20
+ path: string;
21
+ allowIgnored: boolean;
22
+ offset?: number | undefined;
23
+ limit?: number | undefined;
24
+ }, {
25
+ text: string;
26
+ truncated: boolean;
27
+ omittedChars?: undefined;
28
+ path: string;
29
+ startLine: number;
30
+ endLine: number;
31
+ totalLines: number;
32
+ lineNumberedText: string;
33
+ } | {
34
+ text: string;
35
+ truncated: boolean;
36
+ omittedChars: number;
37
+ path: string;
38
+ startLine: number;
39
+ endLine: number;
40
+ totalLines: number;
41
+ lineNumberedText: string;
42
+ }>;
43
+ replaceLines: import("ai").Tool<{
44
+ path: string;
45
+ startLine: number;
46
+ endLine: number;
47
+ content: string;
48
+ allowIgnored: boolean;
49
+ }, {
50
+ ok: boolean;
51
+ path: string;
52
+ startLine: number;
53
+ endLine: number;
54
+ replacementLines: number;
55
+ }>;
56
+ writeFile: import("ai").Tool<{
57
+ path: string;
58
+ content: string;
59
+ allowIgnored: boolean;
60
+ }, {
61
+ ok: boolean;
62
+ path: string;
63
+ bytes: number;
64
+ }>;
65
+ editFile: import("ai").Tool<{
66
+ path: string;
67
+ edits: {
68
+ oldText: string;
69
+ newText: string;
70
+ }[];
71
+ allowIgnored: boolean;
72
+ }, {
73
+ ok: boolean;
74
+ path: string;
75
+ edits: number;
76
+ }>;
77
+ bash: import("ai").Tool<{
78
+ command: string;
79
+ timeoutSeconds?: number | undefined;
80
+ }, unknown>;
81
+ };
82
+ export type HazeTools = typeof hazeTools;
@@ -0,0 +1,226 @@
1
+ import { execFile as execFileCallback, spawn } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { tool } from 'ai';
6
+ import { z } from 'zod';
7
+ import { walkDir } from '../utils/fs.js';
8
+ import { workspaceRoot, resolveWorkspacePath, workspaceRelativePath } from '../utils/path.js';
9
+ const MAX_OUTPUT_CHARS = 50_000;
10
+ const execFile = promisify(execFileCallback);
11
+ async function isGitIgnored(absolutePath) {
12
+ const relative = workspaceRelativePath(absolutePath);
13
+ if (relative === '.')
14
+ return false;
15
+ try {
16
+ await execFile('git', ['-C', workspaceRoot(), 'check-ignore', '-q', '--', relative]);
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ const status = typeof error === 'object' && error != null && 'code' in error ? error.code : undefined;
21
+ if (status === 1 || status === 128)
22
+ return false;
23
+ return false;
24
+ }
25
+ }
26
+ async function assertNotIgnored(absolutePath, inputPath, allowIgnored) {
27
+ if (!allowIgnored && await isGitIgnored(absolutePath)) {
28
+ throw new Error(`Path is ignored by .gitignore: ${inputPath}. Set allowIgnored=true only if you explicitly need to access ignored files.`);
29
+ }
30
+ }
31
+ function truncate(text, maxChars = MAX_OUTPUT_CHARS) {
32
+ if (text.length <= maxChars)
33
+ return { text, truncated: false };
34
+ return {
35
+ text: text.slice(0, maxChars),
36
+ truncated: true,
37
+ omittedChars: text.length - maxChars,
38
+ };
39
+ }
40
+ function numberLines(lines, startLine) {
41
+ return lines.map((line, index) => `${String(startLine + index).padStart(4, ' ')} | ${line}`).join('\n');
42
+ }
43
+ export const hazeTools = {
44
+ listFiles: tool({
45
+ description: 'List files and directories in the current workspace. Prefer this over bash ls/find for discovering project structure.',
46
+ inputSchema: z.object({
47
+ path: z.string().default('.').describe('Directory path relative to the current workspace'),
48
+ recursive: z.boolean().default(false).describe('Whether to list files recursively'),
49
+ maxEntries: z.number().int().positive().max(500).default(100).describe('Maximum number of entries to return'),
50
+ includeIgnored: z.boolean().default(false).describe('Include files ignored by .gitignore. Use only when explicitly needed.'),
51
+ }),
52
+ execute: async ({ path: dirPath, recursive, maxEntries, includeIgnored }) => {
53
+ const absolutePath = resolveWorkspacePath(dirPath);
54
+ await assertNotIgnored(absolutePath, dirPath, includeIgnored);
55
+ const entries = [];
56
+ let ignoredSkipped = 0;
57
+ const walked = await walkDir(absolutePath, { recursive, maxEntries, filter: async (entry) => {
58
+ if (!includeIgnored && await isGitIgnored(entry.absolutePath)) {
59
+ ignoredSkipped++;
60
+ return false;
61
+ }
62
+ return true;
63
+ } });
64
+ for (const entry of walked) {
65
+ if (entry.isDirectory) {
66
+ entries.push({ path: entry.path, type: 'directory' });
67
+ }
68
+ else if (entry.isFile) {
69
+ const stat = await fs.stat(entry.absolutePath);
70
+ entries.push({ path: entry.path, type: 'file', size: stat.size });
71
+ }
72
+ }
73
+ return { path: dirPath, recursive, includeIgnored, ignoredSkipped, entries, truncated: entries.length >= maxEntries };
74
+ },
75
+ }),
76
+ readFile: tool({
77
+ description: 'Read a UTF-8 text file from the current workspace. Supports optional 1-based line offset and line limit.',
78
+ inputSchema: z.object({
79
+ path: z.string().describe('File path relative to the current workspace'),
80
+ offset: z.number().int().positive().optional().describe('1-based line number to start reading from'),
81
+ limit: z.number().int().positive().max(2000).optional().describe('Maximum number of lines to return'),
82
+ allowIgnored: z.boolean().default(false).describe('Read the file even if it is ignored by .gitignore. Use only when explicitly needed.'),
83
+ }),
84
+ execute: async ({ path: filePath, offset, limit, allowIgnored }) => {
85
+ const absolutePath = resolveWorkspacePath(filePath);
86
+ await assertNotIgnored(absolutePath, filePath, allowIgnored);
87
+ const content = await fs.readFile(absolutePath, 'utf8');
88
+ const lines = content.split(/\r?\n/);
89
+ const start = offset == null ? 0 : offset - 1;
90
+ const end = limit == null ? lines.length : start + limit;
91
+ const selectedLines = lines.slice(start, end);
92
+ const selected = selectedLines.join('\n');
93
+ return {
94
+ path: filePath,
95
+ startLine: start + 1,
96
+ endLine: Math.min(end, lines.length),
97
+ totalLines: lines.length,
98
+ lineNumberedText: numberLines(selectedLines, start + 1),
99
+ ...truncate(selected),
100
+ };
101
+ },
102
+ }),
103
+ replaceLines: tool({
104
+ description: 'Replace a 1-based inclusive line range in an existing UTF-8 text file. Prefer this after reading a file when exact editFile replacements are ambiguous or fail.',
105
+ inputSchema: z.object({
106
+ path: z.string().describe('File path relative to the current workspace'),
107
+ startLine: z.number().int().positive().describe('First 1-based line number to replace'),
108
+ endLine: z.number().int().positive().describe('Last 1-based line number to replace, inclusive'),
109
+ content: z.string().describe('Replacement content for the line range'),
110
+ allowIgnored: z.boolean().default(false).describe('Edit the file even if it is ignored by .gitignore. Use only when explicitly needed.'),
111
+ }),
112
+ execute: async ({ path: filePath, startLine, endLine, content, allowIgnored }) => {
113
+ if (endLine < startLine)
114
+ throw new Error('endLine must be greater than or equal to startLine');
115
+ const absolutePath = resolveWorkspacePath(filePath);
116
+ await assertNotIgnored(absolutePath, filePath, allowIgnored);
117
+ const original = await fs.readFile(absolutePath, 'utf8');
118
+ const hasTrailingNewline = original.endsWith('\n');
119
+ const lines = original.split(/\r?\n/);
120
+ if (hasTrailingNewline)
121
+ lines.pop();
122
+ if (startLine > lines.length + 1)
123
+ throw new Error(`startLine ${startLine} is beyond end of file (${lines.length} lines)`);
124
+ if (endLine > lines.length)
125
+ throw new Error(`endLine ${endLine} is beyond end of file (${lines.length} lines)`);
126
+ const replacementLines = content.length === 0 ? [] : content.split(/\r?\n/);
127
+ lines.splice(startLine - 1, endLine - startLine + 1, ...replacementLines);
128
+ const updated = lines.join('\n') + (hasTrailingNewline ? '\n' : '');
129
+ await fs.writeFile(absolutePath, updated, 'utf8');
130
+ return { ok: true, path: filePath, startLine, endLine, replacementLines: replacementLines.length };
131
+ },
132
+ }),
133
+ writeFile: tool({
134
+ description: 'Create or overwrite a UTF-8 text file in the current workspace. Creates parent directories as needed. Use for new files or after smaller edit tools are not suitable.',
135
+ inputSchema: z.object({
136
+ path: z.string().describe('File path relative to the current workspace'),
137
+ content: z.string().describe('Complete file contents to write'),
138
+ allowIgnored: z.boolean().default(false).describe('Write the file even if it is ignored by .gitignore. Use only when explicitly needed.'),
139
+ }),
140
+ execute: async ({ path: filePath, content, allowIgnored }) => {
141
+ const absolutePath = resolveWorkspacePath(filePath);
142
+ await assertNotIgnored(absolutePath, filePath, allowIgnored);
143
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
144
+ await fs.writeFile(absolutePath, content, 'utf8');
145
+ return { ok: true, path: filePath, bytes: Buffer.byteLength(content, 'utf8') };
146
+ },
147
+ }),
148
+ editFile: tool({
149
+ description: 'Edit a text file using exact replacements. Each oldText must match exactly once in the original file and edits must not overlap. If this fails because text is missing or not unique, do not retry; use replaceLines instead.',
150
+ inputSchema: z.object({
151
+ path: z.string().describe('File path relative to the current workspace'),
152
+ edits: z.array(z.object({
153
+ oldText: z.string().min(1).describe('Exact text to replace; must appear exactly once'),
154
+ newText: z.string().describe('Replacement text'),
155
+ })).min(1).describe('One or more non-overlapping exact replacements'),
156
+ allowIgnored: z.boolean().default(false).describe('Edit the file even if it is ignored by .gitignore. Use only when explicitly needed.'),
157
+ }),
158
+ execute: async ({ path: filePath, edits, allowIgnored }) => {
159
+ const absolutePath = resolveWorkspacePath(filePath);
160
+ await assertNotIgnored(absolutePath, filePath, allowIgnored);
161
+ const original = await fs.readFile(absolutePath, 'utf8');
162
+ const ranges = edits.map((edit, index) => {
163
+ const first = original.indexOf(edit.oldText);
164
+ if (first === -1)
165
+ throw new Error(`edit ${index}: oldText was not found`);
166
+ const second = original.indexOf(edit.oldText, first + edit.oldText.length);
167
+ if (second !== -1)
168
+ throw new Error(`edit ${index}: oldText is not unique`);
169
+ return { index, start: first, end: first + edit.oldText.length, edit };
170
+ }).sort((a, b) => a.start - b.start);
171
+ for (let i = 1; i < ranges.length; i++) {
172
+ if (ranges[i].start < ranges[i - 1].end) {
173
+ throw new Error(`edits ${ranges[i - 1].index} and ${ranges[i].index} overlap`);
174
+ }
175
+ }
176
+ let updated = original;
177
+ for (const range of [...ranges].sort((a, b) => b.start - a.start)) {
178
+ updated = updated.slice(0, range.start) + range.edit.newText + updated.slice(range.end);
179
+ }
180
+ await fs.writeFile(absolutePath, updated, 'utf8');
181
+ return { ok: true, path: filePath, edits: edits.length };
182
+ },
183
+ }),
184
+ bash: tool({
185
+ description: 'Run a bash command in the current workspace. Use for inspecting files, running tests, builds, and other shell tasks. Avoid destructive commands unless the user explicitly asked.',
186
+ inputSchema: z.object({
187
+ command: z.string().min(1).describe('Command to execute with bash -lc'),
188
+ timeoutSeconds: z.number().int().positive().max(600).optional().describe('Timeout in seconds; defaults to 60'),
189
+ }),
190
+ execute: async ({ command, timeoutSeconds }, { abortSignal }) => {
191
+ const timeoutMs = (timeoutSeconds ?? 60) * 1000;
192
+ return await new Promise(resolve => {
193
+ const child = spawn('bash', ['-lc', command], { cwd: workspaceRoot(), stdio: ['ignore', 'pipe', 'pipe'] });
194
+ let stdout = '';
195
+ let stderr = '';
196
+ let settled = false;
197
+ const timer = setTimeout(() => {
198
+ if (!settled)
199
+ child.kill('SIGTERM');
200
+ }, timeoutMs);
201
+ const abort = () => child.kill('SIGTERM');
202
+ abortSignal?.addEventListener('abort', abort, { once: true });
203
+ child.stdout.on('data', data => stdout += data.toString());
204
+ child.stderr.on('data', data => stderr += data.toString());
205
+ child.on('close', code => {
206
+ settled = true;
207
+ clearTimeout(timer);
208
+ abortSignal?.removeEventListener('abort', abort);
209
+ resolve({
210
+ ok: code === 0,
211
+ code,
212
+ command,
213
+ stdout: truncate(stdout),
214
+ stderr: truncate(stderr),
215
+ });
216
+ });
217
+ child.on('error', error => {
218
+ settled = true;
219
+ clearTimeout(timer);
220
+ abortSignal?.removeEventListener('abort', abort);
221
+ resolve({ ok: false, command, error: error.message });
222
+ });
223
+ });
224
+ },
225
+ }),
226
+ };
@@ -0,0 +1 @@
1
+ export declare function buildInitPrompt(): string;
@@ -0,0 +1,19 @@
1
+ export function buildInitPrompt() {
2
+ return `Initialize this repository for Haze by creating a best-practice AGENTS.md file.
3
+
4
+ Explore the codebase first, respecting .gitignore:
5
+ 1. Use listFiles recursively from the workspace root.
6
+ 2. Read only the files needed to understand project conventions, commands, architecture, and release workflow. Usually read package/config files, README, and key source entrypoints.
7
+ 3. Do not read ignored files unless truly necessary.
8
+
9
+ Create or update AGENTS.md at the workspace root. It should be concise and useful for future coding agents. Include sections when known:
10
+ - Project overview
11
+ - Common commands for install, development, typecheck, build, test, lint, release
12
+ - Architecture and important directories
13
+ - Coding conventions
14
+ - Tooling and package manager notes
15
+ - Testing/validation expectations
16
+ - Safety notes or files/directories to avoid
17
+
18
+ If AGENTS.md already exists, preserve useful existing instructions and improve them. After writing it, summarize what you learned and what you wrote.`;
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { ContextFile } from '../config/contextFiles.js';
2
+ export declare function buildSystemPrompt(contextFiles?: ContextFile[]): string;
@@ -0,0 +1,33 @@
1
+ export function buildSystemPrompt(contextFiles = []) {
2
+ const date = new Date().toISOString().slice(0, 10);
3
+ const cwd = process.cwd().replace(/\\/g, '/');
4
+ const projectContext = contextFiles.length > 0 ? `\n\n<project_context>\nProject-specific instructions and guidelines:\n\n${contextFiles.map(file => `<project_instructions path="${file.path}">\n${file.content}\n</project_instructions>`).join('\n\n')}\n</project_context>` : '';
5
+ return `You are Haze, an expert coding assistant operating inside a terminal-based agent CLI. You help users build apps by understanding the current conversation, inspecting projects, running commands, and editing files.
6
+
7
+ Available tools:
8
+ - listFiles: List files and directories in the current workspace. Prefer this over bash ls/find for project discovery.
9
+ - readFile: Read UTF-8 files with optional line ranges. Returns lineNumberedText for line-based edits.
10
+ - editFile: Edit files with exact unique text replacements. Use only for small, unambiguous replacements.
11
+ - replaceLines: Replace a 1-based inclusive line range. Use when editFile is ambiguous or has failed once.
12
+ - writeFile: Create or overwrite files. Use for new files or intentional complete rewrites.
13
+ - bash: Run shell commands for tests, builds, scripts, and inspection that cannot be done with file tools.
14
+
15
+ Guidelines:
16
+ - Be concise, technical, and practical.
17
+ - Preserve user-provided content exactly. When the user asks to add, modify, or use "this", "that", "it", or previous content, refer to the current conversation and do not substitute different text.
18
+ - Use listFiles for project discovery instead of bash ls/find.
19
+ - Do not list or read the same path repeatedly unless the file changed or the previous result was insufficient.
20
+ - Read only directly relevant files, usually once. Do not read README/package files unless needed for the task.
21
+ - File tools follow .gitignore by default. Only set includeIgnored/allowIgnored when the user explicitly asks or the task truly requires ignored files, and say why.
22
+ - Prefer editFile for existing files when one small exact replacement is unique.
23
+ - If editFile fails because oldText is missing or not unique, do not retry editFile for the same change; use replaceLines with lineNumberedText from readFile.
24
+ - Use writeFile only for new files or complete rewrites.
25
+ - Use bash mainly for tests, builds, package scripts, and commands that are not covered by file tools.
26
+ - After making changes, validate with the project's relevant test/typecheck/build command when practical.
27
+ - After tool use, always respond with a concise summary of what changed or what failed.
28
+ - Ask before destructive actions.
29
+ - Show file paths clearly when working with files.${projectContext}
30
+
31
+ Current date: ${date}
32
+ Current working directory: ${cwd}`;
33
+ }
@@ -0,0 +1,2 @@
1
+ import type { LoadedSkill } from './types.js';
2
+ export declare function loadSkill(dir: string, source: 'global' | 'local'): Promise<LoadedSkill | null>;
@@ -0,0 +1,22 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { readYaml } from '../utils/yaml.js';
4
+ import { skillManifestSchema } from './manifestSchema.js';
5
+ export async function loadSkill(dir, source) {
6
+ const manifestPath = path.join(dir, 'skill.yaml');
7
+ if (!(await fs.pathExists(manifestPath)))
8
+ return null;
9
+ const raw = await readYaml(manifestPath);
10
+ const manifest = skillManifestSchema.parse(raw);
11
+ const prompts = await Promise.all((manifest.prompts ?? []).map(async (p) => {
12
+ const absolutePath = path.resolve(dir, p.path);
13
+ return { ...p, absolutePath, content: await fs.readFile(absolutePath, 'utf8') };
14
+ }));
15
+ const tools = (manifest.tools ?? []).map(t => ({
16
+ ...t,
17
+ id: `${manifest.name}.${t.name}`,
18
+ skillName: manifest.name,
19
+ absolutePath: path.resolve(dir, t.path)
20
+ }));
21
+ return { dir, manifestPath, manifest, prompts, tools, source };
22
+ }
@@ -0,0 +1,6 @@
1
+ import type { LoadedSkill, LoadedTool } from './types.js';
2
+ export interface SkillRegistry {
3
+ skills: Map<string, LoadedSkill>;
4
+ tools: Map<string, LoadedTool>;
5
+ }
6
+ export declare function loadSkillRegistry(): Promise<SkillRegistry>;
@@ -0,0 +1,28 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR } from '../config/paths.js';
4
+ import { loadSkill } from './SkillLoader.js';
5
+ export async function loadSkillRegistry() {
6
+ const skills = new Map();
7
+ const tools = new Map();
8
+ async function scanDir(root, source) {
9
+ if (!(await fs.pathExists(root)))
10
+ return;
11
+ for (const name of await fs.readdir(root)) {
12
+ const dir = path.join(root, name);
13
+ if (!(await fs.stat(dir)).isDirectory())
14
+ continue;
15
+ const skill = await loadSkill(dir, source);
16
+ if (skill)
17
+ skills.set(skill.manifest.name, skill);
18
+ }
19
+ }
20
+ await fs.ensureDir(GLOBAL_SKILLS_DIR);
21
+ await fs.ensureDir(LOCAL_SKILLS_DIR);
22
+ await scanDir(GLOBAL_SKILLS_DIR, 'global');
23
+ await scanDir(LOCAL_SKILLS_DIR, 'local');
24
+ for (const skill of skills.values())
25
+ for (const tool of skill.tools)
26
+ tools.set(tool.id, tool);
27
+ return { skills, tools };
28
+ }
@@ -0,0 +1 @@
1
+ export declare function buildSkill(description: string): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { input, confirm } from '@inquirer/prompts';
4
+ import { GLOBAL_SKILLS_DIR } from '../../config/paths.js';
5
+ import { writeYaml } from '../../utils/yaml.js';
6
+ function slug(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'custom-skill'; }
7
+ export async function buildSkill(description) {
8
+ const name = await input({ message: 'Skill name', default: slug(description) });
9
+ const toolName = await input({ message: 'First tool name', default: 'run' });
10
+ const toolDescription = await input({ message: 'What should this tool do?', default: description });
11
+ const dir = path.join(GLOBAL_SKILLS_DIR, name);
12
+ const files = [path.join(dir, 'skill.yaml'), path.join(dir, 'README.md'), path.join(dir, 'tools', `${toolName}.ts`), path.join(dir, 'prompts', 'planning.md')];
13
+ console.log('\nHaze will write:');
14
+ files.forEach(f => console.log(` ${f}`));
15
+ const ok = await confirm({ message: 'Create these skill files?', default: false });
16
+ if (!ok)
17
+ return;
18
+ await fs.ensureDir(path.join(dir, 'tools'));
19
+ await fs.ensureDir(path.join(dir, 'prompts'));
20
+ await writeYaml(files[0], { name, version: '0.1.0', description, tools: [{ name: toolName, description: toolDescription, path: `tools/${toolName}.ts`, input: { type: 'object', properties: {} } }], prompts: [{ name: 'planning', description: 'Planning guidance for this skill.', path: 'prompts/planning.md' }] });
21
+ await fs.writeFile(files[1], `# ${name}\n\n${description}\n`);
22
+ await fs.writeFile(files[2], `export async function execute(input: Record<string, unknown>, context: {cwd: string}) {\n return {ok: false, message: 'Tool ${toolName} is a generated stub. Edit this file to make it useful.', data: {input, cwd: context.cwd}};\n}\n`);
23
+ await fs.writeFile(files[3], `Use this skill only when the user request matches: ${description}\n`);
24
+ console.log(`Created ${name}. Please edit the tool before expecting miracles.`);
25
+ }
@@ -0,0 +1 @@
1
+ export declare function installSkill(spec: string): Promise<void>;