@controlflow-ai/daemon 0.1.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 (67) hide show
  1. package/README.md +360 -0
  2. package/bin/console.js +2 -0
  3. package/bin/daemon.js +2 -0
  4. package/bin/pal.js +2 -0
  5. package/bin/server.js +2 -0
  6. package/package.json +31 -0
  7. package/src/agent-runtime.ts +285 -0
  8. package/src/app.ts +745 -0
  9. package/src/args.ts +54 -0
  10. package/src/artifacts.ts +85 -0
  11. package/src/cli.ts +284 -0
  12. package/src/client.ts +310 -0
  13. package/src/coco.ts +52 -0
  14. package/src/codex.ts +41 -0
  15. package/src/coding-agent-runtime.ts +20 -0
  16. package/src/config.ts +106 -0
  17. package/src/console.ts +349 -0
  18. package/src/daemon-client.ts +91 -0
  19. package/src/daemon.ts +580 -0
  20. package/src/db.ts +2830 -0
  21. package/src/failure-message.ts +17 -0
  22. package/src/format.ts +13 -0
  23. package/src/http.ts +55 -0
  24. package/src/lark/agent-runtime.ts +142 -0
  25. package/src/lark/cli.ts +549 -0
  26. package/src/lark/credentials.ts +105 -0
  27. package/src/lark/daemon-integration.ts +108 -0
  28. package/src/lark/dispatcher.ts +374 -0
  29. package/src/lark/event-router.ts +329 -0
  30. package/src/lark/inbound-events.ts +131 -0
  31. package/src/lark/server-integration.ts +445 -0
  32. package/src/lark/setup.ts +326 -0
  33. package/src/lark/ws-daemon.ts +224 -0
  34. package/src/lark-fixture-diagnostics.ts +56 -0
  35. package/src/lark-fixture.ts +277 -0
  36. package/src/local-api.ts +155 -0
  37. package/src/local-auth.ts +45 -0
  38. package/src/migrations/001_initial.ts +61 -0
  39. package/src/migrations/002_daemon_deliveries.ts +52 -0
  40. package/src/migrations/003_sessions_runs.ts +49 -0
  41. package/src/migrations/004_message_idempotency.ts +21 -0
  42. package/src/migrations/005_artifacts.ts +24 -0
  43. package/src/migrations/006_lark_channel_foundation.ts +119 -0
  44. package/src/migrations/007_agents_a0.ts +17 -0
  45. package/src/migrations/008_b0_chat_history.ts +31 -0
  46. package/src/migrations/009_b0_transcript_ingest_seq.ts +35 -0
  47. package/src/migrations/010_b0_transcript_shadow_external_ids.ts +32 -0
  48. package/src/migrations/011_b0_channel_conversation_audit_only.ts +27 -0
  49. package/src/migrations/012_b0_cross_conversation_invariant.ts +45 -0
  50. package/src/migrations/013_b1_0_eng_inbound_raw_events.ts +56 -0
  51. package/src/migrations/014_agents_runtime.ts +10 -0
  52. package/src/migrations/015_agent_runtime_sessions.ts +15 -0
  53. package/src/migrations/016_room_participants.ts +27 -0
  54. package/src/migrations/017_unified_room_delivery.ts +203 -0
  55. package/src/migrations/018_room_display_names.ts +36 -0
  56. package/src/migrations/019_computer_connections.ts +63 -0
  57. package/src/migrations/020_computer_agent_assignments.ts +20 -0
  58. package/src/migrations/021_provider_identity_bindings.ts +32 -0
  59. package/src/migrations.ts +85 -0
  60. package/src/neeko.ts +23 -0
  61. package/src/provider-identity.ts +40 -0
  62. package/src/runtime-registry.ts +41 -0
  63. package/src/server-auth.ts +13 -0
  64. package/src/server.ts +63 -0
  65. package/src/token-file.ts +57 -0
  66. package/src/types.ts +408 -0
  67. package/src/web.ts +565 -0
