@denizokcu/haze 0.0.2 → 0.1.0

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 (47) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +100 -34
  3. package/dist/cli/commands/chat.d.ts +3 -1
  4. package/dist/cli/commands/chat.js +500 -56
  5. package/dist/cli/commands/commands.d.ts +5 -0
  6. package/dist/cli/commands/commands.js +114 -29
  7. package/dist/cli/commands/formatters.js +32 -2
  8. package/dist/cli/commands/streaming.d.ts +6 -1
  9. package/dist/cli/commands/streaming.js +316 -98
  10. package/dist/cli/index.js +5 -2
  11. package/dist/config/inputHistory.js +8 -0
  12. package/dist/config/providers.d.ts +26 -0
  13. package/dist/config/providers.js +88 -0
  14. package/dist/config/settings.d.ts +9 -2
  15. package/dist/core/agent/compaction.d.ts +13 -0
  16. package/dist/core/agent/compaction.js +34 -0
  17. package/dist/core/agent/errors.d.ts +3 -0
  18. package/dist/core/agent/errors.js +13 -0
  19. package/dist/core/agent/events.d.ts +58 -0
  20. package/dist/core/agent/events.js +3 -0
  21. package/dist/core/goal/completionPolicy.d.ts +28 -0
  22. package/dist/core/goal/completionPolicy.js +84 -0
  23. package/dist/core/goal/requestClassifier.d.ts +6 -0
  24. package/dist/core/goal/requestClassifier.js +31 -0
  25. package/dist/core/goal/sessionGoal.d.ts +30 -0
  26. package/dist/core/goal/sessionGoal.js +88 -0
  27. package/dist/core/session/sessionStore.d.ts +37 -0
  28. package/dist/core/session/sessionStore.js +59 -0
  29. package/dist/core/subagent/subagentRunner.d.ts +33 -0
  30. package/dist/core/subagent/subagentRunner.js +140 -0
  31. package/dist/llm/client.d.ts +1 -1
  32. package/dist/llm/client.js +6 -6
  33. package/dist/llm/hazeTools.d.ts +86 -0
  34. package/dist/llm/hazeTools.js +313 -93
  35. package/dist/llm/initPrompt.js +6 -4
  36. package/dist/llm/systemPrompt.js +11 -7
  37. package/dist/skills/builder/SkillBuilder.d.ts +6 -0
  38. package/dist/skills/builder/SkillBuilder.js +146 -24
  39. package/dist/ui/components/ErrorView.d.ts +2 -1
  40. package/dist/ui/components/Header.d.ts +2 -1
  41. package/dist/ui/components/Header.js +1 -11
  42. package/dist/ui/components/MarkdownText.d.ts +2 -1
  43. package/dist/ui/components/TextInput.d.ts +7 -3
  44. package/dist/ui/components/TextInput.js +112 -27
  45. package/dist/ui/theme.d.ts +3 -0
  46. package/dist/ui/theme.js +4 -1
  47. package/package.json +8 -8
