envpkt 0.6.10 → 0.7.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
@@ -3,8 +3,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync,
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
+ import { Cond, Either, Left, List, Option, Right, Try } from "functype";
6
7
  import { TypeCompiler } from "@sinclair/typebox/compiler";
7
- import { Cond, Left, List, Option, Right, Try } from "functype";
8
8
  import { Env, Fs, Path, Platform } from "functype-os";
9
9
  import { TomlDate, parse, stringify } from "smol-toml";
10
10
  import { FormatRegistry, Type } from "@sinclair/typebox";
@@ -15,6 +15,105 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
15
15
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { createInterface } from "node:readline";
17
17
 
18
+ //#region src/core/audit.ts
19
+ const MS_PER_DAY = 864e5;
20
+ const WARN_BEFORE_DAYS = 30;
21
+ const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
22
+ const parseDate = (dateStr) => {
23
+ const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
24
+ return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
25
+ };
26
+ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
27
+ const issues = [];
28
+ const created = Option(meta?.created).flatMap(parseDate);
29
+ const expires = Option(meta?.expires).flatMap(parseDate);
30
+ const rotationUrl = Option(meta?.rotation_url);
31
+ const purpose = Option(meta?.purpose);
32
+ const service = Option(meta?.service);
33
+ const daysRemaining = expires.map((exp) => daysBetween(today, exp));
34
+ const daysSinceCreated = created.map((c) => daysBetween(c, today));
35
+ const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
36
+ const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
37
+ const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
38
+ const hasSealed = !!meta?.encrypted_value;
39
+ const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
40
+ const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
41
+ if (isExpired) issues.push("Secret has expired");
42
+ if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
43
+ if (isStale) issues.push("Secret is stale (no rotation detected)");
44
+ if (isMissing) issues.push("Key not found in fnox");
45
+ if (isMissingMetadata) {
46
+ if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
47
+ if (requireService && service.isNone()) issues.push("Missing required service");
48
+ }
49
+ return {
50
+ key,
51
+ service,
52
+ status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
53
+ days_remaining: daysRemaining,
54
+ rotation_url: rotationUrl,
55
+ purpose,
56
+ created: Option(meta?.created),
57
+ expires: Option(meta?.expires),
58
+ issues: List(issues)
59
+ };
60
+ };
61
+ const computeAudit = (config, fnoxKeys, today) => {
62
+ const now = today ?? /* @__PURE__ */ new Date();
63
+ const lifecycle = config.lifecycle ?? {};
64
+ const staleWarningDays = lifecycle.stale_warning_days ?? 90;
65
+ const requireExpiration = lifecycle.require_expiration ?? false;
66
+ const requireService = lifecycle.require_service ?? false;
67
+ const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
68
+ const secretEntries = config.secret ?? {};
69
+ const metaKeys = new Set(Object.keys(secretEntries));
70
+ const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
71
+ const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
72
+ const total = secrets.size;
73
+ const expired = secrets.count((s) => s.status === "expired");
74
+ const missing = secrets.count((s) => s.status === "missing");
75
+ const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
76
+ const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
77
+ const stale = secrets.count((s) => s.status === "stale");
78
+ const healthy = secrets.count((s) => s.status === "healthy");
79
+ return {
80
+ status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
81
+ secrets,
82
+ total,
83
+ healthy,
84
+ expiring_soon,
85
+ expired,
86
+ stale,
87
+ missing,
88
+ missing_metadata,
89
+ orphaned,
90
+ identity: config.identity
91
+ };
92
+ };
93
+ const computeEnvAudit = (config, env = process.env) => {
94
+ const envEntries = config.env ?? {};
95
+ const entries = [];
96
+ for (const [key, entry] of Object.entries(envEntries)) {
97
+ const currentValue = env[key];
98
+ const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
99
+ entries.push({
100
+ key,
101
+ defaultValue: entry.value,
102
+ currentValue,
103
+ status,
104
+ purpose: entry.purpose
105
+ });
106
+ }
107
+ return {
108
+ entries,
109
+ total: entries.length,
110
+ defaults_applied: entries.filter((e) => e.status === "default").length,
111
+ overridden: entries.filter((e) => e.status === "overridden").length,
112
+ missing: entries.filter((e) => e.status === "missing").length
113
+ };
114
+ };
115
+
116
+ //#endregion
18
117
  //#region src/core/schema.ts
19
118
  const DATE_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
20
119
  const URI_RE = /^https?:\/\/.+/;
@@ -270,6 +369,83 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
270
369
  }));
271
370
  };
272
371
 
372
+ //#endregion
373
+ //#region src/core/catalog.ts
374
+ /** Load and validate a catalog file, mapping ConfigError → CatalogError */
375
+ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
376
+ if (err._tag === "FileNotFound") return Left({
377
+ _tag: "CatalogNotFound",
378
+ path: err.path
379
+ });
380
+ return Left({
381
+ _tag: "CatalogLoadError",
382
+ message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
383
+ });
384
+ }, (config) => Right(config));
385
+ /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
386
+ const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
387
+ const resolved = {};
388
+ for (const key of agentSecrets) {
389
+ const catalogEntry = catalogMeta[key];
390
+ if (!catalogEntry) return Left({
391
+ _tag: "SecretNotInCatalog",
392
+ key,
393
+ catalogPath
394
+ });
395
+ const agentOverride = agentMeta[key];
396
+ if (agentOverride) resolved[key] = {
397
+ ...catalogEntry,
398
+ ...agentOverride
399
+ };
400
+ else resolved[key] = catalogEntry;
401
+ }
402
+ return Right(resolved);
403
+ };
404
+ /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
405
+ const resolveConfig = (agentConfig, agentConfigDir) => {
406
+ if (!agentConfig.catalog) return Right({
407
+ config: agentConfig,
408
+ merged: [],
409
+ overridden: [],
410
+ warnings: []
411
+ });
412
+ if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
413
+ _tag: "MissingSecretsList",
414
+ message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
415
+ });
416
+ const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
417
+ const agentSecrets = agentConfig.identity.secrets;
418
+ const agentSecretEntries = agentConfig.secret ?? {};
419
+ return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
420
+ const merged = [];
421
+ const overridden = [];
422
+ const warnings = [];
423
+ for (const key of agentSecrets) {
424
+ merged.push(key);
425
+ if (agentSecretEntries[key]) overridden.push(key);
426
+ }
427
+ const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
428
+ const identityData = agentConfig.identity ? (() => {
429
+ const { secrets: _secrets, ...rest } = agentConfig.identity;
430
+ return rest;
431
+ })() : void 0;
432
+ return {
433
+ config: {
434
+ ...agentWithoutCatalog,
435
+ identity: identityData ? {
436
+ ...identityData,
437
+ name: identityData.name
438
+ } : void 0,
439
+ secret: resolvedMeta
440
+ },
441
+ catalogPath,
442
+ merged,
443
+ overridden,
444
+ warnings
445
+ };
446
+ }));
447
+ };
448
+
273
449
  //#endregion
274
450
  //#region src/cli/output.ts
275
451
  const RESET = "\x1B[0m";
