@openclawbrain/openclaw 0.3.0 → 0.3.2

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 (41) hide show
  1. package/README.md +3 -1
  2. package/dist/extension/index.js +9 -1
  3. package/dist/extension/index.js.map +1 -1
  4. package/dist/extension/runtime-guard.js +6 -1
  5. package/dist/extension/runtime-guard.js.map +1 -1
  6. package/dist/src/cli.d.ts +18 -6
  7. package/dist/src/cli.js +1991 -293
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +42 -1
  10. package/dist/src/daemon.js +360 -50
  11. package/dist/src/daemon.js.map +1 -1
  12. package/dist/src/index.d.ts +65 -1
  13. package/dist/src/index.js +627 -56
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/learning-spine.d.ts +3 -1
  16. package/dist/src/learning-spine.js +1 -0
  17. package/dist/src/learning-spine.js.map +1 -1
  18. package/dist/src/local-session-passive-learning.js +6 -1
  19. package/dist/src/local-session-passive-learning.js.map +1 -1
  20. package/dist/src/openclaw-home-layout.d.ts +17 -0
  21. package/dist/src/openclaw-home-layout.js +182 -0
  22. package/dist/src/openclaw-home-layout.js.map +1 -0
  23. package/dist/src/provider-config.d.ts +36 -0
  24. package/dist/src/provider-config.js +181 -25
  25. package/dist/src/provider-config.js.map +1 -1
  26. package/dist/src/resolve-activation-root.d.ts +3 -3
  27. package/dist/src/resolve-activation-root.js +21 -26
  28. package/dist/src/resolve-activation-root.js.map +1 -1
  29. package/dist/src/semantic-metadata.d.ts +4 -0
  30. package/dist/src/semantic-metadata.js +41 -0
  31. package/dist/src/semantic-metadata.js.map +1 -0
  32. package/dist/src/session-store.js +16 -5
  33. package/dist/src/session-store.js.map +1 -1
  34. package/dist/src/session-tail.d.ts +2 -0
  35. package/dist/src/session-tail.js +68 -16
  36. package/dist/src/session-tail.js.map +1 -1
  37. package/dist/src/shadow-extension-proof.js +4 -0
  38. package/dist/src/shadow-extension-proof.js.map +1 -1
  39. package/extension/index.ts +17 -0
  40. package/extension/runtime-guard.ts +7 -1
  41. package/package.json +7 -7
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * macOS launchd daemon management for OpenClawBrain.
3
3
  *
4
- * Manages a launchd user agent that runs `openclawbrain watch` in the background.
4
+ * Manages macOS launchd user agents that run `openclawbrain watch` in the background.
5
+ * Service identity is derived per activation root so one profile/service boundary
6
+ * does not collide with another.
5
7
  *
6
8
  * Commands:
7
9
  * daemon start — generate and load a launchd plist
@@ -10,6 +12,17 @@
10
12
  * daemon logs — tail the daemon log file
11
13
  */
12
14
  type DaemonCommandRunner = (command: string) => string;
15
+ export interface DaemonServiceIdentity {
16
+ requestedActivationRoot: string;
17
+ canonicalActivationRoot: string;
18
+ activationRootHash: string;
19
+ activationRootSlug: string;
20
+ label: string;
21
+ plistFilename: string;
22
+ plistPath: string;
23
+ logPath: string;
24
+ }
25
+ export declare function buildDaemonServiceIdentity(activationRoot: string): DaemonServiceIdentity;
13
26
  export declare function setDaemonCommandRunnerForTesting(runner: DaemonCommandRunner | null): void;
14
27
  export type DaemonSubcommand = "start" | "stop" | "status" | "logs";
