@denizokcu/haze 0.0.1 → 0.0.3

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 (73) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +169 -70
  3. package/dist/cli/commands/chat.d.ts +4 -1
  4. package/dist/cli/commands/chat.js +606 -24
  5. package/dist/cli/commands/commands.d.ts +5 -0
  6. package/dist/cli/commands/commands.js +220 -11
  7. package/dist/cli/commands/formatters.d.ts +1 -0
  8. package/dist/cli/commands/formatters.js +23 -3
  9. package/dist/cli/commands/skills.d.ts +1 -1
  10. package/dist/cli/commands/skills.js +8 -5
  11. package/dist/cli/commands/streaming.d.ts +7 -1
  12. package/dist/cli/commands/streaming.js +533 -41
  13. package/dist/cli/index.js +5 -12
  14. package/dist/config/inputHistory.js +8 -0
  15. package/dist/config/paths.d.ts +0 -1
  16. package/dist/config/paths.js +0 -1
  17. package/dist/config/providers.d.ts +26 -0
  18. package/dist/config/providers.js +88 -0
  19. package/dist/config/settings.d.ts +9 -2
  20. package/dist/core/agent/compaction.d.ts +13 -0
  21. package/dist/core/agent/compaction.js +34 -0
  22. package/dist/core/agent/errors.d.ts +3 -0
  23. package/dist/core/agent/errors.js +13 -0
  24. package/dist/core/agent/events.d.ts +58 -0
  25. package/dist/core/agent/events.js +3 -0
  26. package/dist/core/goal/completionPolicy.d.ts +27 -0
  27. package/dist/core/goal/completionPolicy.js +67 -0
  28. package/dist/core/goal/requestClassifier.d.ts +6 -0
  29. package/dist/core/goal/requestClassifier.js +31 -0
  30. package/dist/core/goal/sessionGoal.d.ts +30 -0
  31. package/dist/core/goal/sessionGoal.js +88 -0
  32. package/dist/core/session/sessionStore.d.ts +37 -0
  33. package/dist/core/session/sessionStore.js +59 -0
  34. package/dist/llm/client.d.ts +1 -1
  35. package/dist/llm/client.js +6 -6
  36. package/dist/llm/hazeTools.d.ts +70 -0
  37. package/dist/llm/hazeTools.js +311 -97
  38. package/dist/llm/initPrompt.js +7 -5
  39. package/dist/llm/systemPrompt.js +25 -11
  40. package/dist/skills/SkillLoader.d.ts +12 -2
  41. package/dist/skills/SkillLoader.js +64 -18
  42. package/dist/skills/SkillRegistry.d.ts +1 -5
  43. package/dist/skills/SkillRegistry.js +10 -21
  44. package/dist/skills/builder/SkillBuilder.d.ts +31 -1
  45. package/dist/skills/builder/SkillBuilder.js +291 -20
  46. package/dist/skills/skillTools.d.ts +20 -0
  47. package/dist/skills/skillTools.js +25 -0
  48. package/dist/skills/types.d.ts +12 -51
  49. package/dist/ui/components/ErrorView.d.ts +2 -1
  50. package/dist/ui/components/Header.d.ts +4 -2
  51. package/dist/ui/components/Header.js +2 -2
  52. package/dist/ui/components/MarkdownText.d.ts +2 -1
  53. package/dist/ui/components/TextInput.d.ts +13 -2
  54. package/dist/ui/components/TextInput.js +125 -25
  55. package/dist/ui/theme.d.ts +2 -0
  56. package/dist/ui/theme.js +3 -1
  57. package/dist/utils/fs.d.ts +1 -0
  58. package/dist/utils/fs.js +10 -6
  59. package/examples/skills/files/SKILL.md +16 -0
  60. package/examples/skills/files/examples/file-editing.md +3 -0
  61. package/package.json +9 -9
  62. package/dist/skills/installer/SkillInstaller.d.ts +0 -1
  63. package/dist/skills/installer/SkillInstaller.js +0 -48
  64. package/dist/skills/manifestSchema.d.ts +0 -31
  65. package/dist/skills/manifestSchema.js +0 -23
  66. package/dist/tools/ToolExecutor.d.ts +0 -3
  67. package/dist/tools/ToolExecutor.js +0 -15
  68. package/dist/tools/types.d.ts +0 -9
  69. package/dist/tools/types.js +0 -1
  70. package/examples/skills/files/prompts/file_tasks.md +0 -1
  71. package/examples/skills/files/skill.yaml +0 -28
  72. package/examples/skills/files/tools/list_files.ts +0 -21
  73. package/examples/skills/files/tools/read_file.ts +0 -12