@@ -454,306 +630,27 @@ const formatCheckTable = (check) => {
454
630
  const formatCheckJson = (check) => JSON.stringify({
455
631
  is_clean: check.is_clean,
456
632
  tracked_and_present: check.tracked_and_present,
457
- missing_from_env: check.missing_from_env,
458
- untracked_credentials: check.untracked_credentials,
459
- entries: check.entries.map((e) => ({
460
- envVar: e.envVar,
461
- service: e.service.fold(() => null, (s) => s),
462
- status: e.status,
463
- confidence: e.confidence.fold(() => null, (c) => c)
464
- })).toArray()
465
- }, null, 2);
466
- const formatAuditMinimal = (audit) => {
467
- if (audit.status === "healthy") return `${GREEN}✓${RESET} ${audit.total} secrets healthy`;
468
- const parts = [];
469
- if (audit.expired > 0) parts.push(`${audit.expired} expired`);
470
- if (audit.expiring_soon > 0) parts.push(`${audit.expiring_soon} expiring`);
471
- if (audit.stale > 0) parts.push(`${audit.stale} stale`);
472
- if (audit.missing > 0) parts.push(`${audit.missing} missing`);
473
- return `${audit.status === "critical" ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`} ${parts.join(", ")}`;
474
- };
475
- const formatConfigSource = (path, source) => {
476
- if (source === "cwd") return "";
477
- return `${DIM}envpkt: loaded ${path}${RESET}`;
478
- };
479
-
480
- //#endregion
481
- //#region src/cli/commands/add.ts
482
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
483
- const buildSecretBlock = (name, options) => {
484
- const lines = [`[secret.${name}]`];
485
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
486
- if (options.service) lines.push(`service = "${options.service}"`);
487
- if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
488
- if (options.comment) lines.push(`comment = "${options.comment}"`);
489
- lines.push(`created = "${today}"`);
490
- if (options.expires) lines.push(`expires = "${options.expires}"`);
491
- if (options.rotates) lines.push(`rotates = "${options.rotates}"`);
492
- if (options.rateLimit) lines.push(`rate_limit = "${options.rateLimit}"`);
493
- if (options.modelHint) lines.push(`model_hint = "${options.modelHint}"`);
494
- if (options.source) lines.push(`source = "${options.source}"`);
495
- if (options.rotationUrl) lines.push(`rotation_url = "${options.rotationUrl}"`);
496
- if (options.required) lines.push(`required = true`);
497
- if (options.capabilities) {
498
- const caps = options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ");
499
- lines.push(`capabilities = [${caps}]`);
500
- }
501
- if (options.tags) {
502
- const pairs = options.tags.split(",").map((pair) => {
503
- const [k, v] = pair.split("=").map((s) => s.trim());
504
- return `${k} = "${v}"`;
505
- });
506
- lines.push(`tags = { ${pairs.join(", ")} }`);
507
- }
508
- return `${lines.join("\n")}\n`;
509
- };
510
- const runAdd = (name, options) => {
511
- if (options.expires && !DATE_RE.test(options.expires)) {
512
- console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
513
- process.exit(1);
514
- }
515
- resolveConfigPath(options.config).fold((err) => {
516
- console.error(formatError(err));
517
- process.exit(2);
518
- }, ({ path: configPath, source }) => {
519
- const sourceMsg = formatConfigSource(configPath, source);
520
- if (sourceMsg) console.error(sourceMsg);
521
- loadConfig(configPath).fold((err) => {
522
- console.error(formatError(err));
523
- process.exit(2);
524
- }, (config) => {
525
- if (config.secret?.[name]) {
526
- console.error(`${RED}Error:${RESET} Secret "${name}" already exists in ${configPath}`);
527
- process.exit(1);
528
- }
529
- const block = buildSecretBlock(name, options);
530
- if (options.dryRun) {
531
- console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
532
- console.log(block);
533
- return;
534
- }
535
- writeFileSync(configPath, `${readFileSync(configPath, "utf-8").trimEnd()}\n\n${block}`, "utf-8");
536
- console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
537
- });
538
- });
539
- };
540
-
541
- //#endregion
542
- //#region src/cli/commands/add-env.ts
543
- const buildEnvBlock = (name, value, options) => {
544
- const lines = [`[env.${name}]`, `value = "${value}"`];
545
- if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
546
- if (options.comment) lines.push(`comment = "${options.comment}"`);
547
- if (options.tags) {
548
- const pairs = options.tags.split(",").map((pair) => {
549
- const [k, v] = pair.split("=").map((s) => s.trim());
550
- return `${k} = "${v}"`;
551
- });
552
- lines.push(`tags = { ${pairs.join(", ")} }`);
553
- }
554
- return `${lines.join("\n")}\n`;
555
- };
556
- const runAddEnv = (name, value, options) => {
557
- resolveConfigPath(options.config).fold((err) => {
558
- console.error(formatError(err));
559
- process.exit(2);
560
- }, ({ path: configPath, source }) => {
561
- const sourceMsg = formatConfigSource(configPath, source);
562
- if (sourceMsg) console.error(sourceMsg);
563
- loadConfig(configPath).fold((err) => {
564
- console.error(formatError(err));
565
- process.exit(2);
566
- }, (config) => {
567
- if (config.env?.[name]) {
568
- console.error(`${RED}Error:${RESET} Env entry "${name}" already exists in ${configPath}`);
569
- process.exit(1);
570
- }
571
- const block = buildEnvBlock(name, value, options);
572
- if (options.dryRun) {
573
- console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
574
- console.log(block);
575
- return;
576
- }
577
- writeFileSync(configPath, `${readFileSync(configPath, "utf-8").trimEnd()}\n\n${block}`, "utf-8");
578
- console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
579
- });
580
- });
581
- };
582
-
583
- //#endregion
584
- //#region src/core/audit.ts
585
- const MS_PER_DAY = 864e5;
586
- const WARN_BEFORE_DAYS = 30;
587
- const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
588
- const parseDate = (dateStr) => {
589
- const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
590
- return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
591
- };
592
- const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
593
- const issues = [];
594
- const created = Option(meta?.created).flatMap(parseDate);
595
- const expires = Option(meta?.expires).flatMap(parseDate);
596
- const rotationUrl = Option(meta?.rotation_url);
597
- const purpose = Option(meta?.purpose);
598
- const service = Option(meta?.service);
599
- const daysRemaining = expires.map((exp) => daysBetween(today, exp));
600
- const daysSinceCreated = created.map((c) => daysBetween(c, today));
601
- const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
602
- const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
603
- const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
604
- const hasSealed = !!meta?.encrypted_value;
605
- const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
606
- const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
607
- if (isExpired) issues.push("Secret has expired");
608
- if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
609
- if (isStale) issues.push("Secret is stale (no rotation detected)");
610
- if (isMissing) issues.push("Key not found in fnox");
611
- if (isMissingMetadata) {
612
- if (requireExpiration && expires.isNone()) issues.push("Missing required expiration date");
613
- if (requireService && service.isNone()) issues.push("Missing required service");
614
- }
615
- return {
616
- key,
617
- service,
618
- status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
619
- days_remaining: daysRemaining,
620
- rotation_url: rotationUrl,
621
- purpose,
622
- created: Option(meta?.created),
623
- expires: Option(meta?.expires),
624
- issues: List(issues)
625
- };
626
- };
627
- const computeAudit = (config, fnoxKeys, today) => {
628
- const now = today ?? /* @__PURE__ */ new Date();
629
- const lifecycle = config.lifecycle ?? {};
630
- const staleWarningDays = lifecycle.stale_warning_days ?? 90;
631
- const requireExpiration = lifecycle.require_expiration ?? false;
632
- const requireService = lifecycle.require_service ?? false;
633
- const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
634
- const secretEntries = config.secret ?? {};
635
- const metaKeys = new Set(Object.keys(secretEntries));
636
- const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
637
- const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
638
- const total = secrets.size;
639
- const expired = secrets.count((s) => s.status === "expired");
640
- const missing = secrets.count((s) => s.status === "missing");
641
- const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
642
- const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
643
- const stale = secrets.count((s) => s.status === "stale");
644
- const healthy = secrets.count((s) => s.status === "healthy");
645
- return {
646
- status: Cond.of().when(expired > 0 || missing > 0, "critical").elseWhen(expiring_soon > 0 || stale > 0 || missing_metadata > 0, "degraded").else("healthy"),
647
- secrets,
648
- total,
649
- healthy,
650
- expiring_soon,
651
- expired,
652
- stale,
653
- missing,
654
- missing_metadata,
655
- orphaned,
656
- identity: config.identity
657
- };
658
- };
659
- const computeEnvAudit = (config, env = process.env) => {
660
- const envEntries = config.env ?? {};
661
- const entries = [];
662
- for (const [key, entry] of Object.entries(envEntries)) {
663
- const currentValue = env[key];
664
- const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
665
- entries.push({
666
- key,
667
- defaultValue: entry.value,
668
- currentValue,
669
- status,
670
- purpose: entry.purpose
671
- });
672
- }
673
- return {
674
- entries,
675
- total: entries.length,
676
- defaults_applied: entries.filter((e) => e.status === "default").length,
677
- overridden: entries.filter((e) => e.status === "overridden").length,
678
- missing: entries.filter((e) => e.status === "missing").length
679
- };
680
- };
681
-
682
- //#endregion
683
- //#region src/core/catalog.ts
684
- /** Load and validate a catalog file, mapping ConfigError → CatalogError */
685
- const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
686
- if (err._tag === "FileNotFound") return Left({
687
- _tag: "CatalogNotFound",
688
- path: err.path
689
- });
690
- return Left({
691
- _tag: "CatalogLoadError",
692
- message: `${err._tag}: ${"message" in err ? err.message : String(err)}`
693
- });
694
- }, (config) => Right(config));
695
- /** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
696
- const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
697
- const resolved = {};
698
- for (const key of agentSecrets) {
699
- const catalogEntry = catalogMeta[key];
700
- if (!catalogEntry) return Left({
701
- _tag: "SecretNotInCatalog",
702
- key,
703
- catalogPath
704
- });
705
- const agentOverride = agentMeta[key];
706
- if (agentOverride) resolved[key] = {
707
- ...catalogEntry,
708
- ...agentOverride
709
- };
710
- else resolved[key] = catalogEntry;
711
- }
712
- return Right(resolved);
713
- };
714
- /** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
715
- const resolveConfig = (agentConfig, agentConfigDir) => {
716
- if (!agentConfig.catalog) return Right({
717
- config: agentConfig,
718
- merged: [],
719
- overridden: [],
720
- warnings: []
721
- });
722
- if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
723
- _tag: "MissingSecretsList",
724
- message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
725
- });
726
- const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
727
- const agentSecrets = agentConfig.identity.secrets;
728
- const agentSecretEntries = agentConfig.secret ?? {};
729
- return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
730
- const merged = [];
731
- const overridden = [];
732
- const warnings = [];
733
- for (const key of agentSecrets) {
734
- merged.push(key);
735
- if (agentSecretEntries[key]) overridden.push(key);
736
- }
737
- const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
738
- const identityData = agentConfig.identity ? (() => {
739
- const { secrets: _secrets, ...rest } = agentConfig.identity;
740
- return rest;
741
- })() : void 0;
742
- return {
743
- config: {
744
- ...agentWithoutCatalog,
745
- identity: identityData ? {
746
- ...identityData,
747
- name: identityData.name
748
- } : void 0,
749
- secret: resolvedMeta
750
- },
751
- catalogPath,
752
- merged,
753
- overridden,
754
- warnings
755
- };
756
- }));
633
+ missing_from_env: check.missing_from_env,
634
+ untracked_credentials: check.untracked_credentials,
635
+ entries: check.entries.map((e) => ({
636
+ envVar: e.envVar,
637
+ service: e.service.fold(() => null, (s) => s),
638
+ status: e.status,
639
+ confidence: e.confidence.fold(() => null, (c) => c)
640
+ })).toArray()
641
+ }, null, 2);
642
+ const formatAuditMinimal = (audit) => {
643
+ if (audit.status === "healthy") return `${GREEN}✓${RESET} ${audit.total} secrets healthy`;
644
+ const parts = [];
645
+ if (audit.expired > 0) parts.push(`${audit.expired} expired`);
646
+ if (audit.expiring_soon > 0) parts.push(`${audit.expiring_soon} expiring`);
647
+ if (audit.stale > 0) parts.push(`${audit.stale} stale`);
648
+ if (audit.missing > 0) parts.push(`${audit.missing} missing`);
649
+ return `${audit.status === "critical" ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`} ${parts.join(", ")}`;
650
+ };
651
+ const formatConfigSource = (path, source) => {
652
+ if (source === "cwd") return "";
653
+ return `${DIM}envpkt: loaded ${path}${RESET}`;
757
654
  };
758
655
 
759
656
  //#endregion
@@ -2039,6 +1936,156 @@ created = "${todayIso$1()}"
2039
1936
  return blocks.join("\n");
2040
1937
  };
