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.
Files changed (42) hide show
  1. package/README.md +14 -11
  2. package/dist/apiClient.d.ts +32 -2
  3. package/dist/apiClient.js +42 -4
  4. package/dist/apiClient.js.map +1 -1
  5. package/dist/bin/cli.js +189 -151
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/import/secrets.js +1 -1
  8. package/dist/import/secrets.js.map +1 -1
  9. package/dist/import/upload.js +7 -1
  10. package/dist/import/upload.js.map +1 -1
  11. package/dist/install.d.ts +66 -12
  12. package/dist/install.js +137 -63
  13. package/dist/install.js.map +1 -1
  14. package/dist/registry/caveats.d.ts +17 -0
  15. package/dist/registry/caveats.js +50 -0
  16. package/dist/registry/caveats.js.map +1 -0
  17. package/dist/registry/clients.d.ts +11 -0
  18. package/dist/registry/clients.js +327 -0
  19. package/dist/registry/clients.js.map +1 -0
  20. package/dist/registry/index.d.ts +4 -0
  21. package/dist/registry/index.js +5 -0
  22. package/dist/registry/index.js.map +1 -0
  23. package/dist/registry/servers.d.ts +50 -0
  24. package/dist/registry/servers.js +225 -0
  25. package/dist/registry/servers.js.map +1 -0
  26. package/dist/registry/types.d.ts +54 -0
  27. package/dist/registry/types.js +6 -0
  28. package/dist/registry/types.js.map +1 -0
  29. package/dist/stubs.js +2 -2
  30. package/dist/tools/getAgentDefinition.js +5 -2
  31. package/dist/tools/getAgentDefinition.js.map +1 -1
  32. package/dist/tools/getSkill.js +1 -1
  33. package/dist/tools/getSkill.js.map +1 -1
  34. package/dist/tools/getSkillFile.js +3 -3
  35. package/dist/tools/getSkillFile.js.map +1 -1
  36. package/dist/tools/memoryGraph.js +16 -11
  37. package/dist/tools/memoryGraph.js.map +1 -1
  38. package/dist/tools/memoryRead.js +1 -1
  39. package/dist/tools/memoryRead.js.map +1 -1
  40. package/dist/tools/memoryWrite.js +1 -1
  41. package/dist/tools/memoryWrite.js.map +1 -1
  42. 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 { appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
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 { targets } from '../install.js';
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('Dieses Geraet mit OnlyMCP verbinden')
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(`Dieses Geraet meldet sich als: ${await deviceLabel()}`);
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('Warte auf Bestaetigung');
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('Verbunden');
77
+ s.stop('Connected');
99
78
  const api = new ApiClient(baseUrl, () => Keyring.read());
100
79
  const counts = await writeStubsEverywhere(api);
101
- outro(`${counts.skills} Skills, ${counts.agents} Agents, ${counts.files} Dateien bereit`);
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('Anmeldung und Cache auf diesem Geraet loeschen')
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
- log.success('Abgemeldet');
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('Zeigt, ob dieses Geraet angemeldet ist')
149
+ .description('Show whether this device is logged in')
133
150
  .action(() => {
134
- log.info(Keyring.read() ? 'angemeldet' : 'nicht angemeldet');
151
+ log.info(Keyring.read() ? 'logged in' : 'not logged in');
135
152
  });
136
153
  program
137
154
  .command('sync')
138
- .description('Stub-Dateien neu schreiben')
139
- .option('--memory <dir>', 'Memory zusaetzlich als Dateien in diesen Ordner schreiben')
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} Skills, ${counts.agents} Agents, ${counts.files} Dateien`);
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
- `Unberuehrt gelassen, weil dort schon etwas Eigenes liegt (${counts.kept.length})`,
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 ? [` ... und ${counts.kept.length - 10} weitere`] : []),
170
+ ...(counts.kept.length > 10 ? [` ... and ${counts.kept.length - 10} more`] : []),
154
171
  ].join('\n'));
155
172
  }
156
- if (!opts.memory)
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} Memory-Dateien nach ${opts.memory}`);
180
+ log.success(`${m.written} memory files to ${opts.memory}`);
160
181
  if (m.collisions.length > 0) {
161
- log.warn(['Team-Dateien mit einem Pfad, den eine eigene Datei schon hat', ...m.collisions.map((p) => ` ${p}`)].join('\n'));
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(['Pfade abgelehnt', ...m.refused.map((p) => ` ${p}`)].join('\n'));
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} fehlgeschlagen`, ...m.failures.map((f) => ` ${f.what} ${f.code}`)].join('\n'));
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 einem MCP-Client eintragen')
174
- .option('--client <id>', 'claude, codex oder gemini')
175
- .option('--print', 'nur ausgeben, nichts schreiben')
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 all = targets(process.env.ONLYMCP_HOME ?? homedir());
180
- if (!opts.client) {
181
- log.info(`Bekannte Clients: ${all.map((t) => t.id).join(', ')}`);
182
- log.info('Fuer jeden anderen MCP-Client: --print und den Schnipsel selbst eintragen.');
183
- return;
184
- }
185
- const t = all.find((x) => x.id === opts.client);
186
- if (!t) {
187
- // Kein Raten. Ein falsch geratener Pfad ueberschreibt sonst eine fremde
188
- // Konfigurationsdatei.
189
- log.error(`Unbekannter Client. Bekannt: ${all.map((x) => x.id).join(', ')}`);
190
- process.exitCode = 1;
191
- return;
192
- }
193
- if (opts.print) {
194
- log.info(`${t.label} -> ${t.configPath}`);
195
- console.log(t.snippet);
196
- // Bewusst keine zweite Zeile mit ONLYMCP_STUB_DIRS: die Variable steht
197
- // seit install.ts im Schnipsel selbst. Zwei Wege, dasselbe zu setzen,
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
- else {
215
- const parsed = JSON.parse(t.snippet);
216
- const entry = Object.entries(parsed.mcpServers)[0];
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
- catch (e) {
223
- log.error(e instanceof Error ? e.message : String(e));
224
- process.exitCode = 1;
225
- return;
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)} aus ${roots.skills}\n` +
248
- ` ${String(files).padStart(4)} weitere Dateien in ${withFiles} Skills ` +
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)} aus ${roots.agents} ${formatBytes(sumBytes(plan.agents))}`);
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)} aus ${roots.memory} ${formatBytes(sumBytes(plan.memory))}`);
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(['Uebersprungen', ...[...byReason].map(([reason, n]) => ` ${String(n).padStart(4)} ${reason}`)].join('\n'));
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
- // Gehen trotzdem hoch. Gemeldet, weil die Liste im Dashboard und die
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(['Ohne description im Kopf der SKILL.md', ...plan.noDescription.map((n) => ` ${n}`)].join('\n'));
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
- 'Zurueckgehalten wegen moeglicher Zugangsdaten (nichts davon geht raus)',
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) => ` Zeile ${h.line} Regel ${h.rule} ${h.excerpt}`),
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('Bestehende lokale Skills, Agents und Memory nach OnlyMCP holen')
282
- .option('--skills <dir>', 'Ordner mit Skill-Unterordnern oder ein Plugin-Cache')
283
- .option('--agents <dir>', 'Ordner mit Agent-Dateien')
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>', 'Ordner mit Memory-Dateien (keine Vorgabe)')
289
- .option('--dry-run', 'Nur zeigen, was hochginge, und beenden')
290
- .option('--yes', 'Ohne Rueckfrage hochladen')
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', 'Was im Konto steht und in der Quelle fehlt, nach Rueckfrage loeschen')
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('Lese die Ordner');
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('Abgebrochen');
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('Gelesen');
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('Nichts zu tun.');
366
+ outro('Nothing to do.');
332
367
  return;