@@ -1,22 +1,68 @@
1
1
  import fs from 'fs-extra';
2
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)))
3
+ import YAML from 'yaml';
4
+ const MAX_REFERENCE_BYTES = 50_000;
5
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
6
+ const MARKDOWN_LINK_RE = /\[[^\]]*\]\(([^)]+)\)/g;
7
+ const PLAIN_REFERENCE_RE = /(?:^|\n)\s*(?:[-*]\s+)?((?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]+)\s*(?=\n|$)/g;
8
+ function validateFrontmatter(value) {
9
+ if (typeof value !== 'object' || value == null)
10
+ throw new Error('SKILL.md frontmatter must be an object');
11
+ const frontmatter = value;
12
+ if (typeof frontmatter.name !== 'string' || frontmatter.name.trim().length === 0)
13
+ throw new Error('SKILL.md frontmatter requires name');
14
+ if (!/^[a-zA-Z0-9_-]+$/.test(frontmatter.name))
15
+ throw new Error('Skill name may only contain letters, numbers, hyphens, and underscores');
16
+ if (typeof frontmatter.description !== 'string' || frontmatter.description.trim().length === 0)
17
+ throw new Error('SKILL.md frontmatter requires description');
18
+ return { name: frontmatter.name.trim(), description: frontmatter.description.trim() };
19
+ }
20
+ function parseSkillMarkdown(content) {
21
+ const match = FRONTMATTER_RE.exec(content);
22
+ if (!match)
23
+ throw new Error('SKILL.md must start with YAML frontmatter delimited by ---');
24
+ const frontmatter = validateFrontmatter(YAML.parse(match[1] ?? ''));
25
+ return { frontmatter, body: content.slice(match[0].length).trim() };
26
+ }
27
+ function normalizeReference(reference) {
28
+ const withoutAnchor = reference.split('#')[0]?.split('?')[0]?.trim() ?? '';
29
+ return withoutAnchor.replace(/^<|>$/g, '');
30
+ }
31
+ function referencedPaths(body) {
32
+ const refs = new Set();
33
+ for (const match of body.matchAll(MARKDOWN_LINK_RE)) {
34
+ const ref = normalizeReference(match[1] ?? '');
35
+ if (ref && !ref.includes('://'))
36
+ refs.add(ref);
37
+ }
38
+ for (const match of body.matchAll(PLAIN_REFERENCE_RE)) {
39
+ const ref = normalizeReference(match[1] ?? '');
40
+ if (ref)
41
+ refs.add(ref);
42
+ }
43
+ return [...refs];
44
+ }
45
+ async function loadReference(dir, referencePath) {
46
+ if (path.isAbsolute(referencePath))
47
+ throw new Error(`Skill reference must be relative: ${referencePath}`);
48
+ const absolutePath = path.resolve(dir, referencePath);
49
+ const relative = path.relative(dir, absolutePath);
50
+ if (relative.startsWith('..') || path.isAbsolute(relative))
51
+ throw new Error(`Skill reference escapes skill directory: ${referencePath}`);
52
+ const stat = await fs.stat(absolutePath);
53
+ if (!stat.isFile())
54
+ throw new Error(`Skill reference is not a file: ${referencePath}`);
55
+ if (stat.size > MAX_REFERENCE_BYTES)
56
+ throw new Error(`Skill reference is too large: ${referencePath}`);
57
+ return { path: referencePath, absolutePath, content: await fs.readFile(absolutePath, 'utf8') };
58
+ }
59
+ export async function loadSkill(dir, source = 'global') {
60
+ const skillPath = path.join(dir, 'SKILL.md');
61
+ if (!(await fs.pathExists(skillPath)))
8
62
  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 };