package/src/client.ts ADDED
@@ -0,0 +1,310 @@
1
+ import { defaultServerToken, defaultServerUrl } from './config.js';
2
+ import { serverAuthHeaders } from './server-auth.js';
3
+ import type { AgentRoomSubscriptionMode, AgentRun, AgentSession, ApiResponse, Chat, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, Message, MessageDelivery, ProvisionedComputer, RoomChannel, RoomParticipant, RunAction } from './types.js';
4
+
5
+ export interface SendRequest {
6
+ chat?: string;
7
+ room?: string;
8
+ chat_id?: string;
9
+ room_id?: string;
10
+ parent_id?: number;
11
+ channel_id?: string | null;
12
+ sender: string;
13
+ recipient?: string | null;
14
+ content: string;
15
+ type?: 'message' | 'system';
16
+ idempotency_key?: string | null;
17
+ mentions?: string[];
18
+ }
19
+
20
+ export interface DaemonAuth {
21
+ computer_id: string;
22
+ connection_id: string;
23
+ token: string;
24
+ }
25
+
26
+ export interface StartRunRequest {
27
+ message_id: number;
28
+ agent: string;
29
+ cwd: string;
30
+ attempt: number;
31
+ pid?: number | null;
32
+ session_id?: string | null;
33
+ trigger_message_id?: number | null;
34
+ daemon_id?: string | null;
35
+ connection_id?: string | null;
36
+ computer_id?: string | null;
37
+ runtime_provider?: string | null;
38
+ runtime_invocation_id?: string | null;
39
+ delivery_id?: string | null;
40
+ }
41
+
42
+ export interface RegisterDaemonRequest {
43
+ id?: string;
44
+ name: string;
45
+ host?: string;
46
+ local_url?: string;
47
+ server_url?: string;
48
+ agents?: Array<{ agent: string; cwd?: string; capabilities?: Record<string, unknown> }>;
49
+ }
50
+
51
+ export interface ConnectComputerRequest {
52
+ computer_id?: string;
53
+ secret?: string;
54
+ api_key?: string;
55
+ name?: string;
56
+ host?: string;
57
+ local_url?: string;
58
+ server_url?: string;
59
+ agents?: Array<{ agent: string; cwd?: string; capabilities?: Record<string, unknown> }>;
60
+ }
61
+
62
+ export interface ProvisionComputerRequest {
63
+ name?: string;
64
+ server_url?: string;
65
+ package_name?: string;
66
+ }
67
+
68
+ export interface CreateDeliveryRequest {
69
+ message_id: number;
70
+ agent: string;
71
+ }
72
+
73
+ export interface ClaimDeliveryRequest {
74
+ daemon_id: string;
75
+ connection_id?: string | null;
76
+ computer_id?: string | null;
77
+ lease_ms?: number;
78
+ }
79
+
80
+ export interface GetOrCreateSessionRequest {
81
+ chat_id: string;
82
+ agent: string;
83
+ daemon_id: string;
84
+ connection_id?: string | null;
85
+ computer_id?: string | null;
86
+ runtime_provider?: string | null;
87
+ cwd: string;
88
+ last_message_id?: number | null;
89
+ }
90
+
91
+ export interface UpdateSessionRuntimeRequest {
92
+ runtime_session_id: string;
93
+ }
94
+
95
+ export interface FinishDeliveryRequest {
96
+ daemon_id: string;
97
+ connection_id?: string | null;
98
+ claim_token: string;
99
+ run_id?: string;
100
+ error?: string;
101
+ }
102
+
103
+ export interface FinishRunRequest {
104
+ status: AgentRun['status'];
105
+ exit_code?: number | null;
106
+ output?: string;
107
+ }
108
+
109
+ export interface CreateArtifactRequest {
110
+ content_base64: string;
111
+ mime_type: string;
112
+ title?: string;
113
+ filename?: string;
114
+ ttl_seconds?: number;
115
+ }
116
+
117
+ export class LockClient {
118
+ readonly baseUrl: string;
119
+ readonly daemonAuth: DaemonAuth | null;
120
+
121
+ constructor(baseUrl = defaultServerUrl(), daemonAuth: DaemonAuth | null = null) {
122
+ this.baseUrl = baseUrl.replace(/\/$/, '');
123
+ this.daemonAuth = daemonAuth;
124
+ }
125
+
126
+ async health(): Promise<{ status: string }> {
127
+ return this.get('/health');
128
+ }
129
+
130
+ async listChats(): Promise<Chat[]> {
131
+ const data = await this.get<{ chats: Chat[] }>('/api/chats');
132
+ return data.chats;
133
+ }
134
+
135
+ async listRooms(): Promise<Chat[]> {
136
+ const data = await this.get<{ rooms: Chat[] }>('/api/rooms');
137
+ return data.rooms;
138
+ }
139
+
140
+ async listRoomMembers(room: string): Promise<{ room: Chat; participants: RoomParticipant[]; completeness: string }> {
141
+ const data = await this.get<{ room: Chat; participants: RoomParticipant[]; completeness: string }>(`/api/rooms/${encodeURIComponent(room)}/members`);
142
+ return data;
143
+ }
144
+
145
+ async inviteAgentToRoom(room: string, input: { agent: string; mode?: AgentRoomSubscriptionMode }): Promise<{ participant: RoomParticipant; subscription: unknown }> {
146
+ return this.post<{ participant: RoomParticipant; subscription: unknown }>(`/api/rooms/${encodeURIComponent(room)}/agents`, input);
147
+ }
148
+
149
+ async createTopic(room: string, input: { name: string; created_by?: string | null }): Promise<RoomChannel> {
150
+ const data = await this.post<{ channel: RoomChannel }>(`/api/rooms/${encodeURIComponent(room)}/topics`, input);
151
+ return data.channel;
152
+ }
153
+
154
+ async getMessages(params: URLSearchParams): Promise<Message[]> {
155
+ const data = await this.get<{ messages: Message[] }>(`/api/messages?${params}`);
156
+ return data.messages;
157
+ }
158
+
159
+ async getInbox(agent: string, after = 0, limit = 50): Promise<Message[]> {
160
+ const params = new URLSearchParams({ agent, after: String(after), limit: String(limit) });
161
+ const data = await this.get<{ messages: Message[] }>(`/api/inbox?${params}`);
162
+ return data.messages;
163
+ }
164
+
165
+ async getMessage(id: number): Promise<Message> {
166
+ const data = await this.get<{ message: Message }>(`/api/messages/${id}`);
167
+ return data.message;
168
+ }
169
+
170
+ async sendMessage(input: SendRequest): Promise<Message> {
171
+ const data = await this.post<{ message: Message }>('/api/messages', input);
172
+ return data.message;
173
+ }
174
+
175
+ async provisionComputer(input: ProvisionComputerRequest = {}): Promise<ProvisionedComputer> {
176
+ const data = await this.post<ProvisionedComputer>('/api/computers/provision', input);
177
+ return data;
178
+ }
179
+
180
+ async listComputers(): Promise<Computer[]> {
181
+ const data = await this.get<{ computers: Computer[] }>('/api/computers');
182
+ return data.computers;
183
+ }
184
+
185
+ async connectComputer(input: ConnectComputerRequest): Promise<{ computer: Computer; connection: ComputerConnection; token: string; daemon: DaemonInstance; agents: ComputerAgentAssignment[] }> {
186
+ const data = await this.post<{ computer: Computer; connection: ComputerConnection; token: string; daemon: DaemonInstance; agents: ComputerAgentAssignment[] }>('/api/computers/connect', input);
187
+ return data;
188
+ }
189
+
190
+ async heartbeatComputer(computerId: string): Promise<{ computer: Computer; connection: ComputerConnection; agents: ComputerAgentAssignment[] }> {
191
+ const data = await this.post<{ computer: Computer; connection: ComputerConnection; agents: ComputerAgentAssignment[] }>(`/api/computers/${encodeURIComponent(computerId)}/heartbeat`, {});
192
+ return data;
193
+ }
194
+
195
+ async registerDaemon(input: RegisterDaemonRequest): Promise<DaemonInstance> {
196
+ const data = await this.post<{ daemon: DaemonInstance }>('/api/daemons', input);
197
+ return data.daemon;
198
+ }
199
+
200
+ async listDeliveries(agent: string, status = 'pending', limit = 50): Promise<MessageDelivery[]> {
201
+ const params = new URLSearchParams({ agent, status, limit: String(limit) });
202
+ const data = await this.get<{ deliveries: MessageDelivery[] }>(`/api/deliveries?${params}`);
203
+ return data.deliveries;
204
+ }
205
+
206
+ async createDelivery(input: CreateDeliveryRequest): Promise<MessageDelivery> {
207
+ const data = await this.post<{ delivery: MessageDelivery }>('/api/deliveries', input);
208
+ return data.delivery;
209
+ }
210
+
211
+ async claimDelivery(id: string, input: ClaimDeliveryRequest): Promise<MessageDelivery> {
212
+ const data = await this.post<{ delivery: MessageDelivery }>(`/api/deliveries/${id}/claim`, input);
213
+ return data.delivery;
214
+ }
215
+
216
+ async ackDelivery(id: string, input: FinishDeliveryRequest): Promise<MessageDelivery> {
217
+ const data = await this.post<{ delivery: MessageDelivery }>(`/api/deliveries/${id}/ack`, input);
218
+ return data.delivery;
219
+ }
220
+
221
+ async getOrCreateSession(input: GetOrCreateSessionRequest): Promise<AgentSession> {
222
+ const data = await this.post<{ session: AgentSession }>('/api/sessions', input);
223
+ return data.session;
224
+ }
225
+
226
+ async updateSessionRuntimeSessionId(id: string, input: UpdateSessionRuntimeRequest): Promise<AgentSession> {
227
+ const data = await this.post<{ session: AgentSession }>(`/api/sessions/${id}/runtime-session`, input);
228
+ return data.session;
229
+ }
230
+
231
+ async failDelivery(id: string, input: FinishDeliveryRequest): Promise<MessageDelivery> {
232
+ const data = await this.post<{ delivery: MessageDelivery }>(`/api/deliveries/${id}/fail`, input);
233
+ return data.delivery;
234
+ }
235
+
236
+ async createArtifact(input: CreateArtifactRequest, token = defaultServerToken()): Promise<{ artifact: unknown; token: string; url: string }> {
237
+ const data = await this.post<{ artifact: unknown; token: string; url: string }>('/api/artifacts', input, serverAuthHeaders(token));
238
+ return data;
239
+ }
240
+
241
+ async listArtifacts(token = defaultServerToken()): Promise<unknown[]> {
242
+ const data = await this.get<{ artifacts: unknown[] }>('/api/artifacts', serverAuthHeaders(token));
243
+ return data.artifacts;
244
+ }
245
+
246
+ async revokeArtifact(id: string, token = defaultServerToken()): Promise<unknown> {
247
+ const data = await this.post<{ artifact: unknown }>(`/api/artifacts/${id}/revoke`, {}, serverAuthHeaders(token));
248
+ return data.artifact;
249
+ }
250
+
251
+ async listRuns(): Promise<AgentRun[]> {
252
+ const data = await this.get<{ runs: AgentRun[] }>('/api/runs');
253
+ return data.runs;
254
+ }
255
+
256
+ async startRun(input: StartRunRequest): Promise<AgentRun> {
257
+ const data = await this.post<{ run: AgentRun }>('/api/runs', input);
258
+ return data.run;
259
+ }
260
+
261
+ async updateRunPid(runId: string, pid: number | null): Promise<AgentRun> {
262
+ const data = await this.post<{ run: AgentRun }>(`/api/runs/${runId}/pid`, { pid });
263
+ return data.run;
264
+ }
265
+
266
+ async finishRun(runId: string, input: FinishRunRequest): Promise<AgentRun> {
267
+ const data = await this.post<{ run: AgentRun }>(`/api/runs/${runId}/finish`, input);
268
+ return data.run;
269
+ }
270
+
271
+ async requestRunAction(runId: string, action: RunAction): Promise<AgentRun> {
272
+ const data = await this.post<{ run: AgentRun }>(`/api/runs/${runId}/${action}`, {});
273
+ return data.run;
274
+ }
275
+
276
+ async getRun(runId: string): Promise<AgentRun> {
277
+ const data = await this.get<{ run: AgentRun }>(`/api/runs/${runId}`);
278
+ return data.run;
279
+ }
280
+
281
+ private async get<T>(path: string, headers?: HeadersInit): Promise<T> {
282
+ return this.request<T>(path, { method: 'GET', headers: { ...this.daemonAuthHeaders(), ...headers } });
283
+ }
284
+
285
+ private async post<T>(path: string, body: unknown, headers?: HeadersInit): Promise<T> {
286
+ return this.request<T>(path, {
287
+ method: 'POST',
288
+ headers: { 'content-type': 'application/json', ...this.daemonAuthHeaders(), ...headers },
289
+ body: JSON.stringify(body),
290
+ });
291
+ }
292
+
293
+ private daemonAuthHeaders(): HeadersInit {
294
+ if (!this.daemonAuth) return {};
295
+ return {
296
+ 'x-pal-computer-id': this.daemonAuth.computer_id,
297
+ 'x-pal-connection-id': this.daemonAuth.connection_id,
298
+ 'x-pal-connection-token': this.daemonAuth.token,
299
+ };
300
+ }
301
+
302
+ private async request<T>(path: string, init: RequestInit): Promise<T> {
303
+ const response = await fetch(`${this.baseUrl}${path}`, init);
304
+ const payload = await response.json() as ApiResponse<T>;
305
+ if (!response.ok || !payload.ok) {
306
+ throw new Error(payload.message ?? `request failed: ${response.status}`);
307
+ }
308
+ return payload.data as T;
309
+ }
310
+ }
package/src/coco.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { buildPalPrompt, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
2
+ export { runAgentRuntime } from './agent-runtime.js';
3
+
4
+ function extractSessionId(stdout: string): string | null {
5
+ for (const line of stdout.split(/\r?\n/)) {
6
+ const trimmed = line.trim();
7
+ if (!trimmed.startsWith('{')) continue;
8
+ try {
9
+ const event = JSON.parse(trimmed) as { session_id?: unknown };
10
+ if (typeof event.session_id === 'string' && event.session_id.trim()) {
11
+ return event.session_id;
12
+ }
13
+ } catch {
14
+ // Non-JSON stdout is kept in output; it just is not a JSONL event.
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export function makeCocoRuntime(_agentUuid: string): AgentRuntime {
21
+ return {
22
+ name: 'coco',
23
+ capabilities: { protocol: 'acp', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: true },
24
+ command: 'coco',
25
+ buildPrompt: buildPalPrompt,
26
+ buildCwd: runtimeCwd,
27
+ buildArgs(input: AgentRuntimeRunInput): string[] {
28
+ return ['acp', 'serve', '-y', ...input.extraArgs];
29
+ },
30
+ };
31
+ }
32
+
33
+ export function makeCocoStreamJsonRuntime(_agentUuid: string): AgentRuntime {
34
+ return {
35
+ name: 'coco-stream-json',
36
+ capabilities: { protocol: 'json-stream', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: false },
37
+ command: 'coco',
38
+ buildPrompt: buildPalPrompt,
39
+ buildCwd: runtimeCwd,
40
+ buildArgs(input: AgentRuntimeRunInput): string[] {
41
+ const prompt = buildPalPrompt(input);
42
+ const resumeArgs = input.runtimeSessionId ? ['--resume', input.runtimeSessionId] : [];
43
+ return ['-y', '-p', '--output-format', 'stream-json', ...resumeArgs, ...input.extraArgs, prompt];
44
+ },
45
+ parseOutput({ stdout, stderr, input }) {
46
+ return {
47
+ output: [stdout.trim(), stderr.trim()].filter(Boolean).join('\n'),
48
+ runtimeSessionId: extractSessionId(stdout) ?? input.runtimeSessionId ?? null,
49
+ };
50
+ },
51
+ };
52
+ }
package/src/codex.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { buildPalPrompt, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
2
+ export { runAgentRuntime } from './agent-runtime.js';
3
+
4
+ function extractThreadId(stdout: string): string | null {
5
+ for (const line of stdout.split(/\r?\n/)) {
6
+ const trimmed = line.trim();
7
+ if (!trimmed.startsWith('{')) continue;
8
+ try {
9
+ const event = JSON.parse(trimmed) as { type?: unknown; thread_id?: unknown };
10
+ if (event.type === 'thread.started' && typeof event.thread_id === 'string' && event.thread_id.trim()) {
11
+ return event.thread_id;
12
+ }
13
+ } catch {
14
+ // Non-JSON stdout is kept in output; it just is not a JSONL event.
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export function makeCodexRuntime(_agentUuid: string): AgentRuntime {
21
+ return {
22
+ name: 'codex',
23
+ capabilities: { protocol: 'json-stream', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: false },
24
+ command: 'codex',
25
+ buildPrompt: buildPalPrompt,
26
+ buildCwd: runtimeCwd,
27
+ buildArgs(input: AgentRuntimeRunInput): string[] {
28
+ const prompt = buildPalPrompt(input);
29
+ if (input.runtimeSessionId) {
30
+ return ['exec', 'resume', '--json', '--dangerously-bypass-approvals-and-sandbox', ...input.extraArgs, input.runtimeSessionId, prompt];
31
+ }
32
+ return ['exec', '--json', '--dangerously-bypass-approvals-and-sandbox', '--cd', runtimeCwd(input), ...input.extraArgs, prompt];
33
+ },
34
+ parseOutput({ stdout, stderr, input }) {
35
+ return {
36
+ output: [stdout.trim(), stderr.trim()].filter(Boolean).join('\n'),
37
+ runtimeSessionId: extractThreadId(stdout) ?? input.runtimeSessionId ?? null,
38
+ };
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ buildPalPrompt,
3
+ formatRecentMessageContext,
4
+ formatRoomParticipantContext,
5
+ runAgentRuntime,
6
+ runCodingAgent,
7
+ runtimeCwd,
8
+ runtimeProjectCwd,
9
+ } from './agent-runtime.js';
10
+ export type {
11
+ AgentRuntime,
12
+ AgentRuntimeCapabilities,
13
+ AgentRuntimeProtocol,
14
+ AgentRuntimeRunInput,
15
+ AgentRuntimeRunResult,
16
+ CodingAgentRunInput,
17
+ CodingAgentRunResult,
18
+ CodingAgentRuntime,
19
+ RuntimeDriver,
20
+ } from './agent-runtime.js';
package/src/config.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ export const DEFAULT_PORT = 4127;
6
+ export const DEFAULT_HOST = '127.0.0.1';
7
+ export const DEFAULT_DAEMON_PORT = 4137;
8
+
9
+ export function homeDir(): string {
10
+ return resolve(process.env.PAL_HOME ?? join(homedir(), '.pal'));
11
+ }
12
+
13
+ export function defaultServerUrl(): string {
14
+ return process.env.LOCK_SERVER_URL ?? process.env.PAL_SERVER ?? `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
15
+ }
16
+
17
+ export function defaultDaemonUrl(): string {
18
+ return process.env.LOCK_DAEMON_URL ?? `http://${DEFAULT_HOST}:${DEFAULT_DAEMON_PORT}`;
19
+ }
20
+
21
+ export function defaultDaemonToken(): string | undefined {
22
+ return process.env.LOCK_DAEMON_TOKEN;
23
+ }
24
+
25
+ export function defaultServerToken(): string | undefined {
26
+ return process.env.LOCK_SERVER_TOKEN;
27
+ }
28
+
29
+ export function defaultDbPath(): string {
30
+ return resolve(process.env.PAL_DB ?? join(homeDir(), 'lock.sqlite'));
31
+ }
32
+
33
+ export function defaultStatePath(agent: string): string {
34
+ return join(homeDir(), 'daemon', `${agent}.json`);
35
+ }
36
+
37
+ export function agentHomePath(agentUuid: string): string {
38
+ return join(homeDir(), 'agents', agentUuid);
39
+ }
40
+
41
+ export function ensureParentDir(path: string): void {
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ }
44
+
45
+ export function ensureDir(path: string): void {
46
+ mkdirSync(path, { recursive: true });
47
+ }
48
+
49
+ export function ensureAgentHome(path: string, agentKey: string, runtime: string): void {
50
+ ensureDir(path);
51
+ ensureDir(join(path, 'state'));
52
+ ensureDir(join(path, 'kb'));
53
+ ensureDir(join(path, 'archive'));
54
+
55
+ const memoryPath = join(path, 'MEMORY.md');
56
+ if (!existsSync(memoryPath)) {
57
+ writeFileSync(memoryPath, `# ${agentKey}
58
+
59
+ ## Active Context
60
+ - Current focus: Fresh PAL agent home initialized.
61
+
62
+ ## Recovery Read Order
63
+ 1. Read state/active-task.json when it exists.
64
+ 2. Read state/open-loops.json when it exists.
65
+ 3. Read state/active-context.md when it exists.
66
+ 4. Read relevant kb/* notes when needed.
67
+
68
+ ## Memory Map
69
+ - kb/index.md: durable local knowledge index.
70
+ - state/: current task and recovery state.
71
+ - archive/: historical fallback notes.
72
+ `);
73
+ }
74
+
75
+ const soulPath = join(path, 'SOUL.md');
76
+ if (!existsSync(soulPath)) {
77
+ writeFileSync(soulPath, `# ${agentKey}
78
+
79
+ ## Purpose
80
+ Persistent PAL agent.
81
+
82
+ ## Defaults
83
+ - runtime: ${runtime}
84
+
85
+ ## Notes
86
+ - This file is local to the agent home and may be edited by the agent.
87
+ `);
88
+ }
89
+ }
90
+
91
+ export function repoCliPath(): string {
92
+ return resolve(import.meta.dir, 'cli.ts');
93
+ }
94
+
95
+ export function privatePalCliBinDir(): string {
96
+ return join(homeDir(), 'bin');
97
+ }
98
+
99
+ export function ensurePrivatePalCliBin(): string {
100
+ const binDir = privatePalCliBinDir();
101
+ ensureDir(binDir);
102
+ const linkPath = join(binDir, 'pal');
103
+ rmSync(linkPath, { force: true });
104
+ symlinkSync(repoCliPath(), linkPath);
105
+ return binDir;
106
+ }