@hncode/hncode 0.5.1 → 0.5.4

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 (60) hide show
  1. package/bin/install.cjs +24 -44
  2. package/bin/preinstall.cjs +0 -14
  3. package/package.json +2 -2
  4. package/src/agent-task.js +12 -1
  5. package/src/agent.js +279 -48
  6. package/src/cache.js +175 -130
  7. package/src/cell-buffer.js +5 -0
  8. package/src/clipboard.js +111 -8
  9. package/src/context-bar.js +46 -5
  10. package/src/doctor.js +7 -2
  11. package/src/git.js +35 -4
  12. package/src/index.js +29 -5
  13. package/src/keybindings.js +25 -3
  14. package/src/llm.js +60 -6
  15. package/src/mcp.js +419 -384
  16. package/src/permissions.js +29 -7
  17. package/src/plugin.js +95 -10
  18. package/src/registry-browser.js +21 -6
  19. package/src/remote.js +307 -289
  20. package/src/session-core.js +203 -0
  21. package/src/session-host.js +213 -0
  22. package/src/session-view.js +110 -0
  23. package/src/settings-schema.js +12 -9
  24. package/src/shell-safety.js +610 -473
  25. package/src/swarm-progress.js +348 -293
  26. package/src/tasks-browser.js +4 -1
  27. package/src/team/constants.js +20 -0
  28. package/src/team/context.js +89 -0
  29. package/src/team/mailbox.js +216 -0
  30. package/src/team/permission.js +121 -0
  31. package/src/team/protocol.js +167 -0
  32. package/src/team/runner.js +462 -0
  33. package/src/team/spawn.js +305 -0
  34. package/src/team/store.js +172 -0
  35. package/src/team/tasks.js +177 -0
  36. package/src/team/team.js +346 -0
  37. package/src/team-panel.js +624 -0
  38. package/src/term-caps.js +457 -410
  39. package/src/term.js +162 -19
  40. package/src/tool-result.js +70 -7
  41. package/src/tools/agent-swarm.js +114 -11
  42. package/src/tools/agent.js +464 -386
  43. package/src/tools/bash.js +53 -14
  44. package/src/tools/diagnostics.js +13 -0
  45. package/src/tools/edit.js +428 -50
  46. package/src/tools/git.js +15 -3
  47. package/src/tools/glob.js +5 -3
  48. package/src/tools/grep.js +19 -5
  49. package/src/tools/ignore.js +17 -3
  50. package/src/tools/index.js +19 -3
  51. package/src/tools/read.js +2 -1
  52. package/src/tools/shell.js +145 -134
  53. package/src/tools/tasks.js +214 -141
  54. package/src/tools/team.js +411 -0
  55. package/src/tools/utils.js +12 -3
  56. package/src/tools/web-search.js +53 -40
  57. package/src/tui.js +7496 -5366
  58. package/src/update.js +320 -115
  59. package/src/web-daemon.js +27 -4
  60. package/src/web-live.js +250 -0
