mu-harness 0.16.9 → 0.16.12

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 (33) hide show
  1. package/esm/harness/npm/src/agents/index.d.ts +2 -2
  2. package/esm/harness/npm/src/agents/index.js +1 -1
  3. package/esm/harness/npm/src/agents/parser.js +16 -3
  4. package/esm/harness/npm/src/agents/registry.d.ts +2 -1
  5. package/esm/harness/npm/src/agents/registry.js +47 -12
  6. package/esm/harness/npm/src/agents/types.d.ts +2 -1
  7. package/esm/harness/npm/src/harness/create.js +22 -9
  8. package/esm/harness/npm/src/subAgents/tool.js +45 -17
  9. package/esm/tui/src/components/scroll-view.d.ts +14 -2
  10. package/esm/tui/src/components/scroll-view.js +29 -4
  11. package/esm/tui/src/components/select-list.d.ts +1 -1
  12. package/esm/tui/src/components/select-list.js +4 -2
  13. package/esm/tui/src/index.d.ts +1 -1
  14. package/esm/tui/src/inputRouter.js +7 -1
  15. package/esm/tui/src/keyboard.js +14 -0
  16. package/esm/tui/src/surface.d.ts +1 -1
  17. package/package.json +3 -3
  18. package/script/harness/npm/src/agents/index.d.ts +2 -2
  19. package/script/harness/npm/src/agents/index.js +2 -1
  20. package/script/harness/npm/src/agents/parser.js +16 -3
  21. package/script/harness/npm/src/agents/registry.d.ts +2 -1
  22. package/script/harness/npm/src/agents/registry.js +50 -14
  23. package/script/harness/npm/src/agents/types.d.ts +2 -1
  24. package/script/harness/npm/src/harness/create.js +21 -8
  25. package/script/harness/npm/src/subAgents/tool.js +45 -17
  26. package/script/tui/src/components/scroll-view.d.ts +14 -2
  27. package/script/tui/src/components/scroll-view.js +29 -4
  28. package/script/tui/src/components/select-list.d.ts +1 -1
  29. package/script/tui/src/components/select-list.js +4 -2
  30. package/script/tui/src/index.d.ts +1 -1
  31. package/script/tui/src/inputRouter.js +7 -1
  32. package/script/tui/src/keyboard.js +14 -0
  33. package/script/tui/src/surface.d.ts +1 -1
