@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.0.2 - 2026-06-01
6
+
7
+ - Reworked skills into Markdown-first workflows stored in `~/.haze/skills/<name>/SKILL.md`.
8
+ - Added LLM-generated `/skill create <description>` for creating workflow skills from natural language.
9
+ - Exposed installed skills as model-selectable `skill_*` tools and slash-invokable commands.
10
+ - Added slash-command and skill autocomplete with `Tab` completion.
11
+ - Grouped tool calls into compact per-turn activity blocks.
12
+ - Added `listFiles` cursor pagination for large recursive listings.
13
+ - Refined startup/onboarding UI with ASCII logo, status bar, model/workspace details, and clearer setup guidance.
14
+ - Updated README for the minimal LLM harness and adaptive skill workflow.
15
+ - Removed old YAML/executable skill tooling.
16
+
3
17
  ## 0.0.1 - 2026-05-31
4
18
 
5
19
  Initial public release.
package/README.md CHANGED
@@ -1,67 +1,114 @@
1
1
  # Haze
2
2
 
3
- Haze is a pragmatic agentic CLI for building apps from the terminal. It uses the Vercel AI SDK, OpenAI-compatible providers such as OpenRouter, and transparent local tools for reading, editing, writing, and testing files.
3
+ A minimal LLM harness for your terminal.
4
+
5
+ Haze gives an AI model a small set of transparent local tools — read files, edit files, write files, list files, and run commands — then gets out of the way. Start with chat. Build your workflows as you work. Teach Haze with Markdown skills when a pattern repeats. Tiny spell, useful goblin.
6
+
7
+ MVP scope: Haze currently uses OpenRouter only. More providers are on the roadmap after the goblin learns to hold a spoon safely.
8
+
9
+ ```txt
10
+ _
11
+ | |
12
+ | |__ __ _ _______
13
+ | '_ \ / _` |_ / _ \
14
+ | | | | (_| |/ / __/
15
+ |_| |_|\__,_/___\___|
16
+ ```
17
+
18
+ Haze keeps guardrails light. The LLM can work from the terminal with freedoms close to yours, while trying to stay scoped to the current project. Watch the tool calls. Keep your hands near the wheel. Progress.
4
19
 
5
20
  ## Install
6
21
 
7
22
  ```bash
8
23
  npm install -g @denizokcu/haze
24
+ haze
9
25
  ```
10
26
 
11
- Then start Haze:
27
+ First run inside Haze, do both steps:
12
28
 
13
- ```bash
14
- haze
29
+ ```txt
30
+ /login
31
+ /model x-ai/grok-build-0.1
15
32
  ```
16
33
 
17
- For local development from this repository:
34
+ `/login` saves your API key. `/model` saves the model Haze should use. The recommended MVP model is `x-ai/grok-build-0.1`.
35
+
36
+ Or use environment variables:
18
37
 
19
38
  ```bash
20
- npm install
21
- npm run dev
39
+ export OPENAI_API_KEY=... # your OpenRouter API key
40
+ export HAZE_MODEL=x-ai/grok-build-0.1
22
41
  ```
23
42
 
24
- ## First-time setup
43
+ Saved settings live in `~/.haze/settings.json`. The current MVP experience is documented around OpenRouter; more provider docs are future work.
25
44
 
26
- Inside Haze, configure OpenRouter:
45
+ ## Get productive immediately
46
+
47
+ Open a project and ask for work:
27
48
 
28
49
  ```txt
29
- /login
30
- /model openai/gpt-4o-mini
50
+ create a calculator in calc-app in ruby with add subtract multiply divide
31
51
  ```
32
52
 
33
- `/login` stores settings in `~/.haze/settings.json`:
53
+ Haze will inspect, write files, run commands, and show compact tool activity inline.
54
+
55
+ Use `/` to discover commands and skills. `Tab` completes the top suggestion.
56
+
57
+ Useful starters:
34
58
 
35
- ```json
36
- {
37
- "provider": "openrouter",
38
- "apiKey": "...",
39
- "baseURL": "https://openrouter.ai/api/v1",
40
- "model": "openai/gpt-4o-mini"
41
- }
59
+ ```txt
60
+ /init
61
+ /skill create review my current branch against main like a senior engineer
62
+ /skill create prepare clean git commits from my uncommitted changes
63
+ /skill create implement small features with tests and a concise summary
42
64
  ```
43
65
 
44
- Environment variables override saved settings:
66
+ `/init` creates or updates `AGENTS.md` so future sessions understand the project.
45
67
 
46
- ```bash
47
- export OPENAI_API_KEY=...
48
- export OPENAI_BASE_URL=https://openrouter.ai/api/v1
49
- export HAZE_MODEL=openai/gpt-4o-mini
68
+ ## Skills: your workflows, grown while working
69
+
70
+ Skills are Markdown workflows stored in `~/.haze/skills`.
71
+
72
+ When you notice yourself asking for the same kind of work, make it a skill:
73
+
74
+ ```txt
75
+ /skill create review the diff between my current branch and main, focusing on bugs, tests, DRY and KISS
50
76
  ```
51
77
 
52
- ## Usage
78
+ Haze uses the model to create:
53
79
 
54
- ```bash
55
- haze
56
- haze --debug
57
- haze skills list
58
- haze skills info <name>
59
- haze skills validate <dir>
60
- haze install-skill <githubRepo>
61
- haze build-skill <description>
80
+ ```txt
81
+ ~/.haze/skills/<skill-name>/SKILL.md
82
+ ```
83
+
84
+ A skill is just Markdown with frontmatter:
85
+
86
+ ```md
87
+ ---
88
+ name: code-review-diff-main
89
+ description: Use when the user asks for a code review of the current branch against main.
90
+ ---
91
+
92
+ # Goal
93
+
94
+ Review the actual change and return useful, evidence-based feedback.
95
+
96
+ # Procedure
97
+
98
+ Inspect branch state, changed files, staged and unstaged diffs, then review incrementally.
62
99
  ```
63
100
 
64
- Chat commands:
101
+ Installed skills appear as slash commands like:
102
+
103
+ ```txt
104
+ /code-review-diff-main
105
+ ```
106
+
107
+ They are also exposed to the model as `skill_*` tools. The skill does not execute code; it gives Haze a workflow to follow.
108
+
109
+ This is the trick: do normal work, notice friction, create a skill, keep going. Your workflow adapts instead of asking you to adapt to the tool. Rude, but in a good way.
110
+
111
+ ## Commands
65
112
 
66
113
  ```txt
67
114
  /help
@@ -72,70 +119,68 @@ Chat commands:
72
119
  /init
73
120
  /clear
74
121
  /exit
75
- ```
76
-
77
- `/init` explores the current workspace using `.gitignore`-aware tools and creates or updates an `AGENTS.md` file with project instructions for future Haze sessions.
78
122
 
79
- Input conveniences:
80
-
81
- - `↑` / `↓` browse persisted input history.
82
- - `←` / `→` move the cursor.
83
- - `Esc` clears the input field.
84
- - `Ctrl+A` / `Ctrl+E` jump to start/end.
123
+ /skill create <description>
124
+ /skill list
125
+ /skill info <name>
126
+ /skill validate <name-or-dir>
127
+ /skill remove <name> --yes
128
+ ```
85
129
 
86
- Input history is stored in `~/.haze/history/input-history.json`.
130
+ `/skills ...` also works as an alias for `/skill ...`.
87
131
 
88
132
  ## Agent tools
89
133
 
90
- Haze exposes a small toolset to the model:
134
+ Haze exposes a deliberately small toolset:
91
135
 
92
- - `listFiles` — structured project discovery.
136
+ - `listFiles` — structured discovery, recursive with cursor pagination when needed.
93
137
  - `readFile` — read UTF-8 files with optional line ranges.
94
138
  - `editFile` — exact unique text replacements.
95
- - `replaceLines` — replace a 1-based line range when exact edits are ambiguous.
96
- - `writeFile` — create or overwrite files.
97
- - `bash` — run shell commands for tests, builds, and inspection.
139
+ - `replaceLines` — line-range edits when exact replacements are awkward.
140
+ - `writeFile` — create files and parent directories.
141
+ - `bash` — run tests, builds, git commands, and inspections.
142
+ - `skill_*` — load Markdown skill instructions on demand.
98
143
 
99
- Tool calls are shown inline in the chat transcript so you can see what Haze is doing.
144
+ Tool calls are grouped in the transcript so you can see what happened without reading a novella.
100
145
 
101
146
  ## Context files
102
147
 
103
- Haze loads project instructions from context files and includes them in the system prompt:
148
+ Haze loads project instructions from:
104
149
 
105
150
  - `~/.haze/AGENTS.md`
106
151
  - `~/.haze/CLAUDE.md`
107
- - `AGENTS.md` files found while walking from the filesystem root to the current workspace
108
- - `CLAUDE.md` files found while walking from the filesystem root to the current workspace
152
+ - `AGENTS.md` files from filesystem root to the current workspace
153
+ - `CLAUDE.md` files from filesystem root to the current workspace
109
154
 
110
- Use `AGENTS.md` for shared project instructions. `CLAUDE.md` is supported for compatibility with existing projects.
155
+ Use `AGENTS.md` for project conventions, commands, architecture notes, and things future-you does not want to re-explain.
111
156
 
112
157
  ## Safety model
113
158
 
114
159
  - File tools are restricted to the current workspace.
115
160
  - File tools follow `.gitignore` by default.
116
- - Ignored files can still be accessed when explicitly needed by using the tool's ignored-file override.
117
- - Haze is prompted to ask before destructive actions.
118
- - Bash is powerful; review commands shown in the transcript, especially in early releases.
119
-
120
- ## Skills
121
-
122
- Default skill locations:
123
-
124
- - `~/.haze/skills/`
125
- - `./.haze/skills/` local overrides global
161
+ - Ignored files require an explicit override.
162
+ - Bash mutations are discouraged by the tool contract.
163
+ - Destructive actions should require explicit user confirmation.
164
+ - Haze is powerful enough to help and dumb enough to deserve supervision. Ideal software, basically.
126
165
 
127
- A skill is a directory containing `skill.yaml`, optional prompts, and TypeScript tool files. Skill tools run in a subprocess via `tsx`.
128
-
129
- ## Development
166
+ ## Local development
130
167
 
131
168
  ```bash
132
169
  npm install
170
+ npm run dev
133
171
  npm run typecheck
172
+ npm test
173
+ npm run lint
134
174
  npm run build
175
+ ```
176
+
177
+ Package check:
178
+
179
+ ```bash
135
180
  npm pack --dry-run
136
181
  ```
137
182
 
138
- The npm package intentionally ships only `bin`, `dist`, `README.md`, `LICENSE`, `CHANGELOG.md`, and `examples`.
183
+ The npm package ships `bin`, `dist`, docs, license, changelog, and examples.
139
184
 
140
185
  ## Release
141
186
 
@@ -143,7 +188,7 @@ The npm package intentionally ships only `bin`, `dist`, `README.md`, `LICENSE`,
143
188
  npm run typecheck
144
189
  npm run build
145
190
  npm pack --dry-run
146
- git tag v0.0.1
191
+ git tag vX.Y.Z
147
192
  git push origin main --tags
148
193
  npm publish --access public
149
194
  ```
@@ -1,6 +1,7 @@
1
1
  export type Mode = 'chat' | 'apiKey' | 'model';
2
2
  interface ChatOptions {
3
3
  debug?: boolean;
4
+ version?: string;
4
5
  }
5
6
  export declare function chatCommand(options?: ChatOptions): Promise<void>;
6
7
  export {};
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { execFile as execFileCallback } from 'node:child_process';
4
+ import { promisify } from 'node:util';
3
5
  import { Box, render, Text, useApp, useStdout } from 'ink';
4
6
  import Spinner from 'ink-spinner';
5
7
  import { readContextFiles } from '../../config/contextFiles.js';
@@ -11,29 +13,140 @@ import { MarkdownText } from '../../ui/components/MarkdownText.js';
11
13
  import { theme } from '../../ui/theme.js';
12
14
  import { handleSlashCommand } from './commands.js';
13
15
  import { runAgentTurn } from './streaming.js';
14
- function ChatScreen({ debug = false }) {
16
+ import { loadSkillRegistry } from '../../skills/SkillRegistry.js';
17
+ const execFile = promisify(execFileCallback);
18
+ async function currentBranchName() {
19
+ try {
20
+ const { stdout } = await execFile('git', ['branch', '--show-current'], { cwd: process.cwd() });
21
+ return stdout.trim() || undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ function toolCallCount(messages) {
28
+ return messages.reduce((total, message) => {
29
+ if (message.role !== 'tool')
30
+ return total;
31
+ const headerCount = /Tools: (\d+) calls?/.exec(message.text)?.[1];
32
+ if (headerCount)
33
+ return total + Number(headerCount);
34
+ const rows = message.text.split('\n').filter(line => /^\s+[✓✗…]\s/.test(line));
35
+ return total + rows.reduce((rowTotal, row) => rowTotal + Number(/×(\d+)/.exec(row)?.[1] ?? 1), 0);
36
+ }, 0);
37
+ }
38
+ function estimateTokens(text) {
39
+ return Math.ceil(text.length / 4);
40
+ }
41
+ function formatTokenCount(tokens) {
42
+ if (tokens >= 1_000_000)
43
+ return `${(tokens / 1_000_000).toFixed(tokens >= 10_000_000 ? 0 : 1).replace(/\.0$/, '')}m`;
44
+ if (tokens >= 1_000)
45
+ return `${(tokens / 1_000).toFixed(tokens >= 10_000 ? 0 : 1).replace(/\.0$/, '')}k`;
46
+ return String(tokens);
47
+ }
48
+ function estimateConversationTokens(messages) {
49
+ const inputText = messages
50
+ .filter(message => message.role === 'user' || message.role === 'tool')
51
+ .map(message => message.text)
52
+ .join('\n');
53
+ const outputText = messages
54
+ .filter(message => message.role === 'assistant')
55
+ .map(message => message.text)
56
+ .join('\n');
57
+ return {
58
+ input: estimateTokens(inputText),
59
+ output: estimateTokens(outputText),
60
+ };
61
+ }
62
+ function ToolMessageText({ text, streaming }) {
63
+ const lines = text.split('\n');
64
+ return _jsx(Box, { flexDirection: "column", children: lines.map((line, index) => {
65
+ const row = /^(\s*)([✓✗…])\s+(\S+)(.*)$/.exec(line);
66
+ if (!row) {
67
+ return _jsxs(Text, { color: theme.muted, children: [index === 0 && streaming ? _jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, line] }, `${index}-${line}`);
68
+ }
69
+ const [, indent, icon, toolName, rest] = row;
70
+ const iconColor = icon === '✓' ? theme.success : icon === '✗' ? theme.danger : theme.muted;
71
+ return _jsxs(Text, { color: theme.muted, children: [indent, _jsx(Text, { color: iconColor, children: icon }), " ", _jsx(Text, { color: theme.purple, children: toolName }), rest] }, `${index}-${line}`);
72
+ }) });
73
+ }
74
+ function startupProviderInfo(settings) {
75
+ const model = process.env.HAZE_MODEL ?? settings.model ?? 'x-ai/grok-build-0.1';
76
+ const modelSource = process.env.HAZE_MODEL ? 'HAZE_MODEL env' : settings.model ? 'settings' : 'default';
77
+ const baseURL = process.env.OPENAI_BASE_URL ?? settings.baseURL ?? 'https://openrouter.ai/api/v1';
78
+ const baseURLSource = process.env.OPENAI_BASE_URL ? 'OPENAI_BASE_URL env' : settings.baseURL ? 'settings' : 'default';
79
+ const apiKeySource = process.env.OPENAI_API_KEY ? 'OPENAI_API_KEY env' : settings.apiKey ? '~/.haze/settings.json' : 'missing';
80
+ const provider = process.env.OPENAI_BASE_URL
81
+ ? 'OpenAI-compatible custom endpoint'
82
+ : settings.provider === 'openrouter' || settings.baseURL || settings.apiKey
83
+ ? 'OpenRouter'
84
+ : 'OpenRouter (not logged in)';
85
+ return [
86
+ 'Provider configuration',
87
+ `- Provider: ${provider}`,
88
+ `- Model: ${model} (${modelSource})`,
89
+ `- Base URL: ${baseURL} (${baseURLSource})`,
90
+ `- API key: ${apiKeySource === 'missing' ? 'not configured; run /login or set OPENAI_API_KEY' : `configured via ${apiKeySource}`}`,
91
+ ].join('\n');
92
+ }
93
+ function ChatScreen({ debug = false, version }) {
15
94
  const { exit } = useApp();
16
95
  const { stdout } = useStdout();
17
96
  const height = stdout.rows ?? process.stdout.rows ?? 24;
18
97
  const [messages, setMessages] = useState([
19
- { role: 'system', text: 'Welcome to Haze. Use /login for OpenRouter, /model to choose a model, /help for commands.' }
98
+ { role: 'system', text: 'Welcome to Haze. Use /help for commands.' }
20
99
  ]);
21
100
  const [settings, setSettings] = useState({});
22
101
  const conversationRef = useRef([]);
23
102
  const lastAssistantTextRef = useRef('');
103
+ const abortControllerRef = useRef(null);
24
104
  const [inputHistory, setInputHistory] = useState([]);
25
105
  const [debugLogs, setDebugLogs] = useState([]);
26
106
  const [contextFiles, setContextFiles] = useState([]);
27
107
  const [mode, setMode] = useState('chat');
28
108
  const [busy, setBusy] = useState(false);
109
+ const [busyLabel, setBusyLabel] = useState('Haze is thinking');
110
+ const [skills, setSkills] = useState([]);
111
+ const [branchName, setBranchName] = useState();
29
112
  useEffect(() => {
30
- readSettings().then(setSettings).catch(() => undefined);
113
+ Promise.all([readSettings(), currentBranchName()]).then(([next, branch]) => {
114
+ setSettings(next);
115
+ setBranchName(branch);
116
+ setMessages(m => [...m, { role: 'system', text: startupProviderInfo(next) }]);
117
+ }).catch(() => {
118
+ currentBranchName().then(branch => {
119
+ setBranchName(branch);
120
+ setMessages(m => [...m, { role: 'system', text: startupProviderInfo({}) }]);
121
+ }).catch(() => {
122
+ setMessages(m => [...m, { role: 'system', text: startupProviderInfo({}) }]);
123
+ });
124
+ });
31
125
  readInputHistory().then(setInputHistory).catch(() => undefined);
32
126
  readContextFiles().then(setContextFiles).catch(() => undefined);
127
+ refreshSkills().catch(() => undefined);
128
+ const branchTimer = setInterval(() => {
129
+ currentBranchName().then(setBranchName).catch(() => setBranchName(undefined));
130
+ }, 3000);
131
+ return () => clearInterval(branchTimer);
33
132
  }, []);
34
133
  function persistInputHistory(value) {
35
134
  addInputHistoryItem(value).then(setInputHistory).catch(() => undefined);
36
135
  }
136
+ async function refreshSkills() {
137
+ const registry = await loadSkillRegistry();
138
+ const nextSkills = [...registry.skills.values()];
139
+ setSkills(nextSkills);
140
+ return nextSkills;
141
+ }
142
+ function skillInvocation(value) {
143
+ if (!value.startsWith('/'))
144
+ return undefined;
145
+ const name = value.slice(1).trim();
146
+ if (!name || name.includes(' '))
147
+ return undefined;
148
+ return skills.find(skill => skill.name === name);
149
+ }
37
150
  function debugLog(line) {
38
151
  if (!debug)
39
152
  return;
@@ -44,6 +157,12 @@ function ChatScreen({ debug = false }) {
44
157
  lastAssistantTextRef.current = '';
45
158
  setMessages([{ role: 'system', text: 'Cleared. The void is productive.' }]);
46
159
  }
160
+ function cancelThinking() {
161
+ if (!busy)
162
+ return;
163
+ abortControllerRef.current?.abort('User pressed Esc.');
164
+ setBusy(false);
165
+ }
47
166
  async function submit(value) {
48
167
  if (busy)
49
168
  return;
@@ -51,16 +170,22 @@ function ChatScreen({ debug = false }) {
51
170
  const next = await updateSettings({ provider: 'openrouter', apiKey: value, baseURL: 'https://openrouter.ai/api/v1' });
52
171
  setSettings(next);
53
172
  setMode('chat');
54
- setMessages(m => [...m, { role: 'system', text: 'OpenRouter login saved to ~/.haze/settings.json. Security theatre completed.' }]);
173
+ setMessages(m => [...m, { role: 'system', text: `OpenRouter login saved to ~/.haze/settings.json. Security theatre completed.\n\n${startupProviderInfo(next)}` }]);
55
174
  return;
56
175
  }
57
176
  if (mode === 'model') {
58
177
  const next = await updateSettings({ model: value });
59
178
  setSettings(next);
60
179
  setMode('chat');
61
- setMessages(m => [...m, { role: 'system', text: `Model set to ${value}.` }]);
180
+ setMessages(m => [...m, { role: 'system', text: `Model set to ${value}.\n\n${startupProviderInfo(next)}` }]);
62
181
  return;
63
182
  }
183
+ const invokedSkill = skillInvocation(value);
184
+ if (invokedSkill) {
185
+ await doAgentTurn(`The user explicitly invoked the "${invokedSkill.name}" skill. Call skill_${invokedSkill.name.replace(/[^a-zA-Z0-9_]/g, '_')} and follow its returned instructions.`, value);
186
+ return;
187
+ }
188
+ const isSkillCreate = /^\/skills? create(?:\s|$)/.test(value);
64
189
  const ctx = {
65
190
  settings,
66
191
  contextFiles,
@@ -69,13 +194,39 @@ function ChatScreen({ debug = false }) {
69
194
  clearConversation,
70
195
  runAgentTurn: (prompt, displayValue) => doAgentTurn(prompt, displayValue),
71
196
  refreshContextFiles: async () => { const files = await readContextFiles().catch(() => contextFiles); setContextFiles(files); return files; },
72
- updateSettings: patch => { const next = { ...settings, ...patch }; setSettings(next); return Promise.resolve(next); },
197
+ updateSettings: async (patch) => {
198
+ const next = await updateSettings(patch);
199
+ setSettings(next);
200
+ return next;
201
+ },
73
202
  };
74
- const result = await handleSlashCommand(value, ctx);
203
+ let result;
204
+ if (isSkillCreate) {
205
+ setBusyLabel('Creating skill');
206
+ setBusy(true);
207
+ }
208
+ try {
209
+ result = await handleSlashCommand(value, ctx);
210
+ }
211
+ catch (error) {
212
+ const text = error instanceof Error ? error.message : String(error);
213
+ setMessages(m => [...m, { role: 'system', text: `Skill creation failed: ${text}` }]);
214
+ return;
215
+ }
216
+ finally {
217
+ if (isSkillCreate) {
218
+ setBusy(false);
219
+ setBusyLabel('Haze is thinking');
220
+ }
221
+ }
75
222
  if (result === 'exit')
76
223
  return exit();
77
- if (result === 'handled')
224
+ if (result === 'handled') {
225
+ if (value === '/skill create' || value.startsWith('/skill create ') || value === '/skills create' || value.startsWith('/skills create ') || value.startsWith('/skill remove ') || value.startsWith('/skills remove ')) {
226
+ await refreshSkills().catch(() => undefined);
227
+ }
78
228
  return;
229
+ }
79
230
  await doAgentTurn(value);
80
231
  }
81
232
  async function doAgentTurn(value, displayValue) {
@@ -89,11 +240,52 @@ function ChatScreen({ debug = false }) {
89
240
  getConversation: () => conversationRef.current,
90
241
  getLastAssistantText: () => lastAssistantTextRef.current,
91
242
  setLastAssistantText: text => { lastAssistantTextRef.current = text; },
243
+ setAbortController: controller => { abortControllerRef.current = controller; },
92
244
  });
93
245
  }
94
- const visible = messages;
95
- const placeholder = mode === 'apiKey' ? 'OpenRouter API key...' : mode === 'model' ? 'openai/gpt-4o-mini' : busy ? 'Thinking, allegedly...' : 'Ask Haze to help build your app...';
96
- return _jsxs(Box, { flexDirection: "column", minHeight: height, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Header, { subtitle: "AI agent CLI for building apps" }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visible.map((message, index) => _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: message.role === 'user' ? theme.purple : message.role === 'assistant' ? theme.success : message.role === 'tool' ? theme.muted : theme.muted, bold: true, children: message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Haze' : message.role === 'tool' ? 'Tool' : 'Info' }), message.role === 'assistant' && !message.streaming ? _jsx(MarkdownText, { content: message.text }) : _jsx(Text, { color: message.role === 'tool' ? theme.muted : undefined, children: message.streaming && message.role === 'tool' ? _jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), " ", message.text] }) : message.text })] }, index)) }), debug && debugLogs.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, marginBottom: 1, borderStyle: "round", borderColor: theme.muted, paddingX: 1, children: [_jsx(Text, { color: theme.muted, bold: true, children: "Debug" }), debugLogs.map((line, index) => _jsxs(Text, { color: theme.muted, children: ["\u2022 ", line] }, index))] }), busy && _jsx(Box, { flexShrink: 0, marginBottom: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Spinner, { type: "dots" }), " Haze is thinking..."] }) }), _jsx(Box, { borderStyle: "round", borderColor: theme.deepPurple, paddingX: 1, height: 3, flexShrink: 0, children: _jsx(Box, { flexGrow: 1, minWidth: 0, children: _jsx(TextInput, { placeholder: placeholder, disabled: busy, mask: mode === 'apiKey', historyItems: inputHistory, recordHistory: mode === 'chat', onHistoryAdd: persistInputHistory, onSubmit: submit }) }) })] });
246
+ const visible = messages.filter(message => !message.hidden);
247
+ const placeholder = mode === 'apiKey' ? 'OpenRouter API key' : mode === 'model' ? 'x-ai/grok-build-0.1' : busy ? 'Thinking, allegedly' : 'Ask Haze to help build your app';
248
+ const activeModelName = process.env.HAZE_MODEL ?? settings.model ?? 'x-ai/grok-build-0.1';
249
+ const hasLogin = Boolean(process.env.OPENAI_API_KEY ?? settings.apiKey);
250
+ const hasChosenModel = Boolean(process.env.HAZE_MODEL ?? settings.model);
251
+ const headerSubtitle = hasLogin && hasChosenModel
252
+ ? [
253
+ 'A minimal LLM harness for growing your own workflows while you work.',
254
+ '',
255
+ 'Start with simple chat, then teach Haze your habits with skills:',
256
+ '/skill create review my branch against main — tiny spell, useful goblin.',
257
+ '',
258
+ 'The most adaptive workflow is the one you shape as you go.',
259
+ '',
260
+ 'Guardrails are light: Haze lets the LLM work from the terminal almost like you,',
261
+ 'while trying to stay scoped to this project.',
262
+ ].join('\n')
263
+ : 'First things first: run /login to add your API key, then /model x-ai/grok-build-0.1 to choose a model.';
264
+ const workspaceLabel = `${process.cwd()}${branchName ? ` (${branchName})` : ''}`;
265
+ const toolsUsed = toolCallCount(messages);
266
+ const estimatedTokens = estimateConversationTokens(messages);
267
+ const statusDetailLabel = `${conversationRef.current.length} messages / ${toolsUsed} tool call${toolsUsed === 1 ? '' : 's'} / ↑ ~${formatTokenCount(estimatedTokens.input)} ↓ ~${formatTokenCount(estimatedTokens.output)} / ${skills.length} skill${skills.length === 1 ? '' : 's'}`;
268
+ const slashSuggestions = mode === 'chat' ? [
269
+ { value: '/help', description: 'Show commands', kind: 'command' },
270
+ { value: '/login', description: 'Save an OpenRouter API key', kind: 'command' },
271
+ { value: '/model', description: 'Choose a model', kind: 'command' },
272
+ { value: '/settings', description: 'Show provider, model, API key, and context status', kind: 'command' },
273
+ { value: '/skill create ', description: 'Create a Markdown skill', kind: 'command' },
274
+ { value: '/skill list', description: 'List installed skills', kind: 'command' },
275
+ { value: '/skill info ', description: 'Show details for a skill', kind: 'command' },
276
+ { value: '/skill validate ', description: 'Validate a skill', kind: 'command' },
277
+ { value: '/skill remove ', description: 'Remove a skill with --yes', kind: 'command' },
278
+ { value: '/init', description: 'Create or update AGENTS.md project instructions', kind: 'command' },
279
+ { value: '/clear', description: 'Clear conversation history', kind: 'command' },
280
+ { value: '/exit', description: 'Exit Haze', kind: 'command' },
281
+ { value: '/quit', description: 'Exit Haze', kind: 'command' },
282
+ ...skills.map(skill => ({ value: `/${skill.name}`, description: skill.description, kind: 'skill' })),
283
+ ] : [];
284
+ return _jsxs(Box, { flexDirection: "column", minHeight: height, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Header, { subtitle: headerSubtitle, version: version }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visible.map((message, index) => _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: message.role === 'user' ? theme.purple : message.role === 'assistant' ? theme.success : message.role === 'tool' ? theme.blue : theme.muted, bold: true, children: message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Haze' : message.role === 'tool' ? 'Tool' : 'Info' }), message.role === 'tool'
285
+ ? _jsx(ToolMessageText, { text: message.text, streaming: message.streaming })
286
+ : message.role === 'assistant' && !message.streaming
287
+ ? _jsx(MarkdownText, { content: message.text })
288
+ : _jsx(Text, { children: message.text })] }, index)) }), debug && debugLogs.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, marginBottom: 1, borderStyle: "round", borderColor: theme.muted, paddingX: 1, children: [_jsx(Text, { color: theme.muted, bold: true, children: "Debug" }), debugLogs.map((line, index) => _jsxs(Text, { color: theme.muted, children: ["\u2022 ", line] }, index))] }), busy && _jsx(Box, { flexShrink: 0, marginBottom: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Spinner, { type: "dots" }), " ", busyLabel, _jsx(Text, { dimColor: true, children: " \u00B7 esc to interrupt" })] }) }), _jsx(Box, { borderStyle: "round", borderColor: theme.deepPurple, paddingX: 1, flexShrink: 0, children: _jsx(Box, { flexGrow: 1, minWidth: 0, children: _jsx(TextInput, { placeholder: placeholder, disabled: busy, mask: mode === 'apiKey', historyItems: inputHistory, recordHistory: mode === 'chat', suggestions: slashSuggestions, onHistoryAdd: persistInputHistory, onCancel: cancelThinking, onSubmit: submit }) }) }), _jsxs(Box, { flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", flexShrink: 1, minWidth: 0, children: [_jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: workspaceLabel }), _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: statusDetailLabel })] }), _jsx(Box, { flexShrink: 0, marginLeft: 2, children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-start", children: activeModelName }) })] })] });
97
289
  }
98
290
  export async function chatCommand(options = {}) {
99
291
  const app = render(_jsx(ChatScreen, { debug: options.debug }));