openbot 0.3.6 → 0.4.2

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 +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +10 -19
  5. package/dist/app/server.js +208 -17
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +109 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +120 -149
  27. package/dist/plugins/bash/index.js +195 -0
  28. package/dist/plugins/delegation/index.js +121 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +125 -0
  32. package/dist/plugins/openbot/history.js +144 -0
  33. package/dist/plugins/openbot/index.js +71 -0
  34. package/dist/plugins/openbot/runtime.js +381 -0
  35. package/dist/plugins/openbot/system-prompt.js +25 -0
  36. package/dist/plugins/plugin-manager/index.js +189 -0
  37. package/dist/plugins/shell/index.js +2 -1
  38. package/dist/plugins/storage/files.js +67 -0
  39. package/dist/plugins/storage/index.js +750 -0
  40. package/dist/plugins/storage/service.js +1316 -0
  41. package/dist/plugins/storage-tools/index.js +2 -2
  42. package/dist/plugins/thread-namer/index.js +72 -0
  43. package/dist/plugins/thread-naming/generate-title.js +44 -0
  44. package/dist/plugins/thread-naming/index.js +103 -0
  45. package/dist/plugins/threads/index.js +114 -0
  46. package/dist/plugins/todo/index.js +24 -25
  47. package/dist/plugins/ui/index.js +109 -180
  48. package/dist/registry/plugins.js +3 -9
  49. package/dist/services/abort.js +43 -0
  50. package/dist/services/plugins/domain.js +1 -0
  51. package/dist/services/plugins/plugin-cache.js +9 -0
  52. package/dist/services/plugins/registry.js +112 -0
  53. package/dist/services/plugins/service.js +232 -0
  54. package/dist/services/plugins/types.js +1 -0
  55. package/dist/services/process.js +29 -0
  56. package/dist/services/storage.js +11 -10
  57. package/dist/services/thread-naming.js +81 -0
  58. package/docs/agents.md +15 -12
  59. package/docs/architecture.md +2 -2
  60. package/docs/plugins.md +29 -17
  61. package/docs/templates/AGENT.example.md +8 -14
  62. package/package.json +1 -2
  63. package/src/app/agent-ids.ts +5 -0
  64. package/src/app/cli.ts +1 -1
  65. package/src/app/config.ts +14 -31
  66. package/src/app/server.ts +243 -19
  67. package/src/app/types.ts +331 -187
  68. package/src/harness/index.ts +166 -0
  69. package/src/plugins/approval/index.ts +107 -188
  70. package/src/plugins/bash/index.ts +232 -0
  71. package/src/plugins/delegation/index.ts +139 -39
  72. package/src/plugins/memory/index.ts +112 -15
  73. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  74. package/src/plugins/openbot/context.ts +140 -0
  75. package/src/plugins/openbot/history.ts +158 -0
  76. package/src/plugins/openbot/index.ts +79 -0
  77. package/src/plugins/openbot/runtime.ts +478 -0
  78. package/src/plugins/openbot/system-prompt.ts +27 -0
  79. package/src/plugins/plugin-manager/index.ts +224 -0
  80. package/src/plugins/storage/files.ts +81 -0
  81. package/src/plugins/storage/index.ts +823 -0
  82. package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
  83. package/src/plugins/ui/index.ts +117 -221
  84. package/src/services/abort.ts +46 -0
  85. package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
  86. package/src/services/plugins/plugin-cache.ts +13 -0
  87. package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
  88. package/src/services/plugins/service.ts +318 -0
  89. package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
  90. package/src/bus/services.ts +0 -954
  91. package/src/harness/context.ts +0 -365
  92. package/src/harness/dispatcher.ts +0 -379
  93. package/src/harness/mcp.ts +0 -78
  94. package/src/harness/runtime-factory.ts +0 -129
  95. package/src/harness/todo-advance.ts +0 -128
  96. package/src/plugins/ai-sdk/index.ts +0 -41
  97. package/src/plugins/ai-sdk/runtime.ts +0 -468
  98. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  99. package/src/plugins/mcp/index.ts +0 -128
  100. package/src/plugins/shell/index.ts +0 -123
  101. package/src/plugins/storage-tools/index.ts +0 -90
  102. package/src/plugins/todo/index.ts +0 -64
  103. package/src/services/plugins.ts +0 -133
  104. /package/src/{harness → services}/process.ts +0 -0