333
368
  }
334
369
  if (opts.dryRun && !opts.prune) {
335
- outro('Nichts hochgeladen (--dry-run).');
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(`Ziel ${baseUrl} als ${me.email} Stufe ${me.plan.name}`);
378
+ log.info(`Target ${baseUrl} as ${me.email} plan ${me.plan.name}`);
344
379
  }
345
380
  catch (e) {
346
- log.error(`Nicht angemeldet oder Server nicht erreichbar (${e instanceof Error ? e.message : String(e)}).`);
347
- log.info('Zuerst: onlymcp login');
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('Kein Terminal fuer die Rueckfrage. Erst --dry-run ansehen, dann --yes.');
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: `${total} Eintraege hochladen?`, initialValue: false });
395
+ const go = await confirm({ message: `Upload ${total} entries?`, initialValue: false });
361
396
  if (isCancel(go) || !go) {
362
- cancel('Abgebrochen. Nichts hochgeladen.');
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: 'Zurueckgehaltene Dateien trotzdem mitschicken?',
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('Abgebrochen. Nichts hochgeladen.');
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('Lade hoch');
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('Fertig');
402
- log.success(`${result.skills} Skills, ${result.skillFiles} Skill-Dateien, ` +
403
- `${result.agents} Agents, ${result.memory} Memory-Dateien`);
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} fehlgeschlagen`, ...result.failures.map((f) => ` ${f.what} ${f.code}`)].join('\n'));
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('Nichts zu loeschen: das Konto haelt nichts, was die Quelle nicht kennt.');
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
- 'Wuerde geloescht (nur Eigenes, nichts aus einem Team, nichts Archiviertes)',
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('Nichts geloescht (--dry-run).');
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('Kein Terminal fuer die Rueckfrage. Nichts geloescht.');
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: `${countTargets(targets)} Eintraege im Konto loeschen?`,
479
+ message: `Delete ${countTargets(targets)} entries in the account?`,
442
480
  initialValue: false,
443
481
  });
444
482
  if (isCancel(go) || !go) {
445
- log.info('Nichts geloescht.');
483
+ log.info('Nothing deleted.');
446
484
  }
447
485
  else {
448
486
  const pruned = await runPrune(api, targets);
449
- log.success(`${pruned.deleted} geloescht`);
487
+ log.success(`${pruned.deleted} deleted`);
450
488
  if (pruned.failures.length > 0) {
451
489
  log.error([
452
- `${pruned.failures.length} fehlgeschlagen`,
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 ? 'Nichts geaendert (--dry-run).' : 'Danach: onlymcp sync auf jedem Geraet.');
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('Alles aus dem Konto holen: Memory, Skills, Agents, Verbindungen')
466
- .option('--out <dir>', 'Archiv in diesen Ordner entpacken')
467
- .option('--zip <file>', 'Archiv als Datei behalten')
468
- .option('--scope <scope>', 'personal, all oder team:<id>', '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('Entweder --out <ordner> oder --zip <datei>, nicht beides und nicht keines.');
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('Hole das Archiv');
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('Abgebrochen');
528
+ s.stop('Aborted');
491
529
  log.error(e instanceof Error ? e.message : String(e));
492
- log.info('Nicht angemeldet? Zuerst: onlymcp login');
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('Fertig');
502
- outro(`Archiv: ${opts.zip}`);
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('Fertig');
508
- log.success(`${result.written} Dateien in ${opts.out}`);
545
+ s.stop('Done');
546
+ log.success(`${result.written} files in ${opts.out}`);
509
547
  if (result.refused.length > 0) {
510
- log.warn(['Nicht geschrieben, unzulaessiger Pfad im Archiv', ...result.refused.map((p) => ` ${p}`)].join('\n'));
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
- 'Nicht geschrieben, der Dateiname geht auf diesem System nicht',
554
+ "Not written, the filename doesn't work on this system",
517
555
  ...result.failed.map((f) => ` ${f.path} ${f.reason}`),
518
- 'Mit --zip <datei> bleibt das Archiv unveraendert erhalten.',
556
+ 'With --zip <file> the archive stays unchanged.',
519
557
  ].join('\n'));
520
558
  process.exitCode = 1;
521
559
  }
522
- outro(`Zurueckspielen: onlymcp import --skills ${opts.out}/skills --agents ${opts.out}/agents --memory ${opts.out}/memory`);
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