@@ -5,8 +5,13 @@ export type CommandContext = {
5
5
  settings: HazeSettings;
6
6
  contextFiles: ContextFile[];
7
7
  setMode: (mode: Mode) => void;
8
+ setModelProviderFilter?: (providerName: string | undefined) => void;
8
9
  addSystemMessage: (text: string) => void;
9
10
  clearConversation: () => void;
11
+ newSession?: () => Promise<void>;
12
+ resumeSession?: () => Promise<void>;
13
+ sessionInfo?: () => string;
14
+ compactConversation?: (instructions?: string) => boolean;
10
15
  runAgentTurn: (prompt: string, displayValue?: string) => Promise<void>;
11
16
  refreshContextFiles: () => Promise<ContextFile[]>;
12
17
  updateSettings: (patch: Partial<HazeSettings>) => Promise<HazeSettings>;
@@ -1,37 +1,47 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
3
  import { buildInitPrompt } from '../../llm/initPrompt.js';
4
+ import { activeProvider, configuredProviders, modelSelector, providerHasKey, resolveModelSelector, upsertProvider } from '../../config/providers.js';
4
5
  function skillHelp() {
5
6
  return [
6
7
  'Skill commands:',
7
- '/skill create <description>',
8
+ '/create-skill <description>',
8
9
  ' Create a Markdown skill in ~/.haze/skills.',
9
- '/skill list',
10
+ '/list-skills',
10
11
  ' List installed skills.',
11
- '/skill info <name>',
12
+ '/skill-info <name>',
12
13
  ' Show a skill description and path.',
13
- '/skill validate <name-or-dir>',
14
+ '/validate-skill <name-or-dir>',
14
15
  ' Validate a skill directory containing SKILL.md.',
15
- '/skill remove <name> --yes',
16
+ '/remove-skill <name> --yes',
16
17
  ' Remove an installed skill. Requires --yes because it deletes files.',
17
18
  ].join('\n');
18
19
  }
20
+ async function skillOverview() {
21
+ const { loadSkillRegistry } = await import('../../skills/SkillRegistry.js');
22
+ const registry = await loadSkillRegistry();
23
+ const skills = [...registry.skills.values()];
24
+ const installed = skills.length === 0
25
+ ? ['Installed skills:', '- None yet. Create one with /create-skill <description>.']
26
+ : ['Installed skills:', ...skills.map(s => `- /${s.name} — ${s.description}`)];
27
+ return `${skillHelp()}\n\n${installed.join('\n')}`;
28
+ }
19
29
  async function handleSkillCommand(value, ctx) {
20
30
  const parts = value.trim().split(/\s+/).filter(Boolean);
21
31
  const subcommand = parts[1];
22
32
  if (!subcommand || subcommand === 'help') {
23
- ctx.addSystemMessage(skillHelp());
33
+ ctx.addSystemMessage(await skillOverview());
24
34
  return 'handled';
25
35
  }
26
36
  if (subcommand === 'create') {
27
37
  const description = parts.slice(2).join(' ');
28
38
  if (!description) {
29
- ctx.addSystemMessage('Usage: /skill create <description>');
39
+ ctx.addSystemMessage('Usage: /create-skill <description>');
30
40
  return 'handled';
31
41
  }
32
42
  const { createSkill } = await import('../../skills/builder/SkillBuilder.js');
33
43
  const result = await createSkill(description);
34
- ctx.addSystemMessage(`Created skill ${result.name} at ${result.file}. Edit SKILL.md to refine its workflow.`);
44
+ ctx.addSystemMessage(`Created skill ${result.name} at ${result.file}. Invoke it with /${result.name}. Edit SKILL.md to refine its workflow.`);
35
45
  return 'handled';
36
46
  }
37
47
  if (subcommand === 'list') {
@@ -39,14 +49,14 @@ async function handleSkillCommand(value, ctx) {
39
49
  const registry = await loadSkillRegistry();
40
50
  const skills = [...registry.skills.values()];
41
51
  ctx.addSystemMessage(skills.length === 0
42
- ? 'No installed skills found.'
43
- : ['Installed skills:', ...skills.map(s => `- ${s.name} — ${s.description}`)].join('\n'));
52
+ ? 'No installed skills found. Create one with /create-skill <description>.'
53
+ : ['Installed skills:', ...skills.map(s => `- /${s.name} — ${s.description}`)].join('\n'));
44
54
  return 'handled';
45
55
  }
46
56
  if (subcommand === 'info') {
47
57
  const name = parts[2];
48
58
  if (!name) {
49
- ctx.addSystemMessage('Usage: /skill info <name>');
59
+ ctx.addSystemMessage('Usage: /skill-info <name>');
50
60
  return 'handled';
51
61
  }
52
62
  const { loadSkillRegistry } = await import('../../skills/SkillRegistry.js');
@@ -68,7 +78,7 @@ async function handleSkillCommand(value, ctx) {
68
78
  if (subcommand === 'validate') {
69
79
  const target = parts[2];
70
80
  if (!target) {
71
- ctx.addSystemMessage('Usage: /skill validate <name-or-dir>');
81
+ ctx.addSystemMessage('Usage: /validate-skill <name-or-dir>');
72
82
  return 'handled';
73
83
  }
74
84
  const { GLOBAL_SKILLS_DIR } = await import('../../config/paths.js');
@@ -82,7 +92,7 @@ async function handleSkillCommand(value, ctx) {
82
92
  if (subcommand === 'remove') {
83
93
  const name = parts[2];
84
94
  if (!name || !parts.includes('--yes')) {
85
- ctx.addSystemMessage('Usage: /skill remove <name> --yes');
95
+ ctx.addSystemMessage('Usage: /remove-skill <name> --yes');
86
96
  return 'handled';
87
97
  }
88
98
  const { loadSkillRegistry } = await import('../../skills/SkillRegistry.js');
@@ -107,18 +117,28 @@ export async function handleSlashCommand(value, ctx) {
107
117
  'Commands:',
108
118
  '/help',
109
119
  ' Show all available slash commands and what they do.',
110
- '/login',
111
- ' Save an OpenRouter API key to ~/.haze/settings.json.',
120
+ '/provider',
121
+ ' Choose a provider, then use it or add models; choose add provider at the bottom to create one.',
112
122
  '/model',
113
- ' Prompt for an OpenRouter model name to use for future chats.',
114
- '/model <name>',
115
- ' Set the OpenRouter model directly, for example x-ai/grok-build-0.1.',
123
+ ' Choose a model from all configured providers.',
124
+ '/model <name-or-provider:name>',
125
+ ' Set a model directly. Selecting a model also sets its provider.',
116
126
  '/settings',
117
127
  ' Show the configured provider, model, API key status, and loaded context files.',
118
- '/skill help',
119
- ' Manage Markdown skills from inside the Haze app.',
128
+ '/create-skill <description>',
129
+ ' Create a reusable Markdown workflow from how you work.',
130
+ '/skills',
131
+ ' Show Markdown skill commands and installed skill slash commands.',
120
132
  '/init',
121
133
  ' Inspect the current workspace and create or update AGENTS.md project instructions.',
134
+ '/session',
135
+ ' Show the current durable session file.',
136
+ '/resume',
137
+ ' Resume the latest saved session for this workspace.',
138
+ '/new',
139
+ ' Start a fresh durable session.',
140
+ '/compact [instructions]',
141
+ ' Summarize older model context and keep recent messages.',
122
142
  '/clear',
123
143
  ' Clear the current chat conversation history.',
124
144
  '/exit',
@@ -128,29 +148,82 @@ export async function handleSlashCommand(value, ctx) {
128
148
  ].join('\n'));
129
149
  return 'handled';
130
150
  }
151
+ if (value === '/session') {
152
+ ctx.addSystemMessage(ctx.sessionInfo?.() ?? 'Session persistence is unavailable.');
153
+ return 'handled';
154
+ }
155
+ if (value === '/resume') {
156
+ if (ctx.resumeSession)
157
+ await ctx.resumeSession();
158
+ else
159
+ ctx.addSystemMessage('Session persistence is unavailable.');
160
+ return 'handled';
161
+ }
162
+ if (value === '/new') {
163
+ if (ctx.newSession)
164
+ await ctx.newSession();
165
+ else
166
+ ctx.addSystemMessage('Session persistence is unavailable.');
167
+ return 'handled';
168
+ }
169
+ if (value === '/compact' || value.startsWith('/compact ')) {
170
+ if (ctx.compactConversation)
171
+ ctx.compactConversation(value.slice('/compact'.length).trim() || undefined);
172
+ else
173
+ ctx.addSystemMessage('Compaction is unavailable.');
174
+ return 'handled';
175
+ }
131
176
  if (value === '/clear') {
132
177
  ctx.clearConversation();
133
178
  ctx.addSystemMessage('Cleared. The void is productive.');
134
179
  return 'handled';
135
180
  }
136
181
  if (value === '/settings') {
137
- ctx.addSystemMessage(`Provider: ${ctx.settings.provider ?? 'not configured'} | Model: ${ctx.settings.model ?? 'not set'} | API key: ${ctx.settings.apiKey ? 'saved' : 'missing'} | Context files: ${ctx.contextFiles.length ? ctx.contextFiles.map(file => file.path).join(', ') : 'none'}`);
182
+ const providers = configuredProviders(ctx.settings);
183
+ const activeProvider = providers.find(provider => provider.name === ctx.settings.provider) ?? providers[0];
184
+ ctx.addSystemMessage([
185
+ `Provider: ${activeProvider?.name ?? 'not configured'}`,
186
+ `Model: ${ctx.settings.model ?? 'not set'}`,
187
+ `Base URL: ${activeProvider?.url ?? ctx.settings.baseURL ?? 'not configured'}`,
188
+ `API key: ${activeProvider && providerHasKey(ctx.settings, activeProvider) ? 'saved' : 'missing'}`,
189
+ `Configured providers: ${providers.map(provider => provider.name).join(', ') || 'none'}`,
190
+ `Context files: ${ctx.contextFiles.length ? ctx.contextFiles.map(file => file.path).join(', ') : 'none'}`,
191
+ ].join(' | '));
138
192
  return 'handled';
139
193
  }
140
- if (value === '/login') {
141
- ctx.setMode('apiKey');
142
- ctx.addSystemMessage('Paste your OpenRouter API key. It will be stored in ~/.haze/settings.json.');
194
+ if (value === '/provider') {
195
+ ctx.setModelProviderFilter?.(undefined);
196
+ ctx.setMode('provider');
197
+ ctx.addSystemMessage('Choose a provider. Selecting one opens provider actions. Choose “add provider” at the bottom to add a new provider.');
143
198
  return 'handled';
144
199
  }
145
200
  if (value === '/model') {
201
+ ctx.setModelProviderFilter?.(undefined);
146
202
  ctx.setMode('model');
147
- ctx.addSystemMessage('Enter an OpenRouter model name, e.g. x-ai/grok-build-0.1 or anthropic/claude-3.5-sonnet.');
203
+ ctx.addSystemMessage('Choose a model. Selecting a model also sets its provider.');
204
+ return 'handled';
205
+ }
206
+ if (value === '/model list') {
207
+ const providers = configuredProviders(ctx.settings);
208
+ ctx.addSystemMessage(['Configured models:', ...providers.flatMap(provider => provider.models.map(model => `- ${modelSelector(provider, model)} — ${provider.name}`))].join('\n'));
148
209
  return 'handled';
149
210
  }
150
211
  if (value.startsWith('/model ')) {
151
- const modelName = value.slice('/model '.length).trim();
152
- await ctx.updateSettings({ model: modelName });
153
- ctx.addSystemMessage(`Model set to ${modelName}. Saved to ~/.haze/settings.json and will be used until you set a new model.`);
212
+ const selector = value.slice('/model '.length).trim();
213
+ const resolved = resolveModelSelector(ctx.settings, selector);
214
+ if (resolved.status === 'ambiguous') {
215
+ ctx.addSystemMessage(`Model ${resolved.model} exists on multiple providers: ${resolved.providers.map(provider => modelSelector(provider, resolved.model)).join(', ')}`);
216
+ return 'handled';
217
+ }
218
+ if (resolved.status === 'missing') {
219
+ const provider = activeProvider(ctx.settings);
220
+ const nextProvider = provider.models.includes(selector) ? provider : { ...provider, models: [...provider.models, selector] };
221
+ await ctx.updateSettings({ provider: provider.name, model: selector, providers: upsertProvider(ctx.settings, nextProvider) });
222
+ ctx.addSystemMessage(`Model set to ${selector} on ${provider.name}. Saved to ~/.haze/settings.json.`);
223
+ return 'handled';
224
+ }
225
+ await ctx.updateSettings({ provider: resolved.provider.name, model: resolved.model });
226
+ ctx.addSystemMessage(`Model set to ${resolved.model} on ${resolved.provider.name}. Saved to ~/.haze/settings.json.`);
154
227
  return 'handled';
155
228
  }
156
229
  if (value === '/init') {
@@ -158,7 +231,19 @@ export async function handleSlashCommand(value, ctx) {
158
231
  await ctx.refreshContextFiles();
159
232
  return 'handled';
160
233
  }
161
- if (value === '/skill' || value.startsWith('/skill ') || value === '/skills' || value.startsWith('/skills ')) {
234
+ if (value === '/skills')
235
+ return await handleSkillCommand('/skill help', ctx);
236
+ if (value === '/list-skills')
237
+ return await handleSkillCommand('/skill list', ctx);
238
+ if (value === '/create-skill' || value.startsWith('/create-skill '))
239
+ return await handleSkillCommand(`/skill create${value.slice('/create-skill'.length)}`, ctx);
240
+ if (value === '/skill-info' || value.startsWith('/skill-info '))
241
+ return await handleSkillCommand(`/skill info${value.slice('/skill-info'.length)}`, ctx);
242
+ if (value === '/validate-skill' || value.startsWith('/validate-skill '))
243
+ return await handleSkillCommand(`/skill validate${value.slice('/validate-skill'.length)}`, ctx);
244
+ if (value === '/remove-skill' || value.startsWith('/remove-skill '))
245
+ return await handleSkillCommand(`/skill remove${value.slice('/remove-skill'.length)}`, ctx);
246
+ if (value === '/skill' || value.startsWith('/skill ') || value.startsWith('/skills ')) {
162
247
  const normalized = value.replace(/^\/skills\b/, '/skill');
163
248
  return await handleSkillCommand(normalized, ctx);
164
249
  }
@@ -19,6 +19,11 @@ export function toolCallSummary(toolName, input) {
19
19
  const timeout = typeof data.timeoutSeconds === 'number' ? ` (timeout ${data.timeoutSeconds}s)` : '';
20
20
  return `bash $ ${data.command}${timeout}`;
21
21
  }
22
+ if (toolName === 'grep' && typeof data?.pattern === 'string') {
23
+ const path = typeof data.path === 'string' && data.path !== '.' ? ` in ${data.path}` : '';
24
+ const glob = typeof data.glob === 'string' ? ` (${data.glob})` : '';
25
+ return `grep "${data.pattern}"${path}${glob}`;
26
+ }
22
27
  if (toolName === 'listFiles' && typeof data?.path === 'string')
23
28
  return `listFiles ${data.path}`;
24
29
  if ((toolName === 'readFile' || toolName === 'writeFile') && typeof data?.path === 'string')
@@ -29,6 +34,10 @@ export function toolCallSummary(toolName, input) {
29
34
  }
30
35
  if (toolName === 'replaceLines' && typeof data?.path === 'string')
31
36
  return `replaceLines ${data.path}:${data.startLine}-${data.endLine}`;
37
+ if (toolName === 'subagent' && typeof data?.task === 'string') {
38
+ const taskPreview = data.task.length > 60 ? `${data.task.slice(0, 60).trimEnd()}…` : data.task;
39
+ return `subagent "${taskPreview}"`;
40
+ }
32
41
  return `${toolName} ${compact(input)}`;
33
42
  }
34
43
  export function toolResultSummary(event) {
@@ -37,10 +46,31 @@ export function toolResultSummary(event) {
37
46
  const output = event.output;
38
47
  if (output?.duplicateSkipped === true)
39
48
  return 'skipped duplicate';
49
+ if (typeof output?.totalMatches === 'number') {
50
+ const count = output.totalMatches;
51
+ return count === 0 ? 'no matches' : `${count} match${count === 1 ? '' : 'es'}`;
52
+ }
40
53
  if (typeof output?.code === 'number')
41
54
  return `exited with code ${output.code}`;
42
- if (typeof output?.ok === 'boolean')
43
- return output.ok ? 'completed' : `failed: ${compact(output)}`;
55
+ if (typeof output?.status === 'string' && typeof output?.summary === 'string') {
56
+ const summary = output.summary.split('\n')[0] ?? '';
57
+ const preview = summary.length > 120 ? `${summary.slice(0, 120).trimEnd()}…` : summary;
58
+ const calls = typeof output.toolCallCount === 'number' ? output.toolCallCount : output.toolCalls?.length ?? 0;
59
+ const duration = typeof output.durationMs === 'number' ? ` in ${(output.durationMs / 1000).toFixed(1)}s` : '';
60
+ const meta = calls > 0 ? ` (${calls} call${calls === 1 ? '' : 's'}${duration})` : '';
61
+ return `${output.status}${meta}: ${preview}`;
62
+ }
63
+ if (typeof output?.ok === 'boolean') {
64
+ if (output.ok) {
65
+ if (typeof output.addedLines === 'number' || typeof output.removedLines === 'number') {
66
+ const added = typeof output.addedLines === 'number' ? output.addedLines : 0;
67
+ const removed = typeof output.removedLines === 'number' ? output.removedLines : 0;
68
+ return `Added ${added} line${added === 1 ? '' : 's'}, removed ${removed} line${removed === 1 ? '' : 's'}`;
69
+ }
70
+ return 'completed';
71
+ }
72
+ return typeof output.error === 'string' ? `failed: ${compact(output.error)}` : 'failed';
73
+ }
44
74
  return 'completed';
45
75
  }
46
76
  export function formatSeconds(milliseconds) {
@@ -1,5 +1,6 @@
1
1
  import { type ModelMessage } from 'ai';
2
2
  import type { ContextFile } from '../../config/contextFiles.js';
3
+ import { type AgentEventSink } from '../../core/agent/events.js';
3
4
  export type Message = {
4
5
  id?: string;
5
6
  role: 'system' | 'user' | 'assistant' | 'tool';
@@ -12,10 +13,14 @@ export interface StreamCallbacks {
12
13
  updateMessage: (id: string, update: Partial<Message>) => void;
13
14
  setConversation: (messages: ModelMessage[]) => void;
14
15
  setBusy: (busy: boolean) => void;
16
+ setBusyLabel?: (label: string) => void;
15
17
  debugLog: (line: string) => void;
16
18
  getConversation: () => ModelMessage[];
17
19
  getLastAssistantText: () => string;
18
20
  setLastAssistantText: (text: string) => void;
19
21
  setAbortController?: (controller: AbortController | null) => void;
22
+ setGoalStatus?: (status: string | undefined) => void;
23
+ onEvent?: AgentEventSink;
24
+ compactConversation?: (instructions?: string) => boolean;
20
25
  }
21
- export declare function runAgentTurn(value: string, displayValue: string | undefined, contextFiles: ContextFile[], callbacks: StreamCallbacks): Promise<void>;
26
+ export declare function runAgentTurn(value: string, displayValue: string | undefined, contextFiles: ContextFile[], callbacks: StreamCallbacks, retryAttempt?: number, retryingExistingRequest?: boolean, contextOverflowRecovered?: boolean): Promise<void>;