@denizokcu/haze 0.0.2 → 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 (45) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +87 -33
  3. package/dist/cli/commands/chat.d.ts +3 -1
  4. package/dist/cli/commands/chat.js +442 -52
  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 +5 -2
  8. package/dist/cli/commands/streaming.d.ts +5 -1
  9. package/dist/cli/commands/streaming.js +193 -86
  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 +27 -0
  22. package/dist/core/goal/completionPolicy.js +67 -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/llm/client.d.ts +1 -1
  30. package/dist/llm/client.js +6 -6
  31. package/dist/llm/hazeTools.d.ts +38 -0
  32. package/dist/llm/hazeTools.js +196 -92
  33. package/dist/llm/initPrompt.js +6 -4
  34. package/dist/llm/systemPrompt.js +3 -3
  35. package/dist/skills/builder/SkillBuilder.d.ts +6 -0
  36. package/dist/skills/builder/SkillBuilder.js +146 -24
  37. package/dist/ui/components/ErrorView.d.ts +2 -1
  38. package/dist/ui/components/Header.d.ts +2 -1
  39. package/dist/ui/components/Header.js +1 -11
  40. package/dist/ui/components/MarkdownText.d.ts +2 -1
  41. package/dist/ui/components/TextInput.d.ts +7 -3
  42. package/dist/ui/components/TextInput.js +112 -27
  43. package/dist/ui/theme.d.ts +1 -0
  44. package/dist/ui/theme.js +2 -1
  45. package/package.json +8 -8
@@ -0,0 +1,88 @@
1
+ export const DEFAULT_PROVIDER_NAME = 'openrouter';
2
+ export const DEFAULT_PROVIDER_URL = 'https://openrouter.ai/api/v1';
3
+ export const DEFAULT_MODEL = 'x-ai/grok-build-0.1';
4
+ function normalizeModels(models, fallbackModel) {
5
+ const values = Array.isArray(models) ? models : [];
6
+ const normalized = values
7
+ .filter((model) => typeof model === 'string')
8
+ .map(model => model.trim())
9
+ .filter(Boolean);
10
+ if (normalized.length > 0)
11
+ return [...new Set(normalized)];
12
+ return fallbackModel ? [fallbackModel] : [];
13
+ }
14
+ function normalizeProvider(provider, fallbackModel) {
15
+ const name = provider.name?.trim();
16
+ const url = provider.url?.trim();
17
+ if (!name || !url)
18
+ return undefined;
19
+ const key = provider.key?.trim();
20
+ return {
21
+ name,
22
+ url,
23
+ ...(key ? { key } : {}),
24
+ models: normalizeModels(provider.models, fallbackModel),
25
+ };
26
+ }
27
+ export function configuredProviders(settings) {
28
+ const providers = (settings.providers ?? [])
29
+ .map(provider => normalizeProvider(provider))
30
+ .filter((provider) => Boolean(provider));
31
+ const legacyUrl = settings.baseURL ?? (settings.provider === DEFAULT_PROVIDER_NAME || settings.apiKey ? DEFAULT_PROVIDER_URL : undefined);
32
+ if (legacyUrl && !providers.some(provider => provider.name === DEFAULT_PROVIDER_NAME)) {
33
+ providers.unshift({
34
+ name: DEFAULT_PROVIDER_NAME,
35
+ url: legacyUrl,
36
+ ...(settings.apiKey ? { key: settings.apiKey } : {}),
37
+ models: normalizeModels([], settings.model ?? DEFAULT_MODEL),
38
+ });
39
+ }
40
+ if (providers.length === 0) {
41
+ return [{ name: DEFAULT_PROVIDER_NAME, url: DEFAULT_PROVIDER_URL, models: [settings.model ?? DEFAULT_MODEL] }];
42
+ }
43
+ return providers;
44
+ }
45
+ export function findProvider(settings, name) {
46
+ return configuredProviders(settings).find(provider => provider.name === name);
47
+ }
48
+ export function activeProvider(settings) {
49
+ const providers = configuredProviders(settings);
50
+ return providers.find(provider => provider.name === settings.provider) ?? providers[0];
51
+ }
52
+ export function activeModel(settings) {
53
+ const provider = activeProvider(settings);
54
+ const model = settings.model && provider.models.includes(settings.model)
55
+ ? settings.model
56
+ : settings.model ?? provider.models[0] ?? DEFAULT_MODEL;
57
+ return { provider, model };
58
+ }
59
+ export function resolveModelSelector(settings, selector) {
60
+ const trimmed = selector.trim();
61
+ if (!trimmed)
62
+ return { status: 'missing' };
63
+ const separator = trimmed.indexOf(':');
64
+ if (separator > 0) {
65
+ const providerName = trimmed.slice(0, separator).trim();
66
+ const model = trimmed.slice(separator + 1).trim();
67
+ const provider = findProvider(settings, providerName);
68
+ if (!provider || !model || !provider.models.includes(model))
69
+ return { status: 'missing' };
70
+ return { status: 'found', provider, model };
71
+ }
72
+ const matches = configuredProviders(settings).filter(provider => provider.models.includes(trimmed));
73
+ if (matches.length === 1)
74
+ return { status: 'found', provider: matches[0], model: trimmed };
75
+ if (matches.length > 1)
76
+ return { status: 'ambiguous', model: trimmed, providers: matches };
77
+ return { status: 'missing' };
78
+ }
79
+ export function modelSelector(provider, model) {
80
+ return `${provider.name}:${model}`;
81
+ }
82
+ export function upsertProvider(settings, provider) {
83
+ const providers = configuredProviders(settings).filter(existing => existing.name !== provider.name);
84
+ return [...providers, provider];
85
+ }
86
+ export function providerHasKey(settings, provider) {
87
+ return Boolean(provider.key ?? (provider.name === DEFAULT_PROVIDER_NAME ? settings.apiKey : undefined));
88
+ }
@@ -1,8 +1,15 @@
1
+ export interface HazeProviderSettings {
2
+ name: string;
3
+ url: string;
4
+ key?: string;
5
+ models: string[];
6
+ }
1
7
  export interface HazeSettings {
2
- provider?: 'openrouter';
8
+ provider?: string;
9
+ model?: string;
10
+ providers?: HazeProviderSettings[];
3
11
  apiKey?: string;
4
12
  baseURL?: string;
5
- model?: string;
6
13
  }
