magic-spells 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js ADDED
@@ -0,0 +1,557 @@
1
+ import {
2
+ MagicError,
3
+ buildContext,
4
+ cell,
5
+ configFilePath,
6
+ contextFor,
7
+ createCredentialStore,
8
+ createMcpServer,
9
+ exitCodeFor,
10
+ forgetProductKey,
11
+ jsonMode,
12
+ manualItem,
13
+ maskKey,
14
+ parseGlobals,
15
+ processIO,
16
+ productConfig,
17
+ productEnvVar,
18
+ readConfigFile,
19
+ readPackageVersion,
20
+ render,
21
+ renderError,
22
+ resolveCredential,
23
+ runDoctorChecks,
24
+ runProductCli,
25
+ saveProductKey,
26
+ toMagicError,
27
+ usage
28
+ } from "./chunk-YT2Z756Z.js";
29
+
30
+ // src/version.ts
31
+ var cached;
32
+ function version() {
33
+ return cached ??= readPackageVersion(import.meta.url);
34
+ }
35
+
36
+ // src/shell.ts
37
+ function chooseEntries(shell, ids) {
38
+ const known = shell.entries.map((e) => e.id);
39
+ for (const id of ids ?? []) {
40
+ if (!known.includes(id)) {
41
+ throw usage(`unknown product "${id}"`, `Products: ${known.join(", ")}.`);
42
+ }
43
+ }
44
+ return ids ? shell.entries.filter((e) => ids.includes(e.id)) : shell.entries;
45
+ }
46
+ async function loadProducts(shell, ids) {
47
+ return Promise.all(chooseEntries(shell, ids).map((e) => e.load()));
48
+ }
49
+ function takeValue(args, name) {
50
+ for (let i = 0; i < args.length; i++) {
51
+ const tok = args[i];
52
+ if (tok === `--${name}`) {
53
+ const v = args[i + 1];
54
+ if (v === void 0 || v.startsWith("--")) throw usage(`flag --${name} expects a value`);
55
+ args.splice(i, 2);
56
+ return v;
57
+ }
58
+ if (tok.startsWith(`--${name}=`)) {
59
+ args.splice(i, 1);
60
+ return tok.slice(name.length + 3);
61
+ }
62
+ }
63
+ return void 0;
64
+ }
65
+ function wantsHelp(args) {
66
+ return args.includes("--help") || args.includes("-h");
67
+ }
68
+ function takeSwitch(args, name) {
69
+ const i = args.indexOf(`--${name}`);
70
+ if (i < 0) return false;
71
+ args.splice(i, 1);
72
+ return true;
73
+ }
74
+ function noMoreArgs(command, args) {
75
+ if (args.length > 0) throw usage(`${command}: unexpected argument "${args[0]}"`);
76
+ }
77
+ function rejectWorkspace(command, opts) {
78
+ if (opts.workspace) throw usage(`--workspace is not supported by ${command}`);
79
+ }
80
+ function storeLabel(where) {
81
+ return where === "keyring" ? "keychain" : where ?? "none";
82
+ }
83
+ function cancelled() {
84
+ return new MagicError("usage", "Cancelled.");
85
+ }
86
+
87
+ // src/commands/doctor.ts
88
+ var REACH_TIMEOUT_MS = 5e3;
89
+ async function doctor(shell, argv) {
90
+ if (wantsHelp(argv)) {
91
+ shell.io.err("Usage: magic doctor [--json]\n\nCheck the install, the keys and each product.\n");
92
+ return 0;
93
+ }
94
+ rejectWorkspace("doctor", shell.opts);
95
+ noMoreArgs("doctor", argv);
96
+ const checks = [...shellChecks(shell)];
97
+ for (const product of await loadProducts(shell))
98
+ checks.push(...await productChecks(shell, product));
99
+ const ok = checks.every((c) => c.ok);
100
+ if (jsonMode(shell.opts, shell.io)) render({ ok, checks }, shell.opts, shell.io);
101
+ else printHuman(shell, checks);
102
+ const failed = checks.find((c) => !c.ok);
103
+ return failed ? exitCodeFor(failed.code ?? "unknown") : 0;
104
+ }
105
+ function shellChecks(shell) {
106
+ const fileChosen = shell.env.MAGIC_CREDENTIALS_FILE === "1";
107
+ return [
108
+ {
109
+ name: "cli",
110
+ product: "magic",
111
+ ok: true,
112
+ detail: `magic ${version()}`,
113
+ data: { version: version() }
114
+ },
115
+ // Information only: bin/magic.js exits before this runs on Node < 24.
116
+ { name: "node", product: "magic", ok: true, detail: `Node ${process.versions.node}` },
117
+ shell.store.backend === "keyring" ? { name: "credential store", product: "magic", ok: true, detail: "OS keychain" } : {
118
+ name: "credential store",
119
+ product: "magic",
120
+ ok: true,
121
+ ...fileChosen ? {} : { warn: true },
122
+ detail: fileChosen ? "file (MAGIC_CREDENTIALS_FILE=1), mode 0600" : "file, mode 0600 \u2014 no OS keychain on this machine"
123
+ }
124
+ ];
125
+ }
126
+ async function productChecks(shell, product) {
127
+ const id = product.id;
128
+ const networkHint = `Check ${productEnvVar(id, "URL")} and your network connection.`;
129
+ const fail = (name, err) => {
130
+ const e = toMagicError(err, { product: id, networkHint });
131
+ return {
132
+ name,
133
+ product: id,
134
+ ok: false,
135
+ code: e.code,
136
+ detail: e.hint ? `${e.message} \u2014 ${e.hint}` : e.message
137
+ };
138
+ };
139
+ let config;
140
+ try {
141
+ config = productConfig(product, shell);
142
+ } catch (err) {
143
+ return [fail(`${id} config`, err)];
144
+ }
145
+ const apiUrl = config.products[id].apiUrl;
146
+ const results = [];
147
+ try {
148
+ const res = await fetch(apiUrl, {
149
+ method: "HEAD",
150
+ signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
151
+ });
152
+ results.push({
153
+ name: `${id} api`,
154
+ product: id,
155
+ ok: true,
156
+ detail: `${apiUrl} answers (HTTP ${res.status})`
157
+ });
158
+ } catch (err) {
159
+ results.push(fail(`${id} api`, networkish(err)));
160
+ }
161
+ let session;
162
+ try {
163
+ session = await resolveCredential(product, { env: shell.env, store: shell.store });
164
+ } catch (err) {
165
+ results.push(fail(`${id} key`, err));
166
+ return results;
167
+ }
168
+ results.push({
169
+ name: `${id} key`,
170
+ product: id,
171
+ ok: true,
172
+ detail: `${maskKey(session.key, product.credential)} from ${session.source === "env" ? "the environment" : "the store"}`
173
+ });
174
+ results.push(
175
+ ...await runDoctorChecks(product.doctor ?? [], contextFor(product, config, session), id)
176
+ );
177
+ return results;
178
+ }
179
+ function networkish(err) {
180
+ if (err instanceof Error && err.cause instanceof Error) {
181
+ return new Error(`${err.message}: ${err.cause.message}`);
182
+ }
183
+ return err;
184
+ }
185
+ function printHuman(shell, checks) {
186
+ const width = Math.max(...checks.map((c) => c.name.length));
187
+ const lines = checks.map((c) => {
188
+ const mark = !c.ok ? "FAIL" : c.warn ? "warn" : "ok ";
189
+ return `${mark} ${c.name.padEnd(width)} ${c.detail}`;
190
+ });
191
+ shell.io.out(`${lines.join("\n")}
192
+ `);
193
+ }
194
+
195
+ // src/commands/login.ts
196
+ var LOGIN_HELP = `Usage: magic login --product-key [<product>]
197
+
198
+ Stores a product key (from a hidden prompt, or stdin when piped) in the OS keychain,
199
+ or a 0600 file where there is none. Without <product>, the key's prefix picks it.
200
+ Account sign-in (\`magic login\` alone) arrives in a later release.
201
+ `;
202
+ var LOGOUT_HELP = `Usage: magic logout [--product <product>]
203
+
204
+ Forgets the stored key of every product, or of one.
205
+ `;
206
+ async function login(shell, argv) {
207
+ const args = [...argv];
208
+ if (wantsHelp(args)) {
209
+ shell.io.err(LOGIN_HELP);
210
+ return 0;
211
+ }
212
+ rejectWorkspace("login", shell.opts);
213
+ if (!takeSwitch(args, "product-key")) {
214
+ throw usage(
215
+ "Account login is not available yet.",
216
+ "Run `magic login --product-key [<product>]` to store a product key."
217
+ );
218
+ }
219
+ if (args.some((a) => a.includes("_"))) {
220
+ throw usage(
221
+ "Keys are never read from arguments.",
222
+ "Run `magic login --product-key` and paste the key at the prompt, or pipe it on stdin."
223
+ );
224
+ }
225
+ const named = args.length > 0 && !args[0].startsWith("--") ? args.shift() : void 0;
226
+ if (args.length > 0) {
227
+ throw usage("login: unexpected arguments.", "Usage: magic login --product-key [<product>]");
228
+ }
229
+ const products = await loadProducts(shell, named ? [named] : void 0);
230
+ const label = named ? `${products[0].title} key` : "Product key";
231
+ const key = await shell.readSecret(`${label} (input hidden): `);
232
+ if (!key) throw usage("No key given.", "Paste the key at the prompt, or pipe it on stdin.");
233
+ const product = named ? products[0] : byPrefix(products, key);
234
+ await saveProductKey(product, shell.store, key);
235
+ const where = storeLabel(await shell.store.locate(manualItem(product.id)));
236
+ envShadowNote(shell, product);
237
+ render(
238
+ { product: product.id, key: maskKey(key, product.credential), stored_in: where },
239
+ shell.opts,
240
+ shell.io
241
+ );
242
+ return 0;
243
+ }
244
+ async function logout(shell, argv) {
245
+ const args = [...argv];
246
+ if (wantsHelp(args)) {
247
+ shell.io.err(LOGOUT_HELP);
248
+ return 0;
249
+ }
250
+ rejectWorkspace("logout", shell.opts);
251
+ const one = takeValue(args, "product");
252
+ noMoreArgs("logout", args);
253
+ const removed = [];
254
+ for (const product of await loadProducts(shell, one ? [one] : void 0)) {
255
+ if (await shell.store.locate(manualItem(product.id)) !== null) removed.push(product.id);
256
+ await forgetProductKey(product, shell.store);
257
+ envShadowNote(shell, product);
258
+ }
259
+ render({ removed }, shell.opts, shell.io);
260
+ return 0;
261
+ }
262
+ function byPrefix(products, key) {
263
+ const match = products.find((p) => key.startsWith(p.credential));
264
+ if (match) return match;
265
+ throw usage(
266
+ "The key prefix matches no product.",
267
+ `Known prefixes: ${products.map((p) => `${p.credential} (${p.id})`).join(", ")}.`
268
+ );
269
+ }
270
+ function envShadowNote(shell, product) {
271
+ const envVar = productEnvVar(product.id, "KEY");
272
+ if (shell.env[envVar]?.trim() && !shell.opts.quiet) {
273
+ shell.io.err(`note: ${envVar} is set and takes precedence over the stored key.
274
+ `);
275
+ }
276
+ }
277
+
278
+ // src/commands/mcp.ts
279
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
280
+ var PREAMBLE = "Magic MCP \u2014 tools for Magic Spells services. Every tool, resource and prompt is prefixed with its product (`pyramid_\u2026`); each product\u2019s rules follow under its own heading. Errors come back as `{ error: { code, message, hint, product } }`: act on `code`.";
281
+ async function buildMcp(shell, argv, signal) {
282
+ const args = [...argv];
283
+ rejectWorkspace("mcp", shell.opts);
284
+ const list = takeValue(args, "products");
285
+ noMoreArgs("mcp", args);
286
+ const ids = list?.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
287
+ const skipped = /* @__PURE__ */ new Set();
288
+ const skip = (id, err) => {
289
+ skipped.add(id);
290
+ shell.io.err(`magic mcp: ${id}: left out (${toMagicError(err, { product: id }).message})
291
+ `);
292
+ };
293
+ const products = [];
294
+ for (const entry of chooseEntries(shell, ids)) {
295
+ try {
296
+ const module = await entry.load();
297
+ const operations = await module.load();
298
+ products.push({ module, operations, context: contextSource(shell, module, signal) });
299
+ } catch (err) {
300
+ skip(entry.id, err);
301
+ }
302
+ }
303
+ const server = createMcpServer({
304
+ name: "magic",
305
+ version: version(),
306
+ preamble: PREAMBLE,
307
+ products,
308
+ onSkip: skip
309
+ });
310
+ return { server, products: products.filter((p) => !skipped.has(p.module.id)) };
311
+ }
312
+ function contextSource(shell, module, signal) {
313
+ return async () => buildContext(module, productConfig(module, shell), {
314
+ env: shell.env,
315
+ store: shell.store,
316
+ signal
317
+ });
318
+ }
319
+ async function serveMcp(shell, argv) {
320
+ if (wantsHelp(argv)) {
321
+ shell.io.err(
322
+ "Usage: magic mcp [--products a,b]\n\nServe every product\u2019s tools to an MCP host over stdio.\n"
323
+ );
324
+ return 0;
325
+ }
326
+ guardStdout();
327
+ const err = (line) => shell.io.err(`magic mcp: ${line}
328
+ `);
329
+ const controller = new AbortController();
330
+ const built = await buildMcp(shell, argv, controller.signal);
331
+ const transport = new StdioServerTransport();
332
+ let closing = false;
333
+ const shutdown = async (reason) => {
334
+ if (closing) return;
335
+ closing = true;
336
+ err(`shutting down (${reason}).`);
337
+ controller.abort();
338
+ try {
339
+ await built.server.close();
340
+ } catch (e) {
341
+ err(`error during shutdown: ${toMagicError(e).message}`);
342
+ }
343
+ process.exit(0);
344
+ };
345
+ process.on("SIGINT", () => void shutdown("SIGINT"));
346
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
347
+ process.stdin.on("close", () => void shutdown("stdin closed"));
348
+ process.stdin.on("end", () => void shutdown("stdin end"));
349
+ await built.server.connect(transport);
350
+ const summary = built.products.map((p) => `${p.module.id} (${p.operations.length} tools)`);
351
+ err(`ready on stdio \xB7 ${summary.join(", ")}`);
352
+ for (const { module } of built.products) {
353
+ try {
354
+ await resolveCredential(module, { env: shell.env, store: shell.store });
355
+ } catch (e) {
356
+ const why = toMagicError(e, { product: module.id });
357
+ err(
358
+ `${module.id}: ${why.message} Its tools answer ${why.code} until that is fixed. ${why.hint ?? ""}`.trim()
359
+ );
360
+ }
361
+ }
362
+ return 0;
363
+ }
364
+ function guardStdout() {
365
+ const toErr = (...args) => {
366
+ process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
367
+ `);
368
+ };
369
+ console.log = toErr;
370
+ console.info = toErr;
371
+ console.debug = toErr;
372
+ }
373
+
374
+ // src/commands/whoami.ts
375
+ async function whoami(shell, argv) {
376
+ if (wantsHelp(argv)) {
377
+ shell.io.err("Usage: magic whoami [--json]\n\nWho each product key belongs to.\n");
378
+ return 0;
379
+ }
380
+ rejectWorkspace("whoami", shell.opts);
381
+ noMoreArgs("whoami", argv);
382
+ const result = { account: null, products: {} };
383
+ let firstError;
384
+ for (const product of await loadProducts(shell)) {
385
+ const entry = await identify(shell, product);
386
+ result.products[product.id] = entry.identity;
387
+ firstError ??= entry.error;
388
+ }
389
+ if (jsonMode(shell.opts, shell.io)) render(result, shell.opts, shell.io);
390
+ else printHuman2(shell, result);
391
+ if (firstError) return exitCodeFor(firstError.code);
392
+ if (Object.values(result.products).every((p) => p.source === "none")) {
393
+ if (!shell.opts.quiet) shell.io.err("No product keys. Run `magic login --product-key`.\n");
394
+ return exitCodeFor("not_logged_in");
395
+ }
396
+ return 0;
397
+ }
398
+ async function identify(shell, product) {
399
+ const source = shell.env[productEnvVar(product.id, "KEY")]?.trim() ? "env" : storeLabel(await shell.store.locate(manualItem(product.id)));
400
+ if (source === "none") return { identity: { source, key: null, identity: null } };
401
+ let key = null;
402
+ try {
403
+ const session = await resolveCredential(product, { env: shell.env, store: shell.store });
404
+ key = maskKey(session.key, product.credential);
405
+ const op = (await product.load()).find((o) => o.name === `${product.id}.whoami`);
406
+ if (!op) return { identity: { source, key, identity: null } };
407
+ const config = productConfig(product, shell);
408
+ const identity = await op.run(op.input.parse({}), contextFor(product, config, session));
409
+ return { identity: { source, key, identity } };
410
+ } catch (err) {
411
+ const error = toMagicError(err, { product: product.id });
412
+ return { identity: { source, key, identity: null, error: error.toJSON() }, error };
413
+ }
414
+ }
415
+ function printHuman2(shell, result) {
416
+ const lines = [];
417
+ for (const [id, p] of Object.entries(result.products)) {
418
+ const rows = [
419
+ ["source", p.source],
420
+ ...p.key ? [["key", p.key]] : []
421
+ ];
422
+ if (p.identity && typeof p.identity === "object") {
423
+ for (const [k, v] of Object.entries(p.identity)) rows.push([k, cell(v)]);
424
+ }
425
+ if (p.error) rows.push(["error", `[${p.error.code}] ${p.error.message}`]);
426
+ const width = Math.max(...rows.map(([k]) => k.length));
427
+ lines.push(id, ...rows.map(([k, v]) => ` ${k.padEnd(width)} ${v}`), "");
428
+ }
429
+ shell.io.out(lines.join("\n"));
430
+ }
431
+
432
+ // src/products.ts
433
+ var PRODUCTS = [
434
+ {
435
+ id: "pyramid",
436
+ title: "Pyramid \u2014 projects and tasks",
437
+ load: async () => (await import("./src-4S3XHPWY.js")).default
438
+ }
439
+ ];
440
+
441
+ // src/secret.ts
442
+ async function readSecret(prompt) {
443
+ const stdin = process.stdin;
444
+ if (!stdin.isTTY) {
445
+ process.stderr.write("reading key from stdin\u2026\n");
446
+ return (await readAll(stdin)).trim();
447
+ }
448
+ process.stderr.write(prompt);
449
+ stdin.setRawMode(true);
450
+ stdin.setEncoding("utf8");
451
+ stdin.resume();
452
+ return new Promise((resolve, reject) => {
453
+ let value = "";
454
+ const finish = () => {
455
+ stdin.off("data", onData);
456
+ stdin.setRawMode(false);
457
+ stdin.pause();
458
+ process.stderr.write("\n");
459
+ };
460
+ const onData = (chunk) => {
461
+ for (const ch of chunk) {
462
+ if (ch === "\r" || ch === "\n" || ch === "") {
463
+ finish();
464
+ resolve(value.trim());
465
+ return;
466
+ }
467
+ if (ch === "") {
468
+ finish();
469
+ reject(cancelled());
470
+ return;
471
+ }
472
+ if (ch === "\x7F" || ch === "\b") value = value.slice(0, -1);
473
+ else value += ch;
474
+ }
475
+ };
476
+ stdin.on("data", onData);
477
+ });
478
+ }
479
+ async function readAll(stream) {
480
+ let text = "";
481
+ for await (const chunk of stream) text += String(chunk);
482
+ return text;
483
+ }
484
+
485
+ // src/main.ts
486
+ var COMMANDS = {
487
+ login: { summary: "store a product key: login --product-key [<product>]", run: login },
488
+ logout: { summary: "forget stored keys: logout [--product <product>]", run: logout },
489
+ whoami: { summary: "who each product key belongs to", run: whoami },
490
+ doctor: { summary: "check the install, the keys and each product", run: doctor },
491
+ mcp: { summary: "serve every product\u2019s tools over stdio: mcp [--products a,b]", run: serveMcp }
492
+ };
493
+ function help(products) {
494
+ const lines = [
495
+ "magic \u2014 the command line for Magic Spells services",
496
+ "",
497
+ "Usage: magic <command> [args] [--flags]",
498
+ " magic <product> <noun> <verb> [args] [--flags]",
499
+ "",
500
+ "Commands:",
501
+ ...Object.entries(COMMANDS).map(([name, c]) => ` ${name.padEnd(21)}${c.summary}`),
502
+ ` ${"version".padEnd(21)}print the version`,
503
+ "",
504
+ "Products:",
505
+ ...products.map((p) => ` ${p.id.padEnd(21)}${p.title}`),
506
+ "",
507
+ "Global flags: --json --yes/-y --no-color --quiet/-q --workspace <handle> --version --help",
508
+ "Run `magic <product> --help` for a product\u2019s commands.",
509
+ ""
510
+ ];
511
+ return lines.join("\n");
512
+ }
513
+ async function run(argv, deps = {}) {
514
+ const io = deps.io ?? processIO;
515
+ const env = deps.env ?? process.env;
516
+ const products = deps.products ?? PRODUCTS;
517
+ const [cmd, ...rest] = argv;
518
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
519
+ io.out(`magic ${version()}
520
+ `);
521
+ return 0;
522
+ }
523
+ if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h") {
524
+ io.err(help(products));
525
+ return 0;
526
+ }
527
+ const store = () => deps.store ?? createCredentialStore({ env });
528
+ const readConfig = () => readConfigFile(configFilePath({ env }));
529
+ const entry = products.find((p) => p.id === cmd);
530
+ if (entry) {
531
+ const product = await entry.load();
532
+ return runProductCli(product, rest, { io, env, store: store(), readConfig });
533
+ }
534
+ let opts = { json: argv.includes("--json"), yes: false, quiet: false };
535
+ try {
536
+ const command = COMMANDS[cmd];
537
+ if (!command) throw usage(`unknown command "${cmd}"`, "Run `magic --help` for usage.");
538
+ const parsed = parseGlobals(rest, [], env);
539
+ opts = parsed.opts;
540
+ const shell = {
541
+ io,
542
+ env,
543
+ store: store(),
544
+ entries: products,
545
+ opts,
546
+ readConfig,
547
+ readSecret: deps.readSecret ?? readSecret
548
+ };
549
+ return await command.run(shell, parsed.rest);
550
+ } catch (err) {
551
+ return renderError(toMagicError(err), opts, io);
552
+ }
553
+ }
554
+ export {
555
+ run,
556
+ version
557
+ };
@@ -0,0 +1,9 @@
1
+ import {
2
+ operations,
3
+ operationsByName
4
+ } from "./chunk-S5IUP7G2.js";
5
+ import "./chunk-YT2Z756Z.js";
6
+ export {
7
+ operations,
8
+ operationsByName
9
+ };