shennian 0.2.89 → 0.2.90
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/dist/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,51 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/external-command.test.ts
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
const MIME_BY_EXT = {
|
|
6
|
-
'.jpg': 'image/jpeg',
|
|
7
|
-
'.jpeg': 'image/jpeg',
|
|
8
|
-
'.png': 'image/png',
|
|
9
|
-
'.gif': 'image/gif',
|
|
10
|
-
'.webp': 'image/webp',
|
|
11
|
-
'.mp4': 'video/mp4',
|
|
12
|
-
'.mov': 'video/quicktime',
|
|
13
|
-
'.pdf': 'application/pdf',
|
|
14
|
-
'.txt': 'text/plain',
|
|
15
|
-
'.md': 'text/markdown',
|
|
16
|
-
'.csv': 'text/csv',
|
|
17
|
-
'.doc': 'application/msword',
|
|
18
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
19
|
-
'.xls': 'application/vnd.ms-excel',
|
|
20
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
21
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
22
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
23
|
-
'.zip': 'application/zip',
|
|
24
|
-
};
|
|
25
|
-
const MAX_EXTERNAL_ATTACHMENT_BYTES = Number(process.env.SHENNIAN_EXTERNAL_ATTACHMENT_MAX_BYTES || 20 * 1024 * 1024);
|
|
26
|
-
function inferMimeType(filePath, kind) {
|
|
27
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
28
|
-
if (MIME_BY_EXT[ext])
|
|
29
|
-
return MIME_BY_EXT[ext];
|
|
30
|
-
if (kind === 'image')
|
|
31
|
-
return 'image/jpeg';
|
|
32
|
-
if (kind === 'video')
|
|
33
|
-
return 'video/mp4';
|
|
34
|
-
return 'application/octet-stream';
|
|
35
|
-
}
|
|
36
|
-
export function readExternalAttachment(filePath, kind) {
|
|
37
|
-
const absolutePath = path.resolve(filePath);
|
|
38
|
-
const stat = fs.statSync(absolutePath);
|
|
39
|
-
if (!stat.isFile())
|
|
40
|
-
throw new Error(`Attachment is not a file: ${absolutePath}`);
|
|
41
|
-
if (stat.size > MAX_EXTERNAL_ATTACHMENT_BYTES) {
|
|
42
|
-
throw new Error(`Attachment is too large: ${stat.size} bytes. Max: ${MAX_EXTERNAL_ATTACHMENT_BYTES} bytes.`);
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
kind,
|
|
46
|
-
name: path.basename(absolutePath),
|
|
47
|
-
mimeType: inferMimeType(absolutePath, kind),
|
|
48
|
-
size: stat.size,
|
|
49
|
-
localPath: absolutePath,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
1
|
+
import m from"node:fs";import n from"node:path";const p={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp",".mp4":"video/mp4",".mov":"video/quicktime",".pdf":"application/pdf",".txt":"text/plain",".md":"text/markdown",".csv":"text/csv",".doc":"application/msword",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".xls":"application/vnd.ms-excel",".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".ppt":"application/vnd.ms-powerpoint",".pptx":"application/vnd.openxmlformats-officedocument.presentationml.presentation",".zip":"application/zip"},a=Number(process.env.SHENNIAN_EXTERNAL_ATTACHMENT_MAX_BYTES||20*1024*1024);function r(i,t){const e=n.extname(i).toLowerCase();return p[e]?p[e]:t==="image"?"image/jpeg":t==="video"?"video/mp4":"application/octet-stream"}function l(i,t){const e=n.resolve(i),o=m.statSync(e);if(!o.isFile())throw new Error(`Attachment is not a file: ${e}`);if(o.size>a)throw new Error(`Attachment is too large: ${o.size} bytes. Max: ${a} bytes.`);return{kind:t,name:n.basename(e),mimeType:r(e,t),size:o.size,localPath:e}}export{l as readExternalAttachment};
|
|
@@ -1,137 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/external-command.test.ts
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { resolveShennianPath } from '../config/index.js';
|
|
6
|
-
import { readExternalAttachment } from './external-attachments.js';
|
|
7
|
-
function loadManagerIpcFromRuntimeFile() {
|
|
8
|
-
const filePath = resolveShennianPath('runtime', 'manager-ipc.json');
|
|
9
|
-
try {
|
|
10
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
11
|
-
const pid = Number(parsed.pid);
|
|
12
|
-
if (Number.isInteger(pid) && pid > 0 && !processExists(pid)) {
|
|
13
|
-
return { error: `Manager IPC runtime file is stale for PID ${pid}. Restart the desktop daemon and retry.` };
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
url: typeof parsed.url === 'string' ? parsed.url : undefined,
|
|
17
|
-
token: typeof parsed.token === 'string' ? parsed.token : undefined,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function requireExternalContext(explicitSessionId) {
|
|
25
|
-
const runtimeIpc = !process.env.SHENNIAN_MANAGER_IPC_URL || !process.env.SHENNIAN_MANAGER_IPC_TOKEN
|
|
26
|
-
? loadManagerIpcFromRuntimeFile()
|
|
27
|
-
: {};
|
|
28
|
-
if (runtimeIpc.error) {
|
|
29
|
-
console.error(chalk.red(`✗ ${runtimeIpc.error}`));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
const url = process.env.SHENNIAN_MANAGER_IPC_URL || runtimeIpc.url;
|
|
33
|
-
const token = process.env.SHENNIAN_MANAGER_IPC_TOKEN || runtimeIpc.token;
|
|
34
|
-
const sessionId = explicitSessionId?.trim() || process.env.SHENNIAN_EXTERNAL_SESSION_ID || process.env.SHENNIAN_MANAGER_SESSION_ID;
|
|
35
|
-
if (!url || !token || !sessionId) {
|
|
36
|
-
console.error(chalk.red('✗ This command must run inside a Shennian conversation with an external channel, or pass --session-id <id>.'));
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
return { url, token, sessionId };
|
|
40
|
-
}
|
|
41
|
-
function processExists(pid) {
|
|
42
|
-
try {
|
|
43
|
-
process.kill(pid, 0);
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async function sendExternal(input) {
|
|
51
|
-
const ctx = requireExternalContext(input.sessionId);
|
|
52
|
-
const replyTarget = input.replyTarget?.trim() || process.env.SHENNIAN_EXTERNAL_REPLY_TARGET || undefined;
|
|
53
|
-
const response = await fetch(`${ctx.url}/external/reply`, {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers: {
|
|
56
|
-
authorization: `Bearer ${ctx.token}`,
|
|
57
|
-
'content-type': 'application/json',
|
|
58
|
-
'x-shennian-manager-session-id': ctx.sessionId,
|
|
59
|
-
},
|
|
60
|
-
body: JSON.stringify({
|
|
61
|
-
managerSessionId: ctx.sessionId,
|
|
62
|
-
text: input.text,
|
|
63
|
-
attachment: input.attachment,
|
|
64
|
-
idempotencyKey: input.idempotencyKey,
|
|
65
|
-
replyTarget,
|
|
66
|
-
}),
|
|
67
|
-
});
|
|
68
|
-
const data = await response.json().catch(() => ({ ok: false, error: response.statusText }));
|
|
69
|
-
if (!response.ok || !data.ok) {
|
|
70
|
-
throw new Error(data.error || `External send failed: ${response.status}`);
|
|
71
|
-
}
|
|
72
|
-
console.log('ok');
|
|
73
|
-
}
|
|
74
|
-
export function registerExternalCommand(program) {
|
|
75
|
-
const external = program.command('external').description('External channel tools for this Shennian conversation');
|
|
76
|
-
external
|
|
77
|
-
.command('send')
|
|
78
|
-
.description('Send a message to the external channel bound to this conversation')
|
|
79
|
-
.requiredOption('--text <text>', 'Message text')
|
|
80
|
-
.option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
|
|
81
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message')
|
|
82
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
83
|
-
.action(async (opts) => {
|
|
84
|
-
await sendExternal({ text: opts.text, replyTarget: opts.replyTarget, idempotencyKey: opts.idempotencyKey, sessionId: opts.sessionId });
|
|
85
|
-
});
|
|
86
|
-
external
|
|
87
|
-
.command('send-image')
|
|
88
|
-
.description('Send an image file to the external channel bound to this conversation')
|
|
89
|
-
.requiredOption('--path <path>', 'Image file path')
|
|
90
|
-
.option('--caption <text>', 'Optional text to send before the image')
|
|
91
|
-
.option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
|
|
92
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message')
|
|
93
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
94
|
-
.action(async (opts) => {
|
|
95
|
-
await sendExternal({
|
|
96
|
-
text: opts.caption,
|
|
97
|
-
attachment: readExternalAttachment(opts.path, 'image'),
|
|
98
|
-
replyTarget: opts.replyTarget,
|
|
99
|
-
idempotencyKey: opts.idempotencyKey,
|
|
100
|
-
sessionId: opts.sessionId,
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
external
|
|
104
|
-
.command('send-video')
|
|
105
|
-
.description('Send a video file to the external channel bound to this conversation')
|
|
106
|
-
.requiredOption('--path <path>', 'Video file path')
|
|
107
|
-
.option('--caption <text>', 'Optional text to send before the video')
|
|
108
|
-
.option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
|
|
109
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message')
|
|
110
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
111
|
-
.action(async (opts) => {
|
|
112
|
-
await sendExternal({
|
|
113
|
-
text: opts.caption,
|
|
114
|
-
attachment: readExternalAttachment(opts.path, 'video'),
|
|
115
|
-
replyTarget: opts.replyTarget,
|
|
116
|
-
idempotencyKey: opts.idempotencyKey,
|
|
117
|
-
sessionId: opts.sessionId,
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
external
|
|
121
|
-
.command('send-file')
|
|
122
|
-
.description('Send a file to the external channel bound to this conversation')
|
|
123
|
-
.requiredOption('--path <path>', 'File path')
|
|
124
|
-
.option('--caption <text>', 'Optional text to send before the file')
|
|
125
|
-
.option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
|
|
126
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message')
|
|
127
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
128
|
-
.action(async (opts) => {
|
|
129
|
-
await sendExternal({
|
|
130
|
-
text: opts.caption,
|
|
131
|
-
attachment: readExternalAttachment(opts.path, 'file'),
|
|
132
|
-
replyTarget: opts.replyTarget,
|
|
133
|
-
idempotencyKey: opts.idempotencyKey,
|
|
134
|
-
sessionId: opts.sessionId,
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
}
|
|
1
|
+
import d from"node:fs";import s from"chalk";import{resolveShennianPath as c}from"../config/index.js";import{readExternalAttachment as a}from"./external-attachments.js";function p(){const n=c("runtime","manager-ipc.json");try{const t=JSON.parse(d.readFileSync(n,"utf-8")),e=Number(t.pid);return Number.isInteger(e)&&e>0&&!m(e)?{error:`Manager IPC runtime file is stale for PID ${e}. Restart the desktop daemon and retry.`}:{url:typeof t.url=="string"?t.url:void 0,token:typeof t.token=="string"?t.token:void 0}}catch{return{}}}function l(n){const t=!process.env.SHENNIAN_MANAGER_IPC_URL||!process.env.SHENNIAN_MANAGER_IPC_TOKEN?p():{};t.error&&(console.error(s.red(`\u2717 ${t.error}`)),process.exit(1));const e=process.env.SHENNIAN_MANAGER_IPC_URL||t.url,o=process.env.SHENNIAN_MANAGER_IPC_TOKEN||t.token,i=n?.trim()||process.env.SHENNIAN_EXTERNAL_SESSION_ID||process.env.SHENNIAN_MANAGER_SESSION_ID;return(!e||!o||!i)&&(console.error(s.red("\u2717 This command must run inside a Shennian conversation with an external channel, or pass --session-id <id>.")),process.exit(1)),{url:e,token:o,sessionId:i}}function m(n){try{return process.kill(n,0),!0}catch{return!1}}async function r(n){const t=l(n.sessionId),e=n.replyTarget?.trim()||process.env.SHENNIAN_EXTERNAL_REPLY_TARGET||void 0,o=await fetch(`${t.url}/external/reply`,{method:"POST",headers:{authorization:`Bearer ${t.token}`,"content-type":"application/json","x-shennian-manager-session-id":t.sessionId},body:JSON.stringify({managerSessionId:t.sessionId,text:n.text,attachment:n.attachment,idempotencyKey:n.idempotencyKey,replyTarget:e})}),i=await o.json().catch(()=>({ok:!1,error:o.statusText}));if(!o.ok||!i.ok)throw new Error(i.error||`External send failed: ${o.status}`);console.log("ok")}function E(n){const t=n.command("external").description("External channel tools for this Shennian conversation");t.command("send").description("Send a message to the external channel bound to this conversation").requiredOption("--text <text>","Message text").option("--session-id <id>","Shennian conversation/session id; defaults to injected current-session env").option("--reply-target <id>","Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message").option("--idempotency-key <key>","Idempotency key").action(async e=>{await r({text:e.text,replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey,sessionId:e.sessionId})}),t.command("send-image").description("Send an image file to the external channel bound to this conversation").requiredOption("--path <path>","Image file path").option("--caption <text>","Optional text to send before the image").option("--session-id <id>","Shennian conversation/session id; defaults to injected current-session env").option("--reply-target <id>","Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message").option("--idempotency-key <key>","Idempotency key").action(async e=>{await r({text:e.caption,attachment:a(e.path,"image"),replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey,sessionId:e.sessionId})}),t.command("send-video").description("Send a video file to the external channel bound to this conversation").requiredOption("--path <path>","Video file path").option("--caption <text>","Optional text to send before the video").option("--session-id <id>","Shennian conversation/session id; defaults to injected current-session env").option("--reply-target <id>","Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message").option("--idempotency-key <key>","Idempotency key").action(async e=>{await r({text:e.caption,attachment:a(e.path,"video"),replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey,sessionId:e.sessionId})}),t.command("send-file").description("Send a file to the external channel bound to this conversation").requiredOption("--path <path>","File path").option("--caption <text>","Optional text to send before the file").option("--session-id <id>","Shennian conversation/session id; defaults to injected current-session env").option("--reply-target <id>","Daemon-generated reply target; defaults to SHENNIAN_EXTERNAL_REPLY_TARGET or latest external message").option("--idempotency-key <key>","Idempotency key").action(async e=>{await r({text:e.caption,attachment:a(e.path,"file"),replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey,sessionId:e.sessionId})})}export{E as registerExternalCommand};
|
|
@@ -1,391 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// @test src/__tests__/manager-command.test.ts
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import { readExternalAttachment } from './external-attachments.js';
|
|
7
|
-
function requireManagerContext() {
|
|
8
|
-
const url = process.env.SHENNIAN_MANAGER_IPC_URL;
|
|
9
|
-
const token = process.env.SHENNIAN_MANAGER_IPC_TOKEN;
|
|
10
|
-
const managerSessionId = process.env.SHENNIAN_MANAGER_SESSION_ID;
|
|
11
|
-
if (!url || !token || !managerSessionId) {
|
|
12
|
-
console.error(chalk.red('✗ This command must run inside a Shennian Manager Agent session.'));
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
return { url, token, managerSessionId };
|
|
16
|
-
}
|
|
17
|
-
async function ipc(path, body) {
|
|
18
|
-
const ctx = requireManagerContext();
|
|
19
|
-
const response = await fetch(`${ctx.url}${path}`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: {
|
|
22
|
-
authorization: `Bearer ${ctx.token}`,
|
|
23
|
-
'content-type': 'application/json',
|
|
24
|
-
'x-shennian-manager-session-id': ctx.managerSessionId,
|
|
25
|
-
},
|
|
26
|
-
body: JSON.stringify({ ...body, managerSessionId: ctx.managerSessionId }),
|
|
27
|
-
});
|
|
28
|
-
const data = await response.json().catch(() => ({ ok: false, error: response.statusText }));
|
|
29
|
-
if (!response.ok || !data.ok) {
|
|
30
|
-
throw new Error(data.error || `Manager IPC failed: ${response.status}`);
|
|
31
|
-
}
|
|
32
|
-
return data;
|
|
33
|
-
}
|
|
34
|
-
function printJson(data) {
|
|
35
|
-
console.log(JSON.stringify(data, null, 2));
|
|
36
|
-
}
|
|
37
|
-
function readMessage(opts) {
|
|
38
|
-
if (opts.messageFile)
|
|
39
|
-
return fs.readFileSync(opts.messageFile, 'utf-8');
|
|
40
|
-
return opts.message ?? '';
|
|
41
|
-
}
|
|
42
|
-
function collect(value, previous = []) {
|
|
43
|
-
previous.push(value);
|
|
44
|
-
return previous;
|
|
45
|
-
}
|
|
46
|
-
function parseBool(value) {
|
|
47
|
-
if (value === undefined)
|
|
48
|
-
return undefined;
|
|
49
|
-
return value === 'true';
|
|
50
|
-
}
|
|
51
|
-
function parseNumber(value) {
|
|
52
|
-
const number = Number(value);
|
|
53
|
-
return Number.isFinite(number) ? number : undefined;
|
|
54
|
-
}
|
|
55
|
-
function printWeChatRpaStatus(channel) {
|
|
56
|
-
if (!channel) {
|
|
57
|
-
console.log('not configured');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const groups = Array.isArray(channel.wechatRpaGroups)
|
|
61
|
-
? channel.wechatRpaGroups
|
|
62
|
-
.map((group) => typeof group === 'object' && group ? String(group.name || '').trim() : '')
|
|
63
|
-
.filter(Boolean)
|
|
64
|
-
.join(', ')
|
|
65
|
-
: '';
|
|
66
|
-
const fields = [
|
|
67
|
-
`enabled=${Boolean(channel.enabled)}`,
|
|
68
|
-
`connected=${Boolean(channel.tokenConfigured ?? channel.enabled)}`,
|
|
69
|
-
channel.wechatRpaSource ? `source=${String(channel.wechatRpaSource)}` : '',
|
|
70
|
-
groups ? `groups=${groups}` : 'groups=',
|
|
71
|
-
channel.pollIntervalMs ? `poll=${String(channel.pollIntervalMs)}ms` : '',
|
|
72
|
-
channel.wechatRpaRuntimeState ? `state=${String(channel.wechatRpaRuntimeState)}` : '',
|
|
73
|
-
Number.isFinite(channel.wechatRpaPendingReplyCount) ? `pendingReplies=${String(channel.wechatRpaPendingReplyCount)}` : '',
|
|
74
|
-
channel.wechatRpaLastRunAt ? `lastRun=${String(channel.wechatRpaLastRunAt)}` : '',
|
|
75
|
-
channel.wechatRpaLastMessageAt ? `lastMessage=${String(channel.wechatRpaLastMessageAt)}` : '',
|
|
76
|
-
channel.wechatRpaLastInterruptedAt ? `lastInterrupted=${String(channel.wechatRpaLastInterruptedAt)}` : '',
|
|
77
|
-
channel.wechatRpaLastError ? `lastError=${String(channel.wechatRpaLastError)}` : 'lastError=none',
|
|
78
|
-
].filter(Boolean);
|
|
79
|
-
console.log(fields.join('\n'));
|
|
80
|
-
}
|
|
81
|
-
export function registerManagerCommand(program) {
|
|
82
|
-
const manager = program.command('manager').description('Manager Agent local tools');
|
|
83
|
-
const sessions = manager.command('sessions').description('Manage same-project worker sessions');
|
|
84
|
-
sessions
|
|
85
|
-
.command('list')
|
|
86
|
-
.description('List same-project sessions visible to this Manager')
|
|
87
|
-
.option('--json', 'Print JSON')
|
|
88
|
-
.action(async (opts) => {
|
|
89
|
-
const result = await ipc('/sessions/list', {});
|
|
90
|
-
if (opts.json)
|
|
91
|
-
printJson(result);
|
|
92
|
-
else {
|
|
93
|
-
const sessions = result.sessions ?? [];
|
|
94
|
-
for (const session of sessions) {
|
|
95
|
-
console.log(`${session.sessionId}\t${session.agentType}\t${session.role ?? ''}\t${session.status}\t${session.title ?? session.summary ?? ''}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
sessions
|
|
100
|
-
.command('start')
|
|
101
|
-
.description('Start a worker session in the same project')
|
|
102
|
-
.requiredOption('--agent <agent>', 'Worker agent type, e.g. codex, claude, gemini, cursor, opencode, pi, or custom:<name>')
|
|
103
|
-
.requiredOption('--workdir <path>', 'Worker workdir; must match Manager workdir')
|
|
104
|
-
.option('--model <model>', 'Worker model id')
|
|
105
|
-
.option('--message <text>', 'Worker prompt')
|
|
106
|
-
.option('--message-file <path>', 'Read worker prompt from file')
|
|
107
|
-
.option('--json', 'Print JSON')
|
|
108
|
-
.action(async (opts) => {
|
|
109
|
-
const result = await ipc('/sessions/start', {
|
|
110
|
-
agentType: opts.agent,
|
|
111
|
-
workDir: opts.workdir,
|
|
112
|
-
modelId: opts.model,
|
|
113
|
-
message: readMessage(opts),
|
|
114
|
-
});
|
|
115
|
-
if (opts.json)
|
|
116
|
-
printJson(result);
|
|
117
|
-
else
|
|
118
|
-
console.log(result.session?.sessionId ?? 'ok');
|
|
119
|
-
});
|
|
120
|
-
sessions
|
|
121
|
-
.command('send')
|
|
122
|
-
.description('Queue a message for a managed worker; runs immediately if idle')
|
|
123
|
-
.requiredOption('--session-id <id>', 'Worker session id')
|
|
124
|
-
.option('--message <text>', 'Message text')
|
|
125
|
-
.option('--message-file <path>', 'Read message from file')
|
|
126
|
-
.option('--direct', 'Bypass queue and send immediately')
|
|
127
|
-
.action(async (opts) => {
|
|
128
|
-
await ipc('/sessions/send', {
|
|
129
|
-
sessionId: opts.sessionId,
|
|
130
|
-
message: readMessage(opts),
|
|
131
|
-
enqueue: !opts.direct,
|
|
132
|
-
});
|
|
133
|
-
console.log('ok');
|
|
134
|
-
});
|
|
135
|
-
const queue = sessions.command('queue').description('Inspect and edit pending worker messages');
|
|
136
|
-
queue
|
|
137
|
-
.command('list')
|
|
138
|
-
.description('List pending messages for a managed worker')
|
|
139
|
-
.requiredOption('--session-id <id>', 'Worker session id')
|
|
140
|
-
.option('--json', 'Print JSON')
|
|
141
|
-
.action(async (opts) => {
|
|
142
|
-
const result = await ipc('/sessions/queue', { sessionId: opts.sessionId });
|
|
143
|
-
if (opts.json)
|
|
144
|
-
printJson(result);
|
|
145
|
-
else {
|
|
146
|
-
const pending = result.queue?.pending ?? [];
|
|
147
|
-
for (const message of pending)
|
|
148
|
-
console.log(`${message.id}\t${message.text.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
queue
|
|
152
|
-
.command('edit')
|
|
153
|
-
.description('Edit a pending worker message')
|
|
154
|
-
.requiredOption('--session-id <id>', 'Worker session id')
|
|
155
|
-
.requiredOption('--message-id <id>', 'Queued message id')
|
|
156
|
-
.option('--message <text>', 'Replacement message text')
|
|
157
|
-
.option('--message-file <path>', 'Read replacement message from file')
|
|
158
|
-
.action(async (opts) => {
|
|
159
|
-
await ipc('/sessions/queue/edit', {
|
|
160
|
-
sessionId: opts.sessionId,
|
|
161
|
-
queueMessageId: opts.messageId,
|
|
162
|
-
message: readMessage(opts),
|
|
163
|
-
});
|
|
164
|
-
console.log('ok');
|
|
165
|
-
});
|
|
166
|
-
queue
|
|
167
|
-
.command('delete')
|
|
168
|
-
.description('Delete a pending worker message')
|
|
169
|
-
.requiredOption('--session-id <id>', 'Worker session id')
|
|
170
|
-
.requiredOption('--message-id <id>', 'Queued message id')
|
|
171
|
-
.action(async (opts) => {
|
|
172
|
-
await ipc('/sessions/queue/delete', {
|
|
173
|
-
sessionId: opts.sessionId,
|
|
174
|
-
queueMessageId: opts.messageId,
|
|
175
|
-
});
|
|
176
|
-
console.log('ok');
|
|
177
|
-
});
|
|
178
|
-
const stopWorker = async (opts) => {
|
|
179
|
-
await ipc('/sessions/stop', { sessionId: opts.sessionId });
|
|
180
|
-
console.log('ok');
|
|
181
|
-
};
|
|
182
|
-
sessions
|
|
183
|
-
.command('stop')
|
|
184
|
-
.aliases(['terminate', 'kill'])
|
|
185
|
-
.description('Stop or terminate a running managed worker')
|
|
186
|
-
.requiredOption('--session-id <id>', 'Worker session id')
|
|
187
|
-
.action(stopWorker);
|
|
188
|
-
sessions
|
|
189
|
-
.command('read')
|
|
190
|
-
.description('Read a managed worker transcript')
|
|
191
|
-
.requiredOption('--session-id <id>', 'Worker session id')
|
|
192
|
-
.option('--limit <n>', 'Message limit', '200')
|
|
193
|
-
.option('--json', 'Print JSON')
|
|
194
|
-
.action(async (opts) => {
|
|
195
|
-
const result = await ipc('/sessions/read', { sessionId: opts.sessionId, limit: Number(opts.limit) });
|
|
196
|
-
if (opts.json)
|
|
197
|
-
printJson(result);
|
|
198
|
-
else
|
|
199
|
-
printJson(result.messages ?? []);
|
|
200
|
-
});
|
|
201
|
-
manager
|
|
202
|
-
.command('memory')
|
|
203
|
-
.description('Manager project memory helpers')
|
|
204
|
-
.command('path')
|
|
205
|
-
.description('Print project memory directory path')
|
|
206
|
-
.action(async () => {
|
|
207
|
-
const result = await ipc('/memory/path', {});
|
|
208
|
-
console.log(result.path);
|
|
209
|
-
});
|
|
210
|
-
const external = manager.command('external').description('External channel tools');
|
|
211
|
-
const sendExternal = async (opts) => {
|
|
212
|
-
await ipc('/external/reply', opts);
|
|
213
|
-
console.log('ok');
|
|
214
|
-
};
|
|
215
|
-
external
|
|
216
|
-
.command('send')
|
|
217
|
-
.description('Send a message to the Manager-bound external channel')
|
|
218
|
-
.requiredOption('--text <text>', 'Message text')
|
|
219
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
|
|
220
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
221
|
-
.action(sendExternal);
|
|
222
|
-
external
|
|
223
|
-
.command('send-image')
|
|
224
|
-
.description('Send an image file to the Manager-bound external channel')
|
|
225
|
-
.requiredOption('--path <path>', 'Image file path')
|
|
226
|
-
.option('--caption <text>', 'Optional text to send before the image')
|
|
227
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
|
|
228
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
229
|
-
.action(async (opts) => {
|
|
230
|
-
await sendExternal({
|
|
231
|
-
text: opts.caption,
|
|
232
|
-
attachment: readExternalAttachment(opts.path, 'image'),
|
|
233
|
-
replyTarget: opts.replyTarget,
|
|
234
|
-
idempotencyKey: opts.idempotencyKey,
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
external
|
|
238
|
-
.command('send-video')
|
|
239
|
-
.description('Send a video file to the Manager-bound external channel')
|
|
240
|
-
.requiredOption('--path <path>', 'Video file path')
|
|
241
|
-
.option('--caption <text>', 'Optional text to send before the video')
|
|
242
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
|
|
243
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
244
|
-
.action(async (opts) => {
|
|
245
|
-
await sendExternal({
|
|
246
|
-
text: opts.caption,
|
|
247
|
-
attachment: readExternalAttachment(opts.path, 'video'),
|
|
248
|
-
replyTarget: opts.replyTarget,
|
|
249
|
-
idempotencyKey: opts.idempotencyKey,
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
external
|
|
253
|
-
.command('send-file')
|
|
254
|
-
.description('Send a file to the Manager-bound external channel')
|
|
255
|
-
.requiredOption('--path <path>', 'File path')
|
|
256
|
-
.option('--caption <text>', 'Optional text to send before the file')
|
|
257
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
|
|
258
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
259
|
-
.action(async (opts) => {
|
|
260
|
-
await sendExternal({
|
|
261
|
-
text: opts.caption,
|
|
262
|
-
attachment: readExternalAttachment(opts.path, 'file'),
|
|
263
|
-
replyTarget: opts.replyTarget,
|
|
264
|
-
idempotencyKey: opts.idempotencyKey,
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
external
|
|
268
|
-
.command('reply')
|
|
269
|
-
.description('Send an explicit reply to an external channel')
|
|
270
|
-
.option('--reply-target <id>', 'Daemon-generated reply target; omitted means latest external message for this Manager')
|
|
271
|
-
.option('--channel-id <id>', 'Channel id')
|
|
272
|
-
.option('--conversation-id <id>', 'Conversation id')
|
|
273
|
-
.requiredOption('--text <text>', 'Reply text')
|
|
274
|
-
.option('--idempotency-key <key>', 'Idempotency key')
|
|
275
|
-
.action(sendExternal);
|
|
276
|
-
const channel = manager.command('channel').description('Manager external channel config');
|
|
277
|
-
channel
|
|
278
|
-
.command('get')
|
|
279
|
-
.description('Get bound external channel config')
|
|
280
|
-
.option('--json', 'Print JSON')
|
|
281
|
-
.action(async (opts) => {
|
|
282
|
-
const result = await ipc('/channel/get', {});
|
|
283
|
-
if (opts.json)
|
|
284
|
-
printJson(result);
|
|
285
|
-
else
|
|
286
|
-
printJson(result.channel ?? null);
|
|
287
|
-
});
|
|
288
|
-
channel
|
|
289
|
-
.command('upsert')
|
|
290
|
-
.description('Create or update an external channel binding')
|
|
291
|
-
.option('--id <id>', 'Channel id')
|
|
292
|
-
.option('--name <name>', 'Channel display name')
|
|
293
|
-
.requiredOption('--enabled <true|false>', 'Whether the channel should be enabled')
|
|
294
|
-
.option('--ws-url <url>', 'External websocket URL')
|
|
295
|
-
.option('--token <token>', 'Bearer token')
|
|
296
|
-
.option('--can-reply <true|false>', 'Whether reply should be allowed')
|
|
297
|
-
.option('--json', 'Print JSON')
|
|
298
|
-
.action(async (opts) => {
|
|
299
|
-
const result = await ipc('/channel/upsert', {
|
|
300
|
-
id: opts.id,
|
|
301
|
-
name: opts.name,
|
|
302
|
-
enabled: opts.enabled === 'true',
|
|
303
|
-
wsUrl: opts.wsUrl,
|
|
304
|
-
token: opts.token,
|
|
305
|
-
canReply: opts.canReply === undefined ? undefined : opts.canReply === 'true',
|
|
306
|
-
});
|
|
307
|
-
if (opts.json)
|
|
308
|
-
printJson(result);
|
|
309
|
-
else
|
|
310
|
-
printJson(result.channel ?? null);
|
|
311
|
-
});
|
|
312
|
-
const wechatRpa = manager.command('wechat-rpa').description('Local WeChat RPA channel config');
|
|
313
|
-
wechatRpa
|
|
314
|
-
.command('get')
|
|
315
|
-
.description('Get local WeChat RPA channel config')
|
|
316
|
-
.option('--json', 'Print JSON')
|
|
317
|
-
.action(async (opts) => {
|
|
318
|
-
const result = await ipc('/wechat-rpa/channel/get', {});
|
|
319
|
-
if (opts.json)
|
|
320
|
-
printJson(result);
|
|
321
|
-
else
|
|
322
|
-
printJson(result.channel ?? null);
|
|
323
|
-
});
|
|
324
|
-
wechatRpa
|
|
325
|
-
.command('status')
|
|
326
|
-
.description('Print local WeChat RPA channel runtime status')
|
|
327
|
-
.option('--json', 'Print JSON')
|
|
328
|
-
.action(async (opts) => {
|
|
329
|
-
const result = await ipc('/wechat-rpa/channel/get', {});
|
|
330
|
-
if (opts.json)
|
|
331
|
-
printJson(result);
|
|
332
|
-
else
|
|
333
|
-
printWeChatRpaStatus((result.channel ?? null));
|
|
334
|
-
});
|
|
335
|
-
wechatRpa
|
|
336
|
-
.command('sync')
|
|
337
|
-
.description('Run one immediate WeChat RPA sync and print the updated runtime status')
|
|
338
|
-
.option('--json', 'Print JSON')
|
|
339
|
-
.action(async (opts) => {
|
|
340
|
-
const result = await ipc('/wechat-rpa/channel/sync', {});
|
|
341
|
-
if (opts.json)
|
|
342
|
-
printJson(result);
|
|
343
|
-
else
|
|
344
|
-
printWeChatRpaStatus((result.channel ?? null));
|
|
345
|
-
});
|
|
346
|
-
wechatRpa
|
|
347
|
-
.command('upsert')
|
|
348
|
-
.description('Create or update a local WeChat RPA channel binding')
|
|
349
|
-
.option('--id <id>', 'Channel id')
|
|
350
|
-
.option('--name <name>', 'Channel display name')
|
|
351
|
-
.requiredOption('--enabled <true|false>', 'Whether the channel should be enabled')
|
|
352
|
-
.option('--group <name>', 'Bound WeChat conversation name; pass once per Shennian conversation', collect, [])
|
|
353
|
-
.option('--can-reply <true|false>', 'Whether reply should be allowed')
|
|
354
|
-
.option('--source <macos-flow|wechat-rpa-lab|macos-probe|fixture-jsonl>', 'WeChat RPA implementation source')
|
|
355
|
-
.option('--poll-interval-ms <n>', 'Polling interval in milliseconds')
|
|
356
|
-
.option('--recent-limit <n>', 'Recent message limit')
|
|
357
|
-
.option('--idle-seconds <n>', 'Minimum user idle seconds before foreground automation')
|
|
358
|
-
.option('--force-foreground <true|false>', 'Force WeChat foreground while syncing')
|
|
359
|
-
.option('--restore-previous <true|false>', 'Restore previous foreground app after syncing')
|
|
360
|
-
.option('--download-attachments <true|false>', 'Click and localize inbound attachment candidates')
|
|
361
|
-
.option('--download-attachments-dir <path>', 'Directory for localized inbound WeChat attachments')
|
|
362
|
-
.option('--privacy-consent <true|false>', 'Confirm WeChat channel data and privacy consent')
|
|
363
|
-
.option('--flow-script-path <path>', 'Override macOS flow script path')
|
|
364
|
-
.option('--json', 'Print JSON')
|
|
365
|
-
.action(async (opts) => {
|
|
366
|
-
if (opts.enabled === 'true' && opts.group.length > 1) {
|
|
367
|
-
throw new Error('WeChat RPA 每个对话只能绑定一个群');
|
|
368
|
-
}
|
|
369
|
-
const result = await ipc('/wechat-rpa/channel/upsert', {
|
|
370
|
-
id: opts.id,
|
|
371
|
-
name: opts.name,
|
|
372
|
-
enabled: opts.enabled === 'true',
|
|
373
|
-
groups: opts.group.map((name) => ({ name })),
|
|
374
|
-
canReply: parseBool(opts.canReply),
|
|
375
|
-
source: opts.source,
|
|
376
|
-
pollIntervalMs: parseNumber(opts.pollIntervalMs),
|
|
377
|
-
recentLimit: parseNumber(opts.recentLimit),
|
|
378
|
-
idleSeconds: parseNumber(opts.idleSeconds),
|
|
379
|
-
forceForeground: parseBool(opts.forceForeground),
|
|
380
|
-
noRestore: opts.restorePrevious === undefined ? undefined : opts.restorePrevious !== 'true',
|
|
381
|
-
downloadAttachments: parseBool(opts.downloadAttachments),
|
|
382
|
-
downloadAttachmentsDir: opts.downloadAttachmentsDir,
|
|
383
|
-
privacyConsentAccepted: parseBool(opts.privacyConsent),
|
|
384
|
-
flowScriptPath: opts.flowScriptPath,
|
|
385
|
-
});
|
|
386
|
-
if (opts.json)
|
|
387
|
-
printJson(result);
|
|
388
|
-
else
|
|
389
|
-
printJson(result.channel ?? null);
|
|
390
|
-
});
|
|
391
|
-
}
|
|
1
|
+
import R from"node:fs";import S from"chalk";import{readExternalAttachment as h}from"./external-attachments.js";function x(){const n=process.env.SHENNIAN_MANAGER_IPC_URL,o=process.env.SHENNIAN_MANAGER_IPC_TOKEN,a=process.env.SHENNIAN_MANAGER_SESSION_ID;return(!n||!o||!a)&&(console.error(S.red("\u2717 This command must run inside a Shennian Manager Agent session.")),process.exit(1)),{url:n,token:o,managerSessionId:a}}async function i(n,o){const a=x(),r=await fetch(`${a.url}${n}`,{method:"POST",headers:{authorization:`Bearer ${a.token}`,"content-type":"application/json","x-shennian-manager-session-id":a.managerSessionId},body:JSON.stringify({...o,managerSessionId:a.managerSessionId})}),c=await r.json().catch(()=>({ok:!1,error:r.statusText}));if(!r.ok||!c.ok)throw new Error(c.error||`Manager IPC failed: ${r.status}`);return c}function s(n){console.log(JSON.stringify(n,null,2))}function y(n){return n.messageFile?R.readFileSync(n.messageFile,"utf-8"):n.message??""}function I(n,o=[]){return o.push(n),o}function g(n){if(n!==void 0)return n==="true"}function f(n){const o=Number(n);return Number.isFinite(o)?o:void 0}function k(n){if(!n){console.log("not configured");return}const o=Array.isArray(n.wechatRpaGroups)?n.wechatRpaGroups.map(r=>typeof r=="object"&&r?String(r.name||"").trim():"").filter(Boolean).join(", "):"",a=[`enabled=${!!n.enabled}`,`connected=${!!(n.tokenConfigured??n.enabled)}`,n.wechatRpaSource?`source=${String(n.wechatRpaSource)}`:"",o?`groups=${o}`:"groups=",n.pollIntervalMs?`poll=${String(n.pollIntervalMs)}ms`:"",n.wechatRpaRuntimeState?`state=${String(n.wechatRpaRuntimeState)}`:"",Number.isFinite(n.wechatRpaPendingReplyCount)?`pendingReplies=${String(n.wechatRpaPendingReplyCount)}`:"",n.wechatRpaLastRunAt?`lastRun=${String(n.wechatRpaLastRunAt)}`:"",n.wechatRpaLastMessageAt?`lastMessage=${String(n.wechatRpaLastMessageAt)}`:"",n.wechatRpaLastInterruptedAt?`lastInterrupted=${String(n.wechatRpaLastInterruptedAt)}`:"",n.wechatRpaLastError?`lastError=${String(n.wechatRpaLastError)}`:"lastError=none"].filter(Boolean);console.log(a.join(`
|
|
2
|
+
`))}function A(n){const o=n.command("manager").description("Manager Agent local tools"),a=o.command("sessions").description("Manage same-project worker sessions");a.command("list").description("List same-project sessions visible to this Manager").option("--json","Print JSON").action(async e=>{const t=await i("/sessions/list",{});if(e.json)s(t);else{const m=t.sessions??[];for(const d of m)console.log(`${d.sessionId} ${d.agentType} ${d.role??""} ${d.status} ${d.title??d.summary??""}`)}}),a.command("start").description("Start a worker session in the same project").requiredOption("--agent <agent>","Worker agent type, e.g. codex, claude, gemini, cursor, opencode, pi, or custom:<name>").requiredOption("--workdir <path>","Worker workdir; must match Manager workdir").option("--model <model>","Worker model id").option("--message <text>","Worker prompt").option("--message-file <path>","Read worker prompt from file").option("--json","Print JSON").action(async e=>{const t=await i("/sessions/start",{agentType:e.agent,workDir:e.workdir,modelId:e.model,message:y(e)});e.json?s(t):console.log(t.session?.sessionId??"ok")}),a.command("send").description("Queue a message for a managed worker; runs immediately if idle").requiredOption("--session-id <id>","Worker session id").option("--message <text>","Message text").option("--message-file <path>","Read message from file").option("--direct","Bypass queue and send immediately").action(async e=>{await i("/sessions/send",{sessionId:e.sessionId,message:y(e),enqueue:!e.direct}),console.log("ok")});const r=a.command("queue").description("Inspect and edit pending worker messages");r.command("list").description("List pending messages for a managed worker").requiredOption("--session-id <id>","Worker session id").option("--json","Print JSON").action(async e=>{const t=await i("/sessions/queue",{sessionId:e.sessionId});if(e.json)s(t);else{const m=t.queue?.pending??[];for(const d of m)console.log(`${d.id} ${d.text.replace(/\s+/g," ").slice(0,120)}`)}}),r.command("edit").description("Edit a pending worker message").requiredOption("--session-id <id>","Worker session id").requiredOption("--message-id <id>","Queued message id").option("--message <text>","Replacement message text").option("--message-file <path>","Read replacement message from file").action(async e=>{await i("/sessions/queue/edit",{sessionId:e.sessionId,queueMessageId:e.messageId,message:y(e)}),console.log("ok")}),r.command("delete").description("Delete a pending worker message").requiredOption("--session-id <id>","Worker session id").requiredOption("--message-id <id>","Queued message id").action(async e=>{await i("/sessions/queue/delete",{sessionId:e.sessionId,queueMessageId:e.messageId}),console.log("ok")});const c=async e=>{await i("/sessions/stop",{sessionId:e.sessionId}),console.log("ok")};a.command("stop").aliases(["terminate","kill"]).description("Stop or terminate a running managed worker").requiredOption("--session-id <id>","Worker session id").action(c),a.command("read").description("Read a managed worker transcript").requiredOption("--session-id <id>","Worker session id").option("--limit <n>","Message limit","200").option("--json","Print JSON").action(async e=>{const t=await i("/sessions/read",{sessionId:e.sessionId,limit:Number(e.limit)});e.json?s(t):s(t.messages??[])}),o.command("memory").description("Manager project memory helpers").command("path").description("Print project memory directory path").action(async()=>{const e=await i("/memory/path",{});console.log(e.path)});const l=o.command("external").description("External channel tools"),p=async e=>{await i("/external/reply",e),console.log("ok")};l.command("send").description("Send a message to the Manager-bound external channel").requiredOption("--text <text>","Message text").option("--reply-target <id>","Daemon-generated reply target; omitted means latest external message for this Manager").option("--idempotency-key <key>","Idempotency key").action(p),l.command("send-image").description("Send an image file to the Manager-bound external channel").requiredOption("--path <path>","Image file path").option("--caption <text>","Optional text to send before the image").option("--reply-target <id>","Daemon-generated reply target; omitted means latest external message for this Manager").option("--idempotency-key <key>","Idempotency key").action(async e=>{await p({text:e.caption,attachment:h(e.path,"image"),replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey})}),l.command("send-video").description("Send a video file to the Manager-bound external channel").requiredOption("--path <path>","Video file path").option("--caption <text>","Optional text to send before the video").option("--reply-target <id>","Daemon-generated reply target; omitted means latest external message for this Manager").option("--idempotency-key <key>","Idempotency key").action(async e=>{await p({text:e.caption,attachment:h(e.path,"video"),replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey})}),l.command("send-file").description("Send a file to the Manager-bound external channel").requiredOption("--path <path>","File path").option("--caption <text>","Optional text to send before the file").option("--reply-target <id>","Daemon-generated reply target; omitted means latest external message for this Manager").option("--idempotency-key <key>","Idempotency key").action(async e=>{await p({text:e.caption,attachment:h(e.path,"file"),replyTarget:e.replyTarget,idempotencyKey:e.idempotencyKey})}),l.command("reply").description("Send an explicit reply to an external channel").option("--reply-target <id>","Daemon-generated reply target; omitted means latest external message for this Manager").option("--channel-id <id>","Channel id").option("--conversation-id <id>","Conversation id").requiredOption("--text <text>","Reply text").option("--idempotency-key <key>","Idempotency key").action(p);const w=o.command("channel").description("Manager external channel config");w.command("get").description("Get bound external channel config").option("--json","Print JSON").action(async e=>{const t=await i("/channel/get",{});e.json?s(t):s(t.channel??null)}),w.command("upsert").description("Create or update an external channel binding").option("--id <id>","Channel id").option("--name <name>","Channel display name").requiredOption("--enabled <true|false>","Whether the channel should be enabled").option("--ws-url <url>","External websocket URL").option("--token <token>","Bearer token").option("--can-reply <true|false>","Whether reply should be allowed").option("--json","Print JSON").action(async e=>{const t=await i("/channel/upsert",{id:e.id,name:e.name,enabled:e.enabled==="true",wsUrl:e.wsUrl,token:e.token,canReply:e.canReply===void 0?void 0:e.canReply==="true"});e.json?s(t):s(t.channel??null)});const u=o.command("wechat-rpa").description("Local WeChat RPA channel config");u.command("get").description("Get local WeChat RPA channel config").option("--json","Print JSON").action(async e=>{const t=await i("/wechat-rpa/channel/get",{});e.json?s(t):s(t.channel??null)}),u.command("status").description("Print local WeChat RPA channel runtime status").option("--json","Print JSON").action(async e=>{const t=await i("/wechat-rpa/channel/get",{});e.json?s(t):k(t.channel??null)}),u.command("sync").description("Run one immediate WeChat RPA sync and print the updated runtime status").option("--json","Print JSON").action(async e=>{const t=await i("/wechat-rpa/channel/sync",{});e.json?s(t):k(t.channel??null)}),u.command("upsert").description("Create or update a local WeChat RPA channel binding").option("--id <id>","Channel id").option("--name <name>","Channel display name").requiredOption("--enabled <true|false>","Whether the channel should be enabled").option("--group <name>","Bound WeChat conversation name; pass once per Shennian conversation",I,[]).option("--can-reply <true|false>","Whether reply should be allowed").option("--source <macos-flow|wechat-rpa-lab|macos-probe|fixture-jsonl>","WeChat RPA implementation source").option("--poll-interval-ms <n>","Polling interval in milliseconds").option("--recent-limit <n>","Recent message limit").option("--idle-seconds <n>","Minimum user idle seconds before foreground automation").option("--force-foreground <true|false>","Force WeChat foreground while syncing").option("--restore-previous <true|false>","Restore previous foreground app after syncing").option("--download-attachments <true|false>","Click and localize inbound attachment candidates").option("--download-attachments-dir <path>","Directory for localized inbound WeChat attachments").option("--privacy-consent <true|false>","Confirm WeChat channel data and privacy consent").option("--flow-script-path <path>","Override macOS flow script path").option("--json","Print JSON").action(async e=>{if(e.enabled==="true"&&e.group.length>1)throw new Error("WeChat RPA \u6BCF\u4E2A\u5BF9\u8BDD\u53EA\u80FD\u7ED1\u5B9A\u4E00\u4E2A\u7FA4");const t=await i("/wechat-rpa/channel/upsert",{id:e.id,name:e.name,enabled:e.enabled==="true",groups:e.group.map(m=>({name:m})),canReply:g(e.canReply),source:e.source,pollIntervalMs:f(e.pollIntervalMs),recentLimit:f(e.recentLimit),idleSeconds:f(e.idleSeconds),forceForeground:g(e.forceForeground),noRestore:e.restorePrevious===void 0?void 0:e.restorePrevious!=="true",downloadAttachments:g(e.downloadAttachments),downloadAttachmentsDir:e.downloadAttachmentsDir,privacyConsentAccepted:g(e.privacyConsent),flowScriptPath:e.flowScriptPath});e.json?s(t):s(t.channel??null)})}export{A as registerManagerCommand};
|
|
@@ -1,6 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/pair-qr.test.ts
|
|
3
|
-
export const PAIR_QR_RENDER_OPTIONS = { small: false };
|
|
4
|
-
export function buildPairQrPayload(pairToken) {
|
|
5
|
-
return `pair:${pairToken}`;
|
|
6
|
-
}
|
|
1
|
+
const a={small:!1};function l(r){return`pair:${r}`}export{a as PAIR_QR_RENDER_OPTIONS,l as buildPairQrPayload};
|