@@ -1,141 +1,214 @@
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
- };
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 OR teammate task aborts through its controller; a process is killed.
71
+ // Keyed on the handle, not the kind — a teammate task carries `_abort` too
72
+ // (see spawnTeammate), and gating on kind==='agent' left teammates unstoppable.
73
+ if (typeof t._abort === 'function') {
74
+ t._abort();
75
+ } else if (t.pid) {
76
+ if (process.platform === 'win32') {
77
+ try { cp.spawnSync('taskkill', ['/pid', String(t.pid), '/t', '/f'], { stdio: 'ignore' }); } catch {}
78
+ } else {
79
+ try { process.kill(t.pid, 'SIGTERM'); } catch {}
80
+ }
81
+ }
82
+ } catch (e) {
83
+ return `Failed to stop ${t.taskId}: ${e.message}`;
84
+ }
85
+ // 'killed' — a deliberate stop, distinct from a failure (kimi's vocabulary).
86
+ settleTask(t, 'killed', { stopReason: reason });
87
+ return `Stopped task ${t.taskId}. reason: ${reason}`;
88
+ },
89
+ };
90
+
91
+ /**
92
+ * Write to a background task started with `Bash {interactive: true}`.
93
+ *
94
+ * WHY THIS EXISTS
95
+ * ---------------
96
+ * A background command's stdin is /dev/null by default — deliberately, because leaving it as an open pipe
97
+ * made `cat` and `npm init` block until the timeout. That also meant a command that WANTS input could never
98
+ * be given any: a REPL, a prompt-driven generator, or a server reading commands had no way in.
99
+ *
100
+ * `interactive` opts ONE run into a real pipe and this tool is the other end of it. Both are needed: the
101
+ * flag without the tool leaves a pipe nobody can write to, and the tool without the flag has no pipe to
102
+ * find.
103
+ *
104
+ * ENTER is a parameter rather than something baked into `text`, because a prompt-driven program
105
+ * distinguishes "line entered" from "line still being typed", and a caller sending three lines then a bare
106
+ * Enter should not have to remember to write a trailing newline.
107
+ */
108
+ export const TaskInputSpec = {
109
+ name: 'TaskInput',
110
+ description: [
111
+ 'Send text to the stdin of a background task started with `Bash {interactive: true}`.',
112
+ '',
113
+ '`text` is written verbatim. `enter` (default true) appends a newline, which is what a prompt-driven',
114
+ 'program needs to accept the line; pass `enter: false` to type without submitting.',
115
+ '',
116
+ 'The task must still be running: a finished one has no stdin, and this says so rather than pretending',
117
+ 'the write landed. Read the reply with TaskOutput.',
118
+ ].join('\n'),
119
+ parameters: {
120
+ type: 'object',
121
+ properties: {
122
+ task_id: { type: 'string', description: 'Background task id (from Bash with interactive: true).' },
123
+ text: { type: 'string', description: 'Text to write, verbatim.' },
124
+ enter: { type: 'boolean', default: true, description: 'Append a newline so the program accepts the line. False types without submitting.' },
125
+ },
126
+ required: ['task_id'],
127
+ },
128
+ async execute(args, ctx) {
129
+ const t = (ctx.tasks || {})[args.task_id];
130
+ if (!t) return `No such task: ${args.task_id}`;
131
+ if (isTerminal(t.status)) {
132
+ return `Task ${t.taskId} is ${STATUS_LABEL[t.status] || t.status} — it has no stdin to write to.`;
133
+ }
134
+ if (!t.stdin) {
135
+ // The two ways to have no pipe, told apart: the task was never interactive, or its process closed
136
+ // stdin by itself. Naming which one saves a round of guessing.
137
+ return t.interactive
138
+ ? `Task ${t.taskId} closed its stdin — the process is no longer reading input.`
139
+ : `Task ${t.taskId} was not started with \`interactive: true\`, so it has no stdin. Restart it with that flag.`;
140
+ }
141
+
142
+ const text = args.text == null ? '' : String(args.text);
143
+ const payload = args.enter === false ? text : `${text}\n`;
144
+ if (!payload) return 'Nothing to write.';
145
+
146
+ try {
147
+ // Resolve on the WRITE CALLBACK, not on the return value: `write` returns false when the buffer is
148
+ // full, and the callback fires once the bytes are actually out. Awaiting it is the difference
149
+ // between "we called write" and "the write landed".
150
+ await new Promise((resolve, reject) => {
151
+ t.stdin.write(payload, (err) => (err ? reject(err) : resolve()));
152
+ });
153
+ } catch (e) {
154
+ // EPIPE: the process exited between the status check and the write. Not a failure of the tool.
155
+ t.stdin = null;
156
+ return `Task ${t.taskId} stopped reading input: ${e.message}`;
157
+ }
158
+
159
+ return `Wrote ${payload.length} character(s) to ${t.taskId}. Read the reply with TaskOutput.`;
160
+ },
161
+ };
162
+ // Wait for background tasks to finish. A timeout is not an error; the result
163
+ // lists the tasks still running so the caller can wait again.
164
+ export const TaskWaitSpec = {
165
+ name: 'TaskWait',
166
+ 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.',
167
+ parameters: {
168
+ type: 'object',
169
+ properties: {
170
+ timeout: { type: 'integer', minimum: 1, maximum: 600, description: 'Max seconds to wait (1-600).' },
171
+ task_id: { type: 'string', description: 'Task id to wait for; omit to wait for any running task.' },
172
+ },
173
+ required: ['timeout'],
174
+ },
175
+ async execute(args, ctx) {
176
+ const tasks = ctx.tasks || {};
177
+ const target = args.task_id ? tasks[args.task_id] : null;
178
+ if (args.task_id && !target) return `No such task: ${args.task_id}`;
179
+ const secs = Math.max(1, Math.min(600, Number(args.timeout) || 1));
180
+ const deadline = Date.now() + secs * 1000;
181
+ const signal = ctx && ctx.signal;
182
+
183
+ const isDone = (t) => t && isTerminal(t.status);
184
+ const anyRunning = () => Object.values(tasks).some((t) => t.status === 'running');
185
+
186
+ if (args.task_id) {
187
+ if (isDone(target)) return `Task ${target.taskId} already ${STATUS_LABEL[target.status] || target.status}${target.exitCode != null ? ` (exit ${target.exitCode})` : ''}.`;
188
+ } else if (!anyRunning()) {
189
+ return 'No background tasks are running.';
190
+ }
191
+
192
+ await new Promise((resolve) => {
193
+ const tick = setInterval(() => {
194
+ const done = args.task_id ? isDone(target) : !anyRunning();
195
+ if (done || Date.now() >= deadline || (signal && signal.aborted)) {
196
+ clearInterval(tick);
197
+ resolve();
198
+ }
199
+ }, 100);
200
+ });
201
+
202
+ if (signal && signal.aborted) return 'Interrupted by user';
203
+ const still = Object.values(tasks).filter((t) => t.status === 'running');
204
+ if (args.task_id) {
205
+ if (isDone(target)) {
206
+ return `Task ${target.taskId} finished: ${STATUS_LABEL[target.status] || target.status}${target.exitCode != null ? ` (exit ${target.exitCode})` : ''}${target.stopReason ? ` — ${target.stopReason}` : ''}.`;
207
+ }
208
+ return `Task ${target.taskId} still running after ${secs}s.`;
209
+ }
210
+ if (!still.length) return 'All background tasks have finished.';
211
+ const list = still.map((t) => `#${t.taskId} ${t.description || t.command || ''}`).join('; ');
212
+ return `Timeout after ${secs}s; still running: ${list}`;
213
+ },
214
+ };