@denizokcu/haze 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +169 -70
  3. package/dist/cli/commands/chat.d.ts +4 -1
  4. package/dist/cli/commands/chat.js +606 -24
  5. package/dist/cli/commands/commands.d.ts +5 -0
  6. package/dist/cli/commands/commands.js +220 -11
  7. package/dist/cli/commands/formatters.d.ts +1 -0
  8. package/dist/cli/commands/formatters.js +23 -3
  9. package/dist/cli/commands/skills.d.ts +1 -1
  10. package/dist/cli/commands/skills.js +8 -5
  11. package/dist/cli/commands/streaming.d.ts +7 -1
  12. package/dist/cli/commands/streaming.js +533 -41
  13. package/dist/cli/index.js +5 -12
  14. package/dist/config/inputHistory.js +8 -0
  15. package/dist/config/paths.d.ts +0 -1
  16. package/dist/config/paths.js +0 -1
  17. package/dist/config/providers.d.ts +26 -0
  18. package/dist/config/providers.js +88 -0
  19. package/dist/config/settings.d.ts +9 -2
  20. package/dist/core/agent/compaction.d.ts +13 -0
  21. package/dist/core/agent/compaction.js +34 -0
  22. package/dist/core/agent/errors.d.ts +3 -0
  23. package/dist/core/agent/errors.js +13 -0
  24. package/dist/core/agent/events.d.ts +58 -0
  25. package/dist/core/agent/events.js +3 -0
  26. package/dist/core/goal/completionPolicy.d.ts +27 -0
  27. package/dist/core/goal/completionPolicy.js +67 -0
  28. package/dist/core/goal/requestClassifier.d.ts +6 -0
  29. package/dist/core/goal/requestClassifier.js +31 -0
  30. package/dist/core/goal/sessionGoal.d.ts +30 -0
  31. package/dist/core/goal/sessionGoal.js +88 -0
  32. package/dist/core/session/sessionStore.d.ts +37 -0
  33. package/dist/core/session/sessionStore.js +59 -0
  34. package/dist/llm/client.d.ts +1 -1
  35. package/dist/llm/client.js +6 -6
  36. package/dist/llm/hazeTools.d.ts +70 -0
  37. package/dist/llm/hazeTools.js +311 -97
  38. package/dist/llm/initPrompt.js +7 -5
  39. package/dist/llm/systemPrompt.js +25 -11
  40. package/dist/skills/SkillLoader.d.ts +12 -2
  41. package/dist/skills/SkillLoader.js +64 -18
  42. package/dist/skills/SkillRegistry.d.ts +1 -5
  43. package/dist/skills/SkillRegistry.js +10 -21
  44. package/dist/skills/builder/SkillBuilder.d.ts +31 -1
  45. package/dist/skills/builder/SkillBuilder.js +291 -20
  46. package/dist/skills/skillTools.d.ts +20 -0
  47. package/dist/skills/skillTools.js +25 -0
  48. package/dist/skills/types.d.ts +12 -51
  49. package/dist/ui/components/ErrorView.d.ts +2 -1
  50. package/dist/ui/components/Header.d.ts +4 -2
  51. package/dist/ui/components/Header.js +2 -2
  52. package/dist/ui/components/MarkdownText.d.ts +2 -1
  53. package/dist/ui/components/TextInput.d.ts +13 -2
  54. package/dist/ui/components/TextInput.js +125 -25
  55. package/dist/ui/theme.d.ts +2 -0
  56. package/dist/ui/theme.js +3 -1
  57. package/dist/utils/fs.d.ts +1 -0
  58. package/dist/utils/fs.js +10 -6
  59. package/examples/skills/files/SKILL.md +16 -0
  60. package/examples/skills/files/examples/file-editing.md +3 -0
  61. package/package.json +9 -9
  62. package/dist/skills/installer/SkillInstaller.d.ts +0 -1
  63. package/dist/skills/installer/SkillInstaller.js +0 -48
  64. package/dist/skills/manifestSchema.d.ts +0 -31
  65. package/dist/skills/manifestSchema.js +0 -23
  66. package/dist/tools/ToolExecutor.d.ts +0 -3
  67. package/dist/tools/ToolExecutor.js +0 -15
  68. package/dist/tools/types.d.ts +0 -9
  69. package/dist/tools/types.js +0 -1
  70. package/examples/skills/files/prompts/file_tasks.md +0 -1
  71. package/examples/skills/files/skill.yaml +0 -28
  72. package/examples/skills/files/tools/list_files.ts +0 -21
  73. package/examples/skills/files/tools/read_file.ts +0 -12
