mu-harness 0.16.19 → 0.16.21

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.
@@ -1159,7 +1159,7 @@ export class ChatApp {
1159
1159
  children.push(listView(rows, this.paletteCursor, this.theme()));
1160
1160
  }
1161
1161
  else if (this.pickerVisible()) {
1162
- const rows = this.pickerRanked.map((c) => ({ left: c.label, right: c.kind === 'agent' ? 'agent' : '' }));
1162
+ const rows = this.pickerRanked.map((c) => ({ left: c.label, right: c.kind === 'file' ? '' : c.kind }));
1163
1163
  children.push(listView(rows, this.pickerCursor, this.theme()));
1164
1164
  }
1165
1165
  const waiting = this.waitingView();
@@ -1,10 +1,9 @@
1
1
  export interface Candidate {
2
2
  label: string;
3
3
  insert: string;
4
- kind: 'file' | 'agent';
4
+ kind: 'file' | 'dir' | 'agent';
5
5
  }
6
6
  export declare function collectCandidates(cwd: string, agentNames: string[]): Candidate[];
7
- export declare function invalidateCandidates(): void;
8
7
  export declare function rank(query: string, candidates: Candidate[], limit?: number): Candidate[];
9
8
  export interface ActiveMention {
10
9
  start: number;
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import { readdirSync, statSync } from 'node:fs';
2
3
  import { join, relative } from 'node:path';
3
4
  const IGNORED = new Set([
@@ -19,11 +20,26 @@ const IGNORED = new Set([
19
20
  ]);
20
21
  const MAX_ENTRIES = 5000;
21
22
  const MAX_DEPTH = 6;
22
- let cache;
23
+ function gitFiles(cwd) {
24
+ try {
25
+ const out = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
26
+ cwd,
27
+ encoding: 'utf8',
28
+ maxBuffer: 64 * 1024 * 1024,
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ });
31
+ const files = out.split('\0').filter((f) => f.length > 0);
32
+ return files.length > 0 ? files.slice(0, MAX_ENTRIES).sort() : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
23
38
  function walk(cwd) {
24
- const out = [];
39
+ const files = [];
40
+ const dirs = [];
25
41
  const stack = [{ dir: cwd, depth: 0 }];
26
- while (stack.length > 0 && out.length < MAX_ENTRIES) {
42
+ while (stack.length > 0 && files.length < MAX_ENTRIES) {
27
43
  const { dir, depth } = stack.pop();
28
44
  let entries;
29
45
  try {
@@ -46,43 +62,65 @@ function walk(cwd) {
46
62
  continue;
47
63
  }
48
64
  if (isDir) {
65
+ dirs.push(relative(cwd, full));
49
66
  if (depth < MAX_DEPTH)
50
67
  stack.push({ dir: full, depth: depth + 1 });
51
68
  }
52
69
  else {
53
- out.push(relative(cwd, full));
54
- if (out.length >= MAX_ENTRIES)
70
+ files.push(relative(cwd, full));
71
+ if (files.length >= MAX_ENTRIES)
55
72
  break;
56
73
  }
57
74
  }
58
75
  }
59
- return out.sort();
76
+ return { files: files.sort(), dirs: dirs.sort() };
60
77
  }
61
78
  export function collectCandidates(cwd, agentNames) {
62
- if (!cache || cache.cwd !== cwd)
63
- cache = { cwd, files: walk(cwd) };
64
79
  const agents = agentNames.map((name) => ({ label: `@${name}`, insert: name, kind: 'agent' }));
65
- const files = cache.files.map((path) => ({ label: path, insert: path, kind: 'file' }));
66
- return [...agents, ...files];
80
+ const tracked = gitFiles(cwd);
81
+ const walked = walk(cwd);
82
+ const paths = tracked ?? walked.files;
83
+ const dirs = new Set(walked.dirs);
84
+ for (const path of paths) {
85
+ for (let i = 0; i < path.length; i++) {
86
+ if (path[i] === '/' || path[i] === '\\')
87
+ dirs.add(path.slice(0, i));
88
+ }
89
+ }
90
+ const dirCandidates = [...dirs].map((path) => ({ label: `${path}/`, insert: `${path}/`, kind: 'dir' }));
91
+ const files = paths.map((path) => ({ label: path, insert: path, kind: 'file' }));
92
+ return [...agents, ...dirCandidates, ...files];
67
93
  }
68
- export function invalidateCandidates() {
69
- cache = undefined;
94
+ function isBoundary(target, i) {
95
+ if (i === 0)
96
+ return true;
97
+ const prev = target[i - 1] ?? '';
98
+ if (/[/\\_\-. ]/.test(prev))
99
+ return true;
100
+ const cur = target[i] ?? '';
101
+ return prev === prev.toLowerCase() && prev !== prev.toUpperCase() && cur === cur.toUpperCase() &&
102
+ cur !== cur.toLowerCase();
70
103
  }
71
- function score(query, target) {
104
+ function fuzzyScore(query, target) {
72
105
  if (query.length === 0)
73
106
  return 0;
74
107
  const q = query.toLowerCase();
75
- const t = target.toLowerCase();
76
108
  let qi = 0;
77
109
  let total = 0;
78
110
  let prev = -2;
79
- for (let i = 0; i < t.length && qi < q.length; i++) {
80
- if (t[i] === q[qi]) {
111
+ let run = 0;
112
+ for (let i = 0; i < target.length && qi < q.length; i++) {
113
+ if (target[i].toLowerCase() === q[qi]) {
81
114
  let s = 1;
82
- if (i === prev + 1)
83
- s += 2;
84
- if (i === 0 || /[/_\-. ]/.test(t[i - 1] ?? ''))
85
- s += 3;
115
+ if (i === prev + 1) {
116
+ run += 1;
117
+ s += 2 + run;
118
+ }
119
+ else {
120
+ run = 0;
121
+ }
122
+ if (isBoundary(target, i))
123
+ s += 4;
86
124
  total += s;
87
125
  prev = i;
88
126
  qi += 1;
@@ -90,14 +128,41 @@ function score(query, target) {
90
128
  }
91
129
  if (qi < q.length)
92
130
  return undefined;
93
- return total - t.length * 0.01;
131
+ return total;
132
+ }
133
+ function basename(path) {
134
+ const p = path.endsWith('/') || path.endsWith('\\') ? path.slice(0, -1) : path;
135
+ const cut = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
136
+ return cut === -1 ? p : p.slice(cut + 1);
137
+ }
138
+ function score(query, candidate) {
139
+ if (query.length === 0)
140
+ return 0;
141
+ const pathScore = fuzzyScore(query, candidate.label);
142
+ if (pathScore === undefined)
143
+ return undefined;
144
+ let total = pathScore;
145
+ if (candidate.kind === 'file' || candidate.kind === 'dir') {
146
+ const base = basename(candidate.label);
147
+ const baseScore = fuzzyScore(query, base);
148
+ if (baseScore !== undefined) {
149
+ total += baseScore * 2;
150
+ const lb = base.toLowerCase();
151
+ const lq = query.toLowerCase();
152
+ if (lb === lq)
153
+ total += 100;
154
+ else if (lb.startsWith(lq))
155
+ total += 40;
156
+ }
157
+ }
158
+ return total - candidate.label.length * 0.05;
94
159
  }
95
160
  export function rank(query, candidates, limit = 8) {
96
161
  if (!query)
97
162
  return candidates.slice(0, limit);
98
163
  const scored = [];
99
164
  for (const candidate of candidates) {
100
- const s = score(query, candidate.label);
165
+ const s = score(query, candidate);
101
166
  if (s !== undefined)
102
167
  scored.push({ candidate, score: s });
103
168
  }
@@ -176,7 +176,7 @@ export function formatToolArgs(name, input, max = 120) {
176
176
  if (input === null || typeof input !== 'object')
177
177
  return truncateText(String(input ?? ''), max);
178
178
  const args = input;
179
- if (name === 'edit' || name === 'write' || name === 'read' || name === 'list_dir') {
179
+ if (name === 'edit' || name === 'write' || name === 'read' || name === 'list') {
180
180
  return truncateText(stringifyArg(args.path), max);
181
181
  }
182
182
  if (name === 'bash')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-harness",
3
- "version": "0.16.19",
3
+ "version": "0.16.21",
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",
@@ -23,8 +23,8 @@
23
23
  "@swc/wasm-typescript": "^1.15.0",
24
24
  "cli-highlight": "^2.1.11",
25
25
  "croner": "^9.0.0",
26
- "mu-core": "^0.16.19",
27
- "mu-tui": "^0.16.19"
26
+ "mu-core": "^0.16.21",
27
+ "mu-tui": "^0.16.21"
28
28
  },
29
29
  "_generatedBy": "dnt@dev",
30
30
  "types": "./esm/index.d.ts"
@@ -1162,7 +1162,7 @@ class ChatApp {
1162
1162
  children.push(listView(rows, this.paletteCursor, this.theme()));
1163
1163
  }
1164
1164
  else if (this.pickerVisible()) {
1165
- const rows = this.pickerRanked.map((c) => ({ left: c.label, right: c.kind === 'agent' ? 'agent' : '' }));
1165
+ const rows = this.pickerRanked.map((c) => ({ left: c.label, right: c.kind === 'file' ? '' : c.kind }));
1166
1166
  children.push(listView(rows, this.pickerCursor, this.theme()));
1167
1167
  }
1168
1168
  const waiting = this.waitingView();
@@ -1,10 +1,9 @@
1
1
  export interface Candidate {
2
2
  label: string;
3
3
  insert: string;
4
- kind: 'file' | 'agent';
4
+ kind: 'file' | 'dir' | 'agent';
5
5
  }
6
6
  export declare function collectCandidates(cwd: string, agentNames: string[]): Candidate[];
7
- export declare function invalidateCandidates(): void;
8
7
  export declare function rank(query: string, candidates: Candidate[], limit?: number): Candidate[];
9
8
  export interface ActiveMention {
10
9
  start: number;
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.collectCandidates = collectCandidates;
4
- exports.invalidateCandidates = invalidateCandidates;
5
4
  exports.rank = rank;
6
5
  exports.activeMention = activeMention;
6
+ const node_child_process_1 = require("node:child_process");
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
9
  const IGNORED = new Set([
@@ -25,11 +25,26 @@ const IGNORED = new Set([
25
25
  ]);
26
26
  const MAX_ENTRIES = 5000;
27
27
  const MAX_DEPTH = 6;
28
- let cache;
28
+ function gitFiles(cwd) {
29
+ try {
30
+ const out = (0, node_child_process_1.execFileSync)('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
31
+ cwd,
32
+ encoding: 'utf8',
33
+ maxBuffer: 64 * 1024 * 1024,
34
+ stdio: ['ignore', 'pipe', 'ignore'],
35
+ });
36
+ const files = out.split('\0').filter((f) => f.length > 0);
37
+ return files.length > 0 ? files.slice(0, MAX_ENTRIES).sort() : undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
29
43
  function walk(cwd) {
30
- const out = [];
44
+ const files = [];
45
+ const dirs = [];
31
46
  const stack = [{ dir: cwd, depth: 0 }];
32
- while (stack.length > 0 && out.length < MAX_ENTRIES) {
47
+ while (stack.length > 0 && files.length < MAX_ENTRIES) {
33
48
  const { dir, depth } = stack.pop();
34
49
  let entries;
35
50
  try {
@@ -52,43 +67,65 @@ function walk(cwd) {
52
67
  continue;
53
68
  }
54
69
  if (isDir) {
70
+ dirs.push((0, node_path_1.relative)(cwd, full));
55
71
  if (depth < MAX_DEPTH)
56
72
  stack.push({ dir: full, depth: depth + 1 });
57
73
  }
58
74
  else {
59
- out.push((0, node_path_1.relative)(cwd, full));
60
- if (out.length >= MAX_ENTRIES)
75
+ files.push((0, node_path_1.relative)(cwd, full));
76
+ if (files.length >= MAX_ENTRIES)
61
77
  break;
62
78
  }
63
79
  }
64
80
  }
65
- return out.sort();
81
+ return { files: files.sort(), dirs: dirs.sort() };
66
82
  }
67
83
  function collectCandidates(cwd, agentNames) {
68
- if (!cache || cache.cwd !== cwd)
69
- cache = { cwd, files: walk(cwd) };
70
84
  const agents = agentNames.map((name) => ({ label: `@${name}`, insert: name, kind: 'agent' }));
71
- const files = cache.files.map((path) => ({ label: path, insert: path, kind: 'file' }));
72
- return [...agents, ...files];
85
+ const tracked = gitFiles(cwd);
86
+ const walked = walk(cwd);
87
+ const paths = tracked ?? walked.files;
88
+ const dirs = new Set(walked.dirs);
89
+ for (const path of paths) {
90
+ for (let i = 0; i < path.length; i++) {
91
+ if (path[i] === '/' || path[i] === '\\')
92
+ dirs.add(path.slice(0, i));
93
+ }
94
+ }
95
+ const dirCandidates = [...dirs].map((path) => ({ label: `${path}/`, insert: `${path}/`, kind: 'dir' }));
96
+ const files = paths.map((path) => ({ label: path, insert: path, kind: 'file' }));
97
+ return [...agents, ...dirCandidates, ...files];
73
98
  }
74
- function invalidateCandidates() {
75
- cache = undefined;
99
+ function isBoundary(target, i) {
100
+ if (i === 0)
101
+ return true;
102
+ const prev = target[i - 1] ?? '';
103
+ if (/[/\\_\-. ]/.test(prev))
104
+ return true;
105
+ const cur = target[i] ?? '';
106
+ return prev === prev.toLowerCase() && prev !== prev.toUpperCase() && cur === cur.toUpperCase() &&
107
+ cur !== cur.toLowerCase();
76
108
  }
77
- function score(query, target) {
109
+ function fuzzyScore(query, target) {
78
110
  if (query.length === 0)
79
111
  return 0;
80
112
  const q = query.toLowerCase();
81
- const t = target.toLowerCase();
82
113
  let qi = 0;
83
114
  let total = 0;
84
115
  let prev = -2;
85
- for (let i = 0; i < t.length && qi < q.length; i++) {
86
- if (t[i] === q[qi]) {
116
+ let run = 0;
117
+ for (let i = 0; i < target.length && qi < q.length; i++) {
118
+ if (target[i].toLowerCase() === q[qi]) {
87
119
  let s = 1;
88
- if (i === prev + 1)
89
- s += 2;
90
- if (i === 0 || /[/_\-. ]/.test(t[i - 1] ?? ''))
91
- s += 3;
120
+ if (i === prev + 1) {
121
+ run += 1;
122
+ s += 2 + run;
123
+ }
124
+ else {
125
+ run = 0;
126
+ }
127
+ if (isBoundary(target, i))
128
+ s += 4;
92
129
  total += s;
93
130
  prev = i;
94
131
  qi += 1;
@@ -96,14 +133,41 @@ function score(query, target) {
96
133
  }
97
134
  if (qi < q.length)
98
135
  return undefined;
99
- return total - t.length * 0.01;
136
+ return total;
137
+ }
138
+ function basename(path) {
139
+ const p = path.endsWith('/') || path.endsWith('\\') ? path.slice(0, -1) : path;
140
+ const cut = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
141
+ return cut === -1 ? p : p.slice(cut + 1);
142
+ }
143
+ function score(query, candidate) {
144
+ if (query.length === 0)
145
+ return 0;
146
+ const pathScore = fuzzyScore(query, candidate.label);
147
+ if (pathScore === undefined)
148
+ return undefined;
149
+ let total = pathScore;
150
+ if (candidate.kind === 'file' || candidate.kind === 'dir') {
151
+ const base = basename(candidate.label);
152
+ const baseScore = fuzzyScore(query, base);
153
+ if (baseScore !== undefined) {
154
+ total += baseScore * 2;
155
+ const lb = base.toLowerCase();
156
+ const lq = query.toLowerCase();
157
+ if (lb === lq)
158
+ total += 100;
159
+ else if (lb.startsWith(lq))
160
+ total += 40;
161
+ }
162
+ }
163
+ return total - candidate.label.length * 0.05;
100
164
  }
101
165
  function rank(query, candidates, limit = 8) {
102
166
  if (!query)
103
167
  return candidates.slice(0, limit);
104
168
  const scored = [];
105
169
  for (const candidate of candidates) {
106
- const s = score(query, candidate.label);
170
+ const s = score(query, candidate);
107
171
  if (s !== undefined)
108
172
  scored.push({ candidate, score: s });
109
173
  }
@@ -183,7 +183,7 @@ function formatToolArgs(name, input, max = 120) {
183
183
  if (input === null || typeof input !== 'object')
184
184
  return truncateText(String(input ?? ''), max);
185
185
  const args = input;
186
- if (name === 'edit' || name === 'write' || name === 'read' || name === 'list_dir') {
186
+ if (name === 'edit' || name === 'write' || name === 'read' || name === 'list') {
187
187
  return truncateText(stringifyArg(args.path), max);
188
188
  }
189
189
  if (name === 'bash')