@modootoday/pmcp 0.9.1 → 0.10.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/cli.js CHANGED
@@ -1,31 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- loadEmbedder,
4
- openEntryVectors
5
- } from "./chunk-7DD2ADL2.js";
6
- import {
7
- ConfigError,
8
3
  ProjectError,
9
4
  TOOLS,
10
5
  doctor,
11
- findConfig,
6
+ prepareSearch,
12
7
  project,
13
- readConfig,
14
8
  readSpec,
15
9
  renderFrontmatter,
16
10
  spawnRunner,
17
11
  splitFrontmatter,
18
12
  validateCatalog,
19
13
  validateSkillDir
20
- } from "./chunk-QCOJ3WCM.js";
21
- import "./chunk-R2QPKEI4.js";
14
+ } from "./chunk-ZUFFVIHQ.js";
15
+ import {
16
+ loadEmbedder,
17
+ openEntryVectors
18
+ } from "./chunk-XEH4ZGGJ.js";
22
19
  import {
23
- serve
24
- } from "./chunk-AJYMOFVX.js";
20
+ serve,
21
+ serveWithMailbox
22
+ } from "./chunk-JRSR4JBR.js";
25
23
  import {
26
24
  kindOf,
27
25
  readCatalog
28
- } from "./chunk-VQHNG3KM.js";
26
+ } from "./chunk-VRZFM2M5.js";
27
+ import {
28
+ MailboxError,
29
+ MailboxService,
30
+ MailboxStore,
31
+ failure,
32
+ initializeMailboxConfig,
33
+ mailboxConfigFrom,
34
+ readBinding,
35
+ readCredential,
36
+ restoreBackup,
37
+ writePrivateJson
38
+ } from "./chunk-HNAVQG6R.js";
39
+ import {
40
+ ConfigError,
41
+ findConfig,
42
+ readConfig
43
+ } from "./chunk-MSTKY33C.js";
44
+ import {
45
+ embeddingModels,
46
+ operationSchema,
47
+ resolveEncoder
48
+ } from "./chunk-XOH2CW6P.js";
29
49
 
30
50
  // src/cli/command.ts
31
51
  var ArgumentError = class extends Error {
@@ -204,6 +224,13 @@ var Ui = class {
204
224
  }
205
225
  };
206
226
 
227
+ // src/commands/reindex.ts
228
+ import { existsSync, mkdirSync } from "fs";
229
+ import { dirname } from "path";
230
+
231
+ // src/commands/embedding.ts
232
+ import { resolve } from "path";
233
+
207
234
  // src/commands/roots.ts