63
+ const content = await fs.readFile(skillPath, 'utf8');
64
+ const { frontmatter, body } = parseSkillMarkdown(content);
65
+ const references = await Promise.all(referencedPaths(body).map(ref => loadReference(dir, ref)));
66
+ return { dir, path: skillPath, name: frontmatter.name, description: frontmatter.description, body, references, source };
22
67
  }
68
+ export const internals = { parseSkillMarkdown, referencedPaths };
@@ -1,6 +1,2 @@
1
- import type { LoadedSkill, LoadedTool } from './types.js';
2
- export interface SkillRegistry {
3
- skills: Map<string, LoadedSkill>;
4
- tools: Map<string, LoadedTool>;
5
- }
1
+ import type { SkillRegistry } from './types.js';
6
2
  export declare function loadSkillRegistry(): Promise<SkillRegistry>;
@@ -1,28 +1,17 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
- import { GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR } from '../config/paths.js';
3
+ import { GLOBAL_SKILLS_DIR } from '../config/paths.js';
4
4
  import { loadSkill } from './SkillLoader.js';
5
5
  export async function loadSkillRegistry() {
6
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
7
  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 };
8
+ for (const name of await fs.readdir(GLOBAL_SKILLS_DIR)) {
9
+ const dir = path.join(GLOBAL_SKILLS_DIR, name);
10
+ if (!(await fs.stat(dir)).isDirectory())
11
+ continue;
12
+ const skill = await loadSkill(dir, 'global');
13
+ if (skill)
14
+ skills.set(skill.name, skill);
15
+ }
16
+ return { skills };
28
17
  }
