@hncode/hncode 0.5.2 → 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.
- package/package.json +2 -2
- package/src/agent.js +172 -19
- package/src/cache.js +175 -130
- package/src/clipboard.js +69 -2
- package/src/index.js +29 -5
- package/src/keybindings.js +25 -3
- package/src/llm.js +54 -6
- package/src/mcp.js +419 -384
- package/src/plugin.js +95 -10
- package/src/remote.js +307 -289
- package/src/shell-safety.js +610 -562
- package/src/swarm-progress.js +1 -1
- package/src/team/mailbox.js +216 -139
- package/src/team/permission.js +121 -0
- package/src/team/protocol.js +167 -0
- package/src/team/runner.js +127 -12
- package/src/team/spawn.js +63 -5
- package/src/team/store.js +31 -4
- package/src/team/tasks.js +15 -2
- package/src/team/team.js +51 -16
- package/src/team-panel.js +418 -20
- package/src/term-caps.js +457 -410
- package/src/term.js +142 -26
- package/src/tool-result.js +50 -2
- package/src/tools/agent.js +464 -436
- package/src/tools/bash.js +53 -14
- package/src/tools/diagnostics.js +13 -0
- package/src/tools/edit.js +813 -590
- package/src/tools/glob.js +5 -3
- package/src/tools/grep.js +19 -5
- package/src/tools/ignore.js +17 -3
- package/src/tools/index.js +17 -3
- package/src/tools/read.js +2 -1
- package/src/tools/shell.js +145 -134
- package/src/tools/tasks.js +214 -141
- package/src/tools/team.js +13 -0
- package/src/tools/utils.js +12 -3
- package/src/tools/web-search.js +88 -88
- package/src/tui.js +1815 -628
- package/src/update.js +38 -3
package/src/plugin.js
CHANGED
|
@@ -146,14 +146,69 @@ function makeLogger(sink) {
|
|
|
146
146
|
fn.error = (...a) => sink.error(...a);
|
|
147
147
|
return fn;
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Where a plugin's `log`/`warn`/`error` goes.
|
|
151
|
+
*
|
|
152
|
+
* TWO DESTINATIONS, NEVER BOTH AT ONCE.
|
|
153
|
+
*
|
|
154
|
+
* The notice queue is what the TUI drains onto its own transcript, so in the TUI those are the messages the
|
|
155
|
+
* user sees. `console.*` was called ALONGSIDE it, and that is wrong whenever the TUI is running: the TUI
|
|
156
|
+
* owns stdout and paints the whole screen itself, so a raw `console.log` lands wherever the cursor happens
|
|
157
|
+
* to be — mid-frame, on top of the composer, wherever. It showed up most visibly after `/reload`, because
|
|
158
|
+
* that is when every plugin re-runs its load-time logging in one burst.
|
|
159
|
+
*
|
|
160
|
+
* Outside the TUI there is no screen to paint and no queue being drained, so the console IS the right
|
|
161
|
+
* destination — headless runs and tests rely on it.
|
|
162
|
+
*
|
|
163
|
+
* `tuiActive` is set by the TUI once it takes over the screen, and cleared on exit.
|
|
164
|
+
*/
|
|
165
|
+
let tuiActive = false;
|
|
166
|
+
|
|
167
|
+
/** Tell the plugin logger whether the TUI currently owns stdout. */
|
|
168
|
+
export function setPluginLoggerTui(active) {
|
|
169
|
+
tuiActive = !!active;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Route `console.*` to the notice queue for the duration of a function, and restore it afterwards.
|
|
174
|
+
*
|
|
175
|
+
* Plugin code runs inside `import()` and `install(api)`, where `console.log` is the natural thing to write.
|
|
176
|
+
* While the TUI owns stdout those writes land on the painted frame, so they are captured and re-emitted as
|
|
177
|
+
* notices instead — the same channel plugin errors already use.
|
|
178
|
+
*
|
|
179
|
+
* Returns the restore function; it MUST be called (a `finally` in every caller), or every later console
|
|
180
|
+
* write in the process would be swallowed.
|
|
181
|
+
*/
|
|
182
|
+
function capturePluginConsole() {
|
|
183
|
+
const saved = { log: console.log, info: console.info, warn: console.warn, error: console.error };
|
|
184
|
+
const route = (level) => (...a) => notice(level, fmtArgs(a));
|
|
185
|
+
console.log = route('info');
|
|
186
|
+
console.info = route('info');
|
|
187
|
+
console.warn = route('warn');
|
|
188
|
+
console.error = route('error');
|
|
189
|
+
return () => {
|
|
190
|
+
console.log = saved.log;
|
|
191
|
+
console.info = saved.info;
|
|
192
|
+
console.warn = saved.warn;
|
|
193
|
+
console.error = saved.error;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
149
196
|
|
|
150
197
|
export const pluginLogger = makeLogger({
|
|
151
|
-
log: (...a) => { notice('info', fmtArgs(a)); console.log('[hncode-plugin]', ...a); },
|
|
152
|
-
info: (...a) => { notice('info', fmtArgs(a)); console.log('[hncode-plugin]', ...a); },
|
|
153
|
-
warn: (...a) => { notice('warn', fmtArgs(a)); console.warn('[hncode-plugin]', ...a); },
|
|
154
|
-
error: (...a) => { notice('error', fmtArgs(a)); console.error('[hncode-plugin]', ...a); },
|
|
198
|
+
log: (...a) => { notice('info', fmtArgs(a)); if (!tuiActive) console.log('[hncode-plugin]', ...a); },
|
|
199
|
+
info: (...a) => { notice('info', fmtArgs(a)); if (!tuiActive) console.log('[hncode-plugin]', ...a); },
|
|
200
|
+
warn: (...a) => { notice('warn', fmtArgs(a)); if (!tuiActive) console.warn('[hncode-plugin]', ...a); },
|
|
201
|
+
error: (...a) => { notice('error', fmtArgs(a)); if (!tuiActive) console.error('[hncode-plugin]', ...a); },
|
|
155
202
|
});
|
|
156
203
|
|
|
204
|
+
// Registration GENERATION: bumped whenever the tool surface changes (a tool
|
|
205
|
+
// registers or unregisters, plugins unload). The toolbelt's by-name map in
|
|
206
|
+
// tools/index.js keys off this, so a background MCP connect or a /reload is
|
|
207
|
+
// visible to `getTool` immediately instead of serving stale entries forever.
|
|
208
|
+
let _toolGeneration = 0;
|
|
209
|
+
function bumpToolGeneration() { _toolGeneration++; }
|
|
210
|
+
export function pluginToolGeneration() { return _toolGeneration; }
|
|
211
|
+
|
|
157
212
|
const API = {
|
|
158
213
|
// Register a new tool (same shape as built-in tool specs).
|
|
159
214
|
// Returns an unregister function.
|
|
@@ -165,9 +220,11 @@ const API = {
|
|
|
165
220
|
throw new Error(`registerTool: tool "${spec.name}" is already registered`);
|
|
166
221
|
}
|
|
167
222
|
registeredTools.push(spec);
|
|
223
|
+
bumpToolGeneration();
|
|
168
224
|
return track(() => {
|
|
169
225
|
const i = registeredTools.findIndex((t) => t.name === spec.name);
|
|
170
226
|
if (i >= 0) registeredTools.splice(i, 1);
|
|
227
|
+
bumpToolGeneration();
|
|
171
228
|
});
|
|
172
229
|
},
|
|
173
230
|
|
|
@@ -545,6 +602,7 @@ export async function unloadPlugins() {
|
|
|
545
602
|
// keybinds/widgets, which a reload used to leave behind forever.
|
|
546
603
|
registeredTools.length = 0;
|
|
547
604
|
registeredCommands.length = 0;
|
|
605
|
+
bumpToolGeneration();
|
|
548
606
|
for (const k of Object.keys(registeredHooks)) delete registeredHooks[k];
|
|
549
607
|
for (const k of Object.keys(registeredConfig)) delete registeredConfig[k];
|
|
550
608
|
for (const k in registeredPatches) delete registeredPatches[k];
|
|
@@ -568,7 +626,10 @@ export function listPluginServices() { return [...services.keys()]; }
|
|
|
568
626
|
// takes effect without restarting the process — the ESM cache would otherwise hand
|
|
569
627
|
// back the module instance loaded last time. Used by the reload path only.
|
|
570
628
|
export async function loadPlugins(dir, opts = {}) {
|
|
571
|
-
|
|
629
|
+
// Falls back to the user's plugin directory. `PLUGIN_DIR` was referenced here
|
|
630
|
+
// but never defined — any loadPlugins() without a dir (the /reload path when
|
|
631
|
+
// no pluginDir is configured) died with a ReferenceError.
|
|
632
|
+
const pluginDir = dir || pluginsDir();
|
|
572
633
|
if (!fs.existsSync(pluginDir)) return [];
|
|
573
634
|
|
|
574
635
|
// Node decides a .js file's module type from the NEAREST package.json, searching
|
|
@@ -630,7 +691,23 @@ export async function loadPlugins(dir, opts = {}) {
|
|
|
630
691
|
// a hot reload wants. Only on a fresh load: the startup path keeps the plain
|
|
631
692
|
// URL so nothing else can see two instances of a plugin.
|
|
632
693
|
const url = pathToFileURL(full).href + (opts.fresh ? `?v=${++loadSeq}` : '');
|
|
633
|
-
|
|
694
|
+
// A PLUGIN'S OWN console.log MUST NOT REACH THE SCREEN.
|
|
695
|
+
//
|
|
696
|
+
// The import below runs the plugin's top-level code, and `install(api)` runs more of it. Both are
|
|
697
|
+
// plugin author territory, where `console.log` is the first thing anyone reaches for — and while the
|
|
698
|
+
// TUI owns stdout, that write lands wherever the cursor happens to be, on top of the frame. It was
|
|
699
|
+
// most visible after `/reload`, which re-runs every plugin's load-time logging in one burst.
|
|
700
|
+
//
|
|
701
|
+
// Redirected to `pluginLogger`, which queues a notice the TUI already renders in its own transcript.
|
|
702
|
+
// The real console is restored immediately afterwards, so this covers exactly the window where
|
|
703
|
+
// plugin code runs and nothing else.
|
|
704
|
+
const restoreConsole = capturePluginConsole();
|
|
705
|
+
let mod;
|
|
706
|
+
try {
|
|
707
|
+
mod = await import(url);
|
|
708
|
+
} finally {
|
|
709
|
+
restoreConsole();
|
|
710
|
+
}
|
|
634
711
|
// `install(api)` may be the named export or live on the default export.
|
|
635
712
|
let installFn;
|
|
636
713
|
if (mod && typeof mod.install === 'function') installFn = mod.install;
|
|
@@ -704,10 +781,17 @@ if (typeof installFn !== 'function') {
|
|
|
704
781
|
// registrations recorded against this one.
|
|
705
782
|
currentUnit = unit;
|
|
706
783
|
try {
|
|
707
|
-
// AWAITED: an async install that registers after an await would otherwise race
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
|
|
784
|
+
// AWAITED: an async install that registers after an await would otherwise race the next plugin's
|
|
785
|
+
// install, which would break the ordering this function exists to provide.
|
|
786
|
+
//
|
|
787
|
+
// The console is redirected for the duration: `install(api)` is plugin code, and it is where most
|
|
788
|
+
// plugins do their logging.
|
|
789
|
+
const restoreInstallConsole = capturePluginConsole();
|
|
790
|
+
try {
|
|
791
|
+
await c.installFn(API);
|
|
792
|
+
} finally {
|
|
793
|
+
restoreInstallConsole();
|
|
794
|
+
}
|
|
711
795
|
pluginUnits.push(unit);
|
|
712
796
|
// install(api) FIRST, then record it. Pushing before the call reported a plugin as
|
|
713
797
|
// loaded even when its install() threw — /plugins would list it under Loaded while
|
|
@@ -849,6 +933,7 @@ export { registeredConfig as pluginConfigDefaults };
|
|
|
849
933
|
export function _resetPlugins() {
|
|
850
934
|
registeredTools.length = 0;
|
|
851
935
|
registeredCommands.length = 0;
|
|
936
|
+
bumpToolGeneration();
|
|
852
937
|
for (const k of Object.keys(registeredHooks)) delete registeredHooks[k];
|
|
853
938
|
for (const k of Object.keys(registeredConfig)) delete registeredConfig[k];
|
|
854
939
|
loadedPlugins.length = 0;
|