@magclaw/cli-core 0.1.32 → 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 +464 -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
|
}
|
|
@@ -1309,6 +1355,7 @@ async function writeCliShimFile(file, content, { force = false } = {}) {
|
|
|
1309
1355
|
...status,
|
|
1310
1356
|
changed: true,
|
|
1311
1357
|
written: true,
|
|
1358
|
+
upToDate: true,
|
|
1312
1359
|
reason: force && status.exists ? 'forced' : status.reason,
|
|
1313
1360
|
previousHash: status.currentHash,
|
|
1314
1361
|
currentHash: status.expectedHash,
|
|
@@ -2152,32 +2199,152 @@ async function globalSkillRoots() {
|
|
|
2152
2199
|
return roots;
|
|
2153
2200
|
}
|
|
2154
2201
|
|
|
2155
|
-
|
|
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 = {}) {
|
|
2156
2336
|
const targetSkillsRoot = path.join(codexHome, 'skills');
|
|
2157
2337
|
await mkdir(targetSkillsRoot, { recursive: true });
|
|
2158
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();
|
|
2159
2343
|
for (const sourceSkillsRoot of [...roots].reverse()) {
|
|
2160
|
-
const
|
|
2161
|
-
for (const entry of entries) {
|
|
2162
|
-
const source = path.join(sourceSkillsRoot, entry.name);
|
|
2163
|
-
if (entry.name === '.system' && (entry.isDirectory() || entry.isSymbolicLink())) {
|
|
2164
|
-
const targetSystemRoot = path.join(targetSkillsRoot, '.system');
|
|
2165
|
-
await mkdir(targetSystemRoot, { recursive: true });
|
|
2166
|
-
const systemEntries = await readdir(source, { withFileTypes: true }).catch(() => []);
|
|
2167
|
-
for (const systemEntry of systemEntries) {
|
|
2168
|
-
const systemSource = path.join(source, systemEntry.name);
|
|
2169
|
-
const systemTarget = path.join(targetSystemRoot, systemEntry.name);
|
|
2170
|
-
await linkPathEntry(systemSource, systemTarget).catch(() => {});
|
|
2171
|
-
}
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.isFile()) continue;
|
|
2175
|
-
await linkPathEntry(source, path.join(targetSkillsRoot, entry.name)).catch(() => {});
|
|
2176
|
-
}
|
|
2344
|
+
for (const name of await linkSkillRootEntries(sourceSkillsRoot, targetSkillsRoot, agent, { includeSystem: true })) desiredNames.add(name);
|
|
2177
2345
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
await linkPathEntry(targetSkillsRoot, workspaceSkillsLink).catch(() => {});
|
|
2346
|
+
for (const name of await linkSkillRootEntries(workspaceSkills, targetSkillsRoot, agent)) desiredNames.add(name);
|
|
2347
|
+
await pruneGeneratedSkillLinks(targetSkillsRoot, desiredNames, agent);
|
|
2181
2348
|
}
|
|
2182
2349
|
|
|
2183
2350
|
function firstFrontmatterValue(content, keys) {
|
|
@@ -2230,11 +2397,12 @@ function shortenSkillPath(absPath, { agentRoot = '', codexHome = '' } = {}) {
|
|
|
2230
2397
|
async function parseSkillFile(filePath, scope, context = {}) {
|
|
2231
2398
|
const content = await readFile(filePath, 'utf8').catch(() => '');
|
|
2232
2399
|
const resolvedFilePath = await realpath(filePath).catch(() => filePath);
|
|
2400
|
+
const logicalFilePath = path.resolve(filePath);
|
|
2233
2401
|
const name = firstFrontmatterValue(content, ['name', 'title']) || skillNameFromPath(filePath);
|
|
2234
2402
|
const description = firstFrontmatterValue(content, ['description', 'summary', 'short_description', 'short-description'])
|
|
2235
2403
|
|| firstMarkdownParagraph(content)
|
|
2236
2404
|
|| 'No description provided.';
|
|
2237
|
-
const shortPath = shortenSkillPath(
|
|
2405
|
+
const shortPath = shortenSkillPath(logicalFilePath, context);
|
|
2238
2406
|
return {
|
|
2239
2407
|
id: `${scope}:${shortPath}`,
|
|
2240
2408
|
name,
|
|
@@ -2325,15 +2493,6 @@ async function findPluginSkillFiles(root, { maxEntries = 400 } = {}) {
|
|
|
2325
2493
|
return found;
|
|
2326
2494
|
}
|
|
2327
2495
|
|
|
2328
|
-
async function resolvedRoots(paths) {
|
|
2329
|
-
const roots = [];
|
|
2330
|
-
for (const item of paths) {
|
|
2331
|
-
if (!item || !existsSync(item)) continue;
|
|
2332
|
-
roots.push(await realpath(item).catch(() => path.resolve(item)));
|
|
2333
|
-
}
|
|
2334
|
-
return roots;
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
2496
|
function uniqueSkills(items) {
|
|
2338
2497
|
const seen = new Set();
|
|
2339
2498
|
return items.filter((item) => {
|
|
@@ -2365,6 +2524,35 @@ function daemonSkillTools() {
|
|
|
2365
2524
|
];
|
|
2366
2525
|
}
|
|
2367
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
|
+
|
|
2368
2556
|
const DAEMON_PROGRESSIVE_DISCLOSURE_SECTION = [
|
|
2369
2557
|
'## 渐进式披露',
|
|
2370
2558
|
'- 其他 Agent 默认只会先读取本文件;不要假设它们已经看到 `notes/` 或 `workspace/` 中的详细文件。',
|
|
@@ -2649,7 +2837,7 @@ class CodexAgentSession {
|
|
|
2649
2837
|
codexHome: this.codexHome(),
|
|
2650
2838
|
runtimeKind: 'codex',
|
|
2651
2839
|
});
|
|
2652
|
-
await syncGlobalSkillsIntoAgentHome(this.codexHome(), this.workspace());
|
|
2840
|
+
await syncGlobalSkillsIntoAgentHome(this.codexHome(), this.workspace(), this.agent);
|
|
2653
2841
|
await writeFile(path.join(this.codexHome(), 'config.toml'), [
|
|
2654
2842
|
'wire_api = "responses"',
|
|
2655
2843
|
'',
|
|
@@ -2667,8 +2855,8 @@ class CodexAgentSession {
|
|
|
2667
2855
|
'',
|
|
2668
2856
|
'This workspace is isolated for a MagClaw cloud-connected agent.',
|
|
2669
2857
|
'Do not assume files from the user localhost MagClaw instance are present here.',
|
|
2670
|
-
'Global Codex skills are linked into `./skills` for read-only reuse when available.',
|
|
2671
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.',
|
|
2672
2860
|
'',
|
|
2673
2861
|
].join('\n'));
|
|
2674
2862
|
}
|
|
@@ -2683,40 +2871,12 @@ class CodexAgentSession {
|
|
|
2683
2871
|
|
|
2684
2872
|
async listSkills() {
|
|
2685
2873
|
await this.prepare();
|
|
2686
|
-
|
|
2687
|
-
|
|
2874
|
+
return listDaemonAgentSkills({
|
|
2875
|
+
agent: this.agent,
|
|
2876
|
+
agentDir: this.agentDir(),
|
|
2877
|
+
workspace: this.workspace(),
|
|
2688
2878
|
codexHome: this.codexHome(),
|
|
2689
|
-
};
|
|
2690
|
-
const roots = await globalSkillRoots();
|
|
2691
|
-
const globalSkills = [];
|
|
2692
|
-
for (const root of roots) globalSkills.push(...await scanSkillsDir(root, 'global', context));
|
|
2693
|
-
const globalResolvedRoots = await resolvedRoots(roots);
|
|
2694
|
-
const agentRoots = [
|
|
2695
|
-
path.join(this.codexHome(), 'skills'),
|
|
2696
|
-
path.join(this.agentDir(), '.codex', 'skills'),
|
|
2697
|
-
path.join(this.agentDir(), '.agents', 'skills'),
|
|
2698
|
-
];
|
|
2699
|
-
const agentSkills = [];
|
|
2700
|
-
for (const root of agentRoots) agentSkills.push(...await scanSkillsDir(root, 'agent', context));
|
|
2701
|
-
const workspaceSkills = agentSkills.filter((skill) => {
|
|
2702
|
-
const resolved = path.resolve(skill.absolutePath);
|
|
2703
|
-
return !globalResolvedRoots.some((root) => resolved === root || resolved.startsWith(`${root}${path.sep}`));
|
|
2704
2879
|
});
|
|
2705
|
-
const pluginFiles = await findPluginSkillFiles(path.join(SOURCE_CODEX_HOME, 'plugins', 'cache'));
|
|
2706
|
-
const pluginSkills = [];
|
|
2707
|
-
for (const file of pluginFiles) pluginSkills.push(await parseSkillFile(file, 'plugin', context));
|
|
2708
|
-
return {
|
|
2709
|
-
agent: {
|
|
2710
|
-
id: this.agent.id,
|
|
2711
|
-
name: this.agent.name || this.agent.id,
|
|
2712
|
-
codexHome: this.codexHome(),
|
|
2713
|
-
workspacePath: this.workspace(),
|
|
2714
|
-
},
|
|
2715
|
-
global: uniqueSkills(globalSkills),
|
|
2716
|
-
workspace: uniqueSkills(workspaceSkills),
|
|
2717
|
-
plugin: uniqueSkills(pluginSkills),
|
|
2718
|
-
tools: daemonSkillTools(),
|
|
2719
|
-
};
|
|
2720
2880
|
}
|
|
2721
2881
|
|
|
2722
2882
|
sendStatus(status, activity = null) {
|
|
@@ -3347,6 +3507,7 @@ class ClaudeAgentSession {
|
|
|
3347
3507
|
|
|
3348
3508
|
async prepare() {
|
|
3349
3509
|
await mkdir(this.workspace(), { recursive: true });
|
|
3510
|
+
await ensureWorkspaceSkillsDir(this.workspace(), path.join(this.agentDir(), 'codex-home'), this.agent);
|
|
3350
3511
|
await ensureDaemonAgentWorkspaceRoot(this.agentDir(), this.agent);
|
|
3351
3512
|
await prepareRuntimeHooks({
|
|
3352
3513
|
agentDir: this.agentDir(),
|
|
@@ -3357,6 +3518,8 @@ class ClaudeAgentSession {
|
|
|
3357
3518
|
'# MagClaw Remote Claude Agent Workspace',
|
|
3358
3519
|
'',
|
|
3359
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.',
|
|
3360
3523
|
'',
|
|
3361
3524
|
].join('\n'));
|
|
3362
3525
|
}
|
|
@@ -3369,6 +3532,15 @@ class ClaudeAgentSession {
|
|
|
3369
3532
|
return readDaemonAgentWorkspaceFile(this.agentDir(), this.agent, relPath);
|
|
3370
3533
|
}
|
|
3371
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
|
+
|
|
3372
3544
|
sendStatus(status, activity = null) {
|
|
3373
3545
|
this.status = status;
|
|
3374
3546
|
this.onStatusChange(this, status);
|
|
@@ -4724,8 +4896,12 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4724
4896
|
|| previousService.packageBin
|
|
4725
4897
|
|| packageBinForPackageName(packageName),
|
|
4726
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));
|
|
4727
4903
|
const service = await writeServiceState(paths.profile, {
|
|
4728
|
-
mode:
|
|
4904
|
+
mode: serviceMode,
|
|
4729
4905
|
background: true,
|
|
4730
4906
|
launcher,
|
|
4731
4907
|
packageSpec,
|
|
@@ -4808,6 +4984,72 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4808
4984
|
return launcher;
|
|
4809
4985
|
}
|
|
4810
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
|
+
|
|
4811
5053
|
function launchAgentLabel(profile) {
|
|
4812
5054
|
return `ai.magclaw.daemon.${safeProfileName(profile)}`;
|
|
4813
5055
|
}
|
|
@@ -4823,6 +5065,61 @@ async function ensureExecutable(file) {
|
|
|
4823
5065
|
await chmod(file, 0o755).catch(() => {});
|
|
4824
5066
|
}
|
|
4825
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
|
+
|
|
4826
5123
|
async function startMacBackground(profile, env = process.env) {
|
|
4827
5124
|
const paths = profilePaths(profile, env);
|
|
4828
5125
|
const label = launchAgentLabel(paths.profile);
|
|
@@ -4933,6 +5230,11 @@ async function startWindowsBackground(profile, env = process.env) {
|
|
|
4933
5230
|
}
|
|
4934
5231
|
|
|
4935
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);
|
|
4936
5238
|
if (process.platform === 'darwin') return startMacBackground(profile, env);
|
|
4937
5239
|
if (process.platform === 'linux') return startLinuxBackground(profile, env);
|
|
4938
5240
|
if (process.platform === 'win32') return startWindowsBackground(profile, env);
|
|
@@ -4960,6 +5262,25 @@ export function parseLaunchdPrintStatus(result = {}) {
|
|
|
4960
5262
|
|
|
4961
5263
|
function backgroundServiceStatus(profile, env = process.env) {
|
|
4962
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
|
+
}
|
|
4963
5284
|
if (process.platform === 'darwin') {
|
|
4964
5285
|
const label = launchAgentLabel(paths.profile);
|
|
4965
5286
|
const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
@@ -5080,9 +5401,77 @@ async function stopActiveDaemon(profile, env = process.env) {
|
|
|
5080
5401
|
return { ok: stopped, running: !stopped, pid, signal };
|
|
5081
5402
|
}
|
|
5082
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
|
+
|
|
5083
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
|
+
}
|
|
5084
5474
|
if (process.platform === 'darwin') {
|
|
5085
|
-
const paths = profilePaths(profile, env);
|
|
5086
5475
|
const label = launchAgentLabel(paths.profile);
|
|
5087
5476
|
const serviceTarget = `gui/${process.getuid()}/${label}`;
|
|
5088
5477
|
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
@@ -5569,7 +5958,10 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
5569
5958
|
|
|
5570
5959
|
await emitProgress({ status: 'upgrading', phase: 'stage_service', progress: 45, message: 'Staging service launcher.' });
|
|
5571
5960
|
await writeServiceState(profile, {
|
|
5572
|
-
mode:
|
|
5961
|
+
mode: normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5962
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
5963
|
+
|| normalizeBackgroundServiceMode(serviceBefore.mode)
|
|
5964
|
+
|| backgroundServiceModeForPlatform(process.platform),
|
|
5573
5965
|
background: true,
|
|
5574
5966
|
packageSpec,
|
|
5575
5967
|
packageName,
|