openbot 0.2.14 → 0.3.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.
Files changed (84) hide show
  1. package/dist/agents/openbot/index.js +76 -0
  2. package/dist/agents/openbot/middleware/approval.js +132 -0
  3. package/dist/agents/openbot/runtime.js +289 -0
  4. package/dist/agents/openbot/system-prompt.js +32 -0
  5. package/dist/agents/openbot/tools/delegation.js +78 -0
  6. package/dist/agents/openbot/tools/mcp.js +99 -0
  7. package/dist/agents/openbot/tools/shell.js +91 -0
  8. package/dist/agents/openbot/tools/storage.js +75 -0
  9. package/dist/agents/openbot/tools/ui.js +176 -0
  10. package/dist/agents/system.js +20 -93
  11. package/dist/app/cli.js +1 -1
  12. package/dist/app/config.js +4 -1
  13. package/dist/app/server.js +15 -8
  14. package/dist/bus/agent-package.js +1 -0
  15. package/dist/bus/plugin.js +1 -0
  16. package/dist/bus/services.js +711 -0
  17. package/dist/bus/types.js +1 -0
  18. package/dist/harness/context.js +250 -0
  19. package/dist/harness/event-normalizer.js +59 -0
  20. package/dist/harness/orchestrator.js +27 -227
  21. package/dist/harness/process.js +25 -3
  22. package/dist/harness/queue-processor.js +227 -0
  23. package/dist/harness/runtime-factory.js +103 -0
  24. package/dist/plugins/ai-sdk/index.js +37 -0
  25. package/dist/plugins/ai-sdk/runtime.js +402 -0
  26. package/dist/plugins/ai-sdk/system-prompt.js +3 -0
  27. package/dist/plugins/ai-sdk.js +277 -87
  28. package/dist/plugins/approval/index.js +159 -0
  29. package/dist/plugins/approval.js +163 -0
  30. package/dist/plugins/delegation/index.js +79 -0
  31. package/dist/plugins/delegation.js +67 -11
  32. package/dist/plugins/mcp/index.js +108 -0
  33. package/dist/plugins/memory/index.js +71 -0
  34. package/dist/plugins/shell/index.js +99 -0
  35. package/dist/plugins/shell.js +123 -0
  36. package/dist/plugins/storage-tools/index.js +85 -0
  37. package/dist/plugins/storage.js +240 -5
  38. package/dist/plugins/ui/index.js +184 -0
  39. package/dist/plugins/ui.js +185 -21
  40. package/dist/registry/agents.js +138 -0
  41. package/dist/registry/plugins.js +93 -50
  42. package/dist/services/agent-packages.js +103 -0
  43. package/dist/services/memory.js +152 -0
  44. package/dist/services/plugins.js +98 -0
  45. package/dist/services/storage.js +366 -94
  46. package/docs/agents.md +52 -65
  47. package/docs/architecture.md +1 -1
  48. package/docs/plugins.md +70 -58
  49. package/docs/templates/AGENT.example.md +57 -0
  50. package/package.json +8 -7
  51. package/src/app/cli.ts +1 -1
  52. package/src/app/config.ts +14 -4
  53. package/src/app/server.ts +23 -10
  54. package/src/app/types.ts +445 -16
  55. package/src/assets/icon.svg +4 -1
  56. package/src/bus/plugin.ts +67 -0
  57. package/src/bus/services.ts +786 -0
  58. package/src/bus/types.ts +160 -0
  59. package/src/harness/context.ts +293 -0
  60. package/src/harness/event-normalizer.ts +82 -0
  61. package/src/harness/orchestrator.ts +35 -273
  62. package/src/harness/process.ts +28 -4
  63. package/src/harness/queue-processor.ts +309 -0
  64. package/src/harness/runtime-factory.ts +125 -0
  65. package/src/plugins/ai-sdk/index.ts +44 -0
  66. package/src/plugins/ai-sdk/runtime.ts +484 -0
  67. package/src/plugins/ai-sdk/system-prompt.ts +4 -0
  68. package/src/plugins/approval/index.ts +228 -0
  69. package/src/plugins/delegation/index.ts +94 -0
  70. package/src/plugins/mcp/index.ts +128 -0
  71. package/src/plugins/memory/index.ts +85 -0
  72. package/src/plugins/shell/index.ts +123 -0
  73. package/src/plugins/storage-tools/index.ts +101 -0
  74. package/src/plugins/ui/index.ts +227 -0
  75. package/src/registry/plugins.ts +108 -55
  76. package/src/services/memory.ts +213 -0
  77. package/src/services/plugins.ts +133 -0
  78. package/src/services/storage.ts +472 -137
  79. package/src/agents/system.ts +0 -112
  80. package/src/plugins/ai-sdk.ts +0 -197
  81. package/src/plugins/delegation.ts +0 -60
  82. package/src/plugins/mcp.ts +0 -154
  83. package/src/plugins/storage.ts +0 -725
  84. package/src/plugins/ui.ts +0 -57
