mu-coding 0.4.0 → 0.8.0

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 (104) hide show
  1. package/README.md +49 -5
  2. package/bin/mu.js +1 -1
  3. package/package.json +17 -4
  4. package/prompts/SYSTEM.md +16 -0
  5. package/src/app/shutdown.ts +94 -0
  6. package/src/app/startApp.ts +43 -0
  7. package/src/cli/args.ts +131 -0
  8. package/src/{install.ts → cli/install.ts} +19 -15
  9. package/src/config/index.test.ts +77 -0
  10. package/src/config/index.ts +199 -0
  11. package/src/main.ts +4 -0
  12. package/src/plugin.ts +96 -0
  13. package/src/runtime/codingTools/bash.ts +114 -0
  14. package/src/runtime/codingTools/edit-file.ts +60 -0
  15. package/src/runtime/codingTools/index.ts +39 -0
  16. package/src/runtime/codingTools/read-file.ts +83 -0
  17. package/src/runtime/codingTools/utils.ts +21 -0
  18. package/src/runtime/codingTools/write-file.ts +42 -0
  19. package/src/runtime/createRegistry.test.ts +146 -0
  20. package/src/runtime/createRegistry.ts +163 -0
  21. package/src/runtime/messageBus.test.ts +62 -0
  22. package/src/runtime/messageBus.ts +78 -0
  23. package/src/runtime/pluginLoader.ts +122 -0
  24. package/src/sessions/index.test.ts +66 -0
  25. package/src/sessions/index.ts +183 -0
  26. package/src/sessions/peek.test.ts +88 -0
  27. package/src/sessions/project.ts +51 -0
  28. package/src/tui/channel/tuiChannel.test.ts +107 -0
  29. package/src/tui/channel/tuiChannel.ts +49 -0
  30. package/src/tui/{context/chat.ts → chat/ChatContext.ts} +1 -1
  31. package/src/tui/chat/MessageRendererContext.ts +44 -0
  32. package/src/tui/chat/ToolDisplayContext.ts +33 -0
  33. package/src/tui/{useAbort.ts → chat/useAbort.ts} +16 -7
  34. package/src/tui/chat/useAttachment.ts +74 -0
  35. package/src/tui/chat/useChat.ts +106 -0
  36. package/src/tui/chat/useChatPanel.ts +98 -0
  37. package/src/tui/chat/useChatSession.ts +284 -0
  38. package/src/tui/{useModelList.ts → chat/useModels.ts} +12 -2
  39. package/src/tui/chat/usePluginStatus.ts +44 -0
  40. package/src/tui/chat/useSessionPersistence.ts +68 -0
  41. package/src/tui/chat/useStatusSegments.ts +62 -0
  42. package/src/tui/components/chat/ChatPanel.tsx +20 -40
  43. package/src/tui/components/chat/ChatPanelBody.tsx +30 -52
  44. package/src/tui/components/chat/Pickers.tsx +2 -2
  45. package/src/tui/components/messageView.tsx +72 -0
  46. package/src/tui/components/messages/EditOutput.tsx +47 -30
  47. package/src/tui/components/messages/ReadOutput.tsx +27 -22
  48. package/src/tui/components/messages/ToolHeader.tsx +28 -0
  49. package/src/tui/components/messages/WriteOutput.tsx +12 -24
  50. package/src/tui/components/messages/assistantMessage.tsx +17 -2
  51. package/src/tui/components/messages/messageItem.tsx +23 -16
  52. package/src/tui/components/messages/reasoningBlock.tsx +4 -2
  53. package/src/tui/components/messages/streamingOutput.tsx +5 -1
  54. package/src/tui/components/messages/toolCallBlock.tsx +61 -38
  55. package/src/tui/components/messages/userMessage.tsx +21 -6
  56. package/src/tui/components/{ui → primitives}/dropdown.tsx +40 -11
  57. package/src/tui/components/{ui → primitives}/modal.tsx +4 -2
  58. package/src/tui/components/primitives/pickerModal.tsx +47 -0
  59. package/src/tui/components/primitives/scrollbar.tsx +27 -0
  60. package/src/tui/components/{ui → primitives}/toast.tsx +5 -3
  61. package/src/tui/components/statusBar.tsx +32 -0
  62. package/src/tui/components/ui/dialogLayer.tsx +32 -13
  63. package/src/tui/context/ThemeContext.tsx +18 -0
  64. package/src/tui/hooks/useScroll.ts +11 -3
  65. package/src/tui/input/InputBox.tsx +6 -0
  66. package/src/tui/input/InputBoxView.tsx +237 -0
  67. package/src/tui/input/commands.test.ts +51 -0
  68. package/src/tui/input/commands.ts +44 -0
  69. package/src/tui/input/cursor.test.ts +136 -0
  70. package/src/tui/input/cursor.ts +214 -0
  71. package/src/tui/input/dumpContext.ts +107 -0
  72. package/src/tui/input/sanitize.ts +33 -0
  73. package/src/tui/input/useCommandExecutor.ts +32 -0
  74. package/src/tui/input/useInputBox.ts +207 -0
  75. package/src/tui/input/useInputHandler.ts +453 -0
  76. package/src/tui/input/useMentionPicker.ts +121 -0
  77. package/src/tui/input/usePluginShortcuts.ts +29 -0
  78. package/src/tui/plugins/InkApprovalChannel.test.ts +51 -0
  79. package/src/tui/plugins/InkApprovalChannel.ts +30 -0
  80. package/src/tui/{services/uiService.ts → plugins/InkUIService.ts} +68 -35
  81. package/src/tui/renderApp.tsx +43 -0
  82. package/src/tui/theme/index.ts +1 -0
  83. package/src/tui/theme/merge.test.ts +49 -0
  84. package/src/tui/theme/merge.ts +43 -0
  85. package/src/tui/theme/presets.ts +79 -0
  86. package/src/tui/theme/types.ts +116 -0
  87. package/src/utils/clipboard.ts +97 -0
  88. package/src/utils/diff.test.ts +56 -0
  89. package/src/cli.ts +0 -96
  90. package/src/clipboard.ts +0 -62
  91. package/src/config.ts +0 -116
  92. package/src/main.tsx +0 -147
  93. package/src/project.ts +0 -32
  94. package/src/session.ts +0 -95
  95. package/src/tui/commands.ts +0 -33
  96. package/src/tui/components/chatLayout.tsx +0 -192
  97. package/src/tui/components/inputBox.tsx +0 -153
  98. package/src/tui/hooks/useInputHandler.ts +0 -268
  99. package/src/tui/useChat.ts +0 -52
  100. package/src/tui/useChatSession.ts +0 -155
  101. package/src/tui/useChatUI.ts +0 -51
  102. package/tsconfig.json +0 -10
  103. /package/src/{subcommands.ts → cli/subcommands.ts} +0 -0
  104. /package/src/{diff.ts → utils/diff.ts} +0 -0
