@gajae-code/utils 0.16.3 → 0.16.6

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.
@@ -82,6 +82,8 @@ export declare function getTrustedConfigRootDir(): string;
82
82
  export declare function setAgentDir(dir: string): void;
83
83
  /** Get the agent config directory (~/.gjc/agent). */
84
84
  export declare function getAgentDir(): string;
85
+ /** Resolver-owned profile classification, stable across HOME refreshes. */
86
+ export declare function getAgentProfileAuthority(): "default" | "custom";
85
87
  export declare function getConfigDirName(): string;
86
88
  /**
87
89
  * Join a file under the provenance-checked agent directory, never the XDG
@@ -112,6 +114,8 @@ export declare function getLogPath(date?: Date): string;
112
114
  * form — XDG semantics are preserved.
113
115
  */
114
116
  export declare function getPluginsDir(home?: string): string;
117
+ /** Get the default-profile plugin directory without ambient profile authority. */
118
+ export declare function getDefaultPluginsDir(): string;
115
119
  /** Where npm installs packages (~/.gjc/plugins/node_modules). */
116
120
  export declare function getPluginsNodeModules(): string;
117
121
  /** Plugin manifest (~/.gjc/plugins/package.json). */
@@ -216,4 +220,4 @@ export declare function getProjectPluginOverridesPath(cwd?: string): string;
216
220
  */
217
221
  export declare function getMCPConfigPath(scope: "user" | "project", cwd?: string, agentDir?: string): string;
218
222
  /** Get the SSH config file path. */
219
- export declare function getSSHConfigPath(scope: "user" | "project", cwd?: string): string;
223
+ export declare function getSSHConfigPath(scope: "user" | "project", cwd?: string, agentDir?: string): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/utils",
4
- "version": "0.16.3",
4
+ "version": "0.16.6",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@gajae-code/natives": "0.16.3",
34
+ "@gajae-code/natives": "0.16.6",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
@@ -25,13 +25,37 @@ export function redactCrashSecrets(text: string): string {
25
25
  redacted = redacted.replace(/\bgh[opsur]_[A-Za-z0-9]{16,}\b/g, "«redacted-github-token»");
26
26
  redacted = redacted.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "«redacted-github-token»");
27
27
  redacted = redacted.replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}\b/g, "«redacted-slack-token»");
28
+ // A PEM block carries the key material itself, so it is redacted whole rather
29
+ // than line by line. It runs before the narrower rules because they would
30
+ // otherwise chew on the base64 body and leave a truncated key behind.
31
+ redacted = redacted.replace(
32
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
33
+ "«redacted-private-key»",
34
+ );
35
+ // Google API keys are a fixed 39-character shape that carries no label of its
36
+ // own, so the labeled-value rule below never sees one.
37
+ redacted = redacted.replace(
38
+ /(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9_-])/g,
39
+ "«redacted-google-api-key»",
40
+ );
41
+ // Basic-auth credentials embedded in a URL. Scheme and host stay readable
42
+ // because they are the diagnostic value; only the userinfo is dropped. The
43
+ // scheme repetition is bounded: an unbounded `[a-z0-9+.-]*` in front of the
44
+ // literal `://` re-tries every prefix of a long alphabetic run, which is
45
+ // quadratic in input length and costs ~10s on a 200 KB crash body.
46
+ redacted = redacted.replace(
47
+ /(?<![A-Za-z0-9+.-])([a-z][a-z0-9+.-]{0,15}:\/\/)[^/\s:@]{1,256}:[^/\s@]{1,256}@/gi,
48
+ "$1«redacted-url-credential»@",
49
+ );
28
50
  // AKIA is the long-term access key id; ASIA is the temporary/STS one, which is
29
- // the shape that actually shows up in a crashed request. The id alone is not
51
+ // the shape that actually shows up in a crashed request. ABIA (bearer) and
52
+ // ACCA (context) complete the set the session-import scrubber already lists.
53
+ // The id alone is not
30
54
  // the credential: an STS payload carries `SecretAccessKey` and `SessionToken`
31
55
  // alongside it, so the labeled-value rule below must name both. `secret_key`
32
56
  // does not match `SecretAccessKey` (the canonical field has `Access` in the
33
57
  // middle), and `access_token` does not match `SessionToken`.