15
28
  export interface DaemonCliArgs {
@@ -19,6 +32,34 @@ export interface DaemonCliArgs {
19
32
  json: boolean;
20
33
  help: boolean;
21
34
  }
35
+ export interface ManagedLearnerServiceInspection {
36
+ requestedActivationRoot: string;
37
+ canonicalActivationRoot: string;
38
+ serviceLabel: string;
39
+ plistPath: string;
40
+ logPath: string;
41
+ installed: boolean;
42
+ running: boolean;
43
+ pid: number | null;
44
+ configuredActivationRoot: string | null;
45
+ matchesRequestedActivationRoot: boolean | null;
46
+ launchctlAvailable: boolean;
47
+ }
48
+ export interface ManagedLearnerServiceEnsureResult {
49
+ state: "started" | "ensured" | "deferred";
50
+ reason: "started_exact_root" | "already_running_exact_root" | "launchctl_unavailable" | "launch_command_unavailable" | "launch_failed";
51
+ detail: string;
52
+ inspection: ManagedLearnerServiceInspection;
53
+ }
54
+ export interface ManagedLearnerServiceRemovalResult {
55
+ state: "removed" | "preserved" | "already_absent";
56
+ reason: "removed_exact_root" | "not_installed" | "configured_root_mismatch" | "launchctl_unavailable" | "stop_failed";
57
+ detail: string;
58
+ inspection: ManagedLearnerServiceInspection;
59
+ }
60
+ export declare function inspectManagedLearnerService(activationRoot: string): ManagedLearnerServiceInspection;
61
+ export declare function ensureManagedLearnerServiceForActivationRoot(activationRoot: string): ManagedLearnerServiceEnsureResult;
62
+ export declare function removeManagedLearnerServiceForActivationRoot(activationRoot: string): ManagedLearnerServiceRemovalResult;
22
63
  export declare function daemonStart(activationRoot: string, json: boolean): number;
23
64
  export declare function daemonStop(activationRoot: string, json: boolean): number;
24
65
  export declare function daemonStatus(activationRoot: string, json: boolean): number;
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * macOS launchd daemon management for OpenClawBrain.
3
3
  *
4
- * Manages a launchd user agent that runs `openclawbrain watch` in the background.
4
+ * Manages macOS launchd user agents that run `openclawbrain watch` in the background.
5
+ * Service identity is derived per activation root so one profile/service boundary
6
+ * does not collide with another.
5
7
  *
6
8
  * Commands:
7
9
  * daemon start — generate and load a launchd plist
@@ -10,11 +12,13 @@
10
12
  * daemon logs — tail the daemon log file
11
13
  */
12
14
  import { execSync } from "node:child_process";
13
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
15
+ import { createHash } from "node:crypto";
16
+ import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
14
17
  import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
15
19
  import { loadTeacherSurface, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath } from "./index.js";
16
- const LABEL = "com.openclawbrain.daemon";
17
- const PLIST_FILENAME = `${LABEL}.plist`;
20
+ const LABEL_PREFIX = "com.openclawbrain.daemon";
21
+ const LOG_ROOT_DIRNAME = "daemon";
18
22
  const DEFAULT_SCAN_ROOT_DIRNAME = "event-exports";
19
23
  const BASELINE_STATE_BASENAME = "baseline-state.json";
20
24
  const SCANNER_CHECKPOINT_BASENAME = ".openclawbrain-scanner-checkpoint.json";
@@ -26,53 +30,138 @@ let daemonCommandRunner = DEFAULT_DAEMON_COMMAND_RUNNER;
26
30
  function getHomeDir() {
27
31
  return process.env.HOME ?? process.env.USERPROFILE ?? "~";
28
32
  }
29
- function getPlistPath() {
30
- return path.join(getHomeDir(), "Library", "LaunchAgents", PLIST_FILENAME);
33
+ function canonicalizeActivationRoot(activationRoot) {
34
+ const resolvedActivationRoot = path.resolve(activationRoot);
35
+ return existsSync(resolvedActivationRoot) ? safeRealpath(resolvedActivationRoot) : resolvedActivationRoot;
31
36
  }
32
- function getLogPath() {
33
- return path.join(getHomeDir(), ".openclawbrain", "daemon.log");
37
+ function sanitizeActivationRootSlug(value) {
38
+ const sanitized = value
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9]+/g, "-")
41
+ .replace(/^-+|-+$/g, "");
42
+ return sanitized.length > 0 ? sanitized.slice(0, 32) : "activation-root";
43
+ }
44
+ export function buildDaemonServiceIdentity(activationRoot) {
45
+ const requestedActivationRoot = path.resolve(activationRoot);
46
+ const canonicalActivationRoot = canonicalizeActivationRoot(requestedActivationRoot);
47
+ const activationRootHash = createHash("sha256").update(canonicalActivationRoot).digest("hex").slice(0, 12);
48
+ const activationRootSlug = sanitizeActivationRootSlug(path.basename(canonicalActivationRoot));
49
+ const label = `${LABEL_PREFIX}.${activationRootSlug}.${activationRootHash}`;
50
+ const plistFilename = `${label}.plist`;
51
+ return {
52
+ requestedActivationRoot,
53
+ canonicalActivationRoot,
54
+ activationRootHash,
55
+ activationRootSlug,
56
+ label,
57
+ plistFilename,
58
+ plistPath: path.join(getHomeDir(), "Library", "LaunchAgents", plistFilename),
59
+ logPath: path.join(getHomeDir(), ".openclawbrain", LOG_ROOT_DIRNAME, `${activationRootSlug}-${activationRootHash}.log`)
60
+ };
34
61
  }
35
62
  export function setDaemonCommandRunnerForTesting(runner) {
36
63
  daemonCommandRunner = runner ?? DEFAULT_DAEMON_COMMAND_RUNNER;
37
64
  }
38
65
  function getOpenclawbrainBinPath() {
39
- // Prefer the resolved path of the currently running CLI
40
- const selfBin = process.argv[1];
41
- if (selfBin) {
42
- // If running from dist/src/cli.js, the bin symlink resolves to the same thing
43
- const dir = path.dirname(selfBin);
44
- const candidate = path.join(dir, "..", "..", "node_modules", ".bin", "openclawbrain");
45
- if (existsSync(candidate)) {
46
- return path.resolve(candidate);
47
- }
66
+ try {
67
+ const resolved = daemonCommandRunner("which openclawbrain").trim();
68
+ return resolved.length > 0 ? resolved : null;
48
69
  }
49
- // Fall back to finding it on PATH
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function safeRealpath(filePath) {
50
75
  try {
51
- return daemonCommandRunner("which openclawbrain").trim();
76
+ return realpathSync(filePath);
52
77
  }
53
78
  catch {
54
- return "openclawbrain";
79
+ return filePath;
80
+ }
81
+ }
82
+ function resolvePackageRoot(startDir) {
83
+ let currentDir = path.resolve(startDir);
84
+ while (true) {
85
+ const packageJsonPath = path.join(currentDir, "package.json");
86
+ if (existsSync(packageJsonPath)) {
87
+ try {
88
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
89
+ if (packageJson.name === "@openclawbrain/openclaw") {
90
+ return currentDir;
91
+ }
92
+ }
93
+ catch {
94
+ // Ignore malformed package.json while searching upward for the real package root.
95
+ }
96
+ }
97
+ const parentDir = path.dirname(currentDir);
98
+ if (parentDir === currentDir) {
99
+ return null;
100
+ }
101
+ currentDir = parentDir;
55
102
  }
56
103
  }
57
- function buildPlistXml(activationRoot) {
58
- const logPath = getLogPath();
104
+ function resolveCliScriptCandidate(candidatePath) {
105
+ if (typeof candidatePath !== "string" || candidatePath.trim().length === 0) {
106
+ return null;
107
+ }
108
+ const absoluteCandidate = path.resolve(candidatePath);
109
+ if (!existsSync(absoluteCandidate)) {
110
+ return null;
111
+ }
112
+ const resolvedCandidate = safeRealpath(absoluteCandidate);
113
+ const basename = path.basename(resolvedCandidate);
114
+ if (basename !== "cli.js" && basename !== "cli.cjs" && basename !== "cli.mjs") {
115
+ return null;
116
+ }
117
+ return resolvedCandidate;
118
+ }
119
+ function getOpenclawbrainCliScriptPath() {
120
+ const moduleFilePath = fileURLToPath(import.meta.url);
121
+ const moduleDir = path.dirname(moduleFilePath);
122
+ const packageRoot = resolvePackageRoot(moduleDir);
123
+ const candidates = [
124
+ process.argv[1],
125
+ path.join(moduleDir, "cli.js"),
126
+ packageRoot === null ? null : path.join(packageRoot, "dist", "src", "cli.js")
127
+ ];
128
+ for (const candidate of candidates) {
129
+ const resolved = resolveCliScriptCandidate(candidate);
130
+ if (resolved !== null) {
131
+ return resolved;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ function resolveDaemonProgramArguments() {
137
+ const cliScriptPath = getOpenclawbrainCliScriptPath();
138
+ if (cliScriptPath !== null) {
139
+ return [process.execPath, cliScriptPath];
140
+ }
59
141
  const binPath = getOpenclawbrainBinPath();
142
+ if (binPath !== null) {
143
+ return [binPath];
144
+ }
145
+ return null;
146
+ }
147
+ function buildPlistXml(serviceIdentity, programArguments) {
148
+ const logPath = serviceIdentity.logPath;
60
149
  const homeDir = getHomeDir();
150
+ const daemonProgramArguments = [...programArguments, "watch", "--activation-root", serviceIdentity.requestedActivationRoot]
151
+ .map((argument) => ` <string>${argument}</string>`)
152
+ .join("\n");
61
153
  return `<?xml version="1.0" encoding="UTF-8"?>
62
154
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
63
155
  <plist version="1.0">
64
156
  <dict>
65
157
  <key>Label</key>
66
- <string>${LABEL}</string>
158
+ <string>${serviceIdentity.label}</string>
67
159
  <key>ProgramArguments</key>
68
160
  <array>
69
- <string>${binPath}</string>
70
- <string>watch</string>
71
- <string>--activation-root</string>
72
- <string>${activationRoot}</string>
161
+ ${daemonProgramArguments}
73
162
  </array>
74
163
  <key>WorkingDirectory</key>
75
- <string>${activationRoot}</string>
164
+ <string>${serviceIdentity.requestedActivationRoot}</string>
76
165
  <key>StandardOutPath</key>
77
166
  <string>${logPath}</string>
78
167
  <key>StandardErrorPath</key>
@@ -92,12 +181,20 @@ function buildPlistXml(activationRoot) {
92
181
  </plist>
93
182
  `;
94
183
  }
95
- function ensureLogDir() {
96
- const logDir = path.dirname(getLogPath());
184
+ function ensureLogDir(logPath) {
185
+ const logDir = path.dirname(logPath);
97
186
  if (!existsSync(logDir)) {
98
187
  mkdirSync(logDir, { recursive: true });
99
188
  }
100
189
  }
190
+ function hasLaunchctl() {
191
+ try {
192
+ return daemonCommandRunner("command -v launchctl").trim().length > 0;
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
101
198
  function launchctlLoad(plistPath) {
102
199
  try {
103
200
  daemonCommandRunner(`launchctl load -w ${JSON.stringify(plistPath)}`);
@@ -118,11 +215,11 @@ function launchctlUnload(plistPath) {
118
215
  return { ok: false, message: `Failed to unload plist: ${message}` };
119
216
  }
120
217
  }
121
- function getLaunchctlInfo() {
218
+ function getLaunchctlInfo(label) {
122
219
  try {
123
220
  const output = daemonCommandRunner("launchctl list");
124
221
  for (const line of output.split("\n")) {
125
- if (line.includes(LABEL)) {
222
+ if (line.includes(label)) {
126
223
  const parts = line.trim().split(/\s+/);
127
224
  const pidStr = parts[0];
128
225
  const pid = pidStr && pidStr !== "-" ? parseInt(pidStr, 10) : null;
@@ -135,6 +232,166 @@ function getLaunchctlInfo() {
135
232
  }
136
233
  return { running: false, pid: null };
137
234
  }
235
+ function inspectManagedLearnerServiceInternal(activationRoot) {
236
+ const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
237
+ const configuredActivationRoot = readDaemonActivationRoot(serviceIdentity.plistPath);
238
+ const info = getLaunchctlInfo(serviceIdentity.label);
239
+ return {
240
+ requestedActivationRoot: serviceIdentity.requestedActivationRoot,
241
+ canonicalActivationRoot: serviceIdentity.canonicalActivationRoot,
242
+ serviceLabel: serviceIdentity.label,
243
+ plistPath: serviceIdentity.plistPath,
244
+ logPath: serviceIdentity.logPath,
245
+ installed: existsSync(serviceIdentity.plistPath),
246
+ running: info.running,
247
+ pid: info.pid,
248
+ configuredActivationRoot,
249
+ matchesRequestedActivationRoot: configuredActivationRoot === null
250
+ ? null
251
+ : canonicalizeActivationRoot(configuredActivationRoot) === serviceIdentity.canonicalActivationRoot,
252
+ launchctlAvailable: hasLaunchctl()
253
+ };
254
+ }
255
+ function startManagedLearnerService(activationRoot) {
256
+ const inspectionBeforeStart = inspectManagedLearnerServiceInternal(activationRoot);
257
+ const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
258
+ if (!inspectionBeforeStart.launchctlAvailable) {
259
+ return {
260
+ ok: false,
261
+ message: "launchctl is unavailable on this host",
262
+ inspection: inspectionBeforeStart
263
+ };
264
+ }
265
+ const programArguments = resolveDaemonProgramArguments();
266
+ if (programArguments === null) {
267
+ return {
268
+ ok: false,
269
+ message: "Failed to resolve an OpenClawBrain CLI launch command.",
270
+ inspection: inspectionBeforeStart
271
+ };
272
+ }
273
+ const launchAgentsDir = path.dirname(inspectionBeforeStart.plistPath);
274
+ if (!existsSync(launchAgentsDir)) {
275
+ mkdirSync(launchAgentsDir, { recursive: true });
276
+ }
277
+ ensureLogDir(serviceIdentity.logPath);
278
+ const plistContent = buildPlistXml(serviceIdentity, programArguments);
279
+ writeFileSync(inspectionBeforeStart.plistPath, plistContent, "utf8");
280
+ const result = launchctlLoad(inspectionBeforeStart.plistPath);
281
+ if (!result.ok && !inspectionBeforeStart.installed) {
282
+ try {
283
+ unlinkSync(inspectionBeforeStart.plistPath);
284
+ }
285
+ catch {
286
+ // Best effort cleanup for failed first-time auto-start attempts.
287
+ }
288
+ }
289
+ return {
290
+ ok: result.ok,
291
+ message: result.message,
292
+ inspection: inspectManagedLearnerServiceInternal(activationRoot)
293
+ };
294
+ }
295
+ function stopManagedLearnerService(activationRoot) {
296
+ const inspectionBeforeStop = inspectManagedLearnerServiceInternal(activationRoot);
297
+ if (!inspectionBeforeStop.installed) {
298
+ return {
299
+ ok: true,
300
+ message: "No daemon plist found.",
301
+ inspection: inspectionBeforeStop
302
+ };
303
+ }
304
+ if (!inspectionBeforeStop.launchctlAvailable) {
305
+ return {
306
+ ok: false,
307
+ message: "launchctl is unavailable on this host",
308
+ inspection: inspectionBeforeStop
309
+ };
310
+ }
311
+ const result = launchctlUnload(inspectionBeforeStop.plistPath);
312
+ if (result.ok) {
313
+ try {
314
+ unlinkSync(inspectionBeforeStop.plistPath);
315
+ }
316
+ catch {
317
+ // best effort
318
+ }
319
+ }
320
+ return {
321
+ ok: result.ok,
322
+ message: result.message,
323
+ inspection: inspectManagedLearnerServiceInternal(activationRoot)
324
+ };
325
+ }
326
+ export function inspectManagedLearnerService(activationRoot) {
327
+ return inspectManagedLearnerServiceInternal(activationRoot);
328
+ }
329
+ export function ensureManagedLearnerServiceForActivationRoot(activationRoot) {
330
+ const inspection = inspectManagedLearnerServiceInternal(activationRoot);
331
+ if (inspection.matchesRequestedActivationRoot === true && inspection.running) {
332
+ return {
333
+ state: "ensured",
334
+ reason: "already_running_exact_root",
335
+ detail: `Learner auto-start already ensured for ${inspection.requestedActivationRoot}; the matching background learner service is running.`,
336
+ inspection
337
+ };
338
+ }
339
+ const startResult = startManagedLearnerService(activationRoot);
340
+ if (startResult.ok) {
341
+ return {
342
+ state: "started",
343
+ reason: "started_exact_root",
344
+ detail: `Started the background learner service for ${startResult.inspection.requestedActivationRoot}; passive learning can begin for this attached profile now.`,
345
+ inspection: startResult.inspection
346
+ };
347
+ }
348
+ const reason = !inspection.launchctlAvailable
349
+ ? "launchctl_unavailable"
350
+ : startResult.message === "Failed to resolve an OpenClawBrain CLI launch command."
351
+ ? "launch_command_unavailable"
352
+ : "launch_failed";
353
+ return {
354
+ state: "deferred",
355
+ reason,
356
+ detail: `Learner auto-start deferred for ${inspection.requestedActivationRoot}: ${startResult.message}`,
357
+ inspection: startResult.inspection
358
+ };
359
+ }
360
+ export function removeManagedLearnerServiceForActivationRoot(activationRoot) {
361
+ const inspection = inspectManagedLearnerServiceInternal(activationRoot);
362
+ if (!inspection.installed) {
363
+ return {
364
+ state: "already_absent",
365
+ reason: "not_installed",
366
+ detail: `No background learner service is installed for ${inspection.requestedActivationRoot}.`,
367
+ inspection
368
+ };
369
+ }
370
+ if (inspection.matchesRequestedActivationRoot === false) {
371
+ return {
372
+ state: "preserved",
373
+ reason: "configured_root_mismatch",
374
+ detail: `Preserved the background learner service because ${inspection.plistPath} is configured for ${inspection.configuredActivationRoot}, ` +
375
+ `not the requested exact root ${inspection.requestedActivationRoot}.`,
376
+ inspection
377
+ };
378
+ }
379
+ const stopResult = stopManagedLearnerService(activationRoot);
380
+ if (stopResult.ok) {
381
+ return {
382
+ state: "removed",
383
+ reason: "removed_exact_root",
384
+ detail: `Removed the background learner service for ${stopResult.inspection.requestedActivationRoot}.`,
385
+ inspection: stopResult.inspection
386
+ };
387
+ }
388
+ return {
389
+ state: "preserved",
390
+ reason: inspection.launchctlAvailable ? "stop_failed" : "launchctl_unavailable",
391
+ detail: `Preserved the background learner service for ${inspection.requestedActivationRoot}: ${stopResult.message}`,
392
+ inspection: stopResult.inspection
393
+ };
394
+ }
138
395
  function readLastLines(filePath, count) {
139
396
  if (!existsSync(filePath))
140
397
  return [];
@@ -351,16 +608,36 @@ function readWatchStateSummary(activationRoot) {
351
608
  }
352
609
  // ─── Subcommand implementations ─────────────────────────────────────────────
353
610
  export function daemonStart(activationRoot, json) {
354
- const plistPath = getPlistPath();
355
- const logPath = getLogPath();
611
+ const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
612
+ const plistPath = serviceIdentity.plistPath;
613
+ const logPath = serviceIdentity.logPath;
614
+ const programArguments = resolveDaemonProgramArguments();
615
+ if (programArguments === null) {
616
+ const message = "Failed to resolve an OpenClawBrain CLI launch command. Install/build the local package or make `openclawbrain` available on PATH.";
617
+ if (json) {
618
+ console.log(JSON.stringify({
619
+ command: "daemon start",
620
+ ok: false,
621
+ plistPath,
622
+ logPath,
623
+ activationRoot: serviceIdentity.requestedActivationRoot,
624
+ serviceLabel: serviceIdentity.label,
625
+ message,
626
+ }, null, 2));
627
+ }
628
+ else {
629
+ console.error(`✗ ${message}`);
630
+ }
631
+ return 1;
632
+ }
356
633
  // Ensure LaunchAgents dir exists
357
634
  const launchAgentsDir = path.dirname(plistPath);
358
635
  if (!existsSync(launchAgentsDir)) {
359
636
  mkdirSync(launchAgentsDir, { recursive: true });
360
637
  }
361
- ensureLogDir();
638
+ ensureLogDir(logPath);
362
639
  // Write the plist
363
- const plistContent = buildPlistXml(activationRoot);
640
+ const plistContent = buildPlistXml(serviceIdentity, programArguments);
364
641
  writeFileSync(plistPath, plistContent, "utf8");
365
642
  // Load it
366
643
  const result = launchctlLoad(plistPath);
@@ -370,16 +647,18 @@ export function daemonStart(activationRoot, json) {
370
647
  ok: result.ok,
371
648
  plistPath,
372
649
  logPath,
373
- activationRoot,
650
+ activationRoot: serviceIdentity.requestedActivationRoot,
651
+ serviceLabel: serviceIdentity.label,
374
652
  message: result.message,
375
653
  }, null, 2));
376
654
  }
377
655
  else {
378
656
  if (result.ok) {
379
657
  console.log(`✓ Daemon started`);
658
+ console.log(` Label: ${serviceIdentity.label}`);
380
659
  console.log(` Plist: ${plistPath}`);
381
660
  console.log(` Log: ${logPath}`);
382
- console.log(` Root: ${activationRoot}`);
661
+ console.log(` Root: ${serviceIdentity.requestedActivationRoot}`);
383
662
  }
384
663
  else {
385
664
  console.error(`✗ ${result.message}`);
@@ -388,11 +667,19 @@ export function daemonStart(activationRoot, json) {
388
667
  return result.ok ? 0 : 1;
389
668
  }
390
669
  export function daemonStop(activationRoot, json) {
391
- const plistPath = getPlistPath();
670
+ const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
671
+ const plistPath = serviceIdentity.plistPath;
392
672
  if (!existsSync(plistPath)) {
393
673
  const msg = "No daemon plist found. Daemon is not installed.";
394
674
  if (json) {
395
- console.log(JSON.stringify({ command: "daemon stop", ok: false, activationRoot, message: msg }, null, 2));
675
+ console.log(JSON.stringify({
676
+ command: "daemon stop",
677
+ ok: false,
678
+ activationRoot: serviceIdentity.requestedActivationRoot,
679
+ serviceLabel: serviceIdentity.label,
680
+ plistPath,
681
+ message: msg
682
+ }, null, 2));
396
683
  }
397
684
  else {
398
685
  console.log(msg);
@@ -412,7 +699,8 @@ export function daemonStop(activationRoot, json) {
412
699
  if (json) {
413
700
  console.log(JSON.stringify({
414
701
  command: "daemon stop",
415
- activationRoot,
702
+ activationRoot: serviceIdentity.requestedActivationRoot,
703
+ serviceLabel: serviceIdentity.label,
416
704
  ok: result.ok,
417
705
  plistPath,
418
706
  message: result.message,
@@ -421,6 +709,7 @@ export function daemonStop(activationRoot, json) {
421
709
  else {
422
710
  if (result.ok) {
423
711
  console.log(`✓ Daemon stopped and plist removed.`);
712
+ console.log(` Label: ${serviceIdentity.label}`);
424
713
  }
425
714
  else {
426
715
  console.error(`✗ ${result.message}`);
@@ -429,22 +718,26 @@ export function daemonStop(activationRoot, json) {
429
718
  return result.ok ? 0 : 1;
430
719
  }
431
720
  export function daemonStatus(activationRoot, json) {
432
- const plistPath = getPlistPath();
433
- const logPath = getLogPath();
721
+ const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
722
+ const plistPath = serviceIdentity.plistPath;
723
+ const logPath = serviceIdentity.logPath;
434
724
  const plistInstalled = existsSync(plistPath);
435
- const info = getLaunchctlInfo();
725
+ const info = getLaunchctlInfo(serviceIdentity.label);
436
726
  const lastLogLines = readLastLines(logPath, 5);
437
727
  const configuredActivationRoot = readDaemonActivationRoot(plistPath);
438
- const requestedActivationRoot = path.resolve(activationRoot);
728
+ const requestedActivationRoot = serviceIdentity.requestedActivationRoot;
439
729
  const watchStatePaths = getWatchStatePaths(requestedActivationRoot);
440
730
  const watchState = readWatchStateSummary(requestedActivationRoot);
441
- const matchesRequestedActivationRoot = configuredActivationRoot === null ? null : path.resolve(configuredActivationRoot) === requestedActivationRoot;
731
+ const matchesRequestedActivationRoot = configuredActivationRoot === null
732
+ ? null
733
+ : canonicalizeActivationRoot(configuredActivationRoot) === serviceIdentity.canonicalActivationRoot;
442
734
  if (json) {
443
735
  console.log(JSON.stringify({
444
736
  command: "daemon status",
445
737
  installed: plistInstalled,
446
738
  running: info.running,
447
739
  pid: info.pid,
740
+ serviceLabel: serviceIdentity.label,
448
741
  plistPath,
449
742
  logPath,
450
743
  activationRoot: requestedActivationRoot,
@@ -463,6 +756,7 @@ export function daemonStatus(activationRoot, json) {
463
756
  console.log(` PID: ${info.pid}`);
464
757
  }
465
758
  if (plistInstalled) {
759
+ console.log(` Label: ${serviceIdentity.label}`);
466
760
  console.log(` Plist: ${plistPath}`);
467
761
  }
468
762
  console.log(` Requested root: ${requestedActivationRoot}`);
@@ -535,11 +829,20 @@ export function daemonStatus(activationRoot, json) {
535
829
  return 0;
536
830
  }
537
831
  export function daemonLogs(activationRoot, json) {
538
- const logPath = getLogPath();
832
+ const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
833
+ const logPath = serviceIdentity.logPath;
539
834
  if (!existsSync(logPath)) {
540
835
  const msg = `No log file found at ${logPath}`;
541
836
  if (json) {
542
- console.log(JSON.stringify({ command: "daemon logs", ok: false, activationRoot, logPath, message: msg, lines: [] }, null, 2));
837
+ console.log(JSON.stringify({
838
+ command: "daemon logs",
839
+ ok: false,
840
+ activationRoot: serviceIdentity.requestedActivationRoot,
841
+ serviceLabel: serviceIdentity.label,
842
+ logPath,
843
+ message: msg,
844
+ lines: []
845
+ }, null, 2));
543
846
  }
544
847
  else {
545
848
  console.log(msg);
@@ -548,7 +851,14 @@ export function daemonLogs(activationRoot, json) {
548
851
  }
549
852
  const lines = readLastLines(logPath, 50);
550
853
  if (json) {
551
- console.log(JSON.stringify({ command: "daemon logs", ok: true, activationRoot, logPath, lines }, null, 2));
854
+ console.log(JSON.stringify({
855
+ command: "daemon logs",
856
+ ok: true,
857
+ activationRoot: serviceIdentity.requestedActivationRoot,
858
+ serviceLabel: serviceIdentity.label,
859
+ logPath,
860
+ lines
861
+ }, null, 2));
552
862
  }
553
863
  else {
554
864
  if (lines.length === 0) {
@@ -595,7 +905,7 @@ export function daemonHelp() {
595
905
  " start Generate a macOS launchd plist and start the daemon (runs openclawbrain watch).",
596
906
  " stop Stop the daemon and remove the launchd plist.",
597
907
  " status Show whether the daemon is running, its PID, and recent log lines.",
598
- " logs Show the last 50 lines of the daemon log (~/.openclawbrain/daemon.log).",
908
+ " logs Show the last 50 lines of the per-activation-root daemon log under ~/.openclawbrain/daemon/.",
599
909
  "",
600
910
  "Options:",
601
911
  " --activation-root <path> Explicit activation root for the wrapped watch daemon.",