@@ -1,128 +0,0 @@
1
- import { MelonyPlugin } from 'melony';
2
- import z from 'zod';
3
- import type { Plugin } from '../../bus/plugin.js';
4
- import { OpenBotEvent, OpenBotState } from '../../app/types.js';
5
- import { mcpService } from '../../harness/mcp.js';
6
-
7
- function stringifyResult(value: unknown): string {
8
- if (typeof value === 'string') return value;
9
- try {
10
- return JSON.stringify(value, null, 2);
11
- } catch {
12
- return String(value);
13
- }
14
- }
15
-
16
- const mcpToolDefinitions = {
17
- mcp_list_tools: {
18
- description:
19
- 'List available tools from a configured MCP server. Use this first before calling tools on an unknown server.',
20
- inputSchema: z.object({
21
- serverId: z.string().describe('Configured MCP server id (e.g. github, notion, linear).'),
22
- }),
23
- },
24
- mcp_call: {
25
- description:
26
- 'Call a tool on a configured MCP server. Provide tool arguments as a JSON object. Use mcp_list_tools first when uncertain.',
27
- inputSchema: z.object({
28
- serverId: z.string().describe('Configured MCP server id.'),
29
- toolName: z.string().describe('Exact MCP tool name from mcp_list_tools.'),
30
- args: z
31
- .record(z.string(), z.unknown())
32
- .default({})
33
- .describe('Tool arguments as a JSON object.'),
34
- }),
35
- },
36
- };
37
-
38
- const mcpPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
39
- builder.on('action:mcp_list_tools', async function* (event, context) {
40
- const serverId = (event.data as { serverId?: string })?.serverId as string;
41
-
42
- try {
43
- const tools = await mcpService.listTools(serverId);
44
- const toolNames = tools.map(
45
- (tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ''}`,
46
- );
47
-
48
- yield {
49
- type: 'action:mcp_list_tools:result',
50
- data: { success: true, serverId, tools },
51
- meta: event.meta,
52
- } as OpenBotEvent;
53
-
54
- yield {
55
- type: 'agent:output',
56
- data: {
57
- content:
58
- toolNames.length > 0
59
- ? `MCP tools available on \`${serverId}\`:\n${toolNames.join('\n')}`
60
- : `MCP server \`${serverId}\` has no tools.`,
61
- },
62
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
63
- } as OpenBotEvent;
64
- } catch (error) {
65
- const message = error instanceof Error ? error.message : 'Unknown MCP error';
66
- yield {
67
- type: 'action:mcp_list_tools:result',
68
- data: { success: false, serverId, tools: [], error: message },
69
- meta: event.meta,
70
- } as OpenBotEvent;
71
- yield {
72
- type: 'agent:output',
73
- data: { content: `Failed to list MCP tools for \`${serverId}\`: ${message}` },
74
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
75
- } as OpenBotEvent;
76
- }
77
- });
78
-
79
- builder.on('action:mcp_call', async function* (event, context) {
80
- const data = event.data as {
81
- serverId?: string;
82
- toolName?: string;
83
- args?: Record<string, unknown>;
84
- };
85
- const serverId = data?.serverId as string;
86
- const toolName = data?.toolName as string;
87
- const args = (data?.args || {}) as Record<string, unknown>;
88
-
89
- try {
90
- const result = await mcpService.callTool(serverId, toolName, args);
91
- const rendered = stringifyResult(result);
92
-
93
- yield {
94
- type: 'action:mcp_call:result',
95
- data: { success: true, serverId, toolName, result },
96
- meta: event.meta,
97
- } as OpenBotEvent;
98
-
99
- yield {
100
- type: 'agent:output',
101
- data: { content: `MCP \`${serverId}.${toolName}\` result:\n\n${rendered}` },
102
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
103
- } as OpenBotEvent;
104
- } catch (error) {
105
- const message = error instanceof Error ? error.message : 'Unknown MCP error';
106
- yield {
107
- type: 'action:mcp_call:result',
108
- data: { success: false, serverId, toolName, error: message },
109
- meta: event.meta,
110
- } as OpenBotEvent;
111
- yield {
112
- type: 'agent:output',
113
- data: { content: `MCP call failed for \`${serverId}.${toolName}\`: ${message}` },
114
- meta: { ...(event.meta || {}), agentId: context.state.agentId },
115
- } as OpenBotEvent;
116
- }
117
- });
118
- };
119
-
120
- export const mcpPlugin: Plugin = {
121
- id: 'mcp',
122
- name: 'MCP',
123
- description: 'Connect to Model Context Protocol servers and call their tools.',
124
- toolDefinitions: mcpToolDefinitions,
125
- factory: () => mcpPluginRuntime(),
126
- };
127
-
128
- export default mcpPlugin;
@@ -1,123 +0,0 @@
1
- import { MelonyPlugin } from 'melony';
2
- import { z } from 'zod';
3
- import { spawn } from 'node:child_process';
4
- import type { Plugin } from '../../bus/plugin.js';
5
- import { OpenBotEvent, OpenBotState } from '../../app/types.js';
6
-
7
- const shellToolDefinitions = {
8
- shell_exec: {
9
- description:
10
- 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
11
- inputSchema: z.object({
12
- command: z.string().describe('The shell command to execute.'),
13
- cwd: z
14
- .string()
15
- .optional()
16
- .describe(
17
- 'Working directory. Defaults to the channel cwd or workspace root. Leave empty unless the user requests a specific directory.',
18
- ),
19
- shell: z.enum(['bash', 'sh', 'zsh']).optional().describe('Shell to use. Defaults to bash.'),
20
- timeoutMs: z
21
- .number()
22
- .optional()
23
- .default(30000)
24
- .describe('Maximum execution time in milliseconds. Defaults to 30000 (30s).'),
25
- }),
26
- },
27
- };
28
-
29
- const shellPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
30
- builder.on('action:shell_exec', async function* (event, context) {
31
- const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
32
-
33
- const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
34
- const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
35
-
36
- try {
37
- const result = await new Promise<{
38
- exitCode: number | null;
39
- stdout: string;
40
- stderr: string;
41
- timedOut: boolean;
42
- }>((resolve) => {
43
- const child = spawn(command, {
44
- shell,
45
- cwd: actualCwd,
46
- env: { ...process.env },
47
- });
48
-
49
- let stdout = '';
50
- let stderr = '';
51
- let timedOut = false;
52
-
53
- const timer = setTimeout(() => {
54
- timedOut = true;
55
- child.kill();
56
- }, actualTimeout);
57
-
58
- child.stdout.on('data', (data) => {
59
- stdout += data.toString();
60
- if (stdout.length > 100000) {
61
- stdout = stdout.substring(0, 100000) + '\n... [output truncated]';
62
- child.kill();
63
- }
64
- });
65
-
66
- child.stderr.on('data', (data) => {
67
- stderr += data.toString();
68
- if (stderr.length > 100000) {
69
- stderr = stderr.substring(0, 100000) + '\n... [output truncated]';
70
- }
71
- });
72
-
73
- child.on('close', (code) => {
74
- clearTimeout(timer);
75
- resolve({ exitCode: code, stdout, stderr, timedOut });
76
- });
77
-
78
- child.on('error', (err) => {
79
- clearTimeout(timer);
80
- resolve({ exitCode: -1, stdout, stderr: stderr + err.message, timedOut: false });
81
- });
82
- });
83
-
84
- const success = result.exitCode === 0 && !result.timedOut;
85
-
86
- yield {
87
- type: 'action:shell_exec:result',
88
- data: {
89
- success,
90
- exitCode: result.exitCode,
91
- stdout: result.stdout,
92
- stderr: result.stderr,
93
- timedOut: result.timedOut,
94
- },
95
- meta: event.meta,
96
- } as OpenBotEvent;
97
- } catch (error) {
98
- const message = error instanceof Error ? error.message : 'Unknown shell error';
99
- yield {
100
- type: 'action:shell_exec:result',
101
- data: {
102
- success: false,
103
- exitCode: -1,
104
- stdout: '',
105
- stderr: message,
106
- timedOut: false,
107
- error: message,
108
- },
109
- meta: event.meta,
110
- } as OpenBotEvent;
111
- }
112
- });
113
- };
114
-
115
- export const shellPlugin: Plugin = {
116
- id: 'shell',
117
- name: 'Shell',
118
- description: 'Execute shell commands in the channel workspace.',
119
- toolDefinitions: shellToolDefinitions,
120
- factory: () => shellPluginRuntime(),
121
- };
122
-
123
- export default shellPlugin;
@@ -1,90 +0,0 @@
1
- import z from 'zod';
2
- import type { Plugin } from '../../bus/plugin.js';
3
-
4
- /**
5
- * `storage-tools` — exposes channel/thread/variable mutation tools to runtime
6
- * plugins. The actual handlers live in `bus/services.ts` because storage is
7
- * platform infrastructure, not agent behaviour.
8
- */
9
- const storageToolDefinitions = {
10
- create_channel: {
11
- description:
12
- 'Create a new channel. Use when the user intent is clearly different from the current channel and should be split. Always confirm before creating. Skip for simple Q&A.',
13
- inputSchema: z.object({
14
- channelId: z
15
- .string()
16
- .describe('Unique channel ID (e.g. product-launch, backend-platform, channel_roadmap).'),
17
- spec: z
18
- .string()
19
- .optional()
20
- .describe('Optional initial markdown content for the channel spec.'),
21
- initialState: z
22
- .record(z.string(), z.unknown())
23
- .optional()
24
- .describe('Optional initial state object for the channel.'),
25
- cwd: z
26
- .string()
27
- .optional()
28
- .describe('Optional initial current working directory for the channel.'),
29
- }),
30
- },
31
- patch_channel_details: {
32
- description: 'Patch current channel details (state, spec, cwd).',
33
- inputSchema: z
34
- .object({
35
- state: z
36
- .record(z.string(), z.unknown())
37
- .optional()
38
- .describe(
39
- 'JSON state object for the channel. Use for structured data like `todos` or metadata.',
40
- ),
41
- spec: z
42
- .string()
43
- .optional()
44
- .describe(
45
- 'Markdown content for the channel specification (SPEC.md). Use for goals and rules.',
46
- ),
47
- cwd: z.string().optional().describe('Current working directory for the channel.'),
48
- })
49
- .refine(
50
- (value) => value.state !== undefined || value.spec !== undefined || value.cwd !== undefined,
51
- { message: 'Provide at least one of state, spec, or cwd.' },
52
- ),
53
- },
54
- patch_thread_details: {
55
- description: 'Patch current thread details (state).',
56
- inputSchema: z.object({
57
- state: z
58
- .record(z.string(), z.unknown())
59
- .describe(
60
- 'JSON state object for the thread. Use for structured data like `todos` or progress.',
61
- ),
62
- }),
63
- },
64
- create_variable: {
65
- description: 'Create or update a variable in the workspace storage.',
66
- inputSchema: z.object({
67
- key: z.string().describe('The key of the variable.'),
68
- value: z.string().describe('The value of the variable.'),
69
- secret: z.boolean().optional().describe('Whether the variable is a secret.'),
70
- }),
71
- },
72
- delete_variable: {
73
- description: 'Delete a variable from the workspace storage.',
74
- inputSchema: z.object({
75
- key: z.string().describe('The key of the variable to delete.'),
76
- }),
77
- },
78
- };
79
-
80
- export const storageToolsPlugin: Plugin = {
81
- id: 'storage-tools',
82
- name: 'Storage Tools',
83
- description: 'Tools for creating channels, patching state, and managing workspace variables.',
84
- toolDefinitions: storageToolDefinitions,
85
- factory: () => () => {
86
- // Handlers live in bus/services.ts; this plugin only contributes tool definitions.
87
- },
88
- };
89
-
90
- export default storageToolsPlugin;
@@ -1,64 +0,0 @@
1
- import z from 'zod';
2
- import type { Plugin } from '../../bus/plugin.js';
3
-
4
- /**
5
- * `todo` — shared, per-thread task list for autonomous multi-agent flows.
6
- *
7
- * Todos live in `threadDetails.state.todos` and are owned by the system
8
- * (handlers in `bus/services.ts`). Any agent in the thread can read the
9
- * list via context, and propose mutations through these tools. Each item
10
- * may carry an `assignee` agent id; combine with `handoff` to drive an
11
- * autonomous, multi-step plan across agents.
12
- *
13
- * Keep the surface minimal: two tools (replace-all, patch-one) cover plan
14
- * authoring, status transitions, and reassignment.
15
- */
16
- const todoStatus = z.enum(['pending', 'in_progress', 'done', 'cancelled']);
17
-
18
- const todoToolDefinitions = {
19
- todo_write: {
20
- description:
21
- 'Author or rewrite the shared todo plan for the current thread. Pass the full ordered list — missing items are removed. Use at the start of multi-step work, or whenever the plan changes shape. For status flips, prefer `todo_update`.',
22
- inputSchema: z.object({
23
- todos: z
24
- .array(
25
- z.object({
26
- id: z
27
- .string()
28
- .optional()
29
- .describe('Stable id. Reuse existing ids to preserve history; omit to create.'),
30
- content: z.string().min(1).describe('What needs to be done. One concrete step.'),
31
- status: todoStatus.optional().describe('Defaults to `pending`.'),
32
- assignee: z
33
- .string()
34
- .optional()
35
- .describe('Agent id responsible for this step. Pair with `handoff` to delegate.'),
36
- }),
37
- )
38
- .describe('The complete, ordered plan.'),
39
- }),
40
- },
41
- todo_update: {
42
- description:
43
- 'Patch a single todo by id. Use to mark progress (`in_progress`, `done`, `cancelled`), rephrase, or reassign without rewriting the whole list.',
44
- inputSchema: z.object({
45
- id: z.string().describe('Todo id from `todo_write` or the rendered list.'),
46
- status: todoStatus.optional(),
47
- content: z.string().min(1).optional(),
48
- assignee: z.string().optional().describe('Use empty string to clear.'),
49
- }),
50
- },
51
- };
52
-
53
- export const todoPlugin: Plugin = {
54
- id: 'todo',
55
- name: 'Todo',
56
- description:
57
- 'Shared per-thread task list for coordinating multi-step, multi-agent work.',
58
- toolDefinitions: todoToolDefinitions,
59
- factory: () => () => {
60
- // Handlers live in bus/services.ts; this plugin only contributes tool definitions.
61
- },
62
- };
63
-
64
- export default todoPlugin;
@@ -1,133 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import path from 'node:path';
4
- import { exec } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import {
7
- DEFAULT_PLUGINS_DIR,
8
- DEFAULT_BASE_DIR,
9
- loadConfig,
10
- resolvePath,
11
- } from '../app/config.js';
12
- import { invalidatePlugin } from '../registry/plugins.js';
13
-
14
- const execAsync = promisify(exec);
15
-
16
- export interface InstallOptions {
17
- packageName: string;
18
- version?: string;
19
- }
20
-
21
- export interface InstalledPlugin {
22
- /** npm package name; doubles as the plugin id used everywhere else. */
23
- name: string;
24
- version: string;
25
- }
26
-
27
- const getPluginsDir = (): string => {
28
- const config = loadConfig();
29
- const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
30
- return path.join(baseDir, DEFAULT_PLUGINS_DIR);
31
- };
32
-
33
- /**
34
- * Lifecycle for community-built plugins distributed via npm.
35
- * Each plugin is installed to `<plugins>/<npm-name>/` and is identified
36
- * everywhere (AGENT.md `plugins[].id`, registry, runtime resolution) by its
37
- * npm name. Scoped packages (`@scope/foo`) live under `<plugins>/@scope/foo/`.
38
- */
39
- export const pluginService = {
40
- isInstalled: async (packageName: string): Promise<boolean> => {
41
- const finalPath = path.join(getPluginsDir(), packageName);
42
- return existsSync(path.join(finalPath, 'dist', 'index.js'));
43
- },
44
-
45
- install: async ({ packageName, version }: InstallOptions): Promise<InstalledPlugin> => {
46
- const pluginsDir = getPluginsDir();
47
- await fs.mkdir(pluginsDir, { recursive: true });
48
-
49
- const finalPath = path.join(pluginsDir, packageName);
50
-
51
- if (existsSync(path.join(finalPath, 'package.json'))) {
52
- try {
53
- const pkgJson = JSON.parse(
54
- await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'),
55
- );
56
- if (!version || pkgJson.version === version) {
57
- console.log(
58
- `[plugins] ${packageName}${version ? `@${version}` : ''} is already installed.`,
59
- );
60
- return { name: pkgJson.name, version: pkgJson.version };
61
- }
62
- } catch {
63
- // corrupted; reinstall below
64
- }
65
- }
66
-
67
- const target = version ? `${packageName}@${version}` : packageName;
68
- console.log(`[plugins] Installing ${target} to ${pluginsDir}...`);
69
-
70
- const tempDir = path.join(pluginsDir, '.tmp_' + Date.now());
71
- try {
72
- await fs.mkdir(tempDir, { recursive: true });
73
- await execAsync(`npm install ${target} --no-save --prefix "${tempDir}"`);
74
-
75
- const installedPath = path.join(tempDir, 'node_modules', packageName);
76
- if (!existsSync(installedPath)) {
77
- throw new Error(`npm did not produce ${installedPath}`);
78
- }
79
-
80
- await fs.mkdir(path.dirname(finalPath), { recursive: true });
81
- await fs.rm(finalPath, { recursive: true, force: true });
82
- await fs.rename(installedPath, finalPath);
83
-
84
- console.log(`[plugins] Running npm install in ${finalPath}...`);
85
- try {
86
- await execAsync(`npm install`, { cwd: finalPath });
87
- console.log(`[plugins] npm install completed in ${finalPath}`);
88
- } catch (e) {
89
- console.warn(`[plugins] Failed to run npm install in ${finalPath}:`, e);
90
- }
91
-
92
- const pkgJson = JSON.parse(
93
- await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'),
94
- );
95
-
96
- invalidatePlugin(packageName);
97
- return { name: pkgJson.name, version: pkgJson.version };
98
- } catch (error) {
99
- console.error(`[plugins] Failed to install ${packageName}:`, error);
100
- throw new Error(
101
- `Failed to install plugin ${packageName}: ${(error as Error).message}`,
102
- );
103
- } finally {
104
- await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
105
- }
106
- },
107
-
108
- uninstall: async (packageName: string): Promise<void> => {
109
- const pluginsDir = getPluginsDir();
110
- const pluginPath = path.join(pluginsDir, packageName);
111
-
112
- try {
113
- await fs.rm(pluginPath, { recursive: true, force: true });
114
- invalidatePlugin(packageName);
115
- console.log(`[plugins] Uninstalled plugin ${packageName}`);
116
-
117
- if (packageName.startsWith('@')) {
118
- const scopeDir = path.dirname(pluginPath);
119
- try {
120
- const remaining = await fs.readdir(scopeDir);
121
- if (remaining.length === 0) await fs.rmdir(scopeDir);
122
- } catch {
123
- // ignore
124
- }
125
- }
126
- } catch (error) {
127
- console.error(`[plugins] Failed to uninstall ${packageName}:`, error);
128
- throw new Error(
129
- `Failed to uninstall plugin ${packageName}: ${(error as Error).message}`,
130
- );
131
- }
132
- },
133
- };
File without changes