@hncode/hncode 0.5.6 → 0.6.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.
Files changed (56) hide show
  1. package/package.json +37 -38
  2. package/src/agent.js +67 -2
  3. package/src/image-row.js +111 -0
  4. package/src/latex.js +1557 -0
  5. package/src/llm.js +19 -4
  6. package/src/mermaid/ansi.js +55 -0
  7. package/src/mermaid/canvas.js +449 -0
  8. package/src/mermaid/class-style.js +57 -0
  9. package/src/mermaid/css-colors.js +153 -0
  10. package/src/mermaid/diagrams/class.js +289 -0
  11. package/src/mermaid/diagrams/er.js +201 -0
  12. package/src/mermaid/diagrams/flowchart.js +488 -0
  13. package/src/mermaid/diagrams/gitgraph.js +205 -0
  14. package/src/mermaid/diagrams/mindmap.js +101 -0
  15. package/src/mermaid/diagrams/pie.js +104 -0
  16. package/src/mermaid/diagrams/sequence.js +282 -0
  17. package/src/mermaid/diagrams/state.js +292 -0
  18. package/src/mermaid/diagrams/timeline.js +99 -0
  19. package/src/mermaid/graph.js +138 -0
  20. package/src/mermaid/index.js +59 -0
  21. package/src/mermaid/labels.js +339 -0
  22. package/src/mermaid/layout-seq.js +225 -0
  23. package/src/mermaid/layout.js +1454 -0
  24. package/src/mermaid/registry.js +54 -0
  25. package/src/mermaid/source-box.js +102 -0
  26. package/src/mermaid/statements.js +235 -0
  27. package/src/mermaid/types.js +1 -0
  28. package/src/mermaid/width.js +29 -0
  29. package/src/screen-diff.js +512 -0
  30. package/src/screen-paint.js +128 -0
  31. package/src/screen.js +277 -0
  32. package/src/subagent-types.js +136 -147
  33. package/src/term-caps.js +10 -4
  34. package/src/tex/font.js +292 -0
  35. package/src/tex/fonts/LICENSE.txt +11 -0
  36. package/src/tex/fonts/STIX2Math.ttf +0 -0
  37. package/src/tex/index.js +293 -0
  38. package/src/tex/kitty.js +186 -0
  39. package/src/tex/layout.js +446 -0
  40. package/src/tex/parse.js +331 -0
  41. package/src/tex/raster.js +216 -0
  42. package/src/tex/render.js +167 -0
  43. package/src/tex/sixel.js +326 -0
  44. package/src/tex/svg.js +237 -0
  45. package/src/tool-result.js +2 -2
  46. package/src/tools/agent-swarm.js +3 -15
  47. package/src/tools/agent.js +454 -464
  48. package/src/tools/ask-user-question.js +195 -206
  49. package/src/tools/bash.js +39 -3
  50. package/src/tools/edit.js +10 -112
  51. package/src/tools/fetch-url.js +130 -120
  52. package/src/tools/memory.js +80 -88
  53. package/src/tools/read.js +27 -23
  54. package/src/tools/todo.js +8 -13
  55. package/src/tools/validate-args.js +78 -0
  56. package/src/tui.js +1346 -502
@@ -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 already saved.
14
-
15
- Use it when you learn something that will still be true next week and that you would
16
- otherwise have to rediscover: a build/test command that works in this repo, a layout
17
- quirk, a convention the user asked for, a decision they already made, a gotcha that
18
- cost you a debugging cycle.
19
-
20
- Do NOT use it for:
21
- - things already in AGENTS.md or the code (read them instead — they are the source);
22
- - facts about the CURRENT task or its progress (use TodoList; this file outlives the task);
23
- - anything the user asked you to keep quiet, secrets, tokens, or credentials;
24
- - one-off details you will never need again.
25
-
26
- Write ONE short note per call, in the user's own language. Prefer several small notes
27
- over one long entry: a note that is wrong or stale later can be edited out of a file
28
- you can read back.
29
-
30
- Scope:
31
- - project (default) — stored in this workspace, follows the repository;
32
- - global — stored in your home directory, applies to every workspace.`,
33
- parameters: {
34
- type: 'object',
35
- properties: {
36
- action: {
37
- type: 'string',
38
- enum: ['add', 'list'],
39
- description: 'add a note, or list what is stored. Defaults to add.',
40
- },
41
- note: {
42
- type: 'string',
43
- description: 'The note to save (one or two sentences). Required when action is add.',
44
- },
45
- scope: {
46
- type: 'string',
47
- enum: ['project', 'global'],
48
- description: 'Which memory to write to. Defaults to project.',
49
- },
50
- },
51
- required: [],
52
- },
53
- async execute(args, ctx) {
54
- // A malformed argument must not throw: an uncaught error here aborts the turn.
55
- const action = String(args.action || 'add').toLowerCase();
56
- const scope = String(args.scope || 'project').toLowerCase() === 'global' ? 'global' : 'project';
57
- const workspace = (ctx && (ctx.workspace || ctx.cwd)) || process.cwd();
58
-
59
- if (action === 'list') {
60
- const project = readMemoryRaw('project', workspace).trim();
61
- const global = readMemoryRaw('global', workspace).trim();
62
- const out = [];
63
- out.push(`project (${memoryFile('project', workspace)}):`);
64
- out.push(project || ' (empty)');
65
- out.push('');
66
- out.push(`global (${memoryFile('global', workspace)}):`);
67
- out.push(global || ' (empty)');
68
- return out.join('\n');
69
- }
70
-
71
- if (action !== 'add') {
72
- return `Error: unknown action ${JSON.stringify(action)}; use "add" or "list".`;
73
- }
74
- const note = String(args.note == null ? '' : args.note).trim();
75
- if (!note) return 'Error: `note` is required when action is "add".';
76
- // Guard against the file quietly becoming a transcript. The tool is for durable
77
- // facts, and a pasted log is neither durable nor a fact.
78
- if (note.length > 600) {
79
- return `Error: the note is ${note.length} characters; keep it under 600 (save a summary, not a dump).`;
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. Returns the whole file by default; use line_offset / n_lines for a range.',
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
- line_offset: {
46
+ start_line: {
44
47
  type: 'integer',
45
- description: '1-based start line. Negative counts from the end (e.g. -100 = last 100 lines).',
46
- default: 1,
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
- n_lines: { type: 'integer', minimum: 1, description: 'Lines to read (default: the rest of the file).' },
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. An explicit n_lines still narrows the window.
88
- const nLines = args.n_lines ? args.n_lines : total;
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 = Math.min(start + nLines - 1, total);
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 n_lines every line is
97
- // returned, so the only way to stop early is a deliberate range request — the
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. Items: title + status (pending | in_progress | done).
6
+ description: `Track multi-step work. Pass the COMPLETE list; it replaces the current one.
7
7
 
8
- When to use it:
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
- Maintaining the list:
13
- - Keep exactly ONE item in_progress at a time until everything is done.
14
- - Never move an item straight from pending to done: set it to in_progress first, so the list reflects
15
- what is actually happening. Do not tick off several items in one update after the fact.
16
- - Step titles are short (5-7 words), one line each.
17
- - Update the list as you go, before starting the next step, not in a batch at the end.
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
+ }