@flowdot.ai/daemon 1.0.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 (85) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +51 -0
  3. package/dist/goals/DependencyResolver.d.ts +54 -0
  4. package/dist/goals/DependencyResolver.js +329 -0
  5. package/dist/goals/ErrorRecovery.d.ts +133 -0
  6. package/dist/goals/ErrorRecovery.js +489 -0
  7. package/dist/goals/GoalApiClient.d.ts +81 -0
  8. package/dist/goals/GoalApiClient.js +743 -0
  9. package/dist/goals/GoalCache.d.ts +65 -0
  10. package/dist/goals/GoalCache.js +243 -0
  11. package/dist/goals/GoalCommsHandler.d.ts +150 -0
  12. package/dist/goals/GoalCommsHandler.js +378 -0
  13. package/dist/goals/GoalExporter.d.ts +164 -0
  14. package/dist/goals/GoalExporter.js +318 -0
  15. package/dist/goals/GoalImporter.d.ts +107 -0
  16. package/dist/goals/GoalImporter.js +345 -0
  17. package/dist/goals/GoalManager.d.ts +110 -0
  18. package/dist/goals/GoalManager.js +535 -0
  19. package/dist/goals/GoalReporter.d.ts +105 -0
  20. package/dist/goals/GoalReporter.js +534 -0
  21. package/dist/goals/GoalScheduler.d.ts +102 -0
  22. package/dist/goals/GoalScheduler.js +209 -0
  23. package/dist/goals/GoalValidator.d.ts +72 -0
  24. package/dist/goals/GoalValidator.js +657 -0
  25. package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
  26. package/dist/goals/MetaGoalEnforcer.js +536 -0
  27. package/dist/goals/MilestoneBreaker.d.ts +74 -0
  28. package/dist/goals/MilestoneBreaker.js +348 -0
  29. package/dist/goals/PermissionBridge.d.ts +109 -0
  30. package/dist/goals/PermissionBridge.js +326 -0
  31. package/dist/goals/ProgressTracker.d.ts +113 -0
  32. package/dist/goals/ProgressTracker.js +324 -0
  33. package/dist/goals/ReviewScheduler.d.ts +106 -0
  34. package/dist/goals/ReviewScheduler.js +360 -0
  35. package/dist/goals/TaskExecutor.d.ts +116 -0
  36. package/dist/goals/TaskExecutor.js +370 -0
  37. package/dist/goals/TaskFeedback.d.ts +126 -0
  38. package/dist/goals/TaskFeedback.js +402 -0
  39. package/dist/goals/TaskGenerator.d.ts +75 -0
  40. package/dist/goals/TaskGenerator.js +329 -0
  41. package/dist/goals/TaskQueue.d.ts +84 -0
  42. package/dist/goals/TaskQueue.js +331 -0
  43. package/dist/goals/TaskSanitizer.d.ts +61 -0
  44. package/dist/goals/TaskSanitizer.js +464 -0
  45. package/dist/goals/errors.d.ts +116 -0
  46. package/dist/goals/errors.js +299 -0
  47. package/dist/goals/index.d.ts +24 -0
  48. package/dist/goals/index.js +23 -0
  49. package/dist/goals/types.d.ts +395 -0
  50. package/dist/goals/types.js +230 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.js +3 -0
  53. package/dist/loop/DaemonIPC.d.ts +67 -0
  54. package/dist/loop/DaemonIPC.js +358 -0
  55. package/dist/loop/IntervalParser.d.ts +39 -0
  56. package/dist/loop/IntervalParser.js +217 -0
  57. package/dist/loop/LoopDaemon.d.ts +123 -0
  58. package/dist/loop/LoopDaemon.js +1821 -0
  59. package/dist/loop/LoopExecutor.d.ts +93 -0
  60. package/dist/loop/LoopExecutor.js +326 -0
  61. package/dist/loop/LoopManager.d.ts +79 -0
  62. package/dist/loop/LoopManager.js +476 -0
  63. package/dist/loop/LoopScheduler.d.ts +69 -0
  64. package/dist/loop/LoopScheduler.js +329 -0
  65. package/dist/loop/LoopStore.d.ts +57 -0
  66. package/dist/loop/LoopStore.js +406 -0
  67. package/dist/loop/LoopValidator.d.ts +55 -0
  68. package/dist/loop/LoopValidator.js +603 -0
  69. package/dist/loop/errors.d.ts +115 -0
  70. package/dist/loop/errors.js +312 -0
  71. package/dist/loop/index.d.ts +11 -0
  72. package/dist/loop/index.js +10 -0
  73. package/dist/loop/notifications/Notifier.d.ts +28 -0
  74. package/dist/loop/notifications/Notifier.js +78 -0
  75. package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
  76. package/dist/loop/notifications/SlackNotifier.js +203 -0
  77. package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
  78. package/dist/loop/notifications/TerminalNotifier.js +72 -0
  79. package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
  80. package/dist/loop/notifications/WebhookNotifier.js +123 -0
  81. package/dist/loop/notifications/index.d.ts +24 -0
  82. package/dist/loop/notifications/index.js +109 -0
  83. package/dist/loop/types.d.ts +280 -0
  84. package/dist/loop/types.js +222 -0
  85. package/package.json +92 -0
