@hncode/hncode 0.2.0 → 0.3.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/LICENSE +674 -219
- package/README.md +1 -0
- package/README_CN.md +1 -0
- package/package.json +1 -1
- package/src/agent-task.js +178 -0
- package/src/agent.js +101 -8
- package/src/colors.js +14 -0
- package/src/config.js +191 -10
- package/src/index.js +31 -9
- package/src/llm.js +60 -11
- package/src/plugin.js +25 -2
- package/src/prompt-presets.js +429 -0
- package/src/session.js +257 -24
- package/src/subagent-types.js +147 -0
- package/src/swarm-progress.js +293 -0
- package/src/task-output-viewer.js +135 -0
- package/src/tasks-browser.js +279 -0
- package/src/tools/agent-swarm.js +208 -0
- package/src/tools/agent.js +342 -0
- package/src/tools/ask-user-question.js +206 -0
- package/src/tools/bash.js +53 -29
- package/src/tools/edit.js +122 -11
- package/src/tools/file-lines.js +41 -8
- package/src/tools/glob.js +17 -5
- package/src/tools/grep.js +16 -3
- package/src/tools/index.js +19 -2
- package/src/tools/matchers.js +75 -71
- package/src/tools/read-media-file.js +17 -3
- package/src/tools/read.js +2 -0
- package/src/tools/tasks.js +141 -126
- package/src/tools/todo.js +29 -2
- package/src/tools/utils.js +11 -1
- package/src/tui.js +2264 -337
- package/src/update.js +95 -0
- package/src/version.js +20 -0
package/src/tools/tasks.js
CHANGED
|
@@ -1,126 +1,141 @@
|
|
|
1
|
-
// TaskList / TaskOutput / TaskStop — manage background
|
|
2
|
-
// run_in_background.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
t
|
|
66
|
-
t
|
|
67
|
-
t.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
1
|
+
// TaskList / TaskOutput / TaskStop / TaskWait — manage background tasks spawned
|
|
2
|
+
// by Bash (run_in_background) and Agent (run_in_background). The task records
|
|
3
|
+
// live in the shared registry (src/agent-task.js), so the CLI tools and the
|
|
4
|
+
// /tasks panel read exactly the same data.
|
|
5
|
+
|
|
6
|
+
import cp from 'node:child_process';
|
|
7
|
+
import { isTerminal, settleTask, STATUS_LABEL } from '../agent-task.js';
|
|
8
|
+
|
|
9
|
+
function taskLine(t) {
|
|
10
|
+
const extra = t.stopReason ? ` (${t.stopReason})` : '';
|
|
11
|
+
const label = STATUS_LABEL[t.status] || t.status;
|
|
12
|
+
const what = t.kind === 'process' ? (t.description || t.command) : t.description;
|
|
13
|
+
return `#${t.taskId} [${label}${extra}] pid=${t.pid || '-'} ${what}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TaskListSpec = {
|
|
17
|
+
name: 'TaskList',
|
|
18
|
+
description: 'List background tasks (from Bash or Agent run_in_background). Reports task id, status, description.',
|
|
19
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
20
|
+
async execute(_args, ctx) {
|
|
21
|
+
const tasks = Object.values(ctx.tasks || {}).sort((a, b) => b.startedAt - a.startedAt);
|
|
22
|
+
if (tasks.length === 0) return 'No background tasks.';
|
|
23
|
+
return tasks.map(taskLine).join('\n');
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const TaskOutputSpec = {
|
|
28
|
+
name: 'TaskOutput',
|
|
29
|
+
description: 'Get the output of a background task by task_id. head_limit caps the lines returned.',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
task_id: { type: 'string', description: 'Task id returned when the task was started.' },
|
|
34
|
+
head_limit: { type: 'integer', minimum: 0, description: 'Max output lines returned.' },
|
|
35
|
+
},
|
|
36
|
+
required: ['task_id'],
|
|
37
|
+
},
|
|
38
|
+
async execute(args, ctx) {
|
|
39
|
+
const t = (ctx.tasks || {})[args.task_id];
|
|
40
|
+
if (!t) return `No such task: ${args.task_id}`;
|
|
41
|
+
const text = t.output || '';
|
|
42
|
+
const lines = text ? text.split('\n').filter((l) => l.length > 0) : [];
|
|
43
|
+
const capped = args.head_limit ? lines.slice(0, args.head_limit) : lines;
|
|
44
|
+
const tail = lines.length > capped.length;
|
|
45
|
+
const label = STATUS_LABEL[t.status] || t.status;
|
|
46
|
+
const head = `#${t.taskId} [${label}] (pid ${t.pid || '-'})`;
|
|
47
|
+
let out = capped.length ? `${head}\n${capped.join('\n')}` : `${head}\n(no output yet)`;
|
|
48
|
+
if (tail) out += `\n... [${lines.length - capped.length} more lines; use head_limit]`;
|
|
49
|
+
return out;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const TaskStopSpec = {
|
|
54
|
+
name: 'TaskStop',
|
|
55
|
+
description: 'Stop a running background task by task_id. Use only when the task must be cancelled.',
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
task_id: { type: 'string', description: 'Background task id to stop.' },
|
|
60
|
+
reason: { type: 'string', description: 'Short reason recorded on stop.' },
|
|
61
|
+
},
|
|
62
|
+
required: ['task_id'],
|
|
63
|
+
},
|
|
64
|
+
async execute(args, ctx) {
|
|
65
|
+
const t = (ctx.tasks || {})[args.task_id];
|
|
66
|
+
if (!t) return `No such task: ${args.task_id}`;
|
|
67
|
+
if (isTerminal(t.status)) return `Task ${t.taskId} already ${STATUS_LABEL[t.status] || t.status}.`;
|
|
68
|
+
const reason = args.reason || 'Stopped by TaskStop';
|
|
69
|
+
try {
|
|
70
|
+
// An agent task aborts through its controller; a process is killed.
|
|
71
|
+
if (t.kind === 'agent' && typeof t._abort === 'function') {
|
|
72
|
+
t._abort();
|
|
73
|
+
} else if (t.pid) {
|
|
74
|
+
if (process.platform === 'win32') {
|
|
75
|
+
try { cp.spawnSync('taskkill', ['/pid', String(t.pid), '/t', '/f'], { stdio: 'ignore' }); } catch {}
|
|
76
|
+
} else {
|
|
77
|
+
try { process.kill(t.pid, 'SIGTERM'); } catch {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return `Failed to stop ${t.taskId}: ${e.message}`;
|
|
82
|
+
}
|
|
83
|
+
// 'killed' — a deliberate stop, distinct from a failure (kimi's vocabulary).
|
|
84
|
+
settleTask(t, 'killed', { stopReason: reason });
|
|
85
|
+
return `Stopped task ${t.taskId}. reason: ${reason}`;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Wait for background tasks to finish. A timeout is not an error; the result
|
|
90
|
+
// lists the tasks still running so the caller can wait again.
|
|
91
|
+
export const TaskWaitSpec = {
|
|
92
|
+
name: 'TaskWait',
|
|
93
|
+
description: 'Wait for background tasks to finish, timeout in seconds (1-600). With task_id, waits for that task; without it, returns when any running task finishes. A timeout is not an error; still-running tasks are listed, so you may call it again.',
|
|
94
|
+
parameters: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
timeout: { type: 'integer', minimum: 1, maximum: 600, description: 'Max seconds to wait (1-600).' },
|
|
98
|
+
task_id: { type: 'string', description: 'Task id to wait for; omit to wait for any running task.' },
|
|
99
|
+
},
|
|
100
|
+
required: ['timeout'],
|
|
101
|
+
},
|
|
102
|
+
async execute(args, ctx) {
|
|
103
|
+
const tasks = ctx.tasks || {};
|
|
104
|
+
const target = args.task_id ? tasks[args.task_id] : null;
|
|
105
|
+
if (args.task_id && !target) return `No such task: ${args.task_id}`;
|
|
106
|
+
const secs = Math.max(1, Math.min(600, Number(args.timeout) || 1));
|
|
107
|
+
const deadline = Date.now() + secs * 1000;
|
|
108
|
+
const signal = ctx && ctx.signal;
|
|
109
|
+
|
|
110
|
+
const isDone = (t) => t && isTerminal(t.status);
|
|
111
|
+
const anyRunning = () => Object.values(tasks).some((t) => t.status === 'running');
|
|
112
|
+
|
|
113
|
+
if (args.task_id) {
|
|
114
|
+
if (isDone(target)) return `Task ${target.taskId} already ${STATUS_LABEL[target.status] || target.status}${target.exitCode != null ? ` (exit ${target.exitCode})` : ''}.`;
|
|
115
|
+
} else if (!anyRunning()) {
|
|
116
|
+
return 'No background tasks are running.';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await new Promise((resolve) => {
|
|
120
|
+
const tick = setInterval(() => {
|
|
121
|
+
const done = args.task_id ? isDone(target) : !anyRunning();
|
|
122
|
+
if (done || Date.now() >= deadline || (signal && signal.aborted)) {
|
|
123
|
+
clearInterval(tick);
|
|
124
|
+
resolve();
|
|
125
|
+
}
|
|
126
|
+
}, 100);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (signal && signal.aborted) return 'Interrupted by user';
|
|
130
|
+
const still = Object.values(tasks).filter((t) => t.status === 'running');
|
|
131
|
+
if (args.task_id) {
|
|
132
|
+
if (isDone(target)) {
|
|
133
|
+
return `Task ${target.taskId} finished: ${STATUS_LABEL[target.status] || target.status}${target.exitCode != null ? ` (exit ${target.exitCode})` : ''}${target.stopReason ? ` — ${target.stopReason}` : ''}.`;
|
|
134
|
+
}
|
|
135
|
+
return `Task ${target.taskId} still running after ${secs}s.`;
|
|
136
|
+
}
|
|
137
|
+
if (!still.length) return 'All background tasks have finished.';
|
|
138
|
+
const list = still.map((t) => `#${t.taskId} ${t.description || t.command || ''}`).join('; ');
|
|
139
|
+
return `Timeout after ${secs}s; still running: ${list}`;
|
|
140
|
+
},
|
|
141
|
+
};
|
package/src/tools/todo.js
CHANGED
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
export const spec = {
|
|
5
5
|
name: 'TodoList',
|
|
6
|
-
description:
|
|
6
|
+
description: `Track multi-step work. Pass the COMPLETE list; it replaces the current one. Items: title + status (pending | in_progress | done).
|
|
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.
|
|
11
|
+
|
|
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.`,
|
|
7
21
|
parameters: {
|
|
8
22
|
type: 'object',
|
|
9
23
|
properties: {
|
|
@@ -23,7 +37,20 @@ export const spec = {
|
|
|
23
37
|
required: ['todos'],
|
|
24
38
|
},
|
|
25
39
|
async execute(args, ctx) {
|
|
26
|
-
|
|
40
|
+
// `todos` is required, but a missing or mistyped value must not throw: an
|
|
41
|
+
// uncaught error here aborts the whole turn. Return a message the model can
|
|
42
|
+
// act on, like every other tool.
|
|
43
|
+
if (!Array.isArray(args.todos)) {
|
|
44
|
+
return 'Error: `todos` must be an array of { title, status }.';
|
|
45
|
+
}
|
|
46
|
+
const VALID = ['pending', 'in_progress', 'done'];
|
|
47
|
+
const badStatus = args.todos.find((t) => t && t.status != null && !VALID.includes(t.status));
|
|
48
|
+
if (badStatus) {
|
|
49
|
+
return `Error: invalid status ${JSON.stringify(badStatus.status)}; use one of ${VALID.join(' | ')}.`;
|
|
50
|
+
}
|
|
51
|
+
const empty = args.todos.find((t) => !t || typeof t.title !== 'string' || !t.title.trim());
|
|
52
|
+
if (empty) return 'Error: every todo needs a non-empty `title`.';
|
|
53
|
+
ctx.todoState = args.todos.map((t) => ({ title: String(t.title).trim(), status: t.status || 'pending' }));
|
|
27
54
|
return render(ctx.todoState);
|
|
28
55
|
},
|
|
29
56
|
};
|
package/src/tools/utils.js
CHANGED
|
@@ -13,7 +13,17 @@ export function truncateBuf(buf) {
|
|
|
13
13
|
}
|
|
14
14
|
const s = String(buf);
|
|
15
15
|
if (s.length <= MAX_OUTPUT_BYTES) return s;
|
|
16
|
-
|
|
16
|
+
// The cap is a BYTE budget, but `s.length` is UTF-16 code units. Slicing by
|
|
17
|
+
// code units both under-counts non-ASCII (a 3-byte CJK char costs 1 unit)
|
|
18
|
+
// and can split a surrogate pair, emitting a lone half. Truncate by BYTES
|
|
19
|
+
// and back off to the nearest code-point boundary.
|
|
20
|
+
const bytes = Buffer.byteLength(s, 'utf8');
|
|
21
|
+
if (bytes <= MAX_OUTPUT_BYTES) return s;
|
|
22
|
+
const cut = Buffer.from(s, 'utf8').subarray(0, MAX_OUTPUT_BYTES).toString('utf8');
|
|
23
|
+
// A trailing lone surrogate (from a split pair) is dropped by decoding; the
|
|
24
|
+
// replacement char U+FFFD at the tail is also stripped so the marker is clean.
|
|
25
|
+
const safe = cut.endsWith('\uFFFD') ? cut.slice(0, -1) : cut;
|
|
26
|
+
return safe + `\n...[truncated ${bytes - MAX_OUTPUT_BYTES} bytes]`;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
// Expand ~ and map Git-Bash /c/x -> C:\x style paths, then resolve against cwd.
|