@denizokcu/haze 0.0.1

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 (69) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/bin/haze.js +2 -0
  5. package/dist/cli/commands/chat.d.ts +6 -0
  6. package/dist/cli/commands/chat.js +101 -0
  7. package/dist/cli/commands/commands.d.ts +15 -0
  8. package/dist/cli/commands/commands.js +46 -0
  9. package/dist/cli/commands/formatters.d.ts +8 -0
  10. package/dist/cli/commands/formatters.js +46 -0
  11. package/dist/cli/commands/skills.d.ts +4 -0
  12. package/dist/cli/commands/skills.js +35 -0
  13. package/dist/cli/commands/streaming.d.ts +19 -0
  14. package/dist/cli/commands/streaming.js +101 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +31 -0
  17. package/dist/config/contextFiles.d.ts +5 -0
  18. package/dist/config/contextFiles.js +59 -0
  19. package/dist/config/inputHistory.d.ts +4 -0
  20. package/dist/config/inputHistory.js +29 -0
  21. package/dist/config/paths.d.ts +3 -0
  22. package/dist/config/paths.js +5 -0
  23. package/dist/config/settings.d.ts +10 -0
  24. package/dist/config/settings.js +16 -0
  25. package/dist/llm/client.d.ts +1 -0
  26. package/dist/llm/client.js +11 -0
  27. package/dist/llm/hazeTools.d.ts +82 -0
  28. package/dist/llm/hazeTools.js +226 -0
  29. package/dist/llm/initPrompt.d.ts +1 -0
  30. package/dist/llm/initPrompt.js +19 -0
  31. package/dist/llm/systemPrompt.d.ts +2 -0
  32. package/dist/llm/systemPrompt.js +33 -0
  33. package/dist/skills/SkillLoader.d.ts +2 -0
  34. package/dist/skills/SkillLoader.js +22 -0
  35. package/dist/skills/SkillRegistry.d.ts +6 -0
  36. package/dist/skills/SkillRegistry.js +28 -0
  37. package/dist/skills/builder/SkillBuilder.d.ts +1 -0
  38. package/dist/skills/builder/SkillBuilder.js +25 -0
  39. package/dist/skills/installer/SkillInstaller.d.ts +1 -0
  40. package/dist/skills/installer/SkillInstaller.js +48 -0
  41. package/dist/skills/manifestSchema.d.ts +31 -0
  42. package/dist/skills/manifestSchema.js +23 -0
  43. package/dist/skills/types.d.ts +60 -0
  44. package/dist/skills/types.js +1 -0
  45. package/dist/tools/ToolExecutor.d.ts +3 -0
  46. package/dist/tools/ToolExecutor.js +15 -0
  47. package/dist/tools/types.d.ts +9 -0
  48. package/dist/tools/types.js +1 -0
  49. package/dist/ui/components/ErrorView.d.ts +3 -0
  50. package/dist/ui/components/ErrorView.js +7 -0
  51. package/dist/ui/components/Header.d.ts +3 -0
  52. package/dist/ui/components/Header.js +6 -0
  53. package/dist/ui/components/MarkdownText.d.ts +3 -0
  54. package/dist/ui/components/MarkdownText.js +108 -0
  55. package/dist/ui/components/TextInput.d.ts +9 -0
  56. package/dist/ui/components/TextInput.js +120 -0
  57. package/dist/ui/theme.d.ts +11 -0
  58. package/dist/ui/theme.js +11 -0
  59. package/dist/utils/fs.d.ts +14 -0
  60. package/dist/utils/fs.js +36 -0
  61. package/dist/utils/path.d.ts +3 -0
  62. package/dist/utils/path.js +16 -0
  63. package/dist/utils/yaml.d.ts +2 -0
  64. package/dist/utils/yaml.js +8 -0
  65. package/examples/skills/files/prompts/file_tasks.md +1 -0
  66. package/examples/skills/files/skill.yaml +28 -0
  67. package/examples/skills/files/tools/list_files.ts +21 -0
  68. package/examples/skills/files/tools/read_file.ts +12 -0
  69. package/package.json +71 -0
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,31 @@
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>;
@@ -0,0 +1,23 @@
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
+ });
@@ -0,0 +1,60 @@
1
+ export interface JsonSchema {
2
+ type?: string;
3
+ required?: string[];
4
+ properties?: Record<string, JsonSchema & {
5
+ description?: string;
6
+ }>;
7
+ items?: JsonSchema;
8
+ description?: string;
9
+ enum?: unknown[];
10
+ additionalProperties?: boolean | JsonSchema;
11
+ }
12
+ export interface SkillManifest {
13
+ name: string;
14
+ version: string;
15
+ description: string;
16
+ author?: string;
17
+ homepage?: string;
18
+ dependencies?: {
19
+ cli?: {
20
+ name: string;
21
+ description?: string;
22
+ required?: boolean;
23
+ }[];
24
+ env?: {
25
+ name: string;
26
+ description?: string;
27
+ required?: boolean;
28
+ }[];
29
+ };
30
+ tools?: SkillToolManifest[];
31
+ prompts?: SkillPromptManifest[];
32
+ }
33
+ export interface SkillToolManifest {
34
+ name: string;
35
+ description: string;
36
+ path: string;
37
+ input?: JsonSchema;
38
+ }
39
+ export interface SkillPromptManifest {
40
+ name: string;
41
+ description?: string;
42
+ path: string;
43
+ }
44
+ export interface LoadedSkill {
45
+ dir: string;
46
+ manifestPath: string;
47
+ manifest: SkillManifest;
48
+ prompts: LoadedPrompt[];
49
+ tools: LoadedTool[];
50
+ source: 'global' | 'local';
51
+ }
52
+ export interface LoadedPrompt extends SkillPromptManifest {
53
+ content: string;
54
+ absolutePath: string;
55
+ }
56
+ export interface LoadedTool extends SkillToolManifest {
57
+ id: string;
58
+ skillName: string;
59
+ absolutePath: string;
60
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
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>;
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function ErrorView({ error }: {
2
+ error: unknown;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { theme } from '../theme.js';
4
+ export function ErrorView({ error }) {
5
+ const message = error instanceof Error ? error.message : String(error);
6
+ return _jsxs(Text, { color: theme.danger, children: ["Error: ", message] });
7
+ }
@@ -0,0 +1,3 @@
1
+ export declare function Header({ subtitle }: {
2
+ subtitle?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { theme } from '../theme.js';
4
+ export function Header({ subtitle }) {
5
+ return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Haze" }), subtitle ? _jsx(Text, { color: theme.muted, children: subtitle }) : _jsx(Text, { color: theme.muted, children: "A small agent, because apparently that is allowed." })] });
6
+ }
@@ -0,0 +1,3 @@
1
+ export declare function MarkdownText({ content }: {
2
+ content: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import { marked } from 'marked';
4
+ import { highlight } from 'cli-highlight';
5
+ import stripAnsi from 'strip-ansi';
6
+ import { theme } from '../theme.js';
7
+ export function MarkdownText({ content }) {
8
+ const tokens = marked.lexer(content, { gfm: true, breaks: true });
9
+ const { stdout } = useStdout();
10
+ const width = Math.max(20, (stdout.columns ?? process.stdout.columns ?? 100) - 2);
11
+ return _jsx(Box, { flexDirection: "column", children: tokens.map((token, index) => _jsx(MarkdownBlock, { token: token, width: width }, index)) });
12
+ }
13
+ function MarkdownBlock({ token, width }) {
14
+ switch (token.type) {
15
+ case 'heading': {
16
+ const heading = token;
17
+ return _jsx(Box, { marginTop: heading.depth <= 2 ? 1 : 0, children: _jsxs(Text, { color: theme.purple, bold: true, children: ['#'.repeat(heading.depth), " ", _jsx(InlineMarkdown, { text: heading.text })] }) });
18
+ }
19
+ case 'paragraph': {
20
+ const paragraph = token;
21
+ return _jsx(Box, { marginBottom: 1, children: _jsx(InlineMarkdown, { text: paragraph.text }) });
22
+ }
23
+ case 'text': {
24
+ const text = token;
25
+ return _jsx(InlineMarkdown, { text: text.text });
26
+ }
27
+ case 'space':
28
+ return _jsx(Text, { children: " " });
29
+ case 'hr':
30
+ return _jsx(Text, { color: theme.deepPurple, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" });
31
+ case 'blockquote': {
32
+ const quote = token;
33
+ return _jsx(Box, { flexDirection: "column", marginY: 1, children: quote.text.split('\n').map((line, index) => _jsx(Text, { backgroundColor: theme.quoteBg, children: padAnsi(line || ' ', width) }, index)) });
34
+ }
35
+ case 'list': {
36
+ const list = token;
37
+ return _jsx(Box, { flexDirection: "column", marginBottom: 1, children: list.items.map((item, index) => _jsxs(Box, { children: [_jsx(Text, { color: theme.purple, children: list.ordered ? `${index + 1}. ` : '• ' }), _jsx(InlineMarkdown, { text: item.text.replace(/\n/g, ' ') })] }, index)) });
38
+ }
39
+ case 'code': {
40
+ const code = token;
41
+ return _jsx(CodeBlock, { code: code.text, language: code.lang, width: width });
42
+ }
43
+ case 'table': {
44
+ const table = token;
45
+ return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.violet, children: table.header.map(cell => stripInline(cell.text)).join(' | ') }), _jsx(Text, { color: theme.deepPurple, children: table.header.map(() => '---').join(' | ') }), table.rows.map((row, index) => _jsx(Text, { children: row.map(cell => stripInline(cell.text)).join(' | ') }, index))] });
46
+ }
47
+ default:
48
+ return _jsx(Text, { children: 'raw' in token ? String(token.raw) : '' });
49
+ }
50
+ }
51
+ function CodeBlock({ code, language, width }) {
52
+ let rendered;
53
+ try {
54
+ rendered = highlight(code, { language: language || undefined, ignoreIllegals: true });
55
+ }
56
+ catch {
57
+ rendered = code;
58
+ }
59
+ const lines = rendered.replace(/\n$/, '').split('\n');
60
+ return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [language ? _jsx(Text, { color: theme.muted, backgroundColor: theme.codeBg, children: padAnsi(language, width) }) : null, lines.map((line, index) => _jsx(Text, { backgroundColor: theme.codeBg, children: padAnsi(line || ' ', width) }, index))] });
61
+ }
62
+ function InlineMarkdown({ text }) {
63
+ const parts = tokenizeInline(text);
64
+ return _jsx(Text, { children: parts.map((part, index) => {
65
+ if (part.kind === 'code')
66
+ return _jsx(Text, { color: theme.warning, children: part.text }, index);
67
+ if (part.kind === 'strong')
68
+ return _jsx(Text, { bold: true, children: part.text }, index);
69
+ if (part.kind === 'em')
70
+ return _jsx(Text, { italic: true, children: part.text }, index);
71
+ if (part.kind === 'link')
72
+ return _jsx(Text, { color: theme.violet, children: part.text }, index);
73
+ return _jsx(Text, { children: part.text }, index);
74
+ }) });
75
+ }
76
+ function tokenizeInline(text) {
77
+ const out = [];
78
+ const regex = /`([^`]+)`|\*\*([^*]+)\*\*|\*([^*]+)\*|\[([^\]]+)\]\(([^)]+)\)/g;
79
+ let last = 0;
80
+ for (const match of text.matchAll(regex)) {
81
+ if (match.index > last)
82
+ out.push({ kind: 'text', text: text.slice(last, match.index) });
83
+ if (match[1])
84
+ out.push({ kind: 'code', text: match[1] });
85
+ else if (match[2])
86
+ out.push({ kind: 'strong', text: match[2] });
87
+ else if (match[3])
88
+ out.push({ kind: 'em', text: match[3] });
89
+ else if (match[4])
90
+ out.push({ kind: 'link', text: `${match[4]} (${match[5]})` });
91
+ last = match.index + match[0].length;
92
+ }
93
+ if (last < text.length)
94
+ out.push({ kind: 'text', text: text.slice(last) });
95
+ return out;
96
+ }
97
+ function stripInline(text) {
98
+ return text
99
+ .replace(/`([^`]+)`/g, '$1')
100
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
101
+ .replace(/\*([^*]+)\*/g, '$1')
102
+ .replace(/~~([^~]+)~~/g, '$1')
103
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
104
+ }
105
+ function padAnsi(value, width) {
106
+ const visible = stripAnsi(value).length;
107
+ return visible >= width ? value : value + ' '.repeat(width - visible);
108
+ }
@@ -0,0 +1,9 @@
1
+ export declare function TextInput({ placeholder, disabled, mask, historyItems, recordHistory, onHistoryAdd, onSubmit }: {
2
+ placeholder?: string;
3
+ disabled?: boolean;
4
+ mask?: boolean;
5
+ historyItems?: string[];
6
+ recordHistory?: boolean;
7
+ onHistoryAdd?: (value: string) => void;
8
+ onSubmit: (value: string) => void;
9
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Text, useInput } from 'ink';
4
+ import { theme } from '../theme.js';
5
+ export function TextInput({ placeholder, disabled, mask, historyItems = [], recordHistory = true, onHistoryAdd, onSubmit }) {
6
+ const [value, setValue] = useState('');
7
+ const [cursor, setCursor] = useState(0);
8
+ const history = useRef(historyItems);
9
+ const historyIndex = useRef(null);
10
+ const draft = useRef('');
11
+ useEffect(() => {
12
+ history.current = historyItems;
13
+ }, [historyItems]);
14
+ useEffect(() => {
15
+ if (!disabled) {
16
+ setValue('');
17
+ setCursor(0);
18
+ historyIndex.current = null;
19
+ draft.current = '';
20
+ }
21
+ }, [disabled]);
22
+ function setInput(next, nextCursor = next.length) {
23
+ setValue(next);
24
+ setCursor(Math.max(0, Math.min(nextCursor, next.length)));
25
+ }
26
+ function showHistory(index) {
27
+ historyIndex.current = index;
28
+ setInput(history.current[index] ?? '');
29
+ }
30
+ useInput((input, key) => {
31
+ if (disabled)
32
+ return;
33
+ if (key.escape) {
34
+ setInput('');
35
+ historyIndex.current = null;
36
+ draft.current = '';
37
+ return;
38
+ }
39
+ if (key.return) {
40
+ const submitted = value.trim();
41
+ setInput('');
42
+ historyIndex.current = null;
43
+ 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
+ }
52
+ return;
53
+ }
54
+ if (key.leftArrow) {
55
+ setCursor(current => Math.max(0, current - 1));
56
+ return;
57
+ }
58
+ if (key.rightArrow) {
59
+ setCursor(current => Math.min(value.length, current + 1));
60
+ return;
61
+ }
62
+ if (key.upArrow) {
63
+ if (history.current.length === 0)
64
+ return;
65
+ if (historyIndex.current === null) {
66
+ draft.current = value;
67
+ showHistory(history.current.length - 1);
68
+ }
69
+ else {
70
+ showHistory(Math.max(0, historyIndex.current - 1));
71
+ }
72
+ return;
73
+ }
74
+ if (key.downArrow) {
75
+ if (historyIndex.current === null)
76
+ return;
77
+ if (historyIndex.current < history.current.length - 1) {
78
+ showHistory(historyIndex.current + 1);
79
+ }
80
+ else {
81
+ historyIndex.current = null;
82
+ setInput(draft.current);
83
+ }
84
+ return;
85
+ }
86
+ if (key.backspace) {
87
+ if (cursor === 0)
88
+ return;
89
+ setInput(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
90
+ historyIndex.current = null;
91
+ return;
92
+ }
93
+ if (key.delete) {
94
+ if (cursor >= value.length)
95
+ return;
96
+ setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
97
+ historyIndex.current = null;
98
+ return;
99
+ }
100
+ if (key.ctrl && input === 'a') {
101
+ setCursor(0);
102
+ return;
103
+ }
104
+ if (key.ctrl && input === 'e') {
105
+ setCursor(value.length);
106
+ return;
107
+ }
108
+ if (key.ctrl && input === 'c')
109
+ return;
110
+ if (input) {
111
+ setInput(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
112
+ historyIndex.current = null;
113
+ }
114
+ });
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] })] });
120
+ }
@@ -0,0 +1,11 @@
1
+ export declare const theme: {
2
+ purple: string;
3
+ deepPurple: string;
4
+ violet: string;
5
+ muted: string;
6
+ danger: string;
7
+ success: string;
8
+ warning: string;
9
+ codeBg: string;
10
+ quoteBg: string;
11
+ };
@@ -0,0 +1,11 @@
1
+ export const theme = {
2
+ purple: '#a78bfa',
3
+ deepPurple: '#6d28d9',
4
+ violet: '#8b5cf6',
5
+ muted: '#9ca3af',
6
+ danger: '#fb7185',
7
+ success: '#34d399',
8
+ warning: '#fbbf24',
9
+ codeBg: '#1f1633',
10
+ quoteBg: '#171127'
11
+ };
@@ -0,0 +1,14 @@
1
+ export interface WalkEntry {
2
+ path: string;
3
+ absolutePath: string;
4
+ name: string;
5
+ isDirectory: boolean;
6
+ isFile: boolean;
7
+ }
8
+ export interface WalkOptions {
9
+ recursive?: boolean;
10
+ maxEntries?: number;
11
+ filter?: (entry: WalkEntry) => boolean | Promise<boolean>;
12
+ }
13
+ export declare function walkDir(root: string, options?: WalkOptions): Promise<WalkEntry[]>;
14
+ export declare function listFilesRecursive(root: string): Promise<string[]>;
@@ -0,0 +1,36 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ const SKIP_ENTRIES = new Set(['node_modules', '.git']);
4
+ export async function walkDir(root, options = {}) {
5
+ const { recursive = false, maxEntries = Infinity, filter } = options;
6
+ const result = [];
7
+ async function walk(dir) {
8
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
9
+ if (result.length >= maxEntries)
10
+ return;
11
+ if (SKIP_ENTRIES.has(entry.name))
12
+ continue;
13
+ const absolutePath = path.join(dir, entry.name);
14
+ const relativePath = path.relative(root, absolutePath);
15
+ const walkEntry = {
16
+ path: relativePath,
17
+ absolutePath,
18
+ name: entry.name,
19
+ isDirectory: entry.isDirectory(),
20
+ isFile: entry.isFile(),
21
+ };
22
+ if (filter && !await filter(walkEntry))
23
+ continue;
24
+ result.push(walkEntry);
25
+ if (entry.isDirectory() && recursive)
26
+ await walk(absolutePath);
27
+ }
28
+ }
29
+ if (await fs.pathExists(root))
30
+ await walk(root);
31
+ return result;
32
+ }
33
+ export async function listFilesRecursive(root) {
34
+ const entries = await walkDir(root, { recursive: true });
35
+ return entries.filter(e => e.isFile).map(e => e.path).sort();
36
+ }
@@ -0,0 +1,3 @@
1
+ export declare function workspaceRoot(): string;
2
+ export declare function resolveWorkspacePath(inputPath: string): string;
3
+ export declare function workspaceRelativePath(absolutePath: string): string;
@@ -0,0 +1,16 @@
1
+ import path from 'node:path';
2
+ export function workspaceRoot() {
3
+ return process.cwd();
4
+ }
5
+ export function resolveWorkspacePath(inputPath) {
6
+ const root = workspaceRoot();
7
+ const resolved = path.resolve(root, inputPath);
8
+ const relative = path.relative(root, resolved);
9
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
10
+ throw new Error(`Path is outside the workspace: ${inputPath}`);
11
+ }
12
+ return resolved;
13
+ }
14
+ export function workspaceRelativePath(absolutePath) {
15
+ return path.relative(workspaceRoot(), absolutePath) || '.';
16
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readYaml<T>(file: string): Promise<T>;
2
+ export declare function writeYaml(file: string, value: unknown): Promise<void>;
@@ -0,0 +1,8 @@
1
+ import fs from 'fs-extra';
2
+ import YAML from 'yaml';
3
+ export async function readYaml(file) {
4
+ return YAML.parse(await fs.readFile(file, 'utf8'));
5
+ }
6
+ export async function writeYaml(file, value) {
7
+ await fs.writeFile(file, YAML.stringify(value));
8
+ }
@@ -0,0 +1 @@
1
+ For file tasks, prefer listing files before reading unknown paths. Never ask to read secrets unless the user explicitly requests it.
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,12 @@
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
+ }