mu-coding 0.15.0 → 0.16.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 +9 -123
- package/bin/coding-agent.ts +95 -0
- package/package.json +10 -21
- package/src/config.ts +122 -0
- package/src/harness.test.ts +159 -0
- package/src/main.ts +53 -3
- package/src/plugins.ts +49 -0
- package/src/systemPrompt.ts +22 -0
- package/src/ui/ChatApp.ts +959 -0
- package/src/ui/commands.ts +35 -0
- package/src/ui/editor.ts +166 -0
- package/src/ui/markdown.ts +363 -0
- package/src/ui/picker.ts +126 -0
- package/src/ui/status.ts +61 -0
- package/src/ui/theme.ts +241 -0
- package/src/ui/transcript.test.ts +121 -0
- package/src/ui/transcript.ts +399 -0
- package/tsconfig.json +8 -0
- package/bin/mu.js +0 -2
- package/prompts/SYSTEM.md +0 -16
- package/src/app/shutdown.ts +0 -94
- package/src/app/startApp.ts +0 -49
- package/src/cli/args.ts +0 -133
- package/src/cli/install.ts +0 -107
- package/src/cli/subcommands.ts +0 -29
- package/src/cli/update.ts +0 -205
- package/src/config/index.test.ts +0 -77
- package/src/config/index.ts +0 -199
- package/src/plugin.ts +0 -124
- package/src/runtime/codingTools/bash.ts +0 -114
- package/src/runtime/codingTools/edit-file.ts +0 -60
- package/src/runtime/codingTools/index.ts +0 -39
- package/src/runtime/codingTools/read-file.ts +0 -83
- package/src/runtime/codingTools/utils.ts +0 -21
- package/src/runtime/codingTools/write-file.ts +0 -42
- package/src/runtime/createRegistry.test.ts +0 -147
- package/src/runtime/createRegistry.ts +0 -195
- package/src/runtime/fileMentionProvider.ts +0 -117
- package/src/runtime/messageBus.test.ts +0 -62
- package/src/runtime/messageBus.ts +0 -78
- package/src/runtime/pluginLoader.ts +0 -153
- package/src/runtime/startupUpdateCheck.ts +0 -163
- package/src/runtime/updateCheck.ts +0 -136
- package/src/sessions/index.test.ts +0 -66
- package/src/sessions/index.ts +0 -183
- package/src/sessions/peek.test.ts +0 -88
- package/src/sessions/project.ts +0 -51
- package/src/tui/channel/tuiChannel.test.ts +0 -107
- package/src/tui/channel/tuiChannel.ts +0 -62
- package/src/tui/chat/ChatContext.ts +0 -10
- package/src/tui/chat/MessageRendererContext.ts +0 -44
- package/src/tui/chat/ToolDisplayContext.ts +0 -33
- package/src/tui/chat/useAbort.ts +0 -85
- package/src/tui/chat/useAttachment.ts +0 -74
- package/src/tui/chat/useChat.ts +0 -113
- package/src/tui/chat/useChatPanel.ts +0 -120
- package/src/tui/chat/useChatSession.ts +0 -384
- package/src/tui/chat/useModels.ts +0 -83
- package/src/tui/chat/usePluginStatus.ts +0 -44
- package/src/tui/chat/useSessionPersistence.ts +0 -84
- package/src/tui/chat/useStatusSegments.ts +0 -85
- package/src/tui/chat/useSubagentBrowser.ts +0 -133
- package/src/tui/components/chat/ChatPanel.tsx +0 -54
- package/src/tui/components/chat/ChatPanelBody.tsx +0 -86
- package/src/tui/components/chat/Pickers.tsx +0 -44
- package/src/tui/components/chat/SubagentBrowserPanel.tsx +0 -145
- package/src/tui/components/messageView.tsx +0 -72
- package/src/tui/components/messages/EditOutput.tsx +0 -112
- package/src/tui/components/messages/ReadOutput.tsx +0 -48
- package/src/tui/components/messages/ToolHeader.tsx +0 -30
- package/src/tui/components/messages/WebFetchOutput.tsx +0 -30
- package/src/tui/components/messages/WriteOutput.tsx +0 -64
- package/src/tui/components/messages/assistantMessage.tsx +0 -72
- package/src/tui/components/messages/markdown.tsx +0 -407
- package/src/tui/components/messages/messageItem.tsx +0 -43
- package/src/tui/components/messages/reasoningBlock.tsx +0 -18
- package/src/tui/components/messages/streamingOutput.tsx +0 -18
- package/src/tui/components/messages/toolCallBlock.tsx +0 -125
- package/src/tui/components/messages/userMessage.tsx +0 -44
- package/src/tui/components/primitives/dropdown.tsx +0 -125
- package/src/tui/components/primitives/modal.tsx +0 -47
- package/src/tui/components/primitives/pickerModal.tsx +0 -47
- package/src/tui/components/primitives/scrollbar.tsx +0 -27
- package/src/tui/components/primitives/toast.tsx +0 -100
- package/src/tui/components/statusBar.tsx +0 -41
- package/src/tui/components/ui/dialogLayer.tsx +0 -175
- package/src/tui/context/ThemeContext.tsx +0 -18
- package/src/tui/hooks/useChordKeyboard.ts +0 -87
- package/src/tui/hooks/useInputInfoSegments.ts +0 -22
- package/src/tui/hooks/useScroll.ts +0 -64
- package/src/tui/hooks/useTerminal.ts +0 -40
- package/src/tui/hooks/useUI.ts +0 -15
- package/src/tui/input/InputBox.tsx +0 -6
- package/src/tui/input/InputBoxView.tsx +0 -293
- package/src/tui/input/commands.test.ts +0 -71
- package/src/tui/input/commands.ts +0 -55
- package/src/tui/input/cursor.test.ts +0 -136
- package/src/tui/input/cursor.ts +0 -214
- package/src/tui/input/dumpContext.ts +0 -107
- package/src/tui/input/sanitize.ts +0 -33
- package/src/tui/input/useCommandExecutor.ts +0 -32
- package/src/tui/input/useInputBox.ts +0 -265
- package/src/tui/input/useInputHandler.ts +0 -455
- package/src/tui/input/useMentionPicker.ts +0 -133
- package/src/tui/input/usePluginShortcuts.ts +0 -29
- package/src/tui/plugins/InkApprovalChannel.test.ts +0 -51
- package/src/tui/plugins/InkApprovalChannel.ts +0 -30
- package/src/tui/plugins/InkUIService.ts +0 -188
- package/src/tui/renderApp.tsx +0 -66
- package/src/tui/theme/index.ts +0 -1
- package/src/tui/theme/merge.test.ts +0 -49
- package/src/tui/theme/merge.ts +0 -43
- package/src/tui/theme/presets.ts +0 -90
- package/src/tui/theme/types.ts +0 -138
- package/src/tui/update/runUpdateInTui.ts +0 -127
- package/src/utils/clipboard.ts +0 -97
- package/src/utils/diff.test.ts +0 -56
- package/src/utils/diff.ts +0 -81
package/src/config/index.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import type { ProviderConfig } from 'mu-core';
|
|
6
|
-
import type { ThemeConfig } from '../tui/theme/types';
|
|
7
|
-
|
|
8
|
-
// ─── XDG Path Helpers ─────────────────────────────────────────────────────────
|
|
9
|
-
//
|
|
10
|
-
// Path resolution lives alongside config because the config module owns the
|
|
11
|
-
// "where do mu's files live?" question end-to-end (config.json, SYSTEM.md,
|
|
12
|
-
// sessions, plugin caches). Resolved lazily so tests can stub the env after
|
|
13
|
-
// module import; production callers pay only one `process.env` lookup per call.
|
|
14
|
-
|
|
15
|
-
const HOME = homedir();
|
|
16
|
-
|
|
17
|
-
export function getConfigDir(): string {
|
|
18
|
-
return process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'mu') : join(HOME, '.config', 'mu');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getDataDir(): string {
|
|
22
|
-
return process.env.XDG_DATA_HOME ? join(process.env.XDG_DATA_HOME, 'mu') : join(HOME, '.local', 'share', 'mu');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getCacheDir(): string {
|
|
26
|
-
return process.env.XDG_CACHE_HOME ? join(process.env.XDG_CACHE_HOME, 'mu') : join(HOME, '.cache', 'mu');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getPluginsDir(): string {
|
|
30
|
-
return join(getConfigDir(), 'plugins');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─── npm: specifier parsing ───────────────────────────────────────────────────
|
|
34
|
-
//
|
|
35
|
-
// Plugin specifiers stored in `config.plugins` use the form `npm:<bare>` where
|
|
36
|
-
// `<bare>` is a bun/npm package spec — possibly versioned (`foo@1.2.3`,
|
|
37
|
-
// `@scope/foo@^1.0.0`). Multiple call sites need to split this consistently:
|
|
38
|
-
// `mu install`/`mu uninstall` use it for canonicalization & version stripping;
|
|
39
|
-
// the runtime plugin loader uses it to resolve from
|
|
40
|
-
// `~/.local/share/mu/node_modules/<name>`. Centralized here so the rules
|
|
41
|
-
// can never drift between install-time and load-time.
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parsed npm specifier: package name (always present, scope preserved) and an
|
|
45
|
-
* optional install-time version range. Examples:
|
|
46
|
-
*
|
|
47
|
-
* foo → { name: "foo" }
|
|
48
|
-
* foo@1.2.3 → { name: "foo", version: "1.2.3" }
|
|
49
|
-
* @scope/foo → { name: "@scope/foo" }
|
|
50
|
-
* @scope/foo@^1.0.0 → { name: "@scope/foo", version: "^1.0.0" }
|
|
51
|
-
*/
|
|
52
|
-
export interface ParsedNpmSpec {
|
|
53
|
-
name: string;
|
|
54
|
-
version?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Strip the leading `@` for scoped names before searching for the version `@`. */
|
|
58
|
-
export function parseBareNpmSpec(bare: string): ParsedNpmSpec {
|
|
59
|
-
const scoped = bare.startsWith('@');
|
|
60
|
-
const at = bare.indexOf('@', scoped ? 1 : 0);
|
|
61
|
-
if (at === -1) return { name: bare };
|
|
62
|
-
return { name: bare.slice(0, at), version: bare.slice(at + 1) };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Canonical form stored in `config.plugins`: `npm:<package-name>` with the
|
|
67
|
-
* version stripped, so `npm:foo@1.2.3` and `npm:foo` deduplicate correctly.
|
|
68
|
-
*/
|
|
69
|
-
export function canonicalNpmSpecifier(bare: string): string {
|
|
70
|
-
return `npm:${parseBareNpmSpec(bare).name}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface AppConfig extends ProviderConfig {
|
|
74
|
-
plugins?: Array<string | { name: string; config?: Record<string, unknown> }>;
|
|
75
|
-
/**
|
|
76
|
-
* Optional per-leaf overrides on top of the built-in theme. See
|
|
77
|
-
* `tui/theme/types.ts` for the available sections and color leaves.
|
|
78
|
-
*/
|
|
79
|
-
theme?: ThemeConfig;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Keys that `loadConfig`/`saveConfig` know how to materialize from `AppConfig`.
|
|
84
|
-
* Used as an allow-list when seeding a fresh config.json. On `saveConfig` we
|
|
85
|
-
* preserve every key already present in the file so users can keep custom
|
|
86
|
-
* fields (or fields added by future versions) without losing them on round-trip.
|
|
87
|
-
*/
|
|
88
|
-
const CONFIG_FILE_KEYS = [
|
|
89
|
-
'baseUrl',
|
|
90
|
-
'model',
|
|
91
|
-
'maxTokens',
|
|
92
|
-
'temperature',
|
|
93
|
-
'streamTimeoutMs',
|
|
94
|
-
'plugins',
|
|
95
|
-
'theme',
|
|
96
|
-
] as const;
|
|
97
|
-
|
|
98
|
-
function configPath(): string {
|
|
99
|
-
return join(getConfigDir(), 'config.json');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function systemPromptPath(): string {
|
|
103
|
-
return join(getConfigDir(), 'SYSTEM.md');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Path to the SYSTEM.md bundled with mu-coding. Used as the lowest-priority
|
|
108
|
-
* fallback when no user override is configured. Resolved from this module's
|
|
109
|
-
* location so it works both from `src/` (dev via bun) and any compiled layout
|
|
110
|
-
* that preserves the `prompts/` sibling of `src/` or `dist/`.
|
|
111
|
-
*/
|
|
112
|
-
function bundledSystemPromptPath(): string {
|
|
113
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
114
|
-
// src/config/index.ts → ../../prompts/SYSTEM.md
|
|
115
|
-
// dist/config/index.js → ../../prompts/SYSTEM.md
|
|
116
|
-
return join(here, '..', '..', 'prompts', 'SYSTEM.md');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function tryRead(path: string): string | undefined {
|
|
120
|
-
try {
|
|
121
|
-
return readFileSync(path, 'utf-8').trim() || undefined;
|
|
122
|
-
} catch {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function tryParseJson(text: string | undefined): Partial<AppConfig> {
|
|
128
|
-
if (!text) {
|
|
129
|
-
return {};
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(text);
|
|
133
|
-
} catch {
|
|
134
|
-
return {};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function envInt(key: string): number | undefined {
|
|
139
|
-
const v = process.env[key];
|
|
140
|
-
if (!v) {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
const n = Number.parseInt(v, 10);
|
|
144
|
-
return Number.isNaN(n) ? undefined : n;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function envFloat(key: string): number | undefined {
|
|
148
|
-
const v = process.env[key];
|
|
149
|
-
if (!v) {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
const n = Number.parseFloat(v);
|
|
153
|
-
return Number.isNaN(n) ? undefined : n;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function loadConfig(cliModel?: string): AppConfig {
|
|
157
|
-
const path = configPath();
|
|
158
|
-
const file = tryParseJson(tryRead(path));
|
|
159
|
-
|
|
160
|
-
const config: AppConfig = {
|
|
161
|
-
baseUrl: process.env.MU_BASE_URL || file.baseUrl || 'http://localhost:8080/v1',
|
|
162
|
-
model: cliModel || process.env.MU_MODEL || file.model,
|
|
163
|
-
maxTokens: envInt('MU_MAX_TOKENS') ?? file.maxTokens ?? 4096,
|
|
164
|
-
temperature: envFloat('MU_TEMPERATURE') ?? file.temperature ?? 0.7,
|
|
165
|
-
streamTimeoutMs: envInt('MU_STREAM_TIMEOUT') ?? file.streamTimeoutMs ?? 60_000,
|
|
166
|
-
systemPrompt: tryRead(systemPromptPath()) || tryRead(bundledSystemPromptPath()),
|
|
167
|
-
plugins: file.plugins,
|
|
168
|
-
theme: file.theme,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
if (!existsSync(path)) {
|
|
172
|
-
mkdirSync(getConfigDir(), { recursive: true });
|
|
173
|
-
const fileConfig = Object.fromEntries(
|
|
174
|
-
CONFIG_FILE_KEYS.filter((k) => file[k] !== undefined).map((k) => [k, file[k]]),
|
|
175
|
-
) as Partial<AppConfig>;
|
|
176
|
-
writeFileSync(path, JSON.stringify(fileConfig, null, 2), 'utf-8');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return config;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Persist `updates` to config.json, preserving any keys (known or unknown)
|
|
184
|
-
* that are already present in the file. Only `undefined` values are stripped.
|
|
185
|
-
*/
|
|
186
|
-
export function saveConfig(updates: Partial<AppConfig>): void {
|
|
187
|
-
const path = configPath();
|
|
188
|
-
const file = tryParseJson(tryRead(path)) as Record<string, unknown>;
|
|
189
|
-
const merged: Record<string, unknown> = { ...file };
|
|
190
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
191
|
-
if (value === undefined) {
|
|
192
|
-
delete merged[key];
|
|
193
|
-
} else {
|
|
194
|
-
merged[key] = value;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
mkdirSync(getConfigDir(), { recursive: true });
|
|
198
|
-
writeFileSync(path, JSON.stringify(merged, null, 2), 'utf-8');
|
|
199
|
-
}
|
package/src/plugin.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mu-coding plugin — packages the TUI channel + coding tools into a single
|
|
3
|
-
* plugin. The standalone `mu` binary uses this as its primary surface
|
|
4
|
-
* (registered by `createRegistry`) and any generic host (Arya, future web
|
|
5
|
-
* app) can opt in to the coding tools by including it in its plugin list.
|
|
6
|
-
*
|
|
7
|
-
* The factory takes a `CodingPluginConfig` rather than reading values from
|
|
8
|
-
* `PluginContext` because the TUI layer needs the **concrete**
|
|
9
|
-
* `PluginRegistry` (it subscribes to renderer / shortcut / status changes
|
|
10
|
-
* via methods that are not part of the read-only `PluginRegistryView`).
|
|
11
|
-
* `ctx.registry` only exposes the View; the host wires the concrete
|
|
12
|
-
* registry in after constructing it.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { ApprovalGateway, SubagentRunRegistry } from 'mu-agents';
|
|
16
|
-
import type { ChatMessage, Plugin, PluginContext, PluginRegistry } from 'mu-core';
|
|
17
|
-
import type { ShutdownFn } from './app/shutdown';
|
|
18
|
-
import type { AppConfig } from './config/index';
|
|
19
|
-
import { createCodingToolsPlugin } from './runtime/codingTools/index';
|
|
20
|
-
import type { SessionPathHolder } from './runtime/createRegistry';
|
|
21
|
-
import { createFileMentionProvider } from './runtime/fileMentionProvider';
|
|
22
|
-
import type { HostMessageBus } from './runtime/messageBus';
|
|
23
|
-
import { createTuiChannel } from './tui/channel/tuiChannel';
|
|
24
|
-
import { createInkApprovalChannel } from './tui/plugins/InkApprovalChannel';
|
|
25
|
-
import type { InkUIService } from './tui/plugins/InkUIService';
|
|
26
|
-
|
|
27
|
-
export interface CodingPluginConfig {
|
|
28
|
-
appConfig: AppConfig;
|
|
29
|
-
initialMessages?: ChatMessage[];
|
|
30
|
-
messageBus: HostMessageBus;
|
|
31
|
-
uiService: InkUIService;
|
|
32
|
-
shutdown: ShutdownFn;
|
|
33
|
-
/**
|
|
34
|
-
* Mutable holder updated by the TUI's session persistence hook so other
|
|
35
|
-
* plugins (mu-agents) can read the current parent session path when
|
|
36
|
-
* dispatching subagents. `undefined` until the React tree mounts.
|
|
37
|
-
*/
|
|
38
|
-
sessionPathHolder?: SessionPathHolder;
|
|
39
|
-
/**
|
|
40
|
-
* Concrete `PluginRegistry` instance used by the TUI to subscribe to
|
|
41
|
-
* renderers / shortcuts / status segments. Required because `ctx.registry`
|
|
42
|
-
* (the read-only View) does not expose those subscription methods.
|
|
43
|
-
*/
|
|
44
|
-
registry: PluginRegistry;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface AgentPluginShape {
|
|
48
|
-
approvalGateway?: ApprovalGateway;
|
|
49
|
-
runs?: SubagentRunRegistry;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function createCodingPlugin(config: CodingPluginConfig): Plugin {
|
|
53
|
-
// Coding tools are an inner plugin; we delegate via the registered tools
|
|
54
|
-
// list rather than a recursive register call so a single Plugin object
|
|
55
|
-
// is returned (matches the SDK's expected factory shape).
|
|
56
|
-
const inner = createCodingToolsPlugin();
|
|
57
|
-
|
|
58
|
-
// Captured at activation time so deactivate can clean up both registrations.
|
|
59
|
-
let unregisterTuiChannel: (() => void) | null = null;
|
|
60
|
-
let unregisterApprovalChannel: (() => void) | null = null;
|
|
61
|
-
let unregisterFileMentions: (() => void) | null = null;
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
name: 'mu-coding',
|
|
65
|
-
version: '0.5.0',
|
|
66
|
-
tools: inner.tools,
|
|
67
|
-
systemPrompt: inner.systemPrompt,
|
|
68
|
-
activate(ctx: PluginContext) {
|
|
69
|
-
// Forward inner plugin's activate (captures cwd for tool path resolution).
|
|
70
|
-
inner.activate?.(ctx);
|
|
71
|
-
|
|
72
|
-
// Register the TUI channel so other code can stop it gracefully via
|
|
73
|
-
// ctx.channels.stopAll(). The TUI subscribes to the concrete registry
|
|
74
|
-
// (passed in via config), not the narrow context-exposed view.
|
|
75
|
-
// Resolve the live mu-agents handle so the TUI can subscribe to the
|
|
76
|
-
// subagent run registry (browser panel + live header). Looked up
|
|
77
|
-
// loosely so coding still works in setups that disabled the agent
|
|
78
|
-
// plugin — the TUI then renders a fallback header without the live
|
|
79
|
-
// subagent navigation surfaces.
|
|
80
|
-
const agentPlugin = ctx.getPlugin?.<Plugin & AgentPluginShape>('mu-agents');
|
|
81
|
-
|
|
82
|
-
unregisterTuiChannel =
|
|
83
|
-
ctx.channels?.register(
|
|
84
|
-
createTuiChannel({
|
|
85
|
-
config: config.appConfig,
|
|
86
|
-
initialMessages: config.initialMessages,
|
|
87
|
-
registry: config.registry,
|
|
88
|
-
messageBus: config.messageBus,
|
|
89
|
-
uiService: config.uiService,
|
|
90
|
-
shutdown: config.shutdown,
|
|
91
|
-
sessionPathHolder: config.sessionPathHolder,
|
|
92
|
-
subagentRuns: agentPlugin?.runs,
|
|
93
|
-
}),
|
|
94
|
-
) ?? null;
|
|
95
|
-
|
|
96
|
-
// Register a file completion provider on `@`. Sits alongside the
|
|
97
|
-
// mu-agents `@`-provider — useMentionPicker concatenates results from
|
|
98
|
-
// every provider matching a trigger, grouped by category in the UI.
|
|
99
|
-
// When the user types `@foo`, agents that match by name appear first;
|
|
100
|
-
// files matching by basename/path follow.
|
|
101
|
-
if (ctx.registerMentionProvider) {
|
|
102
|
-
unregisterFileMentions = ctx.registerMentionProvider('@', createFileMentionProvider(ctx.cwd));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Register the Ink approval channel against mu-agents' gateway when
|
|
106
|
-
// it's available (the lookup above already resolved `agentPlugin`).
|
|
107
|
-
if (agentPlugin?.approvalGateway) {
|
|
108
|
-
unregisterApprovalChannel = agentPlugin.approvalGateway.registerChannel(
|
|
109
|
-
'tui',
|
|
110
|
-
createInkApprovalChannel(config.uiService),
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
deactivate() {
|
|
115
|
-
unregisterApprovalChannel?.();
|
|
116
|
-
unregisterApprovalChannel = null;
|
|
117
|
-
unregisterFileMentions?.();
|
|
118
|
-
unregisterFileMentions = null;
|
|
119
|
-
unregisterTuiChannel?.();
|
|
120
|
-
unregisterTuiChannel = null;
|
|
121
|
-
inner.deactivate?.();
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import type { PluginTool, ToolExecutorResult } from 'mu-core';
|
|
3
|
-
|
|
4
|
-
function executeBash(command: string, cwd: string, signal?: AbortSignal): Promise<ToolExecutorResult> {
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
const proc = spawn('bash', ['-c', command], {
|
|
7
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
8
|
-
detached: true,
|
|
9
|
-
cwd,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
let stdout = '';
|
|
13
|
-
let stderr = '';
|
|
14
|
-
|
|
15
|
-
proc.stdout.on('data', (data: Buffer) => {
|
|
16
|
-
try {
|
|
17
|
-
stdout += data.toString('utf-8');
|
|
18
|
-
} catch {
|
|
19
|
-
// skip binary data
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
proc.stderr.on('data', (data: Buffer) => {
|
|
23
|
-
try {
|
|
24
|
-
stderr += data.toString('utf-8');
|
|
25
|
-
} catch {
|
|
26
|
-
// skip binary data
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const onAbort = () => {
|
|
31
|
-
const pid = proc.pid;
|
|
32
|
-
if (pid) {
|
|
33
|
-
try {
|
|
34
|
-
process.kill(-pid, 'SIGTERM');
|
|
35
|
-
} catch {
|
|
36
|
-
proc.kill('SIGTERM');
|
|
37
|
-
}
|
|
38
|
-
setTimeout(() => {
|
|
39
|
-
if (!proc.killed) {
|
|
40
|
-
try {
|
|
41
|
-
process.kill(-pid, 'SIGKILL');
|
|
42
|
-
} catch {
|
|
43
|
-
proc.kill('SIGKILL');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}, 500);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
if (signal) {
|
|
51
|
-
if (signal.aborted) {
|
|
52
|
-
onAbort();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
proc.on('close', (code) => {
|
|
59
|
-
signal?.removeEventListener('abort', onAbort);
|
|
60
|
-
const output = [stdout, stderr]
|
|
61
|
-
.map((s) => s.trim())
|
|
62
|
-
.filter(Boolean)
|
|
63
|
-
.join('\n');
|
|
64
|
-
if (signal?.aborted) {
|
|
65
|
-
resolve({ content: 'Aborted', error: true });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (code !== 0 && !output) {
|
|
69
|
-
resolve({ content: `Error: Process exited with code ${code}`, error: true });
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
// Non-zero exit with output: treat as error so the LLM sees it as such,
|
|
73
|
-
// but preserve stdout/stderr in the content.
|
|
74
|
-
resolve({ content: output || '(no output)', error: code !== 0 });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
proc.on('error', (err) => {
|
|
78
|
-
signal?.removeEventListener('abort', onAbort);
|
|
79
|
-
resolve({ content: `Error: ${err.message}`, error: true });
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function createBashTool(getCwd: () => string): PluginTool {
|
|
85
|
-
return {
|
|
86
|
-
definition: {
|
|
87
|
-
type: 'function',
|
|
88
|
-
function: {
|
|
89
|
-
name: 'bash',
|
|
90
|
-
description:
|
|
91
|
-
'Run a shell command via bash in the project cwd. Returns stdout+stderr; non-zero exit is an error.',
|
|
92
|
-
parameters: {
|
|
93
|
-
type: 'object',
|
|
94
|
-
properties: {
|
|
95
|
-
cmd: { type: 'string' },
|
|
96
|
-
},
|
|
97
|
-
required: ['cmd'],
|
|
98
|
-
additionalProperties: false,
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
display: {
|
|
103
|
-
verb: 'running',
|
|
104
|
-
kind: 'shell',
|
|
105
|
-
fields: { command: 'cmd' },
|
|
106
|
-
},
|
|
107
|
-
permission: {
|
|
108
|
-
matchKey: (args) => args.cmd as string | undefined,
|
|
109
|
-
},
|
|
110
|
-
execute(args, signal) {
|
|
111
|
-
return executeBash(args.cmd as string, getCwd(), signal);
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import type { PluginTool, ToolExecutorResult } from 'mu-core';
|
|
3
|
-
import { sanitizePath } from './utils';
|
|
4
|
-
|
|
5
|
-
export function createEditFileTool(getCwd: () => string): PluginTool {
|
|
6
|
-
return {
|
|
7
|
-
definition: {
|
|
8
|
-
type: 'function',
|
|
9
|
-
function: {
|
|
10
|
-
name: 'edit',
|
|
11
|
-
description: 'Replace an exact substring in an existing file.',
|
|
12
|
-
parameters: {
|
|
13
|
-
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
path: { type: 'string' },
|
|
16
|
-
from: {
|
|
17
|
-
type: 'string',
|
|
18
|
-
description:
|
|
19
|
-
'Must occur exactly once in the file \u2014 include surrounding context to disambiguate. Whitespace must match exactly.',
|
|
20
|
-
},
|
|
21
|
-
to: { type: 'string' },
|
|
22
|
-
},
|
|
23
|
-
required: ['path', 'from', 'to'],
|
|
24
|
-
additionalProperties: false,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
display: {
|
|
29
|
-
verb: 'editing',
|
|
30
|
-
kind: 'diff',
|
|
31
|
-
fields: { path: 'path', from: 'from', to: 'to' },
|
|
32
|
-
},
|
|
33
|
-
permission: {
|
|
34
|
-
matchKey: (args) => args.path as string | undefined,
|
|
35
|
-
},
|
|
36
|
-
execute(args): ToolExecutorResult {
|
|
37
|
-
const path = sanitizePath(args.path as string, getCwd());
|
|
38
|
-
const oldString = args.from as string;
|
|
39
|
-
const newString = args.to as string;
|
|
40
|
-
|
|
41
|
-
if (!existsSync(path)) {
|
|
42
|
-
return { content: `Error: File not found: ${path}`, error: true };
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
const content = readFileSync(path, 'utf-8');
|
|
46
|
-
const count = content.split(oldString).length - 1;
|
|
47
|
-
if (count === 0) {
|
|
48
|
-
return { content: 'Error: "from" not found in file', error: true };
|
|
49
|
-
}
|
|
50
|
-
if (count > 1) {
|
|
51
|
-
return { content: `Error: "from" found ${count} times, must be unique`, error: true };
|
|
52
|
-
}
|
|
53
|
-
writeFileSync(path, content.replace(oldString, newString), 'utf-8');
|
|
54
|
-
return { content: `File edited: ${path}` };
|
|
55
|
-
} catch (err) {
|
|
56
|
-
return { content: `Error: ${err instanceof Error ? err.message : 'Unknown error'}`, error: true };
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mu-coding's filesystem + shell tools, packaged as a plugin. Replaces the
|
|
3
|
-
* legacy `createBuiltinPlugin` that lived in mu-core. Tools declare a
|
|
4
|
-
* `permission.matchKey` so agent definitions can authorise them via globs.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Plugin } from 'mu-core';
|
|
8
|
-
import { createBashTool } from './bash';
|
|
9
|
-
import { createEditFileTool } from './edit-file';
|
|
10
|
-
import { createReadFileTool } from './read-file';
|
|
11
|
-
import { createWriteFileTool } from './write-file';
|
|
12
|
-
|
|
13
|
-
export function createCodingToolsPlugin(): Plugin {
|
|
14
|
-
let pluginCwd: string | undefined;
|
|
15
|
-
const getCwd = (): string => pluginCwd ?? process.cwd();
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
name: 'mu-coding-tools',
|
|
19
|
-
version: '0.5.0',
|
|
20
|
-
tools: [
|
|
21
|
-
createReadFileTool(getCwd),
|
|
22
|
-
createWriteFileTool(getCwd),
|
|
23
|
-
createEditFileTool(getCwd),
|
|
24
|
-
createBashTool(getCwd),
|
|
25
|
-
],
|
|
26
|
-
systemPrompt: [
|
|
27
|
-
'File & shell tools:',
|
|
28
|
-
'- Prefer `read` over `cat`/`sed`; pass `start`/`end` for large files.',
|
|
29
|
-
'- Use `edit` for surgical changes; include enough context in `from` to be unique. One `edit` call per change site.',
|
|
30
|
-
'- Use `write` only for new files or full rewrites.',
|
|
31
|
-
'- Use `bash` for ops without a dedicated tool (ls, rg, build, tests). Avoid using it to read or rewrite files.',
|
|
32
|
-
].join('\n'),
|
|
33
|
-
activate(ctx) {
|
|
34
|
-
pluginCwd = ctx.cwd;
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export { createBashTool, createEditFileTool, createReadFileTool, createWriteFileTool };
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import type { PluginTool } from 'mu-core';
|
|
3
|
-
import { sanitizePath } from './utils';
|
|
4
|
-
|
|
5
|
-
function executeReadFileSingle(rawPath: string, cwd: string, start?: number, end?: number): string {
|
|
6
|
-
const path = sanitizePath(rawPath, cwd);
|
|
7
|
-
if (!existsSync(path)) {
|
|
8
|
-
return `Error: File not found: ${path}`;
|
|
9
|
-
}
|
|
10
|
-
try {
|
|
11
|
-
const content = readFileSync(path, 'utf-8');
|
|
12
|
-
const allLines = content.split('\n');
|
|
13
|
-
const totalLines = allLines.length;
|
|
14
|
-
|
|
15
|
-
const startLine = Math.max(1, start ?? 1);
|
|
16
|
-
const endLine = end ?? totalLines;
|
|
17
|
-
const clampedStart = Math.min(startLine, totalLines);
|
|
18
|
-
const clampedEnd = Math.min(endLine, totalLines);
|
|
19
|
-
|
|
20
|
-
if (clampedStart > clampedEnd) {
|
|
21
|
-
return `Error: start (${startLine}) > end (${endLine})`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const lines = allLines.slice(clampedStart - 1, clampedEnd);
|
|
25
|
-
const numbered = lines.map((line, i) => `${String(clampedStart + i).padStart(4)} │ ${line}`).join('\n');
|
|
26
|
-
const rangeLabel = start ? ` (lines ${clampedStart}-${clampedEnd})` : '';
|
|
27
|
-
const header = `── ${path}${rangeLabel} (${lines.length} lines) ──`;
|
|
28
|
-
return `${header}\n${numbered}`;
|
|
29
|
-
} catch (err) {
|
|
30
|
-
return `Error: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function createReadFileTool(getCwd: () => string): PluginTool {
|
|
35
|
-
return {
|
|
36
|
-
definition: {
|
|
37
|
-
type: 'function',
|
|
38
|
-
function: {
|
|
39
|
-
name: 'read',
|
|
40
|
-
description: 'Read text file(s) with line numbers. `path` may be a single path or array.',
|
|
41
|
-
parameters: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
properties: {
|
|
44
|
-
path: { type: ['string', 'array'], items: { type: 'string' } },
|
|
45
|
-
start: { type: 'integer', description: '1-indexed first line, inclusive.' },
|
|
46
|
-
end: { type: 'integer', description: '1-indexed last line, inclusive.' },
|
|
47
|
-
},
|
|
48
|
-
required: ['path'],
|
|
49
|
-
additionalProperties: false,
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
display: {
|
|
54
|
-
verb: 'reading',
|
|
55
|
-
kind: 'file-read',
|
|
56
|
-
fields: { path: 'path', start: 'start', end: 'end' },
|
|
57
|
-
},
|
|
58
|
-
permission: {
|
|
59
|
-
matchKey: (args) => {
|
|
60
|
-
const p = args.path;
|
|
61
|
-
if (typeof p === 'string') return p;
|
|
62
|
-
if (Array.isArray(p)) return p[0] as string | undefined;
|
|
63
|
-
return undefined;
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
execute(args) {
|
|
67
|
-
const paths = Array.isArray(args.path) ? (args.path as string[]) : [args.path as string];
|
|
68
|
-
const start = args.start as number | undefined;
|
|
69
|
-
const end = args.end as number | undefined;
|
|
70
|
-
const cwd = getCwd();
|
|
71
|
-
|
|
72
|
-
if (paths.length === 1) {
|
|
73
|
-
return executeReadFileSingle(paths[0], cwd, start, end);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const results: string[] = [];
|
|
77
|
-
for (const path of paths) {
|
|
78
|
-
results.push(executeReadFileSingle(path, cwd, start, end));
|
|
79
|
-
}
|
|
80
|
-
return results.join('\n\n');
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sanitize a file path from LLM arguments.
|
|
5
|
-
* Local models often wrap paths in extra quotes or add whitespace.
|
|
6
|
-
*
|
|
7
|
-
* When `cwd` is supplied, relative paths are resolved against it instead of
|
|
8
|
-
* `process.cwd()` — this lets the agent operate on a different working
|
|
9
|
-
* directory than the host process (the host passes its `PluginContext.cwd`
|
|
10
|
-
* into the builtin plugin factory at activation time).
|
|
11
|
-
*/
|
|
12
|
-
export function sanitizePath(raw: string, cwd?: string): string {
|
|
13
|
-
let p = raw.trim();
|
|
14
|
-
if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
|
|
15
|
-
p = p.slice(1, -1).trim();
|
|
16
|
-
}
|
|
17
|
-
if (cwd && !isAbsolute(p)) {
|
|
18
|
-
return resolve(cwd, p);
|
|
19
|
-
}
|
|
20
|
-
return p;
|
|
21
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import type { PluginTool } from 'mu-core';
|
|
3
|
-
import { sanitizePath } from './utils';
|
|
4
|
-
|
|
5
|
-
export function createWriteFileTool(getCwd: () => string): PluginTool {
|
|
6
|
-
return {
|
|
7
|
-
definition: {
|
|
8
|
-
type: 'function',
|
|
9
|
-
function: {
|
|
10
|
-
name: 'write',
|
|
11
|
-
description: 'Create or overwrite a file. Use `edit` for partial changes to existing files.',
|
|
12
|
-
parameters: {
|
|
13
|
-
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
path: { type: 'string' },
|
|
16
|
-
content: { type: 'string' },
|
|
17
|
-
},
|
|
18
|
-
required: ['path', 'content'],
|
|
19
|
-
additionalProperties: false,
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
display: {
|
|
24
|
-
verb: 'writing',
|
|
25
|
-
kind: 'file-write',
|
|
26
|
-
fields: { path: 'path', content: 'content' },
|
|
27
|
-
},
|
|
28
|
-
permission: {
|
|
29
|
-
matchKey: (args) => args.path as string | undefined,
|
|
30
|
-
},
|
|
31
|
-
execute(args) {
|
|
32
|
-
const path = sanitizePath(args.path as string, getCwd());
|
|
33
|
-
const content = args.content as string;
|
|
34
|
-
try {
|
|
35
|
-
writeFileSync(path, content, 'utf-8');
|
|
36
|
-
return { content: `File written: ${path}` };
|
|
37
|
-
} catch (err) {
|
|
38
|
-
return { content: `Error: ${err instanceof Error ? err.message : 'Unknown error'}`, error: true };
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|