@denizokcu/haze 0.0.1 → 0.0.2

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 (51) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +114 -69
  3. package/dist/cli/commands/chat.d.ts +1 -0
  4. package/dist/cli/commands/chat.js +203 -11
  5. package/dist/cli/commands/commands.js +130 -6
  6. package/dist/cli/commands/formatters.d.ts +1 -0
  7. package/dist/cli/commands/formatters.js +18 -1
  8. package/dist/cli/commands/skills.d.ts +1 -1
  9. package/dist/cli/commands/skills.js +8 -5
  10. package/dist/cli/commands/streaming.d.ts +2 -0
  11. package/dist/cli/commands/streaming.js +424 -39
  12. package/dist/cli/index.js +1 -11
  13. package/dist/config/paths.d.ts +0 -1
  14. package/dist/config/paths.js +0 -1
  15. package/dist/llm/client.js +1 -1
  16. package/dist/llm/hazeTools.d.ts +32 -0
  17. package/dist/llm/hazeTools.js +136 -26
  18. package/dist/llm/initPrompt.js +2 -2
  19. package/dist/llm/systemPrompt.js +23 -9
  20. package/dist/skills/SkillLoader.d.ts +12 -2
  21. package/dist/skills/SkillLoader.js +64 -18
  22. package/dist/skills/SkillRegistry.d.ts +1 -5
  23. package/dist/skills/SkillRegistry.js +10 -21
  24. package/dist/skills/builder/SkillBuilder.d.ts +25 -1
  25. package/dist/skills/builder/SkillBuilder.js +169 -20
  26. package/dist/skills/skillTools.d.ts +20 -0
  27. package/dist/skills/skillTools.js +25 -0
  28. package/dist/skills/types.d.ts +12 -51
  29. package/dist/ui/components/Header.d.ts +2 -1
  30. package/dist/ui/components/Header.js +12 -2
  31. package/dist/ui/components/TextInput.d.ts +8 -1
  32. package/dist/ui/components/TextInput.js +29 -14
  33. package/dist/ui/theme.d.ts +1 -0
  34. package/dist/ui/theme.js +1 -0
  35. package/dist/utils/fs.d.ts +1 -0
  36. package/dist/utils/fs.js +10 -6
  37. package/examples/skills/files/SKILL.md +16 -0
  38. package/examples/skills/files/examples/file-editing.md +3 -0
  39. package/package.json +2 -2
  40. package/dist/skills/installer/SkillInstaller.d.ts +0 -1
  41. package/dist/skills/installer/SkillInstaller.js +0 -48
  42. package/dist/skills/manifestSchema.d.ts +0 -31
  43. package/dist/skills/manifestSchema.js +0 -23
  44. package/dist/tools/ToolExecutor.d.ts +0 -3
  45. package/dist/tools/ToolExecutor.js +0 -15
  46. package/dist/tools/types.d.ts +0 -9
  47. package/dist/tools/types.js +0 -1
  48. package/examples/skills/files/prompts/file_tasks.md +0 -1
  49. package/examples/skills/files/skill.yaml +0 -28
  50. package/examples/skills/files/tools/list_files.ts +0 -21
  51. package/examples/skills/files/tools/read_file.ts +0 -12
@@ -1,25 +1,174 @@
1
+ import { generateObject } from 'ai';
1
2
  import fs from 'fs-extra';
2
3
  import path from 'node:path';
