@magclaw/cli-core 0.1.22

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.
@@ -0,0 +1,612 @@
1
+ #!/usr/bin/env node
2
+ // MCP stdio bridge used by cloud-connected daemon agents. It forwards MagClaw
3
+ // tool calls to the cloud server with the daemon machine token.
4
+ import crypto from 'node:crypto';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
+ import path from 'node:path';
8
+
9
+ function parseArgs(argv) {
10
+ const options = {
11
+ agentId: '',
12
+ agentRoot: '',
13
+ baseUrl: process.env.MAGCLAW_SERVER_URL || 'http://127.0.0.1:6543',
14
+ token: process.env.MAGCLAW_MACHINE_TOKEN || '',
15
+ tokenFile: '',
16
+ };
17
+ for (let index = 2; index < argv.length; index += 1) {
18
+ const item = argv[index];
19
+ const next = argv[index + 1] || '';
20
+ if (item === '--agent-id') {
21
+ options.agentId = next;
22
+ index += 1;
23
+ } else if (item === '--agent-root') {
24
+ options.agentRoot = next || '';
25
+ index += 1;
26
+ } else if (item === '--base-url') {
27
+ options.baseUrl = next || options.baseUrl;
28
+ index += 1;
29
+ } else if (item === '--token') {
30
+ options.token = next || options.token;
31
+ index += 1;
32
+ } else if (item === '--token-file') {
33
+ options.tokenFile = next || '';
34
+ index += 1;
35
+ }
36
+ }
37
+ options.baseUrl = String(options.baseUrl || '').replace(/\/+$/, '');
38
+ return options;
39
+ }
40
+
41
+ const options = parseArgs(process.argv);
42
+ let buffer = '';
43
+ let cachedMachineConfig = null;
44
+
45
+ function machineConfig() {
46
+ if (cachedMachineConfig) return cachedMachineConfig;
47
+ cachedMachineConfig = {};
48
+ if (!options.tokenFile) return cachedMachineConfig;
49
+ try {
50
+ cachedMachineConfig = JSON.parse(readFileSync(options.tokenFile, 'utf8')) || {};
51
+ } catch {
52
+ cachedMachineConfig = {};
53
+ }
54
+ return cachedMachineConfig;
55
+ }
56
+
57
+ function machineToken() {
58
+ if (options.token) return options.token;
59
+ return String(machineConfig().token || machineConfig().machineToken || '');
60
+ }
61
+
62
+ function workspaceId() {
63
+ const config = machineConfig();
64
+ return String(config.workspaceId || config.workspace || '');
65
+ }
66
+
67
+ function machineHeaders(body) {
68
+ const token = machineToken();
69
+ const workspace = workspaceId();
70
+ return {
71
+ ...(body ? { 'content-type': 'application/json' } : {}),
72
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
73
+ ...(workspace ? { 'x-magclaw-workspace-id': workspace } : {}),
74
+ };
75
+ }
76
+
77
+ const PROGRESSIVE_DISCLOSURE_SECTION = [
78
+ '## 渐进式披露',
79
+ '- 其他 Agent 默认只会先读取本文件;不要假设它们已经看到 `notes/` 或 `workspace/` 中的详细文件。',
80
+ '- 如果信息不足、但已经知道具体需要什么内容,请再次请求明确路径,例如 `read_agent_memory(targetAgentId="<agent-id>", path="notes/profile.md")` 或 `read_agent_file(targetAgentId="<agent-id>", path="workspace/<file>")`。',
81
+ '- 本文件只放入口索引、能力边界和路径线索;详细规则、任务记录和交付物放入 `notes/` 或 `workspace/` 的明确文件。',
82
+ ].join('\n');
83
+
84
+ function localAgentRoot() {
85
+ return options.agentRoot ? path.resolve(options.agentRoot) : '';
86
+ }
87
+
88
+ function localMemoryHash(content) {
89
+ return crypto.createHash('sha256').update(String(content || '')).digest('hex');
90
+ }
91
+
92
+ function defaultLocalMemory(agentId) {
93
+ return [
94
+ `# ${agentId || 'Agent'}`,
95
+ '',
96
+ '## 知识索引',
97
+ '- `notes/profile.md` - 角色边界、稳定能力和回复习惯。',
98
+ '- `notes/work-log.md` - 任务记录、长期决策和完成产物。',
99
+ '',
100
+ PROGRESSIVE_DISCLOSURE_SECTION,
101
+ '',
102
+ '## 能力',
103
+ '- 暂无经过真实任务验证的稳定能力。',
104
+ '',
105
+ '## 当前上下文',
106
+ '- 暂无需要跨回合延续的任务。',
107
+ '',
108
+ '## 近期工作',
109
+ '- 暂无近期可复用记录。',
110
+ '',
111
+ ].join('\n');
112
+ }
113
+
114
+ function ensureLocalMemoryGuidance(content, agentId) {
115
+ const value = String(content || '').replace(/\s+$/u, '');
116
+ if (/^##\s+渐进式披露\s*$/m.test(value)) return `${value}\n`;
117
+ if (!value.trim()) return defaultLocalMemory(agentId);
118
+ return `${value}\n\n${PROGRESSIVE_DISCLOSURE_SECTION}\n`;
119
+ }
120
+
121
+ function headingForMemoryKind(kind) {
122
+ const value = String(kind || '').trim().toLowerCase();
123
+ if (value === 'capability') return '能力';
124
+ if (value === 'preference' || value === 'communication_style') return '当前上下文';
125
+ return '近期工作';
126
+ }
127
+
128
+ function upsertLocalMemoryBullet(content, heading, summary) {
129
+ const lines = String(content || '').replace(/\s+$/u, '').split(/\r?\n/);
130
+ const text = String(summary || '').trim().replace(/^\-\s*/, '');
131
+ if (!text) return `${lines.join('\n')}\n`;
132
+ const bullet = `- ${text}`;
133
+ if (lines.some((line) => line.trim() === bullet)) return `${lines.join('\n')}\n`;
134
+ const headingIndex = lines.findIndex((line) => line.trim() === `## ${heading}`);
135
+ if (headingIndex === -1) return `${lines.join('\n')}\n\n## ${heading}\n${bullet}\n`;
136
+ let insertAt = headingIndex + 1;
137
+ while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt += 1;
138
+ lines.splice(insertAt, 0, bullet);
139
+ return `${lines.join('\n')}\n`;
140
+ }
141
+
142
+ async function writeLocalMemory(args = {}) {
143
+ const root = localAgentRoot();
144
+ if (!root) return null;
145
+ await mkdir(path.join(root, 'notes'), { recursive: true });
146
+ await mkdir(path.join(root, 'workspace'), { recursive: true });
147
+ const memoryPath = path.join(root, 'MEMORY.md');
148
+ if (!existsSync(memoryPath)) await writeFile(memoryPath, defaultLocalMemory(options.agentId), 'utf8');
149
+ const current = await readFile(memoryPath, 'utf8').catch(() => defaultLocalMemory(options.agentId));
150
+ const summary = String(args.summary || args.content || args.sourceText || '').trim();
151
+ if (!summary) throw new Error('Memory summary is required.');
152
+ const next = upsertLocalMemoryBullet(
153
+ ensureLocalMemoryGuidance(current, options.agentId),
154
+ headingForMemoryKind(args.kind),
155
+ summary,
156
+ );
157
+ await writeFile(memoryPath, next, 'utf8');
158
+ return {
159
+ content: next,
160
+ documentHash: localMemoryHash(next),
161
+ path: memoryPath,
162
+ };
163
+ }
164
+
165
+ function schema(properties, required = []) {
166
+ return {
167
+ type: 'object',
168
+ properties,
169
+ required,
170
+ additionalProperties: false,
171
+ };
172
+ }
173
+
174
+ const tools = [
175
+ {
176
+ name: 'send_message',
177
+ description: 'Send a MagClaw message. With workItemId it replies to the current routed task; without workItemId it can proactively send to a visible target such as dm:@Agent.',
178
+ inputSchema: schema({
179
+ workItemId: { type: 'string' },
180
+ target: { type: 'string' },
181
+ content: { type: 'string' },
182
+ }, ['target', 'content']),
183
+ },
184
+ {
185
+ name: 'read_history',
186
+ description: 'Read bounded MagClaw conversation history.',
187
+ inputSchema: schema({
188
+ target: { type: 'string' },
189
+ workItemId: { type: 'string' },
190
+ limit: { type: 'number' },
191
+ around: { type: 'string' },
192
+ before: { type: 'string' },
193
+ after: { type: 'string' },
194
+ }),
195
+ },
196
+ {
197
+ name: 'search_messages',
198
+ description: 'Search MagClaw message history by text.',
199
+ inputSchema: schema({
200
+ query: { type: 'string' },
201
+ target: { type: 'string' },
202
+ workItemId: { type: 'string' },
203
+ limit: { type: 'number' },
204
+ }, ['query']),
205
+ },
206
+ {
207
+ name: 'search_agent_memory',
208
+ description: 'Search MagClaw agent memory files.',
209
+ inputSchema: schema({
210
+ query: { type: 'string' },
211
+ targetAgentId: { type: 'string' },
212
+ limit: { type: 'number' },
213
+ }, ['query']),
214
+ },
215
+ {
216
+ name: 'read_agent_memory',
217
+ description: 'Read MEMORY.md by default, or a permitted notes/*.md|txt file when the path is explicitly known.',
218
+ inputSchema: schema({
219
+ targetAgentId: { type: 'string' },
220
+ path: { type: 'string' },
221
+ }, ['targetAgentId']),
222
+ },
223
+ {
224
+ name: 'read_agent_file',
225
+ description: 'Read an explicit detailed Agent workspace path after MEMORY.md points to it.',
226
+ inputSchema: schema({
227
+ targetAgentId: { type: 'string' },
228
+ path: { type: 'string' },
229
+ }, ['targetAgentId', 'path']),
230
+ },
231
+ {
232
+ name: 'list_agents',
233
+ description: 'List compact MagClaw agent profiles visible in the current server or channel.',
234
+ inputSchema: schema({
235
+ query: { type: 'string' },
236
+ target: { type: 'string' },
237
+ channel: { type: 'string' },
238
+ limit: { type: 'number' },
239
+ }),
240
+ },
241
+ {
242
+ name: 'read_agent_profile',
243
+ description: 'Read a concise MagClaw agent profile with runtime, description, channels, and safe public fields.',
244
+ inputSchema: schema({
245
+ targetAgentId: { type: 'string' },
246
+ targetAgent: { type: 'string' },
247
+ }),
248
+ },
249
+ {
250
+ name: 'write_memory',
251
+ description: 'Record a concise durable memory for this agent.',
252
+ inputSchema: schema({
253
+ kind: { type: 'string', enum: ['capability', 'communication_style', 'preference', 'memory'] },
254
+ summary: { type: 'string' },
255
+ sourceText: { type: 'string' },
256
+ messageId: { type: 'string' },
257
+ }, ['summary']),
258
+ },
259
+ {
260
+ name: 'list_tasks',
261
+ description: 'List visible MagClaw tasks.',
262
+ inputSchema: schema({
263
+ channel: { type: 'string' },
264
+ target: { type: 'string' },
265
+ status: { type: 'string' },
266
+ assigneeId: { type: 'string' },
267
+ limit: { type: 'number' },
268
+ }),
269
+ },
270
+ {
271
+ name: 'create_tasks',
272
+ description: 'Create one or more MagClaw tasks.',
273
+ inputSchema: schema({
274
+ channel: { type: 'string' },
275
+ target: { type: 'string' },
276
+ title: { type: 'string' },
277
+ body: { type: 'string' },
278
+ tasks: { type: 'array', items: { type: 'object' } },
279
+ claim: { type: 'boolean' },
280
+ assigneeId: { type: 'string' },
281
+ assigneeIds: { type: 'array', items: { type: 'string' } },
282
+ sourceMessageId: { type: 'string' },
283
+ sourceReplyId: { type: 'string' },
284
+ }),
285
+ },
286
+ {
287
+ name: 'claim_tasks',
288
+ description: 'Claim existing tasks or promote messages into claimed tasks.',
289
+ inputSchema: schema({
290
+ channel: { type: 'string' },
291
+ target: { type: 'string' },
292
+ taskNumbers: { type: 'array', items: { type: 'number' } },
293
+ messageIds: { type: 'array', items: { type: 'string' } },
294
+ title: { type: 'string' },
295
+ force: { type: 'boolean' },
296
+ }),
297
+ },
298
+ {
299
+ name: 'update_task_status',
300
+ description: 'Update a claimed MagClaw task status. Use done for ready/accepted work and closed for close, stop, cancel, or terminated work.',
301
+ inputSchema: schema({
302
+ taskId: { type: 'string' },
303
+ taskNumber: { type: 'number' },
304
+ channel: { type: 'string' },
305
+ status: { type: 'string' },
306
+ force: { type: 'boolean' },
307
+ }, ['status']),
308
+ },
309
+ {
310
+ name: 'propose_channel_members',
311
+ description: 'Suggest adding server members to a MagClaw channel for human review.',
312
+ inputSchema: schema({
313
+ channelId: { type: 'string' },
314
+ channel: { type: 'string' },
315
+ memberIds: { type: 'array', items: { type: 'string' } },
316
+ memberId: { type: 'string' },
317
+ reason: { type: 'string' },
318
+ }, ['reason']),
319
+ },
320
+ {
321
+ name: 'schedule_reminder',
322
+ description: 'Schedule a one-time MagClaw reminder.',
323
+ inputSchema: schema({
324
+ target: { type: 'string' },
325
+ channel: { type: 'string' },
326
+ title: { type: 'string' },
327
+ body: { type: 'string' },
328
+ delaySeconds: { type: 'number' },
329
+ fireAt: { type: 'string' },
330
+ messageId: { type: 'string' },
331
+ parentMessageId: { type: 'string' },
332
+ sourceMessageId: { type: 'string' },
333
+ }, ['title']),
334
+ },
335
+ {
336
+ name: 'list_reminders',
337
+ description: 'List reminders owned by this agent.',
338
+ inputSchema: schema({
339
+ status: { type: 'string' },
340
+ limit: { type: 'number' },
341
+ }),
342
+ },
343
+ {
344
+ name: 'cancel_reminder',
345
+ description: 'Cancel a scheduled reminder.',
346
+ inputSchema: schema({
347
+ reminderId: { type: 'string' },
348
+ id: { type: 'string' },
349
+ }),
350
+ },
351
+ ];
352
+
353
+ function send(id, result) {
354
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
355
+ }
356
+
357
+ function sendError(id, code, message, data = null) {
358
+ process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message, data } })}\n`);
359
+ }
360
+
361
+ function textResult(text) {
362
+ return { content: [{ type: 'text', text: String(text || '') }] };
363
+ }
364
+
365
+ function jsonText(value) {
366
+ if (typeof value?.text === 'string' && value.text.trim()) return value.text;
367
+ return JSON.stringify(value ?? {}, null, 2);
368
+ }
369
+
370
+ function withAgentId(args = {}) {
371
+ return { ...args, agentId: args.agentId || options.agentId };
372
+ }
373
+
374
+ function queryString(params = {}) {
375
+ const search = new URLSearchParams();
376
+ for (const [key, value] of Object.entries(params || {})) {
377
+ if (value === undefined || value === null || value === '') continue;
378
+ search.set(key, String(value));
379
+ }
380
+ const value = search.toString();
381
+ return value ? `?${value}` : '';
382
+ }
383
+
384
+ async function request(pathname, { method = 'GET', query = {}, body = null } = {}) {
385
+ const response = await fetch(`${options.baseUrl}${pathname}${queryString(query)}`, {
386
+ method,
387
+ headers: machineHeaders(body),
388
+ body: body ? JSON.stringify(body) : undefined,
389
+ });
390
+ const text = await response.text();
391
+ let data = null;
392
+ try {
393
+ data = text ? JSON.parse(text) : null;
394
+ } catch {
395
+ data = { text };
396
+ }
397
+ if (!response.ok) {
398
+ const error = new Error(data?.error || data?.message || text || `HTTP ${response.status}`);
399
+ error.status = response.status;
400
+ error.data = data;
401
+ throw error;
402
+ }
403
+ return data;
404
+ }
405
+
406
+ async function callTool(name, rawArgs = {}) {
407
+ const args = withAgentId(rawArgs);
408
+ switch (name) {
409
+ case 'send_message':
410
+ return request('/api/agent-tools/messages/send', {
411
+ method: 'POST',
412
+ body: {
413
+ agentId: args.agentId,
414
+ workItemId: args.workItemId || args.work_item_id,
415
+ target: args.target,
416
+ content: args.content,
417
+ },
418
+ });
419
+ case 'read_history':
420
+ return request('/api/agent-tools/history', {
421
+ query: {
422
+ agentId: args.agentId,
423
+ target: args.target || args.channel,
424
+ workItemId: args.workItemId || args.work_item_id,
425
+ limit: args.limit,
426
+ around: args.around,
427
+ before: args.before,
428
+ after: args.after,
429
+ },
430
+ });
431
+ case 'search_messages':
432
+ return request('/api/agent-tools/search', {
433
+ query: {
434
+ agentId: args.agentId,
435
+ query: args.query || args.q,
436
+ target: args.target || args.channel,
437
+ workItemId: args.workItemId || args.work_item_id,
438
+ limit: args.limit,
439
+ },
440
+ });
441
+ case 'search_agent_memory':
442
+ return request('/api/agent-tools/memory/search', {
443
+ query: {
444
+ agentId: args.agentId,
445
+ query: args.query || args.q,
446
+ targetAgentId: args.targetAgentId || args.targetAgent,
447
+ limit: args.limit,
448
+ },
449
+ });
450
+ case 'read_agent_memory':
451
+ return request('/api/agent-tools/memory/read', {
452
+ query: {
453
+ agentId: args.agentId,
454
+ targetAgentId: args.targetAgentId || args.targetAgent,
455
+ path: args.path || 'MEMORY.md',
456
+ },
457
+ });
458
+ case 'read_agent_file':
459
+ return request('/api/agent-tools/files/read', {
460
+ query: {
461
+ agentId: args.agentId,
462
+ targetAgentId: args.targetAgentId || args.targetAgent,
463
+ path: args.path,
464
+ },
465
+ });
466
+ case 'write_memory':
467
+ {
468
+ const local = await writeLocalMemory(args);
469
+ if (!local) {
470
+ return request('/api/agent-tools/memory', {
471
+ method: 'POST',
472
+ body: args,
473
+ });
474
+ }
475
+ request('/api/agent-tools/memory/mirror', {
476
+ method: 'POST',
477
+ body: {
478
+ ...args,
479
+ content: local.content,
480
+ documentHash: local.documentHash,
481
+ idempotencyKey: `daemon-memory:${workspaceId() || 'local'}:${args.agentId}:${local.documentHash}`,
482
+ },
483
+ }).catch((error) => {
484
+ console.error(`[magclaw-mcp] async MEMORY.md mirror sync failed: ${error.message}`);
485
+ });
486
+ return {
487
+ ok: true,
488
+ status: 'local_applied',
489
+ mirrorSync: 'queued',
490
+ file: {
491
+ path: 'MEMORY.md',
492
+ absolutePath: local.path,
493
+ documentHash: local.documentHash,
494
+ },
495
+ text: 'Memory updated locally. Cloud MEMORY.md mirror sync queued.',
496
+ };
497
+ }
498
+ case 'list_agents':
499
+ return request('/api/agent-tools/agents', {
500
+ query: {
501
+ agentId: args.agentId,
502
+ query: args.query || args.q,
503
+ target: args.target || args.channel,
504
+ limit: args.limit,
505
+ },
506
+ });
507
+ case 'read_agent_profile':
508
+ return request('/api/agent-tools/agents/read', {
509
+ query: {
510
+ agentId: args.agentId,
511
+ targetAgentId: args.targetAgentId || args.targetAgent,
512
+ },
513
+ });
514
+ case 'list_tasks':
515
+ return request('/api/agent-tools/tasks', {
516
+ query: {
517
+ agentId: args.agentId,
518
+ channel: args.channel,
519
+ target: args.target,
520
+ status: args.status,
521
+ assigneeId: args.assigneeId,
522
+ limit: args.limit,
523
+ },
524
+ });
525
+ case 'create_tasks':
526
+ return request('/api/agent-tools/tasks', {
527
+ method: 'POST',
528
+ body: args,
529
+ });
530
+ case 'claim_tasks':
531
+ return request('/api/agent-tools/tasks/claim', {
532
+ method: 'POST',
533
+ body: args,
534
+ });
535
+ case 'update_task_status':
536
+ return request('/api/agent-tools/tasks/update', {
537
+ method: 'POST',
538
+ body: args,
539
+ });
540
+ case 'propose_channel_members':
541
+ return request('/api/agent-tools/channel-member-proposals', {
542
+ method: 'POST',
543
+ body: {
544
+ agentId: args.agentId,
545
+ channelId: args.channelId || args.channel_id || args.channel,
546
+ memberIds: args.memberIds || args.member_ids || (args.memberId ? [args.memberId] : undefined),
547
+ reason: args.reason,
548
+ },
549
+ });
550
+ case 'schedule_reminder':
551
+ return request('/api/agent-tools/reminders', {
552
+ method: 'POST',
553
+ body: args,
554
+ });
555
+ case 'list_reminders':
556
+ return request('/api/agent-tools/reminders', {
557
+ query: {
558
+ agentId: args.agentId,
559
+ status: args.status,
560
+ limit: args.limit,
561
+ },
562
+ });
563
+ case 'cancel_reminder':
564
+ return request('/api/agent-tools/reminders/cancel', {
565
+ method: 'POST',
566
+ body: args,
567
+ });
568
+ default:
569
+ throw new Error(`Unsupported tool: ${name || '(empty)'}`);
570
+ }
571
+ }
572
+
573
+ async function handle(message) {
574
+ const id = message.id;
575
+ try {
576
+ if (message.method === 'initialize') {
577
+ send(id, {
578
+ protocolVersion: message.params?.protocolVersion || '2024-11-05',
579
+ serverInfo: { name: 'magclaw-cloud', version: '0.1.0' },
580
+ capabilities: { tools: {} },
581
+ });
582
+ return;
583
+ }
584
+ if (message.method === 'tools/list') {
585
+ send(id, { tools });
586
+ return;
587
+ }
588
+ if (message.method === 'tools/call') {
589
+ const result = await callTool(message.params?.name, message.params?.arguments || {});
590
+ send(id, textResult(jsonText(result)));
591
+ return;
592
+ }
593
+ if (message.method === 'notifications/initialized' || message.method === 'initialized') return;
594
+ sendError(id, -32601, `Unsupported method: ${message.method || 'unknown'}`);
595
+ } catch (error) {
596
+ sendError(id, -32000, error.message, error.data || null);
597
+ }
598
+ }
599
+
600
+ process.stdin.on('data', (chunk) => {
601
+ buffer += chunk.toString();
602
+ const lines = buffer.split(/\r?\n/);
603
+ buffer = lines.pop() || '';
604
+ for (const line of lines) {
605
+ if (!line.trim()) continue;
606
+ try {
607
+ handle(JSON.parse(line));
608
+ } catch (error) {
609
+ sendError(null, -32700, error.message);
610
+ }
611
+ }
612
+ });