@@ -0,0 +1,406 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { LOOP_REGISTRY_VERSION, serializeLoop, deserializeLoop, serializeLoopRun, deserializeLoopRun, } from './types.js';
4
+ import { StorageReadError, StorageWriteError, StorageCorruptError, StorageMigrationError, LoopNotFoundError, LoopRunNotFoundError, wrapError, } from './errors.js';
5
+ const LOOPS_DIRECTORY = 'loops';
6
+ const REGISTRY_FILE = 'registry.json';
7
+ const CONFIG_FILE = 'config.json';
8
+ const HISTORY_DIRECTORY = 'history';
9
+ const DEFAULT_STORE_OPTIONS = {
10
+ baseDir: process.cwd(),
11
+ flowdotDir: '.flowdot',
12
+ maxHistoryEntries: 100,
13
+ };
14
+ const noopLogger = {
15
+ debug: () => { },
16
+ info: () => { },
17
+ warn: () => { },
18
+ error: () => { },
19
+ };
20
+ export class LoopStore {
21
+ baseDir;
22
+ flowdotDir;
23
+ maxHistoryEntries;
24
+ loopsPath;
25
+ registryPath;
26
+ logger;
27
+ constructor(options = {}) {
28
+ const opts = { ...DEFAULT_STORE_OPTIONS, ...options };
29
+ this.baseDir = opts.baseDir;
30
+ this.flowdotDir = opts.flowdotDir;
31
+ this.maxHistoryEntries = opts.maxHistoryEntries;
32
+ this.loopsPath = path.join(this.baseDir, this.flowdotDir, LOOPS_DIRECTORY);
33
+ this.registryPath = path.join(this.loopsPath, REGISTRY_FILE);
34
+ this.logger = options.logger ?? noopLogger;
35
+ }
36
+ async initialize() {
37
+ await this.ensureDirectory(this.loopsPath);
38
+ await this.ensureRegistry();
39
+ }
40
+ async isInitialized() {
41
+ try {
42
+ await fs.access(this.registryPath);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ async createLoop(loop) {
50
+ const loopDir = this.getLoopDirectory(loop.id);
51
+ const historyDir = path.join(loopDir, HISTORY_DIRECTORY);
52
+ try {
53
+ await this.ensureDirectory(loopDir);
54
+ await this.ensureDirectory(historyDir);
55
+ const configPath = path.join(loopDir, CONFIG_FILE);
56
+ await this.writeJsonFile(configPath, serializeLoop(loop));
57
+ await this.addToRegistry(loop);
58
+ }
59
+ catch (error) {
60
+ try {
61
+ await fs.rm(loopDir, { recursive: true, force: true });
62
+ }
63
+ catch {
64
+ }
65
+ throw wrapError(error, 'STORAGE_WRITE_ERROR', `Failed to create loop ${loop.id}`);
66
+ }
67
+ }
68
+ async getLoop(loopId) {
69
+ const configPath = path.join(this.getLoopDirectory(loopId), CONFIG_FILE);
70
+ try {
71
+ const data = await this.readJsonFile(configPath);
72
+ return deserializeLoop(data);
73
+ }
74
+ catch (error) {
75
+ if (this.isNotFoundError(error)) {
76
+ throw new LoopNotFoundError(loopId, 'id');
77
+ }
78
+ throw wrapError(error, 'STORAGE_READ_ERROR', `Failed to read loop ${loopId}`);
79
+ }
80
+ }
81
+ async getLoopByName(name) {
82
+ const registry = await this.readRegistry();
83
+ const entry = Object.values(registry.loops).find((e) => e.name === name);
84
+ if (!entry) {
85
+ return null;
86
+ }
87
+ return this.getLoop(entry.id);
88
+ }
89
+ async updateLoop(loop) {
90
+ const loopDir = this.getLoopDirectory(loop.id);
91
+ const configPath = path.join(loopDir, CONFIG_FILE);
92
+ const exists = await this.loopExists(loop.id);
93
+ if (!exists) {
94
+ throw new LoopNotFoundError(loop.id, 'id');
95
+ }
96
+ try {
97
+ await this.writeJsonFile(configPath, serializeLoop(loop));
98
+ await this.updateRegistryEntry(loop);
99
+ }
100
+ catch (error) {
101
+ throw wrapError(error, 'STORAGE_WRITE_ERROR', `Failed to update loop ${loop.id}`);
102
+ }
103
+ }
104
+ async deleteLoop(loopId) {
105
+ const loopDir = this.getLoopDirectory(loopId);
106
+ const exists = await this.loopExists(loopId);
107
+ if (!exists) {
108
+ throw new LoopNotFoundError(loopId, 'id');
109
+ }
110
+ try {
111
+ await this.removeFromRegistry(loopId);
112
+ await fs.rm(loopDir, { recursive: true, force: true });
113
+ }
114
+ catch (error) {
115
+ throw wrapError(error, 'STORAGE_WRITE_ERROR', `Failed to delete loop ${loopId}`);
116
+ }
117
+ }
118
+ async loopExists(loopId) {
119
+ const registry = await this.readRegistry();
120
+ return loopId in registry.loops;
121
+ }
122
+ async isNameTaken(name, excludeLoopId) {
123
+ const registry = await this.readRegistry();
124
+ return Object.values(registry.loops).some((entry) => entry.name === name && entry.id !== excludeLoopId);
125
+ }
126
+ async getAllLoops() {
127
+ const registry = await this.readRegistry();
128
+ const loops = [];
129
+ for (const loopId of Object.keys(registry.loops)) {
130
+ try {
131
+ const loop = await this.getLoop(loopId);
132
+ loops.push(loop);
133
+ }
134
+ catch {
135
+ }
136
+ }
137
+ return loops;
138
+ }
139
+ async getLoopsByStatus(status) {
140
+ const allLoops = await this.getAllLoops();
141
+ return allLoops.filter((loop) => loop.status === status);
142
+ }
143
+ async getActiveLoopCount() {
144
+ const registry = await this.readRegistry();
145
+ return Object.values(registry.loops).filter((entry) => entry.status === 'running').length;
146
+ }
147
+ async createRun(run) {
148
+ const historyDir = path.join(this.getLoopDirectory(run.loopId), HISTORY_DIRECTORY);
149
+ const runPath = path.join(historyDir, `${run.id}.json`);
150
+ try {
151
+ await this.ensureDirectory(historyDir);
152
+ await this.writeJsonFile(runPath, serializeLoopRun(run));
153
+ }
154
+ catch (error) {
155
+ throw wrapError(error, 'STORAGE_WRITE_ERROR', `Failed to create run ${run.id} for loop ${run.loopId}`);
156
+ }
157
+ }
158
+ async updateRun(run) {
159
+ const runPath = this.getRunPath(run.loopId, run.id);
160
+ try {
161
+ await this.writeJsonFile(runPath, serializeLoopRun(run));
162
+ }
163
+ catch (error) {
164
+ throw wrapError(error, 'STORAGE_WRITE_ERROR', `Failed to update run ${run.id} for loop ${run.loopId}`);
165
+ }
166
+ }
167
+ async getRun(loopId, runId) {
168
+ const runPath = this.getRunPath(loopId, runId);
169
+ try {
170
+ const data = await this.readJsonFile(runPath);
171
+ return deserializeLoopRun(data);
172
+ }
173
+ catch (error) {
174
+ if (this.isNotFoundError(error)) {
175
+ throw new LoopRunNotFoundError(loopId, runId);
176
+ }
177
+ throw wrapError(error, 'STORAGE_READ_ERROR', `Failed to read run ${runId} for loop ${loopId}`);
178
+ }
179
+ }
180
+ async getRunHistory(loopId, limit) {
181
+ const historyDir = path.join(this.getLoopDirectory(loopId), HISTORY_DIRECTORY);
182
+ try {
183
+ const files = await fs.readdir(historyDir);
184
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
185
+ const runs = [];
186
+ for (const file of jsonFiles) {
187
+ try {
188
+ const filePath = path.join(historyDir, file);
189
+ const data = await this.readJsonFile(filePath);
190
+ runs.push(deserializeLoopRun(data));
191
+ }
192
+ catch {
193
+ }
194
+ }
195
+ runs.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
196
+ if (limit !== undefined && limit > 0) {
197
+ return runs.slice(0, limit);
198
+ }
199
+ return runs;
200
+ }
201
+ catch (error) {
202
+ if (this.isNotFoundError(error)) {
203
+ return [];
204
+ }
205
+ throw wrapError(error, 'STORAGE_READ_ERROR', `Failed to read run history for loop ${loopId}`);
206
+ }
207
+ }
208
+ async cleanupRunHistory(loopId) {
209
+ const runs = await this.getRunHistory(loopId);
210
+ if (runs.length <= this.maxHistoryEntries) {
211
+ return 0;
212
+ }
213
+ const runsToDelete = runs.slice(this.maxHistoryEntries);
214
+ let deletedCount = 0;
215
+ for (const run of runsToDelete) {
216
+ try {
217
+ const runPath = this.getRunPath(loopId, run.id);
218
+ await fs.unlink(runPath);
219
+ deletedCount++;
220
+ }
221
+ catch {
222
+ }
223
+ }
224
+ return deletedCount;
225
+ }
226
+ async readRegistry() {
227
+ try {
228
+ const data = await this.readJsonFile(this.registryPath);
229
+ if (data.version !== LOOP_REGISTRY_VERSION) {
230
+ await this.migrateRegistry(data);
231
+ return this.readRegistry();
232
+ }
233
+ return data;
234
+ }
235
+ catch (error) {
236
+ if (this.isNotFoundError(error)) {
237
+ return this.createEmptyRegistry();
238
+ }
239
+ throw wrapError(error, 'STORAGE_READ_ERROR', 'Failed to read loop registry');
240
+ }
241
+ }
242
+ async getRegistryEntries() {
243
+ const registry = await this.readRegistry();
244
+ return Object.values(registry.loops);
245
+ }
246
+ async addToRegistry(loop) {
247
+ const registry = await this.readRegistry();
248
+ const entry = {
249
+ id: loop.id,
250
+ name: loop.name,
251
+ status: loop.status,
252
+ createdAt: loop.createdAt.toISOString(),
253
+ lastRunAt: loop.stats.lastRunAt?.toISOString() ?? null,
254
+ };
255
+ registry.loops[loop.id] = entry;
256
+ registry.updatedAt = new Date().toISOString();
257
+ await this.writeRegistry(registry);
258
+ }
259
+ async updateRegistryEntry(loop) {
260
+ const registry = await this.readRegistry();
261
+ if (!(loop.id in registry.loops)) {
262
+ throw new LoopNotFoundError(loop.id, 'id');
263
+ }
264
+ registry.loops[loop.id] = {
265
+ ...registry.loops[loop.id],
266
+ name: loop.name,
267
+ status: loop.status,
268
+ lastRunAt: loop.stats.lastRunAt?.toISOString() ?? null,
269
+ };
270
+ registry.updatedAt = new Date().toISOString();
271
+ await this.writeRegistry(registry);
272
+ }
273
+ async removeFromRegistry(loopId) {
274
+ const registry = await this.readRegistry();
275
+ if (!(loopId in registry.loops)) {
276
+ throw new LoopNotFoundError(loopId, 'id');
277
+ }
278
+ delete registry.loops[loopId];
279
+ registry.updatedAt = new Date().toISOString();
280
+ await this.writeRegistry(registry);
281
+ }
282
+ async writeRegistry(registry) {
283
+ await this.writeJsonFile(this.registryPath, registry);
284
+ }
285
+ async ensureRegistry() {
286
+ try {
287
+ await fs.access(this.registryPath);
288
+ }
289
+ catch {
290
+ const registry = this.createEmptyRegistry();
291
+ await this.writeRegistry(registry);
292
+ }
293
+ }
294
+ createEmptyRegistry() {
295
+ return {
296
+ version: LOOP_REGISTRY_VERSION,
297
+ updatedAt: new Date().toISOString(),
298
+ loops: {},
299
+ };
300
+ }
301
+ async migrateRegistry(oldRegistry) {
302
+ if (oldRegistry.version > LOOP_REGISTRY_VERSION) {
303
+ throw new StorageMigrationError(oldRegistry.version, LOOP_REGISTRY_VERSION, 'Registry version is newer than supported');
304
+ }
305
+ const migratedRegistry = {
306
+ ...oldRegistry,
307
+ version: LOOP_REGISTRY_VERSION,
308
+ updatedAt: new Date().toISOString(),
309
+ };
310
+ await this.writeRegistry(migratedRegistry);
311
+ }
312
+ async writeDaemonPid(pid) {
313
+ const pidPath = path.join(this.loopsPath, 'daemon.pid');
314
+ try {
315
+ await this.writeJsonFile(pidPath, {
316
+ pid,
317
+ startedAt: new Date().toISOString(),
318
+ });
319
+ }
320
+ catch (error) {
321
+ throw wrapError(error, 'STORAGE_WRITE_ERROR', 'Failed to write daemon PID file');
322
+ }
323
+ }
324
+ async readDaemonPid() {
325
+ const pidPath = path.join(this.loopsPath, 'daemon.pid');
326
+ try {
327
+ const data = await this.readJsonFile(pidPath);
328
+ return {
329
+ pid: data.pid,
330
+ startedAt: new Date(data.startedAt),
331
+ };
332
+ }
333
+ catch {
334
+ return null;
335
+ }
336
+ }
337
+ async deleteDaemonPid() {
338
+ const pidPath = path.join(this.loopsPath, 'daemon.pid');
339
+ try {
340
+ await fs.unlink(pidPath);
341
+ }
342
+ catch {
343
+ }
344
+ }
345
+ getLoopDirectory(loopId) {
346
+ return path.join(this.loopsPath, loopId);
347
+ }
348
+ getRunPath(loopId, runId) {
349
+ return path.join(this.getLoopDirectory(loopId), HISTORY_DIRECTORY, `${runId}.json`);
350
+ }
351
+ async ensureDirectory(dirPath) {
352
+ try {
353
+ await fs.mkdir(dirPath, { recursive: true });
354
+ }
355
+ catch (error) {
356
+ throw new StorageWriteError(dirPath, 'Failed to create directory', error);
357
+ }
358
+ }
359
+ async readJsonFile(filePath) {
360
+ try {
361
+ const content = await fs.readFile(filePath, 'utf-8');
362
+ return JSON.parse(content);
363
+ }
364
+ catch (error) {
365
+ if (this.isNotFoundError(error)) {
366
+ throw error;
367
+ }
368
+ if (error instanceof SyntaxError) {
369
+ throw new StorageCorruptError(filePath, 'Invalid JSON');
370
+ }
371
+ throw new StorageReadError(filePath, 'Failed to read file', error);
372
+ }
373
+ }
374
+ async writeJsonFile(filePath, data) {
375
+ const content = JSON.stringify(data, null, 2);
376
+ const tempPath = `${filePath}.tmp`;
377
+ try {
378
+ await fs.writeFile(tempPath, content, 'utf-8');
379
+ await fs.rename(tempPath, filePath);
380
+ }
381
+ catch (error) {
382
+ try {
383
+ await fs.unlink(tempPath);
384
+ }
385
+ catch {
386
+ }
387
+ throw new StorageWriteError(filePath, 'Failed to write file', error);
388
+ }
389
+ }
390
+ isNotFoundError(error) {
391
+ return (error instanceof Error &&
392
+ 'code' in error &&
393
+ error.code === 'ENOENT');
394
+ }
395
+ getStoragePath() {
396
+ return this.loopsPath;
397
+ }
398
+ }
399
+ export function createLoopStore(options) {
400
+ return new LoopStore(options);
401
+ }
402
+ export async function initializeLoopStore(options) {
403
+ const store = new LoopStore(options);
404
+ await store.initialize();
405
+ return store;
406
+ }
@@ -0,0 +1,55 @@
1
+ import type { CreateLoopInput, UpdateLoopInput, LoopConfig } from './types.js';
2
+ import { type IntervalParserOptions } from './IntervalParser.js';
3
+ export interface ValidationConstraints {
4
+ maxPromptLength: number;
5
+ minPromptLength: number;
6
+ maxNameLength: number;
7
+ minNameLength: number;
8
+ namePattern: RegExp;
9
+ maxWebhookUrlLength: number;
10
+ maxMaxRuns: number;
11
+ validModelTiers: readonly string[];
12
+ }
13
+ export declare const DEFAULT_VALIDATION_CONSTRAINTS: ValidationConstraints;
14
+ export interface ValidationResult {
15
+ valid: boolean;
16
+ errors: ValidationIssue[];
17
+ }
18
+ export interface ValidationIssue {
19
+ field: string;
20
+ message: string;
21
+ value?: unknown;
22
+ }
23
+ export declare class LoopValidator {
24
+ private readonly constraints;
25
+ private readonly intervalParser;
26
+ constructor(constraints?: Partial<ValidationConstraints>, intervalParserOptions?: IntervalParserOptions);
27
+ validateCreateInput(input: CreateLoopInput): void;
28
+ safeValidateCreateInput(input: CreateLoopInput): ValidationResult;
29
+ private collectCreateInputErrors;
30
+ validateUpdateInput(input: UpdateLoopInput): void;
31
+ safeValidateUpdateInput(input: UpdateLoopInput): ValidationResult;
32
+ private collectUpdateInputErrors;
33
+ private validatePromptField;
34
+ private validateIntervalField;
35
+ private validateNameField;
36
+ private validateIdentifierField;
37
+ private validateMaxRunsField;
38
+ private validateExpiresField;
39
+ private validateModelField;
40
+ private validateOnErrorField;
41
+ private validateNotifyField;
42
+ private validateWebhookUrlField;
43
+ validateConfig(config: Partial<LoopConfig>): void;
44
+ safeValidateConfig(config: Partial<LoopConfig>): ValidationResult;
45
+ private collectConfigErrors;
46
+ validateName(name: string): void;
47
+ validatePrompt(prompt: string): void;
48
+ validateInterval(interval: string): void;
49
+ private throwValidationError;
50
+ }
51
+ export declare function createLoopValidator(constraints?: Partial<ValidationConstraints>, intervalParserOptions?: IntervalParserOptions): LoopValidator;
52
+ export declare function validateCreateInput(input: CreateLoopInput): void;
53
+ export declare function validateUpdateInput(input: UpdateLoopInput): void;
54
+ export declare function validateLoopName(name: string): void;
55
+ export declare function validatePrompt(prompt: string): void;