3
- import { input, confirm } from '@inquirer/prompts';
4
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 });
5
+ import { model } from '../../llm/client.js';
6
+ import { loadSkill } from '../SkillLoader.js';
7
+ import { z } from 'zod';
8
+ const STANDARD_SKILL_REQUIREMENTS = `
9
+
10
+ # Operational guardrails
11
+
12
+ - Always ground the work in actual tool output or file contents before producing the final answer.
13
+ - Define the exact commands, files, or project state that count as input for this workflow.
14
+ - Inspect large inputs incrementally. Prefer summary/list commands first, then targeted per-file reads or per-file diffs for the files most relevant to the goal.
15
+ - If a command output is truncated, do not stop. Run narrower commands or read specific files to gather enough evidence for a useful answer.
16
+ - If the primary expected input is empty, check the natural fallback inputs before stopping. For example, when reviewing a branch diff, also inspect staged and unstaged working-tree changes.
17
+ - Only report "nothing to do" when every explicitly relevant input source has been checked and is empty.
18
+ - Only call something a blocker when a concrete tool failure, missing permission, missing dependency, or ambiguous user requirement prevents progress. Truncated output is not a blocker when narrower follow-up inspection is possible.
19
+ - Do not stop after status/summary commands when the workflow requires analysis; inspect the actual content to analyze.
20
+ - In the final response, cite the concrete files, commands, or evidence used. Exact line numbers are helpful but must not be required when the available evidence supports file/function-level feedback.
21
+ `;
22
+ const SKILL_CREATOR_SKILL = `---
23
+ name: skill-creator
24
+ description: Use when the user asks Haze to create a new skill from a natural-language description.
25
+ ---
26
+
27
+ You create predictable, high-quality Haze skills.
28
+
29
+ A Haze skill is a directory in ~/.haze/skills containing SKILL.md and optional referenced files.
30
+ SKILL.md must be Markdown with YAML frontmatter:
31
+
32
+ ---
33
+ name: kebab-case-name
34
+ description: Use when the user asks ...
35
+ ---
36
+
37
+ The description must tell the model exactly when to use the skill.
38
+ The body must be a deterministic operating procedure, not generic advice.
39
+ Additional files are allowed only when SKILL.md explicitly references them with relative paths.
40
+ Skills do not execute code. They teach Haze how to behave for a workflow.
41
+
42
+ Every skill you create must include:
43
+ - Goal: the user's underlying intent, outcome, and definition of success inferred from their description.
44
+ - Trigger: when to use the skill.
45
+ - Inputs to inspect: exact commands/files/state to read, with incremental inspection for large outputs.
46
+ - Procedure: ordered steps with fallback paths for empty, missing, or truncated primary inputs.
47
+ - Stop conditions: when it is valid to say there is nothing to do.
48
+ - Blocker policy: concrete conditions that justify stopping, excluding truncation when narrower inspection is possible.
49
+ - Output format: predictable sections for the final answer.
50
+ - Evidence rule: require final answers to be grounded in actual inspected content.
51
+
52
+ Infer the user's intent from their wording and make that intent the explicit goal of the skill. A good goal states what the skill should accomplish, not just what commands it should run.
53
+ Avoid fragile skills that stop after one empty command or one truncated command. If a workflow has common edge cases, encode them explicitly.
54
+ For diff/review skills, require this pattern: get branch/status/stat/name-only first; if a full diff is empty, inspect staged and unstaged diffs; if a full diff is truncated, inspect targeted per-file diffs or read changed files before reviewing.
55
+ Do not require exact line citations for every finding in generated skills; require concrete file/function/code-area evidence instead, with exact lines only when available.
56
+ `;
57
+ const generatedSkillSchema = z.object({
58
+ name: z.string().min(1).describe('Kebab-case skill name'),
59
+ files: z.array(z.object({
60
+ path: z.string().min(1).describe('Relative file path inside the skill directory'),
61
+ content: z.string().describe('Complete file content'),
62
+ })).min(1).describe('Generated skill files, including SKILL.md'),
63
+ });
64
+ export function slug(s) {
65
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'custom-skill';
66
+ }
67
+ function yamlString(value) {
68
+ return JSON.stringify(value);
69
+ }
70
+ function fallbackSkill(description) {
71
+ const name = slug(description);
72
+ return {
73
+ name,
74
+ files: [{
75
+ path: 'SKILL.md',
76
+ content: `---\nname: ${name}\ndescription: ${yamlString(`Use when the user asks: ${description}`)}\n---\n\n# Goal\n\nAccomplish the user's intended outcome: ${description}\n\n# Trigger\n\nUse this skill when the user asks: ${description}\n\n# Inputs to inspect\n\nIdentify the concrete commands, files, diffs, logs, or project state needed for this workflow. Inspect actual content, not only summaries.\n\n# Procedure\n\n1. Confirm the relevant project state.\n2. Inspect the primary input for the workflow.\n3. If the primary input is empty, unavailable, or truncated, inspect natural fallback inputs or narrower targeted inputs before stopping.\n4. For large inputs, inspect summaries first, then targeted files, sections, or commands most relevant to the goal.\n5. Perform the requested analysis or implementation using the inspected evidence.\n6. Produce the final answer in the output format below.\n\n# Stop conditions\n\nOnly say there is nothing to do after every relevant input source has been checked and is empty.\n\n# Blocker policy\n\nOnly stop as blocked for a concrete tool failure, missing permission, unavailable dependency, or ambiguous requirement that prevents progress. Truncated output is not a blocker when narrower follow-up inspection is possible.\n\n# Output format\n\n- Summary\n- Findings or actions\n- Evidence inspected\n- Recommendation or next step\n${STANDARD_SKILL_REQUIREMENTS}\n# References\n\nAdd relative file references here if this skill needs examples, templates, or supporting docs.\n`,
77
+ }],
78
+ };
79
+ }
80
+ function withStandardRequirements(content) {
81
+ return content.includes('# Operational guardrails') ? content : `${content.trim()}${STANDARD_SKILL_REQUIREMENTS}\n`;
82
+ }
83
+ function extractJson(text) {
84
+ const fenced = /```(?:json)?\s*([\s\S]*?)\s*```/.exec(text);
85
+ return fenced?.[1] ?? text;
86
+ }
87
+ function parseGeneratedSkill(text, description) {
88
+ const parsed = JSON.parse(extractJson(text));
89
+ const name = typeof parsed.name === 'string' && parsed.name.trim() ? slug(parsed.name) : slug(description);
90
+ const files = Array.isArray(parsed.files) ? parsed.files.filter((file) => {
91
+ return typeof file === 'object' && file != null && typeof file.path === 'string' && typeof file.content === 'string';
92
+ }) : [];
93
+ if (!files.some(file => file.path === 'SKILL.md'))
94
+ throw new Error('Generated skill did not include SKILL.md');
95
+ return { name, files };
96
+ }
97
+ function assertSafeGeneratedFile(filePath) {
98
+ if (path.isAbsolute(filePath))
99
+ throw new Error(`Generated skill file must be relative: ${filePath}`);
100
+ const normalized = path.normalize(filePath);
101
+ if (normalized === '..' || normalized.startsWith(`..${path.sep}`))
102
+ throw new Error(`Generated skill file escapes skill directory: ${filePath}`);
103
+ if (normalized.length === 0 || normalized === '.')
104
+ throw new Error('Generated skill file path is empty');
105
+ return normalized;
106
+ }
107
+ async function generateSkill(description) {
108
+ const activeModel = await model();
109
+ if (!activeModel)
110
+ throw new Error('No API key configured. Run /login, then /model x-ai/grok-build-0.1, before using /skill create.');
111
+ const result = await generateObject({
112
+ model: activeModel,
113
+ temperature: 0,
114
+ system: SKILL_CREATOR_SKILL,
115
+ schema: generatedSkillSchema,
116
+ schemaName: 'GeneratedHazeSkill',
117
+ schemaDescription: 'A generated Haze Markdown skill and optional referenced files.',
118
+ prompt: [
119
+ 'Create a Haze skill from this user description:',
120
+ description,
121
+ '',
122
+ 'Rules:',
123
+ '- SKILL.md must include frontmatter with name and description.',
124
+ '- The frontmatter description must start with "Use when".',
125
+ '- Infer the user intent from the description and make it the explicit Goal of the skill.',
126
+ '- The body must use these headings: Goal, Trigger, Inputs to inspect, Procedure, Fallbacks, Stop conditions, Blocker policy, Output format.',
127
+ '- The Goal must describe the desired outcome and definition of success, not just restate the trigger.',
128
+ '- Procedure steps must name exact commands/files/state to inspect whenever the workflow implies them.',
129
+ '- Include fallback behavior for empty or truncated primary inputs. Example: if branch diff is empty, inspect staged and unstaged changes before stopping; if full diff is truncated, inspect per-file diffs or read changed files.',
130
+ '- For workflows with potentially large outputs, require incremental inspection: stat/name-only/summary first, then targeted content reads.',
131
+ '- Define when it is valid to say "nothing to do".',
132
+ '- Define blockers narrowly: concrete tool failure, missing permission/dependency, or ambiguous requirement. Truncation alone is not a blocker if targeted follow-up inspection is possible.',
133
+ '- Require final output to cite actual evidence inspected, using exact lines when available and file/function/code-area references otherwise.',
134
+ '- Include extra files only when genuinely useful, and reference them from SKILL.md.',
135
+ '- File paths must be relative and stay inside the skill directory.',
136
+ ].join('\n'),
137
+ });
138
+ const generated = result.object;
139
+ return { name: slug(generated.name || description), files: generated.files };
140
+ }
141
+ export async function createSkill(description) {
142
+ const generated = await generateSkill(description).catch(error => {
143
+ throw error instanceof Error ? error : new Error(String(error));
144
+ });
145
+ const name = generated.name || fallbackSkill(description).name;
11
146
  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.`);
147
+ const skillFile = path.join(dir, 'SKILL.md');
148
+ await fs.ensureDir(dir);
149
+ if (await fs.pathExists(skillFile))
150
+ throw new Error(`Skill already exists: ${name}`);
151
+ for (const generatedFile of generated.files) {
152
+ const safePath = assertSafeGeneratedFile(generatedFile.path);
153
+ const absolutePath = path.join(dir, safePath);
154
+ await fs.ensureDir(path.dirname(absolutePath));
155
+ const content = safePath === 'SKILL.md' ? withStandardRequirements(generatedFile.content) : generatedFile.content;
156
+ await fs.writeFile(absolutePath, content, 'utf8');
157
+ }
158
+ if (!(await fs.pathExists(skillFile))) {
159
+ const fallback = fallbackSkill(description);
160
+ await fs.writeFile(skillFile, fallback.files[0].content, 'utf8');
161
+ }
162
+ const loaded = await loadSkill(dir, 'global');
163
+ if (!loaded)
164
+ throw new Error('Generated skill is missing SKILL.md');
165
+ if (loaded.name !== name) {
166
+ const nextDir = path.join(GLOBAL_SKILLS_DIR, loaded.name);
167
+ if (await fs.pathExists(nextDir))
168
+ throw new Error(`Skill already exists: ${loaded.name}`);
169
+ await fs.move(dir, nextDir);
170
+ return { name: loaded.name, dir: nextDir, file: path.join(nextDir, 'SKILL.md') };
171
+ }
172
+ return { name, dir, file: skillFile };
25
173
  }
174
+ export const internals = { SKILL_CREATOR_SKILL, STANDARD_SKILL_REQUIREMENTS, parseGeneratedSkill, fallbackSkill, withStandardRequirements };
@@ -0,0 +1,20 @@
1
+ import type { SkillRegistry } from './types.js';
2
+ declare function toolNameForSkill(name: string): string;
3
+ export declare function buildSkillTools(registry: SkillRegistry): {
4
+ [k: string]: import("ai").Tool<{
5
+ reason?: string | undefined;
6
+ }, {
7
+ name: string;
8
+ description: string;
9
+ reason: string | undefined;
10
+ instructions: string;
11
+ references: {
12
+ path: string;
13
+ content: string;
14
+ }[];
15
+ }>;
16
+ };
17
+ export declare const internals: {
18
+ toolNameForSkill: typeof toolNameForSkill;
19
+ };
20
+ export {};
@@ -0,0 +1,25 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ function toolNameForSkill(name) {
4
+ return `skill_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`;
5
+ }
6
+ export function buildSkillTools(registry) {
7
+ const entries = [...registry.skills.values()].map(skill => [toolNameForSkill(skill.name), tool({
8
+ description: skill.description,
9
+ inputSchema: z.object({
10
+ reason: z.string().optional().describe('Why this skill is relevant to the current task'),
11
+ }),
12
+ execute: async ({ reason }) => ({
13
+ name: skill.name,
14
+ description: skill.description,
15
+ reason,
16
+ instructions: skill.body,
17
+ references: skill.references.map(reference => ({
18
+ path: reference.path,
19
+ content: reference.content,
20
+ })),
21
+ }),
22
+ })]);
23
+ return Object.fromEntries(entries);
24
+ }
25
+ export const internals = { toolNameForSkill };
@@ -1,60 +1,21 @@
1
- export interface JsonSchema {
2
- type?: string;
3
- required?: string[];
4
- properties?: Record<string, JsonSchema & {
5
- description?: string;
6
- }>;
7
- items?: JsonSchema;
8
- description?: string;
9
- enum?: unknown[];
10
- additionalProperties?: boolean | JsonSchema;
11
- }
12
- export interface SkillManifest {
13
- name: string;
14
- version: string;
15
- description: string;
16
- author?: string;
17
- homepage?: string;
18
- dependencies?: {
19
- cli?: {
20
- name: string;
21
- description?: string;
22
- required?: boolean;
23
- }[];
24
- env?: {
25
- name: string;
26
- description?: string;
27
- required?: boolean;
28
- }[];
29
- };
30
- tools?: SkillToolManifest[];
31
- prompts?: SkillPromptManifest[];
32
- }
33
- export interface SkillToolManifest {
1
+ export interface SkillFrontmatter {
34
2
  name: string;
35
3
  description: string;
36
- path: string;
37
- input?: JsonSchema;
38
4
  }
39
- export interface SkillPromptManifest {
40
- name: string;
41
- description?: string;
5
+ export interface LoadedSkillReference {
42
6
  path: string;
7
+ absolutePath: string;
8
+ content: string;
43
9
  }
44
10
  export interface LoadedSkill {
45
11
  dir: string;
46
- manifestPath: string;
47
- manifest: SkillManifest;
48
- prompts: LoadedPrompt[];
49
- tools: LoadedTool[];
50
- source: 'global' | 'local';
51
- }
52
- export interface LoadedPrompt extends SkillPromptManifest {
53
- content: string;
54
- absolutePath: string;
12
+ path: string;
13
+ name: string;
14
+ description: string;
15
+ body: string;
16
+ references: LoadedSkillReference[];
17
+ source: 'global';
55
18
  }
56
- export interface LoadedTool extends SkillToolManifest {
57
- id: string;
58
- skillName: string;
59
- absolutePath: string;
19
+ export interface SkillRegistry {
20
+ skills: Map<string, LoadedSkill>;
60
21
  }
@@ -1,3 +1,4 @@
1
- export declare function Header({ subtitle }: {
1
+ export declare function Header({ subtitle, version }: {
2
2
  subtitle?: string;
3
+ version?: string;
3
4
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { theme } from '../theme.js';
4
- export function Header({ subtitle }) {
5
- return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Haze" }), subtitle ? _jsx(Text, { color: theme.muted, children: subtitle }) : _jsx(Text, { color: theme.muted, children: "A small agent, because apparently that is allowed." })] });
4
+ const logo = [
5
+ ' _ ',
6
+ ' | | ',
7
+ ' | |__ __ _ _______ ',
8
+ " | '_ \\ / _` |_ / _ \\",
9
+ ' | | | | (_| |/ / __/',
10
+ ' |_| |_|\\__,_/___\\___|',
11
+ ];
12
+ export function Header({ subtitle, version }) {
13
+ return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [logo.map((line, index) => index === logo.length - 1
14
+ ? _jsxs(Box, { children: [_jsx(Text, { color: index % 2 === 0 ? theme.purple : theme.violet, bold: true, children: line }), version ? _jsxs(Text, { color: theme.muted, children: [" v", version] }) : null] }, line)
15
+ : _jsx(Text, { color: index % 2 === 0 ? theme.purple : theme.violet, bold: true, children: line }, line)), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: subtitle ?? 'A tiny terminal fog machine for building software.' })] });
6
16
  }
