@hncode/hncode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +219 -0
- package/README.md +330 -0
- package/TODO.md +72 -0
- package/bin/hncode +9 -0
- package/bin/hncode.cmd +2 -0
- package/bin/hncode.ps1 +4 -0
- package/bin/install.js +96 -0
- package/bin/preinstall.js +42 -0
- package/package.json +26 -0
- package/src/agent.js +373 -0
- package/src/clipboard.js +114 -0
- package/src/colors.js +149 -0
- package/src/config.js +533 -0
- package/src/index.js +296 -0
- package/src/llm.js +449 -0
- package/src/plugin.js +191 -0
- package/src/session.js +73 -0
- package/src/term.js +292 -0
- package/src/toml.js +281 -0
- package/src/tools/bash.js +240 -0
- package/src/tools/edit.js +138 -0
- package/src/tools/fetch-url.js +108 -0
- package/src/tools/file-lines.js +36 -0
- package/src/tools/glob.js +47 -0
- package/src/tools/grep.js +152 -0
- package/src/tools/ignore.js +99 -0
- package/src/tools/index.js +61 -0
- package/src/tools/matchers.js +71 -0
- package/src/tools/read-media-file.js +54 -0
- package/src/tools/read.js +99 -0
- package/src/tools/tasks.js +126 -0
- package/src/tools/todo.js +33 -0
- package/src/tools/utils.js +71 -0
- package/src/tools/web-search.js +75 -0
- package/src/tools/write.js +29 -0
- package/src/tui.js +5503 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// TodoList tool — mirrors the hncode TodoList tool schema & behavior.
|
|
2
|
+
// Replaces the agent's todo list with the full list provided.
|
|
3
|
+
|
|
4
|
+
export const spec = {
|
|
5
|
+
name: 'TodoList',
|
|
6
|
+
description: 'Track multi-step work. Pass the COMPLETE list; it replaces the current one. Items: title + status (pending | in_progress | done).',
|
|
7
|
+
parameters: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
todos: {
|
|
11
|
+
type: 'array',
|
|
12
|
+
items: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
title: { type: 'string', description: 'Short, actionable task title.' },
|
|
16
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'done'], description: 'Current status.' },
|
|
17
|
+
},
|
|
18
|
+
required: ['title', 'status'],
|
|
19
|
+
},
|
|
20
|
+
description: 'The full todo list.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['todos'],
|
|
24
|
+
},
|
|
25
|
+
async execute(args, ctx) {
|
|
26
|
+
ctx.todoState = args.todos.map((t) => ({ title: String(t.title), status: t.status || 'pending' }));
|
|
27
|
+
return render(ctx.todoState);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function render(list) {
|
|
32
|
+
return 'Todo list:\n' + (list || []).map((t) => `- [${t.status}] ${t.title}`).join('\n');
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Shared helpers for the hncode toolbelt.
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
export const MAX_OUTPUT_BYTES = 128 * 1024; // cap tool results to avoid token blowup
|
|
8
|
+
|
|
9
|
+
export function truncateBuf(buf) {
|
|
10
|
+
if (Buffer.isBuffer(buf)) {
|
|
11
|
+
if (buf.length <= MAX_OUTPUT_BYTES) return buf.toString('utf8');
|
|
12
|
+
return buf.subarray(0, MAX_OUTPUT_BYTES).toString('utf8') + `\n...[truncated ${buf.length - MAX_OUTPUT_BYTES} bytes]`;
|
|
13
|
+
}
|
|
14
|
+
const s = String(buf);
|
|
15
|
+
if (s.length <= MAX_OUTPUT_BYTES) return s;
|
|
16
|
+
return s.slice(0, MAX_OUTPUT_BYTES) + `\n...[truncated ${s.length - MAX_OUTPUT_BYTES} chars]`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Expand ~ and map Git-Bash /c/x -> C:\x style paths, then resolve against cwd.
|
|
20
|
+
export function normalizeInput(p) {
|
|
21
|
+
let s = String(p);
|
|
22
|
+
if (s.startsWith('~')) s = path.join(os.homedir(), s.slice(1));
|
|
23
|
+
const m = s.match(/^\/([a-zA-Z])\/(.*)$/);
|
|
24
|
+
if (m) return path.join(`${m[1].toUpperCase()}:\\`, ...m[2].split('/'));
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolvePath(input, ctx) {
|
|
29
|
+
const raw = normalizeInput(input);
|
|
30
|
+
let p = path.resolve(ctx.cwd || ctx.workspace, raw);
|
|
31
|
+
try { p = fs.realpathSync(p); } catch {} // resolve symlinks if present
|
|
32
|
+
const ws = path.resolve(ctx.cwd || ctx.workspace);
|
|
33
|
+
if (!ctx.allowExternal) {
|
|
34
|
+
let rel = path.relative(ws, p);
|
|
35
|
+
if (rel === '') return p;
|
|
36
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
37
|
+
throw new Error(`Path outside workspace: ${input}. Run hncode from that directory or set HNCODE_ALLOW_EXTERNAL=1.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ensureDir(p) {
|
|
44
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read a file fully, returning its string. Throws if not a file.
|
|
48
|
+
export function readFileBytes(p) {
|
|
49
|
+
const st = fs.statSync(p);
|
|
50
|
+
if (!st.isFile() && !st.isFIFO()) throw new Error(`Not a file: ${p}`);
|
|
51
|
+
return fs.readFileSync(p);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Visible width ignoring ANSI escapes (approx).
|
|
55
|
+
export function visibleWidth(s) {
|
|
56
|
+
return (s || '').replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isProbablyTextFile(buf) {
|
|
60
|
+
// Treat as text if mostly printable ASCII/UTF-8 and few NULs.
|
|
61
|
+
if (buf.length === 0) return true;
|
|
62
|
+
let nulls = 0;
|
|
63
|
+
for (let i = 0; i < Math.min(buf.length, 8192); i++) if (buf[i] === 0) nulls++;
|
|
64
|
+
if (nulls > 0) return false;
|
|
65
|
+
let bad = 0;
|
|
66
|
+
for (let i = 0; i < Math.min(buf.length, 8192); i++) {
|
|
67
|
+
const b = buf[i];
|
|
68
|
+
if (b < 9 || (b > 13 && b < 32) || b > 254) bad++;
|
|
69
|
+
}
|
|
70
|
+
return bad / Math.min(buf.length, 8192) < 0.10;
|
|
71
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// WebSearch tool — search the web via Bing's HTML endpoint (no API key needed).
|
|
2
|
+
|
|
3
|
+
import { htmlToText } from './fetch-url.js';
|
|
4
|
+
|
|
5
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
|
|
6
|
+
|
|
7
|
+
function decodeEntities(s) {
|
|
8
|
+
return String(s || '')
|
|
9
|
+
.replace(/ /gi, ' ')
|
|
10
|
+
.replace(/&/gi, '&')
|
|
11
|
+
.replace(/</gi, '<')
|
|
12
|
+
.replace(/>/gi, '>')
|
|
13
|
+
.replace(/"/gi, '"')
|
|
14
|
+
.replace(/'|'/gi, "'")
|
|
15
|
+
.replace(/ /gi, ' ')
|
|
16
|
+
.replace(/ /gi, ' ')
|
|
17
|
+
.replace(/·/gi, '·')
|
|
18
|
+
.replace(/…/gi, '…')
|
|
19
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCharCode(Number(d)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Parse Bing's HTML into { title, url, snippet } results.
|
|
23
|
+
export function parseBingResults(html, limit = 8) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const blocks = String(html).match(/<li class="b_algo"[\s\S]*?<\/li>/g) || [];
|
|
26
|
+
for (const block of blocks) {
|
|
27
|
+
const href = (block.match(/<a[^>]+href="(https?:\/\/[^"]+)"/) || [])[1];
|
|
28
|
+
if (!href) continue;
|
|
29
|
+
const titleHtml = (block.match(/<h2[^>]*>[\s\S]*?<a[^>]*>([\s\S]*?)<\/a>/) || [])[1] || '';
|
|
30
|
+
const title = decodeEntities(titleHtml.replace(/<[^>]+>/g, '')).trim();
|
|
31
|
+
// Snippet lives in <p> or a caption div; fall back to stripped block text.
|
|
32
|
+
let snippet = (block.match(/<p[^>]*>([\s\S]*?)<\/p>/) || [])[1]
|
|
33
|
+
|| (block.match(/class="b_caption"[^>]*>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/) || [])[1]
|
|
34
|
+
|| '';
|
|
35
|
+
snippet = decodeEntities(snippet.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
|
|
36
|
+
if (!snippet) snippet = htmlToText(block).replace(/\s+/g, ' ').slice(0, 240);
|
|
37
|
+
out.push({ title: title || href, url: href, snippet });
|
|
38
|
+
if (out.length >= limit) break;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const spec = {
|
|
44
|
+
name: 'WebSearch',
|
|
45
|
+
description: 'Search the web. Results are title/URL/snippet; snippets are summaries, so fetch a promising URL with FetchURL. Cite sources.',
|
|
46
|
+
parameters: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
query: { type: 'string', description: 'Search query.' },
|
|
50
|
+
},
|
|
51
|
+
required: ['query'],
|
|
52
|
+
},
|
|
53
|
+
async execute(args, ctx) {
|
|
54
|
+
const query = String(args.query || '').trim();
|
|
55
|
+
if (!query) return 'Error: query must not be empty.';
|
|
56
|
+
const signal = (ctx && ctx.signal) || undefined;
|
|
57
|
+
const url = 'https://www.bing.com/search?q=' + encodeURIComponent(query) + '&setlang=en';
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
headers: { 'User-Agent': UA, 'Accept-Language': 'en-US,en;q=0.9' },
|
|
61
|
+
signal,
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) return `Error: search failed (${res.status} ${res.statusText}).`;
|
|
64
|
+
const html = await res.text();
|
|
65
|
+
const results = parseBingResults(html, 8);
|
|
66
|
+
if (!results.length) return `No search results found for: ${query}`;
|
|
67
|
+
return results
|
|
68
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`)
|
|
69
|
+
.join('\n\n');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
const msg = e && e.name === 'AbortError' ? 'interrupted' : (e.message || String(e));
|
|
72
|
+
return `Error searching "${query}": ${msg}`;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Write tool — mirrors the hncode Write tool schema & behavior.
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { resolvePath, ensureDir, truncateBuf } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export const spec = {
|
|
7
|
+
name: 'Write',
|
|
8
|
+
description: 'Create or overwrite a file (prefer Edit for existing files). mode: overwrite (default) | append.',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
path: { type: 'string', description: 'Path to the file.' },
|
|
13
|
+
content: { type: 'string', description: 'Content to write.' },
|
|
14
|
+
mode: { type: 'string', enum: ['overwrite', 'append'], default: 'overwrite', description: 'Overwrite or append.' },
|
|
15
|
+
},
|
|
16
|
+
required: ['path', 'content'],
|
|
17
|
+
},
|
|
18
|
+
async execute(args, ctx) {
|
|
19
|
+
let p;
|
|
20
|
+
try { p = resolvePath(args.path, ctx); } catch (e) { return e.message; }
|
|
21
|
+
try {
|
|
22
|
+
ensureDir(p);
|
|
23
|
+
const flag = args.mode === 'append' ? 'a' : 'w';
|
|
24
|
+
fs.writeFileSync(p, args.content, { flag });
|
|
25
|
+
} catch (e) { return `Error writing ${args.path}: ${e.message}`; }
|
|
26
|
+
const size = fs.statSync(p).size;
|
|
27
|
+
return `File written: ${args.path} (${size} bytes)`;
|
|
28
|
+
},
|
|
29
|
+
};
|