@hncode/hncode 0.4.2 → 0.5.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/README.md +1 -15
- package/README_CN.md +1 -15
- package/bin/hncode +22 -9
- package/package.json +3 -2
- package/src/agent.js +149 -35
- package/src/bootstrap.js +56 -0
- package/src/btw-panel.js +224 -0
- package/src/cache.js +3 -2
- package/src/cell-buffer.js +173 -0
- package/src/ci.js +4 -1
- package/src/clipboard.js +202 -43
- package/src/colors.js +478 -242
- package/src/config.js +409 -40
- package/src/context-bar.js +237 -0
- package/src/doctor.js +518 -0
- package/src/experiments.js +122 -0
- package/src/file-history.js +112 -0
- package/src/file-tree.js +255 -0
- package/src/flush-tick.js +56 -0
- package/src/fork.js +44 -0
- package/src/fx.js +148 -0
- package/src/index.js +123 -11
- package/src/keybindings.js +229 -0
- package/src/llm.js +355 -34
- package/src/lsp.js +401 -0
- package/src/memory-profile.js +247 -0
- package/src/notebook.js +232 -0
- package/src/output-styles.js +183 -0
- package/src/patch.js +248 -0
- package/src/permissions.js +130 -1
- package/src/plan.js +3 -2
- package/src/plugin.js +611 -58
- package/src/prompt-presets.js +1 -1
- package/src/registry-browser.js +31 -13
- package/src/remote.js +20 -2
- package/src/rollout.js +151 -0
- package/src/row-cache.js +148 -0
- package/src/schedule.js +294 -0
- package/src/session-approvals.js +86 -0
- package/src/session-hub.js +1 -0
- package/src/session.js +178 -8
- package/src/settings-schema.js +301 -0
- package/src/shell-safety.js +473 -0
- package/src/side-thread.js +240 -0
- package/src/skills.js +27 -0
- package/src/stream-buffer.js +350 -0
- package/src/subagent-control.js +160 -0
- package/src/teleport.js +260 -0
- package/src/term-caps.js +411 -0
- package/src/term.js +283 -8
- package/src/tool-result.js +264 -236
- package/src/tools/agent-control.js +98 -0
- package/src/tools/agent-swarm.js +11 -2
- package/src/tools/agent.js +35 -1
- package/src/tools/diagnostics.js +175 -0
- package/src/tools/edit.js +25 -10
- package/src/tools/grep.js +62 -20
- package/src/tools/ignore.js +38 -14
- package/src/tools/index.js +10 -0
- package/src/tools/notebook.js +113 -0
- package/src/tools/read-media-file.js +10 -3
- package/src/tools/shell.js +34 -6
- package/src/tui.js +5637 -746
- package/src/web-public/app.js +7 -0
- package/src/web.js +3 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// NotebookEdit — read and edit a Jupyter notebook by CELL.
|
|
2
|
+
//
|
|
3
|
+
// WHY A TOOL AND NOT A BRANCH IN `Edit`
|
|
4
|
+
// ------------------------------------
|
|
5
|
+
// `Edit`'s contract is "replace this text with that text", and a notebook has no text to
|
|
6
|
+
// match: the source lives inside a JSON array, so a `old_string` would match in one place in
|
|
7
|
+
// the .ipynb and mean something different in the cell. Expressing the operation as CELL
|
|
8
|
+
// indices removes the ambiguity entirely — there is nothing to match, so nothing to match
|
|
9
|
+
// wrongly.
|
|
10
|
+
//
|
|
11
|
+
// `tools/edit.js` still refuses a .ipynb, and now says to use THIS instead. That refusal is
|
|
12
|
+
// the safety property: a notebook can only be changed through an operation that knows its
|
|
13
|
+
// structure, so the corruption the refusal prevented remains impossible.
|
|
14
|
+
//
|
|
15
|
+
// Everything outside the edited `source` is preserved: outputs, metadata, cell ids, the
|
|
16
|
+
// top-level metadata. See src/notebook.js for why that is an invariant rather than a nicety.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
loadNotebook, listCells, describeCells, replaceSource, insertCell,
|
|
20
|
+
deleteCell, checkIndex, cellSource, saveNotebook,
|
|
21
|
+
} from '../notebook.js';
|
|
22
|
+
import { resolvePath } from './utils.js';
|
|
23
|
+
import { checkpointBeforeWrite } from './utils.js';
|
|
24
|
+
|
|
25
|
+
export const spec = {
|
|
26
|
+
name: 'NotebookEdit',
|
|
27
|
+
description: `Read and change a Jupyter notebook (.ipynb) by CELL. Plain Edit refuses .ipynb because a notebook is JSON — use this instead.
|
|
28
|
+
|
|
29
|
+
Actions:
|
|
30
|
+
- \`list\`: every cell with its index, type, line count and a one-line preview. Do this FIRST unless you already know the indices.
|
|
31
|
+
- \`read\`: one cell's source in full.
|
|
32
|
+
- \`replace\`: replace one cell's source. Everything else about the cell (its outputs, metadata, id) is preserved.
|
|
33
|
+
- \`insert\`: add a cell BEFORE an index; use index = cell count to append.
|
|
34
|
+
- \`delete\`: remove one cell.
|
|
35
|
+
|
|
36
|
+
Indices are 0-based and refer to the list \`list\` prints. Recent output is kept when a cell's source is replaced, exactly as Jupyter does when you edit without re-running.`,
|
|
37
|
+
parameters: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: 'string', description: 'Path to the .ipynb file.' },
|
|
41
|
+
action: { type: 'string', enum: ['list', 'read', 'replace', 'insert', 'delete'], description: 'What to do.' },
|
|
42
|
+
index: { type: 'integer', description: '0-based cell index. Required for read/replace/insert/delete. For insert, the new cell goes BEFORE this index (cell count to append).' },
|
|
43
|
+
source: { type: 'string', description: 'The cell source. Required for replace and insert.' },
|
|
44
|
+
cell_type: { type: 'string', enum: ['code', 'markdown', 'raw'], description: 'Type for an inserted cell; defaults to code.' },
|
|
45
|
+
},
|
|
46
|
+
required: ['path', 'action'],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async execute(args, ctx) {
|
|
50
|
+
if (typeof args.path !== 'string' || !args.path.trim()) return 'Error: `path` is required.';
|
|
51
|
+
const action = String(args.action || '').toLowerCase();
|
|
52
|
+
if (!['list', 'read', 'replace', 'insert', 'delete'].includes(action)) {
|
|
53
|
+
return `Error: \`action\` must be one of list, read, replace, insert, delete (got ${JSON.stringify(args.action)}).`;
|
|
54
|
+
}
|
|
55
|
+
let abs;
|
|
56
|
+
try { abs = resolvePath(args.path, ctx); } catch (e) { return e.message; }
|
|
57
|
+
if (!String(abs).toLowerCase().endsWith('.ipynb')) {
|
|
58
|
+
return `Error: ${args.path} is not a .ipynb file. Use Edit for ordinary text files.`;
|
|
59
|
+
}
|
|
60
|
+
const loaded = loadNotebook(abs);
|
|
61
|
+
if (!loaded.ok) return `Error: ${loaded.error}`;
|
|
62
|
+
|
|
63
|
+
if (action === 'list') {
|
|
64
|
+
return describeCells(loaded.doc, args.path).join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const idx = checkIndex(loaded.doc, args.index);
|
|
68
|
+
if (!idx.ok) return `Error: ${idx.error}`;
|
|
69
|
+
|
|
70
|
+
if (action === 'read') {
|
|
71
|
+
const cell = loaded.doc.cells[args.index];
|
|
72
|
+
const type = cell.cell_type || 'unknown';
|
|
73
|
+
const src = cellSource(cell);
|
|
74
|
+
return `cell ${args.index} (${type}, ${src ? src.split('\n').length : 0} line(s)):\n\n${src}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (action === 'replace') {
|
|
78
|
+
if (typeof args.source !== 'string') return 'Error: `source` is required for replace.';
|
|
79
|
+
const before = cellSource(loaded.doc.cells[args.index]);
|
|
80
|
+
const r = replaceSource(loaded.doc, args.index, args.source);
|
|
81
|
+
if (!r.ok) return `Error: ${r.error}`;
|
|
82
|
+
// A checkpoint BEFORE the write, like Edit does, so /checkpoints and /undo can put
|
|
83
|
+
// the previous cell content back.
|
|
84
|
+
try { checkpointBeforeWrite(abs, ctx); } catch { /* best effort */ }
|
|
85
|
+
const saved = saveNotebook(abs, loaded.doc, loaded.indent);
|
|
86
|
+
if (!saved.ok) return `Error: saving ${args.path}: ${saved.error}`;
|
|
87
|
+
const beforeLines = before ? before.split('\n').length : 0;
|
|
88
|
+
const afterLines = args.source ? args.source.split('\n').length : 0;
|
|
89
|
+
return `Replaced the source of ${args.path} cell ${args.index} (${beforeLines} -> ${afterLines} line(s)). The cell's outputs and metadata are unchanged.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (action === 'insert') {
|
|
93
|
+
if (typeof args.source !== 'string') return 'Error: `source` is required for insert.';
|
|
94
|
+
const r = insertCell(loaded.doc, args.index, args.source, args.cell_type);
|
|
95
|
+
if (!r.ok) return `Error: ${r.error}`;
|
|
96
|
+
try { checkpointBeforeWrite(abs, ctx); } catch { /* best effort */ }
|
|
97
|
+
const saved = saveNotebook(abs, loaded.doc, loaded.indent);
|
|
98
|
+
if (!saved.ok) return `Error: saving ${args.path}: ${saved.error}`;
|
|
99
|
+
return `Inserted a ${r.cell.cell_type} cell at index ${args.index} in ${args.path}. The notebook now has ${loaded.doc.cells.length} cell(s).`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// delete
|
|
103
|
+
const r = deleteCell(loaded.doc, args.index);
|
|
104
|
+
if (!r.ok) return `Error: ${r.error}`;
|
|
105
|
+
try { checkpointBeforeWrite(abs, ctx); } catch { /* best effort */ }
|
|
106
|
+
const saved = saveNotebook(abs, loaded.doc, loaded.indent);
|
|
107
|
+
if (!saved.ok) return `Error: saving ${args.path}: ${saved.error}`;
|
|
108
|
+
return `Deleted cell ${args.index} from ${args.path}. The notebook now has ${loaded.doc.cells.length} cell(s).`;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Exported for tests: the cell list without touching the filesystem. */
|
|
113
|
+
export { listCells };
|
|
@@ -52,8 +52,12 @@ export const spec = {
|
|
|
52
52
|
let p;
|
|
53
53
|
try { p = resolvePath(args.path, ctx); } catch (e) { return e.message; }
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Async fs throughout. These calls used to be sync, and an image may be tens
|
|
56
|
+
// of MB — reading it (plus the base64 encode) blocked the event loop long
|
|
57
|
+
// enough to freeze the TUI while the Working spinner was animating.
|
|
58
|
+
let stat;
|
|
59
|
+
try { stat = await fs.promises.stat(p); }
|
|
60
|
+
catch { return `Error: file not found: ${args.path}`; }
|
|
57
61
|
if (!stat.isFile()) return `Error: not a file: ${args.path}`;
|
|
58
62
|
|
|
59
63
|
const ext = path.extname(p).toLowerCase();
|
|
@@ -67,7 +71,10 @@ export const spec = {
|
|
|
67
71
|
+ 'Resize it, or raise the limit with HNCODE_MEDIA_MAX_BYTES.';
|
|
68
72
|
}
|
|
69
73
|
const mimeType = IMAGE_MIME[ext];
|
|
70
|
-
|
|
74
|
+
let buf;
|
|
75
|
+
try { buf = await fs.promises.readFile(p); }
|
|
76
|
+
catch (e) { return `Error: could not read ${args.path}: ${e.message}`; }
|
|
77
|
+
const data = buf.toString('base64');
|
|
71
78
|
const approxKB = Math.round(data.length / 1024);
|
|
72
79
|
return {
|
|
73
80
|
text: `[image] ${args.path} (${sizeKB}KB, ${mimeType})`,
|
package/src/tools/shell.js
CHANGED
|
@@ -15,10 +15,26 @@
|
|
|
15
15
|
// on which one was found.
|
|
16
16
|
|
|
17
17
|
import cp from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
18
19
|
|
|
19
20
|
// The two PowerShell families, by the name they are invoked as.
|
|
20
21
|
export const SHELL_CANDIDATES = ['pwsh', 'powershell'];
|
|
21
22
|
|
|
23
|
+
// Last-resort Windows fallback. A Windows box always has cmd.exe (%COMSPEC%),
|
|
24
|
+
// so when neither PowerShell exists the Bash tool must still run SOMETHING
|
|
25
|
+
// rather than failing outright. `cmd` is not a PowerShell, so the syntax
|
|
26
|
+
// guidance and the spawn args differ (see cmdSpawnArgs).
|
|
27
|
+
export function findCmdExe() {
|
|
28
|
+
const fromEnv = process.env.COMSPEC || process.env.ComSpec;
|
|
29
|
+
if (fromEnv) {
|
|
30
|
+
try { if (fs.existsSync(fromEnv)) return fromEnv; } catch { /* fall through to the PATH probe */ }
|
|
31
|
+
}
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
return whichSync('cmd') || whichSync('cmd.exe') || 'cmd.exe';
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
39
|
* Resolve an executable name through PATH without spawning it.
|
|
24
40
|
*
|
|
@@ -42,13 +58,15 @@ function whichSync(name) {
|
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
/**
|
|
45
|
-
* Find a usable
|
|
46
|
-
* Returns { path, name, edition } or null
|
|
61
|
+
* Find a usable shell: pwsh, then the built-in powershell, then (on Windows)
|
|
62
|
+
* cmd.exe as a last resort. Returns { path, name, edition } or null.
|
|
47
63
|
*
|
|
48
64
|
* `edition` drives syntax guidance — see the header. It is inferred from the NAME
|
|
49
65
|
* rather than by running the binary (a spawn per probe is not worth it, and
|
|
50
66
|
* PowerShell 6, also named `pwsh`, is EOL and not a realistic target, so `pwsh`
|
|
51
|
-
* safely implies 7+).
|
|
67
|
+
* safely implies 7+). `edition: 'cmd'` means neither PowerShell exists and the
|
|
68
|
+
* Bash tool is running through cmd.exe: it still works, but the command syntax is
|
|
69
|
+
* cmd's, not PowerShell's.
|
|
52
70
|
*/
|
|
53
71
|
export function detectShell() {
|
|
54
72
|
for (const name of SHELL_CANDIDATES) {
|
|
@@ -57,6 +75,9 @@ export function detectShell() {
|
|
|
57
75
|
return { path, name, edition: name === 'pwsh' ? 'core' : 'desktop' };
|
|
58
76
|
}
|
|
59
77
|
}
|
|
78
|
+
// No PowerShell at all: on Windows, guarantee cmd.exe so the tool is not dead.
|
|
79
|
+
const cmd = findCmdExe();
|
|
80
|
+
if (cmd) return { path: cmd, name: 'cmd', edition: 'cmd' };
|
|
60
81
|
return null;
|
|
61
82
|
}
|
|
62
83
|
|
|
@@ -91,16 +112,23 @@ export function resetShellCache() {
|
|
|
91
112
|
* on every single tool call, and `-NonInteractive` stops a command that prompts
|
|
92
113
|
* from hanging the tool forever with no way to answer.
|
|
93
114
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
115
|
+
* When neither PowerShell exists, `edition === 'cmd'` selects cmd.exe args instead
|
|
116
|
+
* (see detectShell). Throws a message naming the fix only when no shell at all
|
|
117
|
+
* was found, instead of letting the caller hit a bare ENOENT.
|
|
96
118
|
*/
|
|
97
119
|
export function shellSpawnArgs(command) {
|
|
98
120
|
const shell = getShell();
|
|
99
121
|
if (!shell) {
|
|
100
122
|
throw new Error(
|
|
101
|
-
'No
|
|
123
|
+
'No shell found (tried pwsh, powershell, and cmd.exe). '
|
|
102
124
|
+ 'Install PowerShell 7+ or add it to PATH, then retry.',
|
|
103
125
|
);
|
|
104
126
|
}
|
|
127
|
+
if (shell.edition === 'cmd') {
|
|
128
|
+
// cmd.exe: `/d` skips AutoRun commands, `/s` makes `/c` quoting behave
|
|
129
|
+
// predictably. The command is passed as ONE argv element, so Node does the
|
|
130
|
+
// quoting and we do not wrap it ourselves.
|
|
131
|
+
return { bin: shell.path, args: ['/d', '/s', '/c', command], shell };
|
|
132
|
+
}
|
|
105
133
|
return { bin: shell.path, args: ['-NoProfile', '-NonInteractive', '-Command', command], shell };
|
|
106
134
|
}
|