@@ -1,9 +1,16 @@
1
- export declare function TextInput({ placeholder, disabled, mask, historyItems, recordHistory, onHistoryAdd, onSubmit }: {
1
+ export type TextInputSuggestion = {
2
+ value: string;
3
+ description?: string;
4
+ kind?: 'command' | 'skill';
5
+ };
6
+ export declare function TextInput({ placeholder, disabled, mask, historyItems, recordHistory, suggestions, onHistoryAdd, onCancel, onSubmit }: {
2
7
  placeholder?: string;
3
8
  disabled?: boolean;
4
9
  mask?: boolean;
5
10
  historyItems?: string[];
6
11
  recordHistory?: boolean;
12
+ suggestions?: TextInputSuggestion[];
7
13
  onHistoryAdd?: (value: string) => void;
14
+ onCancel?: () => void;
8
15
  onSubmit: (value: string) => void;
9
16
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import { Text, useInput } from 'ink';
3
+ import { Box, Text, useInput } from 'ink';
4
4
  import { theme } from '../theme.js';
5
- export function TextInput({ placeholder, disabled, mask, historyItems = [], recordHistory = true, onHistoryAdd, onSubmit }) {
5
+ export function TextInput({ placeholder, disabled, mask, historyItems = [], recordHistory = true, suggestions = [], onHistoryAdd, onCancel, onSubmit }) {
6
6
  const [value, setValue] = useState('');
7
7
  const [cursor, setCursor] = useState(0);
8
8
  const history = useRef(historyItems);
@@ -27,28 +27,43 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
27
27
  historyIndex.current = index;
28
28
  setInput(history.current[index] ?? '');
29
29
  }
30
+ const slashQuery = !mask && value.startsWith('/') ? value.slice(1).toLowerCase() : undefined;
31
+ const filteredSuggestions = slashQuery == null ? [] : suggestions
32
+ .filter(suggestion => suggestion.value.slice(1).toLowerCase().includes(slashQuery) || suggestion.description?.toLowerCase().includes(slashQuery))
33
+ .slice(0, 8);
34
+ const topSuggestion = filteredSuggestions[0];
35
+ function submitValue(submitted) {
36
+ if (recordHistory) {
37
+ if (history.current[history.current.length - 1] !== submitted)
38
+ history.current = [...history.current, submitted];
39
+ onHistoryAdd?.(submitted);
40
+ }
41
+ onSubmit(submitted);
42
+ }
30
43
  useInput((input, key) => {
31
- if (disabled)
44
+ if (disabled) {
45
+ if (key.escape)
46
+ onCancel?.();
32
47
  return;
48
+ }
33
49
  if (key.escape) {
34
50
  setInput('');
35
51
  historyIndex.current = null;
36
52
  draft.current = '';
37
53
  return;
38
54
  }
55
+ if (key.tab && topSuggestion) {
56
+ setInput(topSuggestion.value);
57
+ historyIndex.current = null;
58
+ return;
59
+ }
39
60
  if (key.return) {
40
- const submitted = value.trim();
61
+ const submitted = (value.startsWith('/') && topSuggestion && topSuggestion.value !== value.trim()) ? topSuggestion.value : value.trim();
41
62
  setInput('');
42
63
  historyIndex.current = null;
43
64
  draft.current = '';
44
- if (submitted) {
45
- if (recordHistory) {
46
- if (history.current[history.current.length - 1] !== submitted)
47
- history.current = [...history.current, submitted];
48
- onHistoryAdd?.(submitted);
49
- }
50
- onSubmit(submitted);
51
- }
65
+ if (submitted)
66
+ submitValue(submitted);
52
67
  return;
53
68
  }
54
69
  if (key.leftArrow) {
@@ -116,5 +131,5 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
116
131
  const beforeCursor = displayValue.slice(0, cursor);
117
132
  const cursorChar = displayValue[cursor] ?? ' ';
118
133
  const afterCursor = displayValue.slice(cursor + 1);
119
- return _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: theme.purple, children: "\u203A " }), value.length === 0 ? _jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { color: theme.muted, children: [" ", placeholder ?? 'Type a message...'] })] }) : _jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { inverse: true, children: cursorChar }), afterCursor] })] });
134
+ return _jsxs(Box, { flexDirection: "column", width: "100%", children: [filteredSuggestions.length > 0 && _jsx(Box, { flexDirection: "column", marginBottom: 1, children: filteredSuggestions.map((suggestion, index) => _jsxs(Text, { color: index === 0 ? theme.success : theme.muted, wrap: "truncate-end", children: [index === 0 ? '› ' : ' ', suggestion.value, _jsxs(Text, { color: theme.muted, children: [" ", suggestion.kind === 'skill' ? 'skill' : 'command', suggestion.description ? ` — ${suggestion.description}` : ''] })] }, suggestion.value)) }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: theme.purple, children: "\u203A " }), value.length === 0 ? _jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { color: theme.muted, dimColor: true, children: [" ", placeholder ?? 'Type a message...'] })] }) : _jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { inverse: true, children: cursorChar }), afterCursor] })] })] });
120
135
  }
