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