@hncode/hncode 0.5.6 → 0.6.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/package.json +2 -3
- package/src/agent.js +67 -2
- package/src/clipboard.js +221 -33
- package/src/image-row.js +111 -0
- package/src/latex.js +1557 -0
- package/src/llm.js +19 -4
- package/src/mermaid/ansi.js +55 -0
- package/src/mermaid/canvas.js +449 -0
- package/src/mermaid/class-style.js +57 -0
- package/src/mermaid/css-colors.js +153 -0
- package/src/mermaid/diagrams/class.js +289 -0
- package/src/mermaid/diagrams/er.js +201 -0
- package/src/mermaid/diagrams/flowchart.js +488 -0
- package/src/mermaid/diagrams/gitgraph.js +205 -0
- package/src/mermaid/diagrams/mindmap.js +101 -0
- package/src/mermaid/diagrams/pie.js +104 -0
- package/src/mermaid/diagrams/sequence.js +282 -0
- package/src/mermaid/diagrams/state.js +292 -0
- package/src/mermaid/diagrams/timeline.js +99 -0
- package/src/mermaid/graph.js +138 -0
- package/src/mermaid/index.js +59 -0
- package/src/mermaid/labels.js +339 -0
- package/src/mermaid/layout-seq.js +225 -0
- package/src/mermaid/layout.js +1454 -0
- package/src/mermaid/registry.js +54 -0
- package/src/mermaid/source-box.js +102 -0
- package/src/mermaid/statements.js +235 -0
- package/src/mermaid/types.js +1 -0
- package/src/mermaid/width.js +29 -0
- package/src/screen-diff.js +512 -0
- package/src/screen-paint.js +128 -0
- package/src/screen.js +277 -0
- package/src/subagent-types.js +136 -147
- package/src/term-caps.js +10 -4
- package/src/tex/font.js +292 -0
- package/src/tex/fonts/LICENSE.txt +11 -0
- package/src/tex/fonts/STIX2Math.ttf +0 -0
- package/src/tex/index.js +293 -0
- package/src/tex/kitty.js +186 -0
- package/src/tex/layout.js +446 -0
- package/src/tex/parse.js +331 -0
- package/src/tex/raster.js +216 -0
- package/src/tex/render.js +167 -0
- package/src/tex/sixel.js +326 -0
- package/src/tex/svg.js +237 -0
- package/src/tool-result.js +2 -2
- package/src/tools/agent-swarm.js +3 -15
- package/src/tools/agent.js +454 -464
- package/src/tools/ask-user-question.js +195 -206
- package/src/tools/bash.js +39 -3
- package/src/tools/edit.js +10 -112
- package/src/tools/fetch-url.js +130 -120
- package/src/tools/goal.js +93 -0
- package/src/tools/index.js +2 -0
- package/src/tools/memory.js +80 -88
- package/src/tools/read.js +27 -23
- package/src/tools/todo.js +8 -13
- package/src/tools/validate-args.js +78 -0
- package/src/tui.js +1718 -585
package/src/tools/memory.js
CHANGED
|
@@ -1,88 +1,80 @@
|
|
|
1
|
-
// Memory tool — the agent records a durable note for FUTURE sessions.
|
|
2
|
-
//
|
|
3
|
-
// Distinct from /personal: that file is the USER's standing preferences and the
|
|
4
|
-
// agent must never write it. This tool writes MEMORY.md, which is the agent's own
|
|
5
|
-
// scratchpad, injected into the system prompt on later turns (see config.js's
|
|
6
|
-
// readMemory). The point is to stop rediscovering the same facts every session —
|
|
7
|
-
// build quirks, a command that works here, a decision the user already made.
|
|
8
|
-
|
|
9
|
-
import { appendMemory, readMemoryRaw, memoryFile } from '../config.js';
|
|
10
|
-
|
|
11
|
-
export const spec = {
|
|
12
|
-
name: 'Memory',
|
|
13
|
-
description: `Save a durable note for FUTURE sessions, or list what is
|
|
14
|
-
|
|
15
|
-
Use it
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const file = appendMemory(scope, workspace, note);
|
|
83
|
-
return `Saved to ${scope} memory (${file}).`;
|
|
84
|
-
} catch (e) {
|
|
85
|
-
return `Error writing memory: ${e.message}`;
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
};
|
|
1
|
+
// Memory tool — the agent records a durable note for FUTURE sessions.
|
|
2
|
+
//
|
|
3
|
+
// Distinct from /personal: that file is the USER's standing preferences and the
|
|
4
|
+
// agent must never write it. This tool writes MEMORY.md, which is the agent's own
|
|
5
|
+
// scratchpad, injected into the system prompt on later turns (see config.js's
|
|
6
|
+
// readMemory). The point is to stop rediscovering the same facts every session —
|
|
7
|
+
// build quirks, a command that works here, a decision the user already made.
|
|
8
|
+
|
|
9
|
+
import { appendMemory, readMemoryRaw, memoryFile } from '../config.js';
|
|
10
|
+
|
|
11
|
+
export const spec = {
|
|
12
|
+
name: 'Memory',
|
|
13
|
+
description: `Save a durable note for FUTURE sessions, or list what is saved.
|
|
14
|
+
|
|
15
|
+
Use it for something still true next week that you would otherwise rediscover: a build or test command that
|
|
16
|
+
works here, a layout quirk, a convention the user asked for, a decision already made, a gotcha that cost a
|
|
17
|
+
debugging cycle.
|
|
18
|
+
|
|
19
|
+
Not for: what is already in AGENTS.md or the code (read it — that is the source); the current task or its
|
|
20
|
+
progress (that is TodoList; this file outlives the task); secrets, tokens, credentials, or anything the user
|
|
21
|
+
asked you to keep quiet; one-off details.
|
|
22
|
+
|
|
23
|
+
One short note per call, in the user's language. Prefer several small notes over one long entry.
|
|
24
|
+
project (default) follows the repository; global applies to every workspace.`,
|
|
25
|
+
parameters: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
action: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['add', 'list'],
|
|
31
|
+
description: 'add a note, or list what is stored. Defaults to add.',
|
|
32
|
+
},
|
|
33
|
+
note: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'The note to save (one or two sentences). Required when action is add.',
|
|
36
|
+
},
|
|
37
|
+
scope: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: ['project', 'global'],
|
|
40
|
+
description: 'Which memory to write to. Defaults to project.',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: [],
|
|
44
|
+
},
|
|
45
|
+
async execute(args, ctx) {
|
|
46
|
+
// A malformed argument must not throw: an uncaught error here aborts the turn.
|
|
47
|
+
const action = String(args.action || 'add').toLowerCase();
|
|
48
|
+
const scope = String(args.scope || 'project').toLowerCase() === 'global' ? 'global' : 'project';
|
|
49
|
+
const workspace = (ctx && (ctx.workspace || ctx.cwd)) || process.cwd();
|
|
50
|
+
|
|
51
|
+
if (action === 'list') {
|
|
52
|
+
const project = readMemoryRaw('project', workspace).trim();
|
|
53
|
+
const global = readMemoryRaw('global', workspace).trim();
|
|
54
|
+
const out = [];
|
|
55
|
+
out.push(`project (${memoryFile('project', workspace)}):`);
|
|
56
|
+
out.push(project || ' (empty)');
|
|
57
|
+
out.push('');
|
|
58
|
+
out.push(`global (${memoryFile('global', workspace)}):`);
|
|
59
|
+
out.push(global || ' (empty)');
|
|
60
|
+
return out.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (action !== 'add') {
|
|
64
|
+
return `Error: unknown action ${JSON.stringify(action)}; use "add" or "list".`;
|
|
65
|
+
}
|
|
66
|
+
const note = String(args.note == null ? '' : args.note).trim();
|
|
67
|
+
if (!note) return 'Error: `note` is required when action is "add".';
|
|
68
|
+
// Guard against the file quietly becoming a transcript. The tool is for durable
|
|
69
|
+
// facts, and a pasted log is neither durable nor a fact.
|
|
70
|
+
if (note.length > 600) {
|
|
71
|
+
return `Error: the note is ${note.length} characters; keep it under 600 (save a summary, not a dump).`;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const file = appendMemory(scope, workspace, note);
|
|
75
|
+
return `Saved to ${scope} memory (${file}).`;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return `Error writing memory: ${e.message}`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
package/src/tools/read.js
CHANGED
|
@@ -35,17 +35,21 @@ export function forgetReadPool(ctx) {
|
|
|
35
35
|
}
|
|
36
36
|
export const spec = {
|
|
37
37
|
name: 'Read',
|
|
38
|
-
description: 'Read a text file.
|
|
38
|
+
description: 'Read a text file. With no line arguments it returns the whole file; pass start_line and '
|
|
39
|
+
+ 'end_line for a range. BOTH BOUNDS ARE INCLUSIVE: start_line=3, end_line=5 returns THREE lines — 3, 4 '
|
|
40
|
+
+ 'and 5. A negative start_line counts from the end (-100 = the last 100 lines). Every returned line is '
|
|
41
|
+
+ 'prefixed with its 1-based number.',
|
|
39
42
|
parameters: {
|
|
40
43
|
type: 'object',
|
|
41
44
|
properties: {
|
|
42
45
|
path: { type: 'string', description: 'Path to the text file.' },
|
|
43
|
-
|
|
46
|
+
start_line: {
|
|
44
47
|
type: 'integer',
|
|
45
|
-
description: '1-based
|
|
46
|
-
|
|
48
|
+
description: '1-based FIRST line to return, INCLUSIVE — it is a line that is returned, not the line '
|
|
49
|
+
+ 'before the first one. Omit to start at the first line, or pass a negative value to count from the '
|
|
50
|
+
+ 'end (-100 = last 100 lines).',
|
|
47
51
|
},
|
|
48
|
-
|
|
52
|
+
end_line: { type: 'integer', minimum: 1, description: '1-based LAST line to return, INCLUSIVE — it is a line that is returned, not the line after the last one. start_line=3, end_line=5 returns lines 3, 4 and 5. Omit to read to the end of the file.' },
|
|
49
53
|
},
|
|
50
54
|
required: ['path'],
|
|
51
55
|
},
|
|
@@ -72,30 +76,30 @@ export const spec = {
|
|
|
72
76
|
// staleness hashes recorded below because they were computed from the
|
|
73
77
|
// TRUNCATED text rather than the file on disk.
|
|
74
78
|
const text = buf.toString('utf8');
|
|
75
|
-
// A FILE ENDING IN A NEWLINE HAS ONE FEWER LINE THAN `split` REPORTS.
|
|
76
|
-
//
|
|
77
|
-
// `"a\nb\n".split("\n")` is `["a", "b", ""]` — the trailing newline leaves an empty element, and
|
|
78
|
-
// counting it made a ten-line file report `lines 1-11 of 11`. The numbers on each line were right;
|
|
79
|
-
// the TOTAL was one too high, so a caller planning an edit at the end of the file aimed at a row that
|
|
80
|
-
// does not exist.
|
|
81
|
-
//
|
|
82
|
-
// Only the element the trailing newline produced is dropped: a file that genuinely ends with a blank
|
|
83
|
-
// line (`"a\n\n"`) keeps it, because that is real content.
|
|
84
|
-
const lines = normalizeText(text).split("\n");
|
|
85
|
-
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
79
|
+
// A FILE ENDING IN A NEWLINE HAS ONE FEWER LINE THAN `split` REPORTS.
|
|
80
|
+
//
|
|
81
|
+
// `"a\nb\n".split("\n")` is `["a", "b", ""]` — the trailing newline leaves an empty element, and
|
|
82
|
+
// counting it made a ten-line file report `lines 1-11 of 11`. The numbers on each line were right;
|
|
83
|
+
// the TOTAL was one too high, so a caller planning an edit at the end of the file aimed at a row that
|
|
84
|
+
// does not exist.
|
|
85
|
+
//
|
|
86
|
+
// Only the element the trailing newline produced is dropped: a file that genuinely ends with a blank
|
|
87
|
+
// line (`"a\n\n"`) keeps it, because that is real content.
|
|
88
|
+
const lines = normalizeText(text).split("\n");
|
|
89
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
86
90
|
const total = lines.length;
|
|
87
|
-
// Default: every line.
|
|
88
|
-
|
|
89
|
-
let start = args.line_offset !== undefined ? args.line_offset : 1;
|
|
91
|
+
// Default: every line. `start_line` / `end_line` narrow the window, and either may be omitted.
|
|
92
|
+
let start = args.start_line !== undefined ? Number(args.start_line) : 1;
|
|
90
93
|
if (start < 0) start = Math.max(1, total + 1 + start);
|
|
91
94
|
start = Math.max(1, Math.min(start, total));
|
|
92
|
-
const end =
|
|
95
|
+
const end = args.end_line !== undefined
|
|
96
|
+
? Math.min(Math.max(start, Number(args.end_line)), total)
|
|
97
|
+
: total;
|
|
93
98
|
let out = '';
|
|
94
99
|
out += `${args.path} (lines ${start}-${end} of ${total})\n`;
|
|
95
100
|
for (let i = start; i <= end; i++) out += `${i}\t${lines[i - 1]}\n`;
|
|
96
|
-
// No trailing "truncated" banner: without an explicit
|
|
97
|
-
//
|
|
98
|
-
// header already states which lines those were.
|
|
101
|
+
// No trailing "truncated" banner: without an explicit end_line every line is returned, so the only way
|
|
102
|
+
// to stop early is a deliberate range request — the header already states which lines those were.
|
|
99
103
|
|
|
100
104
|
// --- Record a hash fingerprint so a later Edit can verify the AI is
|
|
101
105
|
// editing against the same content it read (snippet-staleness guard). ---
|
package/src/tools/todo.js
CHANGED
|
@@ -3,21 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
export const spec = {
|
|
5
5
|
name: 'TodoList',
|
|
6
|
-
description: `Track multi-step work. Pass the COMPLETE list; it replaces the current one.
|
|
6
|
+
description: `Track multi-step work. Pass the COMPLETE list; it replaces the current one.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
- Non-trivial work with several ordered steps, or a request that asks for more than one thing.
|
|
10
|
-
- NOT for a straightforward task you can finish in a step or two, and never as a single-item list.
|
|
8
|
+
Use it for non-trivial work with several ordered steps, or a request asking for more than one thing. Not for a task you can finish in a step or two, and never for a single item.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- If your understanding changes, replace the whole list with the corrected one and say why.
|
|
19
|
-
- Finish with every item done, or explicitly say which you abandoned and why. Do not leave the list stale.
|
|
20
|
-
- Do not repeat the list back to the user after updating it — the UI already shows it.`,
|
|
10
|
+
- Exactly ONE item in_progress at a time.
|
|
11
|
+
- Never pending → done: set in_progress first. Do not tick several off in one update after the fact.
|
|
12
|
+
- Titles 5-7 words, one line. Update before starting the next step, not in a batch at the end.
|
|
13
|
+
- If your understanding changes, replace the whole list and say why.
|
|
14
|
+
- Finish with every item done, or say which you abandoned and why — never leave it stale.
|
|
15
|
+
- Do not repeat the list back to the user; the UI already shows it.`,
|
|
21
16
|
parameters: {
|
|
22
17
|
type: 'object',
|
|
23
18
|
properties: {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reject a tool call whose arguments do not match the tool's own schema.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS AN ERROR AND NOT A NUDGE
|
|
5
|
+
* ------------------------------------
|
|
6
|
+
* An unknown parameter used to be accepted in silence, and that silence is expensive: the model sends
|
|
7
|
+
* `line_offset` to a tool that takes `start_line`, gets a plausible answer computed from defaults, and has no
|
|
8
|
+
* way to learn that the argument it took trouble to compute was thrown away. The failure then looks like "the
|
|
9
|
+
* tool is broken" rather than "the call was wrong", and the next attempt usually makes the same mistake with
|
|
10
|
+
* a different spelling.
|
|
11
|
+
*
|
|
12
|
+
* So a call carrying a key the schema does not declare is refused before it runs, and the message NAMES the
|
|
13
|
+
* key and lists the ones the tool accepts. `required` properties are checked in the same place: a missing
|
|
14
|
+
* `path` is the same class of mistake and should not reach a tool that will only fail on it later.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT a full JSON-Schema validator. It checks object shape, unknown keys, missing required keys
|
|
17
|
+
* and primitive types, which is what a hand-written tool description needs; a tool that wants to be stricter
|
|
18
|
+
* still validates inside `execute`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The JSON type name for a value, in the vocabulary a schema uses. */
|
|
22
|
+
function typeOf(value) {
|
|
23
|
+
if (value === null) return 'null';
|
|
24
|
+
if (Array.isArray(value)) return 'array';
|
|
25
|
+
if (Number.isInteger(value)) return 'integer';
|
|
26
|
+
return typeof value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether a value satisfies one schema type name. `number` accepts an integer. */
|
|
30
|
+
function matchesType(value, type) {
|
|
31
|
+
const actual = typeOf(value);
|
|
32
|
+
if (type === 'number') return actual === 'number' || actual === 'integer';
|
|
33
|
+
if (type === 'integer') return actual === 'integer';
|
|
34
|
+
return actual === type;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate arguments against a tool's parameter schema.
|
|
39
|
+
*
|
|
40
|
+
* @param {object|undefined|null} args The parsed arguments.
|
|
41
|
+
* @param {{type?: string, properties?: object, required?: string[]}} [schema] The tool's `parameters`.
|
|
42
|
+
* @returns {string|null} An error message, or null when the call may run.
|
|
43
|
+
*/
|
|
44
|
+
export function validateToolArgs(args, schema) {
|
|
45
|
+
// A tool with no schema takes anything; there is nothing to be wrong about.
|
|
46
|
+
if (!schema || !schema.properties) return null;
|
|
47
|
+
// A tool whose schema is a free-form object (the shape has no declared properties) also accepts anything.
|
|
48
|
+
const properties = schema.properties || {};
|
|
49
|
+
const declared = Object.keys(properties);
|
|
50
|
+
if (declared.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
const isObject = args !== null && typeof args === 'object' && !Array.isArray(args);
|
|
53
|
+
if (!isObject) {
|
|
54
|
+
return `the arguments must be a JSON object with keys: ${declared.join(', ')}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const unknown = Object.keys(args).filter((key) => !(key in properties));
|
|
58
|
+
if (unknown.length > 0) {
|
|
59
|
+
const also = declared.length > 0 ? ` It accepts: ${declared.join(', ')}.` : '';
|
|
60
|
+
return `${unknown.map((k) => `"${k}"`).join(', ')} ${unknown.length === 1 ? 'is not a parameter' : 'are not parameters'}`
|
|
61
|
+
+ ` of this tool.${also}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
65
|
+
const missing = required.filter((key) => args[key] === undefined);
|
|
66
|
+
if (missing.length > 0) {
|
|
67
|
+
return `missing required ${missing.length === 1 ? 'parameter' : 'parameters'}: ${missing.join(', ')}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const [key, value] of Object.entries(args)) {
|
|
71
|
+
const type = properties[key] && properties[key].type;
|
|
72
|
+
if (type && !matchesType(value, type)) {
|
|
73
|
+
return `"${key}" must be ${type === 'integer' ? 'an' : 'a'} ${type}, got ${typeOf(value)}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|