@@ -1 +1,31 @@
1
- export declare function buildSkill(description: string): Promise<void>;
1
+ type GeneratedSkillFile = {
2
+ path: string;
3
+ content: string;
4
+ };
5
+ type GeneratedSkill = {
6
+ name: string;
7
+ files: GeneratedSkillFile[];
8
+ };
9
+ export declare function slug(s: string): string;
10
+ declare function fallbackSkill(description: string): GeneratedSkill;
11
+ declare function withStandardRequirements(content: string): string;
12
+ declare function withSkillName(content: string, name: string): string;
13
+ declare function normalizeSkillDescription(description: string): string;
14
+ declare function withSkillDescription(content: string, description: string): string;
15
+ declare function parseGeneratedSkill(text: string, description: string): GeneratedSkill;
16
+ export declare function createSkill(description: string): Promise<{
17
+ name: string;
18
+ dir: string;
19
+ file: string;
20
+ }>;
21
+ export declare const internals: {
22
+ SKILL_CREATOR_SKILL: string;
23
+ STANDARD_SKILL_REQUIREMENTS: string;
24
+ parseGeneratedSkill: typeof parseGeneratedSkill;
25
+ fallbackSkill: typeof fallbackSkill;
26
+ withStandardRequirements: typeof withStandardRequirements;
27
+ withSkillName: typeof withSkillName;
28
+ withSkillDescription: typeof withSkillDescription;
29
+ normalizeSkillDescription: typeof normalizeSkillDescription;
30
+ };
31
+ export {};
@@ -1,25 +1,296 @@
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, followed by a role and focused prompt:
31
+
32
+ ---
33
+ name: kebab-case-name
34
+ description: Use when the user asks ...
35
+ ---
36
+
37
+ # Role
38
+ You are a focused, practical assistant for this workflow.
39
+
40
+ # Focused prompt
41
+ Complete the user's goal with the smallest reliable workflow.
42
+
43
+ The description must tell the model exactly when to use the skill.
44
+ The body must be a deterministic operating procedure, not generic advice.
45
+ Keep skills simple, short, and practical: prefer the fewest commands and sections that reliably complete the workflow.
46
+ Avoid exhaustive checklists, rigid citation requirements, or heavyweight output formats unless the user's request truly requires them.
47
+ Additional files are allowed only when SKILL.md explicitly references them with relative paths.
48
+ Skills do not execute code. They teach Haze how to behave for a workflow.
49
+
50
+ Every skill you create must include, in this order:
51
+ - YAML frontmatter.
52
+ - Role: the specific assistant role to adopt for this workflow.
53
+ - Focused prompt: a concise directive that explains the goal and keeps the workflow scoped.
54
+ - Inputs to inspect: only the essential commands/files/state needed for the workflow, with incremental inspection for large outputs.
55
+ - Procedure: a short ordered list with fallback paths for empty, missing, or truncated primary inputs.
56
+ - Stop conditions: when it is valid to say there is nothing to do.
57
+ - Blocker policy: concrete conditions that justify stopping, excluding truncation when narrower inspection is possible.
58
+ - Output template: a compact, reusable final-answer template with predictable headings/placeholders.
59
+ - Evidence rule: require final answers to be grounded in actual inspected content, but do not require exhaustive citations.
60
+
61
+ Infer the user's intent from their wording and make that intent explicit in the Focused prompt. A good focused prompt states what the skill should accomplish, not just what commands it should run.
62
+ Favor skills that a small or slower model can follow in one pass. The model should be able to finish with a concise response after a small number of tool calls.
63
+ Avoid fragile skills that stop after one empty command or one truncated command, but do not over-correct by requiring exhaustive inspection. Encode only common, necessary fallbacks.
64
+ For diff/review skills, keep the default path simple: get status/stat/name-only, inspect unstaged and staged diffs if present, and if no changes or target exist, return a short no-changes response. Use targeted per-file diffs only when the full diff is too large or truncated.
65
+ 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.
66
+ `;
67
+ const generatedSkillSchema = z.object({
68
+ name: z.string().min(1).describe('Meaningful kebab-case skill name with 2-4 words'),
69
+ files: z.array(z.object({
70
+ path: z.string().min(1).describe('Relative file path inside the skill directory'),
71
+ content: z.string().describe('Complete file content'),
72
+ })).min(1).describe('Generated skill files, including SKILL.md'),
73
+ });
74
+ const SKILL_NAME_STOP_WORDS = new Set([
75
+ 'a', 'an', 'the', 'to', 'for', 'with', 'and', 'or', 'of', 'in', 'on', 'my', 'our', 'me', 'i', 'from', 'against', 'as', 'by', 'into', 'using', 'use', 'when', 'asks', 'ask',
76
+ ]);
77
+ const SKILL_NAME_TRAILING_FILLER_WORDS = new Set([
78
+ 'write', 'create', 'make', 'build', 'generate', 'do', 'run', 'handle', 'help', 'using', 'with', 'for', 'to',
79
+ ]);
80
+ export function slug(s) {
81
+ const rawWords = s.toLowerCase().match(/[a-z0-9]+/g) ?? [];
82
+ const meaningfulWords = rawWords.filter(word => !SKILL_NAME_STOP_WORDS.has(word));
83
+ const words = (meaningfulWords.length >= 2 ? meaningfulWords : rawWords).slice(0, 4);
84
+ while (words.length > 2 && SKILL_NAME_TRAILING_FILLER_WORDS.has(words.at(-1) ?? ''))
85
+ words.pop();
86
+ if (words.length === 0)
87
+ return 'custom-skill';
88
+ if (words.length === 1)
89
+ words.push(words[0] === 'custom' ? 'skill' : 'workflow');
90
+ return words.join('-');
91
+ }
92
+ function yamlString(value) {
93
+ return JSON.stringify(value);
94
+ }
95
+ function fallbackSkill(description) {
96
+ const name = slug(description);
97
+ return {
98
+ name,
99
+ files: [{
100
+ path: 'SKILL.md',
101
+ content: `---\nname: ${name}\ndescription: ${yamlString(`Use when the user asks: ${description}`)}\n---\n\n# Role\n\nYou are a focused, practical assistant for this workflow.\n\n# Focused prompt\n\nAccomplish the user's intended outcome with the smallest reliable workflow: ${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 using the output template 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 template\n\n## Summary\n- <one-to-three bullets with the result>\n\n## Actions or findings\n- <concrete actions taken or findings discovered>\n\n## Evidence inspected\n- <commands, files, diffs, or outputs used>\n\n## Next step\n- <recommended next action, or "None" if complete>\n${STANDARD_SKILL_REQUIREMENTS}\n# References\n\nAdd relative file references here if this skill needs examples, templates, or supporting docs.\n`,
102
+ }],
103
+ };
104
+ }
105
+ function withStandardRequirements(content) {
106
+ return content.includes('# Operational guardrails') ? content : `${content.trim()}${STANDARD_SKILL_REQUIREMENTS}\n`;
107
+ }
108
+ function withSkillName(content, name) {
109
+ if (/^---\n[\s\S]*?^name:\s*.*$/m.test(content))
110
+ return content.replace(/^(---\n[\s\S]*?^name:\s*).*$/m, `$1${name}`);
111
+ return content;
112
+ }
113
+ function normalizeSkillDescription(description) {
114
+ const trimmed = description.replace(/\s+/g, ' ').trim();
115
+ if (!trimmed)
116
+ return 'Use when the user asks for this workflow.';
117
+ return /^use when\b/i.test(trimmed) ? trimmed : `Use when ${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
118
+ }
119
+ function withSkillDescription(content, description) {
120
+ const normalized = normalizeSkillDescription(description);
121
+ if (/^---\n[\s\S]*?^description:\s*.*$/m.test(content))
122
+ return content.replace(/^(---\n[\s\S]*?^description:\s*).*$/m, `$1${yamlString(normalized)}`);
123
+ return content;
124
+ }
125
+ function extractJson(text) {
126
+ const fenced = /```(?:json)?\s*([\s\S]*?)\s*```/.exec(text);
127
+ return fenced?.[1] ?? text;
128
+ }
129
+ function parseGeneratedSkill(text, description) {
130
+ const parsed = JSON.parse(extractJson(text));
131
+ const name = typeof parsed.name === 'string' && parsed.name.trim() ? slug(parsed.name) : slug(description);
132
+ const files = Array.isArray(parsed.files) ? parsed.files.filter((file) => {
133
+ return typeof file === 'object' && file != null && typeof file.path === 'string' && typeof file.content === 'string';
134
+ }) : [];
135
+ if (!files.some(file => file.path === 'SKILL.md'))
136
+ throw new Error('Generated skill did not include SKILL.md');
137
+ return { name, files };
138
+ }
139
+ async function descriptionFromSkillSummary(description, finalName, files) {
140
+ const skillMd = files.find(file => file.path === 'SKILL.md')?.content;
141
+ if (!skillMd)
142
+ return normalizeSkillDescription(`the user asks: ${description}`);
143
+ const activeModel = await model();
144
+ if (!activeModel)
145
+ return normalizeSkillDescription(`the user asks: ${description}`);
146
+ const result = await generateObject({
147
+ model: activeModel,
148
+ temperature: 0,
149
+ schema: z.object({ description: z.string().min(1).describe('Final Use when description that tells an LLM when to invoke this skill') }),
150
+ schemaName: 'GeneratedHazeSkillDescription',
151
+ schemaDescription: 'A final skill description chosen from the complete generated SKILL.md.',
152
+ prompt: [
153
+ 'Write the final Haze skill frontmatter description after reading the entire generated SKILL.md.',
154
+ '',
155
+ `Original user request: ${description}`,
156
+ `Final skill name: ${finalName}`,
157
+ '',
158
+ 'Description rules:',
159
+ '- Start with "Use when".',
160
+ '- Optimize for LLM understandability: make it obvious when this skill should be invoked.',
161
+ '- Summarize the actual workflow in the SKILL.md, not only the user wording.',
162
+ '- Be specific about the trigger and desired outcome.',
163
+ '- Keep it one sentence, concise but complete.',
164
+ '- Avoid vague descriptions like "Use when the user asks for this workflow" unless no better signal exists.',
165
+ '',
166
+ 'Generated SKILL.md:',
167
+ skillMd,
168
+ ].join('\n'),
169
+ });
170
+ return normalizeSkillDescription(result.object.description || `the user asks: ${description}`);
171
+ }
172
+ async function nameFromSkillSummary(description, generatedName, files) {
173
+ const skillMd = files.find(file => file.path === 'SKILL.md')?.content;
174
+ if (!skillMd)
175
+ return slug(generatedName || description);
176
+ const activeModel = await model();
177
+ if (!activeModel)
178
+ return slug(generatedName || description);
179
+ const result = await generateObject({
180
+ model: activeModel,
181
+ temperature: 0,
182
+ schema: z.object({ name: z.string().min(1).describe('Final meaningful 2-4 word kebab-case skill name') }),
183
+ schemaName: 'GeneratedHazeSkillName',
184
+ schemaDescription: 'A final skill name chosen from the complete generated SKILL.md.',
185
+ prompt: [
186
+ 'Choose the final Haze skill directory name after reading the entire generated SKILL.md.',
187
+ '',
188
+ `Original user request: ${description}`,
189
+ `Draft/generated name: ${generatedName}`,
190
+ '',
191
+ 'Naming rules:',
192
+ '- Return 2-4 meaningful words in kebab-case.',
193
+ '- The name must summarize the whole skill workflow, not just copy the first words of the request.',
194
+ '- Do not include a loose trailing word from a cut-off sentence. Bad: commit-current-changes-write. Good: commit-current-changes.',
195
+ '- Prefer nouns that convey the outcome, such as review, commit, release-notes, migration, validation, or triage.',
196
+ '- Avoid vague words like helper, workflow, custom-skill, write, create, make, or do unless they are essential to the meaning.',
197
+ '',
198
+ 'Generated SKILL.md:',
199
+ skillMd,
200
+ ].join('\n'),
201
+ });
202
+ return slug(result.object.name || generatedName || description);
203
+ }
204
+ function assertSafeGeneratedFile(filePath) {
205
+ if (path.isAbsolute(filePath))
206
+ throw new Error(`Generated skill file must be relative: ${filePath}`);
207
+ const normalized = path.normalize(filePath);
208
+ if (normalized === '..' || normalized.startsWith(`..${path.sep}`))
209
+ throw new Error(`Generated skill file escapes skill directory: ${filePath}`);
210
+ if (normalized.length === 0 || normalized === '.')
211
+ throw new Error('Generated skill file path is empty');
212
+ return normalized;
213
+ }
214
+ async function generateSkill(description) {
215
+ const activeModel = await model();
216
+ if (!activeModel)
217
+ throw new Error('No model provider configured. Run /provider to choose or add a provider before using /create-skill.');
218
+ const result = await generateObject({
219
+ model: activeModel,
220
+ temperature: 0,
221
+ system: SKILL_CREATOR_SKILL,
222
+ schema: generatedSkillSchema,
223
+ schemaName: 'GeneratedHazeSkill',
224
+ schemaDescription: 'A generated Haze Markdown skill and optional referenced files.',
225
+ prompt: [
226
+ 'Create a Haze skill from this user description:',
227
+ description,
228
+ '',
229
+ 'Rules:',
230
+ '- SKILL.md must include frontmatter with name and description.',
231
+ '- The skill name must be 2-4 meaningful words in kebab-case and convey the workflow intent, for example "branch-diff-review" or "release-notes-draft".',
232
+ '- Pick the name as if it were written after summarizing the complete SKILL.md, not by truncating the first words of the request.',
233
+ '- Do not include loose trailing words from cut-off sentences. Bad: "commit-current-changes-write". Good: "commit-current-changes".',
234
+ '- Avoid vague names like "helper", "workflow", or "custom-skill" unless paired with a specific domain word.',
235
+ '- The frontmatter description must start with "Use when".',
236
+ '- Infer the user intent from the description and make it explicit in the Focused prompt.',
237
+ '- The body must start with these headings immediately after frontmatter: Role, Focused prompt.',
238
+ '- The body must then include these headings: Inputs to inspect, Procedure, Fallbacks, Stop conditions, Blocker policy, Output template.',
239
+ '- The Focused prompt must describe the desired outcome and definition of success, not just restate the trigger.',
240
+ '- Keep the skill simple enough for a small or slower model to complete in one pass.',
241
+ '- Prefer short procedures, compact final output templates, and the minimum necessary tool calls.',
242
+ '- Avoid exhaustive checklists, mandatory line citations for every claim, and large rigid report templates unless the user explicitly asks for them.',
243
+ '- Procedure steps must name exact commands/files/state to inspect whenever the workflow implies them, but only include essential inspections.',
244
+ '- Include fallback behavior for empty or truncated primary inputs. Example: if branch diff is empty, inspect staged and unstaged changes before stopping; if no changes or target exist, return a concise no-changes response; if full diff is truncated, inspect per-file diffs or read changed files.',
245
+ '- For workflows with potentially large outputs, require incremental inspection: stat/name-only/summary first, then targeted content reads.',
246
+ '- Define when it is valid to say "nothing to do".',
247
+ '- 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.',
248
+ '- Require final output to cite actual evidence inspected, using exact lines when available and file/function/code-area references otherwise.',
249
+ '- Include extra files only when genuinely useful, and reference them from SKILL.md.',
250
+ '- File paths must be relative and stay inside the skill directory.',
251
+ ].join('\n'),
252
+ });
253
+ const generated = result.object;
254
+ const draftName = slug(generated.name || description);
255
+ const finalName = await nameFromSkillSummary(description, draftName, generated.files).catch(() => draftName);
256
+ const finalDescription = await descriptionFromSkillSummary(description, finalName, generated.files).catch(() => normalizeSkillDescription(`the user asks: ${description}`));
257
+ const files = generated.files.map(file => file.path === 'SKILL.md' ? { ...file, content: withSkillDescription(file.content, finalDescription) } : file);
258
+ return { name: finalName, files };
259
+ }
260
+ export async function createSkill(description) {
261
+ const generated = await generateSkill(description).catch(error => {
262
+ const message = error instanceof Error ? error.message : String(error);
263
+ if (message.startsWith('No model provider configured.'))
264
+ throw error instanceof Error ? error : new Error(message);
265
+ return fallbackSkill(description);
266
+ });
267
+ const name = generated.name || fallbackSkill(description).name;
11
268
  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.`);