2041
1938
 
1939
+ //#endregion
1940
+ //#region src/core/toml-edit.ts
1941
+ const SECTION_RE = /^\[.+\]\s*$/;
1942
+ const MULTILINE_OPEN = "\"\"\"";
1943
+ /**
1944
+ * Find the line range [start, end) of a TOML section by its header string.
1945
+ * The range includes the header line through to (but not including) the next section header or EOF.
1946
+ * Handles multiline `"""..."""` values when scanning for section boundaries.
1947
+ */
1948
+ const findSectionRange = (lines, sectionHeader) => {
1949
+ let start = -1;
1950
+ for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
1951
+ start = i;
1952
+ break;
1953
+ }
1954
+ if (start === -1) return void 0;
1955
+ let end = lines.length;
1956
+ let inMultiline = false;
1957
+ for (let i = start + 1; i < lines.length; i++) {
1958
+ const line = lines[i];
1959
+ if (inMultiline) {
1960
+ if (line.includes(MULTILINE_OPEN)) inMultiline = false;
1961
+ continue;
1962
+ }
1963
+ if (line.includes(MULTILINE_OPEN)) {
1964
+ if ((line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) inMultiline = true;
1965
+ continue;
1966
+ }
1967
+ if (SECTION_RE.test(line)) {
1968
+ end = i;
1969
+ break;
1970
+ }
1971
+ }
1972
+ return {
1973
+ start,
1974
+ end
1975
+ };
1976
+ };
1977
+ /** Check whether a section header exists in the raw TOML */
1978
+ const sectionExists = (lines, sectionHeader) => lines.some((l) => l.trim() === sectionHeader);
1979
+ /**
1980
+ * Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
1981
+ * Strips trailing blank lines left behind.
1982
+ */
1983
+ const removeSection = (raw, sectionHeader) => {
1984
+ const lines = raw.split("\n");
1985
+ const range = findSectionRange(lines, sectionHeader);
1986
+ if (!range) return Either.left({
1987
+ _tag: "SectionNotFound",
1988
+ section: sectionHeader
1989
+ });
1990
+ let removeEnd = range.end;
1991
+ while (removeEnd > range.start && removeEnd - 1 >= range.start && lines[removeEnd - 1].trim() === "") removeEnd--;
1992
+ const before = lines.slice(0, range.start);
1993
+ const after = lines.slice(range.end);
1994
+ while (before.length > 0 && before[before.length - 1].trim() === "") before.pop();
1995
+ const result = [...before, ...after].join("\n");
1996
+ return Either.right(result);
1997
+ };
1998
+ /**
1999
+ * Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
2000
+ * Errors if old doesn't exist or new already exists.
2001
+ */
2002
+ const renameSection = (raw, oldHeader, newHeader) => {
2003
+ const lines = raw.split("\n");
2004
+ if (!sectionExists(lines, oldHeader)) return Either.left({
2005
+ _tag: "SectionNotFound",
2006
+ section: oldHeader
2007
+ });
2008
+ if (sectionExists(lines, newHeader)) return Either.left({
2009
+ _tag: "SectionAlreadyExists",
2010
+ section: newHeader
2011
+ });
2012
+ const result = lines.map((line) => line.trim() === oldHeader ? newHeader : line).join("\n");
2013
+ return Either.right(result);
2014
+ };
2015
+ /**
2016
+ * Update, add, or remove fields within an existing TOML section.
2017
+ * - A string value replaces or adds the field
2018
+ * - A null value removes the field
2019
+ * Does NOT re-serialize — operates on raw text lines.
2020
+ */
2021
+ const updateSectionFields = (raw, sectionHeader, updates) => {
2022
+ const lines = raw.split("\n");
2023
+ const range = findSectionRange(lines, sectionHeader);
2024
+ if (!range) return Either.left({
2025
+ _tag: "SectionNotFound",
2026
+ section: sectionHeader
2027
+ });
2028
+ const before = lines.slice(0, range.start + 1);
2029
+ const after = lines.slice(range.end);
2030
+ const sectionBody = lines.slice(range.start + 1, range.end);
2031
+ const remaining = [];
2032
+ const updatedKeys = /* @__PURE__ */ new Set();
2033
+ let inMultiline = false;
2034
+ let multilineKey = "";
2035
+ for (let i = 0; i < sectionBody.length; i++) {
2036
+ const line = sectionBody[i];
2037
+ if (inMultiline) {
2038
+ if (line.includes(MULTILINE_OPEN)) {
2039
+ inMultiline = false;
2040
+ if (updates[multilineKey] === null) continue;
2041
+ if (multilineKey in updates) continue;
2042
+ } else {
2043
+ if (updates[multilineKey] === null) continue;
2044
+ if (multilineKey in updates) continue;
2045
+ }
2046
+ remaining.push(line);
2047
+ continue;
2048
+ }
2049
+ const eqIdx = line.indexOf("=");
2050
+ if (eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[")) {
2051
+ const key = line.slice(0, eqIdx).trim();
2052
+ if (key in updates) {
2053
+ updatedKeys.add(key);
2054
+ const afterEquals = line.slice(eqIdx + 1).trim();
2055
+ if (afterEquals.includes(MULTILINE_OPEN)) {
2056
+ if ((afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) {
2057
+ inMultiline = true;
2058
+ multilineKey = key;
2059
+ }
2060
+ }
2061
+ if (updates[key] === null) continue;
2062
+ remaining.push(`${key} = ${updates[key]}`);
2063
+ if (inMultiline) {
2064
+ for (let j = i + 1; j < sectionBody.length; j++) if (sectionBody[j].includes(MULTILINE_OPEN)) {
2065
+ i = j;
2066
+ inMultiline = false;
2067
+ break;
2068
+ }
2069
+ }
2070
+ continue;
2071
+ }
2072
+ }
2073
+ remaining.push(line);
2074
+ }
2075
+ for (const [key, value] of Object.entries(updates)) if (value !== null && !updatedKeys.has(key)) remaining.push(`${key} = ${value}`);
2076
+ const result = [
2077
+ ...before,
2078
+ ...remaining,
2079
+ ...after
2080
+ ].join("\n");
2081
+ return Either.right(result);
2082
+ };
2083
+ /**
2084
+ * Append a new TOML section block to the end of the file.
2085
+ * Ensures proper spacing (double newline before the block).
2086
+ */
2087
+ const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
2088
+
2042
2089
  //#endregion
2043
2090
  //#region src/cli/commands/env.ts
2044
2091
  const printPostWriteGuidance = () => {
@@ -2131,6 +2178,148 @@ const runEnvExport = (options) => {
2131
2178
  for (const [key, value] of Object.entries(boot.secrets)) console.log(`export ${key}='${shellEscape(value)}'`);
2132
2179
  });
2133
2180
  };
2181
+ const buildEnvBlock = (name, value, options) => {
2182
+ const lines = [`[env.${name}]`, `value = "${value}"`];
2183
+ if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
2184
+ if (options.comment) lines.push(`comment = "${options.comment}"`);
2185
+ if (options.tags) {
2186
+ const pairs = options.tags.split(",").map((pair) => {
2187
+ const [k, v] = pair.split("=").map((s) => s.trim());
2188
+ return `${k} = "${v}"`;
2189
+ });
2190
+ lines.push(`tags = { ${pairs.join(", ")} }`);
2191
+ }
2192
+ return `${lines.join("\n")}\n`;
2193
+ };
2194
+ const withConfig$1 = (configFlag, fn) => {
2195
+ resolveConfigPath(configFlag).fold((err) => {
2196
+ console.error(formatError(err));
2197
+ process.exit(2);
2198
+ }, ({ path: configPath, source }) => {
2199
+ const sourceMsg = formatConfigSource(configPath, source);
2200
+ if (sourceMsg) console.error(sourceMsg);
2201
+ fn(configPath, readFileSync(configPath, "utf-8"));
2202
+ });
2203
+ };
2204
+ const runEnvAdd = (name, value, options) => {
2205
+ resolveConfigPath(options.config).fold((err) => {
2206
+ console.error(formatError(err));
2207
+ process.exit(2);
2208
+ }, ({ path: configPath, source }) => {
2209
+ const sourceMsg = formatConfigSource(configPath, source);
2210
+ if (sourceMsg) console.error(sourceMsg);
2211
+ loadConfig(configPath).fold((err) => {
2212
+ console.error(formatError(err));
2213
+ process.exit(2);
2214
+ }, (config) => {
2215
+ if (config.env?.[name]) {
2216
+ console.error(`${RED}Error:${RESET} Env entry "${name}" already exists in ${configPath}`);
2217
+ process.exit(1);
2218
+ }
2219
+ const block = buildEnvBlock(name, value, options);
2220
+ if (options.dryRun) {
2221
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
2222
+ console.log(block);
2223
+ return;
2224
+ }
2225
+ writeFileSync(configPath, appendSection(readFileSync(configPath, "utf-8"), block), "utf-8");
2226
+ console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
2227
+ });
2228
+ });
2229
+ };
2230
+ const runEnvEdit = (name, options) => {
2231
+ withConfig$1(options.config, (configPath, raw) => {
2232
+ loadConfig(configPath).fold((err) => {
2233
+ console.error(formatError(err));
2234
+ process.exit(2);
2235
+ }, (config) => {
2236
+ if (!config.env?.[name]) {
2237
+ console.error(`${RED}Error:${RESET} Env entry "${name}" not found in ${configPath}`);
2238
+ process.exit(1);
2239
+ }
2240
+ const updates = {};
2241
+ if (options.value !== void 0) updates["value"] = `"${options.value}"`;
2242
+ if (options.purpose !== void 0) updates["purpose"] = `"${options.purpose}"`;
2243
+ if (options.comment !== void 0) updates["comment"] = `"${options.comment}"`;
2244
+ if (options.tags !== void 0) updates["tags"] = `{ ${options.tags.split(",").map((pair) => {
2245
+ const [k, v] = pair.split("=").map((s) => s.trim());
2246
+ return `${k} = "${v}"`;
2247
+ }).join(", ")} }`;
2248
+ if (Object.keys(updates).length === 0) {
2249
+ console.error(`${RED}Error:${RESET} No fields to update. Provide at least one --flag.`);
2250
+ process.exit(1);
2251
+ }
2252
+ updateSectionFields(raw, `[env.${name}]`, updates).fold((err) => {
2253
+ console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
2254
+ process.exit(2);
2255
+ }, (updated) => {
2256
+ if (options.dryRun) {
2257
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
2258
+ console.log(updated);
2259
+ return;
2260
+ }
2261
+ writeFileSync(configPath, updated, "utf-8");
2262
+ console.log(`${GREEN}✓${RESET} Updated ${BOLD}${name}${RESET} in ${CYAN}${configPath}${RESET}`);
2263
+ });
2264
+ });
2265
+ });
2266
+ };
2267
+ const runEnvRm = (name, options) => {
2268
+ withConfig$1(options.config, (configPath, raw) => {
2269
+ removeSection(raw, `[env.${name}]`).fold((err) => {
2270
+ console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
2271
+ process.exit(1);
2272
+ }, (updated) => {
2273
+ if (options.dryRun) {
2274
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
2275
+ console.log(updated);
2276
+ return;
2277
+ }
2278
+ writeFileSync(configPath, updated, "utf-8");
2279
+ console.log(`${GREEN}✓${RESET} Removed ${BOLD}${name}${RESET} from ${CYAN}${configPath}${RESET}`);
2280
+ });
2281
+ });
2282
+ };
2283
+ const runEnvRename = (oldName, newName, options) => {
2284
+ withConfig$1(options.config, (configPath, raw) => {
2285
+ renameSection(raw, `[env.${oldName}]`, `[env.${newName}]`).fold((err) => {
2286
+ console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
2287
+ process.exit(1);
2288
+ }, (updated) => {
2289
+ if (options.dryRun) {
2290
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
2291
+ console.log(updated);
2292
+ return;
2293
+ }
2294
+ writeFileSync(configPath, updated, "utf-8");
2295
+ console.log(`${GREEN}✓${RESET} Renamed ${BOLD}${oldName}${RESET} → ${BOLD}${newName}${RESET} in ${CYAN}${configPath}${RESET}`);
2296
+ });
2297
+ });
2298
+ };
2299
+ const registerEnvCommands = (program) => {
2300
+ const env = program.command("env").description("Manage environment defaults and discover credentials");
2301
+ env.command("scan").description("Auto-discover credentials from process.env and scaffold TOML entries — first step in the developer workflow").option("-c, --config <path>", "Path to envpkt.toml (write target for --write)").option("--format <format>", "Output format: table | json", "table").option("--write", "Write discovered credentials to envpkt.toml").option("--dry-run", "Preview TOML that would be written (implies --write)").option("--include-unknown", "Include vars where service could not be inferred").action((options) => {
2302
+ runEnvScan(options);
2303
+ });
2304
+ env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
2305
+ runEnvCheck(options);
2306
+ });
2307
+ env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").action((options) => {
2308
+ runEnvExport(options);
2309
+ });
2310
+ env.command("add").description("Add a new environment default entry to envpkt.toml").argument("<name>", "Environment variable name").argument("<value>", "Default value").option("-c, --config <path>", "Path to envpkt.toml").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, value, options) => {
2311
+ runEnvAdd(name, value, options);
2312
+ });
2313
+ env.command("edit").description("Update fields on an existing env entry").argument("<name>", "Environment variable name to edit").option("-c, --config <path>", "Path to envpkt.toml").option("--value <value>", "New default value").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the changes without writing").action((name, options) => {
2314
+ runEnvEdit(name, options);
2315
+ });
2316
+ env.command("rm").description("Remove an env entry from envpkt.toml").argument("<name>", "Environment variable name to remove").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((name, options) => {
2317
+ runEnvRm(name, options);
2318
+ });
2319
+ env.command("rename").description("Rename an env entry, preserving all fields").argument("<old>", "Current env variable name").argument("<new>", "New env variable name").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((oldName, newName, options) => {
2320
+ runEnvRename(oldName, newName, options);
2321
+ });
2322
+ };
2134
2323
 