@@ -0,0 +1,213 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
5
+
6
+ /**
7
+ * Global memory service.
8
+ *
9
+ * Persistent, agent-shared knowledge store that lives outside of any single
10
+ * channel/thread conversation. Designed as a stable foundation we can extend
11
+ * later with embeddings, retrieval ranking, TTLs, etc.
12
+ *
13
+ * Storage format
14
+ * --------------
15
+ * `~/.openbot/memory/log.jsonl` — append-only log. Each line is one of:
16
+ *
17
+ * { "op": "add", "record": MemoryRecord }
18
+ * { "op": "delete", "id": string, "at": ISO }
19
+ * { "op": "update", "id": string, "patch": Partial<MemoryRecord>, "at": ISO }
20
+ *
21
+ * Reads replay the log into an in-memory map. The log is append-only so
22
+ * concurrent writers are line-atomic on every POSIX filesystem we target.
23
+ *
24
+ * Scopes
25
+ * ------
26
+ * `global` — visible to every agent everywhere.
27
+ * `agent:<agentId>` — visible only when that agent is running.
28
+ * `channel:<channelId>` — visible only inside that channel.
29
+ *
30
+ * Scope strings are opaque to the store; new scopes can be introduced without
31
+ * a migration.
32
+ */
33
+ export interface MemoryRecord {
34
+ id: string;
35
+ scope: string;
36
+ content: string;
37
+ tags?: string[];
38
+ createdAt: string;
39
+ updatedAt: string;
40
+ }
41
+
42
+ export interface ListMemoriesArgs {
43
+ /** Exact scope match (e.g. `global`, `agent:foo`, `channel:bar`). */
44
+ scope?: string;
45
+ /** Multiple scopes — OR'd together. Useful for "global + agent:X + channel:Y". */
46
+ scopes?: string[];
47
+ /** Substring match (case-insensitive) against `content`. */
48
+ query?: string;
49
+ /** Match if any of these tags is present. */
50
+ tag?: string;
51
+ /** Default 50, hard cap 500. */
52
+ limit?: number;
53
+ }
54
+
55
+ interface AddEntry { op: 'add'; record: MemoryRecord }
56
+ interface DeleteEntry { op: 'delete'; id: string; at: string }
57
+ interface UpdateEntry { op: 'update'; id: string; patch: Partial<MemoryRecord>; at: string }
58
+ type LogEntry = AddEntry | DeleteEntry | UpdateEntry;
59
+
60
+ const DEFAULT_LIMIT = 50;
61
+ const MAX_LIMIT = 500;
62
+
63
+ const getMemoryDir = (): string => {
64
+ const config = loadConfig();
65
+ return path.join(resolvePath(config.baseDir || DEFAULT_BASE_DIR), 'memory');
66
+ };
67
+
68
+ const getLogPath = (): string => path.join(getMemoryDir(), 'log.jsonl');
69
+
70
+ const ensureDir = async (): Promise<void> => {
71
+ await fs.mkdir(getMemoryDir(), { recursive: true });
72
+ };
73
+
74
+ const readLog = async (): Promise<LogEntry[]> => {
75
+ try {
76
+ const raw = await fs.readFile(getLogPath(), 'utf-8');
77
+ return raw
78
+ .split(/\r?\n/)
79
+ .map((line) => line.trim())
80
+ .filter(Boolean)
81
+ .map((line) => {
82
+ try {
83
+ return JSON.parse(line) as LogEntry;
84
+ } catch {
85
+ return null;
86
+ }
87
+ })
88
+ .filter((e): e is LogEntry => !!e);
89
+ } catch (e: unknown) {
90
+ if ((e as { code?: string })?.code === 'ENOENT') return [];
91
+ throw e;
92
+ }
93
+ };
94
+
95
+ const replay = (entries: LogEntry[]): Map<string, MemoryRecord> => {
96
+ const out = new Map<string, MemoryRecord>();
97
+ for (const entry of entries) {
98
+ if (entry.op === 'add') {
99
+ out.set(entry.record.id, entry.record);
100
+ } else if (entry.op === 'delete') {
101
+ out.delete(entry.id);
102
+ } else if (entry.op === 'update') {
103
+ const existing = out.get(entry.id);
104
+ if (!existing) continue;
105
+ out.set(entry.id, {
106
+ ...existing,
107
+ ...entry.patch,
108
+ id: existing.id,
109
+ updatedAt: entry.at,
110
+ });
111
+ }
112
+ }
113
+ return out;
114
+ };
115
+
116
+ const appendEntry = async (entry: LogEntry): Promise<void> => {
117
+ await ensureDir();
118
+ await fs.appendFile(getLogPath(), `${JSON.stringify(entry)}\n`, 'utf-8');
119
+ };
120
+
121
+ const matchesQuery = (record: MemoryRecord, query?: string, tag?: string): boolean => {
122
+ if (tag) {
123
+ if (!record.tags || !record.tags.includes(tag)) return false;
124
+ }
125
+ if (query) {
126
+ const q = query.toLowerCase();
127
+ if (!record.content.toLowerCase().includes(q)) return false;
128
+ }
129
+ return true;
130
+ };
131
+
132
+ export const memoryService = {
133
+ appendMemory: async (args: {
134
+ scope: string;
135
+ content: string;
136
+ tags?: string[];
137
+ }): Promise<MemoryRecord> => {
138
+ const now = new Date().toISOString();
139
+ const record: MemoryRecord = {
140
+ id: crypto.randomUUID(),
141
+ scope: args.scope,
142
+ content: args.content,
143
+ tags: args.tags?.length ? args.tags : undefined,
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ };
147
+ await appendEntry({ op: 'add', record });
148
+ return record;
149
+ },
150
+
151
+ updateMemory: async (args: {
152
+ id: string;
153
+ content?: string;
154
+ tags?: string[];
155
+ }): Promise<boolean> => {
156
+ const entries = await readLog();
157
+ const map = replay(entries);
158
+ if (!map.has(args.id)) return false;
159
+ const at = new Date().toISOString();
160
+ const patch: Partial<MemoryRecord> = {};
161
+ if (args.content !== undefined) patch.content = args.content;
162
+ if (args.tags !== undefined) patch.tags = args.tags.length ? args.tags : undefined;
163
+ if (Object.keys(patch).length === 0) return true;
164
+ await appendEntry({ op: 'update', id: args.id, patch, at });
165
+ return true;
166
+ },
167
+
168
+ deleteMemory: async (args: { id: string }): Promise<boolean> => {
169
+ const entries = await readLog();
170
+ const map = replay(entries);
171
+ if (!map.has(args.id)) return false;
172
+ await appendEntry({ op: 'delete', id: args.id, at: new Date().toISOString() });
173
+ return true;
174
+ },
175
+
176
+ listMemories: async (args: ListMemoriesArgs = {}): Promise<MemoryRecord[]> => {
177
+ const entries = await readLog();
178
+ const map = replay(entries);
179
+ const limit = Math.min(Math.max(args.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
180
+
181
+ const scopeSet = (() => {
182
+ if (args.scope) return new Set([args.scope]);
183
+ if (args.scopes && args.scopes.length > 0) return new Set(args.scopes);
184
+ return null;
185
+ })();
186
+
187
+ const filtered: MemoryRecord[] = [];
188
+ for (const record of map.values()) {
189
+ if (scopeSet && !scopeSet.has(record.scope)) continue;
190
+ if (!matchesQuery(record, args.query, args.tag)) continue;
191
+ filtered.push(record);
192
+ }
193
+
194
+ filtered.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1));
195
+ return filtered.slice(0, limit);
196
+ },
197
+
198
+ /**
199
+ * Compact the log into a single `add` per surviving record. Cheap to call
200
+ * occasionally; not required for correctness.
201
+ */
202
+ compact: async (): Promise<number> => {
203
+ const entries = await readLog();
204
+ const map = replay(entries);
205
+ const surviving = Array.from(map.values());
206
+ await ensureDir();
207
+ const tmp = `${getLogPath()}.tmp`;
208
+ const body = surviving.map((record) => JSON.stringify({ op: 'add', record })).join('\n');
209
+ await fs.writeFile(tmp, body ? `${body}\n` : '', 'utf-8');
210
+ await fs.rename(tmp, getLogPath());
211
+ return surviving.length;
212
+ },
213
+ };
@@ -0,0 +1,133 @@
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
+ };