synomem 0.1.0
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/AGENTS.md +42 -0
- package/ARCHITECTURE.md +126 -0
- package/CHANGELOG.md +35 -0
- package/CODE_OF_CONDUCT.md +29 -0
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/SECURITY.md +117 -0
- package/dist/backend.d.ts +9 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +60 -0
- package/dist/backend.js.map +1 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1040 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +168 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1031 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +153 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +36 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +127 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +60 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +68 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/import.d.ts +655 -0
- package/dist/import.d.ts.map +1 -0
- package/dist/import.js +226 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +698 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +45 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/oauth.d.ts +27 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +201 -0
- package/dist/oauth.js.map +1 -0
- package/dist/ports/projections.d.ts +6 -0
- package/dist/ports/projections.d.ts.map +1 -0
- package/dist/ports/projections.js +2 -0
- package/dist/ports/projections.js.map +1 -0
- package/dist/ports/repository.d.ts +31 -0
- package/dist/ports/repository.d.ts.map +1 -0
- package/dist/ports/repository.js +2 -0
- package/dist/ports/repository.js.map +1 -0
- package/dist/projections.d.ts +24 -0
- package/dist/projections.d.ts.map +1 -0
- package/dist/projections.js +531 -0
- package/dist/projections.js.map +1 -0
- package/dist/remote.d.ts +120 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +286 -0
- package/dist/remote.js.map +1 -0
- package/dist/schemas.d.ts +901 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +424 -0
- package/dist/schemas.js.map +1 -0
- package/dist/service.d.ts +126 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +2 -0
- package/dist/service.js.map +1 -0
- package/dist/skill-install.d.ts +34 -0
- package/dist/skill-install.d.ts.map +1 -0
- package/dist/skill-install.js +256 -0
- package/dist/skill-install.js.map +1 -0
- package/dist/storage.d.ts +94 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +1229 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +14 -0
- package/dist/version.js.map +1 -0
- package/docs/cli.md +180 -0
- package/docs/examples.md +70 -0
- package/docs/mcp.md +96 -0
- package/docs/recovery.md +45 -0
- package/docs/releasing.md +61 -0
- package/docs/skill.md +93 -0
- package/docs/storage-format.md +83 -0
- package/openapi/synomem-v1.yaml +674 -0
- package/package.json +100 -0
- package/skills/synomem/SKILL.md +71 -0
- package/skills/synomem/agents/openai.yaml +8 -0
- package/skills/synomem/references/examples.md +28 -0
- package/src/backend.ts +91 -0
- package/src/cli.ts +1623 -0
- package/src/client.ts +1261 -0
- package/src/config.ts +170 -0
- package/src/credentials.ts +194 -0
- package/src/errors.ts +64 -0
- package/src/fs-utils.ts +85 -0
- package/src/import.ts +284 -0
- package/src/index.ts +61 -0
- package/src/mcp/index.ts +973 -0
- package/src/mcp-server.ts +46 -0
- package/src/oauth.ts +252 -0
- package/src/ports/projections.ts +6 -0
- package/src/ports/repository.ts +67 -0
- package/src/projections.ts +624 -0
- package/src/remote.ts +516 -0
- package/src/schemas.ts +457 -0
- package/src/service.ts +140 -0
- package/src/skill-install.ts +332 -0
- package/src/storage.ts +1603 -0
- package/src/types.ts +458 -0
- package/src/version.ts +16 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readlinkSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { SynomemError } from './errors.js';
|
|
17
|
+
import { actorSchema } from './schemas.js';
|
|
18
|
+
import { packageVersion } from './version.js';
|
|
19
|
+
|
|
20
|
+
export const skillRuntimeNames = [
|
|
21
|
+
'codex',
|
|
22
|
+
'claude',
|
|
23
|
+
'hermes',
|
|
24
|
+
'openclaw',
|
|
25
|
+
'cursor',
|
|
26
|
+
'grok',
|
|
27
|
+
] as const;
|
|
28
|
+
export type SkillRuntime = (typeof skillRuntimeNames)[number];
|
|
29
|
+
export type SkillState = 'current' | 'stale' | 'missing' | 'unavailable' | 'conflict';
|
|
30
|
+
|
|
31
|
+
export interface SkillLocation {
|
|
32
|
+
runtime: SkillRuntime;
|
|
33
|
+
runtimeHome: string;
|
|
34
|
+
target: string;
|
|
35
|
+
state: SkillState;
|
|
36
|
+
installedVersion?: string;
|
|
37
|
+
mode?: 'copy' | 'link';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SkillOperationResult {
|
|
41
|
+
changed: boolean;
|
|
42
|
+
dryRun: boolean;
|
|
43
|
+
packageVersion: string;
|
|
44
|
+
locations: SkillLocation[];
|
|
45
|
+
mcpCommands: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Stamp {
|
|
49
|
+
package: 'synomem';
|
|
50
|
+
version: string;
|
|
51
|
+
runtime: SkillRuntime;
|
|
52
|
+
mode: 'copy';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SkillOptions {
|
|
56
|
+
runtimes?: SkillRuntime[];
|
|
57
|
+
apply?: boolean;
|
|
58
|
+
force?: boolean;
|
|
59
|
+
link?: boolean;
|
|
60
|
+
actorId?: string;
|
|
61
|
+
actorName?: string;
|
|
62
|
+
userHome?: string;
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
source?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const stampName = '.synomem-install.json';
|
|
68
|
+
|
|
69
|
+
function runtimeHomes(userHome: string, env: NodeJS.ProcessEnv): Record<SkillRuntime, string> {
|
|
70
|
+
return {
|
|
71
|
+
codex: resolve(env.CODEX_HOME || join(userHome, '.codex')),
|
|
72
|
+
claude: resolve(env.CLAUDE_CONFIG_DIR || join(userHome, '.claude')),
|
|
73
|
+
hermes: resolve(env.HERMES_HOME || join(userHome, '.hermes')),
|
|
74
|
+
openclaw: resolve(env.OPENCLAW_STATE_DIR || join(userHome, '.openclaw')),
|
|
75
|
+
cursor: resolve(join(userHome, '.cursor')),
|
|
76
|
+
grok: resolve(env.GROK_HOME || join(userHome, '.grok')),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function selectedRuntimes(options: SkillOptions): SkillRuntime[] {
|
|
81
|
+
const defaults: SkillRuntime[] = [...skillRuntimeNames];
|
|
82
|
+
return [...new Set(options.runtimes?.length ? options.runtimes : defaults)];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sourcePath(options: SkillOptions): string {
|
|
86
|
+
return resolve(options.source ?? fileURLToPath(new URL('../skills/synomem', import.meta.url)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertSource(source: string): void {
|
|
90
|
+
if (!existsSync(join(source, 'SKILL.md'))) {
|
|
91
|
+
throw new SynomemError('INTERNAL_ERROR', 'The packaged Synomem skill is missing.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readStamp(target: string): Stamp | undefined {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(readFileSync(join(target, stampName), 'utf8')) as Partial<Stamp>;
|
|
98
|
+
if (
|
|
99
|
+
parsed.package === 'synomem' &&
|
|
100
|
+
typeof parsed.version === 'string' &&
|
|
101
|
+
skillRuntimeNames.some((runtime) => runtime === parsed.runtime) &&
|
|
102
|
+
parsed.mode === 'copy'
|
|
103
|
+
) {
|
|
104
|
+
return parsed as Stamp;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Missing or invalid stamps are treated as unowned conflicts.
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function entryExists(path: string): boolean {
|
|
113
|
+
try {
|
|
114
|
+
lstatSync(path);
|
|
115
|
+
return true;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function linkIsOurs(target: string, source: string): boolean {
|
|
123
|
+
if (!entryExists(target) || !lstatSync(target).isSymbolicLink()) return false;
|
|
124
|
+
return resolve(dirname(target), readlinkSync(target)) === source;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function inspect(runtime: SkillRuntime, runtimeHome: string, source: string): SkillLocation {
|
|
128
|
+
const target = join(runtimeHome, 'skills', 'synomem');
|
|
129
|
+
if (!existsSync(runtimeHome)) return { runtime, runtimeHome, target, state: 'unavailable' };
|
|
130
|
+
const skillsHome = dirname(target);
|
|
131
|
+
if (
|
|
132
|
+
lstatSync(runtimeHome).isSymbolicLink() ||
|
|
133
|
+
!lstatSync(runtimeHome).isDirectory() ||
|
|
134
|
+
(entryExists(skillsHome) &&
|
|
135
|
+
(lstatSync(skillsHome).isSymbolicLink() || !lstatSync(skillsHome).isDirectory()))
|
|
136
|
+
) {
|
|
137
|
+
return { runtime, runtimeHome, target, state: 'conflict' };
|
|
138
|
+
}
|
|
139
|
+
if (!entryExists(target)) return { runtime, runtimeHome, target, state: 'missing' };
|
|
140
|
+
if (linkIsOurs(target, source)) {
|
|
141
|
+
return { runtime, runtimeHome, target, state: 'current', mode: 'link' };
|
|
142
|
+
}
|
|
143
|
+
if (lstatSync(target).isSymbolicLink() || !lstatSync(target).isDirectory()) {
|
|
144
|
+
return { runtime, runtimeHome, target, state: 'conflict' };
|
|
145
|
+
}
|
|
146
|
+
const stamp = readStamp(target);
|
|
147
|
+
if (!stamp || stamp.runtime !== runtime)
|
|
148
|
+
return { runtime, runtimeHome, target, state: 'conflict' };
|
|
149
|
+
return {
|
|
150
|
+
runtime,
|
|
151
|
+
runtimeHome,
|
|
152
|
+
target,
|
|
153
|
+
state: stamp.version === packageVersion() ? 'current' : 'stale',
|
|
154
|
+
installedVersion: stamp.version,
|
|
155
|
+
mode: 'copy',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function shellQuote(value: string): string {
|
|
160
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mcpCommand(
|
|
164
|
+
runtime: SkillRuntime,
|
|
165
|
+
actorId?: string,
|
|
166
|
+
actorName?: string,
|
|
167
|
+
): string | undefined {
|
|
168
|
+
if (!actorId) return undefined;
|
|
169
|
+
const identity = actorSchema.parse({
|
|
170
|
+
kind: 'agent',
|
|
171
|
+
id: actorId,
|
|
172
|
+
displayName: actorName ?? actorId,
|
|
173
|
+
});
|
|
174
|
+
const name = identity.displayName ?? identity.id;
|
|
175
|
+
const args = ['--actor-id', identity.id, '--actor-kind', 'agent', '--actor-name', name];
|
|
176
|
+
if (runtime === 'codex') {
|
|
177
|
+
return `codex mcp add synomem -- synomem-mcp ${args.map(shellQuote).join(' ')}`;
|
|
178
|
+
}
|
|
179
|
+
if (runtime === 'claude') {
|
|
180
|
+
return `claude mcp add --scope user synomem -- synomem-mcp ${args.map(shellQuote).join(' ')}`;
|
|
181
|
+
}
|
|
182
|
+
if (runtime === 'hermes') {
|
|
183
|
+
return `hermes mcp add synomem --command synomem-mcp --args ${args.map(shellQuote).join(' ')}`;
|
|
184
|
+
}
|
|
185
|
+
if (runtime === 'openclaw') {
|
|
186
|
+
return `openclaw mcp add synomem --command synomem-mcp ${args.map((item) => `--arg=${shellQuote(item)}`).join(' ')}`;
|
|
187
|
+
}
|
|
188
|
+
if (runtime === 'grok') {
|
|
189
|
+
return `grok mcp add synomem -- synomem-mcp ${args.map(shellQuote).join(' ')}`;
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function assertSafeTarget(runtimeHome: string, target: string): void {
|
|
195
|
+
if (target !== join(runtimeHome, 'skills', 'synomem')) {
|
|
196
|
+
throw new SynomemError('UNSAFE_PATH', 'Refusing an unexpected skill destination.');
|
|
197
|
+
}
|
|
198
|
+
const skillsHome = dirname(target);
|
|
199
|
+
if (
|
|
200
|
+
!existsSync(runtimeHome) ||
|
|
201
|
+
lstatSync(runtimeHome).isSymbolicLink() ||
|
|
202
|
+
!lstatSync(runtimeHome).isDirectory() ||
|
|
203
|
+
(entryExists(skillsHome) &&
|
|
204
|
+
(lstatSync(skillsHome).isSymbolicLink() || !lstatSync(skillsHome).isDirectory()))
|
|
205
|
+
) {
|
|
206
|
+
throw new SynomemError('UNSAFE_PATH', `Refusing an unsafe runtime skill path: ${runtimeHome}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function replaceTarget(
|
|
211
|
+
location: SkillLocation,
|
|
212
|
+
source: string,
|
|
213
|
+
runtime: SkillRuntime,
|
|
214
|
+
link: boolean,
|
|
215
|
+
): void {
|
|
216
|
+
const parent = dirname(location.target);
|
|
217
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
218
|
+
const temporary = join(parent, `.synomem-${process.pid}-${Date.now()}.tmp`);
|
|
219
|
+
const backup = join(parent, `.synomem-${process.pid}-${Date.now()}.bak`);
|
|
220
|
+
try {
|
|
221
|
+
if (link) {
|
|
222
|
+
symlinkSync(source, temporary, process.platform === 'win32' ? 'junction' : 'dir');
|
|
223
|
+
} else {
|
|
224
|
+
cpSync(source, temporary, { recursive: true, errorOnExist: true });
|
|
225
|
+
const stamp: Stamp = {
|
|
226
|
+
package: 'synomem',
|
|
227
|
+
version: packageVersion(),
|
|
228
|
+
runtime,
|
|
229
|
+
mode: 'copy',
|
|
230
|
+
};
|
|
231
|
+
writeFileSync(join(temporary, stampName), `${JSON.stringify(stamp, null, 2)}\n`, {
|
|
232
|
+
encoding: 'utf8',
|
|
233
|
+
mode: 0o600,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (entryExists(location.target)) renameSync(location.target, backup);
|
|
237
|
+
renameSync(temporary, location.target);
|
|
238
|
+
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!entryExists(location.target) && entryExists(backup)) renameSync(backup, location.target);
|
|
241
|
+
throw error;
|
|
242
|
+
} finally {
|
|
243
|
+
if (entryExists(temporary)) rmSync(temporary, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function skillStatus(options: SkillOptions = {}): SkillOperationResult {
|
|
248
|
+
const userHome = resolve(options.userHome ?? homedir());
|
|
249
|
+
const source = sourcePath(options);
|
|
250
|
+
assertSource(source);
|
|
251
|
+
const homes = runtimeHomes(userHome, options.env ?? process.env);
|
|
252
|
+
const locations = selectedRuntimes(options).map((runtime) =>
|
|
253
|
+
inspect(runtime, homes[runtime], source),
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
changed: false,
|
|
257
|
+
dryRun: true,
|
|
258
|
+
packageVersion: packageVersion(),
|
|
259
|
+
locations,
|
|
260
|
+
mcpCommands: locations
|
|
261
|
+
.map((location) => mcpCommand(location.runtime, options.actorId, options.actorName))
|
|
262
|
+
.filter((value): value is string => Boolean(value)),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function installSkill(options: SkillOptions = {}): SkillOperationResult {
|
|
267
|
+
const result = skillStatus(options);
|
|
268
|
+
const source = sourcePath(options);
|
|
269
|
+
const desiredMode = options.link ? 'link' : 'copy';
|
|
270
|
+
for (const location of result.locations) {
|
|
271
|
+
if (
|
|
272
|
+
location.state === 'unavailable' ||
|
|
273
|
+
(location.state === 'current' && location.mode === desiredMode)
|
|
274
|
+
) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (location.state === 'conflict' && options.apply && !options.force) {
|
|
278
|
+
throw new SynomemError(
|
|
279
|
+
'INVALID_INPUT',
|
|
280
|
+
`${location.target} already exists and is not owned by Synomem; use --force to replace it.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (options.apply) {
|
|
284
|
+
assertSafeTarget(location.runtimeHome, location.target);
|
|
285
|
+
replaceTarget(location, source, location.runtime, Boolean(options.link));
|
|
286
|
+
result.changed = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
result.dryRun = !options.apply;
|
|
290
|
+
if (!options.apply) return result;
|
|
291
|
+
const updated = skillStatus({ ...options, apply: false });
|
|
292
|
+
return { ...updated, changed: result.changed, dryRun: false };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function uninstallSkill(options: SkillOptions = {}): SkillOperationResult {
|
|
296
|
+
const result = skillStatus(options);
|
|
297
|
+
for (const location of result.locations) {
|
|
298
|
+
if (location.state === 'missing' || location.state === 'unavailable') continue;
|
|
299
|
+
if (location.state === 'conflict' && options.apply && !options.force) {
|
|
300
|
+
throw new SynomemError(
|
|
301
|
+
'INVALID_INPUT',
|
|
302
|
+
`${location.target} is not owned by Synomem; refusing to remove it without --force.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (options.apply) {
|
|
306
|
+
assertSafeTarget(location.runtimeHome, location.target);
|
|
307
|
+
rmSync(location.target, { recursive: true, force: false });
|
|
308
|
+
result.changed = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
result.dryRun = !options.apply;
|
|
312
|
+
if (!options.apply) return result;
|
|
313
|
+
const updated = skillStatus({ ...options, apply: false });
|
|
314
|
+
return { ...updated, changed: result.changed, dryRun: false };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function formatSkillResult(
|
|
318
|
+
result: SkillOperationResult,
|
|
319
|
+
operation: 'status' | 'install' | 'uninstall',
|
|
320
|
+
): string {
|
|
321
|
+
const lines = result.locations.map((location) => {
|
|
322
|
+
const version = location.installedVersion ? ` (version ${location.installedVersion})` : '';
|
|
323
|
+
return `${location.runtime.padEnd(6)} ${location.state.padEnd(11)} ${location.target}${version}`;
|
|
324
|
+
});
|
|
325
|
+
if (operation !== 'status' && result.dryRun)
|
|
326
|
+
lines.push('', 'Dry run only. Re-run with --yes to apply.');
|
|
327
|
+
if (result.mcpCommands.length) lines.push('', 'MCP registration:', ...result.mcpCommands);
|
|
328
|
+
else if (operation === 'install') {
|
|
329
|
+
lines.push('', 'Tip: add --actor-id <agent-id> to print MCP registration commands.');
|
|
330
|
+
}
|
|
331
|
+
return lines.join('\n');
|
|
332
|
+
}
|