@controlflow-ai/daemon 0.1.2 → 0.1.3

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 (61) hide show
  1. package/README.md +54 -6
  2. package/package.json +3 -1
  3. package/src/agent-avatar.ts +30 -0
  4. package/src/agent-key.ts +28 -0
  5. package/src/agent-permissions.ts +359 -0
  6. package/src/agent-runtime.ts +795 -28
  7. package/src/agent-workspace.ts +183 -0
  8. package/src/app.ts +1970 -79
  9. package/src/args.ts +54 -7
  10. package/src/cli.ts +873 -14
  11. package/src/client.ts +472 -10
  12. package/src/coco.ts +9 -40
  13. package/src/codex.ts +33 -5
  14. package/src/config.ts +28 -4
  15. package/src/console.ts +230 -20
  16. package/src/daemon-client.ts +116 -3
  17. package/src/daemon.ts +936 -98
  18. package/src/db.ts +3128 -122
  19. package/src/delivery-ws.ts +269 -0
  20. package/src/format.ts +4 -1
  21. package/src/lark/cli.ts +3 -3
  22. package/src/lark/event-router.ts +60 -4
  23. package/src/lark/inbound-events.ts +156 -3
  24. package/src/lark/server-integration.ts +659 -111
  25. package/src/lark/ws-daemon.ts +136 -10
  26. package/src/local-api.ts +545 -15
  27. package/src/local-auth.ts +33 -1
  28. package/src/message-attachments.ts +71 -0
  29. package/src/messaging-cli.ts +741 -0
  30. package/src/messaging-status.ts +669 -0
  31. package/src/migrations/024_agents_model.ts +10 -0
  32. package/src/migrations/025_room_archive.ts +44 -0
  33. package/src/migrations/026_project_archive.ts +44 -0
  34. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  35. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  36. package/src/migrations/029_held_message_drafts.ts +32 -0
  37. package/src/migrations/030_agent_room_read_state.ts +25 -0
  38. package/src/migrations/031_room_tasks.ts +29 -0
  39. package/src/migrations/032_room_reminders.ts +29 -0
  40. package/src/migrations/033_room_saved_messages.ts +25 -0
  41. package/src/migrations/034_agent_activity_events.ts +27 -0
  42. package/src/migrations/035_agent_avatars.ts +17 -0
  43. package/src/migrations/036_project_agent_defaults.ts +21 -0
  44. package/src/migrations/037_message_attachments.ts +36 -0
  45. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  46. package/src/migrations/039_message_attachments_path.ts +34 -0
  47. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  48. package/src/migrations/041_room_system_events.ts +30 -0
  49. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  50. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  51. package/src/migrations/044_workflow_runtime.ts +69 -0
  52. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  53. package/src/migrations.ts +69 -1
  54. package/src/neeko.ts +40 -4
  55. package/src/runtime-env.ts +179 -0
  56. package/src/runtime-registry.ts +83 -13
  57. package/src/server.ts +244 -4
  58. package/src/token-file.ts +13 -6
  59. package/src/types.ts +362 -0
  60. package/src/workflow-runtime.ts +275 -0
  61. package/src/web.ts +0 -904