269
+ const skillFile = path.join(dir, 'SKILL.md');
270
+ await fs.ensureDir(dir);
271
+ if (await fs.pathExists(skillFile))
272
+ throw new Error(`Skill already exists: ${name}`);
273
+ for (const generatedFile of generated.files) {
274
+ const safePath = assertSafeGeneratedFile(generatedFile.path);
275
+ const absolutePath = path.join(dir, safePath);
276
+ await fs.ensureDir(path.dirname(absolutePath));
277
+ const content = safePath === 'SKILL.md' ? withSkillName(withStandardRequirements(generatedFile.content), name) : generatedFile.content;
278
+ await fs.writeFile(absolutePath, content, 'utf8');
279
+ }
280
+ if (!(await fs.pathExists(skillFile))) {
281
+ const fallback = fallbackSkill(description);
282
+ await fs.writeFile(skillFile, fallback.files[0].content, 'utf8');
283
+ }
284
+ const loaded = await loadSkill(dir, 'global');
285
+ if (!loaded)
286
+ throw new Error('Generated skill is missing SKILL.md');
287
+ if (loaded.name !== name) {
288
+ const nextDir = path.join(GLOBAL_SKILLS_DIR, loaded.name);
289
+ if (await fs.pathExists(nextDir))
290
+ throw new Error(`Skill already exists: ${loaded.name}`);
291
+ await fs.move(dir, nextDir);
292
+ return { name: loaded.name, dir: nextDir, file: path.join(nextDir, 'SKILL.md') };
293
+ }
294
+ return { name, dir, file: skillFile };
25
295
  }