7
14
  export declare const SETTINGS_FILE: string;
8
15
  export declare function readSettings(): Promise<HazeSettings>;
@@ -0,0 +1,13 @@
1
+ import type { ModelMessage } from 'ai';
2
+ export interface CompactionResult {
3
+ compacted: boolean;
4
+ messages: ModelMessage[];
5
+ olderCount: number;
6
+ keptCount: number;
7
+ summary?: string;
8
+ }
9
+ export declare function modelMessageText(message: ModelMessage): string;
10
+ export declare function compactModelMessages(messages: ModelMessage[], options?: {
11
+ keepRecentMessages?: number;
12
+ instructions?: string;
13
+ }): CompactionResult;
@@ -0,0 +1,34 @@
1
+ export function modelMessageText(message) {
2
+ const content = message.content;
3
+ if (typeof content === 'string')
4
+ return content;
5
+ if (!Array.isArray(content))
6
+ return '';
7
+ return content.map(part => typeof part === 'object' && part != null && 'text' in part && typeof part.text === 'string' ? part.text : '').filter(Boolean).join('\n');
8
+ }
9
+ export function compactModelMessages(messages, options = {}) {
10
+ const keepRecentMessages = options.keepRecentMessages ?? 12;
11
+ if (messages.length <= keepRecentMessages) {
12
+ return { compacted: false, messages, olderCount: 0, keptCount: messages.length };
13
+ }
14
+ const older = messages.slice(0, -keepRecentMessages);
15
+ const recent = messages.slice(-keepRecentMessages);
16
+ const oldText = older.map(message => {
17
+ const text = modelMessageText(message).replace(/\s+/g, ' ').trim();
18
+ return text ? `- ${message.role}: ${text.slice(0, 500)}` : '';
19
+ }).filter(Boolean).join('\n');
20
+ const summary = [
21
+ 'Compacted prior Haze conversation. Continue preserving the user goal, constraints, decisions, files touched, validation results, and unresolved next steps from this summary.',
22
+ options.instructions ? `User compaction instructions: ${options.instructions}` : undefined,
23
+ '',
24
+ 'Older context summary:',
25
+ oldText || '- Older messages were tool-only or non-text.',
26
+ ].filter((line) => line !== undefined).join('\n');
27
+ return {
28
+ compacted: true,
29
+ messages: [{ role: 'system', content: summary }, ...recent],
30
+ olderCount: older.length,
31
+ keptCount: recent.length,
32
+ summary,
33
+ };
34
+ }
@@ -0,0 +1,3 @@
1
+ export declare function errorText(error: unknown): string;
2
+ export declare function isContextOverflowError(error: unknown): boolean;
3
+ export declare function isRetryableModelError(error: unknown): boolean;
@@ -0,0 +1,13 @@
1
+ export function errorText(error) {
2
+ return (error instanceof Error ? `${error.name} ${error.message}` : String(error)).toLowerCase();
3
+ }
4
+ export function isContextOverflowError(error) {
5
+ const text = errorText(error);
6
+ return /context length|context window|context limit|maximum context|max context|token limit|too many tokens|input too long|prompt too long|context.*exceed|tokens.*exceed/.test(text);
7
+ }
8
+ export function isRetryableModelError(error) {
9
+ const text = errorText(error);
10
+ if (isContextOverflowError(error) || /quota|billing|balance|auth|api key|invalid request|permission|forbidden|401|403|400/.test(text))
11
+ return false;
12
+ return /overload|rate limit|429|500|502|503|504|network|connection|stream|timeout|timed? out|econnreset|etimedout|fetch failed/.test(text);
13
+ }
@@ -0,0 +1,58 @@
1
+ export type AgentEvent = {
2
+ type: 'turn_start';
3
+ request: string;
4
+ at: string;
5
+ } | {
6
+ type: 'turn_end';
7
+ request: string;
8
+ at: string;
9
+ status: 'complete' | 'aborted' | 'failed';
10
+ } | {
11
+ type: 'message_start';
12
+ id: string;
13
+ role: 'assistant';
14
+ at: string;
15
+ } | {
16
+ type: 'message_update';
17
+ id: string;
18
+ text: string;
19
+ at: string;
20
+ } | {
21
+ type: 'message_end';
22
+ id: string;
23
+ text: string;
24
+ at: string;
25
+ hidden?: boolean;
26
+ } | {
27
+ type: 'tool_start';
28
+ id: string;
29
+ name: string;
30
+ input: unknown;
31
+ at: string;
32
+ } | {
33
+ type: 'tool_end';
34
+ id: string;
35
+ name: string;
36
+ success: boolean;
37
+ output?: unknown;
38
+ error?: unknown;
39
+ durationMs: number;
40
+ at: string;
41
+ } | {
42
+ type: 'retry';
43
+ attempt: number;
44
+ maxAttempts: number;
45
+ delayMs: number;
46
+ error: string;
47
+ at: string;
48
+ } | {
49
+ type: 'context_overflow';
50
+ recovered: boolean;
51
+ error: string;
52
+ at: string;
53
+ };
54
+ export type AgentEventSink = (event: AgentEvent) => void;
55
+ export type AgentEventInput = AgentEvent extends infer Event ? Event extends {
56
+ at: string;
57
+ } ? Omit<Event, 'at'> : never : never;
58
+ export declare function agentEvent(event: AgentEventInput): AgentEvent;
@@ -0,0 +1,3 @@
1
+ export function agentEvent(event) {
2
+ return { ...event, at: new Date().toISOString() };
3
+ }
@@ -0,0 +1,27 @@
1
+ import type { SessionGoal } from './sessionGoal.js';
2
+ export declare function looksIncomplete(text: string): boolean;
3
+ export declare function looksBlocked(text: string): boolean;
4
+ export interface CompletionPolicyInput {
5
+ request: string;
6
+ goal: SessionGoal;
7
+ assistantText: string;
8
+ sawReadOnlyTool: boolean;
9
+ sawToolCall: boolean;
10
+ mutatingToolSucceeded: boolean;
11
+ validationToolSucceeded: boolean;
12
+ validationToolFailed: boolean;
13
+ editFileFailed: boolean;
14
+ editRecoveryPath?: string;
15
+ }
16
+ export interface CompletionDecision {
17
+ needsActionContinuation: boolean;
18
+ needsValidationContinuation: boolean;
19
+ requestCompletedByTools: boolean;
20
+ assistantAdmitsIncomplete: boolean;
21
+ assistantReportsBlocker: boolean;
22
+ continuationPrompt?: string;
23
+ }
24
+ export declare function completionDecision(input: CompletionPolicyInput): CompletionDecision;
25
+ export declare function toolLoopBudgetPrompt(): string;
26
+ export declare function postContinuationPrompt(): string;
27
+ export declare function noTextAfterToolPrompt(allowTools: boolean): "Continue the original request now. If it asks for a change, edit or write the necessary files. If it asks to run or verify tests, run the command. Do not provide only a retrospective summary unless blocked." | "Continue from the tool result and answer my original request. Do not call tools. Summarize only current-turn changes and validation; do not recap unrelated earlier tasks.";
@@ -0,0 +1,67 @@
1
+ import { isActionRequest, isPlanOnlyRequest, isValidationRequest } from './requestClassifier.js';
2
+ export function looksIncomplete(text) {
3
+ return /\b(incomplete|what remains|remains:|remaining:|next:|not implemented|not created|no tests exist|created no docs|has not been|have not been|not yet|never executed|not executed|not run|cannot retry|cannot write|cannot validate|tool budget reached)/i.test(text);
4
+ }
5
+ export function looksBlocked(text) {
6
+ return /\b(blocked|blocker|needs user|need user|missing permission|permission denied|missing dependency|no practical validation|unable to validate|can't validate|cannot validate)\b/i.test(text);
7
+ }
8
+ export function completionDecision(input) {
9
+ const likelyPlanOnlyRequest = isPlanOnlyRequest(input.request);
10
+ const likelyActionRequest = isActionRequest(input.request);
11
+ const likelyValidationRequest = isValidationRequest(input.request);
12
+ const assistantAdmitsIncomplete = looksIncomplete(input.assistantText);
13
+ const assistantReportsBlocker = looksBlocked(input.assistantText);
14
+ const requestCompletedByTools = input.mutatingToolSucceeded && input.validationToolSucceeded && !input.editRecoveryPath;
15
+ const changedActionNeedsValidation = likelyActionRequest
16
+ && !likelyPlanOnlyRequest
17
+ && input.mutatingToolSucceeded
18
+ && !input.validationToolSucceeded
19
+ && !input.validationToolFailed
20
+ && !input.editRecoveryPath
21
+ && !assistantReportsBlocker;
22
+ const needsActionContinuation = likelyActionRequest
23
+ && !likelyPlanOnlyRequest
24
+ && !requestCompletedByTools
25
+ && ((input.sawReadOnlyTool && !input.mutatingToolSucceeded) || input.validationToolFailed || input.editFileFailed || assistantAdmitsIncomplete);
26
+ const needsValidationContinuation = (likelyValidationRequest || changedActionNeedsValidation)
27
+ && !requestCompletedByTools
28
+ && !input.validationToolSucceeded
29
+ && !assistantReportsBlocker;
30
+ let continuationPrompt;
31
+ if (input.editFileFailed) {
32
+ continuationPrompt = 'Your editFile attempt failed. Use the latest readFile line-numbered output and replaceLines to complete the requested change. Continue with any remaining tests or validation if relevant. Do not stop with a summary.';
33
+ }
34
+ else if (input.validationToolFailed && input.mutatingToolSucceeded) {
35
+ continuationPrompt = 'Validation failed after files changed in this task. Inspect the failure output, fix failures that are plausibly caused by the current change, then rerun the relevant validation once. If the failure is clearly unrelated or environment-specific, summarize the blocker instead of expanding scope.';
36
+ }
37
+ else if (needsValidationContinuation) {
38
+ continuationPrompt = changedActionNeedsValidation
39
+ ? 'Files changed for this request, but no validation has run yet. Continue by running the smallest relevant test/check command you can identify from the project. If no practical validation exists, state that concrete blocker briefly instead of claiming the goal is complete.'
40
+ : 'You have not run the requested validation yet. Continue now by running the appropriate test/check command. Summarize only after the command finishes.';
41
+ }
42
+ else if (input.mutatingToolSucceeded && assistantAdmitsIncomplete) {
43
+ continuationPrompt = 'Your previous response says the current request is incomplete. Continue now with the remaining edits and validation for this same request. Do not summarize a plan unless blocked.';
44
+ }
45
+ else if (needsActionContinuation) {
46
+ continuationPrompt = 'You inspected files but have not made the requested change yet. Continue now by editing or writing the necessary files. Do not summarize a plan unless blocked.';
47
+ }
48
+ return {
49
+ needsActionContinuation,
50
+ needsValidationContinuation,
51
+ requestCompletedByTools,
52
+ assistantAdmitsIncomplete,
53
+ assistantReportsBlocker,
54
+ continuationPrompt,
55
+ };
56
+ }
57
+ export function toolLoopBudgetPrompt() {
58
+ return 'Tool budget reached. You cannot call tools now, and you must not output XML, JSON tool-call syntax, <tool_call> blocks, or function-call markup. If the current request is complete, summarize only current-turn changes and validation. If the requested change is incomplete, state the concrete blocker briefly. Do not claim tools are unavailable, recap unrelated earlier tasks, or provide a generic remains list.';
59
+ }
60
+ export function postContinuationPrompt() {
61
+ return 'Your previous response still described unfinished work, missing validation, or a tool-budget issue. If any tools are still available, complete the remaining edit or run the final validation now. Only call something a blocker if a concrete tool failure prevents progress.';
62
+ }
63
+ export function noTextAfterToolPrompt(allowTools) {
64
+ return allowTools
65
+ ? 'Continue the original request now. If it asks for a change, edit or write the necessary files. If it asks to run or verify tests, run the command. Do not provide only a retrospective summary unless blocked.'
66
+ : 'Continue from the tool result and answer my original request. Do not call tools. Summarize only current-turn changes and validation; do not recap unrelated earlier tasks.';
67
+ }
@@ -0,0 +1,6 @@
1
+ export type RequestIntent = 'implement' | 'fix' | 'test' | 'review' | 'plan' | 'answer' | 'unknown';
2
+ export declare function isPlanOnlyRequest(value: string): boolean;
3
+ export declare function isPlanImplementationRequest(value: string): boolean;
4
+ export declare function isValidationRequest(value: string): boolean;
5
+ export declare function isActionRequest(value: string): boolean;
6
+ export declare function classifyRequestIntent(value: string): RequestIntent;
@@ -0,0 +1,31 @@
1
+ export function isPlanOnlyRequest(value) {
2
+ return /\b(create|make|write|draft|outline)\s+(?:a\s+)?plan\b|\bplan\s+(?:for|to)\b/i.test(value) && !/\bimplement|execute|do\b/i.test(value);
3
+ }
4
+ export function isPlanImplementationRequest(value) {
5
+ return /\b(implement|execute|do)\b.*\bplan\b|\bplan\.md\b|\btest_plan\.md\b/i.test(value);
6
+ }
7
+ export function isValidationRequest(value) {
8
+ if (isPlanOnlyRequest(value))
9
+ return false;
10
+ return /\b(run|verify|test|tests|check|validate)\b/i.test(value);
11
+ }
12
+ export function isActionRequest(value) {
13
+ if (isPlanOnlyRequest(value))
14
+ return false;
15
+ return /\b(add|create|write|implement|update|fix|change|support|wire|test|tests|document|docs|documentation|run|verify)\b/i.test(value);
16
+ }
17
+ export function classifyRequestIntent(value) {
18
+ if (isPlanOnlyRequest(value))
19
+ return 'plan';
20
+ if (/\b(review|audit|inspect|analy[sz]e|compare)\b/i.test(value))
21
+ return 'review';
22
+ if (/\b(fix|repair|resolve|debug)\b/i.test(value))
23
+ return 'fix';
24
+ if (/\b(run|verify|check|validate)\b/i.test(value) || /\btests?\b/i.test(value) && !/\b(add|create|write)\b/i.test(value))
25
+ return 'test';
26
+ if (/\b(add|create|write|implement|update|change|support|wire|document|docs|documentation)\b/i.test(value))
27
+ return 'implement';
28
+ if (/\b(what|why|how|explain|tell me)\b/i.test(value))
29
+ return 'answer';
30
+ return 'unknown';
31
+ }
@@ -0,0 +1,30 @@
1
+ import { type RequestIntent } from './requestClassifier.js';
2
+ export type SessionGoalStatus = 'active' | 'needs-user' | 'blocked' | 'complete' | 'aborted';
3
+ export type ValidationStatus = 'pending' | 'passed' | 'failed';
4
+ export interface SessionGoal {
5
+ id: string;
6
+ originalUserRequest: string;
7
+ normalizedIntent: RequestIntent;
8
+ successCriteria: string[];
9
+ constraints: string[];
10
+ touchedFiles: string[];
11
+ validationCommands: Array<{
12
+ command: string;
13
+ status: ValidationStatus;
14
+ }>;
15
+ status: SessionGoalStatus;
16
+ phase: 'starting' | 'inspecting' | 'editing' | 'validating' | 'summarizing' | 'done';
17
+ blocker?: string;
18
+ lastProgressAt: number;
19
+ }
20
+ export interface GoalToolEvent {
21
+ toolName: string;
22
+ input?: unknown;
23
+ success: boolean;
24
+ output?: unknown;
25
+ error?: unknown;
26
+ duplicateSkipped?: boolean;
27
+ }
28
+ export declare function createSessionGoal(request: string, now?: number): SessionGoal;
29
+ export declare function observeGoalToolEvent(goal: SessionGoal, event: GoalToolEvent, now?: number): SessionGoal;
30
+ export declare function formatGoalStatus(goal: SessionGoal): string;
@@ -0,0 +1,88 @@
1
+ import { classifyRequestIntent } from './requestClassifier.js';
2
+ function shortRequest(value) {
3
+ return value.replace(/\s+/g, ' ').trim().slice(0, 160) || 'current request';
4
+ }
5
+ function inputPath(input) {
6
+ return typeof input === 'object' && input != null && 'path' in input && typeof input.path === 'string'
7
+ ? input.path
8
+ : undefined;
9
+ }
10
+ function bashCommand(input) {
11
+ return typeof input === 'object' && input != null && 'command' in input && typeof input.command === 'string'
12
+ ? input.command
13
+ : undefined;
14
+ }
15
+ function uniquePush(values, value) {
16
+ if (!values.includes(value))
17
+ values.push(value);
18
+ }
19
+ export function createSessionGoal(request, now = Date.now()) {
20
+ const intent = classifyRequestIntent(request);
21
+ const criteria = intent === 'plan'
22
+ ? ['Create or update the requested plan artifact/answer', 'Do not implement source changes unless asked']
23
+ : intent === 'test'
24
+ ? ['Run the requested validation or closest relevant check', 'Report pass/fail accurately']
25
+ : intent === 'review'
26
+ ? ['Inspect the relevant current project state', 'Return evidence-based findings with file paths']
27
+ : intent === 'answer'
28
+ ? ['Answer the user using current project context when needed']
29
+ : ['Inspect the relevant files', 'Make the requested change when needed', 'Validate the change when practical', 'Summarize only current-task changes and validation'];
30
+ return {
31
+ id: `goal-${now}-${Math.random().toString(36).slice(2)}`,
32
+ originalUserRequest: request,
33
+ normalizedIntent: intent,
34
+ successCriteria: criteria,
35
+ constraints: [],
36
+ touchedFiles: [],
37
+ validationCommands: [],
38
+ status: 'active',
39
+ phase: 'starting',
40
+ lastProgressAt: now,
41
+ };
42
+ }
43
+ export function observeGoalToolEvent(goal, event, now = Date.now()) {
44
+ if (event.duplicateSkipped)
45
+ return goal;
46
+ if (event.success && ['listFiles', 'readFile'].includes(event.toolName)) {
47
+ goal.phase = goal.phase === 'editing' || goal.phase === 'validating' ? goal.phase : 'inspecting';
48
+ goal.lastProgressAt = now;
49
+ }
50
+ if (['editFile', 'replaceLines', 'writeFile'].includes(event.toolName)) {
51
+ const path = inputPath(event.input);
52
+ if (path)
53
+ uniquePush(goal.touchedFiles, path);
54
+ if (event.success) {
55
+ goal.phase = 'editing';
56
+ goal.lastProgressAt = now;
57
+ }
58
+ else {
59
+ goal.blocker = `File edit failed${path ? ` for ${path}` : ''}; recovery read is required before retry.`;
60
+ }
61
+ }
62
+ if (event.toolName === 'bash') {
63
+ const command = bashCommand(event.input);
64
+ if (command) {
65
+ const ok = typeof event.output === 'object' && event.output != null && 'ok' in event.output ? Boolean(event.output.ok) : event.success;
66
+ const existing = goal.validationCommands.find(item => item.command === command);
67
+ const status = ok ? 'passed' : 'failed';
68
+ if (existing)
69
+ existing.status = status;
70
+ else
71
+ goal.validationCommands.push({ command, status });
72
+ goal.phase = 'validating';
73
+ goal.lastProgressAt = now;
74
+ if (!ok)
75
+ goal.blocker = `Validation command failed: ${command}`;
76
+ }
77
+ }
78
+ return goal;
79
+ }
80
+ export function formatGoalStatus(goal) {
81
+ const action = goal.phase === 'starting' ? 'starting'
82
+ : goal.phase === 'inspecting' ? 'inspecting'
83
+ : goal.phase === 'editing' ? `${goal.touchedFiles.length} file${goal.touchedFiles.length === 1 ? '' : 's'} changed`
84
+ : goal.phase === 'validating' ? `validation ${goal.validationCommands.at(-1)?.status ?? 'running'}`
85
+ : goal.phase === 'summarizing' ? 'summarizing'
86
+ : 'done';
87
+ return `Goal: ${shortRequest(goal.originalUserRequest)} · ${action}`;
88
+ }
@@ -0,0 +1,37 @@
1
+ import type { ModelMessage } from 'ai';
2
+ export type SessionEntry = {
3
+ type: 'header';
4
+ id: string;
5
+ cwd: string;
6
+ createdAt: string;
7
+ hazeVersion?: string;
8
+ } | {
9
+ type: 'ui_message';
10
+ at: string;
11
+ role: 'system' | 'user' | 'assistant' | 'tool';
12
+ text: string;
13
+ } | {
14
+ type: 'conversation_snapshot';
15
+ at: string;
16
+ messages: ModelMessage[];
17
+ } | {
18
+ type: 'event';
19
+ at: string;
20
+ name: string;
21
+ text?: string;
22
+ };
23
+ export interface HazeSession {
24
+ id: string;
25
+ file: string;
26
+ cwd: string;
27
+ }
28
+ export declare function createSession(options?: {
29
+ cwd?: string;
30
+ hazeVersion?: string;
31
+ sessionsDir?: string;
32
+ }): Promise<HazeSession>;
33
+ export declare function latestSession(cwd?: string, sessionsDir?: string): Promise<HazeSession | undefined>;
34
+ export declare function appendSessionEntry(session: HazeSession, entry: SessionEntry): Promise<void>;
35
+ export declare function readSessionEntries(session: HazeSession): Promise<SessionEntry[]>;
36
+ export declare function restoreConversation(session: HazeSession): Promise<ModelMessage[]>;
37
+ export declare function formatSession(session: HazeSession): string;
@@ -0,0 +1,59 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import fs from 'fs-extra';
4
+ import { HAZE_DIR } from '../../config/paths.js';
5
+ const DEFAULT_SESSIONS_DIR = path.join(HAZE_DIR, 'sessions');
6
+ function cwdHash(cwd = process.cwd()) {
7
+ return crypto.createHash('sha256').update(path.resolve(cwd)).digest('hex').slice(0, 16);
8
+ }
9
+ function sessionDir(cwd = process.cwd(), sessionsDir = DEFAULT_SESSIONS_DIR) {
10
+ return path.join(sessionsDir, cwdHash(cwd));
11
+ }
12
+ function sessionFile(id, cwd = process.cwd(), sessionsDir = DEFAULT_SESSIONS_DIR) {
13
+ return path.join(sessionDir(cwd, sessionsDir), `${id}.jsonl`);
14
+ }
15
+ function newSessionId(now = new Date()) {
16
+ return now.toISOString().replace(/[:.]/g, '-');
17
+ }
18
+ export async function createSession(options = {}) {
19
+ const cwd = path.resolve(options.cwd ?? process.cwd());
20
+ const id = newSessionId();
21
+ const file = sessionFile(id, cwd, options.sessionsDir);
22
+ await fs.ensureDir(path.dirname(file));
23
+ await appendSessionEntry({ id, file, cwd }, { type: 'header', id, cwd, createdAt: new Date().toISOString(), hazeVersion: options.hazeVersion });
24
+ return { id, file, cwd };
25
+ }
26
+ export async function latestSession(cwd = process.cwd(), sessionsDir = DEFAULT_SESSIONS_DIR) {
27
+ const dir = sessionDir(cwd, sessionsDir);
28
+ const files = (await fs.readdir(dir).catch(() => []))
29
+ .filter(file => file.endsWith('.jsonl'))
30
+ .sort();
31
+ const latest = files.at(-1);
32
+ if (!latest)
33
+ return undefined;
34
+ const id = path.basename(latest, '.jsonl');
35
+ return { id, file: path.join(dir, latest), cwd: path.resolve(cwd) };
36
+ }
37
+ export async function appendSessionEntry(session, entry) {
38
+ await fs.ensureDir(path.dirname(session.file));
39
+ await fs.appendFile(session.file, `${JSON.stringify(entry)}\n`, 'utf8');
40
+ }
41
+ export async function readSessionEntries(session) {
42
+ const raw = await fs.readFile(session.file, 'utf8');
43
+ return raw.split('\n').filter(Boolean).flatMap(line => {
44
+ try {
45
+ return [JSON.parse(line)];
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ });
51
+ }
52
+ export async function restoreConversation(session) {
53
+ const entries = await readSessionEntries(session);
54
+ const snapshots = entries.filter((entry) => entry.type === 'conversation_snapshot');
55
+ return snapshots.at(-1)?.messages ?? [];
56
+ }
57
+ export function formatSession(session) {
58
+ return `${session.id} · ${session.file}`;
59
+ }
@@ -1 +1 @@
1
- export declare function model(): Promise<import("@ai-sdk/provider").LanguageModelV3 | null>;
1
+ export declare function model(): Promise<import("@ai-sdk/provider").LanguageModelV3>;
@@ -1,11 +1,11 @@
1
1
  import { createOpenAI } from '@ai-sdk/openai';
2
2
  import { readSettings } from '../config/settings.js';
3
+ import { activeModel } from '../config/providers.js';
3
4
  export async function model() {
4
5
  const settings = await readSettings();
5
- const baseURL = process.env.OPENAI_BASE_URL ?? settings.baseURL;
6
- const apiKey = process.env.OPENAI_API_KEY ?? settings.apiKey;
7
- const name = process.env.HAZE_MODEL ?? settings.model ?? 'x-ai/grok-build-0.1';
8
- if (!apiKey)
9
- return null;
10
- return createOpenAI({ apiKey, baseURL })(name);
6
+ const selection = activeModel(settings);
7
+ const baseURL = process.env.OPENAI_BASE_URL ?? selection.provider.url;
8
+ const apiKey = process.env.OPENAI_API_KEY ?? selection.provider.key ?? settings.apiKey ?? 'not-needed';
9
+ const name = process.env.HAZE_MODEL ?? selection.model;
10
+ return createOpenAI({ apiKey, baseURL }).chat(name);
11
11
  }