@wipcomputer/wip-ldm-os 0.4.60 → 0.4.62
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/SKILL.md +1 -1
- package/bin/ldm.js +467 -48
- package/catalog.json +2 -1
- package/package.json +1 -1
- package/scripts/ldm-backup.sh +16 -6
- package/shared/launchagents/ai.openclaw.gateway.plist +41 -0
- package/shared/launchagents/ai.openclaw.healthcheck.plist +28 -0
- package/shared/launchagents/ai.openclaw.ldm-backup.plist +4 -4
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ license: MIT
|
|
|
9
9
|
compatibility: Requires git, npm, node. Node.js 18+.
|
|
10
10
|
metadata:
|
|
11
11
|
display-name: "LDM OS"
|
|
12
|
-
version: "0.4.
|
|
12
|
+
version: "0.4.62"
|
|
13
13
|
homepage: "https://github.com/wipcomputer/wip-ldm-os"
|
|
14
14
|
author: "Parker Todd Brooks"
|
|
15
15
|
category: infrastructure
|
package/bin/ldm.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* ldm install Install/update all registered components
|
|
10
10
|
* ldm doctor Check health of all extensions
|
|
11
11
|
* ldm status Show LDM OS version and extension count
|
|
12
|
+
* ldm backup Run a full backup now
|
|
13
|
+
* ldm backup --dry-run Preview what would be backed up (with sizes)
|
|
14
|
+
* ldm backup --pin "x" Pin the latest backup so rotation skips it
|
|
12
15
|
* ldm sessions List active sessions
|
|
13
16
|
* ldm msg send <to> <b> Send a message to a session
|
|
14
17
|
* ldm msg list List pending messages
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
* ldm --version Show version
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync } from 'node:fs';
|
|
21
24
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
22
25
|
import { execSync } from 'node:child_process';
|
|
23
26
|
import { fileURLToPath } from 'node:url';
|
|
@@ -112,6 +115,26 @@ function acquireInstallLock() {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
const args = process.argv.slice(2);
|
|
118
|
+
|
|
119
|
+
// Normalize dry-run flag variants before parsing (#239)
|
|
120
|
+
// --dryrun -> --dry-run
|
|
121
|
+
// --dry run (two words) -> --dry-run (consume the stray "run" so it doesn't
|
|
122
|
+
// become a package target and install random npm packages)
|
|
123
|
+
for (let i = 0; i < args.length; i++) {
|
|
124
|
+
if (args[i] === '--dryrun') {
|
|
125
|
+
args[i] = '--dry-run';
|
|
126
|
+
} else if (args[i] === '--dry') {
|
|
127
|
+
if (args[i + 1] === 'run') {
|
|
128
|
+
args[i] = '--dry-run';
|
|
129
|
+
args.splice(i + 1, 1);
|
|
130
|
+
} else {
|
|
131
|
+
// Bare --dry with no "run" after it. Treat as --dry-run since there
|
|
132
|
+
// is no other --dry flag and the intent is obvious.
|
|
133
|
+
args[i] = '--dry-run';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
const command = args[0];
|
|
116
139
|
const DRY_RUN = args.includes('--dry-run');
|
|
117
140
|
const JSON_OUTPUT = args.includes('--json');
|
|
@@ -152,6 +175,60 @@ function checkCliVersion() {
|
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
177
|
|
|
178
|
+
// ── Dead backup trigger cleanup (#207) ──
|
|
179
|
+
// Three backup systems were competing. Only ai.openclaw.ldm-backup (3am) works.
|
|
180
|
+
// This removes: broken cron entry (LDMDevTools.app), old com.wipcomputer.daily-backup.
|
|
181
|
+
|
|
182
|
+
function cleanDeadBackupTriggers() {
|
|
183
|
+
let cleaned = 0;
|
|
184
|
+
|
|
185
|
+
// 1. Remove broken cron entries referencing LDMDevTools.app
|
|
186
|
+
// Matches both "LDMDevTools.app" and "LDM Dev Tools.app" (old naming)
|
|
187
|
+
try {
|
|
188
|
+
const crontab = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
189
|
+
const lines = crontab.split('\n');
|
|
190
|
+
const filtered = lines.filter(line => {
|
|
191
|
+
const lower = line.toLowerCase();
|
|
192
|
+
// Remove any line (active or commented) that references LDMDevTools
|
|
193
|
+
if (lower.includes('ldmdevtools.app') || lower.includes('ldm dev tools.app')) {
|
|
194
|
+
cleaned++;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Remove orphaned descriptive comment for the old backup verification cron
|
|
198
|
+
if (line.trim() === '# Verify daily backup ran - 00:30 PST') return false;
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
if (cleaned > 0) {
|
|
202
|
+
// Write filtered crontab via temp file (avoids shell escaping issues)
|
|
203
|
+
const tmpCron = join(LDM_TMP, 'crontab.tmp');
|
|
204
|
+
mkdirSync(LDM_TMP, { recursive: true });
|
|
205
|
+
writeFileSync(tmpCron, filtered.join('\n'));
|
|
206
|
+
execSync(`crontab "${tmpCron}"`, { stdio: 'pipe' });
|
|
207
|
+
try { unlinkSync(tmpCron); } catch {}
|
|
208
|
+
console.log(` + Removed ${cleaned} dead cron entry(s) (LDMDevTools.app)`);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// No crontab or crontab command failed. Not critical.
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 2. Unload and disable com.wipcomputer.daily-backup LaunchAgent
|
|
215
|
+
const oldPlist = join(HOME, 'Library', 'LaunchAgents', 'com.wipcomputer.daily-backup.plist');
|
|
216
|
+
const disabledPlist = oldPlist + '.disabled';
|
|
217
|
+
if (existsSync(oldPlist)) {
|
|
218
|
+
try { execSync(`launchctl unload "${oldPlist}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
219
|
+
try {
|
|
220
|
+
renameSync(oldPlist, disabledPlist);
|
|
221
|
+
} catch {
|
|
222
|
+
// If rename fails, just try to remove it
|
|
223
|
+
try { unlinkSync(oldPlist); } catch {}
|
|
224
|
+
}
|
|
225
|
+
console.log(' + Disabled dead LaunchAgent: com.wipcomputer.daily-backup');
|
|
226
|
+
cleaned++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return cleaned;
|
|
230
|
+
}
|
|
231
|
+
|
|
155
232
|
// ── Stale hook cleanup (#30) ──
|
|
156
233
|
|
|
157
234
|
function cleanStaleHooks() {
|
|
@@ -229,22 +306,75 @@ function loadCatalog() {
|
|
|
229
306
|
|
|
230
307
|
function findInCatalog(id) {
|
|
231
308
|
const q = id.toLowerCase();
|
|
309
|
+
// Strip org/ prefix for matching (e.g. "wipcomputer/openclaw-tavily" -> "openclaw-tavily")
|
|
310
|
+
const qBase = q.includes('/') ? q.split('/').pop() : q;
|
|
232
311
|
const catalog = loadCatalog();
|
|
233
312
|
// Exact id match
|
|
234
|
-
const exact = catalog.find(c => c.id === id);
|
|
313
|
+
const exact = catalog.find(c => c.id === id || c.id === qBase);
|
|
235
314
|
if (exact) return exact;
|
|
315
|
+
// Exact repo match (e.g. "wipcomputer/openclaw-tavily" matches repo field directly)
|
|
316
|
+
const byRepo = catalog.find(c => c.repo && c.repo.toLowerCase() === q);
|
|
317
|
+
if (byRepo) return byRepo;
|
|
236
318
|
// Partial id match (e.g. "xai-grok" matches "wip-xai-grok")
|
|
237
|
-
|
|
319
|
+
// Check both directions but require word-boundary alignment (hyphen or start of string)
|
|
320
|
+
// to prevent "openclaw" matching "openclaw-tavily"
|
|
321
|
+
const partial = catalog.find(c => {
|
|
322
|
+
const cid = c.id.toLowerCase();
|
|
323
|
+
if (cid === qBase) return false;
|
|
324
|
+
// Query is suffix of catalog id: "xai-grok" matches "wip-xai-grok"
|
|
325
|
+
if (cid.endsWith(qBase) && (cid.length === qBase.length || cid[cid.length - qBase.length - 1] === '-')) return true;
|
|
326
|
+
// Catalog id is suffix of query: "wip-xai-grok" matches when query is "wip-xai-grok-private"
|
|
327
|
+
if (qBase.endsWith(cid) && (qBase.length === cid.length || qBase[qBase.length - cid.length - 1] === '-')) return true;
|
|
328
|
+
return false;
|
|
329
|
+
});
|
|
238
330
|
if (partial) return partial;
|
|
239
331
|
// Name match (case-insensitive, e.g. "xAI Grok")
|
|
240
332
|
const byName = catalog.find(c => c.name && c.name.toLowerCase() === q);
|
|
241
333
|
if (byName) return byName;
|
|
242
334
|
// registryMatches match
|
|
243
|
-
const byRegistry = catalog.find(c => (c.registryMatches || []).some(m => m.toLowerCase() === q));
|
|
335
|
+
const byRegistry = catalog.find(c => (c.registryMatches || []).some(m => m.toLowerCase() === q || m.toLowerCase() === qBase));
|
|
244
336
|
if (byRegistry) return byRegistry;
|
|
245
337
|
return null;
|
|
246
338
|
}
|
|
247
339
|
|
|
340
|
+
// Install a single catalog component directly (no subprocess).
|
|
341
|
+
// Replaces the old execSync('ldm install ${c.repo}') which spawned
|
|
342
|
+
// a full installer process for each component.
|
|
343
|
+
async function installCatalogComponent(c) {
|
|
344
|
+
const { installFromPath } = await import('../lib/deploy.mjs');
|
|
345
|
+
const repoTarget = c.repo;
|
|
346
|
+
const repoName = basename(repoTarget);
|
|
347
|
+
const repoPath = join(LDM_TMP, repoName);
|
|
348
|
+
const httpsUrl = `https://github.com/${repoTarget}.git`;
|
|
349
|
+
const sshUrl = `git@github.com:${repoTarget}.git`;
|
|
350
|
+
|
|
351
|
+
mkdirSync(LDM_TMP, { recursive: true });
|
|
352
|
+
console.log(` Cloning ${repoTarget}...`);
|
|
353
|
+
try {
|
|
354
|
+
if (existsSync(repoPath)) {
|
|
355
|
+
execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
execSync(`git clone --depth 1 "${httpsUrl}" "${repoPath}"`, { stdio: 'pipe' });
|
|
359
|
+
} catch {
|
|
360
|
+
console.log(` HTTPS failed. Trying SSH...`);
|
|
361
|
+
if (existsSync(repoPath)) execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
|
|
362
|
+
execSync(`git clone --depth 1 "${sshUrl}" "${repoPath}"`, { stdio: 'pipe' });
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error(` x Clone failed: ${e.message}`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await installFromPath(repoPath);
|
|
370
|
+
|
|
371
|
+
// Clean up staging clone
|
|
372
|
+
if (repoPath.startsWith(LDM_TMP)) {
|
|
373
|
+
try { execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' }); } catch {}
|
|
374
|
+
}
|
|
375
|
+
console.log(` ✓ Installed ${c.name}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
248
378
|
// ── ldm init ──
|
|
249
379
|
|
|
250
380
|
async function cmdInit() {
|
|
@@ -595,29 +725,57 @@ async function cmdInit() {
|
|
|
595
725
|
}
|
|
596
726
|
|
|
597
727
|
// Deploy LaunchAgents to ~/Library/LaunchAgents/
|
|
728
|
+
// Templates use {{HOME}} and {{OPENCLAW_GATEWAY_TOKEN}} placeholders, replaced at deploy time.
|
|
598
729
|
const launchSrc = join(__dirname, '..', 'shared', 'launchagents');
|
|
599
730
|
const launchDest = join(HOME, 'Library', 'LaunchAgents');
|
|
600
731
|
if (existsSync(launchSrc) && existsSync(launchDest)) {
|
|
732
|
+
// Ensure log directory exists for LaunchAgent output
|
|
733
|
+
mkdirSync(join(LDM_ROOT, 'logs'), { recursive: true });
|
|
734
|
+
|
|
735
|
+
// Read gateway token from openclaw.json (if it exists)
|
|
736
|
+
let gatewayToken = '';
|
|
737
|
+
try {
|
|
738
|
+
const ocConfig = JSON.parse(readFileSync(join(HOME, '.openclaw', 'openclaw.json'), 'utf8'));
|
|
739
|
+
gatewayToken = ocConfig?.gateway?.auth?.token || '';
|
|
740
|
+
} catch {}
|
|
741
|
+
|
|
601
742
|
let launchCount = 0;
|
|
743
|
+
let launchUpToDate = 0;
|
|
602
744
|
for (const file of readdirSync(launchSrc)) {
|
|
603
745
|
if (!file.endsWith('.plist')) continue;
|
|
604
746
|
const src = join(launchSrc, file);
|
|
605
747
|
const dest = join(launchDest, file);
|
|
606
|
-
|
|
748
|
+
// Replace template placeholders with actual values
|
|
749
|
+
let srcContent = readFileSync(src, 'utf8');
|
|
750
|
+
srcContent = srcContent.replace(/\{\{HOME\}\}/g, HOME);
|
|
751
|
+
srcContent = srcContent.replace(/\{\{OPENCLAW_GATEWAY_TOKEN\}\}/g, gatewayToken);
|
|
607
752
|
const destContent = existsSync(dest) ? readFileSync(dest, 'utf8') : '';
|
|
608
753
|
if (srcContent !== destContent) {
|
|
609
|
-
// Unload old
|
|
754
|
+
// Unload old agent before overwriting
|
|
610
755
|
try { execSync(`launchctl unload "${dest}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
611
|
-
|
|
756
|
+
writeFileSync(dest, srcContent);
|
|
612
757
|
try { execSync(`launchctl load "${dest}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
758
|
+
const label = file.replace('.plist', '');
|
|
759
|
+
console.log(` + ${label} deployed and loaded`);
|
|
760
|
+
installLog(`LaunchAgent deployed: ${file}`);
|
|
613
761
|
launchCount++;
|
|
762
|
+
} else {
|
|
763
|
+
launchUpToDate++;
|
|
614
764
|
}
|
|
615
765
|
}
|
|
616
766
|
if (launchCount > 0) {
|
|
617
767
|
console.log(` + ${launchCount} LaunchAgent(s) deployed to ~/Library/LaunchAgents/`);
|
|
618
768
|
}
|
|
769
|
+
if (launchUpToDate > 0) {
|
|
770
|
+
console.log(` - ${launchUpToDate} LaunchAgent(s) already up to date`);
|
|
771
|
+
}
|
|
619
772
|
}
|
|
620
773
|
|
|
774
|
+
// Clean up dead backup triggers (#207)
|
|
775
|
+
// Bug: three backup systems were competing. Only ai.openclaw.ldm-backup (3am) works.
|
|
776
|
+
// The old cron entry (LDMDevTools.app) and com.wipcomputer.daily-backup are dead.
|
|
777
|
+
cleanDeadBackupTriggers();
|
|
778
|
+
|
|
621
779
|
console.log('');
|
|
622
780
|
console.log(` LDM OS v${PKG_VERSION} initialized at ${LDM_ROOT}`);
|
|
623
781
|
console.log('');
|
|
@@ -669,7 +827,7 @@ async function showCatalogPicker() {
|
|
|
669
827
|
for (const c of recommended) {
|
|
670
828
|
console.log(` Installing ${c.name}...`);
|
|
671
829
|
try {
|
|
672
|
-
|
|
830
|
+
await installCatalogComponent(c);
|
|
673
831
|
} catch (e) {
|
|
674
832
|
console.error(` x Failed to install ${c.name}: ${e.message}`);
|
|
675
833
|
}
|
|
@@ -705,7 +863,7 @@ async function showCatalogPicker() {
|
|
|
705
863
|
console.log('');
|
|
706
864
|
console.log(` Installing ${c.name}...`);
|
|
707
865
|
try {
|
|
708
|
-
|
|
866
|
+
await installCatalogComponent(c);
|
|
709
867
|
} catch (e) {
|
|
710
868
|
console.error(` x Failed to install ${c.name}: ${e.message}`);
|
|
711
869
|
}
|
|
@@ -988,54 +1146,53 @@ async function cmdInstallCatalog() {
|
|
|
988
1146
|
const state = detectSystemState();
|
|
989
1147
|
const reconciled = reconcileState(state);
|
|
990
1148
|
|
|
991
|
-
// Show the real system state
|
|
992
|
-
console.log(formatReconciliation(reconciled));
|
|
993
|
-
|
|
994
1149
|
// Check catalog: use registryMatches + cliMatches to detect what's really installed
|
|
995
1150
|
const registry = readJSON(REGISTRY_PATH);
|
|
996
|
-
const registeredNames = Object.keys(registry?.extensions || {});
|
|
997
|
-
const reconciledNames = Object.keys(reconciled);
|
|
998
1151
|
const components = loadCatalog();
|
|
999
1152
|
|
|
1000
|
-
function isCatalogItemInstalled(c) {
|
|
1001
|
-
// Direct ID match
|
|
1002
|
-
if (registeredNames.includes(c.id) || reconciled[c.id]) return true;
|
|
1003
|
-
// Check registryMatches (aliases)
|
|
1004
|
-
const matches = c.registryMatches || [];
|
|
1005
|
-
if (matches.some(m => registeredNames.includes(m) || reconciled[m])) return true;
|
|
1006
|
-
// Check CLI binaries
|
|
1007
|
-
const cliMatches = c.cliMatches || [];
|
|
1008
|
-
if (cliMatches.some(b => state.cliBinaries[b])) return true;
|
|
1009
|
-
return false;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
const available = components.filter(c =>
|
|
1013
|
-
c.status !== 'coming-soon' && !isCatalogItemInstalled(c)
|
|
1014
|
-
);
|
|
1015
|
-
|
|
1016
|
-
if (available.length > 0) {
|
|
1017
|
-
console.log(' Available in catalog (not yet installed):');
|
|
1018
|
-
for (const c of available) {
|
|
1019
|
-
console.log(` [ ] ${c.name} ... ${c.description}`);
|
|
1020
|
-
}
|
|
1021
|
-
console.log('');
|
|
1022
|
-
} else {
|
|
1023
|
-
console.log(' All catalog components are installed.');
|
|
1024
|
-
console.log('');
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
1153
|
// Clean ghost entries from registry (#134, #135)
|
|
1154
|
+
// Run BEFORE system state display so ghosts don't appear in the installed list.
|
|
1028
1155
|
if (registry?.extensions) {
|
|
1029
1156
|
const names = Object.keys(registry.extensions);
|
|
1030
1157
|
let cleaned = 0;
|
|
1031
1158
|
for (const name of names) {
|
|
1032
1159
|
// Remove -private duplicates (e.g. wip-xai-grok-private when wip-xai-grok exists)
|
|
1160
|
+
// Only public versions should be installed as extensions. Private repos are for development.
|
|
1033
1161
|
const publicName = name.replace(/-private$/, '');
|
|
1034
1162
|
if (name !== publicName && registry.extensions[publicName]) {
|
|
1035
1163
|
delete registry.extensions[name];
|
|
1164
|
+
if (!DRY_RUN) {
|
|
1165
|
+
for (const base of [LDM_EXTENSIONS, join(HOME, '.openclaw', 'extensions')]) {
|
|
1166
|
+
const ghostDir = join(base, name);
|
|
1167
|
+
if (existsSync(ghostDir)) {
|
|
1168
|
+
const trashDir = join(LDM_TRASH, `${name}.ghost-${Date.now()}`);
|
|
1169
|
+
try { execSync(`mv "${ghostDir}" "${trashDir}"`, { stdio: 'pipe' }); } catch {}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1036
1173
|
cleaned++;
|
|
1037
1174
|
continue;
|
|
1038
1175
|
}
|
|
1176
|
+
// Fix -private path mismatch: registry says "wip-xai-x" but paths point to "wip-xai-x-private".
|
|
1177
|
+
// This happens when the installer cloned a public repo whose package.json had a -private name.
|
|
1178
|
+
// Rename the directories to match the public registry name.
|
|
1179
|
+
const ext = registry.extensions[name];
|
|
1180
|
+
if (ext && !name.endsWith('-private')) {
|
|
1181
|
+
const privateName = name + '-private';
|
|
1182
|
+
let pathFixed = false;
|
|
1183
|
+
for (const [pathKey, base] of [['ldmPath', LDM_EXTENSIONS], ['ocPath', join(HOME, '.openclaw', 'extensions')]]) {
|
|
1184
|
+
if (ext[pathKey] && ext[pathKey].endsWith(privateName)) {
|
|
1185
|
+
const privateDir = join(base, privateName);
|
|
1186
|
+
const publicDir = join(base, name);
|
|
1187
|
+
if (!DRY_RUN && existsSync(privateDir) && !existsSync(publicDir)) {
|
|
1188
|
+
try { execSync(`mv "${privateDir}" "${publicDir}"`, { stdio: 'pipe' }); } catch {}
|
|
1189
|
+
}
|
|
1190
|
+
ext[pathKey] = publicDir;
|
|
1191
|
+
pathFixed = true;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (pathFixed) cleaned++;
|
|
1195
|
+
}
|
|
1039
1196
|
// Rename ldm-install- prefixed entries to clean names (#141)
|
|
1040
1197
|
if (name.startsWith('ldm-install-')) {
|
|
1041
1198
|
const cleanName = name.replace(/^ldm-install-/, '');
|
|
@@ -1074,6 +1231,122 @@ async function cmdInstallCatalog() {
|
|
|
1074
1231
|
}
|
|
1075
1232
|
}
|
|
1076
1233
|
|
|
1234
|
+
// Clean orphaned -private directories (#132)
|
|
1235
|
+
// Pre-v0.4.30 installs could create -private extension dirs that linger
|
|
1236
|
+
// even after registry entries are cleaned. If the public name is in the
|
|
1237
|
+
// registry, rename the directory (or trash it if public dir already exists).
|
|
1238
|
+
try {
|
|
1239
|
+
const extDirs = readdirSync(LDM_EXTENSIONS, { withFileTypes: true })
|
|
1240
|
+
.filter(d => d.isDirectory() && d.name.endsWith('-private'));
|
|
1241
|
+
for (const d of extDirs) {
|
|
1242
|
+
const publicName = d.name.replace(/-private$/, '');
|
|
1243
|
+
// Only act if the public name is known (registry entry or catalog match)
|
|
1244
|
+
const inRegistry = !!registry?.extensions?.[publicName];
|
|
1245
|
+
const inCatalog = components.some(c =>
|
|
1246
|
+
c.id === publicName || (c.registryMatches || []).includes(publicName)
|
|
1247
|
+
);
|
|
1248
|
+
if (!inRegistry && !inCatalog) continue;
|
|
1249
|
+
|
|
1250
|
+
const ghostDir = join(LDM_EXTENSIONS, d.name);
|
|
1251
|
+
const publicDir = join(LDM_EXTENSIONS, publicName);
|
|
1252
|
+
|
|
1253
|
+
if (!DRY_RUN) {
|
|
1254
|
+
if (!existsSync(publicDir)) {
|
|
1255
|
+
// No public dir yet. Rename -private to public name.
|
|
1256
|
+
console.log(` Renaming ghost: ${d.name} -> ${publicName}`);
|
|
1257
|
+
try { execSync(`mv "${ghostDir}" "${publicDir}"`, { stdio: 'pipe' }); } catch {}
|
|
1258
|
+
} else {
|
|
1259
|
+
// Public dir exists. Trash the ghost.
|
|
1260
|
+
console.log(` Trashing ghost: ${d.name} (public "${publicName}" exists)`);
|
|
1261
|
+
const trashDir = join(LDM_EXTENSIONS, '_trash', d.name + '--' + new Date().toISOString().slice(0, 10));
|
|
1262
|
+
try {
|
|
1263
|
+
mkdirSync(join(LDM_EXTENSIONS, '_trash'), { recursive: true });
|
|
1264
|
+
execSync(`mv "${ghostDir}" "${trashDir}"`, { stdio: 'pipe' });
|
|
1265
|
+
} catch {}
|
|
1266
|
+
}
|
|
1267
|
+
// Fix registry paths that still reference the -private name
|
|
1268
|
+
if (registry?.extensions?.[publicName]) {
|
|
1269
|
+
const entry = registry.extensions[publicName];
|
|
1270
|
+
if (entry.ldmPath && entry.ldmPath.includes(d.name)) {
|
|
1271
|
+
entry.ldmPath = entry.ldmPath.replace(d.name, publicName);
|
|
1272
|
+
}
|
|
1273
|
+
if (entry.ocPath && entry.ocPath.includes(d.name)) {
|
|
1274
|
+
entry.ocPath = entry.ocPath.replace(d.name, publicName);
|
|
1275
|
+
}
|
|
1276
|
+
writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
|
1277
|
+
}
|
|
1278
|
+
} else {
|
|
1279
|
+
if (!existsSync(publicDir)) {
|
|
1280
|
+
console.log(` Would rename ghost: ${d.name} -> ${publicName}`);
|
|
1281
|
+
} else {
|
|
1282
|
+
console.log(` Would trash ghost: ${d.name} (public "${publicName}" exists)`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
// Remove from reconciled so it doesn't appear in installed list or update checks
|
|
1286
|
+
delete reconciled[d.name];
|
|
1287
|
+
}
|
|
1288
|
+
// Same for OC extensions
|
|
1289
|
+
const ocExtDir = join(HOME, '.openclaw', 'extensions');
|
|
1290
|
+
if (existsSync(ocExtDir)) {
|
|
1291
|
+
const ocDirs = readdirSync(ocExtDir, { withFileTypes: true })
|
|
1292
|
+
.filter(d => d.isDirectory() && d.name.endsWith('-private'));
|
|
1293
|
+
for (const d of ocDirs) {
|
|
1294
|
+
const publicName = d.name.replace(/-private$/, '');
|
|
1295
|
+
const publicDir = join(ocExtDir, publicName);
|
|
1296
|
+
const ghostDir = join(ocExtDir, d.name);
|
|
1297
|
+
const inCatalog = components.some(c =>
|
|
1298
|
+
c.id === publicName || (c.registryMatches || []).includes(publicName)
|
|
1299
|
+
);
|
|
1300
|
+
if (!inCatalog) continue;
|
|
1301
|
+
if (!DRY_RUN) {
|
|
1302
|
+
if (!existsSync(publicDir)) {
|
|
1303
|
+
try { execSync(`mv "${ghostDir}" "${publicDir}"`, { stdio: 'pipe' }); } catch {}
|
|
1304
|
+
} else {
|
|
1305
|
+
const trashDir = join(ocExtDir, '_trash', d.name + '--' + new Date().toISOString().slice(0, 10));
|
|
1306
|
+
try {
|
|
1307
|
+
mkdirSync(join(ocExtDir, '_trash'), { recursive: true });
|
|
1308
|
+
execSync(`mv "${ghostDir}" "${trashDir}"`, { stdio: 'pipe' });
|
|
1309
|
+
} catch {}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
delete reconciled[d.name];
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
} catch {}
|
|
1316
|
+
|
|
1317
|
+
// Show the system state (after ghost cleanup, so ghosts don't appear)
|
|
1318
|
+
console.log(formatReconciliation(reconciled));
|
|
1319
|
+
|
|
1320
|
+
const registeredNames = Object.keys(registry?.extensions || {});
|
|
1321
|
+
const reconciledNames = Object.keys(reconciled);
|
|
1322
|
+
|
|
1323
|
+
function isCatalogItemInstalled(c) {
|
|
1324
|
+
// Direct ID match
|
|
1325
|
+
if (registeredNames.includes(c.id) || reconciled[c.id]) return true;
|
|
1326
|
+
// Check registryMatches (aliases)
|
|
1327
|
+
const matches = c.registryMatches || [];
|
|
1328
|
+
if (matches.some(m => registeredNames.includes(m) || reconciled[m])) return true;
|
|
1329
|
+
// Check CLI binaries
|
|
1330
|
+
const cliMatches = c.cliMatches || [];
|
|
1331
|
+
if (cliMatches.some(b => state.cliBinaries[b])) return true;
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const available = components.filter(c =>
|
|
1336
|
+
c.status !== 'coming-soon' && !isCatalogItemInstalled(c)
|
|
1337
|
+
);
|
|
1338
|
+
|
|
1339
|
+
if (available.length > 0) {
|
|
1340
|
+
console.log(' Available in catalog (not yet installed):');
|
|
1341
|
+
for (const c of available) {
|
|
1342
|
+
console.log(` [ ] ${c.name} ... ${c.description}`);
|
|
1343
|
+
}
|
|
1344
|
+
console.log('');
|
|
1345
|
+
} else {
|
|
1346
|
+
console.log(' All catalog components are installed.');
|
|
1347
|
+
console.log('');
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1077
1350
|
// Build the update plan: check ALL installed extensions against npm (#55)
|
|
1078
1351
|
const npmUpdates = [];
|
|
1079
1352
|
|
|
@@ -1196,9 +1469,12 @@ async function cmdInstallCatalog() {
|
|
|
1196
1469
|
encoding: 'utf8', timeout: 10000,
|
|
1197
1470
|
}).trim();
|
|
1198
1471
|
if (latest && latest !== currentVersion) {
|
|
1199
|
-
// Remove any sub-tool entries that
|
|
1472
|
+
// Remove any sub-tool entries that belong to this parent.
|
|
1473
|
+
// Match by name in registryMatches (sub-tools have their own npm names,
|
|
1474
|
+
// not the parent's, so catalogNpm comparison doesn't work).
|
|
1475
|
+
const parentMatches = new Set(comp.registryMatches || []);
|
|
1200
1476
|
for (let i = npmUpdates.length - 1; i >= 0; i--) {
|
|
1201
|
-
if (npmUpdates[i].
|
|
1477
|
+
if (!npmUpdates[i].isCLI && parentMatches.has(npmUpdates[i].name)) {
|
|
1202
1478
|
npmUpdates.splice(i, 1);
|
|
1203
1479
|
}
|
|
1204
1480
|
}
|
|
@@ -1729,6 +2005,69 @@ async function cmdDoctor() {
|
|
|
1729
2005
|
console.log(` + CLI binaries: ${Object.keys(state.cliBinaries).join(', ')}`);
|
|
1730
2006
|
}
|
|
1731
2007
|
|
|
2008
|
+
// 8. LaunchAgents health check
|
|
2009
|
+
const managedAgents = [
|
|
2010
|
+
'ai.openclaw.ldm-backup',
|
|
2011
|
+
'ai.openclaw.healthcheck',
|
|
2012
|
+
'ai.openclaw.gateway',
|
|
2013
|
+
];
|
|
2014
|
+
const launchAgentsDir = join(HOME, 'Library', 'LaunchAgents');
|
|
2015
|
+
const launchAgentsSrc = join(__dirname, '..', 'shared', 'launchagents');
|
|
2016
|
+
|
|
2017
|
+
// Read gateway token for template comparison
|
|
2018
|
+
let doctorGatewayToken = '';
|
|
2019
|
+
try {
|
|
2020
|
+
const ocConfig = JSON.parse(readFileSync(join(HOME, '.openclaw', 'openclaw.json'), 'utf8'));
|
|
2021
|
+
doctorGatewayToken = ocConfig?.gateway?.auth?.token || '';
|
|
2022
|
+
} catch {}
|
|
2023
|
+
|
|
2024
|
+
let launchOk = 0;
|
|
2025
|
+
let launchIssues = 0;
|
|
2026
|
+
for (const label of managedAgents) {
|
|
2027
|
+
const plistFile = `${label}.plist`;
|
|
2028
|
+
const deployedPath = join(launchAgentsDir, plistFile);
|
|
2029
|
+
const srcPath = join(launchAgentsSrc, plistFile);
|
|
2030
|
+
|
|
2031
|
+
if (!existsSync(deployedPath)) {
|
|
2032
|
+
console.log(` x LaunchAgent ${label}: plist missing from ~/Library/LaunchAgents/`);
|
|
2033
|
+
launchIssues++;
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Check if deployed plist matches source template (after placeholder substitution)
|
|
2038
|
+
if (existsSync(srcPath)) {
|
|
2039
|
+
let srcContent = readFileSync(srcPath, 'utf8');
|
|
2040
|
+
srcContent = srcContent.replace(/\{\{HOME\}\}/g, HOME);
|
|
2041
|
+
srcContent = srcContent.replace(/\{\{OPENCLAW_GATEWAY_TOKEN\}\}/g, doctorGatewayToken);
|
|
2042
|
+
const deployedContent = readFileSync(deployedPath, 'utf8');
|
|
2043
|
+
if (srcContent !== deployedContent) {
|
|
2044
|
+
console.log(` ! LaunchAgent ${label}: plist out of date (run: ldm install)`);
|
|
2045
|
+
launchIssues++;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Check if loaded via launchctl
|
|
2051
|
+
try {
|
|
2052
|
+
const result = execSync(`launchctl list 2>/dev/null | grep "${label}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
2053
|
+
if (result.trim()) {
|
|
2054
|
+
launchOk++;
|
|
2055
|
+
} else {
|
|
2056
|
+
console.log(` ! LaunchAgent ${label}: plist exists but not loaded`);
|
|
2057
|
+
launchIssues++;
|
|
2058
|
+
}
|
|
2059
|
+
} catch {
|
|
2060
|
+
console.log(` ! LaunchAgent ${label}: plist exists but not loaded`);
|
|
2061
|
+
launchIssues++;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
if (launchOk > 0) {
|
|
2065
|
+
console.log(` + LaunchAgents: ${launchOk}/${managedAgents.length} loaded`);
|
|
2066
|
+
}
|
|
2067
|
+
if (launchIssues > 0) {
|
|
2068
|
+
issues += launchIssues;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1732
2071
|
console.log('');
|
|
1733
2072
|
if (issues === 0) {
|
|
1734
2073
|
console.log(' All checks passed.');
|
|
@@ -1829,16 +2168,29 @@ async function cmdBackup() {
|
|
|
1829
2168
|
const backupArgs = [];
|
|
1830
2169
|
if (DRY_RUN) backupArgs.push('--dry-run');
|
|
1831
2170
|
|
|
2171
|
+
// --full is explicit but currently all backups are full (incrementals are Phase 2)
|
|
2172
|
+
// Accept it as a no-op so the command reads naturally: ldm backup --full
|
|
2173
|
+
const FULL_FLAG = args.includes('--full');
|
|
2174
|
+
|
|
2175
|
+
// --keep N: pass through to backup script
|
|
2176
|
+
const keepIndex = args.indexOf('--keep');
|
|
2177
|
+
if (keepIndex !== -1 && args[keepIndex + 1]) {
|
|
2178
|
+
backupArgs.push('--keep', args[keepIndex + 1]);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
1832
2181
|
// --pin: mark the latest backup to skip rotation
|
|
1833
2182
|
const pinIndex = args.indexOf('--pin');
|
|
1834
2183
|
if (pinIndex !== -1) {
|
|
1835
2184
|
const reason = args[pinIndex + 1] || 'pinned';
|
|
1836
2185
|
// Find latest backup dir
|
|
1837
2186
|
const backupRoot = join(LDM_ROOT, 'backups');
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2187
|
+
let dirs = [];
|
|
2188
|
+
try {
|
|
2189
|
+
dirs = readdirSync(backupRoot)
|
|
2190
|
+
.filter(d => d.match(/^20\d\d-\d\d-\d\d--/))
|
|
2191
|
+
.sort()
|
|
2192
|
+
.reverse();
|
|
2193
|
+
} catch {}
|
|
1842
2194
|
if (dirs.length === 0) {
|
|
1843
2195
|
console.error(' x No backups found to pin.');
|
|
1844
2196
|
process.exit(1);
|
|
@@ -1851,7 +2203,70 @@ async function cmdBackup() {
|
|
|
1851
2203
|
return;
|
|
1852
2204
|
}
|
|
1853
2205
|
|
|
1854
|
-
|
|
2206
|
+
// --unpin: remove .pinned marker from the latest (or specified) backup
|
|
2207
|
+
const unpinIndex = args.indexOf('--unpin');
|
|
2208
|
+
if (unpinIndex !== -1) {
|
|
2209
|
+
const backupRoot = join(LDM_ROOT, 'backups');
|
|
2210
|
+
let dirs = [];
|
|
2211
|
+
try {
|
|
2212
|
+
dirs = readdirSync(backupRoot)
|
|
2213
|
+
.filter(d => d.match(/^20\d\d-\d\d-\d\d--/))
|
|
2214
|
+
.sort()
|
|
2215
|
+
.reverse();
|
|
2216
|
+
} catch {}
|
|
2217
|
+
// Find first pinned backup
|
|
2218
|
+
let unpinned = false;
|
|
2219
|
+
for (const d of dirs) {
|
|
2220
|
+
const pinFile = join(backupRoot, d, '.pinned');
|
|
2221
|
+
if (existsSync(pinFile)) {
|
|
2222
|
+
unlinkSync(pinFile);
|
|
2223
|
+
console.log(` - Unpinned backup ${d}`);
|
|
2224
|
+
unpinned = true;
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (!unpinned) {
|
|
2229
|
+
console.log(' No pinned backups found.');
|
|
2230
|
+
}
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// --list: show existing backups with pinned status
|
|
2235
|
+
const LIST_FLAG = args.includes('--list');
|
|
2236
|
+
if (LIST_FLAG) {
|
|
2237
|
+
const backupRoot = join(LDM_ROOT, 'backups');
|
|
2238
|
+
let dirs = [];
|
|
2239
|
+
try {
|
|
2240
|
+
dirs = readdirSync(backupRoot)
|
|
2241
|
+
.filter(d => d.match(/^20\d\d-\d\d-\d\d--/))
|
|
2242
|
+
.sort()
|
|
2243
|
+
.reverse();
|
|
2244
|
+
} catch {}
|
|
2245
|
+
if (dirs.length === 0) {
|
|
2246
|
+
console.log(' No backups found.');
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
console.log('');
|
|
2250
|
+
console.log(' Backups:');
|
|
2251
|
+
for (const d of dirs) {
|
|
2252
|
+
const pinFile = join(backupRoot, d, '.pinned');
|
|
2253
|
+
const pinned = existsSync(pinFile);
|
|
2254
|
+
let size = '?';
|
|
2255
|
+
try {
|
|
2256
|
+
size = execSync(`du -sh "${join(backupRoot, d)}" | cut -f1`, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
2257
|
+
} catch {}
|
|
2258
|
+
const marker = pinned ? ' [pinned]' : '';
|
|
2259
|
+
console.log(` ${d} ${size}${marker}`);
|
|
2260
|
+
}
|
|
2261
|
+
console.log('');
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
if (FULL_FLAG) {
|
|
2266
|
+
console.log(' Running full backup...');
|
|
2267
|
+
} else {
|
|
2268
|
+
console.log(' Running backup...');
|
|
2269
|
+
}
|
|
1855
2270
|
console.log('');
|
|
1856
2271
|
try {
|
|
1857
2272
|
execSync(`bash "${BACKUP_SCRIPT}" ${backupArgs.join(' ')}`, {
|
|
@@ -2396,6 +2811,10 @@ async function main() {
|
|
|
2396
2811
|
console.log(' ldm msg broadcast <body> Send to all sessions');
|
|
2397
2812
|
console.log(' ldm stack list Show available stacks');
|
|
2398
2813
|
console.log(' ldm stack install <name> Install a stack (core, web, all)');
|
|
2814
|
+
console.log(' ldm backup Run a full backup now');
|
|
2815
|
+
console.log(' ldm backup --dry-run Preview what would be backed up (with sizes)');
|
|
2816
|
+
console.log(' ldm backup --keep N Keep last N backups (default: 7)');
|
|
2817
|
+
console.log(' ldm backup --pin "reason" Pin latest backup so rotation skips it');
|
|
2399
2818
|
console.log(' ldm updates Show available updates from cache');
|
|
2400
2819
|
console.log(' ldm updates --check Re-check npm registry for updates');
|
|
2401
2820
|
console.log('');
|
package/catalog.json
CHANGED
package/package.json
CHANGED
package/scripts/ldm-backup.sh
CHANGED
|
@@ -81,14 +81,23 @@ if [ "$DRY_RUN" = true ]; then
|
|
|
81
81
|
echo " ~/.ldm/state/ (cp -a)"
|
|
82
82
|
echo " ~/.ldm/config.json (cp)"
|
|
83
83
|
[ -f "$OC_HOME/memory/main.sqlite" ] && echo " ~/.openclaw/memory/main.sqlite (sqlite3 .backup) [$(du -sh "$OC_HOME/memory/main.sqlite" | cut -f1)]"
|
|
84
|
-
[ -f "$OC_HOME/memory/context-embeddings.sqlite" ] && echo " ~/.openclaw/memory/context-embeddings.sqlite (sqlite3 .backup)"
|
|
85
|
-
[ -d "$OC_HOME/workspace" ] && echo " ~/.openclaw/workspace/ (tar)"
|
|
86
|
-
[ -d "$OC_HOME/agents/main/sessions" ] && echo " ~/.openclaw/agents/main/sessions/ (tar)"
|
|
84
|
+
[ -f "$OC_HOME/memory/context-embeddings.sqlite" ] && echo " ~/.openclaw/memory/context-embeddings.sqlite (sqlite3 .backup) [$(du -sh "$OC_HOME/memory/context-embeddings.sqlite" | cut -f1)]"
|
|
85
|
+
[ -d "$OC_HOME/workspace" ] && echo " ~/.openclaw/workspace/ (tar) [$(du -sh "$OC_HOME/workspace" | cut -f1)]"
|
|
86
|
+
[ -d "$OC_HOME/agents/main/sessions" ] && echo " ~/.openclaw/agents/main/sessions/ (tar) [$(du -sh "$OC_HOME/agents/main/sessions" | cut -f1)]"
|
|
87
87
|
[ -f "$OC_HOME/openclaw.json" ] && echo " ~/.openclaw/openclaw.json (cp)"
|
|
88
88
|
[ -f "$CLAUDE_HOME/CLAUDE.md" ] && echo " ~/.claude/CLAUDE.md (cp)"
|
|
89
89
|
[ -f "$CLAUDE_HOME/settings.json" ] && echo " ~/.claude/settings.json (cp)"
|
|
90
|
-
[ -d "$CLAUDE_HOME/projects" ] && echo " ~/.claude/projects/ (tar)"
|
|
91
|
-
[ -n "$WORKSPACE" ] &&
|
|
90
|
+
[ -d "$CLAUDE_HOME/projects" ] && echo " ~/.claude/projects/ (tar) [$(du -sh "$CLAUDE_HOME/projects" | cut -f1)]"
|
|
91
|
+
if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE" ]; then
|
|
92
|
+
# macOS du uses -I for exclusions (not --exclude)
|
|
93
|
+
WS_KB=$(du -sk -I "node_modules" -I ".git" -I "_temp" -I "_trash" "$WORKSPACE" 2>/dev/null | cut -f1 || echo "?")
|
|
94
|
+
WS_MB=$((WS_KB / 1024))
|
|
95
|
+
echo " $WORKSPACE/ (tar, excludes node_modules/.git/objects/_temp/_trash)"
|
|
96
|
+
echo " estimated size: ${WS_MB}MB (${WS_KB}KB)"
|
|
97
|
+
if [ "$WS_KB" -gt 10000000 ] 2>/dev/null; then
|
|
98
|
+
echo " WARNING: exceeds 10GB limit. Backup would abort."
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
92
101
|
[ "$INCLUDE_SECRETS" = true ] && echo " ~/.ldm/secrets/ (cp -a)"
|
|
93
102
|
echo ""
|
|
94
103
|
echo "[DRY RUN] No files modified."
|
|
@@ -195,7 +204,8 @@ if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE" ]; then
|
|
|
195
204
|
echo "--- $WORKSPACE/ ---"
|
|
196
205
|
|
|
197
206
|
# Size guard: estimate workspace size before tarring
|
|
198
|
-
|
|
207
|
+
# macOS du uses -I for exclusions (not --exclude)
|
|
208
|
+
ESTIMATED_KB=$(du -sk -I "node_modules" -I ".git" -I "_temp" -I "_trash" "$WORKSPACE" 2>/dev/null | cut -f1 || echo "0")
|
|
199
209
|
MAX_KB=10000000 # 10GB
|
|
200
210
|
if [ "$ESTIMATED_KB" -gt "$MAX_KB" ] 2>/dev/null; then
|
|
201
211
|
echo " ERROR: Workspace estimated at ${ESTIMATED_KB}KB (>10GB). Aborting tar to prevent disk fill."
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>ai.openclaw.gateway</string>
|
|
7
|
+
<key>Comment</key>
|
|
8
|
+
<string>OpenClaw Gateway (managed by ldm install)</string>
|
|
9
|
+
<key>RunAtLoad</key>
|
|
10
|
+
<true/>
|
|
11
|
+
<key>KeepAlive</key>
|
|
12
|
+
<true/>
|
|
13
|
+
<key>ProgramArguments</key>
|
|
14
|
+
<array>
|
|
15
|
+
<string>/opt/homebrew/bin/node</string>
|
|
16
|
+
<string>/opt/homebrew/lib/node_modules/openclaw/dist/index.js</string>
|
|
17
|
+
<string>gateway</string>
|
|
18
|
+
<string>--port</string>
|
|
19
|
+
<string>18789</string>
|
|
20
|
+
</array>
|
|
21
|
+
<key>StandardOutPath</key>
|
|
22
|
+
<string>{{HOME}}/.openclaw/logs/gateway.log</string>
|
|
23
|
+
<key>StandardErrorPath</key>
|
|
24
|
+
<string>{{HOME}}/.openclaw/logs/gateway.err.log</string>
|
|
25
|
+
<key>EnvironmentVariables</key>
|
|
26
|
+
<dict>
|
|
27
|
+
<key>HOME</key>
|
|
28
|
+
<string>{{HOME}}</string>
|
|
29
|
+
<key>PATH</key>
|
|
30
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
31
|
+
<key>OPENCLAW_GATEWAY_PORT</key>
|
|
32
|
+
<string>18789</string>
|
|
33
|
+
<key>OPENCLAW_LAUNCHD_LABEL</key>
|
|
34
|
+
<string>ai.openclaw.gateway</string>
|
|
35
|
+
<key>OPENCLAW_GATEWAY_TOKEN</key>
|
|
36
|
+
<string>{{OPENCLAW_GATEWAY_TOKEN}}</string>
|
|
37
|
+
<key>NODE_OPTIONS</key>
|
|
38
|
+
<string>--max-old-space-size=8192</string>
|
|
39
|
+
</dict>
|
|
40
|
+
</dict>
|
|
41
|
+
</plist>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>ai.openclaw.healthcheck</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/opt/homebrew/bin/node</string>
|
|
10
|
+
<string>{{HOME}}/.openclaw/wip-healthcheck/healthcheck.mjs</string>
|
|
11
|
+
</array>
|
|
12
|
+
<key>StartInterval</key>
|
|
13
|
+
<integer>180</integer>
|
|
14
|
+
<key>StandardOutPath</key>
|
|
15
|
+
<string>{{HOME}}/.ldm/logs/healthcheck-stdout.log</string>
|
|
16
|
+
<key>StandardErrorPath</key>
|
|
17
|
+
<string>{{HOME}}/.ldm/logs/healthcheck-stderr.log</string>
|
|
18
|
+
<key>EnvironmentVariables</key>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>HOME</key>
|
|
21
|
+
<string>{{HOME}}</string>
|
|
22
|
+
<key>PATH</key>
|
|
23
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
24
|
+
</dict>
|
|
25
|
+
<key>RunAtLoad</key>
|
|
26
|
+
<true/>
|
|
27
|
+
</dict>
|
|
28
|
+
</plist>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<key>ProgramArguments</key>
|
|
8
8
|
<array>
|
|
9
9
|
<string>bash</string>
|
|
10
|
-
<string
|
|
10
|
+
<string>{{HOME}}/.ldm/bin/ldm-backup.sh</string>
|
|
11
11
|
</array>
|
|
12
12
|
<key>StartCalendarInterval</key>
|
|
13
13
|
<dict>
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
<integer>0</integer>
|
|
18
18
|
</dict>
|
|
19
19
|
<key>StandardOutPath</key>
|
|
20
|
-
<string
|
|
20
|
+
<string>{{HOME}}/.ldm/logs/backup.log</string>
|
|
21
21
|
<key>StandardErrorPath</key>
|
|
22
|
-
<string
|
|
22
|
+
<string>{{HOME}}/.ldm/logs/backup.log</string>
|
|
23
23
|
<key>EnvironmentVariables</key>
|
|
24
24
|
<dict>
|
|
25
25
|
<key>HOME</key>
|
|
26
|
-
<string
|
|
26
|
+
<string>{{HOME}}</string>
|
|
27
27
|
<key>PATH</key>
|
|
28
28
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
29
29
|
</dict>
|