@@ -1,9 +1,20 @@
1
- export declare function TextInput({ placeholder, disabled, mask, historyItems, recordHistory, onHistoryAdd, onSubmit }: {
1
+ import React from 'react';
2
+ export type TextInputSuggestion = {
3
+ value: string;
4
+ description?: string;
5
+ kind?: 'command' | 'skill' | 'provider' | 'model';
6
+ };
7
+ export declare function TextInput({ placeholder, disabled, mask, historyItems, recordHistory, suggestions, suggestionMode, submitOnEmpty, onHistoryAdd, onCancel, onEscape, onSubmit }: {
2
8
  placeholder?: string;
3
9
  disabled?: boolean;
4
10
  mask?: boolean;
5
11
  historyItems?: string[];
6
12
  recordHistory?: boolean;
13
+ suggestions?: TextInputSuggestion[];
14
+ suggestionMode?: 'slash' | 'always';
15
+ submitOnEmpty?: boolean;
7
16
  onHistoryAdd?: (value: string) => void;
17
+ onCancel?: () => void;
18
+ onEscape?: () => void;
8
19
  onSubmit: (value: string) => void;
9
- }): import("react/jsx-runtime").JSX.Element;
20
+ }): React.JSX.Element;
@@ -1,13 +1,64 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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
+ const COMPACT_PASTE_MIN_LINES = 4;
6
+ function normalizeLineEndings(text) {
7
+ return text.replace(/\r\n|\r/g, '\n');
8
+ }
9
+ function lineCount(text) {
10
+ return normalizeLineEndings(text).split('\n').length;
11
+ }
12
+ function pastePlaceholder(block) {
13
+ return `[paste #${block.id} +${block.lineCount} lines]`;
14
+ }
15
+ function updatePasteBlocksForReplacement(blocks, start, end, insertedLength) {
16
+ const delta = insertedLength - (end - start);
17
+ return blocks.flatMap(block => {
18
+ const replacesInsideBlock = start < block.end && end > block.start;
19
+ const insertsInsideBlock = start === end && start > block.start && start < block.end;
20
+ if (replacesInsideBlock || insertsInsideBlock)
21
+ return [];
22
+ if (block.start >= end)
23
+ return [{ ...block, start: block.start + delta, end: block.end + delta }];
24
+ return [block];
25
+ });
26
+ }
27
+ function displayCursorForValueCursor(blocks, valueCursor) {
28
+ let displayCursor = valueCursor;
29
+ for (const block of [...blocks].sort((a, b) => a.start - b.start)) {
30
+ const placeholderLength = pastePlaceholder(block).length;
31
+ const compactedLength = block.end - block.start - placeholderLength;
32
+ if (valueCursor <= block.start)
33
+ break;
34
+ if (valueCursor < block.end)
35
+ return block.start + placeholderLength;
36
+ displayCursor -= compactedLength;
37
+ }
38
+ return displayCursor;
39
+ }
40
+ function compactPasteBlocksForDisplay(value, blocks) {
41
+ if (blocks.length === 0)
42
+ return value;
43
+ let displayValue = '';
44
+ let offset = 0;
45
+ for (const block of [...blocks].sort((a, b) => a.start - b.start)) {
46
+ displayValue += value.slice(offset, block.start);
47
+ displayValue += pastePlaceholder(block);
48
+ offset = block.end;
49
+ }
50
+ displayValue += value.slice(offset);
51
+ return displayValue;
52
+ }
53
+ export function TextInput({ placeholder, disabled, mask, historyItems = [], recordHistory = true, suggestions = [], suggestionMode = 'slash', submitOnEmpty = false, onHistoryAdd, onCancel, onEscape, onSubmit }) {
6
54
  const [value, setValue] = useState('');
7
55
  const [cursor, setCursor] = useState(0);
56
+ const [pasteBlocks, setPasteBlocks] = useState([]);
57
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0);
8
58
  const history = useRef(historyItems);