@@ -2,6 +2,7 @@ export declare const theme: {
2
2
  purple: string;
3
3
  deepPurple: string;
4
4
  violet: string;
5
+ blue: string;
5
6
  muted: string;
6
7
  danger: string;
7
8
  success: string;
package/dist/ui/theme.js CHANGED
@@ -2,6 +2,7 @@ export const theme = {
2
2
  purple: '#a78bfa',
3
3
  deepPurple: '#6d28d9',
4
4
  violet: '#8b5cf6',
5
+ blue: '#60a5fa',
5
6
  muted: '#9ca3af',
6
7
  danger: '#fb7185',
7
8
  success: '#34d399',
@@ -8,6 +8,7 @@ export interface WalkEntry {
8
8
  export interface WalkOptions {
9
9
  recursive?: boolean;
10
10
  maxEntries?: number;
11
+ cursor?: string;
11
12
  filter?: (entry: WalkEntry) => boolean | Promise<boolean>;
12
13
  }
13
14
  export declare function walkDir(root: string, options?: WalkOptions): Promise<WalkEntry[]>;
package/dist/utils/fs.js CHANGED
@@ -2,10 +2,12 @@ import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
3
  const SKIP_ENTRIES = new Set(['node_modules', '.git']);
4
4
  export async function walkDir(root, options = {}) {
5
- const { recursive = false, maxEntries = Infinity, filter } = options;
5
+ const { recursive = false, maxEntries = Infinity, cursor, filter } = options;
6
6
  const result = [];
7
+ let cursorSeen = cursor == null;
7
8
  async function walk(dir) {
8
- for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
9
+ const entries = (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
10
+ for (const entry of entries) {
9
11
  if (result.length >= maxEntries)
10
12
  return;
11
13
  if (SKIP_ENTRIES.has(entry.name))
@@ -19,10 +21,12 @@ export async function walkDir(root, options = {}) {
19
21
  isDirectory: entry.isDirectory(),
20
22
  isFile: entry.isFile(),
21
23
  };
22
- if (filter && !await filter(walkEntry))
23
- continue;
24
- result.push(walkEntry);
25
- if (entry.isDirectory() && recursive)
24
+ const passesFilter = !filter || await filter(walkEntry);
25
+ if (passesFilter && cursorSeen)
26
+ result.push(walkEntry);
27
+ if (passesFilter && !cursorSeen && relativePath === cursor)
28
+ cursorSeen = true;
29
+ if (entry.isDirectory() && recursive && (!filter || passesFilter))
26
30
  await walk(absolutePath);
27
31
  }
28
32
  }
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: files
3
+ description: Use when the user asks for a careful file-inspection or file-editing workflow.
4
+ ---
5
+
6
+ Use Haze's built-in file tools rather than shell commands for file discovery and edits.
7
+
8
+ Workflow:
9
+ 1. Use `listFiles` for project discovery.
10
+ 2. Use `readFile` before editing existing files.
11
+ 3. Prefer `editFile` for small exact changes.
12
+ 4. Use `replaceLines` when exact replacement is ambiguous.
13
+ 5. Use `writeFile` only for new files or intentional complete rewrites.
14
+
15
+ References:
16
+ - examples/file-editing.md
@@ -0,0 +1,3 @@
1
+ # File editing example
2
+
3
+ When changing existing files, inspect the current contents first, make the smallest targeted edit, then run the relevant validation command when practical.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denizokcu/haze",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A pragmatic agentic CLI for building apps from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "node": ">=20"
20
20
  },
21
21
  "bin": {
22
- "haze": "./bin/haze.js"
22
+ "haze": "bin/haze.js"
23
23
  },
24
24
  "files": [
25
25
  "bin",
@@ -1 +0,0 @@
1
- export declare function installSkill(spec: string): Promise<void>;
@@ -1,48 +0,0 @@
1
- import fs from 'fs-extra';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { spawnSync } from 'node:child_process';
5
- import { confirm } from '@inquirer/prompts';
6
- import { GLOBAL_SKILLS_DIR } from '../../config/paths.js';
7
- import { loadSkill } from '../SkillLoader.js';
8
- import { listFilesRecursive } from '../../utils/fs.js';
9
- function repoUrl(spec) {
10
- if (spec.startsWith('http'))
11
- return spec;
12
- if (spec.startsWith('github:'))
13
- return `https://github.com/${spec.slice(7)}.git`;
14
- if (/^[\w.-]+\/[\w.-]+$/.test(spec))
15
- return `https://github.com/${spec}.git`;
16
- return spec;
17
- }
18
- export async function installSkill(spec) {
19
- const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'haze-skill-'));
20
- const url = repoUrl(spec);
21
- const clone = spawnSync('git', ['clone', '--depth=1', url, tmp], { stdio: 'inherit' });
22
- if (clone.status !== 0)
23
- throw new Error('git clone failed');
24
- const skill = await loadSkill(tmp, 'global');
25
- if (!skill)
26
- throw new Error('Repository does not contain a root skill.yaml');
27
- console.log(`\nSkill: ${skill.manifest.name} ${skill.manifest.version}`);
28
- console.log(skill.manifest.description);
29
- console.log('\nFiles:');
30
- for (const f of await listFilesRecursive(tmp))
31
- console.log(` ${f}`);
32
- const deps = skill.manifest.dependencies;
33
- if (deps?.cli?.length)
34
- console.log(`\nCLI dependencies: ${deps.cli.map(d => d.name).join(', ')}`);
35
- if (deps?.env?.length)
36
- console.log(`Env dependencies: ${deps.env.map(d => d.name).join(', ')}`);
37
- const dest = path.join(GLOBAL_SKILLS_DIR, skill.manifest.name);
38
- if (await fs.pathExists(dest))
39
- console.log(`\nExisting skill will be replaced: ${dest}`);
40
- const ok = await confirm({ message: 'Approve and activate this skill? It is code from the internet, regrettably.', default: false });
41
- if (!ok)
42
- return;
43
- await fs.remove(dest);
44
- await fs.ensureDir(path.dirname(dest));
45
- await fs.copy(tmp, dest, { filter: src => !src.includes(`${path.sep}.git${path.sep}`) });
46
- await fs.remove(path.join(dest, '.git'));
47
- console.log(`Installed ${skill.manifest.name} to ${dest}`);
48
- }
@@ -1,31 +0,0 @@
1
- import { z } from 'zod';
2
- export declare const skillManifestSchema: z.ZodObject<{
3
- name: z.ZodString;
4
- version: z.ZodString;
5
- description: z.ZodString;
6
- author: z.ZodOptional<z.ZodString>;
7
- homepage: z.ZodOptional<z.ZodString>;
8
- dependencies: z.ZodOptional<z.ZodObject<{
9
- cli: z.ZodOptional<z.ZodArray<z.ZodObject<{
10
- name: z.ZodString;
11
- description: z.ZodOptional<z.ZodString>;
12
- required: z.ZodOptional<z.ZodBoolean>;
13
- }, z.core.$strip>>>;
14
- env: z.ZodOptional<z.ZodArray<z.ZodObject<{
15
- name: z.ZodString;
16
- description: z.ZodOptional<z.ZodString>;
17
- required: z.ZodOptional<z.ZodBoolean>;
18
- }, z.core.$strip>>>;
19
- }, z.core.$strip>>;
20
- tools: z.ZodOptional<z.ZodArray<z.ZodObject<{
21
- name: z.ZodString;
22
- description: z.ZodString;
23
- path: z.ZodString;
24
- input: z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
25
- }, z.core.$strip>>>;
26
- prompts: z.ZodOptional<z.ZodArray<z.ZodObject<{
27
- name: z.ZodString;
28
- description: z.ZodOptional<z.ZodString>;
29
- path: z.ZodString;
30
- }, z.core.$strip>>>;
31
- }, z.core.$strip>;
@@ -1,23 +0,0 @@
1
- import { z } from 'zod';
2
- const jsonSchema = z.lazy(() => z.object({
3
- type: z.string().optional(),
4
- required: z.array(z.string()).optional(),
5
- properties: z.record(z.string(), jsonSchema).optional(),
6
- items: jsonSchema.optional(),
7
- description: z.string().optional(),
8
- enum: z.array(z.unknown()).optional(),
9
- additionalProperties: z.union([z.boolean(), jsonSchema]).optional()
10
- }).passthrough());
11
- export const skillManifestSchema = z.object({
12
- name: z.string().min(1).regex(/^[a-zA-Z0-9_-]+$/),
13
- version: z.string().min(1),
14
- description: z.string().min(1),
15
- author: z.string().optional(),
16
- homepage: z.string().url().optional(),
17
- dependencies: z.object({
18
- cli: z.array(z.object({ name: z.string(), description: z.string().optional(), required: z.boolean().optional() })).optional(),
19
- env: z.array(z.object({ name: z.string(), description: z.string().optional(), required: z.boolean().optional() })).optional()
20
- }).optional(),
21
- tools: z.array(z.object({ name: z.string().min(1), description: z.string().min(1), path: z.string().min(1), input: jsonSchema.optional() })).optional(),
22
- prompts: z.array(z.object({ name: z.string().min(1), description: z.string().optional(), path: z.string().min(1) })).optional(),
23
- });