296
+ export const internals = { SKILL_CREATOR_SKILL, STANDARD_SKILL_REQUIREMENTS, parseGeneratedSkill, fallbackSkill, withStandardRequirements, withSkillName, withSkillDescription, normalizeSkillDescription };
@@ -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
+ import React from 'react';
1
2
  export declare function ErrorView({ error }: {
2
3
  error: unknown;
3
- }): import("react/jsx-runtime").JSX.Element;
4
+ }): React.JSX.Element;
@@ -1,3 +1,5 @@
1
- export declare function Header({ subtitle }: {
1
+ import React from 'react';
2
+ export declare function Header({ subtitle, version }: {
2
3
  subtitle?: string;
3
- }): import("react/jsx-runtime").JSX.Element;
4
+ version?: string;
5
+ }): React.JSX.Element;
@@ -1,6 +1,6 @@
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
+ export function Header({ subtitle, version }) {
5
+ return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.purple, bold: true, children: "haze" }), version ? _jsxs(Text, { color: theme.muted, children: [" v", version] }) : null] }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: subtitle ?? 'A tiny terminal fog machine for building software.' })] });
6
6
  }
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  export declare function MarkdownText({ content }: {
2
3
  content: string;
3
- }): import("react/jsx-runtime").JSX.Element;
4
+ }): React.JSX.Element;