@magclaw/cli-core 0.1.33 → 0.1.34
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/package.json +1 -1
- package/src/cli.js +463 -72
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,8 +2,8 @@ import http from 'node:http';
|
|
|
2
2
|
import https from 'node:https';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, readlink, realpath, rm, stat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
@@ -489,6 +489,38 @@ export async function activeComputerLock(env = process.env) {
|
|
|
489
489
|
return activeLockFile(paths.lockFile, { scope: 'computer' });
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
function readJsonFileSync(file, fallback = {}) {
|
|
493
|
+
if (!existsSync(file)) return fallback;
|
|
494
|
+
try {
|
|
495
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
496
|
+
} catch {
|
|
497
|
+
return fallback;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function writeJsonFileSync(file, value) {
|
|
502
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
503
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function readServiceStateSync(profile = DEFAULT_PROFILE, env = process.env) {
|
|
507
|
+
const paths = profilePaths(profile, env);
|
|
508
|
+
return readJsonFileSync(paths.service, {});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function activeDaemonLockSync(profile = DEFAULT_PROFILE, env = process.env) {
|
|
512
|
+
const paths = profilePaths(profile, env);
|
|
513
|
+
const lock = readJsonFileSync(paths.lockFile, null);
|
|
514
|
+
if (lock?.pid && pidIsRunning(lock.pid)) {
|
|
515
|
+
return {
|
|
516
|
+
profile: paths.profile,
|
|
517
|
+
...lock,
|
|
518
|
+
lockFile: paths.lockFile,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
492
524
|
async function writeLockFile(file, lock) {
|
|
493
525
|
const handle = await open(file, 'wx');
|
|
494
526
|
try {
|
|
@@ -730,7 +762,7 @@ function renderComputerHelp(subcommand = '') {
|
|
|
730
762
|
' --dry-run Preview upgrade actions',
|
|
731
763
|
' --channel <name> latest | alpha | pinned:<semver>',
|
|
732
764
|
' --target-version <semver> Explicit target version',
|
|
733
|
-
' --force Accepted for
|
|
765
|
+
' --force Accepted for MagClaw compatibility; currently maps to the normal upgrade path',
|
|
734
766
|
],
|
|
735
767
|
};
|
|
736
768
|
if (command && usage[command]) return `${usage[command].join('\n')}\n`;
|
|
@@ -873,10 +905,24 @@ function backgroundServiceModeForPlatform(platform = process.platform) {
|
|
|
873
905
|
return 'foreground';
|
|
874
906
|
}
|
|
875
907
|
|
|
908
|
+
function normalizeBackgroundServiceMode(value = '') {
|
|
909
|
+
const mode = String(value || '').trim().toLowerCase();
|
|
910
|
+
if (['container', 'k8s', 'kubernetes', 'pod'].includes(mode)) return 'container';
|
|
911
|
+
if (['launchd', 'systemd', 'schtasks', 'foreground'].includes(mode)) return mode;
|
|
912
|
+
return '';
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function requestedBackgroundServiceMode(env = process.env, platform = process.platform) {
|
|
916
|
+
return normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
917
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
918
|
+
|| backgroundServiceModeForPlatform(platform);
|
|
919
|
+
}
|
|
920
|
+
|
|
876
921
|
export function serviceStatePatchForDaemonRun(service = {}, env = process.env, platform = process.platform) {
|
|
877
922
|
if (daemonRunLaunchedByBackgroundService(env)) {
|
|
923
|
+
const serviceMode = normalizeBackgroundServiceMode(service.mode);
|
|
878
924
|
return {
|
|
879
|
-
mode:
|
|
925
|
+
mode: serviceMode && serviceMode !== 'foreground' ? serviceMode : requestedBackgroundServiceMode(env, platform),
|
|
880
926
|
background: true,
|
|
881
927
|
};
|
|
882
928
|
}
|
|
@@ -2153,32 +2199,152 @@ async function globalSkillRoots() {
|
|
|
2153
2199
|
return roots;
|
|
2154
2200
|
}
|
|
2155
2201
|
|
|
2156
|
-
|
|
2202
|
+
function pathIsWithinResolvedRoots(resolvedPath, roots = []) {
|
|
2203
|
+
const cleanPath = path.resolve(resolvedPath);
|
|
2204
|
+
return roots.some((root) => cleanPath === root || cleanPath.startsWith(`${root}${path.sep}`));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
async function resolvedRoots(roots = []) {
|
|
2208
|
+
const resolved = [];
|
|
2209
|
+
for (const root of roots) {
|
|
2210
|
+
const logical = path.resolve(root);
|
|
2211
|
+
if (!resolved.includes(logical)) resolved.push(logical);
|
|
2212
|
+
const physical = await realpath(root).catch(() => logical);
|
|
2213
|
+
if (!resolved.includes(physical)) resolved.push(physical);
|
|
2214
|
+
}
|
|
2215
|
+
return resolved;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
async function ensureWorkspaceSkillsDir(workspace, codexHome = '', agent = {}) {
|
|
2219
|
+
const workspaceSkills = path.join(workspace, 'skills');
|
|
2220
|
+
const legacyGeneratedSkills = codexHome ? path.join(codexHome, 'skills') : '';
|
|
2221
|
+
await mkdir(path.dirname(workspaceSkills), { recursive: true });
|
|
2222
|
+
try {
|
|
2223
|
+
const existing = await lstat(workspaceSkills);
|
|
2224
|
+
if (existing.isSymbolicLink()) {
|
|
2225
|
+
const current = await readlink(workspaceSkills);
|
|
2226
|
+
const resolved = path.resolve(path.dirname(workspaceSkills), current);
|
|
2227
|
+
if (legacyGeneratedSkills && resolved === path.resolve(legacyGeneratedSkills)) {
|
|
2228
|
+
await unlink(workspaceSkills);
|
|
2229
|
+
await mkdir(workspaceSkills, { recursive: true });
|
|
2230
|
+
logInfo('skills', `Repaired workspace skills directory for agent ${agent.id || 'unknown'}.`);
|
|
2231
|
+
} else {
|
|
2232
|
+
logWarning('skills', `Workspace skills path for agent ${agent.id || 'unknown'} is a custom symlink; leaving it untouched.`);
|
|
2233
|
+
}
|
|
2234
|
+
} else if (!existing.isDirectory()) {
|
|
2235
|
+
logWarning('skills', `Workspace skills path for agent ${agent.id || 'unknown'} is not a directory; leaving it untouched.`);
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2240
|
+
await mkdir(workspaceSkills, { recursive: true });
|
|
2241
|
+
}
|
|
2242
|
+
return workspaceSkills;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
async function migrateLegacyAgentSkills(codexHome, workspaceSkills, globalResolvedRoots, agent = {}) {
|
|
2246
|
+
const codexSkillsRoot = path.join(codexHome, 'skills');
|
|
2247
|
+
if (!workspaceSkills || !existsSync(codexSkillsRoot)) return;
|
|
2248
|
+
const entries = await readdir(codexSkillsRoot, { withFileTypes: true }).catch(() => []);
|
|
2249
|
+
for (const entry of entries) {
|
|
2250
|
+
if (entry.name.startsWith('.') || entry.name === '.system') continue;
|
|
2251
|
+
const source = path.join(codexSkillsRoot, entry.name);
|
|
2252
|
+
const target = path.join(workspaceSkills, entry.name);
|
|
2253
|
+
if (existsSync(target)) continue;
|
|
2254
|
+
const sourceInfo = await lstat(source).catch(() => null);
|
|
2255
|
+
if (!sourceInfo) continue;
|
|
2256
|
+
try {
|
|
2257
|
+
if (sourceInfo.isSymbolicLink()) {
|
|
2258
|
+
const current = await readlink(source);
|
|
2259
|
+
const resolved = path.resolve(path.dirname(source), current);
|
|
2260
|
+
if (pathIsWithinResolvedRoots(resolved, globalResolvedRoots)) continue;
|
|
2261
|
+
const realTarget = await realpath(resolved).catch(() => resolved);
|
|
2262
|
+
if (pathIsWithinResolvedRoots(realTarget, globalResolvedRoots)) continue;
|
|
2263
|
+
const targetInfo = await stat(realTarget).catch(() => null);
|
|
2264
|
+
if (!targetInfo) continue;
|
|
2265
|
+
await symlink(realTarget, target, targetInfo.isDirectory() ? 'dir' : 'file');
|
|
2266
|
+
await unlink(source);
|
|
2267
|
+
} else {
|
|
2268
|
+
await rename(source, target);
|
|
2269
|
+
}
|
|
2270
|
+
logInfo('skills', `Migrated legacy local skill ${entry.name} for agent ${agent.id || 'unknown'}.`);
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
logWarning('skills', `Could not migrate legacy local skill ${entry.name} for agent ${agent.id || 'unknown'}: ${error.message}`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
async function linkRuntimeSkillEntry(source, target, agent = {}) {
|
|
2278
|
+
const linked = await linkPathEntry(source, target);
|
|
2279
|
+
if (linked) return true;
|
|
2280
|
+
const existing = await lstat(target).catch(() => null);
|
|
2281
|
+
if (existing && !existing.isSymbolicLink() && path.resolve(source) !== path.resolve(target)) {
|
|
2282
|
+
logWarning('skills', `Could not link skill ${path.basename(target)} for agent ${agent.id || 'unknown'} because the runtime path is not a symlink.`);
|
|
2283
|
+
}
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
async function linkSkillRootEntries(sourceRoot, targetRoot, agent = {}, { includeSystem = false } = {}) {
|
|
2288
|
+
const desired = new Set();
|
|
2289
|
+
if (!sourceRoot || !existsSync(sourceRoot)) return desired;
|
|
2290
|
+
const entries = await readdir(sourceRoot, { withFileTypes: true }).catch(() => []);
|
|
2291
|
+
for (const entry of entries) {
|
|
2292
|
+
if (entry.name.startsWith('.') && !(includeSystem && entry.name === '.system')) continue;
|
|
2293
|
+
const source = path.join(sourceRoot, entry.name);
|
|
2294
|
+
if (includeSystem && entry.name === '.system' && (entry.isDirectory() || entry.isSymbolicLink())) {
|
|
2295
|
+
desired.add(entry.name);
|
|
2296
|
+
const targetSystemRoot = path.join(targetRoot, '.system');
|
|
2297
|
+
await mkdir(targetSystemRoot, { recursive: true });
|
|
2298
|
+
const systemEntries = await readdir(source, { withFileTypes: true }).catch(() => []);
|
|
2299
|
+
for (const systemEntry of systemEntries) {
|
|
2300
|
+
if (!systemEntry.isDirectory() && !systemEntry.isSymbolicLink() && !systemEntry.isFile()) continue;
|
|
2301
|
+
const systemSource = path.join(source, systemEntry.name);
|
|
2302
|
+
const systemTarget = path.join(targetSystemRoot, systemEntry.name);
|
|
2303
|
+
await linkRuntimeSkillEntry(systemSource, systemTarget, agent).catch((error) => {
|
|
2304
|
+
logWarning('skills', `Could not link system skill ${systemEntry.name} for agent ${agent.id || 'unknown'}: ${error.message}`);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.isFile()) continue;
|
|
2310
|
+
desired.add(entry.name);
|
|
2311
|
+
const target = path.join(targetRoot, entry.name);
|
|
2312
|
+
await linkRuntimeSkillEntry(source, target, agent).catch((error) => {
|
|
2313
|
+
logWarning('skills', `Could not link skill ${entry.name} for agent ${agent.id || 'unknown'}: ${error.message}`);
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
return desired;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
async function pruneGeneratedSkillLinks(targetSkillsRoot, desiredNames, agent = {}) {
|
|
2320
|
+
const entries = await readdir(targetSkillsRoot, { withFileTypes: true }).catch(() => []);
|
|
2321
|
+
for (const entry of entries) {
|
|
2322
|
+
if (entry.name.startsWith('.') && entry.name !== '.system') continue;
|
|
2323
|
+
if (desiredNames.has(entry.name)) continue;
|
|
2324
|
+
const target = path.join(targetSkillsRoot, entry.name);
|
|
2325
|
+
const existing = await lstat(target).catch(() => null);
|
|
2326
|
+
if (!existing) continue;
|
|
2327
|
+
if (existing.isSymbolicLink()) {
|
|
2328
|
+
await unlink(target);
|
|
2329
|
+
} else if (entry.name !== '.system') {
|
|
2330
|
+
logWarning('skills', `Stale runtime skill ${entry.name} for agent ${agent.id || 'unknown'} was not removed because it is not a symlink.`);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
async function syncGlobalSkillsIntoAgentHome(codexHome, workspace, agent = {}) {
|
|
2157
2336
|
const targetSkillsRoot = path.join(codexHome, 'skills');
|
|
2158
2337
|
await mkdir(targetSkillsRoot, { recursive: true });
|
|
2159
2338
|
const roots = await globalSkillRoots();
|
|
2339
|
+
const globalResolvedRoots = await resolvedRoots(roots);
|
|
2340
|
+
const workspaceSkills = await ensureWorkspaceSkillsDir(workspace, codexHome, agent);
|
|
2341
|
+
await migrateLegacyAgentSkills(codexHome, workspaceSkills, globalResolvedRoots, agent);
|
|
2342
|
+
const desiredNames = new Set();
|
|
2160
2343
|
for (const sourceSkillsRoot of [...roots].reverse()) {
|
|
2161
|
-
const
|
|
2162
|
-
for (const entry of entries) {
|
|
2163
|
-
const source = path.join(sourceSkillsRoot, entry.name);
|
|
2164
|
-
if (entry.name === '.system' && (entry.isDirectory() || entry.isSymbolicLink())) {
|
|
2165
|
-
const targetSystemRoot = path.join(targetSkillsRoot, '.system');
|
|
2166
|
-
await mkdir(targetSystemRoot, { recursive: true });
|
|
2167
|
-
const systemEntries = await readdir(source, { withFileTypes: true }).catch(() => []);
|
|
2168
|
-
for (const systemEntry of systemEntries) {
|
|
2169
|
-
const systemSource = path.join(source, systemEntry.name);
|
|
2170
|
-
const systemTarget = path.join(targetSystemRoot, systemEntry.name);
|
|
2171
|
-
await linkPathEntry(systemSource, systemTarget).catch(() => {});
|
|
2172
|
-
}
|
|
2173
|
-
continue;
|
|
2174
|
-
}
|
|
2175
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.isFile()) continue;
|
|
2176
|
-
await linkPathEntry(source, path.join(targetSkillsRoot, entry.name)).catch(() => {});
|
|
2177
|
-
}
|
|
2344
|
+
for (const name of await linkSkillRootEntries(sourceSkillsRoot, targetSkillsRoot, agent, { includeSystem: true })) desiredNames.add(name);
|
|
2178
2345
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
await linkPathEntry(targetSkillsRoot, workspaceSkillsLink).catch(() => {});
|
|
2346
|
+
for (const name of await linkSkillRootEntries(workspaceSkills, targetSkillsRoot, agent)) desiredNames.add(name);
|
|
2347
|
+
await pruneGeneratedSkillLinks(targetSkillsRoot, desiredNames, agent);
|
|
2182
2348
|
}
|
|
2183
2349
|
|
|
2184
2350
|
function firstFrontmatterValue(content, keys) {
|
|
@@ -2231,11 +2397,12 @@ function shortenSkillPath(absPath, { agentRoot = '', codexHome = '' } = {}) {
|
|
|
2231
2397
|
async function parseSkillFile(filePath, scope, context = {}) {
|
|
2232
2398
|
const content = await readFile(filePath, 'utf8').catch(() => '');
|
|
2233
2399
|
const resolvedFilePath = await realpath(filePath).catch(() => filePath);
|
|
2400
|
+
const logicalFilePath = path.resolve(filePath);
|
|
2234
2401
|
const name = firstFrontmatterValue(content, ['name', 'title']) || skillNameFromPath(filePath);
|
|
2235
2402
|
const description = firstFrontmatterValue(content, ['description', 'summary', 'short_description', 'short-description'])
|
|
2236
2403
|
|| firstMarkdownParagraph(content)
|
|
2237
2404
|
|| 'No description provided.';
|
|
2238
|
-
const shortPath = shortenSkillPath(
|
|
2405
|
+
const shortPath = shortenSkillPath(logicalFilePath, context);
|
|
2239
2406
|
return {
|
|
2240
2407
|
id: `${scope}:${shortPath}`,
|
|
2241
2408
|
name,
|
|
@@ -2326,15 +2493,6 @@ async function findPluginSkillFiles(root, { maxEntries = 400 } = {}) {
|
|
|
2326
2493
|
return found;
|
|
2327
2494
|
}
|
|
2328
2495
|
|
|
2329
|
-
async function resolvedRoots(paths) {
|
|
2330
|
-
const roots = [];
|
|
2331
|
-
for (const item of paths) {
|
|
2332
|
-
if (!item || !existsSync(item)) continue;
|
|
2333
|
-
roots.push(await realpath(item).catch(() => path.resolve(item)));
|
|
2334
|
-
}
|
|
2335
|
-
return roots;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
2496
|
function uniqueSkills(items) {
|
|
2339
2497
|
const seen = new Set();
|
|
2340
2498
|
return items.filter((item) => {
|
|
@@ -2366,6 +2524,35 @@ function daemonSkillTools() {
|
|
|
2366
2524
|
];
|
|
2367
2525
|
}
|
|
2368
2526
|
|
|
2527
|
+
async function listDaemonAgentSkills({ agent, agentDir, workspace, codexHome = '' }) {
|
|
2528
|
+
const context = { agentRoot: agentDir, codexHome };
|
|
2529
|
+
const roots = await globalSkillRoots();
|
|
2530
|
+
const globalSkills = [];
|
|
2531
|
+
for (const root of roots) globalSkills.push(...await scanSkillsDir(root, 'global', context));
|
|
2532
|
+
const agentRoots = [
|
|
2533
|
+
path.join(workspace, 'skills'),
|
|
2534
|
+
path.join(agentDir, '.codex', 'skills'),
|
|
2535
|
+
path.join(agentDir, '.agents', 'skills'),
|
|
2536
|
+
];
|
|
2537
|
+
const agentSkills = [];
|
|
2538
|
+
for (const root of agentRoots) agentSkills.push(...await scanSkillsDir(root, 'agent', context));
|
|
2539
|
+
const pluginFiles = await findPluginSkillFiles(path.join(SOURCE_CODEX_HOME, 'plugins', 'cache'));
|
|
2540
|
+
const pluginSkills = [];
|
|
2541
|
+
for (const file of pluginFiles) pluginSkills.push(await parseSkillFile(file, 'plugin', context));
|
|
2542
|
+
return {
|
|
2543
|
+
agent: {
|
|
2544
|
+
id: agent.id,
|
|
2545
|
+
name: agent.name || agent.id,
|
|
2546
|
+
codexHome: codexHome || undefined,
|
|
2547
|
+
workspacePath: workspace,
|
|
2548
|
+
},
|
|
2549
|
+
global: uniqueSkills(globalSkills),
|
|
2550
|
+
workspace: uniqueSkills(agentSkills),
|
|
2551
|
+
plugin: uniqueSkills(pluginSkills),
|
|
2552
|
+
tools: daemonSkillTools(),
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2369
2556
|
const DAEMON_PROGRESSIVE_DISCLOSURE_SECTION = [
|
|
2370
2557
|
'## 渐进式披露',
|
|
2371
2558
|
'- 其他 Agent 默认只会先读取本文件;不要假设它们已经看到 `notes/` 或 `workspace/` 中的详细文件。',
|
|
@@ -2650,7 +2837,7 @@ class CodexAgentSession {
|
|
|
2650
2837
|
codexHome: this.codexHome(),
|
|
2651
2838
|
runtimeKind: 'codex',
|
|
2652
2839
|
});
|
|
2653
|
-
await syncGlobalSkillsIntoAgentHome(this.codexHome(), this.workspace());
|
|
2840
|
+
await syncGlobalSkillsIntoAgentHome(this.codexHome(), this.workspace(), this.agent);
|
|
2654
2841
|
await writeFile(path.join(this.codexHome(), 'config.toml'), [
|
|
2655
2842
|
'wire_api = "responses"',
|
|
2656
2843
|
'',
|
|
@@ -2668,8 +2855,8 @@ class CodexAgentSession {
|
|
|
2668
2855
|
'',
|
|
2669
2856
|
'This workspace is isolated for a MagClaw cloud-connected agent.',
|
|
2670
2857
|
'Do not assume files from the user localhost MagClaw instance are present here.',
|
|
2671
|
-
'Global Codex skills are linked into `./skills` for read-only reuse when available.',
|
|
2672
2858
|
'Agent-specific skills can be installed under `./skills/<skill-name>/SKILL.md`; this path belongs to this agent only.',
|
|
2859
|
+
'Runtime-generated adapter directories are not skill install targets.',
|
|
2673
2860
|
'',
|
|
2674
2861
|
].join('\n'));
|
|
2675
2862
|
}
|
|
@@ -2684,40 +2871,12 @@ class CodexAgentSession {
|
|
|
2684
2871
|
|
|
2685
2872
|
async listSkills() {
|
|
2686
2873
|
await this.prepare();
|
|
2687
|
-
|
|
2688
|
-
|
|
2874
|
+
return listDaemonAgentSkills({
|
|
2875
|
+
agent: this.agent,
|
|
2876
|
+
agentDir: this.agentDir(),
|
|
2877
|
+
workspace: this.workspace(),
|
|
2689
2878
|
codexHome: this.codexHome(),
|
|
2690
|
-
};
|
|
2691
|
-
const roots = await globalSkillRoots();
|
|
2692
|
-
const globalSkills = [];
|
|
2693
|
-
for (const root of roots) globalSkills.push(...await scanSkillsDir(root, 'global', context));
|
|
2694
|
-
const globalResolvedRoots = await resolvedRoots(roots);
|
|
2695
|
-
const agentRoots = [
|
|
2696
|
-
path.join(this.codexHome(), 'skills'),
|
|
2697
|
-
path.join(this.agentDir(), '.codex', 'skills'),
|
|
2698
|
-
path.join(this.agentDir(), '.agents', 'skills'),
|
|
2699
|
-
];
|
|
2700
|
-
const agentSkills = [];
|
|
2701
|
-
for (const root of agentRoots) agentSkills.push(...await scanSkillsDir(root, 'agent', context));
|
|
2702
|
-
const workspaceSkills = agentSkills.filter((skill) => {
|
|
2703
|
-
const resolved = path.resolve(skill.absolutePath);
|
|
2704
|
-
return !globalResolvedRoots.some((root) => resolved === root || resolved.startsWith(`${root}${path.sep}`));
|
|
2705
2879
|
});
|
|
2706
|
-
const pluginFiles = await findPluginSkillFiles(path.join(SOURCE_CODEX_HOME, 'plugins', 'cache'));
|
|
2707
|
-
const pluginSkills = [];
|
|
2708
|
-
for (const file of pluginFiles) pluginSkills.push(await parseSkillFile(file, 'plugin', context));
|
|
2709
|
-
return {
|
|
2710
|
-
agent: {
|
|
2711
|
-
id: this.agent.id,
|
|
2712
|
-
name: this.agent.name || this.agent.id,
|
|
2713
|
-
codexHome: this.codexHome(),
|
|
2714
|
-
workspacePath: this.workspace(),
|
|
2715
|
-
},
|
|
2716
|
-
global: uniqueSkills(globalSkills),
|
|
2717
|
-
workspace: uniqueSkills(workspaceSkills),
|
|
2718
|
-
plugin: uniqueSkills(pluginSkills),
|
|
2719
|
-
tools: daemonSkillTools(),
|
|
2720
|
-
};
|
|
2721
2880
|
}
|
|
2722
2881
|
|
|
2723
2882
|
sendStatus(status, activity = null) {
|
|
@@ -3348,6 +3507,7 @@ class ClaudeAgentSession {
|
|
|
3348
3507
|
|
|
3349
3508
|
async prepare() {
|
|
3350
3509
|
await mkdir(this.workspace(), { recursive: true });
|
|
3510
|
+
await ensureWorkspaceSkillsDir(this.workspace(), path.join(this.agentDir(), 'codex-home'), this.agent);
|
|
3351
3511
|
await ensureDaemonAgentWorkspaceRoot(this.agentDir(), this.agent);
|
|
3352
3512
|
await prepareRuntimeHooks({
|
|
3353
3513
|
agentDir: this.agentDir(),
|
|
@@ -3358,6 +3518,8 @@ class ClaudeAgentSession {
|
|
|
3358
3518
|
'# MagClaw Remote Claude Agent Workspace',
|
|
3359
3519
|
'',
|
|
3360
3520
|
'This workspace is isolated for a MagClaw cloud-connected Claude Code agent.',
|
|
3521
|
+
'Agent-specific skills can be installed under `./skills/<skill-name>/SKILL.md`; this path belongs to this agent only.',
|
|
3522
|
+
'Runtime-generated adapter directories are not skill install targets.',
|
|
3361
3523
|
'',
|
|
3362
3524
|
].join('\n'));
|
|
3363
3525
|
}
|
|
@@ -3370,6 +3532,15 @@ class ClaudeAgentSession {
|
|
|
3370
3532
|
return readDaemonAgentWorkspaceFile(this.agentDir(), this.agent, relPath);
|
|
3371
3533
|
}
|
|
3372
3534
|
|
|
3535
|
+
async listSkills() {
|
|
3536
|
+
await this.prepare();
|
|
3537
|
+
return listDaemonAgentSkills({
|
|
3538
|
+
agent: this.agent,
|
|
3539
|
+
agentDir: this.agentDir(),
|
|
3540
|
+
workspace: this.workspace(),
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3373
3544
|
sendStatus(status, activity = null) {
|
|
3374
3545
|
this.status = status;
|
|
3375
3546
|
this.onStatusChange(this, status);
|
|
@@ -4725,8 +4896,12 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4725
4896
|
|| previousService.packageBin
|
|
4726
4897
|
|| packageBinForPackageName(packageName),
|
|
4727
4898
|
).trim() || packageBinForPackageName(packageName);
|
|
4899
|
+
const envServiceMode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
4900
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE);
|
|
4901
|
+
const previousMode = normalizeBackgroundServiceMode(previousService.mode);
|
|
4902
|
+
const serviceMode = envServiceMode || (previousMode && previousMode !== 'foreground' ? previousMode : backgroundServiceModeForPlatform(process.platform));
|
|
4728
4903
|
const service = await writeServiceState(paths.profile, {
|
|
4729
|
-
mode:
|
|
4904
|
+
mode: serviceMode,
|
|
4730
4905
|
background: true,
|
|
4731
4906
|
launcher,
|
|
4732
4907
|
packageSpec,
|
|
@@ -4809,6 +4984,72 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4809
4984
|
return launcher;
|
|
4810
4985
|
}
|
|
4811
4986
|
|
|
4987
|
+
async function writeContainerSupervisor(profile, launcher, env = process.env) {
|
|
4988
|
+
const paths = profilePaths(profile, env);
|
|
4989
|
+
await mkdir(paths.runDir, { recursive: true });
|
|
4990
|
+
await mkdir(paths.logDir, { recursive: true });
|
|
4991
|
+
const supervisor = path.join(paths.runDir, 'container-supervisor.js');
|
|
4992
|
+
const restartSec = Math.max(1, Math.min(60, Number(env.MAGCLAW_DAEMON_CONTAINER_RESTART_SEC || 3) || 3));
|
|
4993
|
+
const code = [
|
|
4994
|
+
'#!/usr/bin/env node',
|
|
4995
|
+
"const { spawn } = require('node:child_process');",
|
|
4996
|
+
"const fs = require('node:fs');",
|
|
4997
|
+
"const path = require('node:path');",
|
|
4998
|
+
`const launcher = ${JSON.stringify(launcher)};`,
|
|
4999
|
+
`const serviceFile = ${JSON.stringify(paths.service)};`,
|
|
5000
|
+
`const logDir = ${JSON.stringify(paths.logDir)};`,
|
|
5001
|
+
`const restartMs = ${JSON.stringify(restartSec * 1000)};`,
|
|
5002
|
+
'let child = null;',
|
|
5003
|
+
'let stopping = false;',
|
|
5004
|
+
'function readService() {',
|
|
5005
|
+
" try { return JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch { return {}; }",
|
|
5006
|
+
'}',
|
|
5007
|
+
'function shouldStop() {',
|
|
5008
|
+
' const service = readService();',
|
|
5009
|
+
' return Boolean(service.remoteClosed || service.containerSupervisorDisabled);',
|
|
5010
|
+
'}',
|
|
5011
|
+
'function openLog(name) {',
|
|
5012
|
+
' fs.mkdirSync(logDir, { recursive: true });',
|
|
5013
|
+
" return fs.openSync(path.join(logDir, name), 'a');",
|
|
5014
|
+
'}',
|
|
5015
|
+
'function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }',
|
|
5016
|
+
'function stop(signal) {',
|
|
5017
|
+
' stopping = true;',
|
|
5018
|
+
" if (child && child.exitCode === null && child.signalCode === null) child.kill(signal || 'SIGTERM');",
|
|
5019
|
+
' setTimeout(() => process.exit(0), 5000).unref?.();',
|
|
5020
|
+
'}',
|
|
5021
|
+
"process.once('SIGINT', () => stop('SIGINT'));",
|
|
5022
|
+
"process.once('SIGTERM', () => stop('SIGTERM'));",
|
|
5023
|
+
'(async () => {',
|
|
5024
|
+
' while (!stopping) {',
|
|
5025
|
+
' if (shouldStop()) break;',
|
|
5026
|
+
" const out = openLog('daemon.log');",
|
|
5027
|
+
" const err = openLog('daemon.err.log');",
|
|
5028
|
+
' child = spawn(process.execPath, [launcher], {',
|
|
5029
|
+
" stdio: ['ignore', out, err],",
|
|
5030
|
+
' env: {',
|
|
5031
|
+
' ...process.env,',
|
|
5032
|
+
" MAGCLAW_DAEMON_SERVICE_MODE: 'container',",
|
|
5033
|
+
' },',
|
|
5034
|
+
' });',
|
|
5035
|
+
" await new Promise((resolve) => child.once('exit', resolve));",
|
|
5036
|
+
' fs.closeSync(out);',
|
|
5037
|
+
' fs.closeSync(err);',
|
|
5038
|
+
' child = null;',
|
|
5039
|
+
' if (stopping || shouldStop()) break;',
|
|
5040
|
+
' await sleep(restartMs);',
|
|
5041
|
+
' }',
|
|
5042
|
+
'})().catch((error) => {',
|
|
5043
|
+
" console.error(`[magclaw-container-supervisor] ${error && error.stack ? error.stack : error}`);",
|
|
5044
|
+
' process.exit(1);',
|
|
5045
|
+
'});',
|
|
5046
|
+
'',
|
|
5047
|
+
].join('\n');
|
|
5048
|
+
await writeFile(supervisor, code);
|
|
5049
|
+
await chmod(supervisor, 0o755).catch(() => {});
|
|
5050
|
+
return supervisor;
|
|
5051
|
+
}
|
|
5052
|
+
|
|
4812
5053
|
function launchAgentLabel(profile) {
|
|
4813
5054
|
return `ai.magclaw.daemon.${safeProfileName(profile)}`;
|
|
4814
5055
|
}
|
|
@@ -4824,6 +5065,61 @@ async function ensureExecutable(file) {
|
|
|
4824
5065
|
await chmod(file, 0o755).catch(() => {});
|
|
4825
5066
|
}
|
|
4826
5067
|
|
|
5068
|
+
async function startContainerBackground(profile, env = process.env) {
|
|
5069
|
+
const paths = profilePaths(profile, env);
|
|
5070
|
+
await mkdir(paths.logDir, { recursive: true });
|
|
5071
|
+
const launcher = await writeLauncher(paths.profile, { ...env, MAGCLAW_DAEMON_SERVICE_MODE: 'container' });
|
|
5072
|
+
await ensureExecutable(launcher);
|
|
5073
|
+
const supervisor = await writeContainerSupervisor(paths.profile, launcher, env);
|
|
5074
|
+
await ensureExecutable(supervisor);
|
|
5075
|
+
await writeServiceState(paths.profile, {
|
|
5076
|
+
mode: 'container',
|
|
5077
|
+
background: true,
|
|
5078
|
+
launcher,
|
|
5079
|
+
containerSupervisor: supervisor,
|
|
5080
|
+
containerSupervisorDisabled: false,
|
|
5081
|
+
}, env);
|
|
5082
|
+
const current = backgroundServiceStatus(paths.profile, env);
|
|
5083
|
+
if (current.active) {
|
|
5084
|
+
return {
|
|
5085
|
+
ok: true,
|
|
5086
|
+
mode: 'container',
|
|
5087
|
+
active: true,
|
|
5088
|
+
alreadyRunning: true,
|
|
5089
|
+
pid: current.pid || null,
|
|
5090
|
+
supervisorPid: current.supervisorPid || null,
|
|
5091
|
+
file: supervisor,
|
|
5092
|
+
launcher,
|
|
5093
|
+
status: current.status || 'running',
|
|
5094
|
+
};
|
|
5095
|
+
}
|
|
5096
|
+
const child = spawn(process.execPath, [supervisor], {
|
|
5097
|
+
cwd: process.cwd(),
|
|
5098
|
+
detached: true,
|
|
5099
|
+
stdio: 'ignore',
|
|
5100
|
+
env: {
|
|
5101
|
+
...env,
|
|
5102
|
+
MAGCLAW_DAEMON_SERVICE_MODE: 'container',
|
|
5103
|
+
},
|
|
5104
|
+
});
|
|
5105
|
+
child.unref();
|
|
5106
|
+
await writeServiceState(paths.profile, {
|
|
5107
|
+
mode: 'container',
|
|
5108
|
+
background: true,
|
|
5109
|
+
launcher,
|
|
5110
|
+
containerSupervisor: supervisor,
|
|
5111
|
+
containerSupervisorPid: child.pid || null,
|
|
5112
|
+
}, env);
|
|
5113
|
+
return {
|
|
5114
|
+
ok: true,
|
|
5115
|
+
mode: 'container',
|
|
5116
|
+
active: true,
|
|
5117
|
+
supervisorPid: child.pid || null,
|
|
5118
|
+
file: supervisor,
|
|
5119
|
+
launcher,
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
5122
|
+
|
|
4827
5123
|
async function startMacBackground(profile, env = process.env) {
|
|
4828
5124
|
const paths = profilePaths(profile, env);
|
|
4829
5125
|
const label = launchAgentLabel(paths.profile);
|
|
@@ -4934,6 +5230,11 @@ async function startWindowsBackground(profile, env = process.env) {
|
|
|
4934
5230
|
}
|
|
4935
5231
|
|
|
4936
5232
|
async function startBackground(profile, env = process.env) {
|
|
5233
|
+
const service = await readServiceState(profile, env);
|
|
5234
|
+
const mode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5235
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
5236
|
+
|| normalizeBackgroundServiceMode(service.mode);
|
|
5237
|
+
if (mode === 'container') return startContainerBackground(profile, env);
|
|
4937
5238
|
if (process.platform === 'darwin') return startMacBackground(profile, env);
|
|
4938
5239
|
if (process.platform === 'linux') return startLinuxBackground(profile, env);
|
|
4939
5240
|
if (process.platform === 'win32') return startWindowsBackground(profile, env);
|
|
@@ -4961,6 +5262,25 @@ export function parseLaunchdPrintStatus(result = {}) {
|
|
|
4961
5262
|
|
|
4962
5263
|
function backgroundServiceStatus(profile, env = process.env) {
|
|
4963
5264
|
const paths = profilePaths(profile, env);
|
|
5265
|
+
const serviceState = readServiceStateSync(paths.profile, env);
|
|
5266
|
+
const requestedMode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5267
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE);
|
|
5268
|
+
if (requestedMode === 'container' || normalizeBackgroundServiceMode(serviceState.mode) === 'container') {
|
|
5269
|
+
const lock = activeDaemonLockSync(paths.profile, env);
|
|
5270
|
+
const supervisorPid = Number(serviceState.containerSupervisorPid || 0);
|
|
5271
|
+
const supervisorRunning = Number.isInteger(supervisorPid) && supervisorPid > 0 && pidIsRunning(supervisorPid);
|
|
5272
|
+
const daemonRunning = Boolean(lock?.pid);
|
|
5273
|
+
return {
|
|
5274
|
+
mode: 'container',
|
|
5275
|
+
active: daemonRunning || supervisorRunning,
|
|
5276
|
+
pid: lock?.pid || null,
|
|
5277
|
+
supervisorPid: supervisorRunning ? supervisorPid : null,
|
|
5278
|
+
file: serviceState.containerSupervisor || '',
|
|
5279
|
+
launcher: serviceState.launcher || '',
|
|
5280
|
+
status: daemonRunning ? 'running' : supervisorRunning ? 'supervising' : 'inactive',
|
|
5281
|
+
error: '',
|
|
5282
|
+
};
|
|
5283
|
+
}
|
|
4964
5284
|
if (process.platform === 'darwin') {
|
|
4965
5285
|
const label = launchAgentLabel(paths.profile);
|
|
4966
5286
|
const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
@@ -5081,9 +5401,77 @@ async function stopActiveDaemon(profile, env = process.env) {
|
|
|
5081
5401
|
return { ok: stopped, running: !stopped, pid, signal };
|
|
5082
5402
|
}
|
|
5083
5403
|
|
|
5404
|
+
function waitForPidExitSync(pid, timeoutMs = 2000) {
|
|
5405
|
+
const deadline = Date.now() + timeoutMs;
|
|
5406
|
+
while (Date.now() < deadline) {
|
|
5407
|
+
if (!pidIsRunning(pid)) return true;
|
|
5408
|
+
sleepSync(100);
|
|
5409
|
+
}
|
|
5410
|
+
return !pidIsRunning(pid);
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
function stopPidSync(pid, { allowCurrent = false, timeoutMs = 2000 } = {}) {
|
|
5414
|
+
const value = Number(pid);
|
|
5415
|
+
if (!Number.isInteger(value) || value <= 0) return { ok: true, running: false };
|
|
5416
|
+
if (!allowCurrent && value === process.pid) {
|
|
5417
|
+
return { ok: true, running: true, pid: value, skippedCurrent: true };
|
|
5418
|
+
}
|
|
5419
|
+
try {
|
|
5420
|
+
process.kill(value, 'SIGTERM');
|
|
5421
|
+
} catch (error) {
|
|
5422
|
+
if (error?.code === 'ESRCH') return { ok: true, running: false, pid: value, stale: true };
|
|
5423
|
+
return { ok: false, running: true, pid: value, error: error.message };
|
|
5424
|
+
}
|
|
5425
|
+
if (waitForPidExitSync(value, timeoutMs)) return { ok: true, running: false, pid: value, signal: 'SIGTERM' };
|
|
5426
|
+
try {
|
|
5427
|
+
process.kill(value, 'SIGKILL');
|
|
5428
|
+
} catch (error) {
|
|
5429
|
+
if (error?.code === 'ESRCH') return { ok: true, running: false, pid: value, signal: 'SIGKILL' };
|
|
5430
|
+
return { ok: false, running: true, pid: value, signal: 'SIGKILL', error: error.message };
|
|
5431
|
+
}
|
|
5432
|
+
const stopped = waitForPidExitSync(value, 1000);
|
|
5433
|
+
return { ok: stopped, running: !stopped, pid: value, signal: 'SIGKILL' };
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
function stopContainerBackground(profile, env = process.env, options = {}) {
|
|
5437
|
+
const paths = profilePaths(profile, env);
|
|
5438
|
+
const state = readServiceStateSync(paths.profile, env);
|
|
5439
|
+
const lock = activeDaemonLockSync(paths.profile, env);
|
|
5440
|
+
const supervisor = stopPidSync(state.containerSupervisorPid);
|
|
5441
|
+
const daemon = stopPidSync(lock?.pid);
|
|
5442
|
+
if (options.disable) {
|
|
5443
|
+
writeJsonFileSync(paths.service, {
|
|
5444
|
+
...state,
|
|
5445
|
+
version: 1,
|
|
5446
|
+
profile: paths.profile,
|
|
5447
|
+
mode: 'container',
|
|
5448
|
+
background: true,
|
|
5449
|
+
containerSupervisorDisabled: true,
|
|
5450
|
+
updatedAt: now(),
|
|
5451
|
+
});
|
|
5452
|
+
}
|
|
5453
|
+
return {
|
|
5454
|
+
ok: Boolean(supervisor.ok && daemon.ok),
|
|
5455
|
+
mode: 'container',
|
|
5456
|
+
supervisorPid: state.containerSupervisorPid || null,
|
|
5457
|
+
pid: lock?.pid || null,
|
|
5458
|
+
supervisor,
|
|
5459
|
+
process: daemon,
|
|
5460
|
+
file: state.containerSupervisor || '',
|
|
5461
|
+
launcher: state.launcher || '',
|
|
5462
|
+
disabled: Boolean(options.disable),
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5084
5466
|
function stopBackground(profile, env = process.env, options = {}) {
|
|
5467
|
+
const paths = profilePaths(profile, env);
|
|
5468
|
+
const state = readServiceStateSync(paths.profile, env);
|
|
5469
|
+
const requestedMode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5470
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE);
|
|
5471
|
+
if (requestedMode === 'container' || normalizeBackgroundServiceMode(state.mode) === 'container') {
|
|
5472
|
+
return stopContainerBackground(paths.profile, env, options);
|
|
5473
|
+
}
|
|
5085
5474
|
if (process.platform === 'darwin') {
|
|
5086
|
-
const paths = profilePaths(profile, env);
|
|
5087
5475
|
const label = launchAgentLabel(paths.profile);
|
|
5088
5476
|
const serviceTarget = `gui/${process.getuid()}/${label}`;
|
|
5089
5477
|
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
@@ -5570,7 +5958,10 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
5570
5958
|
|
|
5571
5959
|
await emitProgress({ status: 'upgrading', phase: 'stage_service', progress: 45, message: 'Staging service launcher.' });
|
|
5572
5960
|
await writeServiceState(profile, {
|
|
5573
|
-
mode:
|
|
5961
|
+
mode: normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5962
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
5963
|
+
|| normalizeBackgroundServiceMode(serviceBefore.mode)
|
|
5964
|
+
|| backgroundServiceModeForPlatform(process.platform),
|
|
5574
5965
|
background: true,
|
|
5575
5966
|
packageSpec,
|
|
5576
5967
|
packageName,
|