onlymcp 0.10.0 → 0.11.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/README.md +14 -11
- package/dist/apiClient.d.ts +32 -2
- package/dist/apiClient.js +42 -4
- package/dist/apiClient.js.map +1 -1
- package/dist/bin/cli.js +189 -151
- package/dist/bin/cli.js.map +1 -1
- package/dist/import/secrets.js +1 -1
- package/dist/import/secrets.js.map +1 -1
- package/dist/import/upload.js +7 -1
- package/dist/import/upload.js.map +1 -1
- package/dist/install.d.ts +66 -12
- package/dist/install.js +137 -63
- package/dist/install.js.map +1 -1
- package/dist/registry/caveats.d.ts +17 -0
- package/dist/registry/caveats.js +50 -0
- package/dist/registry/caveats.js.map +1 -0
- package/dist/registry/clients.d.ts +11 -0
- package/dist/registry/clients.js +327 -0
- package/dist/registry/clients.js.map +1 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.js +5 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/servers.d.ts +50 -0
- package/dist/registry/servers.js +225 -0
- package/dist/registry/servers.js.map +1 -0
- package/dist/registry/types.d.ts +54 -0
- package/dist/registry/types.js +6 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/stubs.js +2 -2
- package/dist/tools/getAgentDefinition.js +5 -2
- package/dist/tools/getAgentDefinition.js.map +1 -1
- package/dist/tools/getSkill.js +1 -1
- package/dist/tools/getSkill.js.map +1 -1
- package/dist/tools/getSkillFile.js +3 -3
- package/dist/tools/getSkillFile.js.map +1 -1
- package/dist/tools/memoryGraph.js +16 -11
- package/dist/tools/memoryGraph.js.map +1 -1
- package/dist/tools/memoryRead.js +1 -1
- package/dist/tools/memoryRead.js.map +1 -1
- package/dist/tools/memoryWrite.js +1 -1
- package/dist/tools/memoryWrite.js.map +1 -1
- package/package.json +3 -3
package/dist/bin/cli.js
CHANGED
|
@@ -5,15 +5,15 @@ import open from 'open';
|
|
|
5
5
|
import QRCode from 'qrcode';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import {
|
|
8
|
+
import { createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { pipeline } from 'node:stream/promises';
|
|
10
10
|
import { Readable } from 'node:stream';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
|
-
import { ApiClient } from '../apiClient.js';
|
|
12
|
+
import { ApiClient, ApiError } from '../apiClient.js';
|
|
13
13
|
import { Keyring } from '../auth/keyring.js';
|
|
14
14
|
import { pollForToken } from '../auth/deviceLogin.js';
|
|
15
15
|
import { TtlCache } from '../cache.js';
|
|
16
|
-
import {
|
|
16
|
+
import { planInstall, writeInstallTarget } from '../install.js';
|
|
17
17
|
import { stubDirs } from '../paths.js';
|
|
18
18
|
import { writeStubs } from '../stubs.js';
|
|
19
19
|
import { collect } from '../import/collect.js';
|
|
@@ -45,37 +45,16 @@ function safeOpenUrl(url, expectedOrigin) {
|
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
// `writeFileSync` allein wuerde ~/.claude.json oder ~/.gemini/settings.json
|
|
49
|
-
// komplett ersetzen und damit jede andere dort gespeicherte Einstellung
|
|
50
|
-
// wegwerfen. Eine vorhandene, aber kaputte Datei wird nie stillschweigend
|
|
51
|
-
// ueberschrieben, lieber ein Fehler als ein Datenverlust.
|
|
52
|
-
function mergeJsonMcpServer(path, key, value) {
|
|
53
|
-
let existing = {};
|
|
54
|
-
if (existsSync(path)) {
|
|
55
|
-
const raw = readFileSync(path, 'utf8').trim();
|
|
56
|
-
if (raw) {
|
|
57
|
-
try {
|
|
58
|
-
existing = JSON.parse(raw);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
throw new Error(`${path} enthaelt ungueltiges JSON. Nichts geschrieben.`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const mcpServers = existing.mcpServers ?? {};
|
|
66
|
-
existing.mcpServers = { ...mcpServers, [key]: value };
|
|
67
|
-
writeFileSync(path, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
|
|
68
|
-
}
|
|
69
48
|
program
|
|
70
49
|
.command('login')
|
|
71
|
-
.description('
|
|
50
|
+
.description('Connect this device to OnlyMCP')
|
|
72
51
|
.action(async () => {
|
|
73
52
|
intro('OnlyMCP');
|
|
74
53
|
// Einmal ehrlich sagen, was mitgeht, BEVOR es mitgeht. Die beiden Werte
|
|
75
54
|
// stehen nachher in der Geraeteliste dieses Kontos, sonst nirgends - aber
|
|
76
55
|
// wer sie nicht schicken will, soll das erfahren, solange er noch abbrechen
|
|
77
56
|
// kann.
|
|
78
|
-
log.info(`
|
|
57
|
+
log.info(`This device will register itself as: ${await deviceLabel()}`);
|
|
79
58
|
const res = await fetch(`${baseUrl}/api/auth/device/code`, {
|
|
80
59
|
method: 'POST',
|
|
81
60
|
headers: { 'content-type': 'application/json' },
|
|
@@ -92,13 +71,13 @@ program
|
|
|
92
71
|
await open(d.verification_uri_complete).catch(() => { });
|
|
93
72
|
}
|
|
94
73
|
const s = spinner();
|
|
95
|
-
s.start('
|
|
74
|
+
s.start('Waiting for confirmation');
|
|
96
75
|
const token = await pollForToken(baseUrl, d.device_code, d.interval ?? 5);
|
|
97
76
|
Keyring.save(token);
|
|
98
|
-
s.stop('
|
|
77
|
+
s.stop('Connected');
|
|
99
78
|
const api = new ApiClient(baseUrl, () => Keyring.read());
|
|
100
79
|
const counts = await writeStubsEverywhere(api);
|
|
101
|
-
outro(`${counts.skills}
|
|
80
|
+
outro(`${counts.skills} skills, ${counts.agents} agents, ${counts.files} files ready`);
|
|
102
81
|
});
|
|
103
82
|
/**
|
|
104
83
|
* Stubs in jeden Ordner, den ein Client auf diesem Geraet liest.
|
|
@@ -119,28 +98,66 @@ async function writeStubsEverywhere(api) {
|
|
|
119
98
|
}
|
|
120
99
|
return total;
|
|
121
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Reports a finished import or sync, for the "last import / last sync" line on the devices page.
|
|
103
|
+
* A report that fails (an older server answering 404, no network, a timeout) prints nothing and
|
|
104
|
+
* changes nothing: the command already succeeded.
|
|
105
|
+
*/
|
|
106
|
+
async function reportRun(api, kind, files) {
|
|
107
|
+
try {
|
|
108
|
+
await api.reportRun(kind, files);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* deliberately silent, see above */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ends this device's session on the server (next round A6). Clearing only the keyring left the
|
|
116
|
+
* session alive there, where it kept counting as a device until it expired. Best effort: null when
|
|
117
|
+
* the session ended, else what went wrong, and the logout goes on on this machine either way.
|
|
118
|
+
*/
|
|
119
|
+
async function endServerSession(token) {
|
|
120
|
+
try {
|
|
121
|
+
await new ApiClient(baseUrl, () => token).signOut();
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (e instanceof ApiError && e.status > 0)
|
|
126
|
+
return `HTTP ${e.status}`;
|
|
127
|
+
return e instanceof Error ? e.message : String(e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
122
130
|
program
|
|
123
131
|
.command('logout')
|
|
124
|
-
.description('
|
|
132
|
+
.description("End this device's session, then remove its login and cache")
|
|
125
133
|
.action(async () => {
|
|
134
|
+
// The server first, while the token is still here to name the session by.
|
|
135
|
+
const token = Keyring.read();
|
|
136
|
+
const failure = token ? await endServerSession(token) : null;
|
|
126
137
|
Keyring.clear();
|
|
127
138
|
await new TtlCache().clear();
|
|
128
|
-
|
|
139
|
+
if (failure) {
|
|
140
|
+
log.warn(`The session on the server could not be ended (${failure}). It counts as a device until it ` +
|
|
141
|
+
'expires, or until you remove it in the dashboard under Sessions and devices.');
|
|
142
|
+
log.success('Logged out on this device');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
log.success('Logged out');
|
|
129
146
|
});
|
|
130
147
|
program
|
|
131
148
|
.command('status')
|
|
132
|
-
.description('
|
|
149
|
+
.description('Show whether this device is logged in')
|
|
133
150
|
.action(() => {
|
|
134
|
-
log.info(Keyring.read() ? '
|
|
151
|
+
log.info(Keyring.read() ? 'logged in' : 'not logged in');
|
|
135
152
|
});
|
|
136
153
|
program
|
|
137
154
|
.command('sync')
|
|
138
|
-
.description('
|
|
139
|
-
.option('--memory <dir>', '
|
|
155
|
+
.description('Rewrite the stub files')
|
|
156
|
+
.option('--memory <dir>', 'Also write memory as files into this folder')
|
|
140
157
|
.action(async (opts) => {
|
|
141
158
|
const api = new ApiClient(baseUrl, () => Keyring.read());
|
|
142
159
|
const counts = await writeStubsEverywhere(api);
|
|
143
|
-
log.success(`${counts.skills}
|
|
160
|
+
log.success(`${counts.skills} skills, ${counts.agents} agents, ${counts.files} files`);
|
|
144
161
|
if (counts.kept.length > 0) {
|
|
145
162
|
// Stubs liegen in denselben Ordnern, aus denen importiert wird. Was
|
|
146
163
|
// dort schon stand und kein Stub war, bleibt stehen - aber nicht
|
|
@@ -148,85 +165,103 @@ program
|
|
|
148
165
|
// wurde. Gekuerzt, weil ein Geraet mit 270 eigenen Agent-Dateien sonst
|
|
149
166
|
// 270 Zeilen bekaeme und die Erfolgsmeldung aus dem Bild schoebe.
|
|
150
167
|
log.warn([
|
|
151
|
-
`
|
|
168
|
+
`Left untouched, because something of your own is already there (${counts.kept.length})`,
|
|
152
169
|
...counts.kept.slice(0, 10).map((k) => ` ${k}`),
|
|
153
|
-
...(counts.kept.length > 10 ? [` ...
|
|
170
|
+
...(counts.kept.length > 10 ? [` ... and ${counts.kept.length - 10} more`] : []),
|
|
154
171
|
].join('\n'));
|
|
155
172
|
}
|
|
156
|
-
|
|
173
|
+
// Every file this run wrote: the stubs, the skills' companion files, and (below) memory.
|
|
174
|
+
const stubFiles = counts.skills + counts.agents + counts.files;
|
|
175
|
+
if (!opts.memory) {
|
|
176
|
+
await reportRun(api, 'sync', stubFiles);
|
|
157
177
|
return;
|
|
178
|
+
}
|
|
158
179
|
const m = await syncMemoryToDisk(api, opts.memory);
|
|
159
|
-
log.success(`${m.written}
|
|
180
|
+
log.success(`${m.written} memory files to ${opts.memory}`);
|
|
160
181
|
if (m.collisions.length > 0) {
|
|
161
|
-
log.warn([
|
|
182
|
+
log.warn([
|
|
183
|
+
'Team files with a path an existing file of yours already has',
|
|
184
|
+
...m.collisions.map((p) => ` ${p}`),
|
|
185
|
+
].join('\n'));
|
|
162
186
|
}
|
|
163
187
|
if (m.refused.length > 0) {
|
|
164
|
-
log.warn(['
|
|
188
|
+
log.warn(['Paths refused', ...m.refused.map((p) => ` ${p}`)].join('\n'));
|
|
165
189
|
}
|
|
166
190
|
if (m.failures.length > 0) {
|
|
167
|
-
log.error([`${m.failures.length}
|
|
191
|
+
log.error([`${m.failures.length} failed`, ...m.failures.map((f) => ` ${f.what} ${f.code}`)].join('\n'));
|
|
168
192
|
process.exitCode = 1;
|
|
169
193
|
}
|
|
194
|
+
// A sync with failures is not a successful one: nothing is reported for it.
|
|
195
|
+
if (m.failures.length === 0)
|
|
196
|
+
await reportRun(api, 'sync', stubFiles + m.written);
|
|
170
197
|
});
|
|
171
198
|
program
|
|
172
199
|
.command('install')
|
|
173
|
-
.description('OnlyMCP in
|
|
174
|
-
.option('--client <id>', 'claude, codex
|
|
175
|
-
.option('--
|
|
200
|
+
.description('Register OnlyMCP in an MCP client')
|
|
201
|
+
.option('--client <id>', 'claude-code, codex or gemini-cli (aliases: claude, gemini)')
|
|
202
|
+
.option('--all', 'Write every config file that already exists on this machine')
|
|
203
|
+
.option('--print', 'Only print, write nothing')
|
|
176
204
|
.action((opts) => {
|
|
177
205
|
// ONLYMCP_HOME ist dieselbe Testschleuse wie in paths.ts: damit trifft
|
|
178
206
|
// kein Testlauf jemals den echten ~/.codex- oder ~/.claude-Ordner.
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// laden dazu ein, beide zu gehen.
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
mkdirSync(dirname(t.configPath), { recursive: true });
|
|
202
|
-
try {
|
|
203
|
-
if (t.id === 'codex') {
|
|
204
|
-
// TOML wird angehaengt, nicht ersetzt. Ein zweiter Lauf haengt sonst
|
|
205
|
-
// ein zweites [mcp_servers.onlymcp] an, und zwei gleiche Tabellen
|
|
206
|
-
// machen die Datei fuer die meisten TOML-Parser ungueltig.
|
|
207
|
-
const already = existsSync(t.configPath) && readFileSync(t.configPath, 'utf8').includes('[mcp_servers.onlymcp]');
|
|
208
|
-
if (already) {
|
|
209
|
-
log.info(`${t.label} ist bereits eingerichtet: ${t.configPath}`);
|
|
210
|
-
return;
|
|
207
|
+
const home = process.env.ONLYMCP_HOME ?? homedir();
|
|
208
|
+
const outcome = planInstall(home, opts);
|
|
209
|
+
/** Writes one target, or only prints it with --print. Shared by --client and --all. */
|
|
210
|
+
function apply(t) {
|
|
211
|
+
if (opts.print) {
|
|
212
|
+
log.info(`${t.name} -> ${t.configPath}`);
|
|
213
|
+
console.log(t.snippet);
|
|
214
|
+
// Bewusst keine zweite Zeile mit ONLYMCP_STUB_DIRS: die Variable steht
|
|
215
|
+
// seit install.ts im Schnipsel selbst. Zwei Wege, dasselbe zu setzen,
|
|
216
|
+
// laden dazu ein, beide zu gehen.
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const result = writeInstallTarget(t);
|
|
221
|
+
if (result === 'already-present') {
|
|
222
|
+
log.info(`${t.name} is already set up: ${t.configPath}`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
log.success(`${t.name} set up: ${t.configPath}`);
|
|
211
226
|
}
|
|
212
|
-
appendFileSync(t.configPath, `\n${t.snippet}`);
|
|
213
227
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!entry)
|
|
218
|
-
throw new Error(`${t.label}: leerer mcpServers-Schnipsel.`);
|
|
219
|
-
mergeJsonMcpServer(t.configPath, entry[0], entry[1]);
|
|
228
|
+
catch (e) {
|
|
229
|
+
log.error(e instanceof Error ? e.message : String(e));
|
|
230
|
+
process.exitCode = 1;
|
|
220
231
|
}
|
|
221
232
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
switch (outcome.kind) {
|
|
234
|
+
case 'conflicting-options':
|
|
235
|
+
log.error('Pass either --client <id> or --all, not both.');
|
|
236
|
+
process.exitCode = 2;
|
|
237
|
+
return;
|
|
238
|
+
case 'missing-client':
|
|
239
|
+
log.info(`Known clients: ${outcome.targets.map((t) => t.id).join(', ')}`);
|
|
240
|
+
log.info('Pass --client <id>, or --all.');
|
|
241
|
+
process.exitCode = 2;
|
|
242
|
+
return;
|
|
243
|
+
case 'unknown-client':
|
|
244
|
+
// Kein Raten. Ein falsch geratener Pfad ueberschreibt sonst eine
|
|
245
|
+
// fremde Konfigurationsdatei.
|
|
246
|
+
log.error(`Unknown client. Known: ${outcome.targets.map((t) => t.id).join(', ')}`);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return;
|
|
249
|
+
case 'none-present-for-all':
|
|
250
|
+
log.error('No known client is set up on this machine yet.');
|
|
251
|
+
log.info(`Pass --client <id> to write its config anyway. Known: ${outcome.targets.map((t) => t.id).join(', ')}.`);
|
|
252
|
+
process.exitCode = 2;
|
|
253
|
+
return;
|
|
254
|
+
case 'single':
|
|
255
|
+
apply(outcome.target);
|
|
256
|
+
return;
|
|
257
|
+
case 'all':
|
|
258
|
+
for (const t of outcome.present)
|
|
259
|
+
apply(t);
|
|
260
|
+
if (outcome.skipped.length > 0) {
|
|
261
|
+
log.info(`Skipped, not set up on this machine: ${outcome.skipped.map((t) => t.id).join(', ')}`);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
226
264
|
}
|
|
227
|
-
if (t.stubDir)
|
|
228
|
-
mkdirSync(t.stubDir, { recursive: true });
|
|
229
|
-
log.success(`${t.label} eingerichtet: ${t.configPath}`);
|
|
230
265
|
});
|
|
231
266
|
function formatBytes(bytes) {
|
|
232
267
|
if (bytes < 1024)
|
|
@@ -244,54 +279,54 @@ function printPlan(plan, roots) {
|
|
|
244
279
|
const withFiles = plan.skills.filter((s) => s.files.length > 0).length;
|
|
245
280
|
const fileBytes = plan.skills.reduce((n, s) => n + sumBytes(s.files), 0);
|
|
246
281
|
if (roots.skills) {
|
|
247
|
-
log.message(`Skills ${String(plan.skills.length).padStart(4)}
|
|
248
|
-
` ${String(files).padStart(4)}
|
|
282
|
+
log.message(`Skills ${String(plan.skills.length).padStart(4)} from ${roots.skills}\n` +
|
|
283
|
+
` ${String(files).padStart(4)} more files across ${withFiles} skills ` +
|
|
249
284
|
`${formatBytes(sumBytes(plan.skills) + fileBytes)}`);
|
|
250
285
|
}
|
|
251
286
|
if (roots.agents) {
|
|
252
|
-
log.message(`Agents ${String(plan.agents.length).padStart(4)}
|
|
287
|
+
log.message(`Agents ${String(plan.agents.length).padStart(4)} from ${roots.agents} ${formatBytes(sumBytes(plan.agents))}`);
|
|
253
288
|
}
|
|
254
289
|
if (roots.memory) {
|
|
255
|
-
log.message(`Memory ${String(plan.memory.length).padStart(4)}
|
|
290
|
+
log.message(`Memory ${String(plan.memory.length).padStart(4)} from ${roots.memory} ${formatBytes(sumBytes(plan.memory))}`);
|
|
256
291
|
}
|
|
257
292
|
if (plan.skipped.length > 0) {
|
|
258
293
|
const byReason = new Map();
|
|
259
294
|
for (const s of plan.skipped)
|
|
260
295
|
byReason.set(s.reason, (byReason.get(s.reason) ?? 0) + 1);
|
|
261
|
-
log.message(['
|
|
296
|
+
log.message(['Skipped', ...[...byReason].map(([reason, n]) => ` ${String(n).padStart(4)} ${reason}`)].join('\n'));
|
|
262
297
|
}
|
|
263
298
|
if (plan.noDescription.length > 0) {
|
|
264
|
-
//
|
|
299
|
+
// Geht trotzdem hoch. Gemeldet, weil die Liste im Dashboard und die
|
|
265
300
|
// Stubs auf jedem Geraet von der Beschreibung leben und ein Skill ohne
|
|
266
301
|
// sie dort als blosser Name steht.
|
|
267
|
-
log.warn(['
|
|
302
|
+
log.warn(['Missing a description in the SKILL.md header', ...plan.noDescription.map((n) => ` ${n}`)].join('\n'));
|
|
268
303
|
}
|
|
269
304
|
if (plan.flagged.length > 0) {
|
|
270
305
|
log.warn([
|
|
271
|
-
'
|
|
306
|
+
'Held back over possible credentials (none of it left this machine)',
|
|
272
307
|
...plan.flagged.flatMap((f) => [
|
|
273
308
|
` ${f.relative}`,
|
|
274
|
-
...f.hits.map((h) => `
|
|
309
|
+
...f.hits.map((h) => ` line ${h.line} rule ${h.rule} ${h.excerpt}`),
|
|
275
310
|
]),
|
|
276
311
|
].join('\n'));
|
|
277
312
|
}
|
|
278
313
|
}
|
|
279
314
|
program
|
|
280
315
|
.command('import')
|
|
281
|
-
.description('
|
|
282
|
-
.option('--skills <dir>', '
|
|
283
|
-
.option('--agents <dir>', '
|
|
316
|
+
.description('Bring existing local skills, agents and memory into OnlyMCP')
|
|
317
|
+
.option('--skills <dir>', 'Folder with skill subfolders, or a plugin cache')
|
|
318
|
+
.option('--agents <dir>', 'Folder with agent files')
|
|
284
319
|
// --memory hat mit Absicht keine Vorgabe. Skills und Agents liegen an einem
|
|
285
320
|
// bekannten Pfad, an dem nur Skills und Agents liegen. Ein Memory-Ordner
|
|
286
321
|
// nicht: eine Vorgabe auf irgendjemandes Notizen zu richten und die
|
|
287
322
|
// hochzuladen waere das Schlimmste, was dieser Befehl tun koennte.
|
|
288
|
-
.option('--memory <dir>', '
|
|
289
|
-
.option('--dry-run', '
|
|
290
|
-
.option('--yes', '
|
|
323
|
+
.option('--memory <dir>', 'Folder with memory files (no default)')
|
|
324
|
+
.option('--dry-run', 'Only show what would be uploaded, then stop')
|
|
325
|
+
.option('--yes', 'Upload without asking first')
|
|
291
326
|
// --prune loescht im Konto. Es hat deshalb bewusst KEINE Abkuerzung ueber
|
|
292
327
|
// --yes: die Liste wird immer gezeigt und immer einzeln bestaetigt, auch
|
|
293
328
|
// wenn der Upload selbst unbeaufsichtigt laeuft.
|
|
294
|
-
.option('--prune', '
|
|
329
|
+
.option('--prune', 'Delete, after confirmation, what the account holds and the source no longer has')
|
|
295
330
|
.action(async (opts) => {
|
|
296
331
|
// Dieselbe Testschleuse wie in install.ts und paths.ts.
|
|
297
332
|
const home = process.env.ONLYMCP_HOME ?? homedir();
|
|
@@ -302,7 +337,7 @@ program
|
|
|
302
337
|
};
|
|
303
338
|
intro('OnlyMCP Import');
|
|
304
339
|
const s = spinner();
|
|
305
|
-
s.start('
|
|
340
|
+
s.start('Reading the folders');
|
|
306
341
|
let plan;
|
|
307
342
|
try {
|
|
308
343
|
const [skills, agents, memory] = await Promise.all([
|
|
@@ -313,12 +348,12 @@ program
|
|
|
313
348
|
plan = buildImportPlan({ skills, agents, memory }, { skillsRoot: roots.skills });
|
|
314
349
|
}
|
|
315
350
|
catch (e) {
|
|
316
|
-
s.stop('
|
|
351
|
+
s.stop('Aborted');
|
|
317
352
|
log.error(e instanceof Error ? e.message : String(e));
|
|
318
353
|
process.exitCode = 1;
|
|
319
354
|
return;
|
|
320
355
|
}
|
|
321
|
-
s.stop('
|
|
356
|
+
s.stop('Read');
|
|
322
357
|
printPlan(plan, {
|
|
323
358
|
skills: existsSync(roots.skills) ? roots.skills : undefined,
|
|
324
359
|
agents: existsSync(roots.agents) ? roots.agents : undefined,
|
|
@@ -328,11 +363,11 @@ program
|
|
|
328
363
|
// Mit --prune gibt es auch dann etwas zu tun, wenn nichts hochgeht: genau
|
|
329
364
|
// ein leerer Quellordner ist der Fall, in dem am meisten geloescht wuerde.
|
|
330
365
|
if (total === 0 && !opts.prune) {
|
|
331
|
-
outro('
|
|
366
|
+
outro('Nothing to do.');
|
|
332
367
|
return;
|
|
333
368
|
}
|
|
334
369
|
if (opts.dryRun && !opts.prune) {
|
|
335
|
-
outro('
|
|
370
|
+
outro('Nothing uploaded (--dry-run).');
|
|
336
371
|
return;
|
|
337
372
|
}
|
|
338
373
|
const api = new ApiClient(baseUrl, () => Keyring.read());
|
|
@@ -340,11 +375,11 @@ program
|
|
|
340
375
|
// behebender Fehler. Die Gegenmassnahme ist eine Zeile Ausgabe.
|
|
341
376
|
try {
|
|
342
377
|
const me = await api.getMe();
|
|
343
|
-
log.info(`
|
|
378
|
+
log.info(`Target ${baseUrl} as ${me.email} plan ${me.plan.name}`);
|
|
344
379
|
}
|
|
345
380
|
catch (e) {
|
|
346
|
-
log.error(`
|
|
347
|
-
log.info('
|
|
381
|
+
log.error(`Not logged in, or the server is unreachable (${e instanceof Error ? e.message : String(e)}).`);
|
|
382
|
+
log.info('First: npx onlymcp login');
|
|
348
383
|
process.exitCode = 1;
|
|
349
384
|
return;
|
|
350
385
|
}
|
|
@@ -353,24 +388,24 @@ program
|
|
|
353
388
|
// Ohne Terminal gibt es keine Rueckfrage, also auch keinen Upload.
|
|
354
389
|
// Wer das automatisieren will, sagt --yes und hat den Plan vorher mit
|
|
355
390
|
// --dry-run gesehen.
|
|
356
|
-
log.error('
|
|
391
|
+
log.error('No terminal for the prompt. Run --dry-run first, then --yes.');
|
|
357
392
|
process.exitCode = 1;
|
|
358
393
|
return;
|
|
359
394
|
}
|
|
360
|
-
const go = await confirm({ message:
|
|
395
|
+
const go = await confirm({ message: `Upload ${total} entries?`, initialValue: false });
|
|
361
396
|
if (isCancel(go) || !go) {
|
|
362
|
-
cancel('
|
|
397
|
+
cancel('Cancelled. Nothing uploaded.');
|
|
363
398
|
return;
|
|
364
399
|
}
|
|
365
400
|
if (plan.flagged.length > 0) {
|
|
366
401
|
const release = await multiselect({
|
|
367
|
-
message: '
|
|
402
|
+
message: 'Send the held-back files anyway?',
|
|
368
403
|
options: plan.flagged.map((f) => ({ value: f.relative, label: f.relative })),
|
|
369
404
|
required: false,
|
|
370
405
|
initialValues: [],
|
|
371
406
|
});
|
|
372
407
|
if (isCancel(release)) {
|
|
373
|
-
cancel('
|
|
408
|
+
cancel('Cancelled. Nothing uploaded.');
|
|
374
409
|
return;
|
|
375
410
|
}
|
|
376
411
|
if (release.length > 0) {
|
|
@@ -394,17 +429,20 @@ program
|
|
|
394
429
|
}
|
|
395
430
|
if (total > 0 && !opts.dryRun) {
|
|
396
431
|
const up = spinner();
|
|
397
|
-
up.start('
|
|
432
|
+
up.start('Uploading');
|
|
398
433
|
const result = await runUpload(api, plan, (done, all, label) => {
|
|
399
434
|
up.message(`${done}/${all} ${label}`);
|
|
400
435
|
});
|
|
401
|
-
up.stop('
|
|
402
|
-
log.success(`${result.skills}
|
|
403
|
-
`${result.agents}
|
|
436
|
+
up.stop('Done');
|
|
437
|
+
log.success(`${result.skills} skills, ${result.skillFiles} skill files, ` +
|
|
438
|
+
`${result.agents} agents, ${result.memory} memory files`);
|
|
404
439
|
if (result.failures.length > 0) {
|
|
405
|
-
log.error([`${result.failures.length}
|
|
440
|
+
log.error([`${result.failures.length} failed`, ...result.failures.map((f) => ` ${f.what} ${f.code}`)].join('\n'));
|
|
406
441
|
process.exitCode = 1;
|
|
407
442
|
}
|
|
443
|
+
else {
|
|
444
|
+
await reportRun(api, 'import', result.skills + result.skillFiles + result.agents + result.memory);
|
|
445
|
+
}
|
|
408
446
|
}
|
|
409
447
|
if (opts.prune) {
|
|
410
448
|
// Verglichen wird nur, was auch gelesen wurde. Ohne --memory sagt ein
|
|
@@ -417,39 +455,39 @@ program
|
|
|
417
455
|
};
|
|
418
456
|
const targets = await planPrune(api, plan, covered);
|
|
419
457
|
if (isEmpty(targets)) {
|
|
420
|
-
log.info('
|
|
458
|
+
log.info('Nothing to delete: the account holds nothing the source does not know.');
|
|
421
459
|
}
|
|
422
460
|
else {
|
|
423
461
|
// Vollstaendig aufgezaehlt, nicht gezaehlt. Eine Zahl zu bestaetigen
|
|
424
462
|
// heisst, etwas zu bestaetigen, das man nicht gesehen hat.
|
|
425
463
|
log.warn([
|
|
426
|
-
'
|
|
464
|
+
'Would delete (only your own, nothing from a team, nothing archived)',
|
|
427
465
|
...targets.skills.map((n) => ` Skill ${n}`),
|
|
428
466
|
...targets.agents.map((n) => ` Agent ${n}`),
|
|
429
467
|
...targets.memory.map((p) => ` Memory ${p}`),
|
|
430
468
|
].join('\n'));
|
|
431
469
|
if (opts.dryRun) {
|
|
432
|
-
log.info('
|
|
470
|
+
log.info('Nothing deleted (--dry-run).');
|
|
433
471
|
}
|
|
434
472
|
else if (!process.stdin.isTTY) {
|
|
435
473
|
// Kein --yes-Durchgriff: geloescht wird nur von Hand.
|
|
436
|
-
log.error('
|
|
474
|
+
log.error('No terminal for the prompt. Nothing deleted.');
|
|
437
475
|
process.exitCode = 1;
|
|
438
476
|
}
|
|
439
477
|
else {
|
|
440
478
|
const go = await confirm({
|
|
441
|
-
message:
|
|
479
|
+
message: `Delete ${countTargets(targets)} entries in the account?`,
|
|
442
480
|
initialValue: false,
|
|
443
481
|
});
|
|
444
482
|
if (isCancel(go) || !go) {
|
|
445
|
-
log.info('
|
|
483
|
+
log.info('Nothing deleted.');
|
|
446
484
|
}
|
|
447
485
|
else {
|
|
448
486
|
const pruned = await runPrune(api, targets);
|
|
449
|
-
log.success(`${pruned.deleted}
|
|
487
|
+
log.success(`${pruned.deleted} deleted`);
|
|
450
488
|
if (pruned.failures.length > 0) {
|
|
451
489
|
log.error([
|
|
452
|
-
`${pruned.failures.length}
|
|
490
|
+
`${pruned.failures.length} failed`,
|
|
453
491
|
...pruned.failures.map((f) => ` ${f.what} ${f.code}`),
|
|
454
492
|
].join('\n'));
|
|
455
493
|
process.exitCode = 1;
|
|
@@ -458,27 +496,27 @@ program
|
|
|
458
496
|
}
|
|
459
497
|
}
|
|
460
498
|
}
|
|
461
|
-
outro(opts.dryRun ? '
|
|
499
|
+
outro(opts.dryRun ? 'Nothing changed (--dry-run).' : 'Then: npx onlymcp sync on every device.');
|
|
462
500
|
});
|
|
463
501
|
program
|
|
464
502
|
.command('export')
|
|
465
|
-
.description('
|
|
466
|
-
.option('--out <dir>', '
|
|
467
|
-
.option('--zip <file>', '
|
|
468
|
-
.option('--scope <scope>', 'personal, all
|
|
503
|
+
.description('Get everything out of the account: memory, skills, agents, connections')
|
|
504
|
+
.option('--out <dir>', 'Unpack the archive into this folder')
|
|
505
|
+
.option('--zip <file>', 'Keep the archive as a file')
|
|
506
|
+
.option('--scope <scope>', 'personal, all or team:<id>', 'all')
|
|
469
507
|
.option('--types <list>', 'memory,skills,agents,connections')
|
|
470
508
|
.action(async (opts) => {
|
|
471
509
|
// Genau eines von beiden. Beides hiesse zweimal herunterladen, keines
|
|
472
510
|
// hiesse, die teuerste Leseoperation des Produkts ins Nichts zu schicken.
|
|
473
511
|
if ((opts.out ? 1 : 0) + (opts.zip ? 1 : 0) !== 1) {
|
|
474
|
-
log.error('
|
|
512
|
+
log.error('Either --out <folder> or --zip <file>, not both and not neither.');
|
|
475
513
|
process.exitCode = 1;
|
|
476
514
|
return;
|
|
477
515
|
}
|
|
478
516
|
intro('OnlyMCP Export');
|
|
479
517
|
const api = new ApiClient(baseUrl, () => Keyring.read());
|
|
480
518
|
const s = spinner();
|
|
481
|
-
s.start('
|
|
519
|
+
s.start('Fetching the archive');
|
|
482
520
|
let res;
|
|
483
521
|
try {
|
|
484
522
|
res = await api.exportArchive({
|
|
@@ -487,9 +525,9 @@ program
|
|
|
487
525
|
});
|
|
488
526
|
}
|
|
489
527
|
catch (e) {
|
|
490
|
-
s.stop('
|
|
528
|
+
s.stop('Aborted');
|
|
491
529
|
log.error(e instanceof Error ? e.message : String(e));
|
|
492
|
-
log.info('
|
|
530
|
+
log.info('Not logged in? First: npx onlymcp login');
|
|
493
531
|
process.exitCode = 1;
|
|
494
532
|
return;
|
|
495
533
|
}
|
|
@@ -498,28 +536,28 @@ program
|
|
|
498
536
|
// Gigabyte soll nicht erst zwei Gigabyte Arbeitsspeicher kosten.
|
|
499
537
|
mkdirSync(dirname(opts.zip), { recursive: true });
|
|
500
538
|
await pipeline(Readable.fromWeb(res.body), createWriteStream(opts.zip));
|
|
501
|
-
s.stop('
|
|
502
|
-
outro(`
|
|
539
|
+
s.stop('Done');
|
|
540
|
+
outro(`Archive: ${opts.zip}`);
|
|
503
541
|
return;
|
|
504
542
|
}
|
|
505
543
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
506
544
|
const result = await unpackArchive(bytes, opts.out);
|
|
507
|
-
s.stop('
|
|
508
|
-
log.success(`${result.written}
|
|
545
|
+
s.stop('Done');
|
|
546
|
+
log.success(`${result.written} files in ${opts.out}`);
|
|
509
547
|
if (result.refused.length > 0) {
|
|
510
|
-
log.warn(['
|
|
548
|
+
log.warn(['Not written, path in the archive is not allowed', ...result.refused.map((p) => ` ${p}`)].join('\n'));
|
|
511
549
|
}
|
|
512
550
|
if (result.failed.length > 0) {
|
|
513
551
|
// Auf Windows der Normalfall fuer einen Skill mit Namensraum: der
|
|
514
552
|
// Doppelpunkt ist dort kein zulaessiges Zeichen in einem Ordnernamen.
|
|
515
553
|
log.warn([
|
|
516
|
-
|
|
554
|
+
"Not written, the filename doesn't work on this system",
|
|
517
555
|
...result.failed.map((f) => ` ${f.path} ${f.reason}`),
|
|
518
|
-
'
|
|
556
|
+
'With --zip <file> the archive stays unchanged.',
|
|
519
557
|
].join('\n'));
|
|
520
558
|
process.exitCode = 1;
|
|
521
559
|
}
|
|
522
|
-
outro(`
|
|
560
|
+
outro(`Restore: npx onlymcp import --skills ${opts.out}/skills --agents ${opts.out}/agents --memory ${opts.out}/memory`);
|
|
523
561
|
});
|
|
524
562
|
await program.parseAsync();
|
|
525
563
|
//# sourceMappingURL=cli.js.map
|