@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/haze.js +2 -0
- package/dist/cli/commands/chat.d.ts +6 -0
- package/dist/cli/commands/chat.js +101 -0
- package/dist/cli/commands/commands.d.ts +15 -0
- package/dist/cli/commands/commands.js +46 -0
- package/dist/cli/commands/formatters.d.ts +8 -0
- package/dist/cli/commands/formatters.js +46 -0
- package/dist/cli/commands/skills.d.ts +4 -0
- package/dist/cli/commands/skills.js +35 -0
- package/dist/cli/commands/streaming.d.ts +19 -0
- package/dist/cli/commands/streaming.js +101 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +31 -0
- package/dist/config/contextFiles.d.ts +5 -0
- package/dist/config/contextFiles.js +59 -0
- package/dist/config/inputHistory.d.ts +4 -0
- package/dist/config/inputHistory.js +29 -0
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.js +5 -0
- package/dist/config/settings.d.ts +10 -0
- package/dist/config/settings.js +16 -0
- package/dist/llm/client.d.ts +1 -0
- package/dist/llm/client.js +11 -0
- package/dist/llm/hazeTools.d.ts +82 -0
- package/dist/llm/hazeTools.js +226 -0
- package/dist/llm/initPrompt.d.ts +1 -0
- package/dist/llm/initPrompt.js +19 -0
- package/dist/llm/systemPrompt.d.ts +2 -0
- package/dist/llm/systemPrompt.js +33 -0
- package/dist/skills/SkillLoader.d.ts +2 -0
- package/dist/skills/SkillLoader.js +22 -0
- package/dist/skills/SkillRegistry.d.ts +6 -0
- package/dist/skills/SkillRegistry.js +28 -0
- package/dist/skills/builder/SkillBuilder.d.ts +1 -0
- package/dist/skills/builder/SkillBuilder.js +25 -0
- package/dist/skills/installer/SkillInstaller.d.ts +1 -0
- package/dist/skills/installer/SkillInstaller.js +48 -0
- package/dist/skills/manifestSchema.d.ts +31 -0
- package/dist/skills/manifestSchema.js +23 -0
- package/dist/skills/types.d.ts +60 -0
- package/dist/skills/types.js +1 -0
- package/dist/tools/ToolExecutor.d.ts +3 -0
- package/dist/tools/ToolExecutor.js +15 -0
- package/dist/tools/types.d.ts +9 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/components/ErrorView.d.ts +3 -0
- package/dist/ui/components/ErrorView.js +7 -0
- package/dist/ui/components/Header.d.ts +3 -0
- package/dist/ui/components/Header.js +6 -0
- package/dist/ui/components/MarkdownText.d.ts +3 -0
- package/dist/ui/components/MarkdownText.js +108 -0
- package/dist/ui/components/TextInput.d.ts +9 -0
- package/dist/ui/components/TextInput.js +120 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +11 -0
- package/dist/utils/fs.d.ts +14 -0
- package/dist/utils/fs.js +36 -0
- package/dist/utils/path.d.ts +3 -0
- package/dist/utils/path.js +16 -0
- package/dist/utils/yaml.d.ts +2 -0
- package/dist/utils/yaml.js +8 -0
- package/examples/skills/files/prompts/file_tasks.md +1 -0
- package/examples/skills/files/skill.yaml +28 -0
- package/examples/skills/files/tools/list_files.ts +21 -0
- package/examples/skills/files/tools/read_file.ts +12 -0
- 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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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,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
|
+
}
|
package/dist/ui/theme.js
ADDED
|
@@ -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[]>;
|
package/dist/utils/fs.js
ADDED
|
@@ -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,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 @@
|
|
|
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
|
+
}
|