bopodev-api 0.1.11 → 0.1.13

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.
@@ -0,0 +1,249 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { join, relative, resolve } from "node:path";
3
+ import type { AgentMemoryContext } from "bopodev-agent-sdk";
4
+ import {
5
+ isInsidePath,
6
+ resolveAgentDailyMemoryPath,
7
+ resolveAgentDurableMemoryPath,
8
+ resolveAgentMemoryRootPath
9
+ } from "../lib/instance-paths";
10
+
11
+ const MAX_DAILY_LINES = 12;
12
+ const MAX_DURABLE_FACTS = 12;
13
+ const MAX_TACIT_NOTES_CHARS = 1_500;
14
+ const MAX_OBSERVABILITY_FILES = 200;
15
+ const MAX_OBSERVABILITY_FILE_BYTES = 512 * 1024;
16
+
17
+ export type PersistedHeartbeatMemory = {
18
+ memoryRoot: string;
19
+ dailyNotePath: string;
20
+ dailyEntry: string;
21
+ candidateFacts: string[];
22
+ };
23
+
24
+ export async function loadAgentMemoryContext(input: {
25
+ companyId: string;
26
+ agentId: string;
27
+ }): Promise<AgentMemoryContext> {
28
+ const memoryRoot = resolveAgentMemoryRootPath(input.companyId, input.agentId);
29
+ const durableRoot = resolveAgentDurableMemoryPath(input.companyId, input.agentId);
30
+ const dailyRoot = resolveAgentDailyMemoryPath(input.companyId, input.agentId);
31
+ await ensureMemoryDirs(memoryRoot, durableRoot, dailyRoot);
32
+ const tacitNotes = await readTacitNotes(memoryRoot);
33
+ const durableFacts = await readDurableFacts(durableRoot, MAX_DURABLE_FACTS);
34
+ const dailyNotes = await readRecentDailyNotes(dailyRoot, MAX_DAILY_LINES);
35
+ return {
36
+ memoryRoot,
37
+ tacitNotes,
38
+ durableFacts,
39
+ dailyNotes
40
+ };
41
+ }
42
+
43
+ export async function persistHeartbeatMemory(input: {
44
+ companyId: string;
45
+ agentId: string;
46
+ runId: string;
47
+ status: string;
48
+ summary: string;
49
+ outcomeKind?: string | null;
50
+ }): Promise<PersistedHeartbeatMemory> {
51
+ const memoryRoot = resolveAgentMemoryRootPath(input.companyId, input.agentId);
52
+ const durableRoot = resolveAgentDurableMemoryPath(input.companyId, input.agentId);
53
+ const dailyRoot = resolveAgentDailyMemoryPath(input.companyId, input.agentId);
54
+ await ensureMemoryDirs(memoryRoot, durableRoot, dailyRoot);
55
+ const now = new Date();
56
+ const dailyFileName = `${now.toISOString().slice(0, 10)}.md`;
57
+ const dailyNotePath = join(dailyRoot, dailyFileName);
58
+ const summary = collapseWhitespace(input.summary);
59
+ const dailyEntry = [
60
+ `## ${now.toISOString()}`,
61
+ `- run: ${input.runId}`,
62
+ `- status: ${input.status}`,
63
+ `- outcome: ${input.outcomeKind ?? "unknown"}`,
64
+ `- summary: ${summary || "No summary provided."}`,
65
+ ""
66
+ ].join("\n");
67
+ await writeFile(dailyNotePath, dailyEntry, { encoding: "utf8", flag: "a" });
68
+ const candidateFacts = deriveCandidateFacts(summary);
69
+ return {
70
+ memoryRoot,
71
+ dailyNotePath,
72
+ dailyEntry,
73
+ candidateFacts
74
+ };
75
+ }
76
+
77
+ export async function appendDurableFact(input: {
78
+ companyId: string;
79
+ agentId: string;
80
+ fact: string;
81
+ sourceRunId?: string | null;
82
+ }) {
83
+ const durableRoot = resolveAgentDurableMemoryPath(input.companyId, input.agentId);
84
+ await mkdir(durableRoot, { recursive: true });
85
+ const targetFile = join(durableRoot, "items.yaml");
86
+ const normalizedFact = collapseWhitespace(input.fact);
87
+ if (!normalizedFact) {
88
+ return null;
89
+ }
90
+ const row = `- fact: "${escapeYamlString(normalizedFact)}"\n sourceRunId: "${escapeYamlString(input.sourceRunId ?? "")}"\n`;
91
+ await writeFile(targetFile, row, { encoding: "utf8", flag: "a" });
92
+ return targetFile;
93
+ }
94
+
95
+ export async function listAgentMemoryFiles(input: {
96
+ companyId: string;
97
+ agentId: string;
98
+ maxFiles?: number;
99
+ }) {
100
+ const root = resolveAgentMemoryRootPath(input.companyId, input.agentId);
101
+ await mkdir(root, { recursive: true });
102
+ const maxFiles = Math.max(1, Math.min(MAX_OBSERVABILITY_FILES, input.maxFiles ?? 100));
103
+ const files = await walkFiles(root, maxFiles);
104
+ return files.map((filePath) => ({
105
+ path: filePath,
106
+ relativePath: relative(root, filePath),
107
+ memoryRoot: root
108
+ }));
109
+ }
110
+
111
+ export async function readAgentMemoryFile(input: {
112
+ companyId: string;
113
+ agentId: string;
114
+ relativePath: string;
115
+ }) {
116
+ const root = resolveAgentMemoryRootPath(input.companyId, input.agentId);
117
+ await mkdir(root, { recursive: true });
118
+ const candidate = resolve(root, input.relativePath);
119
+ if (!isInsidePath(root, candidate)) {
120
+ throw new Error("Requested memory path is outside of memory root.");
121
+ }
122
+ const info = await stat(candidate);
123
+ if (!info.isFile()) {
124
+ throw new Error("Requested memory path is not a file.");
125
+ }
126
+ if (info.size > MAX_OBSERVABILITY_FILE_BYTES) {
127
+ throw new Error("Requested memory file exceeds size limit.");
128
+ }
129
+ const content = await readFile(candidate, "utf8");
130
+ return {
131
+ path: candidate,
132
+ relativePath: relative(root, candidate),
133
+ content,
134
+ sizeBytes: info.size
135
+ };
136
+ }
137
+
138
+ function collapseWhitespace(value: string) {
139
+ return value.replace(/\s+/g, " ").trim();
140
+ }
141
+
142
+ function deriveCandidateFacts(summary: string) {
143
+ if (!summary || summary.length < 18) {
144
+ return [];
145
+ }
146
+ return [summary.slice(0, 400)];
147
+ }
148
+
149
+ async function ensureMemoryDirs(memoryRoot: string, durableRoot: string, dailyRoot: string) {
150
+ await mkdir(memoryRoot, { recursive: true });
151
+ await mkdir(durableRoot, { recursive: true });
152
+ await mkdir(dailyRoot, { recursive: true });
153
+ }
154
+
155
+ async function readTacitNotes(memoryRoot: string) {
156
+ const tacitPath = join(memoryRoot, "MEMORY.md");
157
+ try {
158
+ const text = await readFile(tacitPath, "utf8");
159
+ const trimmed = text.trim();
160
+ if (!trimmed) {
161
+ return undefined;
162
+ }
163
+ return trimmed.slice(0, MAX_TACIT_NOTES_CHARS);
164
+ } catch {
165
+ return undefined;
166
+ }
167
+ }
168
+
169
+ async function readDurableFacts(durableRoot: string, limit: number) {
170
+ const candidates = [join(durableRoot, "summary.md"), join(durableRoot, "items.yaml")];
171
+ const facts: string[] = [];
172
+ for (const candidate of candidates) {
173
+ try {
174
+ const content = await readFile(candidate, "utf8");
175
+ const lines = content
176
+ .split(/\r?\n/)
177
+ .map((line) => line.trim())
178
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
179
+ for (const line of lines) {
180
+ if (facts.length >= limit) {
181
+ return facts;
182
+ }
183
+ facts.push(line.slice(0, 300));
184
+ }
185
+ } catch {
186
+ // best effort
187
+ }
188
+ }
189
+ return facts;
190
+ }
191
+
192
+ async function readRecentDailyNotes(dailyRoot: string, limit: number) {
193
+ try {
194
+ const entries = await readdir(dailyRoot, { withFileTypes: true });
195
+ const files = entries
196
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
197
+ .map((entry) => entry.name)
198
+ .sort()
199
+ .reverse()
200
+ .slice(0, 3);
201
+ const notes: string[] = [];
202
+ for (const fileName of files) {
203
+ const content = await readFile(join(dailyRoot, fileName), "utf8");
204
+ const lines = content
205
+ .split(/\r?\n/)
206
+ .map((line) => line.trim())
207
+ .filter(Boolean);
208
+ for (const line of lines.reverse()) {
209
+ if (notes.length >= limit) {
210
+ return notes;
211
+ }
212
+ notes.push(line.slice(0, 300));
213
+ }
214
+ }
215
+ return notes;
216
+ } catch {
217
+ return [];
218
+ }
219
+ }
220
+
221
+ async function walkFiles(root: string, maxFiles: number) {
222
+ const collected: string[] = [];
223
+ const queue = [root];
224
+ while (queue.length > 0 && collected.length < maxFiles) {
225
+ const current = queue.shift();
226
+ if (!current) {
227
+ continue;
228
+ }
229
+ const entries = await readdir(current, { withFileTypes: true });
230
+ for (const entry of entries) {
231
+ const absolutePath = join(current, entry.name);
232
+ if (entry.isDirectory()) {
233
+ queue.push(absolutePath);
234
+ continue;
235
+ }
236
+ if (entry.isFile()) {
237
+ collected.push(absolutePath);
238
+ if (collected.length >= maxFiles) {
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ return collected.sort();
245
+ }
246
+
247
+ function escapeYamlString(value: string) {
248
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
249
+ }
@@ -0,0 +1,65 @@
1
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { PluginManifestSchema, type PluginManifest } from "bopodev-contracts";
4
+
5
+ export type FilesystemPluginManifestLoadResult = {
6
+ manifests: PluginManifest[];
7
+ warnings: string[];
8
+ };
9
+
10
+ export async function loadFilesystemPluginManifests(): Promise<FilesystemPluginManifestLoadResult> {
11
+ const pluginRoot = resolvePluginManifestsDir();
12
+ let entries: string[] = [];
13
+ try {
14
+ entries = await readdir(pluginRoot);
15
+ } catch {
16
+ // Missing plugin directory is valid; startup should continue without file-based manifests.
17
+ return { manifests: [], warnings: [] };
18
+ }
19
+
20
+ const manifests: PluginManifest[] = [];
21
+ const warnings: string[] = [];
22
+ for (const entry of entries) {
23
+ const manifestPath = resolve(pluginRoot, entry, "plugin.json");
24
+ let raw: string;
25
+ try {
26
+ raw = await readFile(manifestPath, "utf8");
27
+ } catch {
28
+ continue;
29
+ }
30
+ try {
31
+ const parsed = JSON.parse(raw) as unknown;
32
+ const manifest = PluginManifestSchema.parse(parsed);
33
+ manifests.push(manifest);
34
+ } catch (error) {
35
+ warnings.push(`Invalid plugin manifest at '${manifestPath}': ${String(error)}`);
36
+ }
37
+ }
38
+
39
+ return { manifests, warnings };
40
+ }
41
+
42
+ export function resolvePluginManifestsDir() {
43
+ return process.env.BOPO_PLUGIN_MANIFESTS_DIR || resolve(process.cwd(), "plugins");
44
+ }
45
+
46
+ export async function writePluginManifestToFilesystem(manifest: PluginManifest) {
47
+ const pluginRoot = resolvePluginManifestsDir();
48
+ const safeDirName = sanitizePluginDirectoryName(manifest.id);
49
+ const pluginDir = resolve(pluginRoot, safeDirName);
50
+ const manifestPath = resolve(pluginDir, "plugin.json");
51
+ await mkdir(pluginDir, { recursive: true });
52
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
53
+ return manifestPath;
54
+ }
55
+
56
+ export async function deletePluginManifestFromFilesystem(pluginId: string) {
57
+ const pluginRoot = resolvePluginManifestsDir();
58
+ const safeDirName = sanitizePluginDirectoryName(pluginId);
59
+ const pluginDir = resolve(pluginRoot, safeDirName);
60
+ await rm(pluginDir, { recursive: true, force: true });
61
+ }
62
+
63
+ function sanitizePluginDirectoryName(pluginId: string) {
64
+ return pluginId.replace(/[^a-zA-Z0-9._-]/g, "-");
65
+ }