34
- redacted = redacted.replace(/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "«redacted-aws-key»");
58
+ redacted = redacted.replace(/\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g, "«redacted-aws-key»");
35
59
  redacted = redacted.replace(
36
60
  /(?<![A-Za-z0-9_])(["']?(?:api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|client[_-]?secret|secret[_-]?key|secret[_-]?access[_-]?key|password|passwd|authorization)["']?\s*[=:]\s*["']?)[^\s"',;}\]]{8,}/gi,
37
61
  "$1«redacted»",
@@ -47,5 +71,8 @@ export const CRASH_REDACTION_MARKERS: readonly string[] = [
47
71
  "«redacted-github-token»",
48
72
  "«redacted-slack-token»",
49
73
  "«redacted-aws-key»",
74
+ "«redacted-private-key»",
75
+ "«redacted-google-api-key»",
76
+ "«redacted-url-credential»",
50
77
  "«redacted»",
51
78
  ];
package/src/dirs.ts CHANGED
@@ -438,9 +438,11 @@ class DirResolver {
438
438
  // Per-category base dirs. Without XDG, all three equal configRoot / agentDir.
439
439
  // With XDG on Linux, they point to $XDG_*_HOME/gjc/.
440
440
  #rootDirs: Record<XdgCategory, string>;
441
+ #defaultRootDirs: Record<XdgCategory, string>;
441
442
  #agentDirs: Record<XdgCategory, string>;
442
443
 
443
444
  readonly #rootCache = new Map<string, string>();
445
+ readonly #defaultRootCache = new Map<string, string>();
444
446
  readonly #agentCache = new Map<string, string>();
445
447
 
446
448
  constructor(agentDirOverride?: string, snapshot = projectEnvSnapshot()) {
@@ -463,7 +465,7 @@ class DirResolver {
463
465
  // the parent reading `$XDG_STATE_HOME/gjc/python-gateway` while the child
464
466
  // read `<agentDir>/python-gateway`. Splitting a live store in half is worse
465
467
  // than the narrower complaint it was meant to answer.
466
- const isDefault = this.agentDir === defaultAgent;
468
+ const isDefault = normalizePathForComparison(this.agentDir) === normalizePathForComparison(defaultAgent);
467
469
  // That decision is then *sticky*. Recomputing it later from path shape is
468
470
  // what let a pinned agent directory silently change storage lane when a home
469
471
  // refresh made it coincide with the new default: `getAgentDir()` looked
@@ -471,10 +473,15 @@ class DirResolver {
471
473
  this.#xdgEligible = isDefault;
472
474
 
473
475
  this.#rootDirs = { data: this.configRoot, state: this.configRoot, cache: this.configRoot };
476
+ this.#defaultRootDirs = { data: this.configRoot, state: this.configRoot, cache: this.configRoot };
474
477
  this.#agentDirs = { data: this.agentDir, state: this.agentDir, cache: this.agentDir };
475
478
  this.refreshCategoryDirs(snapshot, isDefault);
476
479
  }
477
480
 
481
+ get profileAuthority(): "default" | "custom" {
482
+ return this.#xdgEligible ? "default" : "custom";
483
+ }
484
+
478
485
  /**
479
486
  * `isDefault` decides whether the agent directory may follow `$XDG_*_HOME`.
480
487
  *
@@ -491,7 +498,7 @@ class DirResolver {
491
498
  let xdgData: string | undefined;
492
499
  let xdgState: string | undefined;
493
500
  let xdgCache: string | undefined;
494
- if ((process.platform === "linux" || process.platform === "darwin") && isDefault) {
501
+ if (process.platform === "linux" || process.platform === "darwin") {
495
502
  const resolveIf = (envVar: string) => {
496
503
  const value = trustedValue(envVar, snapshot);
497
504
  if (!value) return undefined;
@@ -506,15 +513,20 @@ class DirResolver {
506
513
  xdgState = resolveIf("XDG_STATE_HOME");
507
514
  xdgCache = resolveIf("XDG_CACHE_HOME");
508
515
  }
509
- this.#rootDirs = {
516
+ this.#defaultRootDirs = {
510
517
  data: xdgData ?? this.configRoot,
511
518
  state: xdgState ?? this.configRoot,
512
519
  cache: xdgCache ?? this.configRoot,
513
520
  };
521
+ this.#rootDirs = {
522
+ data: isDefault ? (xdgData ?? this.configRoot) : this.configRoot,
523
+ state: isDefault ? (xdgState ?? this.configRoot) : this.configRoot,
524
+ cache: isDefault ? (xdgCache ?? this.configRoot) : this.configRoot,
525
+ };
514
526
  this.#agentDirs = {
515
- data: xdgData ?? this.agentDir,
516
- state: xdgState ?? this.agentDir,
517
- cache: xdgCache ?? this.agentDir,
527
+ data: isDefault ? (xdgData ?? this.agentDir) : this.agentDir,
528
+ state: isDefault ? (xdgState ?? this.agentDir) : this.agentDir,
529
+ cache: isDefault ? (xdgCache ?? this.agentDir) : this.agentDir,
518
530
  };
519
531
  }
520
532
 
@@ -543,6 +555,7 @@ class DirResolver {
543
555
  // its path coincide with (or diverge from) the new default.
544
556
  this.refreshCategoryDirs(this.#projectEnv, this.#xdgEligible);
545
557
  this.#rootCache.clear();
558
+ this.#defaultRootCache.clear();
546
559
  this.#agentCache.clear();
547
560
  }
548
561
 
@@ -571,7 +584,7 @@ class DirResolver {
571
584
  agentSubdir(userAgentDir: string | undefined, subdir: string, xdg?: XdgCategory): string {
572
585
  this.refreshConfigDirOverride();
573
586
  if (!this.#homeAvailable) throw new Error("User state is unavailable: no trustworthy home directory");
574
- if (!userAgentDir || userAgentDir === this.agentDir) {
587
+ if (!userAgentDir || normalizePathForComparison(userAgentDir) === normalizePathForComparison(this.agentDir)) {
575
588
  const cached = this.#agentCache.get(subdir);
576
589
  if (cached) return cached;
577
590
  const base = xdg ? this.#agentDirs[xdg] : this.agentDir;
@@ -582,6 +595,19 @@ class DirResolver {
582
595
  return path.join(userAgentDir, subdir);
583
596
  }
584
597
 
598
+ /** Config-root subdirectory for the resolver-owned default profile. */
599
+ rootSubdirForDefaultProfile(subdir: string, xdg?: XdgCategory): string {
600
+ this.refreshConfigDirOverride();
601
+ if (!this.#homeAvailable) throw new Error("User state is unavailable: no trustworthy home directory");
602
+ const cacheKey = `${xdg ?? "config"}:${subdir}`;
603
+ const cached = this.#defaultRootCache.get(cacheKey);
604
+ if (cached) return cached;
605
+ const base = xdg ? this.#defaultRootDirs[xdg] : this.configRoot;
606
+ const result = path.join(base, subdir);
607
+ this.#defaultRootCache.set(cacheKey, result);
608
+ return result;
609
+ }
610
+
585
611
  get configDirName(): string {
586
612
  return this.#configDirName;
587
613
  }
@@ -654,6 +680,11 @@ export function getAgentDir(): string {
654
680
  return dirs.agentDir;
655
681
  }
656
682
 
683
+ /** Resolver-owned profile classification, stable across HOME refreshes. */
684
+ export function getAgentProfileAuthority(): "default" | "custom" {
685
+ return dirs.profileAuthority;
686
+ }
687
+
657
688
  export function getConfigDirName(): string {
658
689
  dirs.refreshConfigDirOverride();
659
690
  return dirs.configDirName;
@@ -723,6 +754,11 @@ export function getPluginsDir(home?: string): string {
723
754
  return dirs.rootSubdir("plugins", "data");
724
755
  }
725
756
 
757
+ /** Get the default-profile plugin directory without ambient profile authority. */
758
+ export function getDefaultPluginsDir(): string {
759
+ return dirs.rootSubdirForDefaultProfile("plugins", "data");
760
+ }
761
+
726
762
  /** Where npm installs packages (~/.gjc/plugins/node_modules). */
727
763
  export function getPluginsNodeModules(): string {
728
764
  return path.join(getPluginsDir(), "node_modules");
@@ -972,9 +1008,9 @@ export function getMCPConfigPath(scope: "user" | "project", cwd: string = getPro
972
1008
  }
973
1009
 
974
1010
  /** Get the SSH config file path. */
975
- export function getSSHConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
1011
+ export function getSSHConfigPath(scope: "user" | "project", cwd: string = getProjectDir(), agentDir?: string): string {
976
1012
  if (scope === "user") {
977
- return path.join(getAgentDir(), "ssh.json");
1013
+ return path.join(agentDir ?? getAgentDir(), "ssh.json");
978
1014
  }
979
1015
  return path.join(getProjectAgentDir(cwd), "ssh.json");
980
1016
  }