@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.
- package/CHANGELOG.md +14 -0
- package/README.md +114 -69
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +203 -11
- package/dist/cli/commands/commands.js +130 -6
- package/dist/cli/commands/formatters.d.ts +1 -0
- package/dist/cli/commands/formatters.js +18 -1
- package/dist/cli/commands/skills.d.ts +1 -1
- package/dist/cli/commands/skills.js +8 -5
- package/dist/cli/commands/streaming.d.ts +2 -0
- package/dist/cli/commands/streaming.js +424 -39
- package/dist/cli/index.js +1 -11
- package/dist/config/paths.d.ts +0 -1
- package/dist/config/paths.js +0 -1
- package/dist/llm/client.js +1 -1
- package/dist/llm/hazeTools.d.ts +32 -0
- package/dist/llm/hazeTools.js +136 -26
- package/dist/llm/initPrompt.js +2 -2
- package/dist/llm/systemPrompt.js +23 -9
- package/dist/skills/SkillLoader.d.ts +12 -2
- package/dist/skills/SkillLoader.js +64 -18
- package/dist/skills/SkillRegistry.d.ts +1 -5
- package/dist/skills/SkillRegistry.js +10 -21
- package/dist/skills/builder/SkillBuilder.d.ts +25 -1
- package/dist/skills/builder/SkillBuilder.js +169 -20
- package/dist/skills/skillTools.d.ts +20 -0
- package/dist/skills/skillTools.js +25 -0
- package/dist/skills/types.d.ts +12 -51
- package/dist/ui/components/Header.d.ts +2 -1
- package/dist/ui/components/Header.js +12 -2
- package/dist/ui/components/TextInput.d.ts +8 -1
- package/dist/ui/components/TextInput.js +29 -14
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.js +1 -0
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.js +10 -6
- package/examples/skills/files/SKILL.md +16 -0
- package/examples/skills/files/examples/file-editing.md +3 -0
- package/package.json +2 -2
- package/dist/skills/installer/SkillInstaller.d.ts +0 -1
- package/dist/skills/installer/SkillInstaller.js +0 -48
- package/dist/skills/manifestSchema.d.ts +0 -31
- package/dist/skills/manifestSchema.js +0 -23
- package/dist/tools/ToolExecutor.d.ts +0 -3
- package/dist/tools/ToolExecutor.js +0 -15
- package/dist/tools/types.d.ts +0 -9
- package/dist/tools/types.js +0 -1
- package/examples/skills/files/prompts/file_tasks.md +0 -1
- package/examples/skills/files/skill.yaml +0 -28
- package/examples/skills/files/tools/list_files.ts +0 -21
- 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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
await fs.
|
|
24
|
-
|
|
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 };
|
package/dist/skills/types.d.ts
CHANGED
|
@@ -1,60 +1,21 @@
|
|
|
1
|
-
export interface
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
skillName: string;
|
|
59
|
-
absolutePath: string;
|
|
19
|
+
export interface SkillRegistry {
|
|
20
|
+
skills: Map<string, LoadedSkill>;
|
|
60
21
|
}
|
|
@@ -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
|
-
|
|
5
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
package/dist/ui/theme.d.ts
CHANGED
package/dist/ui/theme.js
CHANGED
package/dist/utils/fs.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@denizokcu/haze",
|
|
3
|
-
"version": "0.0.
|
|
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": "
|
|
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
|
-
});
|