@@ -0,0 +1,183 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { basename, extname, join, resolve, sep } from 'node:path';
3
+ import { agentHomePath } from './config.js';
4
+ import type { AgentDefinition, AgentWorkspaceFile, AgentWorkspaceFileKind } from './types.js';
5
+
6
+ const maxWorkspaceFileBytes = 20_000;
7
+ const maxWorkspaceWriteBytes = 64_000;
8
+ const allowedTextExtensions = new Set(['.md', '.txt', '.json']);
9
+ const rootFileSpecs = new Map<string, { kind: AgentWorkspaceFileKind; label: string }>([
10
+ ['MEMORY.md', { kind: 'memory', label: 'Memory' }],
11
+ ['SOUL.md', { kind: 'soul', label: 'Soul' }],
12
+ ]);
13
+
14
+ type DirectorySpec = {
15
+ dir: 'state' | 'kb';
16
+ kind: AgentWorkspaceFileKind;
17
+ };
18
+
19
+ const directorySpecs: DirectorySpec[] = [
20
+ { dir: 'state', kind: 'state' },
21
+ { dir: 'kb', kind: 'knowledge' },
22
+ ];
23
+
24
+ export type AgentWorkspaceFileErrorCode =
25
+ | 'WORKSPACE_FILE_NOT_ALLOWED'
26
+ | 'WORKSPACE_FILE_NOT_FOUND'
27
+ | 'WORKSPACE_FILE_TOO_LARGE';
28
+
29
+ export class AgentWorkspaceFileError extends Error {
30
+ constructor(
31
+ public readonly code: AgentWorkspaceFileErrorCode,
32
+ public readonly status: number,
33
+ message: string,
34
+ ) {
35
+ super(message);
36
+ }
37
+ }
38
+
39
+ function isInsideHome(home: string, path: string): boolean {
40
+ const resolvedHome = resolve(home);
41
+ const resolvedPath = resolve(path);
42
+ return resolvedPath === resolvedHome || resolvedPath.startsWith(`${resolvedHome}${sep}`);
43
+ }
44
+
45
+ function normalizeWorkspaceRelativePath(input: string): string | null {
46
+ const trimmed = input.trim();
47
+ if (!trimmed || trimmed.includes('\\') || trimmed.includes('\0') || trimmed.startsWith('/')) return null;
48
+ const segments = trimmed.split('/');
49
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..' || segment.startsWith('.'))) return null;
50
+ return segments.join('/');
51
+ }
52
+
53
+ function workspaceFileSpec(relativePath: string): { kind: AgentWorkspaceFileKind; label: string } | null {
54
+ const rootSpec = rootFileSpecs.get(relativePath);
55
+ if (rootSpec) return rootSpec;
56
+
57
+ const [directory, ...rest] = relativePath.split('/');
58
+ if (!rest.length || rest.length > 3) return null;
59
+ const directorySpec = directorySpecs.find((spec) => spec.dir === directory);
60
+ if (!directorySpec) return null;
61
+ const fileName = rest[rest.length - 1]!;
62
+ if (!allowedTextExtensions.has(extname(fileName).toLowerCase())) return null;
63
+ return { kind: directorySpec.kind, label: basename(fileName) };
64
+ }
65
+
66
+ function readWorkspaceFile(home: string, path: string, relativePath: string, kind: AgentWorkspaceFileKind, label: string): AgentWorkspaceFile | null {
67
+ if (!isInsideHome(home, path)) return null;
68
+ let stats;
69
+ try {
70
+ stats = lstatSync(path);
71
+ } catch {
72
+ return null;
73
+ }
74
+ if (!stats.isFile()) return null;
75
+ const bytes = readFileSync(path);
76
+ if (bytes.includes(0)) return null;
77
+ const truncated = bytes.byteLength > maxWorkspaceFileBytes;
78
+ const content = bytes.subarray(0, maxWorkspaceFileBytes).toString('utf8');
79
+ return {
80
+ path: relativePath,
81
+ label,
82
+ kind,
83
+ size_bytes: stats.size,
84
+ updated_at: stats.mtime.toISOString(),
85
+ content,
86
+ truncated,
87
+ };
88
+ }
89
+
90
+ function collectDirectoryFiles(home: string, spec: DirectorySpec): AgentWorkspaceFile[] {
91
+ const root = join(home, spec.dir);
92
+ if (!existsSync(root) || !isInsideHome(home, root)) return [];
93
+ try {
94
+ if (!lstatSync(root).isDirectory()) return [];
95
+ } catch {
96
+ return [];
97
+ }
98
+
99
+ const files: AgentWorkspaceFile[] = [];
100
+ const walk = (directory: string, relativeDirectory: string, depth: number) => {
101
+ if (depth > 2) return;
102
+ let entries;
103
+ try {
104
+ entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
105
+ } catch {
106
+ return;
107
+ }
108
+ for (const entry of entries) {
109
+ if (entry.name.startsWith('.')) continue;
110
+ const absolutePath = join(directory, entry.name);
111
+ const relativePath = `${relativeDirectory}/${entry.name}`;
112
+ if (!isInsideHome(home, absolutePath)) continue;
113
+ if (entry.isDirectory()) {
114
+ walk(absolutePath, relativePath, depth + 1);
115
+ continue;
116
+ }
117
+ if (!entry.isFile() || !allowedTextExtensions.has(extname(entry.name).toLowerCase())) continue;
118
+ const file = readWorkspaceFile(home, absolutePath, relativePath, spec.kind, basename(entry.name));
119
+ if (file) files.push(file);
120
+ }
121
+ };
122
+ walk(root, spec.dir, 0);
123
+ return files;
124
+ }
125
+
126
+ export function listAgentWorkspaceFiles(agent: AgentDefinition): AgentWorkspaceFile[] {
127
+ const home = agentHomePath(agent.id);
128
+ const files: AgentWorkspaceFile[] = [];
129
+ const memory = readWorkspaceFile(home, join(home, 'MEMORY.md'), 'MEMORY.md', 'memory', 'Memory');
130
+ if (memory) files.push(memory);
131
+ const soul = readWorkspaceFile(home, join(home, 'SOUL.md'), 'SOUL.md', 'soul', 'Soul');
132
+ if (soul) files.push(soul);
133
+ for (const spec of directorySpecs) {
134
+ files.push(...collectDirectoryFiles(home, spec));
135
+ }
136
+ return files;
137
+ }
138
+
139
+ export function updateAgentWorkspaceFile(agent: AgentDefinition, inputPath: string, content: string): AgentWorkspaceFile {
140
+ if (Buffer.byteLength(content, 'utf8') > maxWorkspaceWriteBytes) {
141
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_TOO_LARGE', 400, `workspace file edits are limited to ${maxWorkspaceWriteBytes} bytes`);
142
+ }
143
+ if (content.includes('\0')) {
144
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file content must be text');
145
+ }
146
+
147
+ const relativePath = normalizeWorkspaceRelativePath(inputPath);
148
+ if (!relativePath) {
149
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file path is not allowed');
150
+ }
151
+ const spec = workspaceFileSpec(relativePath);
152
+ if (!spec) {
153
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file path is not allowed');
154
+ }
155
+
156
+ const home = agentHomePath(agent.id);
157
+ const absolutePath = join(home, relativePath);
158
+ if (!isInsideHome(home, absolutePath)) {
159
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file path is not allowed');
160
+ }
161
+
162
+ let stats;
163
+ try {
164
+ stats = lstatSync(absolutePath);
165
+ } catch {
166
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_FOUND', 404, 'workspace file not found');
167
+ }
168
+ if (!stats.isFile()) {
169
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file path is not allowed');
170
+ }
171
+
172
+ const existing = readWorkspaceFile(home, absolutePath, relativePath, spec.kind, spec.label);
173
+ if (!existing) {
174
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file path is not allowed');
175
+ }
176
+
177
+ writeFileSync(absolutePath, content, 'utf8');
178
+ const updated = readWorkspaceFile(home, absolutePath, relativePath, spec.kind, spec.label);
179
+ if (!updated) {
180
+ throw new AgentWorkspaceFileError('WORKSPACE_FILE_NOT_ALLOWED', 400, 'workspace file path is not allowed');
181
+ }
182
+ return updated;
183
+ }