9
59
  const historyIndex = useRef(null);
10
60
  const draft = useRef('');
61
+ const nextPasteId = useRef(1);
11
62
  useEffect(() => {
12
63
  history.current = historyItems;
13
64
  }, [historyItems]);
@@ -15,40 +66,83 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
15
66
  if (!disabled) {
16
67
  setValue('');
17
68
  setCursor(0);
69
+ setPasteBlocks([]);
70
+ setSelectedSuggestionIndex(0);
18
71
  historyIndex.current = null;
19
72
  draft.current = '';
73
+ nextPasteId.current = 1;
20
74
  }
21
75
  }, [disabled]);
22
- function setInput(next, nextCursor = next.length) {
76
+ function setInput(next, nextCursor = next.length, nextPasteBlocks = []) {
23
77
  setValue(next);
24
78
  setCursor(Math.max(0, Math.min(nextCursor, next.length)));
79
+ setPasteBlocks(nextPasteBlocks);
80
+ setSelectedSuggestionIndex(0);
81
+ }
82
+ function replaceInput(start, end, inserted) {
83
+ const normalizedInserted = normalizeLineEndings(inserted);
84
+ const next = value.slice(0, start) + normalizedInserted + value.slice(end);
85
+ const insertedLineCount = lineCount(normalizedInserted);
86
+ const updatedPasteBlocks = updatePasteBlocksForReplacement(pasteBlocks, start, end, normalizedInserted.length);
87
+ const insertedPasteBlock = !mask && insertedLineCount >= COMPACT_PASTE_MIN_LINES
88
+ ? [{ id: nextPasteId.current++, start, end: start + normalizedInserted.length, lineCount: insertedLineCount }]
89
+ : [];
90
+ setInput(next, start + normalizedInserted.length, [...updatedPasteBlocks, ...insertedPasteBlock]);
91
+ historyIndex.current = null;
25
92
  }
26
93
  function showHistory(index) {
27
94
  historyIndex.current = index;
28
95
  setInput(history.current[index] ?? '');
29
96
  }
97
+ const suggestionQuery = !mask && (suggestionMode === 'always' || value.startsWith('/'))
98
+ ? (suggestionMode === 'always' ? value : value.slice(1)).toLowerCase()
99
+ : undefined;
100
+ const filteredSuggestions = suggestionQuery == null ? [] : suggestions
101
+ .filter(suggestion => {
102
+ const suggestionValue = suggestionMode === 'always' ? suggestion.value : suggestion.value.slice(1);
103
+ return suggestionValue.toLowerCase().includes(suggestionQuery) || suggestion.description?.toLowerCase().includes(suggestionQuery);
104
+ })
105
+ .slice(0, 8);
106
+ const activeSuggestionIndex = Math.min(selectedSuggestionIndex, Math.max(0, filteredSuggestions.length - 1));
107
+ const activeSuggestion = filteredSuggestions[activeSuggestionIndex];
108
+ function submitValue(submitted, historyValue = submitted) {
109
+ if (recordHistory && historyValue) {
110
+ if (history.current[history.current.length - 1] !== historyValue)
111
+ history.current = [...history.current, historyValue];
112
+ onHistoryAdd?.(historyValue);
113
+ }
114
+ onSubmit(submitted);
115
+ }
30
116
  useInput((input, key) => {
31
- if (disabled)
117
+ if (disabled) {
118
+ if (key.escape)
119
+ onCancel?.();
32
120
  return;
121
+ }
33
122
  if (key.escape) {
34
123
  setInput('');
35
124
  historyIndex.current = null;
36
125
  draft.current = '';
126
+ nextPasteId.current = 1;
127
+ onEscape?.();
128
+ return;
129
+ }
130
+ if (key.tab && activeSuggestion) {
131
+ setInput(activeSuggestion.value);
132
+ historyIndex.current = null;
37
133
  return;
38
134
  }
39
135
  if (key.return) {
40
- const submitted = value.trim();
136
+ const shouldUseSuggestion = activeSuggestion && activeSuggestion.value !== value.trim() && (suggestionMode === 'always' || value.startsWith('/'));
137
+ const submitted = shouldUseSuggestion ? activeSuggestion.value : value.trim();
138
+ const submittedSuggestion = activeSuggestion?.value === submitted ? activeSuggestion : undefined;
139
+ const historyValue = submittedSuggestion && submittedSuggestion.kind !== 'command' ? '' : submitted;
41
140
  setInput('');
42
141
  historyIndex.current = null;
43
142
  draft.current = '';
44
- if (submitted) {
45
- if (recordHistory) {
46
- if (history.current[history.current.length - 1] !== submitted)
47
- history.current = [...history.current, submitted];
48
- onHistoryAdd?.(submitted);
49
- }
50
- onSubmit(submitted);
51
- }
143
+ nextPasteId.current = 1;
144
+ if (submitted || submitOnEmpty)
145
+ submitValue(submitted, historyValue);
52
146
  return;
53
147
  }
54
148
  if (key.leftArrow) {
@@ -60,6 +154,10 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
60
154
  return;
61
155
  }
62
156
  if (key.upArrow) {
157
+ if (filteredSuggestions.length > 0 && activeSuggestionIndex > 0) {
158
+ setSelectedSuggestionIndex(current => Math.max(0, current - 1));
159
+ return;
160
+ }
63
161
  if (history.current.length === 0)
64
162
  return;
65
163
  if (historyIndex.current === null) {
@@ -72,6 +170,10 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
72
170
  return;
73
171
  }
74
172
  if (key.downArrow) {
173
+ if (filteredSuggestions.length > 0 && activeSuggestionIndex < filteredSuggestions.length - 1) {
174
+ setSelectedSuggestionIndex(current => Math.min(filteredSuggestions.length - 1, current + 1));
175
+ return;
176
+ }
75
177
  if (historyIndex.current === null)
76
178
  return;
77
179
  if (historyIndex.current < history.current.length - 1) {
@@ -86,15 +188,13 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
86
188
  if (key.backspace) {
87
189
  if (cursor === 0)
88
190
  return;
89
- setInput(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
90
- historyIndex.current = null;
191
+ replaceInput(cursor - 1, cursor, '');
91
192
  return;
92
193
  }
93
194
  if (key.delete) {
94
195
  if (cursor >= value.length)
95
196
  return;
96
- setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
97
- historyIndex.current = null;
197
+ replaceInput(cursor, cursor + 1, '');
98
198
  return;
99
199
  }
100
200
  if (key.ctrl && input === 'a') {
@@ -108,13 +208,13 @@ export function TextInput({ placeholder, disabled, mask, historyItems = [], reco
108
208
  if (key.ctrl && input === 'c')
109
209
  return;
110
210
  if (input) {
111
- setInput(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
112
- historyIndex.current = null;
211
+ replaceInput(cursor, cursor, input);
113
212
  }
114
213
  });
115
- const displayValue = mask ? '•'.repeat(value.length) : value;
116
- const beforeCursor = displayValue.slice(0, cursor);
117
- const cursorChar = displayValue[cursor] ?? ' ';
118
- 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] })] });
214
+ const displayValue = mask ? '•'.repeat(value.length) : compactPasteBlocksForDisplay(value, pasteBlocks);
215
+ const displayCursor = mask ? cursor : displayCursorForValueCursor(pasteBlocks, cursor);
216
+ const beforeCursor = displayValue.slice(0, displayCursor);
217
+ const cursorChar = displayValue[displayCursor] ?? ' ';
218
+ const afterCursor = displayValue.slice(displayCursor + 1);
219
+ 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 === activeSuggestionIndex ? theme.success : theme.muted, wrap: "truncate-end", children: [index === activeSuggestionIndex ? '› ' : ' ', suggestion.value, _jsxs(Text, { color: theme.muted, children: [" ", suggestion.kind ?? '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
220
  }
@@ -2,10 +2,12 @@ export declare const theme: {
2
2
  purple: string;
3
3
  deepPurple: string;
4
4
  violet: string;
5
+ blue: string;
5
6
  muted: string;
6
7
  danger: string;
7
8
  success: string;
8
9
  warning: string;
10
+ orange: string;
9
11
  codeBg: string;
10
12
  quoteBg: string;
11
13
  };
package/dist/ui/theme.js CHANGED
@@ -2,10 +2,12 @@ export const theme = {
2
2
  purple: '#a78bfa',
3
3
  deepPurple: '#6d28d9',
4
4
  violet: '#8b5cf6',
5
+ blue: '#60a5fa',
5
6
  muted: '#9ca3af',
6
7
  danger: '#fb7185',
7
- success: '#34d399',
8
+ success: '#39ff14',
8
9
  warning: '#fbbf24',
10
+ orange: '#f59e0b',
9
11
  codeBg: '#1f1633',
10
12
  quoteBg: '#171127'
11
13
  };
@@ -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
- for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
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
- if (filter && !await filter(walkEntry))
23
- continue;
24
- result.push(walkEntry);
25
- if (entry.isDirectory() && recursive)
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
@@ -0,0 +1,3 @@
1
+ # File editing example
2
+
3
+ When changing existing files, inspect the current contents first, make the smallest targeted edit, then run the relevant validation command when practical.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denizokcu/haze",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
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": "./bin/haze.js"
22
+ "haze": "bin/haze.js"
23
23
  },
24
24
  "files": [
25
25
  "bin",
@@ -44,15 +44,15 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@ai-sdk/openai": "3.0.67",
47
- "@inquirer/prompts": "8.5.1",
48
- "ai": "6.0.193",
47
+ "@inquirer/prompts": "8.5.2",
48
+ "ai": "6.0.194",
49
49
  "cli-highlight": "2.1.11",
50
50
  "commander": "15.0.0",
51
51
  "fs-extra": "11.3.5",
52
52
  "ink": "7.0.5",
53
53
  "ink-spinner": "5.0.0",
54
54
  "marked": "18.0.4",
55
- "react": "19.2.6",
55
+ "react": "19.2.7",
56
56
  "strip-ansi": "7.2.0",
57
57
  "yaml": "2.9.0",
58
58
  "zod": "4.4.3"
@@ -61,11 +61,11 @@
61
61
  "@eslint/js": "^10.0.1",
62
62
  "@types/fs-extra": "11.0.4",
63
63
  "@types/node": "25.9.1",
64
- "@types/react": "19.2.15",
64
+ "@types/react": "19.2.16",
65
65
  "eslint": "^10.4.1",
66
- "tsx": "4.22.3",
66
+ "tsx": "4.22.4",
67
67
  "typescript": "6.0.3",
68
- "typescript-eslint": "^8.60.0",
69
- "vitest": "^4.1.7"
68
+ "typescript-eslint": "^8.60.1",
69
+ "vitest": "^4.1.8"
70
70
  }
71
71
  }
@@ -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
- });
@@ -1,3 +0,0 @@
1
- import type { LoadedSkill, LoadedTool } from '../skills/types.js';
2
- import type { ToolResult } from './types.js';
3
- export declare function executeTool(tool: LoadedTool, skill: LoadedSkill, input: Record<string, unknown>): Promise<ToolResult>;
@@ -1,15 +0,0 @@
1
- import { pathToFileURL } from 'node:url';
2
- export async function executeTool(tool, skill, input) {
3
- try {
4
- const context = { cwd: process.cwd(), skillDir: skill.dir };
5
- const mod = await import(`${pathToFileURL(tool.absolutePath).href}?t=${Date.now()}`);
6
- if (typeof mod.execute !== 'function') {
7
- return { ok: false, message: 'Tool must export execute(input, context)' };
8
- }
9
- const result = await mod.execute(input ?? {}, context);
10
- return result ?? { ok: true };
11
- }
12
- catch (error) {
13
- return { ok: false, message: error instanceof Error ? error.message : String(error) };
14
- }
15
- }
@@ -1,9 +0,0 @@
1
- export interface ToolContext {
2
- cwd: string;
3
- skillDir: string;
4
- }
5
- export interface ToolResult {
6
- ok: boolean;
7
- message?: string;
8
- data?: unknown;
9
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- For file tasks, prefer listing files before reading unknown paths. Never ask to read secrets unless the user explicitly requests it.
@@ -1,28 +0,0 @@
1
- name: files
2
- version: 0.1.0
3
- description: Safe-ish file inspection tools for the current project.
4
- dependencies: {}
5
- tools:
6
- - name: list_files
7
- description: List files under a directory in the current project. Skips .git and node_modules.
8
- path: tools/list_files.ts
9
- input:
10
- type: object
11
- properties:
12
- dir:
13
- type: string
14
- description: Directory to list, relative to the current working directory.
15
- - name: read_file
16
- description: Read a UTF-8 text file from the current project.
17
- path: tools/read_file.ts
18
- input:
19
- type: object
20
- required: [path]
21
- properties:
22
- path:
23
- type: string
24
- description: File path relative to the current working directory.
25
- prompts:
26
- - name: file_tasks
27
- description: Guidance for file inspection tasks.
28
- path: prompts/file_tasks.md
@@ -1,21 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- export async function execute(input: {dir?: string}, context: {cwd: string}) {
5
- const root = path.resolve(context.cwd, input.dir ?? '.');
6
- if (!root.startsWith(context.cwd)) return {ok: false, message: 'Refusing to list outside the current project.'};
7
- const files: string[] = [];
8
- async function walk(dir: string) {
9
- for (const entry of await fs.readdir(dir)) {
10
- if (entry === '.git' || entry === 'node_modules' || entry === 'dist') continue;
11
- const full = path.join(dir, entry);
12
- const rel = path.relative(context.cwd, full);
13
- const stat = await fs.stat(full);
14
- if (stat.isDirectory()) await walk(full);
15
- else files.push(rel);
16
- if (files.length >= 500) return;
17
- }
18
- }
19
- await walk(root);
20
- return {ok: true, message: `Found ${files.length} files.`, data: files.sort()};
21
- }
@@ -1,12 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- export async function execute(input: {path: string}, context: {cwd: string}) {
5
- if (!input.path) return {ok: false, message: 'Missing path.'};
6
- const full = path.resolve(context.cwd, input.path);
7
- if (!full.startsWith(context.cwd)) return {ok: false, message: 'Refusing to read outside the current project.'};
8
- const stat = await fs.stat(full);
9
- if (!stat.isFile()) return {ok: false, message: 'Path is not a file.'};
10
- if (stat.size > 200_000) return {ok: false, message: 'File is too large for the intentionally tiny attention span.'};
11
- return {ok: true, message: `Read ${input.path}.`, data: await fs.readFile(full, 'utf8')};
12
- }