@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.
- package/README.md +3 -1
- package/dist/extension/index.js +9 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/runtime-guard.js +6 -1
- package/dist/extension/runtime-guard.js.map +1 -1
- package/dist/src/cli.d.ts +18 -6
- package/dist/src/cli.js +1991 -293
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +42 -1
- package/dist/src/daemon.js +360 -50
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/index.d.ts +65 -1
- package/dist/src/index.js +627 -56
- package/dist/src/index.js.map +1 -1
- package/dist/src/learning-spine.d.ts +3 -1
- package/dist/src/learning-spine.js +1 -0
- package/dist/src/learning-spine.js.map +1 -1
- package/dist/src/local-session-passive-learning.js +6 -1
- package/dist/src/local-session-passive-learning.js.map +1 -1
- package/dist/src/openclaw-home-layout.d.ts +17 -0
- package/dist/src/openclaw-home-layout.js +182 -0
- package/dist/src/openclaw-home-layout.js.map +1 -0
- package/dist/src/provider-config.d.ts +36 -0
- package/dist/src/provider-config.js +181 -25
- package/dist/src/provider-config.js.map +1 -1
- package/dist/src/resolve-activation-root.d.ts +3 -3
- package/dist/src/resolve-activation-root.js +21 -26
- package/dist/src/resolve-activation-root.js.map +1 -1
- package/dist/src/semantic-metadata.d.ts +4 -0
- package/dist/src/semantic-metadata.js +41 -0
- package/dist/src/semantic-metadata.js.map +1 -0
- package/dist/src/session-store.js +16 -5
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/session-tail.d.ts +2 -0
- package/dist/src/session-tail.js +68 -16
- package/dist/src/session-tail.js.map +1 -1
- package/dist/src/shadow-extension-proof.js +4 -0
- package/dist/src/shadow-extension-proof.js.map +1 -1
- package/extension/index.ts +17 -0
- package/extension/runtime-guard.ts +7 -1
- package/package.json +7 -7
package/dist/src/daemon.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* macOS launchd daemon management for OpenClawBrain.
|
|
3
3
|
*
|
|
4
|
-
* Manages
|
|
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;
|
package/dist/src/daemon.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* macOS launchd daemon management for OpenClawBrain.
|
|
3
3
|
*
|
|
4
|
-
* Manages
|
|
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 {
|
|
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
|
|
17
|
-
const
|
|
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
|
|
30
|
-
|
|
33
|
+
function canonicalizeActivationRoot(activationRoot) {
|
|
34
|
+
const resolvedActivationRoot = path.resolve(activationRoot);
|
|
35
|
+
return existsSync(resolvedActivationRoot) ? safeRealpath(resolvedActivationRoot) : resolvedActivationRoot;
|
|
31
36
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function safeRealpath(filePath) {
|
|
50
75
|
try {
|
|
51
|
-
return
|
|
76
|
+
return realpathSync(filePath);
|
|
52
77
|
}
|
|
53
78
|
catch {
|
|
54
|
-
return
|
|
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
|
|
58
|
-
|
|
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>${
|
|
158
|
+
<string>${serviceIdentity.label}</string>
|
|
67
159
|
<key>ProgramArguments</key>
|
|
68
160
|
<array>
|
|
69
|
-
|
|
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>${
|
|
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(
|
|
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(
|
|
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
|
|
355
|
-
const
|
|
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(
|
|
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: ${
|
|
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
|
|
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({
|
|
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
|
|
433
|
-
const
|
|
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 =
|
|
728
|
+
const requestedActivationRoot = serviceIdentity.requestedActivationRoot;
|
|
439
729
|
const watchStatePaths = getWatchStatePaths(requestedActivationRoot);
|
|
440
730
|
const watchState = readWatchStateSummary(requestedActivationRoot);
|
|
441
|
-
const matchesRequestedActivationRoot = configuredActivationRoot === null
|
|
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
|
|
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({
|
|
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({
|
|
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
|
|
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.",
|