208
235
  var CATALOG_OPTIONS = [
209
236
  {
@@ -290,6 +317,87 @@ function catalogFrom(context, onReject) {
290
317
  return readCatalog(catalogOptionsFrom(context, onReject));
291
318
  }
292
319
 
320
+ // src/commands/embedding.ts
321
+ var EMBEDDING_OPTIONS = [
322
+ {
323
+ name: "model",
324
+ describe: "Embedding preset or model ID; list presets with pmcp models",
325
+ placeholder: "<name>"
326
+ },
327
+ {
328
+ name: "dtype",
329
+ describe: "Weight precision; uses the selected model default when omitted",
330
+ placeholder: "<q4|q8|fp32>"
331
+ },
332
+ {
333
+ name: "dimensions",
334
+ describe: "Supported output width; uses the model's full output when omitted",
335
+ placeholder: "<number>"
336
+ },
337
+ {
338
+ name: "model-revision",
339
+ describe: "Model revision used for loading and index identity",
340
+ placeholder: "<revision>"
341
+ },
342
+ {
343
+ name: "model-path",
344
+ describe: "Local model artifact directory",
345
+ placeholder: "<dir>"
346
+ },
347
+ {
348
+ name: "model-cache",
349
+ describe: "Model cache directory",
350
+ placeholder: "<dir>"
351
+ },
352
+ {
353
+ name: "query-prefix",
354
+ describe: "Custom model's query instruction prefix",
355
+ placeholder: "<text>"
356
+ },
357
+ {
358
+ name: "passage-prefix",
359
+ describe: "Custom model's document instruction prefix",
360
+ placeholder: "<text>"
361
+ },
362
+ {
363
+ name: "pooling",
364
+ describe: "Custom model's pooling method",
365
+ placeholder: "<mean|cls>"
366
+ }
367
+ ];
368
+ function embeddingFrom(context) {
369
+ const configuration = configFrom(context)?.embedding ?? {};
370
+ const dimension = one(context.args, "dimensions");
371
+ const modelPath = one(context.args, "model-path");
372
+ const cacheDir = one(context.args, "model-cache");
373
+ const queryPrefix = one(context.args, "query-prefix");
374
+ const passagePrefix = one(context.args, "passage-prefix");
375
+ const pooling = one(context.args, "pooling");
376
+ const selection = {
377
+ ...configuration,
378
+ modelId: one(context.args, "model") ?? configuration.modelId,
379
+ dtype: one(context.args, "dtype") ?? configuration.dtype,
380
+ dimensions: dimension === void 0 ? configuration.dimensions : Number(dimension),
381
+ revision: one(context.args, "model-revision") ?? configuration.revision,
382
+ modelPath: modelPath === void 0 ? configuration.modelPath : resolve(context.cwd, modelPath),
383
+ cacheDir: cacheDir === void 0 ? configuration.cacheDir : resolve(context.cwd, cacheDir),
384
+ encoder: {
385
+ ...configuration.encoder,
386
+ ...queryPrefix === void 0 ? {} : { queryPrefix },
387
+ ...passagePrefix === void 0 ? {} : { passagePrefix },
388
+ ...pooling === void 0 ? {} : { pooling }
389
+ }
390
+ };
391
+ try {
392
+ resolveEncoder(selection.modelId, selection);
393
+ } catch (error) {
394
+ throw new ArgumentError(
395
+ error instanceof Error ? error.message : "Invalid embedding options"
396
+ );
397
+ }
398
+ return selection;
399
+ }
400
+
293
401
  // src/commands/reindex.ts
294
402
  function indexPath(root) {
295
403
  return `${root}/.cache/pmcp/vectors.sqlite`;
@@ -300,10 +408,12 @@ var indexCommand = {
300
408
  usage: "pmcp index [--root <dir>] [--model <id>]",
301
409
  options: [
302
410
  ...CATALOG_OPTIONS,
411
+ ...EMBEDDING_OPTIONS,
412
+ { name: "local-only", describe: "Refuse model downloads", boolean: true },
303
413
  {
304
- name: "model",
305
- describe: "Encoder to use. Rows from another model are never scored.",
306
- placeholder: "<id>"
414
+ name: "force",
415
+ describe: "Re-encode the selected model even when descriptions match",
416
+ boolean: true
307
417
  },
308
418
  {
309
419
  name: "json",
@@ -323,8 +433,28 @@ var indexCommand = {
323
433
  return 1;
324
434
  }
325
435
  const dryRun = context.args.flags.has("dry-run");
326
- const model = one(context.args, "model");
327
- const embedder = await loadEmbedder(model);
436
+ const selection = embeddingFrom(context);
437
+ const selected = resolveEncoder(selection.modelId, selection);
438
+ const path = indexPath(rootsFrom(context)[0] ?? ".");
439
+ const descriptions = new Map(
440
+ entries.map((entry2) => [entry2.name, entry2.description])
441
+ );
442
+ const force = context.args.flags.has("force");
443
+ if (dryRun) {
444
+ const existing = existsSync(path) ? await openEntryVectors(path, { readOnly: true }) : null;
445
+ try {
446
+ const pending = !force && existing ? existing.stale(selected.indexKey, descriptions) : [...descriptions.keys()];
447
+ for (const name of pending) context.ui.line(` would encode ${name}`);
448
+ context.ui.info("dry run, nothing written");
449
+ return 0;
450
+ } finally {
451
+ existing?.close();
452
+ }
453
+ }
454
+ const embedder = await loadEmbedder(selected.modelId, {
455
+ ...selection,
456
+ localOnly: context.args.flags.has("local-only")
457
+ });
328
458
  if (embedder === null) {
329
459
  context.ui.error(
330
460
  "semantic ranking needs an encoder that is not installed"
@@ -333,9 +463,8 @@ var indexCommand = {
333
463
  context.ui.info("ranking stays lexical until then, which is the default");
334
464
  return 3;
335
465
  }
336
- const store = await openEntryVectors(
337
- indexPath(rootsFrom(context)[0] ?? ".")
338
- );
466
+ mkdirSync(dirname(path), { recursive: true });
467
+ const store = await openEntryVectors(path);
339
468
  if (store === null) {
340
469
  context.ui.error(
341
470
  "no sqlite in this runtime, so vectors cannot be stored"
@@ -343,29 +472,29 @@ var indexCommand = {
343
472
  return 1;
344
473
  }
345
474
  try {
346
- const descriptions = new Map(entries.map((e) => [e.name, e.description]));
347
- const stale = new Set(store.stale(embedder.modelId, descriptions));
475
+ const key = embedder.indexKey ?? selected.indexKey;
476
+ const stale = new Set(
477
+ force ? [...descriptions.keys()] : store.stale(key, descriptions)
478
+ );
348
479
  const todo = entries.filter((entry2) => stale.has(entry2.name));
349
480
  context.ui.info(
350
481
  `${todo.length} of ${entries.length} to encode`,
351
482
  embedder.modelId
352
483
  );
353
- if (dryRun) {
354
- for (const entry2 of todo)
355
- context.ui.line(` would encode ${entry2.name}`);
356
- context.ui.info("dry run, nothing written");
357
- return 0;
358
- }
359
484
  for (const entry2 of todo) {
360
485
  const vector = embedder.embedPassage ? await embedder.embedPassage(entry2.description) : await embedder.embed(entry2.description);
361
- store.write(embedder.modelId, entry2.name, entry2.description, vector);
486
+ store.write(key, entry2.name, entry2.description, vector);
362
487
  }
363
- const indexed = store.read(embedder.modelId).size;
488
+ const indexed = store.read(key).size;
364
489
  if (context.args.flags.has("json")) {
365
490
  context.ui.data(
366
491
  `${JSON.stringify(
367
492
  {
368
493
  model: embedder.modelId,
494
+ indexKey: key,
495
+ dtype: selected.dtype,
496
+ revision: selected.revision,
497
+ dimensions: embedder.dims || selected.dimensions,
369
498
  encoded: todo.length,
370
499
  indexed,
371
500
  catalog: entries.length
@@ -391,6 +520,31 @@ var indexCommand = {
391
520
  }
392
521
  };
393
522
 
523
+ // src/commands/models.ts
524
+ var modelsCommand = {
525
+ name: "models",
526
+ describe: "List embedding presets without loading or downloading models",
527
+ usage: "pmcp models [--json]",
528
+ options: [
529
+ { name: "json", describe: "Emit preset metadata as JSON", boolean: true }
530
+ ],
531
+ run(context) {
532
+ const models = embeddingModels();
533
+ if (context.args.flags.has("json")) {
534
+ context.ui.data(`${JSON.stringify(models, null, 2)}
535
+ `);
536
+ return 0;
537
+ }
538
+ context.ui.table(
539
+ models.map((model) => [
540
+ model.name,
541
+ `${model.modelId} (${model.dtype}; dimensions: ${model.dimensions.join(", ")})`
542
+ ])
543
+ );
544
+ return 0;
545
+ }
546
+ };
547
+
394
548
  // src/commands/list.ts
395
549
  var listCommand = {
396
550
  name: "list",
@@ -446,6 +600,11 @@ var listCommand = {
446
600
  }
447
601
  };
448
602
 
603
+ // src/cli/invocation.ts
604
+ function invocation(env = process.env) {
605
+ return env["npm_command"] === "exec" ? "npx -y @modootoday/pmcp" : "pmcp";
606
+ }
607
+
449
608
  // src/commands/serve.ts
450
609
  var serveCommand = {
451
610
  name: "serve",
@@ -453,31 +612,101 @@ var serveCommand = {
453
612
  usage: "pmcp serve [--root <dir>] [--scope <prefix>] [--marketplace <dir>] [--package <dir>] [--workspace <dir>] [--config <file> | --no-config] [--prompts]",
454
613
  options: [
455
614
  ...CATALOG_OPTIONS,
615
+ ...EMBEDDING_OPTIONS,
616
+ {
617
+ name: "lexical",
618
+ describe: "Skip semantic index and model loading",
619
+ boolean: true
620
+ },
621
+ {
622
+ name: "mailbox-actor-file",
623
+ describe: "Private launcher actor credential for opt-in mailbox",
624
+ placeholder: "<file>"
625
+ },
626
+ {
627
+ name: "mailbox-binding-file",
628
+ describe: "Private attachment file written by the launcher",
629
+ placeholder: "<file>"
630
+ },
631
+ {
632
+ name: "mailbox-runtime",
633
+ describe: "Launcher-assigned canonical runtime",
634
+ placeholder: "<runtime>"
635
+ },
636
+ {
637
+ name: "mailbox-replace-session",
638
+ describe: "Exact old attachment ID to replace",
639
+ placeholder: "<id>"
640
+ },
641
+ {
642
+ name: "mailbox-native-session",
643
+ describe: "Launcher-owned native session ID",
644
+ placeholder: "<id>"
645
+ },
456
646
  {
457
647
  name: "prompts",
458
648
  describe: "Also offer one prompt per skill, for hosts that turn prompts into commands.",
459
649
  boolean: true
460
650
  }
461
651
  ],
462
- run(context) {
463
- serve(catalogOptionsFrom(context), {
464
- prompts: context.args.flags.has("prompts")
652
+ async run(context) {
653
+ const selection = embeddingFrom(context);
654
+ const project2 = configFrom(context);
655
+ const mailbox = project2 ? mailboxConfigFrom(project2, context.env) : void 0;
656
+ const actorFile = one(context.args, "mailbox-actor-file") ?? context.env.PMCP_MAILBOX_ACTOR_FILE;
657
+ const runtime = one(context.args, "mailbox-runtime") ?? context.env.PMCP_MAILBOX_RUNTIME;
658
+ const catalog = catalogOptionsFrom(context);
659
+ const search = context.args.flags.has("lexical") || selection.enabled === false ? void 0 : await prepareSearch(catalog, {
660
+ indexPath: indexPath(rootsFrom(context)[0] ?? "."),
661
+ ...selection,
662
+ localOnly: true
465
663
  });
664
+ if (search && search.reason !== "index_missing") {
665
+ const hint = search.reason ? ` reason=${search.reason}; run ${invocation(context.env)} index with the selected model` : "";
666
+ process.stderr.write(`pmcp: search ranking=${search.ranking}${hint}
667
+ `);
668
+ }
669
+ const serving = {
670
+ prompts: context.args.flags.has("prompts"),
671
+ ...search?.options
672
+ };
673
+ if (actorFile) {
674
+ if (!mailbox?.enabled)
675
+ throw new ArgumentError("Enable [mailbox] before attaching an actor");
676
+ if (!runtime)
677
+ throw new ArgumentError(
678
+ "Provide --mailbox-runtime or PMCP_MAILBOX_RUNTIME"
679
+ );
680
+ await serveWithMailbox(
681
+ catalogOptionsFrom(context),
682
+ {
683
+ config: mailbox,
684
+ actorFile,
685
+ runtime,
686
+ bindingFile: one(context.args, "mailbox-binding-file") ?? context.env.PMCP_MAILBOX_BINDING_FILE,
687
+ replaceSessionId: one(context.args, "mailbox-replace-session"),
688
+ nativeSessionId: one(context.args, "mailbox-native-session")
689
+ },
690
+ serving
691
+ );
692
+ return 0;
693
+ }
694
+ if ([...context.args.options.keys()].some(
695
+ (name) => name.startsWith("mailbox-")
696
+ ) || runtime || context.env.PMCP_MAILBOX_BINDING_FILE) {
697
+ throw new ArgumentError("Provide a launcher mailbox actor file");
698
+ }
699
+ serve(catalogOptionsFrom(context), serving);
466
700
  return 0;
467
701
  }
468
702
  };
469
703
 
470
- // src/cli/invocation.ts
471
- function invocation(env = process.env) {
472
- return env["npm_command"] === "exec" ? "npx -y @modootoday/pmcp" : "pmcp";
473
- }
474
-
475
704
  // src/remote/session.ts
476
705
  import {
477
706
  chmodSync,
478
707
  closeSync,
479
708
  fchmodSync,
480
- mkdirSync,
709
+ mkdirSync as mkdirSync2,
481
710
  openSync,
482
711
  readFileSync,
483
712
  rmSync,
@@ -609,7 +838,7 @@ function readSession(home) {
609
838
  function writeSession(session, home) {
610
839
  const path = sessionPath(home);
611
840
  const directory = globalDir(home);
612
- mkdirSync(directory, { recursive: true, mode: 448 });
841
+ mkdirSync2(directory, { recursive: true, mode: 448 });
613
842
  chmodSync(directory, 448);
614
843
  const fd = openSync(path, "w", 384);
615
844
  try {
@@ -655,7 +884,7 @@ var DEFAULT_RESOURCE = "https://api.pmcp.build";
655
884
  var DEFAULT_SCOPE = "pmcp:catalog:read pmcp:distribution:read";
656
885
  var fetcher = (url, init) => fetch(url, init);
657
886
  var setting = (env, name, fallback) => env[name] ?? fallback;
658
- var wait = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
887
+ var wait = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
659
888
  var loginCommand = {
660
889
  name: "login",
661
890
  describe: "Sign in on this machine, to obtain skills this project lacks",
@@ -824,7 +1053,7 @@ function sayStanding(context, line) {
824
1053
 
825
1054
  // src/installed.ts
826
1055
  import { readFileSync as readFileSync2 } from "fs";
827
- import { dirname, join as join2, resolve } from "path";
1056
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
828
1057
  function object(value) {
829
1058
  return typeof value === "object" && value !== null && !Array.isArray(value);
830
1059
  }
@@ -845,7 +1074,7 @@ function locate(name, project2) {
845
1074
  );
846
1075
  } catch (error) {
847
1076
  if (!absent(error)) return { name, reason: "unreadable_manifest" };
848
- const parent = dirname(directory);
1077
+ const parent = dirname2(directory);
849
1078
  if (parent === directory) return { name, reason: "not_installed" };
850
1079
  directory = parent;
851
1080
  continue;
@@ -867,7 +1096,7 @@ function locate(name, project2) {
867
1096
  }
868
1097
  }
869
1098
  function readInstalledDependencies(projectDirectory) {
870
- const project2 = resolve(projectDirectory);
1099
+ const project2 = resolve2(projectDirectory);
871
1100
  const manifest = JSON.parse(
872
1101
  readFileSync2(join2(project2, "package.json"), "utf8")
873
1102
  );
@@ -902,8 +1131,8 @@ function readInstalledDependencies(projectDirectory) {
902
1131
 
903
1132
  // src/install/plan.ts
904
1133
  import { createHash } from "crypto";
905
- import { existsSync, readFileSync as readFileSync3 } from "fs";
906
- import { dirname as dirname2, join as join3, relative, resolve as resolve2, sep } from "path";
1134
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
1135
+ import { dirname as dirname3, join as join3, relative, resolve as resolve3, sep } from "path";
907
1136
  import semver from "semver";
908
1137
  function matchingSkills(catalog, inventory) {
909
1138
  const matches = [];
@@ -955,25 +1184,25 @@ function manifestAt(directory) {
955
1184
  return value;
956
1185
  }
957
1186
  function installContext(directory) {
958
- const project2 = resolve2(directory);
1187
+ const project2 = resolve3(directory);
959
1188
  const manifest = manifestAt(project2);
960
1189
  let root = project2;
961
1190
  let ancestor = project2;
962
1191
  for (; ; ) {
963
- if (ancestor !== project2 && existsSync(join3(ancestor, "package.json"))) {
1192
+ if (ancestor !== project2 && existsSync2(join3(ancestor, "package.json"))) {
964
1193
  const candidate = manifestAt(ancestor);
965
1194
  if (candidate.workspaces !== void 0) {
966
1195
  root = ancestor;
967
1196
  break;
968
1197
  }
969
- if (LOCKS.some((file) => existsSync(join3(ancestor, file)))) break;
1198
+ if (LOCKS.some((file) => existsSync2(join3(ancestor, file)))) break;
970
1199
  }
971
- if (existsSync(join3(ancestor, ".git")) || dirname2(ancestor) === ancestor)
1200
+ if (existsSync2(join3(ancestor, ".git")) || dirname3(ancestor) === ancestor)
972
1201
  break;
973
- ancestor = dirname2(ancestor);
1202
+ ancestor = dirname3(ancestor);
974
1203
  }
975
1204
  const owner = manifestAt(root);
976
- const lockNames = LOCKS.filter((file) => existsSync(join3(root, file)));
1205
+ const lockNames = LOCKS.filter((file) => existsSync2(join3(root, file)));
977
1206
  const lockManagers = new Set(
978
1207
  lockNames.map((file) => {
979
1208
  if (file.startsWith("bun.")) return "bun";
@@ -997,7 +1226,7 @@ function installContext(directory) {
997
1226
  throw new Error(
998
1227
  "declare packageManager or install project dependencies first"
999
1228
  );
1000
- if (root !== project2 && LOCKS.some((file) => existsSync(join3(project2, file)))) {
1229
+ if (root !== project2 && LOCKS.some((file) => existsSync2(join3(project2, file)))) {
1001
1230
  throw new Error(
1002
1231
  "nested project has its own lockfile; resolve the workspace boundary first"
1003
1232
  );
@@ -1114,7 +1343,7 @@ function planInstall(options) {
1114
1343
  }
1115
1344
 
1116
1345
  // src/commands/remote-options.ts
1117
- import { resolve as resolve3 } from "path";
1346
+ import { resolve as resolve4 } from "path";
1118
1347
 
1119
1348
  // src/remote/catalog.ts
1120
1349
  import { readFileSync as readFileSync5, statSync } from "fs";
@@ -1122,7 +1351,7 @@ import semver2 from "semver";
1122
1351
  import { z } from "zod";
1123
1352
 
1124
1353
  // src/remote/catalog-cache.ts
1125
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync, writeFileSync } from "fs";
1354
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync, writeFileSync } from "fs";
1126
1355
  import { join as join4 } from "path";
1127
1356
  var MAX_CACHE_BYTES = 8 * 1024 * 1024;
1128
1357
  function memoryCatalogCache() {
@@ -1156,7 +1385,7 @@ function fileCatalogCache(dir) {
1156
1385
  write(origin, entry2) {
1157
1386
  try {
1158
1387
  if (entry2.body.length > MAX_CACHE_BYTES) return;
1159
- mkdirSync2(dir, { recursive: true });
1388
+ mkdirSync3(dir, { recursive: true });
1160
1389
  const temporary = `${path}.${String(process.pid)}`;
1161
1390
  writeFileSync(
1162
1391
  temporary,
@@ -1347,7 +1576,7 @@ var REMOTE_OPTIONS = [
1347
1576
  { name: "json", describe: "Print machine-readable results.", boolean: true }
1348
1577
  ];
1349
1578
  function projectFrom(context) {
1350
- return resolve3(context.cwd, one(context.args, "project") ?? ".");
1579
+ return resolve4(context.cwd, one(context.args, "project") ?? ".");
1351
1580
  }
1352
1581
  function apiOrigin(context) {
1353
1582
  return one(context.args, "api") ?? context.env["PMCP_API_ORIGIN"] ?? DEFAULT_API_ORIGIN;
@@ -1357,7 +1586,7 @@ async function remoteCatalog(context) {
1357
1586
  if (file) {
1358
1587
  if (one(context.args, "api"))
1359
1588
  throw new Error("choose --catalog or --api, not both");
1360
- return readCatalogFile(resolve3(context.cwd, file));
1589
+ return readCatalogFile(resolve4(context.cwd, file));
1361
1590
  }
1362
1591
  return fetchCatalog(apiOrigin(context), fetch, fileCatalogCache(globalDir()));
1363
1592
  }
@@ -1582,7 +1811,7 @@ function applyInstallPlan(plan, runner = packageManagerRunner) {
1582
1811
  // src/install/verify-content.ts
1583
1812
  import { createHash as createHash2 } from "crypto";
1584
1813
  import { readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
1585
- import { join as join5, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
1814
+ import { join as join5, relative as relative2, resolve as resolve5, sep as sep2 } from "path";
1586
1815
  var SKILL = "SKILL.md";
1587
1816
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".cache"]);
1588
1817
  function skillFiles(root) {
@@ -1623,7 +1852,7 @@ function contentDigest(files) {
1623
1852
  return `sha256:${hash.digest("hex")}`;
1624
1853
  }
1625
1854
  function verifyInstalledContent(project2, packageName2, expected) {
1626
- const root = resolve4(project2, "node_modules", ...packageName2.split("/"));
1855
+ const root = resolve5(project2, "node_modules", ...packageName2.split("/"));
1627
1856
  const files = skillFiles(root);
1628
1857
  const actual = contentDigest(files);
1629
1858
  return {
@@ -2005,14 +2234,14 @@ import { homedir as homedir2 } from "os";
2005
2234
  // src/project/edit.ts
2006
2235
  import {
2007
2236
  cpSync,
2008
- existsSync as existsSync2,
2009
- mkdirSync as mkdirSync3,
2237
+ existsSync as existsSync3,
2238
+ mkdirSync as mkdirSync4,
2010
2239
  readFileSync as readFileSync7,
2011
2240
  rmSync as rmSync3,
2012
2241
  statSync as statSync3,
2013
2242
  writeFileSync as writeFileSync3
2014
2243
  } from "fs";
2015
- import { basename, dirname as dirname3, join as join7 } from "path";
2244
+ import { basename, dirname as dirname4, join as join7 } from "path";
2016
2245
  var CROSS_TOOL = ["name", "description"];
2017
2246
  var tomlValue = (value) => JSON.stringify(value);
2018
2247
  function findEntry(entries, wanted) {
@@ -2045,14 +2274,14 @@ function addEntry(root, entry2) {
2045
2274
  }
2046
2275
  if (kind === "skill") {
2047
2276
  const dest2 = join7(root, ".agents/skills", name);
2048
- if (existsSync2(dest2))
2277
+ if (existsSync3(dest2))
2049
2278
  throw new ProjectError(`.agents/skills/${name} already exists`);
2050
- const source = entry2.root ?? dirname3(entry2.path);
2051
- mkdirSync3(dirname3(dest2), { recursive: true });
2279
+ const source = entry2.root ?? dirname4(entry2.path);
2280
+ mkdirSync4(dirname4(dest2), { recursive: true });
2052
2281
  if (statSync3(source).isDirectory()) {
2053
2282
  cpSync(source, dest2, { recursive: true });
2054
2283
  } else {
2055
- mkdirSync3(dest2);
2284
+ mkdirSync4(dest2);
2056
2285
  }
2057
2286
  const head = renderFrontmatter({
2058
2287
  ...meta,
@@ -2067,7 +2296,7 @@ ${body.replace(/^\n/u, "")}`
2067
2296
  return { kind, name, path: `.agents/skills/${name}` };
2068
2297
  }
2069
2298
  const dest = join7(root, ".agents/agents", name);
2070
- if (existsSync2(dest))
2299
+ if (existsSync3(dest))
2071
2300
  throw new ProjectError(`.agents/agents/${name} already exists`);
2072
2301
  const kept = {};
2073
2302
  const moved = {};
@@ -2075,7 +2304,7 @@ ${body.replace(/^\n/u, "")}`
2075
2304
  if (CROSS_TOOL.includes(key)) kept[key] = value;
2076
2305
  else moved[key] = value;
2077
2306
  }
2078
- mkdirSync3(dest, { recursive: true });
2307
+ mkdirSync4(dest, { recursive: true });
2079
2308
  writeFileSync3(
2080
2309
  join7(dest, "agent.md"),
2081
2310
  `${renderFrontmatter(kept)}
@@ -2087,7 +2316,7 @@ ${body.replace(/^\n/u, "")}`
2087
2316
  `[agents.${name}.claude]`,
2088
2317
  ...Object.entries(moved).map(([k, v]) => `${k} = ${tomlValue(v)}`)
2089
2318
  ];
2090
- const before = existsSync2(config) ? readFileSync7(config, "utf8") : "";
2319
+ const before = existsSync3(config) ? readFileSync7(config, "utf8") : "";
2091
2320
  const sep3 = before === "" || before.endsWith("\n\n") ? "" : "\n";
2092
2321
  writeFileSync3(config, `${before}${sep3}${lines.join("\n")}
2093
2322
  `);
@@ -2110,14 +2339,14 @@ function withoutTables(toml, name) {
2110
2339
  function removeAsset(root, name) {
2111
2340
  const removed = [];
2112
2341
  for (const rel of [`.agents/skills/${name}`, `.agents/agents/${name}`]) {
2113
- if (!existsSync2(join7(root, rel))) continue;
2342
+ if (!existsSync3(join7(root, rel))) continue;
2114
2343
  rmSync3(join7(root, rel), { recursive: true, force: true });
2115
2344
  removed.push(rel);
2116
2345
  }
2117
2346
  if (removed.length === 0)
2118
2347
  throw new ProjectError(`no skill or agent named ${name} in .agents/`);
2119
2348
  const config = join7(root, "pmcp.toml");
2120
- if (existsSync2(config)) {
2349
+ if (existsSync3(config)) {
2121
2350
  const before = readFileSync7(config, "utf8");
2122
2351
  const after = withoutTables(before, name);
2123
2352
  if (after !== before) writeFileSync3(config, after);
@@ -2287,11 +2516,214 @@ var validateCommand = {
2287
2516
  }
2288
2517
  };
2289
2518
 
2519
+ // src/commands/mailbox.ts
2520
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
2521
+ import { resolve as resolve6 } from "path";
2522
+ var allowed = {
2523
+ init: ["config", "state-dir"],
2524
+ actor: ["config", "actor", "actor-file", "parent-binding"],
2525
+ rotate: ["config", "actor", "actor-file", "operator"],
2526
+ send: ["config", "actor-file", "binding-file", "input"],
2527
+ inbox: ["config", "actor-file", "binding-file", "input"],
2528
+ read: ["config", "actor-file", "binding-file", "input"],
2529
+ ack: ["config", "actor-file", "binding-file", "input"],
2530
+ sessions: ["config", "actor-file", "binding-file"],
2531
+ export: ["config", "operator", "output", "scope-actor"],
2532
+ backup: ["config", "operator", "output"],
2533
+ restore: ["config", "operator", "input", "output"]
2534
+ };
2535
+ function required(context, name) {
2536
+ const value = one(context.args, name);
2537
+ if (!value) throw new ArgumentError(`Provide --${name}`);
2538
+ return value;
2539
+ }
2540
+ var mailboxCommand = {
2541
+ name: "mailbox",
2542
+ describe: "Manage opt-in local peer mail and launcher credentials",
2543
+ usage: "pmcp mailbox operation [--config <file>] [options]",
2544
+ options: [
2545
+ { name: "config", describe: "Project pmcp.toml", placeholder: "<file>" },
2546
+ {
2547
+ name: "state-dir",
2548
+ describe: "Private mailbox state directory",
2549
+ placeholder: "<dir>"
2550
+ },
2551
+ { name: "actor", describe: "Registered actor ID", placeholder: "<id>" },
2552
+ {
2553
+ name: "actor-file",
2554
+ describe: "Private actor credential file",
2555
+ placeholder: "<file>"
2556
+ },
2557
+ {
2558
+ name: "binding-file",
2559
+ describe: "Private existing attachment file",
2560
+ placeholder: "<file>"
2561
+ },
2562
+ {
2563
+ name: "parent-binding",
2564
+ describe: "Live parent attachment for bounded child delegation",
2565
+ placeholder: "<file>"
2566
+ },
2567
+ {
2568
+ name: "input",
2569
+ describe: "Operation input JSON or backup source",
2570
+ placeholder: "<file>"
2571
+ },
2572
+ {
2573
+ name: "output",
2574
+ describe: "New export or backup destination",
2575
+ placeholder: "<file>"
2576
+ },
2577
+ {
2578
+ name: "scope-actor",
2579
+ describe: "Limit export to one actor's correspondence",
2580
+ placeholder: "<id>"
2581
+ },
2582
+ {
2583
+ name: "operator",
2584
+ describe: "Explicit local administrator intent",
2585
+ boolean: true
2586
+ }
2587
+ ],
2588
+ async run(context) {
2589
+ const [verb, ...extra] = context.args.positional;
2590
+ const supported = verb ? allowed[verb] : void 0;
2591
+ if (!supported || extra.length > 0)
2592
+ throw new ArgumentError("Provide one supported mailbox operation");
2593
+ for (const key of [...context.args.options.keys(), ...context.args.flags]) {
2594
+ if (!supported.includes(key))
2595
+ throw new ArgumentError(`--${key} is not supported by mailbox ${verb}`);
2596
+ }
2597
+ if (verb === "init") {
2598
+ const config2 = initializeMailboxConfig(
2599
+ resolve6(context.cwd, one(context.args, "config") ?? "pmcp.toml"),
2600
+ one(context.args, "state-dir")
2601
+ );
2602
+ const store2 = await MailboxStore.open(config2);
2603
+ await store2.close();
2604
+ context.ui.data(
2605
+ `${JSON.stringify({ ok: true, data: { projectId: config2.projectId } })}
2606
+ `
2607
+ );
2608
+ return 0;
2609
+ }
2610
+ const project2 = configFrom(context);
2611
+ const config = project2 ? mailboxConfigFrom(project2, context.env) : void 0;
2612
+ if (!config?.enabled)
2613
+ throw new MailboxError(
2614
+ "mailbox_disabled",
2615
+ "Enable [mailbox] in pmcp.toml first"
2616
+ );
2617
+ if (["export", "backup", "restore", "rotate"].includes(verb)) {
2618
+ if (!context.args.flags.has("operator"))
2619
+ throw new ArgumentError("Provide --operator for local administration");
2620
+ }
2621
+ if (verb === "restore") {
2622
+ await restoreBackup(required(context, "input"), {
2623
+ ...config,
2624
+ databasePath: resolve6(context.cwd, required(context, "output"))
2625
+ });
2626
+ context.ui.data(
2627
+ `${JSON.stringify({ ok: true, data: { restored: true } })}
2628
+ `
2629
+ );
2630
+ return 0;
2631
+ }
2632
+ const store = await MailboxStore.open(config);
2633
+ let binding;
2634
+ let ownsBinding = false;
2635
+ try {
2636
+ if (verb === "actor") {
2637
+ const parentFile = one(context.args, "parent-binding");
2638
+ const parent = parentFile ? readBinding(parentFile) : void 0;
2639
+ const destination = required(context, "actor-file");
2640
+ const credential2 = await store.registerActor(
2641
+ required(context, "actor"),
2642
+ parent?.actorId,
2643
+ (value) => writePrivateJson(destination, value),
2644
+ 6e4,
2645
+ parent
2646
+ );
2647
+ context.ui.data(
2648
+ `${JSON.stringify({ ok: true, data: { actorId: credential2.actorId } })}
2649
+ `
2650
+ );
2651
+ return 0;
2652
+ }
2653
+ if (verb === "rotate") {
2654
+ const destination = required(context, "actor-file");
2655
+ const credential2 = await store.rotateActor(
2656
+ required(context, "actor"),
2657
+ (value) => writePrivateJson(destination, value)
2658
+ );
2659
+ context.ui.data(
2660
+ `${JSON.stringify({ ok: true, data: { actorId: credential2.actorId } })}
2661
+ `
2662
+ );
2663
+ return 0;
2664
+ }
2665
+ if (verb === "backup" || verb === "export") {
2666
+ const destination = required(context, "output");
2667
+ if (verb === "backup") {
2668
+ await store.backup(destination);
2669
+ } else {
2670
+ const records = await store.exportRecords(
2671
+ one(context.args, "scope-actor")
2672
+ );
2673
+ writeFileSync4(
2674
+ destination,
2675
+ `${records.map((record) => JSON.stringify(record)).join("\n")}
2676
+ `,
2677
+ { flag: "wx", mode: 384 }
2678
+ );
2679
+ }
2680
+ context.ui.data(
2681
+ `${JSON.stringify({ ok: true, data: { completed: verb } })}
2682
+ `
2683
+ );
2684
+ return 0;
2685
+ }
2686
+ const operation = operationSchema.parse(verb);
2687
+ const actorFile = one(context.args, "actor-file");
2688
+ const bindingFile = one(context.args, "binding-file");
2689
+ if (Boolean(actorFile) === Boolean(bindingFile))
2690
+ throw new ArgumentError("Choose one --actor-file or --binding-file");
2691
+ if (bindingFile) {
2692
+ binding = readBinding(bindingFile);
2693
+ } else {
2694
+ binding = await store.attach(readCredential(actorFile), "operator");
2695
+ ownsBinding = true;
2696
+ }
2697
+ const inputFile = one(context.args, "input");
2698
+ const input = inputFile ? JSON.parse(readFileSync8(inputFile, "utf8")) : {};
2699
+ const result = await new MailboxService(store, binding).call(
2700
+ operation,
2701
+ input
2702
+ );
2703
+ context.ui.data(`${JSON.stringify(result)}
2704
+ `);
2705
+ return result.ok ? 0 : 1;
2706
+ } catch (error) {
2707
+ if (error instanceof ArgumentError) throw error;
2708
+ context.ui.data(`${JSON.stringify(failure(error))}
2709
+ `);
2710
+ return 1;
2711
+ } finally {
2712
+ try {
2713
+ if (ownsBinding && binding) await store.detach(binding);
2714
+ } finally {
2715
+ await store.close();
2716
+ }
2717
+ }
2718
+ }
2719
+ };
2720
+
2290
2721
  // src/commands/index.ts
2291
2722
  var COMMANDS = [
2292
2723
  serveCommand,
2293
2724
  listCommand,
2294
2725
  indexCommand,
2726
+ modelsCommand,
2295
2727
  loginCommand,
2296
2728
  logoutCommand,
2297
2729
  whoamiCommand,
@@ -2301,7 +2733,8 @@ var COMMANDS = [
2301
2733
  syncCommand,
2302
2734
  projectCommand,
2303
2735
  doctorCommand,
2304
- validateCommand
2736
+ validateCommand,
2737
+ mailboxCommand
2305
2738
  ];
2306
2739
  var PLANNED = [];
2307
2740
  var GLOBAL = /* @__PURE__ */ new Set(["-q", "--quiet", "--no-color", "--color"]);
@@ -2347,7 +2780,7 @@ function dispatch(argv, options = {}) {
2347
2780
  }
2348
2781
  return run(command, rest, { ui, env, cwd });
2349
2782
  }
2350
- var VERSION = "0.9.1";
2783
+ var VERSION = "0.10.0";
2351
2784
  async function run(command, argv, context) {
2352
2785
  try {
2353
2786
  return await command.run({