2135
2324
  //#endregion
2136
2325
  //#region src/cli/commands/exec.ts
@@ -3171,6 +3360,178 @@ const runSeal = async (options) => {
3171
3360
  });
3172
3361
  };
3173
3362
 
3363
+ //#endregion
3364
+ //#region src/cli/commands/secret.ts
3365
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3366
+ const buildSecretBlock = (name, options) => {
3367
+ const lines = [`[secret.${name}]`];
3368
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3369
+ if (options.service) lines.push(`service = "${options.service}"`);
3370
+ if (options.purpose) lines.push(`purpose = "${options.purpose}"`);
3371
+ if (options.comment) lines.push(`comment = "${options.comment}"`);
3372
+ lines.push(`created = "${today}"`);
3373
+ if (options.expires) lines.push(`expires = "${options.expires}"`);
3374
+ if (options.rotates) lines.push(`rotates = "${options.rotates}"`);
3375
+ if (options.rateLimit) lines.push(`rate_limit = "${options.rateLimit}"`);
3376
+ if (options.modelHint) lines.push(`model_hint = "${options.modelHint}"`);
3377
+ if (options.source) lines.push(`source = "${options.source}"`);
3378
+ if (options.rotationUrl) lines.push(`rotation_url = "${options.rotationUrl}"`);
3379
+ if (options.required) lines.push(`required = true`);
3380
+ if (options.capabilities) {
3381
+ const caps = options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ");
3382
+ lines.push(`capabilities = [${caps}]`);
3383
+ }
3384
+ if (options.tags) {
3385
+ const pairs = options.tags.split(",").map((pair) => {
3386
+ const [k, v] = pair.split("=").map((s) => s.trim());
3387
+ return `${k} = "${v}"`;
3388
+ });
3389
+ lines.push(`tags = { ${pairs.join(", ")} }`);
3390
+ }
3391
+ return `${lines.join("\n")}\n`;
3392
+ };
3393
+ const buildFieldUpdates = (options) => {
3394
+ const updates = {};
3395
+ if (options.service !== void 0) updates["service"] = `"${options.service}"`;
3396
+ if (options.purpose !== void 0) updates["purpose"] = `"${options.purpose}"`;
3397
+ if (options.comment !== void 0) updates["comment"] = `"${options.comment}"`;
3398
+ if (options.expires !== void 0) updates["expires"] = `"${options.expires}"`;
3399
+ if (options.rotates !== void 0) updates["rotates"] = `"${options.rotates}"`;
3400
+ if (options.rateLimit !== void 0) updates["rate_limit"] = `"${options.rateLimit}"`;
3401
+ if (options.modelHint !== void 0) updates["model_hint"] = `"${options.modelHint}"`;
3402
+ if (options.source !== void 0) updates["source"] = `"${options.source}"`;
3403
+ if (options.rotationUrl !== void 0) updates["rotation_url"] = `"${options.rotationUrl}"`;
3404
+ if (options.required !== void 0) updates["required"] = options.required ? "true" : "false";
3405
+ if (options.capabilities !== void 0) updates["capabilities"] = `[${options.capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ")}]`;
3406
+ if (options.tags !== void 0) updates["tags"] = `{ ${options.tags.split(",").map((pair) => {
3407
+ const [k, v] = pair.split("=").map((s) => s.trim());
3408
+ return `${k} = "${v}"`;
3409
+ }).join(", ")} }`;
3410
+ return updates;
3411
+ };
3412
+ const withConfig = (configFlag, fn) => {
3413
+ resolveConfigPath(configFlag).fold((err) => {
3414
+ console.error(formatError(err));
3415
+ process.exit(2);
3416
+ }, ({ path: configPath, source }) => {
3417
+ const sourceMsg = formatConfigSource(configPath, source);
3418
+ if (sourceMsg) console.error(sourceMsg);
3419
+ fn(configPath, readFileSync(configPath, "utf-8"));
3420
+ });
3421
+ };
3422
+ const runSecretAdd = (name, options) => {
3423
+ if (options.expires && !DATE_RE.test(options.expires)) {
3424
+ console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
3425
+ process.exit(1);
3426
+ }
3427
+ resolveConfigPath(options.config).fold((err) => {
3428
+ console.error(formatError(err));
3429
+ process.exit(2);
3430
+ }, ({ path: configPath, source }) => {
3431
+ const sourceMsg = formatConfigSource(configPath, source);
3432
+ if (sourceMsg) console.error(sourceMsg);
3433
+ loadConfig(configPath).fold((err) => {
3434
+ console.error(formatError(err));
3435
+ process.exit(2);
3436
+ }, (config) => {
3437
+ if (config.secret?.[name]) {
3438
+ console.error(`${RED}Error:${RESET} Secret "${name}" already exists in ${configPath}`);
3439
+ process.exit(1);
3440
+ }
3441
+ const block = buildSecretBlock(name, options);
3442
+ if (options.dryRun) {
3443
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
3444
+ console.log(block);
3445
+ return;
3446
+ }
3447
+ writeFileSync(configPath, appendSection(readFileSync(configPath, "utf-8"), block), "utf-8");
3448
+ console.log(`${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
3449
+ });
3450
+ });
3451
+ };
3452
+ const runSecretEdit = (name, options) => {
3453
+ if (options.expires && !DATE_RE.test(options.expires)) {
3454
+ console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
3455
+ process.exit(1);
3456
+ }
3457
+ withConfig(options.config, (configPath, raw) => {
3458
+ loadConfig(configPath).fold((err) => {
3459
+ console.error(formatError(err));
3460
+ process.exit(2);
3461
+ }, (config) => {
3462
+ if (!config.secret?.[name]) {
3463
+ console.error(`${RED}Error:${RESET} Secret "${name}" not found in ${configPath}`);
3464
+ process.exit(1);
3465
+ }
3466
+ const updates = buildFieldUpdates(options);
3467
+ if (Object.keys(updates).length === 0) {
3468
+ console.error(`${RED}Error:${RESET} No fields to update. Provide at least one --flag.`);
3469
+ process.exit(1);
3470
+ }
3471
+ updateSectionFields(raw, `[secret.${name}]`, updates).fold((err) => {
3472
+ console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3473
+ process.exit(2);
3474
+ }, (updated) => {
3475
+ if (options.dryRun) {
3476
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
3477
+ console.log(updated);
3478
+ return;
3479
+ }
3480
+ writeFileSync(configPath, updated, "utf-8");
3481
+ console.log(`${GREEN}✓${RESET} Updated ${BOLD}${name}${RESET} in ${CYAN}${configPath}${RESET}`);
3482
+ });
3483
+ });
3484
+ });
3485
+ };
3486
+ const runSecretRm = (name, options) => {
3487
+ withConfig(options.config, (configPath, raw) => {
3488
+ removeSection(raw, `[secret.${name}]`).fold((err) => {
3489
+ console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3490
+ process.exit(1);
3491
+ }, (updated) => {
3492
+ if (options.dryRun) {
3493
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
3494
+ console.log(updated);
3495
+ return;
3496
+ }
3497
+ writeFileSync(configPath, updated, "utf-8");
3498
+ console.log(`${GREEN}✓${RESET} Removed ${BOLD}${name}${RESET} from ${CYAN}${configPath}${RESET}`);
3499
+ });
3500
+ });
3501
+ };
3502
+ const runSecretRename = (oldName, newName, options) => {
3503
+ withConfig(options.config, (configPath, raw) => {
3504
+ renameSection(raw, `[secret.${oldName}]`, `[secret.${newName}]`).fold((err) => {
3505
+ console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
3506
+ process.exit(1);
3507
+ }, (updated) => {
3508
+ if (options.dryRun) {
3509
+ console.log(`${DIM}# Preview (--dry-run):${RESET}\n`);
3510
+ console.log(updated);
3511
+ return;
3512
+ }
3513
+ writeFileSync(configPath, updated, "utf-8");
3514
+ console.log(`${GREEN}✓${RESET} Renamed ${BOLD}${oldName}${RESET} → ${BOLD}${newName}${RESET} in ${CYAN}${configPath}${RESET}`);
3515
+ });
3516
+ });
3517
+ };
3518
+ const addSecretFlags = (cmd) => cmd.option("--service <service>", "Service this secret authenticates to").option("--purpose <purpose>", "Why this secret exists").option("--comment <comment>", "Free-form annotation").option("--expires <date>", "Expiration date (YYYY-MM-DD)").option("--capabilities <caps>", "Comma-separated capabilities (e.g. read,write)").option("--rotates <schedule>", "Rotation schedule (e.g. 90d, quarterly)").option("--rate-limit <limit>", "Rate limit info (e.g. 1000/min)").option("--model-hint <hint>", "Suggested model or tier").option("--source <source>", "Where the value originates (e.g. vault, ci)").option("--rotation-url <url>", "URL for secret rotation procedure").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)");
3519
+ const registerSecretCommands = (program) => {
3520
+ const secret = program.command("secret").description("Manage secret entries in envpkt.toml");
3521
+ addSecretFlags(secret.command("add").description("Add a new secret entry to envpkt.toml").argument("<name>", "Secret name (becomes the env var key)").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--dry-run", "Preview the TOML block without writing")).action((name, options) => {
3522
+ runSecretAdd(name, options);
3523
+ });
3524
+ addSecretFlags(secret.command("edit").description("Update metadata fields on an existing secret").argument("<name>", "Secret name to edit").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--no-required", "Mark this secret as not required").option("--dry-run", "Preview the changes without writing")).action((name, options) => {
3525
+ runSecretEdit(name, options);
3526
+ });
3527
+ secret.command("rm").description("Remove a secret entry from envpkt.toml").argument("<name>", "Secret name to remove").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((name, options) => {
3528
+ runSecretRm(name, options);
3529
+ });
3530
+ secret.command("rename").description("Rename a secret entry, preserving all metadata").argument("<old>", "Current secret name").argument("<new>", "New secret name").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((oldName, newName, options) => {
3531
+ runSecretRename(oldName, newName, options);
3532
+ });
3533
+ };
3534
+
3174
3535
  //#endregion