package/src/cli.ts DELETED
@@ -1,96 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import type { ChatMessage } from 'mu-provider';
4
- import { getLatestSession, loadSession } from './session';
5
-
6
- interface CliArgs {
7
- model?: string;
8
- continueSession?: boolean;
9
- sessionPath?: string;
10
- }
11
-
12
- function printHelp(): never {
13
- console.log(`mu — minimal terminal AI assistant
14
-
15
- Usage:
16
- mu Start interactive chat
17
- mu -m model Interactive with specific model
18
- mu -c Continue most recent session
19
- mu --session <path> Resume a specific session file
20
- mu install npm:<package> Install a plugin from npm
21
- mu uninstall npm:<pkg> Remove an installed plugin
22
- mu -v, --version Print version and exit
23
-
24
- Config (XDG):
25
- ~/.config/mu/config.json — configuration (baseUrl, model, streamTimeoutMs)
26
- ~/.config/mu/SYSTEM.md — system prompt
27
- ~/.local/share/mu/sessions/ — saved conversation sessions (JSONL)
28
- ~/.cache/mu/repomap/ — code index cache
29
-
30
- Keyboard shortcuts (interactive):
31
- Ctrl+C Abort / Quit (press twice)
32
- Esc Stop generation (press twice while streaming)
33
- Enter Send message
34
- Shift+Enter New line
35
- Ctrl+S Send message
36
- ↑ / ↓ Navigate input history
37
- Ctrl+N New conversation
38
- Ctrl+M Cycle models
39
- Ctrl+O Model picker
40
- Ctrl+V Paste image from clipboard`);
41
- process.exit(0);
42
- }
43
-
44
- function printVersion(): never {
45
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
46
- console.log(`mu ${pkg.version}`);
47
- process.exit(0);
48
- }
49
-
50
- export function parseArgs(): CliArgs {
51
- const args = process.argv.slice(2);
52
- const result: CliArgs = {};
53
-
54
- for (let i = 0; i < args.length; i++) {
55
- const arg = args[i];
56
- if (arg === '-m' && args[i + 1]) {
57
- result.model = args[++i];
58
- } else if (arg === '-c' || arg === '--continue') {
59
- result.continueSession = true;
60
- } else if (arg === '--session' && args[i + 1]) {
61
- result.sessionPath = args[++i];
62
- } else if (arg === '-v' || arg === '--version') {
63
- printVersion();
64
- } else if (arg === '-h' || arg === '--help') {
65
- printHelp();
66
- }
67
- }
68
-
69
- return result;
70
- }
71
-
72
- export function resolveInitialMessages(cliArgs: CliArgs): ChatMessage[] | undefined {
73
- if (cliArgs.sessionPath) {
74
- const msgs = loadSession(cliArgs.sessionPath);
75
- if (msgs.length === 0) {
76
- console.error(`Error: session file is empty or not found: ${cliArgs.sessionPath}`);
77
- process.exit(1);
78
- }
79
- return msgs;
80
- }
81
- if (cliArgs.continueSession) {
82
- const latest = getLatestSession();
83
- if (!latest) {
84
- console.error('Error: no sessions found');
85
- process.exit(1);
86
- }
87
- const msgs = loadSession(latest);
88
- if (msgs.length === 0) {
89
- console.error('Error: latest session is empty');
90
- process.exit(1);
91
- }
92
- console.log(`Resuming session: ${latest}`);
93
- return msgs;
94
- }
95
- return undefined;
96
- }
package/src/clipboard.ts DELETED
@@ -1,62 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { existsSync, readFileSync, statSync, unlinkSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import type { ImageAttachment } from 'mu-provider';
6
-
7
- const CLIPBOARD_TIMEOUT = 3000;
8
-
9
- function tryExecSync(command: string): boolean {
10
- try {
11
- execSync(command, { stdio: 'pipe', timeout: CLIPBOARD_TIMEOUT });
12
- return true;
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- function extractPlatformImage(tmpFile: string): boolean {
19
- if (process.platform === 'darwin') {
20
- if (tryExecSync('which pngpaste')) {
21
- return tryExecSync(`pngpaste "${tmpFile}"`);
22
- }
23
- return tryExecSync(
24
- `osascript -e 'tell application "System Events" -e 'set imgData to the clipboard as «class PNGf»' -e 'set fp to open for access POSIX file "${tmpFile}" with write permission' -e 'write imgData to fp' -e 'close access fp' -e 'end tell'`,
25
- );
26
- }
27
- if (process.platform === 'linux') {
28
- if (tryExecSync(`xclip -selection clipboard -t image/png -o > "${tmpFile}"`)) {
29
- return true;
30
- }
31
- return tryExecSync(`wl-paste --type image/png > "${tmpFile}"`);
32
- }
33
- return false;
34
- }
35
-
36
- function readTmpAsAttachment(tmpFile: string): ImageAttachment | null {
37
- if (!existsSync(tmpFile)) {
38
- return null;
39
- }
40
- if (statSync(tmpFile).size === 0) {
41
- unlinkSync(tmpFile);
42
- return null;
43
- }
44
- const buffer = readFileSync(tmpFile);
45
- unlinkSync(tmpFile);
46
- return { data: buffer.toString('base64'), mimeType: 'image/png', name: 'clipboard.png' };
47
- }
48
-
49
- export function readClipboardImage(): ImageAttachment | null {
50
- const tmpFile = join(tmpdir(), `mu-clip-${Date.now()}.png`);
51
- try {
52
- if (!extractPlatformImage(tmpFile)) {
53
- return null;
54
- }
55
- return readTmpAsAttachment(tmpFile);
56
- } catch {
57
- if (existsSync(tmpFile)) {
58
- unlinkSync(tmpFile);
59
- }
60
- return null;
61
- }
62
- }
package/src/config.ts DELETED
@@ -1,116 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import type { ProviderConfig } from 'mu-provider';
5
-
6
- export interface AppConfig extends ProviderConfig {
7
- plugins?: Array<string | { name: string; config?: Record<string, unknown> }>;
8
- }
9
-
10
- export function getPluginsDir(): string {
11
- return join(CONFIG_DIR, 'plugins');
12
- }
13
-
14
- // XDG Base Directory paths
15
- const HOME = homedir();
16
- const CONFIG_DIR = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'mu') : join(HOME, '.config', 'mu');
17
- const DATA_DIR = process.env.XDG_DATA_HOME
18
- ? join(process.env.XDG_DATA_HOME, 'mu')
19
- : join(HOME, '.local', 'share', 'mu');
20
- const CACHE_DIR = process.env.XDG_CACHE_HOME ? join(process.env.XDG_CACHE_HOME, 'mu') : join(HOME, '.cache', 'mu');
21
-
22
- const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
23
- const SYSTEM_PROMPT_PATH = join(CONFIG_DIR, 'SYSTEM.md');
24
-
25
- export function getConfigDir(): string {
26
- return CONFIG_DIR;
27
- }
28
-
29
- export function getDataDir(): string {
30
- return DATA_DIR;
31
- }
32
-
33
- export function getCacheDir(): string {
34
- return CACHE_DIR;
35
- }
36
-
37
- function tryRead(path: string): string | undefined {
38
- try {
39
- return readFileSync(path, 'utf-8').trim() || undefined;
40
- } catch {
41
- return undefined;
42
- }
43
- }
44
-
45
- function tryParseJson(text: string | undefined): Partial<AppConfig> {
46
- if (!text) {
47
- return {};
48
- }
49
- try {
50
- return JSON.parse(text);
51
- } catch {
52
- return {};
53
- }
54
- }
55
-
56
- function envInt(key: string): number | undefined {
57
- const v = process.env[key];
58
- if (!v) {
59
- return undefined;
60
- }
61
- const n = Number.parseInt(v, 10);
62
- return Number.isNaN(n) ? undefined : n;
63
- }
64
-
65
- function envFloat(key: string): number | undefined {
66
- const v = process.env[key];
67
- if (!v) {
68
- return undefined;
69
- }
70
- const n = Number.parseFloat(v);
71
- return Number.isNaN(n) ? undefined : n;
72
- }
73
-
74
- export function loadConfig(cliModel?: string): AppConfig {
75
- const file = tryParseJson(tryRead(CONFIG_PATH));
76
-
77
- const config: AppConfig = {
78
- baseUrl: process.env.MU_BASE_URL || file.baseUrl || 'http://localhost:8080/v1',
79
- model: cliModel || process.env.MU_MODEL || file.model,
80
- maxTokens: envInt('MU_MAX_TOKENS') ?? file.maxTokens ?? 4096,
81
- temperature: envFloat('MU_TEMPERATURE') ?? file.temperature ?? 0.7,
82
- streamTimeoutMs: envInt('MU_STREAM_TIMEOUT') ?? file.streamTimeoutMs ?? 60_000,
83
- systemPrompt: process.env.MU_SYSTEM_PROMPT || file.systemPrompt || tryRead(SYSTEM_PROMPT_PATH),
84
- plugins: file.plugins,
85
- };
86
-
87
- if (!existsSync(CONFIG_PATH)) {
88
- mkdirSync(CONFIG_DIR, { recursive: true });
89
- const KEYS = [
90
- 'baseUrl',
91
- 'model',
92
- 'maxTokens',
93
- 'temperature',
94
- 'streamTimeoutMs',
95
- 'systemPrompt',
96
- 'plugins',
97
- ] as const;
98
- const fileConfig = Object.fromEntries(
99
- KEYS.filter((k) => file[k] !== undefined).map((k) => [k, file[k]]),
100
- ) as Partial<AppConfig>;
101
- writeFileSync(CONFIG_PATH, JSON.stringify(fileConfig, null, 2), 'utf-8');
102
- }
103
-
104
- return config;
105
- }
106
-
107
- export function saveConfig(updates: Partial<AppConfig>): void {
108
- const file = tryParseJson(tryRead(CONFIG_PATH));
109
- const merged = { ...file, ...updates };
110
- const KEYS = ['baseUrl', 'model', 'maxTokens', 'temperature', 'streamTimeoutMs', 'systemPrompt', 'plugins'] as const;
111
- const fileConfig = Object.fromEntries(
112
- KEYS.filter((k) => merged[k] !== undefined).map((k) => [k, merged[k]]),
113
- ) as Partial<AppConfig>;
114
- mkdirSync(CONFIG_DIR, { recursive: true });
115
- writeFileSync(CONFIG_PATH, JSON.stringify(fileConfig, null, 2), 'utf-8');
116
- }
package/src/main.tsx DELETED
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { readdirSync } from 'node:fs';
3
- import { createRequire } from 'node:module';
4
- import { join, resolve } from 'node:path';
5
- import { render } from 'ink';
6
- import { createBuiltinPlugin, PluginRegistry } from 'mu-agents';
7
- import { parseArgs, resolveInitialMessages } from './cli';
8
- import { type AppConfig, getDataDir, getPluginsDir, loadConfig } from './config';
9
- import { handleSubcommand } from './subcommands';
10
- import { ChatPanel } from './tui/components/chat/ChatPanel';
11
- import { InkUIService } from './tui/services/uiService';
12
-
13
- function discoverPluginFiles(): string[] {
14
- const dir = getPluginsDir();
15
- try {
16
- return readdirSync(dir)
17
- .filter((f) => f.endsWith('.ts'))
18
- .map((f) => join(dir, f));
19
- } catch {
20
- return [];
21
- }
22
- }
23
-
24
- /**
25
- * Resolve an npm: specifier to an absolute path via the data dir's node_modules.
26
- */
27
- function formatPluginError(name: string, err: unknown): string {
28
- const parts: string[] = [`Plugin "${name}" failed`];
29
- let current: unknown = err;
30
- while (current) {
31
- if (current instanceof Error) {
32
- parts.push(current.message);
33
- current = current.cause;
34
- } else {
35
- parts.push(String(current));
36
- break;
37
- }
38
- }
39
- return parts.join(': ');
40
- }
41
-
42
- function resolveNpmPlugin(specifier: string): string {
43
- const bare = specifier.slice(4);
44
- const dataDir = getDataDir();
45
- try {
46
- const require = createRequire(resolve(dataDir, 'package.json'));
47
- return require.resolve(bare);
48
- } catch (err) {
49
- throw new Error(`Cannot resolve "${bare}" from ${dataDir}/node_modules — is it installed?`, { cause: err });
50
- }
51
- }
52
-
53
- /**
54
- * Load a plugin by name or path, resolving from this package's context.
55
- * This allows workspace packages (like mu-pi-compat) to be found even though
56
- * mu-agents' registry can't resolve them from its own location.
57
- *
58
- * Plugins prefixed with npm: are resolved from ~/.local/share/mu/node_modules/.
59
- */
60
- async function loadPluginFromHere(
61
- registry: PluginRegistry,
62
- name: string,
63
- pluginConfig?: Record<string, unknown>,
64
- uiService?: InkUIService,
65
- ): Promise<void> {
66
- try {
67
- const target = name.startsWith('npm:') ? resolveNpmPlugin(name) : name;
68
- const mod = await import(target);
69
- const factory = mod.default ?? mod.createPlugin;
70
-
71
- if (typeof factory === 'function') {
72
- const plugin = factory(pluginConfig ?? {});
73
- await registry.register(plugin);
74
- } else if (typeof mod === 'object' && mod !== null && 'name' in mod) {
75
- await registry.register(mod);
76
- } else {
77
- const exportKeys = Object.keys(mod).join(', ') || '(none)';
78
- uiService?.notify(`Plugin "${name}": no plugin export found. Exports: [${exportKeys}]`, 'error');
79
- }
80
- } catch (err) {
81
- // npm: plugins don't fall back — they must resolve from data dir
82
- if (name.startsWith('npm:')) {
83
- uiService?.notify(formatPluginError(name, err), 'error');
84
- return;
85
- }
86
- // Non-npm plugins fall back to registry loader (for file paths)
87
- try {
88
- await registry.loadPlugin(name, pluginConfig);
89
- } catch (fallbackErr) {
90
- uiService?.notify(formatPluginError(name, fallbackErr), 'error');
91
- }
92
- }
93
- }
94
-
95
- async function createRegistry(cwd: string, config: AppConfig, uiService: InkUIService) {
96
- const registry = new PluginRegistry({ cwd, config: {} });
97
-
98
- // Register built-in tools (read, write, edit, bash)
99
- await registry.register(createBuiltinPlugin());
100
-
101
- // Auto-load .ts plugin files from ~/.config/mu/plugins/
102
- for (const filePath of discoverPluginFiles()) {
103
- await registry.loadPlugin(filePath);
104
- }
105
-
106
- // Load configured plugins
107
- if (config.plugins?.length) {
108
- for (const entry of config.plugins) {
109
- const name = typeof entry === 'string' ? entry : entry.name;
110
- const pluginConfig = typeof entry === 'string' ? undefined : entry.config;
111
-
112
- // Inject uiService for plugins that accept it (duck typing)
113
- const finalConfig = pluginConfig ? { ...pluginConfig, ui: uiService } : { ui: uiService };
114
-
115
- await loadPluginFromHere(registry, name, finalConfig, uiService);
116
- }
117
- }
118
-
119
- return registry;
120
- }
121
-
122
- async function main() {
123
- if (await handleSubcommand()) return;
124
-
125
- const cliArgs = parseArgs();
126
- const config = loadConfig(cliArgs.model);
127
- const root = process.cwd();
128
-
129
- const uiService = new InkUIService();
130
- const registry = await createRegistry(root, config, uiService);
131
-
132
- const initialMessages = resolveInitialMessages(cliArgs);
133
-
134
- render(<ChatPanel config={config} initialMessages={initialMessages} registry={registry} uiService={uiService} />, {
135
- exitOnCtrlC: false,
136
- kittyKeyboard: { mode: 'enabled' },
137
- });
138
-
139
- process.on('exit', () => {
140
- registry.shutdown();
141
- });
142
- }
143
-
144
- main().catch((err) => {
145
- console.error(err);
146
- process.exit(1);
147
- });
package/src/project.ts DELETED
@@ -1,32 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
-
4
- function findGitRoot(from: string): string | null {
5
- try {
6
- const root = execSync('git rev-parse --show-toplevel', {
7
- cwd: from,
8
- encoding: 'utf-8',
9
- stdio: ['pipe', 'pipe', 'pipe'],
10
- }).trim();
11
- return root || null;
12
- } catch {
13
- return null;
14
- }
15
- }
16
-
17
- function getProjectRoot(): string {
18
- const cwd = process.cwd();
19
- return findGitRoot(cwd) ?? cwd;
20
- }
21
-
22
- export function getProjectId(): string {
23
- const root = getProjectRoot();
24
- const hash = createHash('sha256').update(root).digest('hex').slice(0, 12);
25
- const name = root.split('/').pop() || 'unknown';
26
- return `${name}-${hash}`;
27
- }
28
-
29
- export function getProjectName(): string {
30
- const root = getProjectRoot();
31
- return root.split('/').pop() || root;
32
- }
package/src/session.ts DELETED
@@ -1,95 +0,0 @@
1
- import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import type { ChatMessage } from 'mu-provider';
4
- import { getDataDir } from './config';
5
- import { getProjectId, getProjectName } from './project';
6
-
7
- function getProjectSessionsDir(): string {
8
- return join(getDataDir(), 'sessions', getProjectId());
9
- }
10
-
11
- function getSortedSessionFiles(): string[] {
12
- try {
13
- const dir = getProjectSessionsDir();
14
- return readdirSync(dir)
15
- .filter((f) => f.endsWith('.jsonl'))
16
- .sort()
17
- .reverse();
18
- } catch {
19
- return [];
20
- }
21
- }
22
-
23
- export interface SessionInfo {
24
- path: string;
25
- name: string;
26
- date: Date;
27
- messageCount: number;
28
- preview: string;
29
- project: string;
30
- }
31
-
32
- export function generateSessionPath(): string {
33
- const dir = getProjectSessionsDir();
34
- mkdirSync(dir, { recursive: true });
35
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
36
- return join(dir, `${ts}.jsonl`);
37
- }
38
-
39
- export function saveSession(path: string, messages: ChatMessage[]): void {
40
- writeFileSync(path, `${messages.map((m) => JSON.stringify(m)).join('\n')}\n`, 'utf-8');
41
- }
42
-
43
- export function loadSession(path: string): ChatMessage[] {
44
- try {
45
- const content = readFileSync(path, 'utf-8').trim();
46
- if (!content) {
47
- return [];
48
- }
49
- return content
50
- .split('\n')
51
- .map((line) => {
52
- try {
53
- return JSON.parse(line) as ChatMessage;
54
- } catch {
55
- return null;
56
- }
57
- })
58
- .filter((msg): msg is ChatMessage => msg !== null);
59
- } catch {
60
- return [];
61
- }
62
- }
63
-
64
- export function getLatestSession(): string | null {
65
- const files = getSortedSessionFiles();
66
- return files.length ? join(getProjectSessionsDir(), files[0]) : null;
67
- }
68
-
69
- export function listSessions(): SessionInfo[] {
70
- try {
71
- const dir = getProjectSessionsDir();
72
- mkdirSync(dir, { recursive: true });
73
- const files = getSortedSessionFiles();
74
- const project = getProjectName();
75
-
76
- return files.map((file) => {
77
- const path = join(dir, file);
78
- const stat = statSync(path);
79
- const messages = loadSession(path);
80
- const firstUserMsg = messages.find((m) => m.role === 'user');
81
- const preview = firstUserMsg ? firstUserMsg.content.slice(0, 80).replace(/\n/g, ' ') : '(empty)';
82
-
83
- return {
84
- path,
85
- name: file.replace('.jsonl', ''),
86
- date: stat.mtime,
87
- messageCount: messages.length,
88
- preview,
89
- project,
90
- };
91
- });
92
- } catch {
93
- return [];
94
- }
95
- }
@@ -1,33 +0,0 @@
1
- import type { CommandContext, SlashCommand as PluginSlashCommand } from 'mu-agents';
2
-
3
- export interface SlashCommand {
4
- name: string;
5
- description: string;
6
- action?: string;
7
- execute?: (args: string) => Promise<string | undefined>;
8
- }
9
-
10
- const BUILTIN_COMMANDS: SlashCommand[] = [
11
- { name: '/model', description: 'Select a model', action: 'model' },
12
- { name: '/sessions', description: 'List project sessions', action: 'sessions' },
13
- { name: '/new', description: 'New conversation', action: 'new' },
14
- ];
15
-
16
- export function matchCommands(input: string, pluginCommands: PluginSlashCommand[] = []): SlashCommand[] {
17
- if (!input.startsWith('/')) {
18
- return [];
19
- }
20
- const q = input.toLowerCase();
21
-
22
- const fromPlugins: SlashCommand[] = pluginCommands.map((pc) => ({
23
- name: `/${pc.name}`,
24
- description: pc.description,
25
- execute: (args: string) => {
26
- const ctx: CommandContext = { messages: [], cwd: process.cwd(), config: {} as CommandContext['config'] };
27
- return pc.execute(args, ctx);
28
- },
29
- }));
30
-
31
- const all = [...BUILTIN_COMMANDS, ...fromPlugins];
32
- return all.filter((cmd) => cmd.name.startsWith(q));
33
- }