@magclaw/cli-core 0.1.33 → 0.1.35
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 +970 -104
- package/src/mcp-bridge.js +72 -3
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';
|
|
@@ -61,6 +61,79 @@ function now() {
|
|
|
61
61
|
return new Date().toISOString();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function claudeStreamEvents(raw) {
|
|
65
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
66
|
+
const event = raw;
|
|
67
|
+
const output = [];
|
|
68
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
69
|
+
output.push({
|
|
70
|
+
type: 'system',
|
|
71
|
+
sessionId: event.session_id || event.sessionId || '',
|
|
72
|
+
model: event.model || '',
|
|
73
|
+
cwd: event.cwd || '',
|
|
74
|
+
});
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
const content = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
78
|
+
if (event.type === 'assistant') {
|
|
79
|
+
for (const block of content) {
|
|
80
|
+
if (block?.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
81
|
+
output.push({ type: 'text', delta: block.text });
|
|
82
|
+
} else if (block?.type === 'thinking' && typeof block.thinking === 'string' && block.thinking) {
|
|
83
|
+
output.push({ type: 'thinking', delta: block.thinking });
|
|
84
|
+
} else if (block?.type === 'tool_use' && block.id && block.name) {
|
|
85
|
+
output.push({ type: 'tool_use', id: block.id, name: block.name, input: block.input });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
90
|
+
if (event.type === 'user') {
|
|
91
|
+
for (const block of content) {
|
|
92
|
+
if (block?.type === 'tool_result' && block.tool_use_id) {
|
|
93
|
+
output.push({
|
|
94
|
+
type: 'tool_result',
|
|
95
|
+
id: block.tool_use_id,
|
|
96
|
+
output: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
|
|
97
|
+
isError: block.is_error === true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
if (event.type === 'result') {
|
|
104
|
+
if (event.usage) {
|
|
105
|
+
output.push({
|
|
106
|
+
type: 'usage',
|
|
107
|
+
inputTokens: event.usage.input_tokens,
|
|
108
|
+
outputTokens: event.usage.output_tokens,
|
|
109
|
+
costUsd: event.total_cost_usd,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
output.push({ type: 'done', sessionId: event.session_id || event.sessionId || '' });
|
|
113
|
+
}
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function claudeToolActivityDetail(event) {
|
|
118
|
+
if (!event || typeof event !== 'object') return 'Claude Code activity';
|
|
119
|
+
if (event.type === 'tool_use') return `Claude Code using ${event.name || 'tool'}`;
|
|
120
|
+
if (event.type === 'tool_result') return event.isError ? 'Claude Code tool returned an error' : 'Claude Code tool completed';
|
|
121
|
+
if (event.type === 'thinking') return 'Claude Code thinking';
|
|
122
|
+
if (event.type === 'usage') return `Claude Code usage input=${event.inputTokens || 0} output=${event.outputTokens || 0}`;
|
|
123
|
+
return 'Claude Code activity';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function codexStderrRuntimeError(text = '') {
|
|
127
|
+
const detail = String(text || '').trim();
|
|
128
|
+
if (!detail) return '';
|
|
129
|
+
const lower = detail.toLowerCase();
|
|
130
|
+
if (lower.includes('responses_websocket') && lower.includes('error')) return detail.slice(0, 2000);
|
|
131
|
+
if (lower.includes('failed to connect to websocket') && lower.includes('/v1/responses')) return detail.slice(0, 2000);
|
|
132
|
+
if (lower.includes('authentication') && lower.includes('openai')) return detail.slice(0, 2000);
|
|
133
|
+
if (lower.includes('not logged in') || lower.includes('login is required')) return detail.slice(0, 2000);
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
function packageInfoFromSpec(packageSpec = '') {
|
|
65
138
|
const match = String(packageSpec || '').trim().match(/^(@magclaw\/(?:daemon|computer))(?:@(.+))?$/);
|
|
66
139
|
return {
|
|
@@ -489,6 +562,38 @@ export async function activeComputerLock(env = process.env) {
|
|
|
489
562
|
return activeLockFile(paths.lockFile, { scope: 'computer' });
|
|
490
563
|
}
|
|
491
564
|
|
|
565
|
+
function readJsonFileSync(file, fallback = {}) {
|
|
566
|
+
if (!existsSync(file)) return fallback;
|
|
567
|
+
try {
|
|
568
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
569
|
+
} catch {
|
|
570
|
+
return fallback;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function writeJsonFileSync(file, value) {
|
|
575
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
576
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function readServiceStateSync(profile = DEFAULT_PROFILE, env = process.env) {
|
|
580
|
+
const paths = profilePaths(profile, env);
|
|
581
|
+
return readJsonFileSync(paths.service, {});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function activeDaemonLockSync(profile = DEFAULT_PROFILE, env = process.env) {
|
|
585
|
+
const paths = profilePaths(profile, env);
|
|
586
|
+
const lock = readJsonFileSync(paths.lockFile, null);
|
|
587
|
+
if (lock?.pid && pidIsRunning(lock.pid)) {
|
|
588
|
+
return {
|
|
589
|
+
profile: paths.profile,
|
|
590
|
+
...lock,
|
|
591
|
+
lockFile: paths.lockFile,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
492
597
|
async function writeLockFile(file, lock) {
|
|
493
598
|
const handle = await open(file, 'wx');
|
|
494
599
|
try {
|
|
@@ -730,7 +835,7 @@ function renderComputerHelp(subcommand = '') {
|
|
|
730
835
|
' --dry-run Preview upgrade actions',
|
|
731
836
|
' --channel <name> latest | alpha | pinned:<semver>',
|
|
732
837
|
' --target-version <semver> Explicit target version',
|
|
733
|
-
' --force Accepted for
|
|
838
|
+
' --force Accepted for MagClaw compatibility; currently maps to the normal upgrade path',
|
|
734
839
|
],
|
|
735
840
|
};
|
|
736
841
|
if (command && usage[command]) return `${usage[command].join('\n')}\n`;
|
|
@@ -873,10 +978,24 @@ function backgroundServiceModeForPlatform(platform = process.platform) {
|
|
|
873
978
|
return 'foreground';
|
|
874
979
|
}
|
|
875
980
|
|
|
981
|
+
function normalizeBackgroundServiceMode(value = '') {
|
|
982
|
+
const mode = String(value || '').trim().toLowerCase();
|
|
983
|
+
if (['container', 'k8s', 'kubernetes', 'pod'].includes(mode)) return 'container';
|
|
984
|
+
if (['launchd', 'systemd', 'schtasks', 'foreground'].includes(mode)) return mode;
|
|
985
|
+
return '';
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function requestedBackgroundServiceMode(env = process.env, platform = process.platform) {
|
|
989
|
+
return normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
990
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
991
|
+
|| backgroundServiceModeForPlatform(platform);
|
|
992
|
+
}
|
|
993
|
+
|
|
876
994
|
export function serviceStatePatchForDaemonRun(service = {}, env = process.env, platform = process.platform) {
|
|
877
995
|
if (daemonRunLaunchedByBackgroundService(env)) {
|
|
996
|
+
const serviceMode = normalizeBackgroundServiceMode(service.mode);
|
|
878
997
|
return {
|
|
879
|
-
mode:
|
|
998
|
+
mode: serviceMode && serviceMode !== 'foreground' ? serviceMode : requestedBackgroundServiceMode(env, platform),
|
|
880
999
|
background: true,
|
|
881
1000
|
};
|
|
882
1001
|
}
|
|
@@ -1675,6 +1794,60 @@ function contextSnippet(value, limit = 240) {
|
|
|
1675
1794
|
return text.length >= limit ? `${text.slice(0, Math.max(0, limit - 3)).trim()}...` : text;
|
|
1676
1795
|
}
|
|
1677
1796
|
|
|
1797
|
+
function contextImageMimeFromName(value = '') {
|
|
1798
|
+
const name = String(value || '').toLowerCase().split(/[?#]/)[0];
|
|
1799
|
+
if (name.endsWith('.png')) return 'image/png';
|
|
1800
|
+
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) return 'image/jpeg';
|
|
1801
|
+
if (name.endsWith('.webp')) return 'image/webp';
|
|
1802
|
+
if (name.endsWith('.gif')) return 'image/gif';
|
|
1803
|
+
if (name.endsWith('.svg')) return 'image/svg+xml';
|
|
1804
|
+
return '';
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function contextDataImageUrl(value = '') {
|
|
1808
|
+
const text = String(value || '').trim();
|
|
1809
|
+
return /^data:image\//i.test(text) ? text : '';
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function contextImageType(reference = {}) {
|
|
1813
|
+
const explicit = String(reference.type || reference.mime || reference.mimeType || '').toLowerCase();
|
|
1814
|
+
if (explicit.startsWith('image/')) return explicit;
|
|
1815
|
+
const data = contextDataImageUrl(reference.dataUrl || reference.url || reference.downloadUrl);
|
|
1816
|
+
if (data) return data.match(/^data:([^;,]+)[;,]/i)?.[1]?.toLowerCase() || 'image';
|
|
1817
|
+
return contextImageMimeFromName(reference.name || reference.filename || reference.url || reference.downloadUrl || reference.path || reference.description);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function isContextImageReference(reference = {}) {
|
|
1821
|
+
return contextImageType(reference).startsWith('image/');
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function remoteImageUrl(value = '', serverUrl = '', fallbackPath = '') {
|
|
1825
|
+
const raw = String(value || '').trim();
|
|
1826
|
+
if (contextDataImageUrl(raw)) return raw;
|
|
1827
|
+
const base = String(serverUrl || DEFAULT_SERVER_URL).replace(/\/+$/, '');
|
|
1828
|
+
const candidate = raw || String(fallbackPath || '').trim();
|
|
1829
|
+
if (!candidate) return '';
|
|
1830
|
+
if (candidate.startsWith('/')) {
|
|
1831
|
+
try {
|
|
1832
|
+
return new URL(candidate, base).toString();
|
|
1833
|
+
} catch {
|
|
1834
|
+
return '';
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (/^https?:\/\//i.test(candidate)) {
|
|
1838
|
+
try {
|
|
1839
|
+
const parsed = new URL(candidate);
|
|
1840
|
+
if (['0.0.0.0', '127.0.0.1', 'localhost', '::1'].includes(parsed.hostname) && base) {
|
|
1841
|
+
return new URL(`${parsed.pathname}${parsed.search}${parsed.hash}`, base).toString();
|
|
1842
|
+
}
|
|
1843
|
+
return parsed.toString();
|
|
1844
|
+
} catch {
|
|
1845
|
+
return '';
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return '';
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1678
1851
|
function contextActorName(pack, id) {
|
|
1679
1852
|
const value = String(id || '').trim();
|
|
1680
1853
|
if (!value) return 'unknown';
|
|
@@ -1721,6 +1894,50 @@ function renderContextTasks(pack) {
|
|
|
1721
1894
|
}).join('\n');
|
|
1722
1895
|
}
|
|
1723
1896
|
|
|
1897
|
+
function renderContextAttachments(pack) {
|
|
1898
|
+
const attachments = contextArray(pack?.attachments);
|
|
1899
|
+
if (!attachments.length) return '- (none)';
|
|
1900
|
+
return attachments.map((item) => {
|
|
1901
|
+
const details = [
|
|
1902
|
+
item.id ? `id=${item.id}` : '',
|
|
1903
|
+
item.messageId ? `from msg=${item.messageId}` : '',
|
|
1904
|
+
item.source ? `source=${item.source}` : '',
|
|
1905
|
+
item.path ? `path=${item.path}` : '',
|
|
1906
|
+
item.url ? `url=${item.url}` : '',
|
|
1907
|
+
item.id ? `tool=read_attachment(attachmentId="${item.id}")` : '',
|
|
1908
|
+
].filter(Boolean).join(', ');
|
|
1909
|
+
return `- ${item.name || item.filename || item.id || 'attachment'} ${item.type || item.mime || 'file'} ${Number(item.bytes || item.sizeBytes || 0)} bytes${details ? ` (${details})` : ''}`;
|
|
1910
|
+
}).join('\n');
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function renderContextTargetAgentAvatar(pack) {
|
|
1914
|
+
const avatar = pack?.targetAgent?.avatar;
|
|
1915
|
+
if (!avatar || avatar.kind === 'none') return '';
|
|
1916
|
+
const description = avatar.description ? ` (${avatar.description})` : '';
|
|
1917
|
+
if (avatar.visualInput !== false && isContextImageReference(avatar)) {
|
|
1918
|
+
return `- Your profile avatar: image supplied as visual input${description}. Use it when the user asks what your avatar shows.`;
|
|
1919
|
+
}
|
|
1920
|
+
return `- Your profile avatar: ${avatar.description || 'configured'}, but no visual input is available.`;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function contextParticipantAvatarVisualInputs(pack, targetAgentId = '') {
|
|
1924
|
+
return compactContextParticipants(pack, targetAgentId).selected.filter((item) => (
|
|
1925
|
+
item.id !== targetAgentId
|
|
1926
|
+
&& item.type === 'agent'
|
|
1927
|
+
&& item.avatar
|
|
1928
|
+
&& item.avatar.kind !== 'none'
|
|
1929
|
+
&& item.avatar.visualInput !== false
|
|
1930
|
+
&& isContextImageReference(item.avatar)
|
|
1931
|
+
));
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function renderContextParticipantAvatarInputs(pack, targetAgentId = '') {
|
|
1935
|
+
const visible = contextParticipantAvatarVisualInputs(pack, targetAgentId);
|
|
1936
|
+
if (!visible.length) return '';
|
|
1937
|
+
const names = visible.map((item) => `@${item.name || item.id}`).join(', ');
|
|
1938
|
+
return `- Participant avatar visual inputs: ${names}. Use these when comparing an uploaded image to another Agent avatar; call read_agent_avatar if the relevant Agent is omitted.`;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1724
1941
|
function renderContextEventMembers(pack, event = {}) {
|
|
1725
1942
|
const ids = [
|
|
1726
1943
|
...contextArray(event.memberIds),
|
|
@@ -1843,6 +2060,8 @@ function renderRemoteAgentContextPack(pack, targetAgentId = '') {
|
|
|
1843
2060
|
`- Space: ${pack.space?.type || 'space'} (${pack.space?.visibility || 'public'}${pack.space?.defaultChannel ? ', default workspace channel' : ''})`,
|
|
1844
2061
|
pack.space?.description ? `- Channel description: ${contextSnippet(pack.space.description, 180)}` : '',
|
|
1845
2062
|
`- Participants shown: ${participants.selected.map((item) => renderContextParticipant(item, targetAgentId)).join(', ') || '(none)'}`,
|
|
2063
|
+
renderContextTargetAgentAvatar(pack),
|
|
2064
|
+
renderContextParticipantAvatarInputs(pack, targetAgentId),
|
|
1846
2065
|
participants.omitted ? `- Participants omitted: ${participants.omitted}. Use list_agents/read_agent_profile or search_agent_memory when a broader roster or specialties matter.` : '',
|
|
1847
2066
|
pack.space?.type === 'channel' && !pack.space?.defaultChannel ? renderContextSuggestedMembers(pack) : '',
|
|
1848
2067
|
'',
|
|
@@ -1871,9 +2090,12 @@ function renderRemoteAgentContextPack(pack, targetAgentId = '') {
|
|
|
1871
2090
|
'Relevant tasks:',
|
|
1872
2091
|
renderContextTasks(pack),
|
|
1873
2092
|
'',
|
|
2093
|
+
'Visible attachment metadata and original-file tools:',
|
|
2094
|
+
renderContextAttachments(pack),
|
|
2095
|
+
'',
|
|
1874
2096
|
renderContextPeerMemory(pack),
|
|
1875
2097
|
'',
|
|
1876
|
-
'Progressive context tools: list_agents, read_agent_profile, read_history, search_messages, search_agent_memory, read_agent_memory, read_agent_file, and list_tasks are available through MagClaw MCP.',
|
|
2098
|
+
'Progressive context tools: list_agents, read_agent_profile, read_agent_avatar, read_history, search_messages, list_attachments, read_attachment, search_agent_memory, read_agent_memory, read_agent_file, and list_tasks are available through MagClaw MCP.',
|
|
1877
2099
|
'For "who can we bring in" or agent suitability questions, use the server member list above first; call list_agents without a target for the server-wide agent roster, because target filters to the current channel.',
|
|
1878
2100
|
'For agent capability or specialty questions, use peer memory first; if memory is empty or weak, search_messages/read_history for earlier user role assignments before saying the fact is unknown.',
|
|
1879
2101
|
'Use this compact snapshot first. Call the tools only when the answer depends on omitted participants, deeper history, memory, or task details.',
|
|
@@ -1974,11 +2196,14 @@ function canonicalMagClawToolName(name) {
|
|
|
1974
2196
|
'send_message',
|
|
1975
2197
|
'read_history',
|
|
1976
2198
|
'search_messages',
|
|
2199
|
+
'list_attachments',
|
|
2200
|
+
'read_attachment',
|
|
1977
2201
|
'search_agent_memory',
|
|
1978
2202
|
'read_agent_memory',
|
|
1979
2203
|
'read_agent_file',
|
|
1980
2204
|
'list_agents',
|
|
1981
2205
|
'read_agent_profile',
|
|
2206
|
+
'read_agent_avatar',
|
|
1982
2207
|
'write_memory',
|
|
1983
2208
|
'list_tasks',
|
|
1984
2209
|
'create_tasks',
|
|
@@ -2153,32 +2378,152 @@ async function globalSkillRoots() {
|
|
|
2153
2378
|
return roots;
|
|
2154
2379
|
}
|
|
2155
2380
|
|
|
2156
|
-
|
|
2381
|
+
function pathIsWithinResolvedRoots(resolvedPath, roots = []) {
|
|
2382
|
+
const cleanPath = path.resolve(resolvedPath);
|
|
2383
|
+
return roots.some((root) => cleanPath === root || cleanPath.startsWith(`${root}${path.sep}`));
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
async function resolvedRoots(roots = []) {
|
|
2387
|
+
const resolved = [];
|
|
2388
|
+
for (const root of roots) {
|
|
2389
|
+
const logical = path.resolve(root);
|
|
2390
|
+
if (!resolved.includes(logical)) resolved.push(logical);
|
|
2391
|
+
const physical = await realpath(root).catch(() => logical);
|
|
2392
|
+
if (!resolved.includes(physical)) resolved.push(physical);
|
|
2393
|
+
}
|
|
2394
|
+
return resolved;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
async function ensureWorkspaceSkillsDir(workspace, codexHome = '', agent = {}) {
|
|
2398
|
+
const workspaceSkills = path.join(workspace, 'skills');
|
|
2399
|
+
const legacyGeneratedSkills = codexHome ? path.join(codexHome, 'skills') : '';
|
|
2400
|
+
await mkdir(path.dirname(workspaceSkills), { recursive: true });
|
|
2401
|
+
try {
|
|
2402
|
+
const existing = await lstat(workspaceSkills);
|
|
2403
|
+
if (existing.isSymbolicLink()) {
|
|
2404
|
+
const current = await readlink(workspaceSkills);
|
|
2405
|
+
const resolved = path.resolve(path.dirname(workspaceSkills), current);
|
|
2406
|
+
if (legacyGeneratedSkills && resolved === path.resolve(legacyGeneratedSkills)) {
|
|
2407
|
+
await unlink(workspaceSkills);
|
|
2408
|
+
await mkdir(workspaceSkills, { recursive: true });
|
|
2409
|
+
logInfo('skills', `Repaired workspace skills directory for agent ${agent.id || 'unknown'}.`);
|
|
2410
|
+
} else {
|
|
2411
|
+
logWarning('skills', `Workspace skills path for agent ${agent.id || 'unknown'} is a custom symlink; leaving it untouched.`);
|
|
2412
|
+
}
|
|
2413
|
+
} else if (!existing.isDirectory()) {
|
|
2414
|
+
logWarning('skills', `Workspace skills path for agent ${agent.id || 'unknown'} is not a directory; leaving it untouched.`);
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
if (error.code !== 'ENOENT') throw error;
|
|
2419
|
+
await mkdir(workspaceSkills, { recursive: true });
|
|
2420
|
+
}
|
|
2421
|
+
return workspaceSkills;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
async function migrateLegacyAgentSkills(codexHome, workspaceSkills, globalResolvedRoots, agent = {}) {
|
|
2425
|
+
const codexSkillsRoot = path.join(codexHome, 'skills');
|
|
2426
|
+
if (!workspaceSkills || !existsSync(codexSkillsRoot)) return;
|
|
2427
|
+
const entries = await readdir(codexSkillsRoot, { withFileTypes: true }).catch(() => []);
|
|
2428
|
+
for (const entry of entries) {
|
|
2429
|
+
if (entry.name.startsWith('.') || entry.name === '.system') continue;
|
|
2430
|
+
const source = path.join(codexSkillsRoot, entry.name);
|
|
2431
|
+
const target = path.join(workspaceSkills, entry.name);
|
|
2432
|
+
if (existsSync(target)) continue;
|
|
2433
|
+
const sourceInfo = await lstat(source).catch(() => null);
|
|
2434
|
+
if (!sourceInfo) continue;
|
|
2435
|
+
try {
|
|
2436
|
+
if (sourceInfo.isSymbolicLink()) {
|
|
2437
|
+
const current = await readlink(source);
|
|
2438
|
+
const resolved = path.resolve(path.dirname(source), current);
|
|
2439
|
+
if (pathIsWithinResolvedRoots(resolved, globalResolvedRoots)) continue;
|
|
2440
|
+
const realTarget = await realpath(resolved).catch(() => resolved);
|
|
2441
|
+
if (pathIsWithinResolvedRoots(realTarget, globalResolvedRoots)) continue;
|
|
2442
|
+
const targetInfo = await stat(realTarget).catch(() => null);
|
|
2443
|
+
if (!targetInfo) continue;
|
|
2444
|
+
await symlink(realTarget, target, targetInfo.isDirectory() ? 'dir' : 'file');
|
|
2445
|
+
await unlink(source);
|
|
2446
|
+
} else {
|
|
2447
|
+
await rename(source, target);
|
|
2448
|
+
}
|
|
2449
|
+
logInfo('skills', `Migrated legacy local skill ${entry.name} for agent ${agent.id || 'unknown'}.`);
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
logWarning('skills', `Could not migrate legacy local skill ${entry.name} for agent ${agent.id || 'unknown'}: ${error.message}`);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
async function linkRuntimeSkillEntry(source, target, agent = {}) {
|
|
2457
|
+
const linked = await linkPathEntry(source, target);
|
|
2458
|
+
if (linked) return true;
|
|
2459
|
+
const existing = await lstat(target).catch(() => null);
|
|
2460
|
+
if (existing && !existing.isSymbolicLink() && path.resolve(source) !== path.resolve(target)) {
|
|
2461
|
+
logWarning('skills', `Could not link skill ${path.basename(target)} for agent ${agent.id || 'unknown'} because the runtime path is not a symlink.`);
|
|
2462
|
+
}
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
async function linkSkillRootEntries(sourceRoot, targetRoot, agent = {}, { includeSystem = false } = {}) {
|
|
2467
|
+
const desired = new Set();
|
|
2468
|
+
if (!sourceRoot || !existsSync(sourceRoot)) return desired;
|
|
2469
|
+
const entries = await readdir(sourceRoot, { withFileTypes: true }).catch(() => []);
|
|
2470
|
+
for (const entry of entries) {
|
|
2471
|
+
if (entry.name.startsWith('.') && !(includeSystem && entry.name === '.system')) continue;
|
|
2472
|
+
const source = path.join(sourceRoot, entry.name);
|
|
2473
|
+
if (includeSystem && entry.name === '.system' && (entry.isDirectory() || entry.isSymbolicLink())) {
|
|
2474
|
+
desired.add(entry.name);
|
|
2475
|
+
const targetSystemRoot = path.join(targetRoot, '.system');
|
|
2476
|
+
await mkdir(targetSystemRoot, { recursive: true });
|
|
2477
|
+
const systemEntries = await readdir(source, { withFileTypes: true }).catch(() => []);
|
|
2478
|
+
for (const systemEntry of systemEntries) {
|
|
2479
|
+
if (!systemEntry.isDirectory() && !systemEntry.isSymbolicLink() && !systemEntry.isFile()) continue;
|
|
2480
|
+
const systemSource = path.join(source, systemEntry.name);
|
|
2481
|
+
const systemTarget = path.join(targetSystemRoot, systemEntry.name);
|
|
2482
|
+
await linkRuntimeSkillEntry(systemSource, systemTarget, agent).catch((error) => {
|
|
2483
|
+
logWarning('skills', `Could not link system skill ${systemEntry.name} for agent ${agent.id || 'unknown'}: ${error.message}`);
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.isFile()) continue;
|
|
2489
|
+
desired.add(entry.name);
|
|
2490
|
+
const target = path.join(targetRoot, entry.name);
|
|
2491
|
+
await linkRuntimeSkillEntry(source, target, agent).catch((error) => {
|
|
2492
|
+
logWarning('skills', `Could not link skill ${entry.name} for agent ${agent.id || 'unknown'}: ${error.message}`);
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
return desired;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
async function pruneGeneratedSkillLinks(targetSkillsRoot, desiredNames, agent = {}) {
|
|
2499
|
+
const entries = await readdir(targetSkillsRoot, { withFileTypes: true }).catch(() => []);
|
|
2500
|
+
for (const entry of entries) {
|
|
2501
|
+
if (entry.name.startsWith('.') && entry.name !== '.system') continue;
|
|
2502
|
+
if (desiredNames.has(entry.name)) continue;
|
|
2503
|
+
const target = path.join(targetSkillsRoot, entry.name);
|
|
2504
|
+
const existing = await lstat(target).catch(() => null);
|
|
2505
|
+
if (!existing) continue;
|
|
2506
|
+
if (existing.isSymbolicLink()) {
|
|
2507
|
+
await unlink(target);
|
|
2508
|
+
} else if (entry.name !== '.system') {
|
|
2509
|
+
logWarning('skills', `Stale runtime skill ${entry.name} for agent ${agent.id || 'unknown'} was not removed because it is not a symlink.`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
async function syncGlobalSkillsIntoAgentHome(codexHome, workspace, agent = {}) {
|
|
2157
2515
|
const targetSkillsRoot = path.join(codexHome, 'skills');
|
|
2158
2516
|
await mkdir(targetSkillsRoot, { recursive: true });
|
|
2159
2517
|
const roots = await globalSkillRoots();
|
|
2518
|
+
const globalResolvedRoots = await resolvedRoots(roots);
|
|
2519
|
+
const workspaceSkills = await ensureWorkspaceSkillsDir(workspace, codexHome, agent);
|
|
2520
|
+
await migrateLegacyAgentSkills(codexHome, workspaceSkills, globalResolvedRoots, agent);
|
|
2521
|
+
const desiredNames = new Set();
|
|
2160
2522
|
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
|
-
}
|
|
2523
|
+
for (const name of await linkSkillRootEntries(sourceSkillsRoot, targetSkillsRoot, agent, { includeSystem: true })) desiredNames.add(name);
|
|
2178
2524
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
await linkPathEntry(targetSkillsRoot, workspaceSkillsLink).catch(() => {});
|
|
2525
|
+
for (const name of await linkSkillRootEntries(workspaceSkills, targetSkillsRoot, agent)) desiredNames.add(name);
|
|
2526
|
+
await pruneGeneratedSkillLinks(targetSkillsRoot, desiredNames, agent);
|
|
2182
2527
|
}
|
|
2183
2528
|
|
|
2184
2529
|
function firstFrontmatterValue(content, keys) {
|
|
@@ -2231,11 +2576,12 @@ function shortenSkillPath(absPath, { agentRoot = '', codexHome = '' } = {}) {
|
|
|
2231
2576
|
async function parseSkillFile(filePath, scope, context = {}) {
|
|
2232
2577
|
const content = await readFile(filePath, 'utf8').catch(() => '');
|
|
2233
2578
|
const resolvedFilePath = await realpath(filePath).catch(() => filePath);
|
|
2579
|
+
const logicalFilePath = path.resolve(filePath);
|
|
2234
2580
|
const name = firstFrontmatterValue(content, ['name', 'title']) || skillNameFromPath(filePath);
|
|
2235
2581
|
const description = firstFrontmatterValue(content, ['description', 'summary', 'short_description', 'short-description'])
|
|
2236
2582
|
|| firstMarkdownParagraph(content)
|
|
2237
2583
|
|| 'No description provided.';
|
|
2238
|
-
const shortPath = shortenSkillPath(
|
|
2584
|
+
const shortPath = shortenSkillPath(logicalFilePath, context);
|
|
2239
2585
|
return {
|
|
2240
2586
|
id: `${scope}:${shortPath}`,
|
|
2241
2587
|
name,
|
|
@@ -2326,15 +2672,6 @@ async function findPluginSkillFiles(root, { maxEntries = 400 } = {}) {
|
|
|
2326
2672
|
return found;
|
|
2327
2673
|
}
|
|
2328
2674
|
|
|
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
2675
|
function uniqueSkills(items) {
|
|
2339
2676
|
const seen = new Set();
|
|
2340
2677
|
return items.filter((item) => {
|
|
@@ -2350,11 +2687,14 @@ function daemonSkillTools() {
|
|
|
2350
2687
|
'send_message',
|
|
2351
2688
|
'read_history',
|
|
2352
2689
|
'search_messages',
|
|
2690
|
+
'list_attachments',
|
|
2691
|
+
'read_attachment',
|
|
2353
2692
|
'search_agent_memory',
|
|
2354
2693
|
'read_agent_memory',
|
|
2355
2694
|
'read_agent_file',
|
|
2356
2695
|
'list_agents',
|
|
2357
2696
|
'read_agent_profile',
|
|
2697
|
+
'read_agent_avatar',
|
|
2358
2698
|
'write_memory',
|
|
2359
2699
|
'list_tasks',
|
|
2360
2700
|
'create_tasks',
|
|
@@ -2366,6 +2706,35 @@ function daemonSkillTools() {
|
|
|
2366
2706
|
];
|
|
2367
2707
|
}
|
|
2368
2708
|
|
|
2709
|
+
async function listDaemonAgentSkills({ agent, agentDir, workspace, codexHome = '' }) {
|
|
2710
|
+
const context = { agentRoot: agentDir, codexHome };
|
|
2711
|
+
const roots = await globalSkillRoots();
|
|
2712
|
+
const globalSkills = [];
|
|
2713
|
+
for (const root of roots) globalSkills.push(...await scanSkillsDir(root, 'global', context));
|
|
2714
|
+
const agentRoots = [
|
|
2715
|
+
path.join(workspace, 'skills'),
|
|
2716
|
+
path.join(agentDir, '.codex', 'skills'),
|
|
2717
|
+
path.join(agentDir, '.agents', 'skills'),
|
|
2718
|
+
];
|
|
2719
|
+
const agentSkills = [];
|
|
2720
|
+
for (const root of agentRoots) agentSkills.push(...await scanSkillsDir(root, 'agent', context));
|
|
2721
|
+
const pluginFiles = await findPluginSkillFiles(path.join(SOURCE_CODEX_HOME, 'plugins', 'cache'));
|
|
2722
|
+
const pluginSkills = [];
|
|
2723
|
+
for (const file of pluginFiles) pluginSkills.push(await parseSkillFile(file, 'plugin', context));
|
|
2724
|
+
return {
|
|
2725
|
+
agent: {
|
|
2726
|
+
id: agent.id,
|
|
2727
|
+
name: agent.name || agent.id,
|
|
2728
|
+
codexHome: codexHome || undefined,
|
|
2729
|
+
workspacePath: workspace,
|
|
2730
|
+
},
|
|
2731
|
+
global: uniqueSkills(globalSkills),
|
|
2732
|
+
workspace: uniqueSkills(agentSkills),
|
|
2733
|
+
plugin: uniqueSkills(pluginSkills),
|
|
2734
|
+
tools: daemonSkillTools(),
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2369
2738
|
const DAEMON_PROGRESSIVE_DISCLOSURE_SECTION = [
|
|
2370
2739
|
'## 渐进式披露',
|
|
2371
2740
|
'- 其他 Agent 默认只会先读取本文件;不要假设它们已经看到 `notes/` 或 `workspace/` 中的详细文件。',
|
|
@@ -2621,9 +2990,12 @@ class CodexAgentSession {
|
|
|
2621
2990
|
this.completedToolCallIds = new Set();
|
|
2622
2991
|
this.activeTurnToolSignatures = new Set();
|
|
2623
2992
|
this.activeTurnUsedSendMessage = false;
|
|
2993
|
+
this.activeTurnSawResponseDelta = false;
|
|
2994
|
+
this.activeTurnDeltaItemIds = new Set();
|
|
2624
2995
|
this.codexMessageQueue = Promise.resolve();
|
|
2625
2996
|
this.streamActivityTimer = null;
|
|
2626
2997
|
this.pendingStreamActivity = null;
|
|
2998
|
+
this.lastRuntimeError = '';
|
|
2627
2999
|
this.trajectoryCoalesceMs = envInteger(this.env, 'MAGCLAW_DAEMON_TRAJECTORY_COALESCE_MS', DEFAULT_TRAJECTORY_COALESCE_MS, { min: 0, max: 5_000 });
|
|
2628
3000
|
}
|
|
2629
3001
|
|
|
@@ -2650,7 +3022,7 @@ class CodexAgentSession {
|
|
|
2650
3022
|
codexHome: this.codexHome(),
|
|
2651
3023
|
runtimeKind: 'codex',
|
|
2652
3024
|
});
|
|
2653
|
-
await syncGlobalSkillsIntoAgentHome(this.codexHome(), this.workspace());
|
|
3025
|
+
await syncGlobalSkillsIntoAgentHome(this.codexHome(), this.workspace(), this.agent);
|
|
2654
3026
|
await writeFile(path.join(this.codexHome(), 'config.toml'), [
|
|
2655
3027
|
'wire_api = "responses"',
|
|
2656
3028
|
'',
|
|
@@ -2668,8 +3040,8 @@ class CodexAgentSession {
|
|
|
2668
3040
|
'',
|
|
2669
3041
|
'This workspace is isolated for a MagClaw cloud-connected agent.',
|
|
2670
3042
|
'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
3043
|
'Agent-specific skills can be installed under `./skills/<skill-name>/SKILL.md`; this path belongs to this agent only.',
|
|
3044
|
+
'Runtime-generated adapter directories are not skill install targets.',
|
|
2673
3045
|
'',
|
|
2674
3046
|
].join('\n'));
|
|
2675
3047
|
}
|
|
@@ -2684,40 +3056,12 @@ class CodexAgentSession {
|
|
|
2684
3056
|
|
|
2685
3057
|
async listSkills() {
|
|
2686
3058
|
await this.prepare();
|
|
2687
|
-
|
|
2688
|
-
|
|
3059
|
+
return listDaemonAgentSkills({
|
|
3060
|
+
agent: this.agent,
|
|
3061
|
+
agentDir: this.agentDir(),
|
|
3062
|
+
workspace: this.workspace(),
|
|
2689
3063
|
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
3064
|
});
|
|
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
3065
|
}
|
|
2722
3066
|
|
|
2723
3067
|
sendStatus(status, activity = null) {
|
|
@@ -2801,6 +3145,38 @@ class CodexAgentSession {
|
|
|
2801
3145
|
if (payload) this.send(payload);
|
|
2802
3146
|
}
|
|
2803
3147
|
|
|
3148
|
+
async reportRuntimeError(errorText, rawText = '') {
|
|
3149
|
+
const error = String(errorText || rawText || 'Codex runtime error.').trim().slice(0, 2000);
|
|
3150
|
+
if (!error) return;
|
|
3151
|
+
if (this.status === 'error' && this.lastRuntimeError === error) return;
|
|
3152
|
+
this.lastRuntimeError = error;
|
|
3153
|
+
const activity = {
|
|
3154
|
+
source: 'codex-stderr',
|
|
3155
|
+
error,
|
|
3156
|
+
text: String(rawText || error).trim().slice(0, 2000),
|
|
3157
|
+
at: now(),
|
|
3158
|
+
};
|
|
3159
|
+
this.send({
|
|
3160
|
+
type: 'agent:error',
|
|
3161
|
+
commandId: this.activeDeliveryId || undefined,
|
|
3162
|
+
deliveryId: this.activeDeliveryId || null,
|
|
3163
|
+
agentId: this.agent.id,
|
|
3164
|
+
error,
|
|
3165
|
+
});
|
|
3166
|
+
if (this.activeDeliveryId) {
|
|
3167
|
+
await this.markDelivery(this.activeDeliveryId, 'failed', {
|
|
3168
|
+
agentId: this.agent.id,
|
|
3169
|
+
sessionKey: this.sessionKey || null,
|
|
3170
|
+
messageId: this.lastSourceMessage?.id || null,
|
|
3171
|
+
workItemId: this.lastSourceMessage?.workItemId || null,
|
|
3172
|
+
error,
|
|
3173
|
+
}).catch((markError) => {
|
|
3174
|
+
logWarning('daemon', `Failed to mark delivery ${this.activeDeliveryId} failed after Codex runtime error: ${markError.message}`);
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
this.sendStatus('error', activity);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2804
3180
|
async requestMagClawTool(pathname, { method = 'GET', query = {}, body = null } = {}) {
|
|
2805
3181
|
const url = new URL(`${this.serverUrl.replace(/\/+$/, '')}${pathname}`);
|
|
2806
3182
|
for (const [key, value] of Object.entries(query || {})) {
|
|
@@ -2839,6 +3215,92 @@ class CodexAgentSession {
|
|
|
2839
3215
|
}
|
|
2840
3216
|
}
|
|
2841
3217
|
|
|
3218
|
+
async readAttachmentImageInput(reference = {}) {
|
|
3219
|
+
const attachmentId = String(reference.id || reference.attachmentId || reference.attachment_id || '').trim();
|
|
3220
|
+
if (!attachmentId) return null;
|
|
3221
|
+
try {
|
|
3222
|
+
const data = await this.requestMagClawTool('/api/agent-tools/attachments/read', {
|
|
3223
|
+
query: {
|
|
3224
|
+
agentId: this.agent.id,
|
|
3225
|
+
attachmentId,
|
|
3226
|
+
maxBytes: 8 * 1024 * 1024,
|
|
3227
|
+
},
|
|
3228
|
+
});
|
|
3229
|
+
const dataUrl = contextDataImageUrl(data?.dataUrl);
|
|
3230
|
+
if (!dataUrl || data?.file?.truncated) return null;
|
|
3231
|
+
return {
|
|
3232
|
+
key: `attachment:${attachmentId}`,
|
|
3233
|
+
input: { type: 'image', url: dataUrl },
|
|
3234
|
+
};
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
logWarning('attachments', `Could not read image attachment ${attachmentId} for agent ${this.agent.id}: ${error.message}`);
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
async imageInputFromContextReference(reference = {}, { preferReadAttachment = false } = {}) {
|
|
3242
|
+
if (!isContextImageReference(reference)) return null;
|
|
3243
|
+
if (preferReadAttachment) {
|
|
3244
|
+
const resolved = await this.readAttachmentImageInput(reference);
|
|
3245
|
+
if (resolved) return resolved;
|
|
3246
|
+
}
|
|
3247
|
+
const dataUrl = contextDataImageUrl(reference.dataUrl || reference.url || reference.downloadUrl);
|
|
3248
|
+
if (dataUrl) {
|
|
3249
|
+
return {
|
|
3250
|
+
key: `url:${dataUrl}`,
|
|
3251
|
+
input: { type: 'image', url: dataUrl },
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
const url = remoteImageUrl(reference.url || reference.downloadUrl || '', this.serverUrl, reference.description);
|
|
3255
|
+
if (url) {
|
|
3256
|
+
return {
|
|
3257
|
+
key: `url:${url}`,
|
|
3258
|
+
input: { type: 'image', url },
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
const filePath = String(reference.path || '').trim();
|
|
3262
|
+
if (filePath && existsSync(filePath)) {
|
|
3263
|
+
return {
|
|
3264
|
+
key: `path:${filePath}`,
|
|
3265
|
+
input: { type: 'localImage', path: filePath },
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
return null;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
async imageInputsForDelivery(message = {}) {
|
|
3272
|
+
const inputs = [];
|
|
3273
|
+
const seen = new Set();
|
|
3274
|
+
const pack = message?.contextPack || {};
|
|
3275
|
+
const attachments = contextArray(pack.attachments);
|
|
3276
|
+
for (const attachment of attachments) {
|
|
3277
|
+
const resolved = await this.imageInputFromContextReference(attachment, { preferReadAttachment: true });
|
|
3278
|
+
if (!resolved || seen.has(resolved.key)) continue;
|
|
3279
|
+
seen.add(resolved.key);
|
|
3280
|
+
inputs.push(resolved.input);
|
|
3281
|
+
}
|
|
3282
|
+
const avatar = pack?.targetAgent?.avatar || null;
|
|
3283
|
+
if (avatar?.visualInput !== false) {
|
|
3284
|
+
const resolved = await this.imageInputFromContextReference(avatar);
|
|
3285
|
+
if (resolved && !seen.has(resolved.key)) {
|
|
3286
|
+
seen.add(resolved.key);
|
|
3287
|
+
inputs.push(resolved.input);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
const targetAgentId = String(pack?.targetAgentId || pack?.targetAgent?.id || this.agent.id || '');
|
|
3291
|
+
for (const participant of contextArray(pack.participants)) {
|
|
3292
|
+
if (participant?.type !== 'agent') continue;
|
|
3293
|
+
if (targetAgentId && String(participant.id || '') === targetAgentId) continue;
|
|
3294
|
+
const participantAvatar = participant.avatar || null;
|
|
3295
|
+
if (!participantAvatar || participantAvatar.visualInput === false || !isContextImageReference(participantAvatar)) continue;
|
|
3296
|
+
const resolved = await this.imageInputFromContextReference(participantAvatar);
|
|
3297
|
+
if (!resolved || seen.has(resolved.key)) continue;
|
|
3298
|
+
seen.add(resolved.key);
|
|
3299
|
+
inputs.push(resolved.input);
|
|
3300
|
+
}
|
|
3301
|
+
return inputs;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
2842
3304
|
async executeMagClawTool(name, rawArgs = {}) {
|
|
2843
3305
|
const args = { ...rawArgs, agentId: rawArgs.agentId || this.agent.id };
|
|
2844
3306
|
switch (name) {
|
|
@@ -2876,6 +3338,25 @@ class CodexAgentSession {
|
|
|
2876
3338
|
limit: args.limit,
|
|
2877
3339
|
},
|
|
2878
3340
|
});
|
|
3341
|
+
case 'list_attachments':
|
|
3342
|
+
return this.requestMagClawTool('/api/agent-tools/attachments', {
|
|
3343
|
+
query: {
|
|
3344
|
+
agentId: args.agentId,
|
|
3345
|
+
target: args.target || args.channel,
|
|
3346
|
+
workItemId: args.workItemId || args.work_item_id,
|
|
3347
|
+
messageId: args.messageId || args.message_id,
|
|
3348
|
+
limit: args.limit,
|
|
3349
|
+
},
|
|
3350
|
+
});
|
|
3351
|
+
case 'read_attachment':
|
|
3352
|
+
return this.requestMagClawTool('/api/agent-tools/attachments/read', {
|
|
3353
|
+
query: {
|
|
3354
|
+
agentId: args.agentId,
|
|
3355
|
+
attachmentId: args.attachmentId || args.attachment_id || args.id,
|
|
3356
|
+
maxBytes: args.maxBytes || args.max_bytes,
|
|
3357
|
+
format: args.format,
|
|
3358
|
+
},
|
|
3359
|
+
});
|
|
2879
3360
|
case 'search_agent_memory':
|
|
2880
3361
|
return this.requestMagClawTool('/api/agent-tools/memory/search', {
|
|
2881
3362
|
query: {
|
|
@@ -2917,6 +3398,14 @@ class CodexAgentSession {
|
|
|
2917
3398
|
targetAgentId: args.targetAgentId || args.targetAgent,
|
|
2918
3399
|
},
|
|
2919
3400
|
});
|
|
3401
|
+
case 'read_agent_avatar':
|
|
3402
|
+
return this.requestMagClawTool('/api/agent-tools/agents/avatar/read', {
|
|
3403
|
+
query: {
|
|
3404
|
+
agentId: args.agentId,
|
|
3405
|
+
targetAgentId: args.targetAgentId || args.targetAgent,
|
|
3406
|
+
maxBytes: args.maxBytes || args.max_bytes,
|
|
3407
|
+
},
|
|
3408
|
+
});
|
|
2920
3409
|
case 'write_memory':
|
|
2921
3410
|
{
|
|
2922
3411
|
const local = await writeDaemonLocalMemory(this.agentDir(), this.agent, args);
|
|
@@ -3002,6 +3491,18 @@ class CodexAgentSession {
|
|
|
3002
3491
|
}
|
|
3003
3492
|
}
|
|
3004
3493
|
|
|
3494
|
+
appendCompletedAgentText(text = '', { hadDelta = false } = {}) {
|
|
3495
|
+
const value = String(text || '');
|
|
3496
|
+
if (!value) return;
|
|
3497
|
+
if (hadDelta) {
|
|
3498
|
+
if (this.responseBuffer.endsWith(value) || this.responseBuffer.includes(value)) return;
|
|
3499
|
+
if (value.startsWith(this.responseBuffer)) this.responseBuffer = value;
|
|
3500
|
+
else this.responseBuffer += value;
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
if (!this.responseBuffer.includes(value)) this.responseBuffer += value;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3005
3506
|
async executeCodexToolItem(item = {}, requestId = null, params = {}) {
|
|
3006
3507
|
const callId = codexToolCallId(item) || String(params.callId || params.call_id || '');
|
|
3007
3508
|
const name = canonicalMagClawToolName(codexToolName(item));
|
|
@@ -3098,10 +3599,18 @@ class CodexAgentSession {
|
|
|
3098
3599
|
this.child.stderr.on('data', (chunk) => {
|
|
3099
3600
|
const text = chunk.toString().trim();
|
|
3100
3601
|
if (!text) return;
|
|
3602
|
+
const runtimeError = codexStderrRuntimeError(text);
|
|
3603
|
+
if (runtimeError) {
|
|
3604
|
+
this.reportRuntimeError(runtimeError, text).catch((error) => {
|
|
3605
|
+
logWarning('daemon', `Failed to report Codex runtime error for ${this.agent.id}: ${error.message}`);
|
|
3606
|
+
});
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3101
3609
|
this.send({
|
|
3102
3610
|
type: 'agent:activity',
|
|
3103
3611
|
agentId: this.agent.id,
|
|
3104
3612
|
status: this.status || 'working',
|
|
3613
|
+
deliveryId: this.activeDeliveryId || null,
|
|
3105
3614
|
activity: { source: 'codex-stderr', text: text.slice(0, 2000), at: now() },
|
|
3106
3615
|
});
|
|
3107
3616
|
});
|
|
@@ -3142,19 +3651,23 @@ class CodexAgentSession {
|
|
|
3142
3651
|
this.pendingPrompts.push({ prompt, message, workItem, deliveryId });
|
|
3143
3652
|
return;
|
|
3144
3653
|
}
|
|
3145
|
-
this.startTurn(prompt, message, workItem, deliveryId);
|
|
3654
|
+
await this.startTurn(prompt, message, workItem, deliveryId);
|
|
3146
3655
|
}
|
|
3147
3656
|
|
|
3148
|
-
startTurn(prompt, message = {}, workItem = null, deliveryId = '') {
|
|
3657
|
+
async startTurn(prompt, message = {}, workItem = null, deliveryId = '') {
|
|
3149
3658
|
if (!this.threadId) return false;
|
|
3150
3659
|
this.activeDeliveryId = deliveryId || '';
|
|
3151
3660
|
this.activeTurnToolSignatures = new Set();
|
|
3152
3661
|
this.activeTurnUsedSendMessage = false;
|
|
3662
|
+
this.activeTurnSawResponseDelta = false;
|
|
3663
|
+
this.activeTurnDeltaItemIds = new Set();
|
|
3664
|
+
this.lastRuntimeError = '';
|
|
3153
3665
|
const model = this.agent.model || undefined;
|
|
3154
3666
|
const effort = this.agent.reasoningEffort || undefined;
|
|
3667
|
+
const imageInputs = await this.imageInputsForDelivery(message);
|
|
3155
3668
|
const params = {
|
|
3156
3669
|
threadId: this.threadId,
|
|
3157
|
-
input: [{ type: 'text', text: prompt }],
|
|
3670
|
+
input: [{ type: 'text', text: prompt }, ...imageInputs],
|
|
3158
3671
|
approvalPolicy: codexApprovalPolicy(this.env),
|
|
3159
3672
|
...(model ? { model } : {}),
|
|
3160
3673
|
...(effort ? { effort } : {}),
|
|
@@ -3223,7 +3736,7 @@ class CodexAgentSession {
|
|
|
3223
3736
|
this.send({ type: 'agent:session', agentId: this.agent.id, status: 'idle', sessionId: this.threadId, sessionKey: this.sessionKey || null });
|
|
3224
3737
|
this.sendStatus('idle', { source: '@magclaw/daemon', detail: 'Codex session ready', at: now() });
|
|
3225
3738
|
const queued = this.pendingPrompts.splice(0);
|
|
3226
|
-
for (const item of queued) this.startTurn(item.prompt, item.message, item.workItem, item.deliveryId);
|
|
3739
|
+
for (const item of queued) await this.startTurn(item.prompt, item.message, item.workItem, item.deliveryId);
|
|
3227
3740
|
} else if (pending?.method === 'turn/start' || pending?.method === 'turn/steer') {
|
|
3228
3741
|
this.activeTurnId = message.result?.turn?.id || message.result?.turnId || this.activeTurnId;
|
|
3229
3742
|
}
|
|
@@ -3259,6 +3772,9 @@ class CodexAgentSession {
|
|
|
3259
3772
|
}
|
|
3260
3773
|
if (method === 'item/agentMessage/delta' || method === 'response/output_text/delta') {
|
|
3261
3774
|
this.responseBuffer += String(params.delta || params.text || '');
|
|
3775
|
+
const itemId = String(params.itemId || params.item_id || params.item?.id || '');
|
|
3776
|
+
if (itemId) this.activeTurnDeltaItemIds.add(itemId);
|
|
3777
|
+
this.activeTurnSawResponseDelta = true;
|
|
3262
3778
|
this.queueCodexStreamActivity();
|
|
3263
3779
|
return;
|
|
3264
3780
|
}
|
|
@@ -3266,7 +3782,10 @@ class CodexAgentSession {
|
|
|
3266
3782
|
const item = params.item || {};
|
|
3267
3783
|
if (await this.executeCodexToolItem(item, null, params)) return;
|
|
3268
3784
|
const text = item?.text || item?.message || params.text || '';
|
|
3269
|
-
|
|
3785
|
+
const itemId = String(item?.id || item?.itemId || item?.item_id || '');
|
|
3786
|
+
this.appendCompletedAgentText(text, {
|
|
3787
|
+
hadDelta: Boolean((itemId && this.activeTurnDeltaItemIds.has(itemId)) || this.activeTurnSawResponseDelta),
|
|
3788
|
+
});
|
|
3270
3789
|
return;
|
|
3271
3790
|
}
|
|
3272
3791
|
if (method === 'turn/completed' || method === 'turn/failed') {
|
|
@@ -3298,6 +3817,8 @@ class CodexAgentSession {
|
|
|
3298
3817
|
this.responseBuffer = '';
|
|
3299
3818
|
this.activeTurnId = '';
|
|
3300
3819
|
this.activeTurnUsedSendMessage = false;
|
|
3820
|
+
this.activeTurnSawResponseDelta = false;
|
|
3821
|
+
this.activeTurnDeltaItemIds.clear();
|
|
3301
3822
|
this.sendStatus(method === 'turn/completed' ? 'idle' : 'error', {
|
|
3302
3823
|
source: '@magclaw/daemon',
|
|
3303
3824
|
detail: method === 'turn/completed' ? 'Turn completed' : 'Turn failed',
|
|
@@ -3336,6 +3857,11 @@ class ClaudeAgentSession {
|
|
|
3336
3857
|
this.status = 'offline';
|
|
3337
3858
|
this.started = false;
|
|
3338
3859
|
this.activeDeliveryId = '';
|
|
3860
|
+
this.responseBuffer = '';
|
|
3861
|
+
this.lastSourceMessage = null;
|
|
3862
|
+
this.pendingMessageDelta = null;
|
|
3863
|
+
this.messageDeltaTimer = null;
|
|
3864
|
+
this.trajectoryCoalesceMs = envInteger(this.env, 'MAGCLAW_DAEMON_TRAJECTORY_COALESCE_MS', DEFAULT_TRAJECTORY_COALESCE_MS, { min: 0, max: 5_000 });
|
|
3339
3865
|
}
|
|
3340
3866
|
|
|
3341
3867
|
agentDir() {
|
|
@@ -3348,6 +3874,7 @@ class ClaudeAgentSession {
|
|
|
3348
3874
|
|
|
3349
3875
|
async prepare() {
|
|
3350
3876
|
await mkdir(this.workspace(), { recursive: true });
|
|
3877
|
+
await ensureWorkspaceSkillsDir(this.workspace(), path.join(this.agentDir(), 'codex-home'), this.agent);
|
|
3351
3878
|
await ensureDaemonAgentWorkspaceRoot(this.agentDir(), this.agent);
|
|
3352
3879
|
await prepareRuntimeHooks({
|
|
3353
3880
|
agentDir: this.agentDir(),
|
|
@@ -3358,6 +3885,8 @@ class ClaudeAgentSession {
|
|
|
3358
3885
|
'# MagClaw Remote Claude Agent Workspace',
|
|
3359
3886
|
'',
|
|
3360
3887
|
'This workspace is isolated for a MagClaw cloud-connected Claude Code agent.',
|
|
3888
|
+
'Agent-specific skills can be installed under `./skills/<skill-name>/SKILL.md`; this path belongs to this agent only.',
|
|
3889
|
+
'Runtime-generated adapter directories are not skill install targets.',
|
|
3361
3890
|
'',
|
|
3362
3891
|
].join('\n'));
|
|
3363
3892
|
}
|
|
@@ -3370,6 +3899,15 @@ class ClaudeAgentSession {
|
|
|
3370
3899
|
return readDaemonAgentWorkspaceFile(this.agentDir(), this.agent, relPath);
|
|
3371
3900
|
}
|
|
3372
3901
|
|
|
3902
|
+
async listSkills() {
|
|
3903
|
+
await this.prepare();
|
|
3904
|
+
return listDaemonAgentSkills({
|
|
3905
|
+
agent: this.agent,
|
|
3906
|
+
agentDir: this.agentDir(),
|
|
3907
|
+
workspace: this.workspace(),
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3373
3911
|
sendStatus(status, activity = null) {
|
|
3374
3912
|
this.status = status;
|
|
3375
3913
|
this.onStatusChange(this, status);
|
|
@@ -3387,6 +3925,87 @@ class ClaudeAgentSession {
|
|
|
3387
3925
|
});
|
|
3388
3926
|
}
|
|
3389
3927
|
|
|
3928
|
+
queueMessageDelta(delta = '') {
|
|
3929
|
+
const body = this.responseBuffer.trim();
|
|
3930
|
+
if (!body) return;
|
|
3931
|
+
this.pendingMessageDelta = {
|
|
3932
|
+
type: 'agent:message_delta',
|
|
3933
|
+
agentId: this.agent.id,
|
|
3934
|
+
deliveryId: this.activeDeliveryId || null,
|
|
3935
|
+
payload: {
|
|
3936
|
+
body,
|
|
3937
|
+
delta: String(delta || ''),
|
|
3938
|
+
message: this.lastSourceMessage || null,
|
|
3939
|
+
sourceMessage: this.lastSourceMessage || null,
|
|
3940
|
+
spaceType: this.lastSourceMessage?.spaceType || 'channel',
|
|
3941
|
+
spaceId: this.lastSourceMessage?.spaceId || 'chan_all',
|
|
3942
|
+
parentMessageId: this.lastSourceMessage?.parentMessageId || null,
|
|
3943
|
+
idempotencyKey: this.activeDeliveryId || null,
|
|
3944
|
+
},
|
|
3945
|
+
};
|
|
3946
|
+
if (this.trajectoryCoalesceMs <= 0) {
|
|
3947
|
+
this.flushMessageDelta();
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if (this.messageDeltaTimer) return;
|
|
3951
|
+
this.messageDeltaTimer = setTimeout(() => {
|
|
3952
|
+
this.messageDeltaTimer = null;
|
|
3953
|
+
this.flushMessageDelta();
|
|
3954
|
+
}, this.trajectoryCoalesceMs);
|
|
3955
|
+
this.messageDeltaTimer.unref?.();
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
flushMessageDelta() {
|
|
3959
|
+
if (this.messageDeltaTimer) {
|
|
3960
|
+
clearTimeout(this.messageDeltaTimer);
|
|
3961
|
+
this.messageDeltaTimer = null;
|
|
3962
|
+
}
|
|
3963
|
+
const payload = this.pendingMessageDelta;
|
|
3964
|
+
this.pendingMessageDelta = null;
|
|
3965
|
+
if (payload) this.send(payload);
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
handleClaudeStreamEvent(event) {
|
|
3969
|
+
if (event.type === 'system') {
|
|
3970
|
+
if (event.sessionId) {
|
|
3971
|
+
this.send({
|
|
3972
|
+
type: 'agent:session',
|
|
3973
|
+
agentId: this.agent.id,
|
|
3974
|
+
status: this.status,
|
|
3975
|
+
sessionId: event.sessionId,
|
|
3976
|
+
sessionKey: null,
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
if (event.type === 'text') {
|
|
3982
|
+
this.responseBuffer += String(event.delta || '');
|
|
3983
|
+
this.queueMessageDelta(event.delta || '');
|
|
3984
|
+
this.send({
|
|
3985
|
+
type: 'agent:activity',
|
|
3986
|
+
agentId: this.agent.id,
|
|
3987
|
+
status: 'working',
|
|
3988
|
+
deliveryId: this.activeDeliveryId || null,
|
|
3989
|
+
activity: { source: 'claude-code-stream', chars: this.responseBuffer.length, at: now() },
|
|
3990
|
+
});
|
|
3991
|
+
return;
|
|
3992
|
+
}
|
|
3993
|
+
if (event.type === 'thinking' || event.type === 'tool_use' || event.type === 'tool_result' || event.type === 'usage') {
|
|
3994
|
+
this.send({
|
|
3995
|
+
type: 'agent:activity',
|
|
3996
|
+
agentId: this.agent.id,
|
|
3997
|
+
status: event.type === 'thinking' ? 'thinking' : 'working',
|
|
3998
|
+
deliveryId: this.activeDeliveryId || null,
|
|
3999
|
+
activity: {
|
|
4000
|
+
source: 'claude-code-stream',
|
|
4001
|
+
phase: event.type,
|
|
4002
|
+
detail: claudeToolActivityDetail(event),
|
|
4003
|
+
at: now(),
|
|
4004
|
+
},
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
|
|
3390
4009
|
async start() {
|
|
3391
4010
|
if (this.started) return;
|
|
3392
4011
|
await this.prepare();
|
|
@@ -3397,11 +4016,12 @@ class ClaudeAgentSession {
|
|
|
3397
4016
|
async deliver(message = {}, workItem = null, deliveryId = '') {
|
|
3398
4017
|
await this.start();
|
|
3399
4018
|
this.activeDeliveryId = deliveryId || '';
|
|
4019
|
+
this.responseBuffer = '';
|
|
4020
|
+
this.lastSourceMessage = message || null;
|
|
3400
4021
|
const prompt = deliveryPrompt(this.agent, message, workItem);
|
|
3401
4022
|
const claudeCommand = this.env.CLAUDE_PATH || 'claude';
|
|
3402
|
-
const args = ['--
|
|
4023
|
+
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
|
|
3403
4024
|
if (this.agent.model) args.push('--model', String(this.agent.model));
|
|
3404
|
-
args.push(prompt);
|
|
3405
4025
|
const timeoutMs = Number(this.env.MAGCLAW_DAEMON_RUNTIME_TIMEOUT_MS || 10 * 60 * 1000);
|
|
3406
4026
|
if (this.activeDeliveryId) {
|
|
3407
4027
|
await this.markDelivery(this.activeDeliveryId, 'started', {
|
|
@@ -3412,31 +4032,33 @@ class ClaudeAgentSession {
|
|
|
3412
4032
|
}
|
|
3413
4033
|
this.sendStatus('thinking', { source: 'claude-code', detail: 'Claude Code turn started', at: now() });
|
|
3414
4034
|
await new Promise((resolve) => {
|
|
3415
|
-
let stdout = '';
|
|
3416
4035
|
let stderr = '';
|
|
4036
|
+
let stdoutBuffer = '';
|
|
3417
4037
|
let settled = false;
|
|
4038
|
+
const finalMessageFrame = (body) => ({
|
|
4039
|
+
type: 'agent:message',
|
|
4040
|
+
agentId: this.agent.id,
|
|
4041
|
+
deliveryId: this.activeDeliveryId || null,
|
|
4042
|
+
payload: {
|
|
4043
|
+
body,
|
|
4044
|
+
message,
|
|
4045
|
+
sourceMessage: message,
|
|
4046
|
+
spaceType: message.spaceType || 'channel',
|
|
4047
|
+
spaceId: message.spaceId || 'chan_all',
|
|
4048
|
+
parentMessageId: message.parentMessageId || null,
|
|
4049
|
+
idempotencyKey: this.activeDeliveryId || null,
|
|
4050
|
+
},
|
|
4051
|
+
});
|
|
3418
4052
|
const finish = (status, detail) => {
|
|
3419
4053
|
if (settled) return;
|
|
3420
4054
|
settled = true;
|
|
3421
4055
|
clearTimeout(timer);
|
|
4056
|
+
this.flushMessageDelta();
|
|
3422
4057
|
this.child = null;
|
|
4058
|
+
const body = this.responseBuffer.trim();
|
|
3423
4059
|
if (status === 'idle') {
|
|
3424
|
-
const body = stdout.trim();
|
|
3425
4060
|
if (body) {
|
|
3426
|
-
const frame =
|
|
3427
|
-
type: 'agent:message',
|
|
3428
|
-
agentId: this.agent.id,
|
|
3429
|
-
deliveryId: this.activeDeliveryId || null,
|
|
3430
|
-
payload: {
|
|
3431
|
-
body,
|
|
3432
|
-
message,
|
|
3433
|
-
sourceMessage: message,
|
|
3434
|
-
spaceType: message.spaceType || 'channel',
|
|
3435
|
-
spaceId: message.spaceId || 'chan_all',
|
|
3436
|
-
parentMessageId: message.parentMessageId || null,
|
|
3437
|
-
idempotencyKey: this.activeDeliveryId || null,
|
|
3438
|
-
},
|
|
3439
|
-
};
|
|
4061
|
+
const frame = finalMessageFrame(body);
|
|
3440
4062
|
this.send(frame);
|
|
3441
4063
|
this.markDelivery(this.activeDeliveryId, 'completed', { resultFrame: frame }).catch(() => {});
|
|
3442
4064
|
}
|
|
@@ -3444,8 +4066,10 @@ class ClaudeAgentSession {
|
|
|
3444
4066
|
this.sendStatus('idle', { source: 'claude-code', detail: detail || 'Claude Code turn completed', at: now() });
|
|
3445
4067
|
} else {
|
|
3446
4068
|
const error = detail || stderr.trim() || 'Claude Code failed.';
|
|
4069
|
+
const frame = body ? finalMessageFrame(body) : null;
|
|
4070
|
+
if (frame) this.send(frame);
|
|
3447
4071
|
this.send({ type: 'agent:error', commandId: this.activeDeliveryId || undefined, deliveryId: this.activeDeliveryId || null, agentId: this.agent.id, error });
|
|
3448
|
-
this.markDelivery(this.activeDeliveryId, 'failed', { error }).catch(() => {});
|
|
4072
|
+
this.markDelivery(this.activeDeliveryId, 'failed', { error, ...(frame ? { resultFrame: frame } : {}) }).catch(() => {});
|
|
3449
4073
|
this.sendStatus('error', { source: 'claude-code', error, at: now() });
|
|
3450
4074
|
}
|
|
3451
4075
|
this.activeDeliveryId = '';
|
|
@@ -3468,19 +4092,40 @@ class ClaudeAgentSession {
|
|
|
3468
4092
|
finish('error', 'Claude Code session timed out.');
|
|
3469
4093
|
}, timeoutMs);
|
|
3470
4094
|
this.child.stdout.on('data', (chunk) => {
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4095
|
+
stdoutBuffer += chunk.toString();
|
|
4096
|
+
const lines = stdoutBuffer.split(/\r?\n/);
|
|
4097
|
+
stdoutBuffer = lines.pop() || '';
|
|
4098
|
+
for (const line of lines) {
|
|
4099
|
+
if (!line.trim()) continue;
|
|
4100
|
+
try {
|
|
4101
|
+
const raw = JSON.parse(line);
|
|
4102
|
+
for (const event of claudeStreamEvents(raw)) this.handleClaudeStreamEvent(event);
|
|
4103
|
+
} catch (error) {
|
|
4104
|
+
stderr += `${line}\n`;
|
|
4105
|
+
this.send({
|
|
4106
|
+
type: 'agent:activity',
|
|
4107
|
+
agentId: this.agent.id,
|
|
4108
|
+
status: 'working',
|
|
4109
|
+
deliveryId: this.activeDeliveryId || null,
|
|
4110
|
+
activity: { source: 'claude-code-stream', error: `Invalid stream JSON: ${error.message}`, at: now() },
|
|
4111
|
+
});
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
3478
4114
|
});
|
|
3479
4115
|
this.child.stderr.on('data', (chunk) => {
|
|
3480
4116
|
stderr += chunk.toString();
|
|
3481
4117
|
});
|
|
3482
4118
|
this.child.on('error', (error) => finish('error', error.message));
|
|
3483
4119
|
this.child.on('close', (code, signal) => {
|
|
4120
|
+
if (stdoutBuffer.trim()) {
|
|
4121
|
+
try {
|
|
4122
|
+
const raw = JSON.parse(stdoutBuffer.trim());
|
|
4123
|
+
for (const event of claudeStreamEvents(raw)) this.handleClaudeStreamEvent(event);
|
|
4124
|
+
} catch (error) {
|
|
4125
|
+
stderr += `${stdoutBuffer}\n`;
|
|
4126
|
+
}
|
|
4127
|
+
stdoutBuffer = '';
|
|
4128
|
+
}
|
|
3484
4129
|
if (code === 0) finish('idle');
|
|
3485
4130
|
else finish('error', stderr.trim() || `Claude Code exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`);
|
|
3486
4131
|
});
|
|
@@ -3489,6 +4134,7 @@ class ClaudeAgentSession {
|
|
|
3489
4134
|
|
|
3490
4135
|
stop() {
|
|
3491
4136
|
this.status = 'stopping';
|
|
4137
|
+
this.flushMessageDelta();
|
|
3492
4138
|
if (this.child) this.child.kill('SIGTERM');
|
|
3493
4139
|
this.started = false;
|
|
3494
4140
|
}
|
|
@@ -4725,8 +5371,12 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4725
5371
|
|| previousService.packageBin
|
|
4726
5372
|
|| packageBinForPackageName(packageName),
|
|
4727
5373
|
).trim() || packageBinForPackageName(packageName);
|
|
5374
|
+
const envServiceMode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5375
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE);
|
|
5376
|
+
const previousMode = normalizeBackgroundServiceMode(previousService.mode);
|
|
5377
|
+
const serviceMode = envServiceMode || (previousMode && previousMode !== 'foreground' ? previousMode : backgroundServiceModeForPlatform(process.platform));
|
|
4728
5378
|
const service = await writeServiceState(paths.profile, {
|
|
4729
|
-
mode:
|
|
5379
|
+
mode: serviceMode,
|
|
4730
5380
|
background: true,
|
|
4731
5381
|
launcher,
|
|
4732
5382
|
packageSpec,
|
|
@@ -4809,6 +5459,72 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4809
5459
|
return launcher;
|
|
4810
5460
|
}
|
|
4811
5461
|
|
|
5462
|
+
async function writeContainerSupervisor(profile, launcher, env = process.env) {
|
|
5463
|
+
const paths = profilePaths(profile, env);
|
|
5464
|
+
await mkdir(paths.runDir, { recursive: true });
|
|
5465
|
+
await mkdir(paths.logDir, { recursive: true });
|
|
5466
|
+
const supervisor = path.join(paths.runDir, 'container-supervisor.js');
|
|
5467
|
+
const restartSec = Math.max(1, Math.min(60, Number(env.MAGCLAW_DAEMON_CONTAINER_RESTART_SEC || 3) || 3));
|
|
5468
|
+
const code = [
|
|
5469
|
+
'#!/usr/bin/env node',
|
|
5470
|
+
"const { spawn } = require('node:child_process');",
|
|
5471
|
+
"const fs = require('node:fs');",
|
|
5472
|
+
"const path = require('node:path');",
|
|
5473
|
+
`const launcher = ${JSON.stringify(launcher)};`,
|
|
5474
|
+
`const serviceFile = ${JSON.stringify(paths.service)};`,
|
|
5475
|
+
`const logDir = ${JSON.stringify(paths.logDir)};`,
|
|
5476
|
+
`const restartMs = ${JSON.stringify(restartSec * 1000)};`,
|
|
5477
|
+
'let child = null;',
|
|
5478
|
+
'let stopping = false;',
|
|
5479
|
+
'function readService() {',
|
|
5480
|
+
" try { return JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch { return {}; }",
|
|
5481
|
+
'}',
|
|
5482
|
+
'function shouldStop() {',
|
|
5483
|
+
' const service = readService();',
|
|
5484
|
+
' return Boolean(service.remoteClosed || service.containerSupervisorDisabled);',
|
|
5485
|
+
'}',
|
|
5486
|
+
'function openLog(name) {',
|
|
5487
|
+
' fs.mkdirSync(logDir, { recursive: true });',
|
|
5488
|
+
" return fs.openSync(path.join(logDir, name), 'a');",
|
|
5489
|
+
'}',
|
|
5490
|
+
'function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }',
|
|
5491
|
+
'function stop(signal) {',
|
|
5492
|
+
' stopping = true;',
|
|
5493
|
+
" if (child && child.exitCode === null && child.signalCode === null) child.kill(signal || 'SIGTERM');",
|
|
5494
|
+
' setTimeout(() => process.exit(0), 5000).unref?.();',
|
|
5495
|
+
'}',
|
|
5496
|
+
"process.once('SIGINT', () => stop('SIGINT'));",
|
|
5497
|
+
"process.once('SIGTERM', () => stop('SIGTERM'));",
|
|
5498
|
+
'(async () => {',
|
|
5499
|
+
' while (!stopping) {',
|
|
5500
|
+
' if (shouldStop()) break;',
|
|
5501
|
+
" const out = openLog('daemon.log');",
|
|
5502
|
+
" const err = openLog('daemon.err.log');",
|
|
5503
|
+
' child = spawn(process.execPath, [launcher], {',
|
|
5504
|
+
" stdio: ['ignore', out, err],",
|
|
5505
|
+
' env: {',
|
|
5506
|
+
' ...process.env,',
|
|
5507
|
+
" MAGCLAW_DAEMON_SERVICE_MODE: 'container',",
|
|
5508
|
+
' },',
|
|
5509
|
+
' });',
|
|
5510
|
+
" await new Promise((resolve) => child.once('exit', resolve));",
|
|
5511
|
+
' fs.closeSync(out);',
|
|
5512
|
+
' fs.closeSync(err);',
|
|
5513
|
+
' child = null;',
|
|
5514
|
+
' if (stopping || shouldStop()) break;',
|
|
5515
|
+
' await sleep(restartMs);',
|
|
5516
|
+
' }',
|
|
5517
|
+
'})().catch((error) => {',
|
|
5518
|
+
" console.error(`[magclaw-container-supervisor] ${error && error.stack ? error.stack : error}`);",
|
|
5519
|
+
' process.exit(1);',
|
|
5520
|
+
'});',
|
|
5521
|
+
'',
|
|
5522
|
+
].join('\n');
|
|
5523
|
+
await writeFile(supervisor, code);
|
|
5524
|
+
await chmod(supervisor, 0o755).catch(() => {});
|
|
5525
|
+
return supervisor;
|
|
5526
|
+
}
|
|
5527
|
+
|
|
4812
5528
|
function launchAgentLabel(profile) {
|
|
4813
5529
|
return `ai.magclaw.daemon.${safeProfileName(profile)}`;
|
|
4814
5530
|
}
|
|
@@ -4824,6 +5540,61 @@ async function ensureExecutable(file) {
|
|
|
4824
5540
|
await chmod(file, 0o755).catch(() => {});
|
|
4825
5541
|
}
|
|
4826
5542
|
|
|
5543
|
+
async function startContainerBackground(profile, env = process.env) {
|
|
5544
|
+
const paths = profilePaths(profile, env);
|
|
5545
|
+
await mkdir(paths.logDir, { recursive: true });
|
|
5546
|
+
const launcher = await writeLauncher(paths.profile, { ...env, MAGCLAW_DAEMON_SERVICE_MODE: 'container' });
|
|
5547
|
+
await ensureExecutable(launcher);
|
|
5548
|
+
const supervisor = await writeContainerSupervisor(paths.profile, launcher, env);
|
|
5549
|
+
await ensureExecutable(supervisor);
|
|
5550
|
+
await writeServiceState(paths.profile, {
|
|
5551
|
+
mode: 'container',
|
|
5552
|
+
background: true,
|
|
5553
|
+
launcher,
|
|
5554
|
+
containerSupervisor: supervisor,
|
|
5555
|
+
containerSupervisorDisabled: false,
|
|
5556
|
+
}, env);
|
|
5557
|
+
const current = backgroundServiceStatus(paths.profile, env);
|
|
5558
|
+
if (current.active) {
|
|
5559
|
+
return {
|
|
5560
|
+
ok: true,
|
|
5561
|
+
mode: 'container',
|
|
5562
|
+
active: true,
|
|
5563
|
+
alreadyRunning: true,
|
|
5564
|
+
pid: current.pid || null,
|
|
5565
|
+
supervisorPid: current.supervisorPid || null,
|
|
5566
|
+
file: supervisor,
|
|
5567
|
+
launcher,
|
|
5568
|
+
status: current.status || 'running',
|
|
5569
|
+
};
|
|
5570
|
+
}
|
|
5571
|
+
const child = spawn(process.execPath, [supervisor], {
|
|
5572
|
+
cwd: process.cwd(),
|
|
5573
|
+
detached: true,
|
|
5574
|
+
stdio: 'ignore',
|
|
5575
|
+
env: {
|
|
5576
|
+
...env,
|
|
5577
|
+
MAGCLAW_DAEMON_SERVICE_MODE: 'container',
|
|
5578
|
+
},
|
|
5579
|
+
});
|
|
5580
|
+
child.unref();
|
|
5581
|
+
await writeServiceState(paths.profile, {
|
|
5582
|
+
mode: 'container',
|
|
5583
|
+
background: true,
|
|
5584
|
+
launcher,
|
|
5585
|
+
containerSupervisor: supervisor,
|
|
5586
|
+
containerSupervisorPid: child.pid || null,
|
|
5587
|
+
}, env);
|
|
5588
|
+
return {
|
|
5589
|
+
ok: true,
|
|
5590
|
+
mode: 'container',
|
|
5591
|
+
active: true,
|
|
5592
|
+
supervisorPid: child.pid || null,
|
|
5593
|
+
file: supervisor,
|
|
5594
|
+
launcher,
|
|
5595
|
+
};
|
|
5596
|
+
}
|
|
5597
|
+
|
|
4827
5598
|
async function startMacBackground(profile, env = process.env) {
|
|
4828
5599
|
const paths = profilePaths(profile, env);
|
|
4829
5600
|
const label = launchAgentLabel(paths.profile);
|
|
@@ -4934,6 +5705,11 @@ async function startWindowsBackground(profile, env = process.env) {
|
|
|
4934
5705
|
}
|
|
4935
5706
|
|
|
4936
5707
|
async function startBackground(profile, env = process.env) {
|
|
5708
|
+
const service = await readServiceState(profile, env);
|
|
5709
|
+
const mode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5710
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
5711
|
+
|| normalizeBackgroundServiceMode(service.mode);
|
|
5712
|
+
if (mode === 'container') return startContainerBackground(profile, env);
|
|
4937
5713
|
if (process.platform === 'darwin') return startMacBackground(profile, env);
|
|
4938
5714
|
if (process.platform === 'linux') return startLinuxBackground(profile, env);
|
|
4939
5715
|
if (process.platform === 'win32') return startWindowsBackground(profile, env);
|
|
@@ -4961,6 +5737,25 @@ export function parseLaunchdPrintStatus(result = {}) {
|
|
|
4961
5737
|
|
|
4962
5738
|
function backgroundServiceStatus(profile, env = process.env) {
|
|
4963
5739
|
const paths = profilePaths(profile, env);
|
|
5740
|
+
const serviceState = readServiceStateSync(paths.profile, env);
|
|
5741
|
+
const requestedMode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5742
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE);
|
|
5743
|
+
if (requestedMode === 'container' || normalizeBackgroundServiceMode(serviceState.mode) === 'container') {
|
|
5744
|
+
const lock = activeDaemonLockSync(paths.profile, env);
|
|
5745
|
+
const supervisorPid = Number(serviceState.containerSupervisorPid || 0);
|
|
5746
|
+
const supervisorRunning = Number.isInteger(supervisorPid) && supervisorPid > 0 && pidIsRunning(supervisorPid);
|
|
5747
|
+
const daemonRunning = Boolean(lock?.pid);
|
|
5748
|
+
return {
|
|
5749
|
+
mode: 'container',
|
|
5750
|
+
active: daemonRunning || supervisorRunning,
|
|
5751
|
+
pid: lock?.pid || null,
|
|
5752
|
+
supervisorPid: supervisorRunning ? supervisorPid : null,
|
|
5753
|
+
file: serviceState.containerSupervisor || '',
|
|
5754
|
+
launcher: serviceState.launcher || '',
|
|
5755
|
+
status: daemonRunning ? 'running' : supervisorRunning ? 'supervising' : 'inactive',
|
|
5756
|
+
error: '',
|
|
5757
|
+
};
|
|
5758
|
+
}
|
|
4964
5759
|
if (process.platform === 'darwin') {
|
|
4965
5760
|
const label = launchAgentLabel(paths.profile);
|
|
4966
5761
|
const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
@@ -5081,9 +5876,77 @@ async function stopActiveDaemon(profile, env = process.env) {
|
|
|
5081
5876
|
return { ok: stopped, running: !stopped, pid, signal };
|
|
5082
5877
|
}
|
|
5083
5878
|
|
|
5879
|
+
function waitForPidExitSync(pid, timeoutMs = 2000) {
|
|
5880
|
+
const deadline = Date.now() + timeoutMs;
|
|
5881
|
+
while (Date.now() < deadline) {
|
|
5882
|
+
if (!pidIsRunning(pid)) return true;
|
|
5883
|
+
sleepSync(100);
|
|
5884
|
+
}
|
|
5885
|
+
return !pidIsRunning(pid);
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
function stopPidSync(pid, { allowCurrent = false, timeoutMs = 2000 } = {}) {
|
|
5889
|
+
const value = Number(pid);
|
|
5890
|
+
if (!Number.isInteger(value) || value <= 0) return { ok: true, running: false };
|
|
5891
|
+
if (!allowCurrent && value === process.pid) {
|
|
5892
|
+
return { ok: true, running: true, pid: value, skippedCurrent: true };
|
|
5893
|
+
}
|
|
5894
|
+
try {
|
|
5895
|
+
process.kill(value, 'SIGTERM');
|
|
5896
|
+
} catch (error) {
|
|
5897
|
+
if (error?.code === 'ESRCH') return { ok: true, running: false, pid: value, stale: true };
|
|
5898
|
+
return { ok: false, running: true, pid: value, error: error.message };
|
|
5899
|
+
}
|
|
5900
|
+
if (waitForPidExitSync(value, timeoutMs)) return { ok: true, running: false, pid: value, signal: 'SIGTERM' };
|
|
5901
|
+
try {
|
|
5902
|
+
process.kill(value, 'SIGKILL');
|
|
5903
|
+
} catch (error) {
|
|
5904
|
+
if (error?.code === 'ESRCH') return { ok: true, running: false, pid: value, signal: 'SIGKILL' };
|
|
5905
|
+
return { ok: false, running: true, pid: value, signal: 'SIGKILL', error: error.message };
|
|
5906
|
+
}
|
|
5907
|
+
const stopped = waitForPidExitSync(value, 1000);
|
|
5908
|
+
return { ok: stopped, running: !stopped, pid: value, signal: 'SIGKILL' };
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
function stopContainerBackground(profile, env = process.env, options = {}) {
|
|
5912
|
+
const paths = profilePaths(profile, env);
|
|
5913
|
+
const state = readServiceStateSync(paths.profile, env);
|
|
5914
|
+
const lock = activeDaemonLockSync(paths.profile, env);
|
|
5915
|
+
const supervisor = stopPidSync(state.containerSupervisorPid);
|
|
5916
|
+
const daemon = stopPidSync(lock?.pid);
|
|
5917
|
+
if (options.disable) {
|
|
5918
|
+
writeJsonFileSync(paths.service, {
|
|
5919
|
+
...state,
|
|
5920
|
+
version: 1,
|
|
5921
|
+
profile: paths.profile,
|
|
5922
|
+
mode: 'container',
|
|
5923
|
+
background: true,
|
|
5924
|
+
containerSupervisorDisabled: true,
|
|
5925
|
+
updatedAt: now(),
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5928
|
+
return {
|
|
5929
|
+
ok: Boolean(supervisor.ok && daemon.ok),
|
|
5930
|
+
mode: 'container',
|
|
5931
|
+
supervisorPid: state.containerSupervisorPid || null,
|
|
5932
|
+
pid: lock?.pid || null,
|
|
5933
|
+
supervisor,
|
|
5934
|
+
process: daemon,
|
|
5935
|
+
file: state.containerSupervisor || '',
|
|
5936
|
+
launcher: state.launcher || '',
|
|
5937
|
+
disabled: Boolean(options.disable),
|
|
5938
|
+
};
|
|
5939
|
+
}
|
|
5940
|
+
|
|
5084
5941
|
function stopBackground(profile, env = process.env, options = {}) {
|
|
5942
|
+
const paths = profilePaths(profile, env);
|
|
5943
|
+
const state = readServiceStateSync(paths.profile, env);
|
|
5944
|
+
const requestedMode = normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
5945
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE);
|
|
5946
|
+
if (requestedMode === 'container' || normalizeBackgroundServiceMode(state.mode) === 'container') {
|
|
5947
|
+
return stopContainerBackground(paths.profile, env, options);
|
|
5948
|
+
}
|
|
5085
5949
|
if (process.platform === 'darwin') {
|
|
5086
|
-
const paths = profilePaths(profile, env);
|
|
5087
5950
|
const label = launchAgentLabel(paths.profile);
|
|
5088
5951
|
const serviceTarget = `gui/${process.getuid()}/${label}`;
|
|
5089
5952
|
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
@@ -5570,7 +6433,10 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
5570
6433
|
|
|
5571
6434
|
await emitProgress({ status: 'upgrading', phase: 'stage_service', progress: 45, message: 'Staging service launcher.' });
|
|
5572
6435
|
await writeServiceState(profile, {
|
|
5573
|
-
mode:
|
|
6436
|
+
mode: normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_SERVICE_MODE)
|
|
6437
|
+
|| normalizeBackgroundServiceMode(env.MAGCLAW_DAEMON_BACKGROUND_MODE)
|
|
6438
|
+
|| normalizeBackgroundServiceMode(serviceBefore.mode)
|
|
6439
|
+
|| backgroundServiceModeForPlatform(process.platform),
|
|
5574
6440
|
background: true,
|
|
5575
6441
|
packageSpec,
|
|
5576
6442
|
packageName,
|