3175
3536
  //#region src/cli/commands/shell-hook.ts
3176
3537
  const ZSH_HOOK = `# envpkt shell hook — add to your .zshrc
@@ -3225,12 +3586,6 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
3225
3586
  const pkgPath = findPkgJson(dirname(fileURLToPath(import.meta.url)));
3226
3587
  return pkgPath ? JSON.parse(readFileSync(pkgPath, "utf-8")).version : "0.0.0";
3227
3588
  })());
3228
- program.command("add").description("Add a new secret entry to envpkt.toml").argument("<name>", "Secret name (becomes the env var key)").option("-c, --config <path>", "Path to envpkt.toml").option("--service <service>", "Service this secret authenticates to").option("--purpose <purpose>", "Why this secret exists").option("--comment <comment>", "Free-form annotation").option("--expires <date>", "Expiration date (YYYY-MM-DD)").option("--capabilities <caps>", "Comma-separated capabilities (e.g. read,write)").option("--rotates <schedule>", "Rotation schedule (e.g. 90d, quarterly)").option("--rate-limit <limit>", "Rate limit info (e.g. 1000/min)").option("--model-hint <hint>", "Suggested model or tier").option("--source <source>", "Where the value originates (e.g. vault, ci)").option("--required", "Mark this secret as required").option("--rotation-url <url>", "URL for secret rotation procedure").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, options) => {
3229
- runAdd(name, options);
3230
- });
3231
- program.command("add-env").description("Add a new environment default entry to envpkt.toml").argument("<name>", "Environment variable name").argument("<value>", "Default value").option("-c, --config <path>", "Path to envpkt.toml").option("--purpose <purpose>", "Why this env var exists").option("--comment <comment>", "Free-form annotation").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)").option("--dry-run", "Preview the TOML block without writing").action((name, value, options) => {
3232
- runAddEnv(name, value, options);
3233
- });
3234
3589
  program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--identity", "Include [identity] section").option("--name <name>", "Identity name (requires --identity)").option("--capabilities <caps>", "Comma-separated capabilities (requires --identity)").option("--expires <date>", "Credential expiration YYYY-MM-DD (requires --identity)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
3235
3590
  runInit(process.cwd(), options);
3236
3591
  });
@@ -3258,16 +3613,8 @@ program.command("seal").description("Encrypt secret values into envpkt.toml usin
3258
3613
  program.command("mcp").description("Start the envpkt MCP server (stdio transport)").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
3259
3614
  runMcp(options);
3260
3615
  });
3261
- const env = program.command("env").description("Discover and check credentials in your shell environment");
3262
- env.command("scan").description("Auto-discover credentials from process.env and scaffold TOML entries — first step in the developer workflow").option("-c, --config <path>", "Path to envpkt.toml (write target for --write)").option("--format <format>", "Output format: table | json", "table").option("--write", "Write discovered credentials to envpkt.toml").option("--dry-run", "Preview TOML that would be written (implies --write)").option("--include-unknown", "Include vars where service could not be inferred").action((options) => {
3263
- runEnvScan(options);
3264
- });
3265
- env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
3266
- runEnvCheck(options);
3267
- });
3268
- env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").action((options) => {
3269
- runEnvExport(options);
3270
- });
3616
+ registerSecretCommands(program);
3617
+ registerEnvCommands(program);
3271
3618
  program.command("shell-hook").description("Output shell function for ambient credential warnings on cd — combine with env export for full setup").argument("<shell>", "Shell type: zsh | bash").action((shell) => {
3272
3619
  runShellHook(shell);
3273
3620
  });
package/dist/index.d.ts CHANGED
@@ -307,6 +307,13 @@ type KeygenResult = {
307
307
  readonly identityPath: string;
308
308
  readonly configUpdated: boolean;
309
309
  };
310
+ type TomlEditError = {
311
+ readonly _tag: "SectionNotFound";
312
+ readonly section: string;
313
+ } | {
314
+ readonly _tag: "SectionAlreadyExists";
315
+ readonly section: string;
316
+ };
310
317
  //#endregion
311
318
  //#region src/core/config.d.ts
312
319
  /** Find envpkt.toml in the given directory */
@@ -458,6 +465,30 @@ declare const updateConfigRecipient: (configPath: string, recipient: string) =>
458
465
  /** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
459
466
  declare const resolveValues: (keys: ReadonlyArray<string>, profile?: string, agentKey?: string) => Promise<Record<string, string>>;
460
467
  //#endregion
468
+ //#region src/core/toml-edit.d.ts
469
+ /**
470
+ * Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
471
+ * Strips trailing blank lines left behind.
472
+ */
473
+ declare const removeSection: (raw: string, sectionHeader: string) => Either<TomlEditError, string>;
474
+ /**
475
+ * Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
476
+ * Errors if old doesn't exist or new already exists.
477
+ */
478
+ declare const renameSection: (raw: string, oldHeader: string, newHeader: string) => Either<TomlEditError, string>;
479
+ /**
480
+ * Update, add, or remove fields within an existing TOML section.
481
+ * - A string value replaces or adds the field
482
+ * - A null value removes the field
483
+ * Does NOT re-serialize — operates on raw text lines.
484
+ */
485
+ declare const updateSectionFields: (raw: string, sectionHeader: string, updates: Readonly<Record<string, string | null>>) => Either<TomlEditError, string>;
486
+ /**
487
+ * Append a new TOML section block to the end of the file.
488
+ * Ensures proper spacing (double newline before the block).
489
+ */
490
+ declare const appendSection: (raw: string, block: string) => string;
491
+ //#endregion
461
492
  //#region src/core/fleet.d.ts
462
493
  declare const scanFleet: (rootDir: string, options?: {
463
494
  maxDepth?: number;
@@ -515,4 +546,4 @@ type ToolDef = {
515
546
  declare const toolDefinitions: readonly ToolDef[];
516
547
  declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
517
548
  //#endregion
518
- export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
549
+ export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, updateSectionFields, validateConfig };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FormatRegistry, Type } from "@sinclair/typebox";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { TypeCompiler } from "@sinclair/typebox/compiler";
4
- import { Cond, Left, List, None, Option, Right, Some, Try } from "functype";
4
+ import { Cond, Either, Left, List, None, Option, Right, Some, Try } from "functype";
5
5
  import { Env, Fs, Path, Platform } from "functype-os";
6
6
  import { TomlDate, parse } from "smol-toml";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
@@ -1831,6 +1831,156 @@ const resolveValues = async (keys, profile, agentKey) => {
1831
1831
  return result;
1832
1832
  };
1833
1833
 
1834
+ //#endregion
1835
+ //#region src/core/toml-edit.ts
1836
+ const SECTION_RE = /^\[.+\]\s*$/;
1837
+ const MULTILINE_OPEN = "\"\"\"";
1838
+ /**
1839
+ * Find the line range [start, end) of a TOML section by its header string.
1840
+ * The range includes the header line through to (but not including) the next section header or EOF.
1841
+ * Handles multiline `"""..."""` values when scanning for section boundaries.
1842
+ */
1843
+ const findSectionRange = (lines, sectionHeader) => {
1844
+ let start = -1;
1845
+ for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
1846
+ start = i;
1847
+ break;
1848
+ }
1849
+ if (start === -1) return void 0;
1850
+ let end = lines.length;
1851
+ let inMultiline = false;
1852
+ for (let i = start + 1; i < lines.length; i++) {
1853
+ const line = lines[i];
1854
+ if (inMultiline) {
1855
+ if (line.includes(MULTILINE_OPEN)) inMultiline = false;
1856
+ continue;
1857
+ }
1858
+ if (line.includes(MULTILINE_OPEN)) {
1859
+ if ((line.slice(line.indexOf("=") + 1).trim().match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) inMultiline = true;
1860
+ continue;
1861
+ }
1862
+ if (SECTION_RE.test(line)) {
1863
+ end = i;
1864
+ break;
1865
+ }
1866
+ }
1867
+ return {
1868
+ start,
1869
+ end
1870
+ };
1871
+ };
1872
+ /** Check whether a section header exists in the raw TOML */
1873
+ const sectionExists = (lines, sectionHeader) => lines.some((l) => l.trim() === sectionHeader);
1874
+ /**
1875
+ * Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
1876
+ * Strips trailing blank lines left behind.
1877
+ */
1878
+ const removeSection = (raw, sectionHeader) => {
1879
+ const lines = raw.split("\n");
1880
+ const range = findSectionRange(lines, sectionHeader);
1881
+ if (!range) return Either.left({
1882
+ _tag: "SectionNotFound",
1883
+ section: sectionHeader
1884
+ });
1885
+ let removeEnd = range.end;
1886
+ while (removeEnd > range.start && removeEnd - 1 >= range.start && lines[removeEnd - 1].trim() === "") removeEnd--;
1887
+ const before = lines.slice(0, range.start);
1888
+ const after = lines.slice(range.end);
1889
+ while (before.length > 0 && before[before.length - 1].trim() === "") before.pop();
1890
+ const result = [...before, ...after].join("\n");
1891
+ return Either.right(result);
1892
+ };
1893
+ /**
1894
+ * Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
1895
+ * Errors if old doesn't exist or new already exists.
1896
+ */
1897
+ const renameSection = (raw, oldHeader, newHeader) => {
1898
+ const lines = raw.split("\n");
1899
+ if (!sectionExists(lines, oldHeader)) return Either.left({
1900
+ _tag: "SectionNotFound",
1901
+ section: oldHeader
1902
+ });
1903
+ if (sectionExists(lines, newHeader)) return Either.left({
1904
+ _tag: "SectionAlreadyExists",
1905
+ section: newHeader
1906
+ });
1907
+ const result = lines.map((line) => line.trim() === oldHeader ? newHeader : line).join("\n");
1908
+ return Either.right(result);
1909
+ };
1910
+ /**
1911
+ * Update, add, or remove fields within an existing TOML section.
1912
+ * - A string value replaces or adds the field
1913
+ * - A null value removes the field
1914
+ * Does NOT re-serialize — operates on raw text lines.
1915
+ */
1916
+ const updateSectionFields = (raw, sectionHeader, updates) => {
1917
+ const lines = raw.split("\n");
1918
+ const range = findSectionRange(lines, sectionHeader);
1919
+ if (!range) return Either.left({
1920
+ _tag: "SectionNotFound",
1921
+ section: sectionHeader
1922
+ });
1923
+ const before = lines.slice(0, range.start + 1);
1924
+ const after = lines.slice(range.end);
1925
+ const sectionBody = lines.slice(range.start + 1, range.end);
1926
+ const remaining = [];
1927
+ const updatedKeys = /* @__PURE__ */ new Set();
1928
+ let inMultiline = false;
1929
+ let multilineKey = "";
1930
+ for (let i = 0; i < sectionBody.length; i++) {
1931
+ const line = sectionBody[i];
1932
+ if (inMultiline) {
1933
+ if (line.includes(MULTILINE_OPEN)) {
1934
+ inMultiline = false;
1935
+ if (updates[multilineKey] === null) continue;
1936
+ if (multilineKey in updates) continue;
1937
+ } else {
1938
+ if (updates[multilineKey] === null) continue;
1939
+ if (multilineKey in updates) continue;
1940
+ }
1941
+ remaining.push(line);
1942
+ continue;
1943
+ }
1944
+ const eqIdx = line.indexOf("=");
1945
+ if (eqIdx > 0 && !line.trimStart().startsWith("#") && !line.trimStart().startsWith("[")) {
1946
+ const key = line.slice(0, eqIdx).trim();
1947
+ if (key in updates) {
1948
+ updatedKeys.add(key);
1949
+ const afterEquals = line.slice(eqIdx + 1).trim();
1950
+ if (afterEquals.includes(MULTILINE_OPEN)) {
1951
+ if ((afterEquals.match(/* @__PURE__ */ new RegExp("\"\"\"", "g")) ?? []).length === 1) {
1952
+ inMultiline = true;
1953
+ multilineKey = key;
1954
+ }
1955
+ }
1956
+ if (updates[key] === null) continue;
1957
+ remaining.push(`${key} = ${updates[key]}`);
1958
+ if (inMultiline) {
1959
+ for (let j = i + 1; j < sectionBody.length; j++) if (sectionBody[j].includes(MULTILINE_OPEN)) {
1960
+ i = j;
1961
+ inMultiline = false;
1962
+ break;
1963
+ }
1964
+ }
1965
+ continue;
1966
+ }
1967
+ }
1968
+ remaining.push(line);
1969
+ }
1970
+ for (const [key, value] of Object.entries(updates)) if (value !== null && !updatedKeys.has(key)) remaining.push(`${key} = ${value}`);
1971
+ const result = [
1972
+ ...before,
1973
+ ...remaining,
1974
+ ...after
1975
+ ].join("\n");
1976
+ return Either.right(result);
1977
+ };
1978
+ /**
1979
+ * Append a new TOML section block to the end of the file.
1980
+ * Ensures proper spacing (double newline before the block).
1981
+ */
1982
+ const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
1983
+
1834
1984
  //#endregion
1835
1985
  //#region src/core/fleet.ts
1836
1986
  const CONFIG_FILENAME = "envpkt.toml";
@@ -2217,4 +2367,4 @@ const startServer = async () => {
2217
2367
  };
2218
2368
 
2219
2369
  //#endregion
2220
- export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
2370
+ export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, updateSectionFields, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envpkt",
3
- "version": "0.6.10",
3
+ "version": "0.7.0",
4
4
  "description": "Credential lifecycle and fleet management for AI agents",
5
5
  "keywords": [
6
6
  "credentials",