@@ -1,4 +1,4 @@
1
- export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
1
+ export type { Agent, GrantValue, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,3 +1,3 @@
1
- export { createAgentRegistry, toolDecision, toolNames } from './registry.js';
1
+ export { createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
2
2
  export { parseAgent } from './parser.js';
3
3
  export { loadAgents } from './loader.js';
@@ -1,9 +1,17 @@
1
1
  import { parseFrontmatter, str } from '../common/index.js';
2
2
  const DECISIONS = new Set(['allow', 'ask', 'deny']);
3
+ const isDecision = (value) => typeof value === 'string' && DECISIONS.has(value);
3
4
  const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
4
5
  .filter((entry) => typeof entry === 'string')
5
6
  .map((entry) => entry.trim())
6
7
  .filter(Boolean);
8
+ const parseDecisionMap = (raw) => {
9
+ const out = {};
10
+ for (const [key, value] of Object.entries(raw))
11
+ if (isDecision(value))
12
+ out[key] = value;
13
+ return Object.keys(out).length > 0 ? out : undefined;
14
+ };
7
15
  const parseTools = (raw) => {
8
16
  if (Array.isArray(raw) || typeof raw === 'string') {
9
17
  const list = parseStringList(raw);
@@ -11,9 +19,14 @@ const parseTools = (raw) => {
11
19
  }
12
20
  if (raw && typeof raw === 'object') {
13
21
  const out = {};
14
- for (const [tool, decision] of Object.entries(raw)) {
15
- if (typeof decision === 'string' && DECISIONS.has(decision))
16
- out[tool] = decision;
22
+ for (const [tool, value] of Object.entries(raw)) {
23
+ if (isDecision(value))
24
+ out[tool] = value;
25
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
26
+ const nested = parseDecisionMap(value);
27
+ if (nested)
28
+ out[tool] = nested;
29
+ }
17
30
  }
18
31
  return Object.keys(out).length > 0 ? out : undefined;
19
32
  }
@@ -3,6 +3,7 @@ export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
- export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
6
+ export declare const grantArg: (tool: string, input: unknown) => string | undefined;
7
+ export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
7
8
  export declare const toolNames: (agent: Agent) => string[] | undefined;
8
9
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,24 +1,59 @@
1
- const asMap = (tools) => {
2
- if (!tools)
1
+ import { matchesGlob } from 'node:path';
2
+ const asMap = (grants) => {
3
+ if (!grants)
3
4
  return undefined;
4
- if (Array.isArray(tools))
5
- return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
6
- return tools;
5
+ if (Array.isArray(grants))
6
+ return Object.fromEntries(grants.map((name) => [name, 'allow']));
7
+ return grants;
7
8
  };
8
- export const toolDecision = (agent, tool) => {
9
- const map = asMap(agent.tools);
9
+ const matchKey = (keys, name) => {
10
+ if (keys.includes(name))
11
+ return name;
12
+ let glob;
13
+ for (const key of keys) {
14
+ if (key === '*' || key === name)
15
+ continue;
16
+ if (matchesGlob(name, key) && (glob === undefined || key.length > glob.length))
17
+ glob = key;
18
+ }
19
+ if (glob !== undefined)
20
+ return glob;
21
+ return keys.includes('*') ? '*' : undefined;
22
+ };
23
+ const resolveGrant = (grants, tool, arg) => {
24
+ const map = asMap(grants);
10
25
  if (!map)
11
26
  return 'allow';
12
- return map[tool] ?? map['*'] ?? 'deny';
27
+ const key = matchKey(Object.keys(map), tool);
28
+ if (key === undefined)
29
+ return 'deny';
30
+ const value = map[key];
31
+ if (typeof value === 'string')
32
+ return value;
33
+ if (arg === undefined)
34
+ return 'allow';
35
+ const inner = matchKey(Object.keys(value), arg);
36
+ return inner === undefined ? 'deny' : value[inner];
13
37
  };
14
- export const toolNames = (agent) => {
15
- const map = asMap(agent.tools);
38
+ const grantNames = (grants) => {
39
+ const map = asMap(grants);
16
40
  if (!map)
17
41
  return undefined;
18
- if (map['*'] && map['*'] !== 'deny')
42
+ const wildcard = map['*'];
43
+ if (wildcard !== undefined && wildcard !== 'deny')
19
44
  return ['*'];
20
- return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
45
+ return Object.entries(map).filter(([, value]) => value !== 'deny').map(([name]) => name);
46
+ };
47
+ const GRANT_ARG = { skill: 'name', bash: 'command' };
48
+ export const grantArg = (tool, input) => {
49
+ const field = GRANT_ARG[tool];
50
+ if (!field || typeof input !== 'object' || input === null)
51
+ return undefined;
52
+ const value = input[field];
53
+ return typeof value === 'string' ? value : undefined;
21
54
  };
55
+ export const toolDecision = (agent, tool, arg) => resolveGrant(agent.tools, tool, arg);
56
+ export const toolNames = (agent) => grantNames(agent.tools);
22
57
  const merge = (base, child) => ({
23
58
  name: child.name,
24
59
  description: child.description || base.description,
@@ -1,5 +1,6 @@
1
1
  export type ToolDecision = 'allow' | 'ask' | 'deny';
2
- export type ToolGrants = string[] | Record<string, ToolDecision>;
2
+ export type GrantValue = ToolDecision | Record<string, ToolDecision>;
3
+ export type ToolGrants = string[] | Record<string, GrantValue>;
3
4
  export interface Agent {
4
5
  name: string;
5
6
  description: string;
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import process from 'node:process';
3
- import { createAgentRegistry, loadAgents, toolDecision, toolNames } from '../agents/index.js';
3
+ import { createAgentRegistry, grantArg, loadAgents, toolDecision, toolNames } from '../agents/index.js';
4
4
  import { createAgentsCommand, createCommandRegistry, createHelpCommand, createSessionsCommand, createSkillsCommand, } from '../commands/index.js';
5
5
  import { createHarnessConfig } from '../config/index.js';
6
6
  import { mergeHooks } from '../hooks/index.js';
@@ -33,22 +33,35 @@ export const createHarness = async (options) => {
33
33
  const cwdSkills = await loadSkills(cwdSkillsDir);
34
34
  const diskSkills = await loadSkills(skillsDir);
35
35
  const skills = createSkillRegistry([...hostSkills, ...pluginSkills, ...cwdSkills, ...diskSkills]);
36
- const skillTools = [
37
- createSkillTool(skills),
38
- createSkillWriterTool({ dirs: { local: cwdSkillsDir, config: skillsDir }, registry: skills }),
39
- ];
36
+ const skillWriterTool = createSkillWriterTool({
37
+ dirs: { local: cwdSkillsDir, config: skillsDir },
38
+ registry: skills,
39
+ });
40
+ const scopeSkills = (agent) => {
41
+ if (!agent)
42
+ return skills;
43
+ return skills.select(skills.list().map((s) => s.name).filter((name) => toolDecision(agent, 'skill', name) !== 'deny'));
44
+ };
40
45
  const tasks = enableScheduler ? createTaskStore({ dir: join(config.configDir, 'tasks') }) : undefined;
41
46
  let schedulerTools = [];
42
47
  const runs = createSubAgentRegistry();
43
48
  const store = createSessionStore({ dir: join(config.dataDir, 'sessions') });
44
- const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
49
+ const sessionTools = (agent, extra = []) => [
50
+ ...(sessionDefaults.tools ?? []),
51
+ ...extra,
52
+ createSkillTool(scopeSkills(agent)),
53
+ skillWriterTool,
54
+ ...schedulerTools,
55
+ ];
45
56
  const approvalHook = (getAgent) => approvals
46
57
  ? approvals.manager.hooksFor({
47
58
  decide: (call) => {
48
59
  const agent = getAgent();
49
60
  if (!agent)
50
61
  return 'allow';
51
- return approvals.decide ? approvals.decide(agent, call) : toolDecision(agent, call.name);
62
+ if (approvals.decide)
63
+ return approvals.decide(agent, call);
64
+ return toolDecision(agent, call.name, grantArg(call.name, call.input));
52
65
  },
53
66
  agent: () => getAgent()?.name,
54
67
  })
@@ -62,7 +75,7 @@ export const createHarness = async (options) => {
62
75
  id: newId(),
63
76
  });
64
77
  const spawn = (agent) => persistTo(store, persona(agent, {
65
- tools: sessionTools(),
78
+ tools: sessionTools(agent),
66
79
  hooks: mergeHooks([sessionDefaults.hooks, allowList(toolNames(agent)), approvalHook(() => agent)]),
67
80
  }));
68
81
  const scheduler = enableScheduler && tasks
@@ -112,7 +125,7 @@ export const createHarness = async (options) => {
112
125
  revive: ({ id, model: ref, messages }) => createAgentSession({
113
126
  ...sessionDefaults,
114
127
  hooks: mergeHooks([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
115
- tools: sessionTools([createSubAgentTool({ registry: agents, spawn, runs, parentId: id })]),
128
+ tools: sessionTools(undefined, [createSubAgentTool({ registry: agents, spawn, runs, parentId: id })]),
116
129
  ...models.resolve(ref),
117
130
  id,
118
131
  messages,
@@ -1,5 +1,5 @@
1
1
  import { runSubAgent } from './runner.js';
2
- const BASE_PROMPT = 'Delegate an isolated, fully-specifiable task to a named sub-agent instead of doing it inline; it returns only its final answer. Pick the matching sub-agent, brief it from scratch, then verify its answer before relying on it.';
2
+ const BASE_PROMPT = 'Delegate isolated, fully-specifiable tasks to named sub-agents instead of doing them inline; each returns only its final answer. List several tasks in one call to run them concurrently — put all independent work in a single call rather than one at a time. Brief each sub-agent from scratch, then verify its answer before relying on it.';
3
3
  export const createSubAgentTool = (deps) => {
4
4
  const roster = deps.registry.list()
5
5
  .filter((agent) => agent.name !== 'title')
@@ -11,26 +11,54 @@ export const createSubAgentTool = (deps) => {
11
11
  parameters: {
12
12
  type: 'object',
13
13
  properties: {
14
- agent: { type: 'string', description: 'Sub-agent name.' },
15
- task: { type: 'string', description: 'The task to delegate.' },
14
+ tasks: {
15
+ type: 'array',
16
+ description: 'One or more tasks to delegate; multiple entries run in parallel.',
17
+ minItems: 1,
18
+ items: {
19
+ type: 'object',
20
+ properties: {
21
+ agent: { type: 'string', description: 'Sub-agent name.' },
22
+ task: { type: 'string', description: 'The task to delegate.' },
23
+ },
24
+ required: ['agent', 'task'],
25
+ additionalProperties: false,
26
+ },
27
+ },
16
28
  },
17
- required: ['agent', 'task'],
29
+ required: ['tasks'],
18
30
  additionalProperties: false,
19
31
  },
20
32
  run: async (input, ctx) => {
21
- const { agent, task } = (input ?? {});
22
- if (!agent || !task)
23
- return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
24
- const def = deps.registry.get(agent);
25
- if (!def)
26
- return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
27
- const result = await runSubAgent(def, task, {
28
- spawn: deps.spawn,
29
- runs: deps.runs,
30
- parentId: deps.parentId,
31
- signal: ctx.signal,
32
- });
33
- return [{ type: 'text', text: result.text }];
33
+ const { tasks } = (input ?? {});
34
+ if (!Array.isArray(tasks) || tasks.length === 0) {
35
+ return [{ type: 'text', text: 'Error: subagent requires a non-empty `tasks` array.' }];
36
+ }
37
+ const results = await Promise.all(tasks.map(({ agent, task }) => runOne(agent, task, deps, ctx.signal)));
38
+ const labeled = results.length > 1;
39
+ return results.map(({ agent, text }) => ({
40
+ type: 'text',
41
+ text: labeled ? `[${agent ?? 'unknown'}]\n${text}` : text,
42
+ }));
34
43
  },
35
44
  };
36
45
  };
46
+ const runOne = async (agent, task, deps, signal) => {
47
+ if (!agent || !task)
48
+ return { agent, text: 'Error: each task requires `agent` and `task`.' };
49
+ const def = deps.registry.get(agent);
50
+ if (!def)
51
+ return { agent, text: `Error: unknown sub-agent "${agent}".` };
52
+ try {
53
+ const result = await runSubAgent(def, task, {
54
+ spawn: deps.spawn,
55
+ runs: deps.runs,
56
+ parentId: deps.parentId,
57
+ signal,
58
+ });
59
+ return { agent, text: result.text };
60
+ }
61
+ catch (err) {
62
+ return { agent, text: `Error: ${err instanceof Error ? err.message : String(err)}` };
63
+ }
64
+ };
@@ -1,14 +1,26 @@
1
1
  import type { InputEvent } from '../events';
2
2
  import type { Component, Surface } from '../surface';
3
+ export interface ScrollViewOptions {
4
+ wheelStep?: number;
5
+ stickyHeader?: (info: {
6
+ scrollY: number;
7
+ width: number;
8
+ }) => Component | undefined;
9
+ footer?: () => Component | undefined;
10
+ }
3
11
  export declare class ScrollView implements Component {
4
12
  private content;
5
13
  private scrollY;
6
14
  private stick;
7
- constructor(content: Component);
15
+ private readonly wheelStep;
16
+ private readonly stickyHeader?;
17
+ private readonly footer?;
18
+ constructor(content: Component, opts?: ScrollViewOptions);
19
+ atBottom(): boolean;
8
20
  setContent(content: Component): void;
9
21
  scrollToBottom(): void;
10
22
  render(s: Surface): void;
11
23
  handleInput(event: InputEvent): void;
12
24
  private scrollBy;
13
25
  }
14
- export declare const scrollView: (content: Component) => ScrollView;
26
+ export declare const scrollView: (content: Component, opts?: ScrollViewOptions) => ScrollView;
@@ -2,8 +2,17 @@ export class ScrollView {
2
2
  content;
3
3
  scrollY = 0;
4
4
  stick = true;
5
- constructor(content) {
5
+ wheelStep;
6
+ stickyHeader;
7
+ footer;
8
+ constructor(content, opts = {}) {
6
9
  this.content = content;
10
+ this.wheelStep = Math.max(1, opts.wheelStep ?? 3);
11
+ this.stickyHeader = opts.stickyHeader;
12
+ this.footer = opts.footer;
13
+ }
14
+ atBottom() {
15
+ return this.stick;
7
16
  }
8
17
  setContent(content) {
9
18
  this.content = content;
@@ -21,13 +30,29 @@ export class ScrollView {
21
30
  }
22
31
  const scroll = Number.isFinite(s.height) ? this.scrollY : 0;
23
32
  s.child(this.content, { x: 0, y: -scroll, width: s.width, height: contentHeight });
33
+ if (this.stickyHeader && Number.isFinite(s.height) && !this.stick) {
34
+ const header = this.stickyHeader({ scrollY: this.scrollY, width: s.width });
35
+ if (header) {
36
+ const h = Math.min(s.measure(header, s.width), s.height);
37
+ if (h > 0)
38
+ s.child(header, { x: 0, y: 0, width: s.width, height: h });
39
+ }
40
+ }
41
+ if (this.footer && Number.isFinite(s.height) && !this.stick) {
42
+ const footer = this.footer();
43
+ if (footer) {
44
+ const h = Math.min(s.measure(footer, s.width), s.height);
45
+ if (h > 0)
46
+ s.child(footer, { x: 0, y: s.height - h, width: s.width, height: h });
47
+ }
48
+ }
24
49
  }
25
50
  handleInput(event) {
26
51
  if (event.type === 'mouse' && event.kind === 'wheel') {
27
52
  if (event.button === 'wheelUp')
28
- this.scrollBy(-1);
53
+ this.scrollBy(-this.wheelStep);
29
54
  else if (event.button === 'wheelDown')
30
- this.scrollBy(1);
55
+ this.scrollBy(this.wheelStep);
31
56
  return;
32
57
  }
33
58
  if (event.type !== 'key' || event.kind === 'release')
@@ -42,4 +67,4 @@ export class ScrollView {
42
67
  this.scrollY = Math.max(0, this.scrollY + delta);
43
68
  }
44
69
  }
45
- export const scrollView = (content) => new ScrollView(content);
70
+ export const scrollView = (content, opts) => new ScrollView(content, opts);
@@ -17,7 +17,7 @@ export declare class SelectList<T> implements Component {
17
17
  selectedItem(): SelectItem<T> | undefined;
18
18
  move(delta: number): void;
19
19
  render(s: Surface): void;
20
- handleInput(event: InputEvent): void;
20
+ handleInput(event: InputEvent): boolean | void;
21
21
  }
22
22
  export declare const selectList: <T>(items?: SelectItem<T>[], opts?: {
23
23
  maxRows?: number;
@@ -45,13 +45,15 @@ export class SelectList {
45
45
  }
46
46
  }
47
47
  handleInput(event) {
48
- if (event.type === 'mouse' && event.kind === 'press' && event.button === 'left') {
48
+ if (event.type === 'mouse') {
49
+ if (event.kind !== 'press' || event.button !== 'left')
50
+ return false;
49
51
  const index = this.top + (event.localY ?? 0);
50
52
  if (index >= 0 && index < this.items.length) {
51
53
  this.selected = index;
52
54
  this.onSelect?.(this.items[index]);
53
55
  }
54
- return;
56
+ return true;
55
57
  }
56
58
  if (event.type !== 'key' || event.kind === 'release')
57
59
  return;
@@ -2,7 +2,7 @@ export type { Component, Surface, SurfaceEntry } from './surface';
2
2
  export { measure, measureWidth, renderToBuffer } from './surface';
3
3
  export { box, type BoxOptions, column, flex, type FlexItem, modal, type ModalOptions, overlay, type OverlayOptions, row, text, toast, type ToastKind, type ToastOptions, } from './views';
4
4
  export { Editor, editor, type EditorOptions } from './components/editor';
5
- export { ScrollView, scrollView } from './components/scroll-view';
5
+ export { ScrollView, type ScrollViewOptions, scrollView } from './components/scroll-view';
6
6
  export { type SelectItem, SelectList, selectList } from './components/select-list';
7
7
  export { type Command, CommandPalette, commandPalette, type CommandPaletteOptions } from './components/command-palette';
8
8
  export { type LayerHandle, type ToastHandle, TUI, type TuiOptions } from './tui';
@@ -98,7 +98,13 @@ export class InputRouter {
98
98
  continue;
99
99
  if (!containsPoint(entry.rect, event.x, event.y))
100
100
  continue;
101
- entry.component.handleInput({ ...event, localX: event.x - entry.rect.x, localY: event.y - entry.rect.y });
101
+ const handled = entry.component.handleInput({
102
+ ...event,
103
+ localX: event.x - entry.rect.x,
104
+ localY: event.y - entry.rect.y,
105
+ });
106
+ if (handled === false)
107
+ continue;
102
108
  this.host.requestRender();
103
109
  return;
104
110
  }
@@ -257,6 +257,20 @@ function codepointToKey(code) {
257
257
  13: 'enter',
258
258
  27: 'escape',
259
259
  127: 'backspace',
260
+ 57344: 'escape',
261
+ 57345: 'enter',
262
+ 57346: 'tab',
263
+ 57347: 'backspace',
264
+ 57348: 'insert',
265
+ 57349: 'delete',
266
+ 57350: 'left',
267
+ 57351: 'right',
268
+ 57352: 'up',
269
+ 57353: 'down',
270
+ 57354: 'pageUp',
271
+ 57355: 'pageDown',
272
+ 57356: 'home',
273
+ 57357: 'end',
260
274
  };
261
275
  return named[code] ?? codepointToText(code) ?? `unknown:${code}`;
262
276
  }
@@ -3,7 +3,7 @@ import { type CellBuffer } from './layout/cellbuffer';
3
3
  import type { Color, Rect } from './layout/types';
4
4
  export interface Component {
5
5
  render(surface: Surface): void;
6
- handleInput?(event: InputEvent): void;
6
+ handleInput?(event: InputEvent): boolean | void;
7
7
  wantsKeyRelease?: boolean;
8
8
  }
9
9
  export interface Surface {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.16.9",
3
+ "version": "0.16.12",
4
4
  "description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
5
5
  "license": "MIT",
6
6
  "main": "./script/index.js",
@@ -16,8 +16,8 @@
16
16
  "dependencies": {
17
17
  "@swc/wasm-typescript": "^1.15.0",
18
18
  "croner": "^9.0.0",
19
- "mu-core": "^0.16.9",
20
- "mu-tui": "^0.16.9"
19
+ "mu-core": "^0.16.12",
20
+ "mu-tui": "^0.16.12"
21
21
  },
22
22
  "_generatedBy": "dnt@dev"
23
23
  }
@@ -1,4 +1,4 @@
1
- export type { Agent, ToolDecision, ToolGrants } from './types.js';
2
- export { type AgentRegistry, createAgentRegistry, toolDecision, toolNames } from './registry.js';
1
+ export type { Agent, GrantValue, ToolDecision, ToolGrants } from './types.js';
2
+ export { type AgentRegistry, createAgentRegistry, grantArg, toolDecision, toolNames } from './registry.js';
3
3
  export { parseAgent } from './parser.js';
4
4
  export { loadAgents } from './loader.js';
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.createAgentRegistry = void 0;
3
+ exports.loadAgents = exports.parseAgent = exports.toolNames = exports.toolDecision = exports.grantArg = exports.createAgentRegistry = void 0;
4
4
  var registry_js_1 = require("./registry.js");
5
5
  Object.defineProperty(exports, "createAgentRegistry", { enumerable: true, get: function () { return registry_js_1.createAgentRegistry; } });
6
+ Object.defineProperty(exports, "grantArg", { enumerable: true, get: function () { return registry_js_1.grantArg; } });
6
7
  Object.defineProperty(exports, "toolDecision", { enumerable: true, get: function () { return registry_js_1.toolDecision; } });
7
8
  Object.defineProperty(exports, "toolNames", { enumerable: true, get: function () { return registry_js_1.toolNames; } });
8
9
  var parser_js_1 = require("./parser.js");
@@ -3,10 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseAgent = void 0;
4
4
  const index_js_1 = require("../common/index.js");
5
5
  const DECISIONS = new Set(['allow', 'ask', 'deny']);
6
+ const isDecision = (value) => typeof value === 'string' && DECISIONS.has(value);
6
7
  const parseStringList = (raw) => (Array.isArray(raw) ? raw : raw.split(','))
7
8
  .filter((entry) => typeof entry === 'string')
8
9
  .map((entry) => entry.trim())
9
10
  .filter(Boolean);
11
+ const parseDecisionMap = (raw) => {
12
+ const out = {};
13
+ for (const [key, value] of Object.entries(raw))
14
+ if (isDecision(value))
15
+ out[key] = value;
16
+ return Object.keys(out).length > 0 ? out : undefined;
17
+ };
10
18
  const parseTools = (raw) => {
11
19
  if (Array.isArray(raw) || typeof raw === 'string') {
12
20
  const list = parseStringList(raw);
@@ -14,9 +22,14 @@ const parseTools = (raw) => {
14
22
  }
15
23
  if (raw && typeof raw === 'object') {
16
24
  const out = {};
17
- for (const [tool, decision] of Object.entries(raw)) {
18
- if (typeof decision === 'string' && DECISIONS.has(decision))
19
- out[tool] = decision;
25
+ for (const [tool, value] of Object.entries(raw)) {
26
+ if (isDecision(value))
27
+ out[tool] = value;
28
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
29
+ const nested = parseDecisionMap(value);
30
+ if (nested)
31
+ out[tool] = nested;
32
+ }
20
33
  }
21
34
  return Object.keys(out).length > 0 ? out : undefined;
22
35
  }
@@ -3,6 +3,7 @@ export interface AgentRegistry {
3
3
  list(): Agent[];
4
4
  get(name: string): Agent | undefined;
5
5
  }
6
- export declare const toolDecision: (agent: Agent, tool: string) => ToolDecision;
6
+ export declare const grantArg: (tool: string, input: unknown) => string | undefined;
7
+ export declare const toolDecision: (agent: Agent, tool: string, arg?: string) => ToolDecision;
7
8
  export declare const toolNames: (agent: Agent) => string[] | undefined;
8
9
  export declare const createAgentRegistry: (agents?: Agent[]) => AgentRegistry;
@@ -1,28 +1,64 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAgentRegistry = exports.toolNames = exports.toolDecision = void 0;
4
- const asMap = (tools) => {
5
- if (!tools)
3
+ exports.createAgentRegistry = exports.toolNames = exports.toolDecision = exports.grantArg = void 0;
4
+ const node_path_1 = require("node:path");
5
+ const asMap = (grants) => {
6
+ if (!grants)
6
7
  return undefined;
7
- if (Array.isArray(tools))
8
- return Object.fromEntries(tools.map((tool) => [tool, 'allow']));
9
- return tools;
8
+ if (Array.isArray(grants))
9
+ return Object.fromEntries(grants.map((name) => [name, 'allow']));
10
+ return grants;
10
11
  };
11
- const toolDecision = (agent, tool) => {
12
- const map = asMap(agent.tools);
12
+ const matchKey = (keys, name) => {
13
+ if (keys.includes(name))
14
+ return name;
15
+ let glob;
16
+ for (const key of keys) {
17
+ if (key === '*' || key === name)
18
+ continue;
19
+ if ((0, node_path_1.matchesGlob)(name, key) && (glob === undefined || key.length > glob.length))
20
+ glob = key;
21
+ }
22
+ if (glob !== undefined)
23
+ return glob;
24
+ return keys.includes('*') ? '*' : undefined;
25
+ };
26
+ const resolveGrant = (grants, tool, arg) => {
27
+ const map = asMap(grants);
13
28
  if (!map)
14
29
  return 'allow';
15
- return map[tool] ?? map['*'] ?? 'deny';
30
+ const key = matchKey(Object.keys(map), tool);
31
+ if (key === undefined)
32
+ return 'deny';
33
+ const value = map[key];
34
+ if (typeof value === 'string')
35
+ return value;
36
+ if (arg === undefined)
37
+ return 'allow';
38
+ const inner = matchKey(Object.keys(value), arg);
39
+ return inner === undefined ? 'deny' : value[inner];
16
40
  };
17
- exports.toolDecision = toolDecision;
18
- const toolNames = (agent) => {
19
- const map = asMap(agent.tools);
41
+ const grantNames = (grants) => {
42
+ const map = asMap(grants);
20
43
  if (!map)
21
44
  return undefined;
22
- if (map['*'] && map['*'] !== 'deny')
45
+ const wildcard = map['*'];
46
+ if (wildcard !== undefined && wildcard !== 'deny')
23
47
  return ['*'];
24
- return Object.entries(map).filter(([, decision]) => decision !== 'deny').map(([tool]) => tool);
48
+ return Object.entries(map).filter(([, value]) => value !== 'deny').map(([name]) => name);
25
49
  };
50
+ const GRANT_ARG = { skill: 'name', bash: 'command' };
51
+ const grantArg = (tool, input) => {
52
+ const field = GRANT_ARG[tool];
53
+ if (!field || typeof input !== 'object' || input === null)
54
+ return undefined;
55
+ const value = input[field];
56
+ return typeof value === 'string' ? value : undefined;
57
+ };
58
+ exports.grantArg = grantArg;
59
+ const toolDecision = (agent, tool, arg) => resolveGrant(agent.tools, tool, arg);
60
+ exports.toolDecision = toolDecision;
61
+ const toolNames = (agent) => grantNames(agent.tools);
26
62
  exports.toolNames = toolNames;
27
63
  const merge = (base, child) => ({
28
64
  name: child.name,
@@ -1,5 +1,6 @@
1
1
  export type ToolDecision = 'allow' | 'ask' | 'deny';
2
- export type ToolGrants = string[] | Record<string, ToolDecision>;
2
+ export type GrantValue = ToolDecision | Record<string, ToolDecision>;
3
+ export type ToolGrants = string[] | Record<string, GrantValue>;
3
4
  export interface Agent {
4
5
  name: string;
5
6
  description: string;
@@ -39,22 +39,35 @@ const createHarness = async (options) => {
39
39
  const cwdSkills = await (0, index_js_9.loadSkills)(cwdSkillsDir);
40
40
  const diskSkills = await (0, index_js_9.loadSkills)(skillsDir);
41
41
  const skills = (0, index_js_9.createSkillRegistry)([...hostSkills, ...pluginSkills, ...cwdSkills, ...diskSkills]);
42
- const skillTools = [
43
- (0, index_js_9.createSkillTool)(skills),
44
- (0, index_js_9.createSkillWriterTool)({ dirs: { local: cwdSkillsDir, config: skillsDir }, registry: skills }),
45
- ];
42
+ const skillWriterTool = (0, index_js_9.createSkillWriterTool)({
43
+ dirs: { local: cwdSkillsDir, config: skillsDir },
44
+ registry: skills,
45
+ });
46
+ const scopeSkills = (agent) => {
47
+ if (!agent)
48
+ return skills;
49
+ return skills.select(skills.list().map((s) => s.name).filter((name) => (0, index_js_1.toolDecision)(agent, 'skill', name) !== 'deny'));
50
+ };
46
51
  const tasks = enableScheduler ? (0, index_js_7.createTaskStore)({ dir: (0, node_path_1.join)(config.configDir, 'tasks') }) : undefined;
47
52
  let schedulerTools = [];
48
53
  const runs = (0, index_js_10.createSubAgentRegistry)();
49
54
  const store = (0, index_js_8.createSessionStore)({ dir: (0, node_path_1.join)(config.dataDir, 'sessions') });
50
- const sessionTools = (extra = []) => [...(sessionDefaults.tools ?? []), ...extra, ...skillTools, ...schedulerTools];
55
+ const sessionTools = (agent, extra = []) => [
56
+ ...(sessionDefaults.tools ?? []),
57
+ ...extra,
58
+ (0, index_js_9.createSkillTool)(scopeSkills(agent)),
59
+ skillWriterTool,
60
+ ...schedulerTools,
61
+ ];
51
62
  const approvalHook = (getAgent) => approvals
52
63
  ? approvals.manager.hooksFor({
53
64
  decide: (call) => {
54
65
  const agent = getAgent();
55
66
  if (!agent)
56
67
  return 'allow';
57
- return approvals.decide ? approvals.decide(agent, call) : (0, index_js_1.toolDecision)(agent, call.name);
68
+ if (approvals.decide)
69
+ return approvals.decide(agent, call);
70
+ return (0, index_js_1.toolDecision)(agent, call.name, (0, index_js_1.grantArg)(call.name, call.input));
58
71
  },
59
72
  agent: () => getAgent()?.name,
60
73
  })
@@ -68,7 +81,7 @@ const createHarness = async (options) => {
68
81
  id: newId(),
69
82
  });
70
83
  const spawn = (agent) => (0, index_js_8.persistTo)(store, persona(agent, {
71
- tools: sessionTools(),
84
+ tools: sessionTools(agent),
72
85
  hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, (0, index_js_5.allowList)((0, index_js_1.toolNames)(agent)), approvalHook(() => agent)]),
73
86
  }));
74
87
  const scheduler = enableScheduler && tasks
@@ -118,7 +131,7 @@ const createHarness = async (options) => {
118
131
  revive: ({ id, model: ref, messages }) => (0, index_js_8.createAgentSession)({
119
132
  ...sessionDefaults,
120
133
  hooks: (0, index_js_4.mergeHooks)([sessionDefaults.hooks, approvalHook(() => approvals?.activeAgent())]),
121
- tools: sessionTools([(0, index_js_10.createSubAgentTool)({ registry: agents, spawn, runs, parentId: id })]),
134
+ tools: sessionTools(undefined, [(0, index_js_10.createSubAgentTool)({ registry: agents, spawn, runs, parentId: id })]),
122
135
  ...models.resolve(ref),
123
136
  id,
124
137
  messages,
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSubAgentTool = void 0;
4
4
  const runner_js_1 = require("./runner.js");
5
- const BASE_PROMPT = 'Delegate an isolated, fully-specifiable task to a named sub-agent instead of doing it inline; it returns only its final answer. Pick the matching sub-agent, brief it from scratch, then verify its answer before relying on it.';
5
+ const BASE_PROMPT = 'Delegate isolated, fully-specifiable tasks to named sub-agents instead of doing them inline; each returns only its final answer. List several tasks in one call to run them concurrently — put all independent work in a single call rather than one at a time. Brief each sub-agent from scratch, then verify its answer before relying on it.';
6
6
  const createSubAgentTool = (deps) => {
7
7
  const roster = deps.registry.list()
8
8
  .filter((agent) => agent.name !== 'title')
@@ -14,27 +14,55 @@ const createSubAgentTool = (deps) => {
14
14
  parameters: {
15
15
  type: 'object',
16
16
  properties: {
17
- agent: { type: 'string', description: 'Sub-agent name.' },
18
- task: { type: 'string', description: 'The task to delegate.' },
17
+ tasks: {
18
+ type: 'array',
19
+ description: 'One or more tasks to delegate; multiple entries run in parallel.',
20
+ minItems: 1,
21
+ items: {
22
+ type: 'object',
23
+ properties: {
24
+ agent: { type: 'string', description: 'Sub-agent name.' },
25
+ task: { type: 'string', description: 'The task to delegate.' },
26
+ },
27
+ required: ['agent', 'task'],
28
+ additionalProperties: false,
29
+ },
30
+ },
19
31
  },
20
- required: ['agent', 'task'],
32
+ required: ['tasks'],
21
33
  additionalProperties: false,
22
34
  },
23
35
  run: async (input, ctx) => {
24
- const { agent, task } = (input ?? {});
25
- if (!agent || !task)
26
- return [{ type: 'text', text: 'Error: subagent requires `agent` and `task`.' }];
27
- const def = deps.registry.get(agent);
28
- if (!def)
29
- return [{ type: 'text', text: `Error: unknown sub-agent "${agent}".` }];
30
- const result = await (0, runner_js_1.runSubAgent)(def, task, {
31
- spawn: deps.spawn,
32
- runs: deps.runs,
33
- parentId: deps.parentId,
34
- signal: ctx.signal,
35
- });
36
- return [{ type: 'text', text: result.text }];
36
+ const { tasks } = (input ?? {});
37
+ if (!Array.isArray(tasks) || tasks.length === 0) {
38
+ return [{ type: 'text', text: 'Error: subagent requires a non-empty `tasks` array.' }];
39
+ }
40
+ const results = await Promise.all(tasks.map(({ agent, task }) => runOne(agent, task, deps, ctx.signal)));
41
+ const labeled = results.length > 1;
42
+ return results.map(({ agent, text }) => ({
43
+ type: 'text',
44
+ text: labeled ? `[${agent ?? 'unknown'}]\n${text}` : text,
45
+ }));
37
46
  },
38
47
  };
39
48
  };
40
49
  exports.createSubAgentTool = createSubAgentTool;
50
+ const runOne = async (agent, task, deps, signal) => {
51
+ if (!agent || !task)
52
+ return { agent, text: 'Error: each task requires `agent` and `task`.' };
53
+ const def = deps.registry.get(agent);
54
+ if (!def)
55
+ return { agent, text: `Error: unknown sub-agent "${agent}".` };
56
+ try {
57
+ const result = await (0, runner_js_1.runSubAgent)(def, task, {
58
+ spawn: deps.spawn,
59
+ runs: deps.runs,
60
+ parentId: deps.parentId,
61
+ signal,
62
+ });
63
+ return { agent, text: result.text };
64
+ }
65
+ catch (err) {
66
+ return { agent, text: `Error: ${err instanceof Error ? err.message : String(err)}` };
67
+ }
68
+ };
@@ -1,14 +1,26 @@
1
1
  import type { InputEvent } from '../events';
2
2
  import type { Component, Surface } from '../surface';
3
+ export interface ScrollViewOptions {
4
+ wheelStep?: number;
5
+ stickyHeader?: (info: {
6
+ scrollY: number;
7
+ width: number;
8
+ }) => Component | undefined;
9
+ footer?: () => Component | undefined;
10
+ }
3
11
  export declare class ScrollView implements Component {
4
12
  private content;
5
13
  private scrollY;
6
14
  private stick;
7
- constructor(content: Component);
15
+ private readonly wheelStep;
16
+ private readonly stickyHeader?;
17
+ private readonly footer?;
18
+ constructor(content: Component, opts?: ScrollViewOptions);
19
+ atBottom(): boolean;
8
20
  setContent(content: Component): void;
9
21
  scrollToBottom(): void;
10
22
  render(s: Surface): void;
11
23
  handleInput(event: InputEvent): void;
12
24
  private scrollBy;
13
25
  }
14
- export declare const scrollView: (content: Component) => ScrollView;
26
+ export declare const scrollView: (content: Component, opts?: ScrollViewOptions) => ScrollView;
@@ -5,8 +5,17 @@ class ScrollView {
5
5
  content;
6
6
  scrollY = 0;
7
7
  stick = true;
8
- constructor(content) {
8
+ wheelStep;
9
+ stickyHeader;
10
+ footer;
11
+ constructor(content, opts = {}) {
9
12
  this.content = content;
13
+ this.wheelStep = Math.max(1, opts.wheelStep ?? 3);
14
+ this.stickyHeader = opts.stickyHeader;
15
+ this.footer = opts.footer;
16
+ }
17
+ atBottom() {
18
+ return this.stick;
10
19
  }
11
20
  setContent(content) {
12
21
  this.content = content;
@@ -24,13 +33,29 @@ class ScrollView {
24
33
  }
25
34
  const scroll = Number.isFinite(s.height) ? this.scrollY : 0;
26
35
  s.child(this.content, { x: 0, y: -scroll, width: s.width, height: contentHeight });
36
+ if (this.stickyHeader && Number.isFinite(s.height) && !this.stick) {
37
+ const header = this.stickyHeader({ scrollY: this.scrollY, width: s.width });
38
+ if (header) {
39
+ const h = Math.min(s.measure(header, s.width), s.height);
40
+ if (h > 0)
41
+ s.child(header, { x: 0, y: 0, width: s.width, height: h });
42
+ }
43
+ }
44
+ if (this.footer && Number.isFinite(s.height) && !this.stick) {
45
+ const footer = this.footer();
46
+ if (footer) {
47
+ const h = Math.min(s.measure(footer, s.width), s.height);
48
+ if (h > 0)
49
+ s.child(footer, { x: 0, y: s.height - h, width: s.width, height: h });
50
+ }
51
+ }
27
52
  }
28
53
  handleInput(event) {
29
54
  if (event.type === 'mouse' && event.kind === 'wheel') {
30
55
  if (event.button === 'wheelUp')
31
- this.scrollBy(-1);
56
+ this.scrollBy(-this.wheelStep);
32
57
  else if (event.button === 'wheelDown')
33
- this.scrollBy(1);
58
+ this.scrollBy(this.wheelStep);
34
59
  return;
35
60
  }
36
61
  if (event.type !== 'key' || event.kind === 'release')
@@ -46,5 +71,5 @@ class ScrollView {
46
71
  }
47
72
  }
48
73
  exports.ScrollView = ScrollView;
49
- const scrollView = (content) => new ScrollView(content);
74
+ const scrollView = (content, opts) => new ScrollView(content, opts);
50
75
  exports.scrollView = scrollView;
@@ -17,7 +17,7 @@ export declare class SelectList<T> implements Component {
17
17
  selectedItem(): SelectItem<T> | undefined;
18
18
  move(delta: number): void;
19
19
  render(s: Surface): void;
20
- handleInput(event: InputEvent): void;
20
+ handleInput(event: InputEvent): boolean | void;
21
21
  }
22
22
  export declare const selectList: <T>(items?: SelectItem<T>[], opts?: {
23
23
  maxRows?: number;
@@ -48,13 +48,15 @@ class SelectList {
48
48
  }
49
49
  }
50
50
  handleInput(event) {
51
- if (event.type === 'mouse' && event.kind === 'press' && event.button === 'left') {
51
+ if (event.type === 'mouse') {
52
+ if (event.kind !== 'press' || event.button !== 'left')
53
+ return false;
52
54
  const index = this.top + (event.localY ?? 0);
53
55
  if (index >= 0 && index < this.items.length) {
54
56
  this.selected = index;
55
57
  this.onSelect?.(this.items[index]);
56
58
  }
57
- return;
59
+ return true;
58
60
  }
59
61
  if (event.type !== 'key' || event.kind === 'release')
60
62
  return;
@@ -2,7 +2,7 @@ export type { Component, Surface, SurfaceEntry } from './surface';
2
2
  export { measure, measureWidth, renderToBuffer } from './surface';
3
3
  export { box, type BoxOptions, column, flex, type FlexItem, modal, type ModalOptions, overlay, type OverlayOptions, row, text, toast, type ToastKind, type ToastOptions, } from './views';
4
4
  export { Editor, editor, type EditorOptions } from './components/editor';
5
- export { ScrollView, scrollView } from './components/scroll-view';
5
+ export { ScrollView, type ScrollViewOptions, scrollView } from './components/scroll-view';
6
6
  export { type SelectItem, SelectList, selectList } from './components/select-list';
7
7
  export { type Command, CommandPalette, commandPalette, type CommandPaletteOptions } from './components/command-palette';
8
8
  export { type LayerHandle, type ToastHandle, TUI, type TuiOptions } from './tui';
@@ -101,7 +101,13 @@ class InputRouter {
101
101
  continue;
102
102
  if (!(0, insets_1.containsPoint)(entry.rect, event.x, event.y))
103
103
  continue;
104
- entry.component.handleInput({ ...event, localX: event.x - entry.rect.x, localY: event.y - entry.rect.y });
104
+ const handled = entry.component.handleInput({
105
+ ...event,
106
+ localX: event.x - entry.rect.x,
107
+ localY: event.y - entry.rect.y,
108
+ });
109
+ if (handled === false)
110
+ continue;
105
111
  this.host.requestRender();
106
112
  return;
107
113
  }
@@ -260,6 +260,20 @@ function codepointToKey(code) {
260
260
  13: 'enter',
261
261
  27: 'escape',
262
262
  127: 'backspace',
263
+ 57344: 'escape',
264
+ 57345: 'enter',
265
+ 57346: 'tab',
266
+ 57347: 'backspace',
267
+ 57348: 'insert',
268
+ 57349: 'delete',
269
+ 57350: 'left',
270
+ 57351: 'right',
271
+ 57352: 'up',
272
+ 57353: 'down',
273
+ 57354: 'pageUp',
274
+ 57355: 'pageDown',
275
+ 57356: 'home',
276
+ 57357: 'end',
263
277
  };
264
278
  return named[code] ?? codepointToText(code) ?? `unknown:${code}`;
265
279
  }
@@ -3,7 +3,7 @@ import { type CellBuffer } from './layout/cellbuffer';
3
3
  import type { Color, Rect } from './layout/types';
4
4
  export interface Component {
5
5
  render(surface: Surface): void;
6
- handleInput?(event: InputEvent): void;
6
+ handleInput?(event: InputEvent): boolean | void;
7
7
  wantsKeyRelease?: boolean;
8
8
  }
9
9
  export interface Surface {