@ornexus/neocortex 3.9.21 → 3.9.23

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.
Files changed (99) hide show
  1. package/install.ps1 +67 -11
  2. package/install.sh +89 -10
  3. package/package.json +6 -3
  4. package/packages/client/dist/agent/refresh-stubs.js +22 -1
  5. package/packages/client/dist/commands/activate.js +23 -2
  6. package/packages/client/dist/commands/invoke.js +46 -22
  7. package/packages/client/dist/config/secure-config.d.ts +69 -0
  8. package/packages/client/dist/config/secure-config.js +179 -0
  9. package/postinstall.js +102 -0
  10. package/targets-stubs/antigravity/gemini.md +1 -1
  11. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  12. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  13. package/targets-stubs/claude-code/neocortex.md +2 -2
  14. package/targets-stubs/codex/agents.md +1 -1
  15. package/targets-stubs/cursor/agent.md +2 -2
  16. package/targets-stubs/gemini-cli/agent.md +2 -2
  17. package/targets-stubs/vscode/agent.md +2 -2
  18. package/packages/client/dist/adapters/adapter-registry.d.ts.map +0 -1
  19. package/packages/client/dist/adapters/adapter-registry.js.map +0 -1
  20. package/packages/client/dist/adapters/antigravity-adapter.d.ts.map +0 -1
  21. package/packages/client/dist/adapters/antigravity-adapter.js.map +0 -1
  22. package/packages/client/dist/adapters/claude-code-adapter.d.ts.map +0 -1
  23. package/packages/client/dist/adapters/claude-code-adapter.js.map +0 -1
  24. package/packages/client/dist/adapters/codex-adapter.d.ts.map +0 -1
  25. package/packages/client/dist/adapters/codex-adapter.js.map +0 -1
  26. package/packages/client/dist/adapters/cursor-adapter.d.ts.map +0 -1
  27. package/packages/client/dist/adapters/cursor-adapter.js.map +0 -1
  28. package/packages/client/dist/adapters/gemini-adapter.d.ts.map +0 -1
  29. package/packages/client/dist/adapters/gemini-adapter.js.map +0 -1
  30. package/packages/client/dist/adapters/index.d.ts.map +0 -1
  31. package/packages/client/dist/adapters/index.js.map +0 -1
  32. package/packages/client/dist/adapters/platform-detector.d.ts.map +0 -1
  33. package/packages/client/dist/adapters/platform-detector.js.map +0 -1
  34. package/packages/client/dist/adapters/target-adapter.d.ts.map +0 -1
  35. package/packages/client/dist/adapters/target-adapter.js.map +0 -1
  36. package/packages/client/dist/adapters/vscode-adapter.d.ts.map +0 -1
  37. package/packages/client/dist/adapters/vscode-adapter.js.map +0 -1
  38. package/packages/client/dist/agent/refresh-stubs.d.ts.map +0 -1
  39. package/packages/client/dist/agent/refresh-stubs.js.map +0 -1
  40. package/packages/client/dist/agent/update-agent-yaml.d.ts.map +0 -1
  41. package/packages/client/dist/agent/update-agent-yaml.js.map +0 -1
  42. package/packages/client/dist/agent/update-description.d.ts.map +0 -1
  43. package/packages/client/dist/agent/update-description.js.map +0 -1
  44. package/packages/client/dist/cache/crypto-utils.d.ts.map +0 -1
  45. package/packages/client/dist/cache/crypto-utils.js.map +0 -1
  46. package/packages/client/dist/cache/encrypted-cache.d.ts.map +0 -1
  47. package/packages/client/dist/cache/encrypted-cache.js.map +0 -1
  48. package/packages/client/dist/cache/index.d.ts.map +0 -1
  49. package/packages/client/dist/cache/index.js.map +0 -1
  50. package/packages/client/dist/cli.d.ts.map +0 -1
  51. package/packages/client/dist/cli.js.map +0 -1
  52. package/packages/client/dist/commands/activate.d.ts.map +0 -1
  53. package/packages/client/dist/commands/activate.js.map +0 -1
  54. package/packages/client/dist/commands/cache-status.d.ts.map +0 -1
  55. package/packages/client/dist/commands/cache-status.js.map +0 -1
  56. package/packages/client/dist/commands/invoke.d.ts.map +0 -1
  57. package/packages/client/dist/commands/invoke.js.map +0 -1
  58. package/packages/client/dist/config/resolver-selection.d.ts.map +0 -1
  59. package/packages/client/dist/config/resolver-selection.js.map +0 -1
  60. package/packages/client/dist/context/context-collector.d.ts.map +0 -1
  61. package/packages/client/dist/context/context-collector.js.map +0 -1
  62. package/packages/client/dist/context/context-sanitizer.d.ts.map +0 -1
  63. package/packages/client/dist/context/context-sanitizer.js.map +0 -1
  64. package/packages/client/dist/index.d.ts.map +0 -1
  65. package/packages/client/dist/index.js.map +0 -1
  66. package/packages/client/dist/license/index.d.ts.map +0 -1
  67. package/packages/client/dist/license/index.js.map +0 -1
  68. package/packages/client/dist/license/license-client.d.ts.map +0 -1
  69. package/packages/client/dist/license/license-client.js.map +0 -1
  70. package/packages/client/dist/machine/fingerprint.d.ts.map +0 -1
  71. package/packages/client/dist/machine/fingerprint.js.map +0 -1
  72. package/packages/client/dist/machine/index.d.ts.map +0 -1
  73. package/packages/client/dist/machine/index.js.map +0 -1
  74. package/packages/client/dist/resilience/circuit-breaker.d.ts.map +0 -1
  75. package/packages/client/dist/resilience/circuit-breaker.js.map +0 -1
  76. package/packages/client/dist/resilience/degradation-manager.d.ts.map +0 -1
  77. package/packages/client/dist/resilience/degradation-manager.js.map +0 -1
  78. package/packages/client/dist/resilience/freshness-indicator.d.ts.map +0 -1
  79. package/packages/client/dist/resilience/freshness-indicator.js.map +0 -1
  80. package/packages/client/dist/resilience/index.d.ts.map +0 -1
  81. package/packages/client/dist/resilience/index.js.map +0 -1
  82. package/packages/client/dist/resilience/recovery-detector.d.ts.map +0 -1
  83. package/packages/client/dist/resilience/recovery-detector.js.map +0 -1
  84. package/packages/client/dist/resolvers/asset-resolver.d.ts.map +0 -1
  85. package/packages/client/dist/resolvers/asset-resolver.js.map +0 -1
  86. package/packages/client/dist/resolvers/local-resolver.d.ts.map +0 -1
  87. package/packages/client/dist/resolvers/local-resolver.js.map +0 -1
  88. package/packages/client/dist/resolvers/remote-resolver.d.ts.map +0 -1
  89. package/packages/client/dist/resolvers/remote-resolver.js.map +0 -1
  90. package/packages/client/dist/telemetry/index.d.ts.map +0 -1
  91. package/packages/client/dist/telemetry/index.js.map +0 -1
  92. package/packages/client/dist/telemetry/offline-queue.d.ts.map +0 -1
  93. package/packages/client/dist/telemetry/offline-queue.js.map +0 -1
  94. package/packages/client/dist/tier/index.d.ts.map +0 -1
  95. package/packages/client/dist/tier/index.js.map +0 -1
  96. package/packages/client/dist/tier/tier-aware-client.d.ts.map +0 -1
  97. package/packages/client/dist/tier/tier-aware-client.js.map +0 -1
  98. package/packages/client/dist/types/index.d.ts.map +0 -1
  99. package/packages/client/dist/types/index.js.map +0 -1
package/install.ps1 CHANGED
@@ -15,7 +15,7 @@ param(
15
15
  [string]$ServerUrl = "https://api.neocortex.ornexus.com"
16
16
  )
17
17
 
18
- $VERSION = "3.9.21"
18
+ $VERSION = "3.9.23"
19
19
 
20
20
  # =============================================================================
21
21
  # CONFIGURACOES
@@ -388,6 +388,20 @@ function Invoke-AutoCleanupLegacy {
388
388
  # --- Categoria 7: Antigravity ---
389
389
  # Configs legadas de Antigravity sao gerenciadas pelo adapter, sem path fixo global
390
390
 
391
+ # --- Categoria 8: Plaintext cache cleanup (Epic 62 - GAP 1) ---
392
+ $cacheDir = "$env:USERPROFILE\.neocortex\cache"
393
+ if (Test-Path $cacheDir -PathType Container) {
394
+ # Remove plaintext menu-cache.json
395
+ Remove-LegacyItem "$cacheDir\menu-cache.json" "cache plaintext (menu)"
396
+
397
+ # Remove any non-.enc files in cache dir (excluding directories)
398
+ Get-ChildItem -Path $cacheDir -File -ErrorAction SilentlyContinue | Where-Object {
399
+ $_.Extension -ne ".enc"
400
+ } | ForEach-Object {
401
+ Remove-LegacyItem $_.FullName "cache plaintext"
402
+ }
403
+ }
404
+
391
405
  # --- Resultado ---
392
406
  $removed = $script:autoRemoved
393
407
  if ($removed -gt 0) {
@@ -586,6 +600,25 @@ function Set-ThinClientConfig {
586
600
  try {
587
601
  $existingConfig = Get-Content $configFile -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue
588
602
  if ($existingConfig.mode -eq "active" -or $existingConfig.mode -eq "local" -or $existingConfig.mode -eq "remote") {
603
+ # --- Config schema migration (Epic 62 - GAP 4) ---
604
+ if (-not $existingConfig.configVersion) {
605
+ Write-Dbg "Migrando schema do config.json (adicionando configVersion)"
606
+ try {
607
+ # Add configVersion
608
+ $existingConfig | Add-Member -NotePropertyName "configVersion" -NotePropertyValue 1 -Force
609
+ # Remove known obsolete fields
610
+ $existingConfig.PSObject.Properties.Remove("version")
611
+ $existingConfig.PSObject.Properties.Remove("cache")
612
+ # Clean obsolete tier:3 from old base template
613
+ if ($existingConfig.tier -eq 3 -and $existingConfig.mode -eq "pending-activation") {
614
+ $existingConfig.PSObject.Properties.Remove("tier")
615
+ }
616
+ $existingConfig | ConvertTo-Json -Depth 5 | Out-File -FilePath $configFile -Encoding utf8
617
+ Write-Dbg "Config migrada para configVersion 1"
618
+ } catch {
619
+ Write-Dbg "Falha na migracao do config: $_"
620
+ }
621
+ }
589
622
  Write-Dbg "Config existente preservada (mode=$($existingConfig.mode))"
590
623
  return
591
624
  }
@@ -593,15 +626,9 @@ function Set-ThinClientConfig {
593
626
  }
594
627
 
595
628
  $config = @{
596
- version = $VERSION
629
+ configVersion = 1
597
630
  mode = "pending-activation"
598
631
  serverUrl = $NEOCORTEX_SERVER_URL
599
- tier = 3
600
- cache = @{
601
- enabled = $true
602
- directory = "$configDir\cache"
603
- encryption = "AES-256-GCM"
604
- }
605
632
  resilience = @{
606
633
  circuitBreaker = $true
607
634
  maxRetries = 3
@@ -656,13 +683,42 @@ function Install-Core {
656
683
  Set-ThinClientConfig
657
684
  }
658
685
 
659
- # Write version file
686
+ # --- Version-aware cache purge on upgrade (Epic 62 - GAP 2+3) ---
660
687
  $pkgJsonPath = Join-Path $script:SourceDir "package.json"
688
+ $pkgVersion = $null
661
689
  if (Test-Path $pkgJsonPath) {
662
690
  $pkgJson = Get-Content $pkgJsonPath -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue
663
- if ($pkgJson.version) {
664
- $pkgJson.version | Out-File -FilePath "$script:DestDir\.version" -Encoding utf8 -NoNewline
691
+ $pkgVersion = $pkgJson.version
692
+ }
693
+
694
+ if ($pkgVersion) {
695
+ $oldVersion = $null
696
+ # Read existing .version from either location
697
+ $versionPaths = @("$script:DestDir\.version", "$env:USERPROFILE\.neocortex\.version")
698
+ foreach ($vp in $versionPaths) {
699
+ if (Test-Path $vp -PathType Leaf) {
700
+ $oldVersion = (Get-Content $vp -Raw -ErrorAction SilentlyContinue).Trim()
701
+ if ($oldVersion) { break }
702
+ }
703
+ }
704
+
705
+ # If version changed, purge all cache files
706
+ if ($oldVersion -and $oldVersion -ne $pkgVersion) {
707
+ $cachePurgeDir = "$env:USERPROFILE\.neocortex\cache"
708
+ if (Test-Path $cachePurgeDir -PathType Container) {
709
+ $purged = 0
710
+ Get-ChildItem -Path $cachePurgeDir -File -ErrorAction SilentlyContinue | ForEach-Object {
711
+ Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
712
+ $purged++
713
+ }
714
+ if ($purged -gt 0) {
715
+ Write-Info "Cache purgado: versao alterada de $oldVersion para $pkgVersion ($purged arquivo(s))"
716
+ }
717
+ }
665
718
  }
719
+
720
+ # Write version file
721
+ $pkgVersion | Out-File -FilePath "$script:DestDir\.version" -Encoding utf8 -NoNewline
666
722
  }
667
723
 
668
724
  if ($LOCAL_MODE) {
package/install.sh CHANGED
@@ -4,7 +4,7 @@
4
4
  # Development Orchestrator
5
5
 
6
6
  # Versao do instalador
7
- VERSION="3.9.21"
7
+ VERSION="3.9.23"
8
8
 
9
9
  # Flags
10
10
  MIGRATION_DETECTED=false
@@ -415,6 +415,22 @@ auto_cleanup_legacy() {
415
415
  # ─── Categoria 7: Antigravity ─────────────────────────────────────────
416
416
  # Configs legadas de Antigravity sao gerenciadas pelo adapter, sem path fixo global
417
417
 
418
+ # ─── Categoria 8: Plaintext cache cleanup (Epic 62 - GAP 1) ─────────
419
+ local cache_dir="$HOME/.neocortex/cache"
420
+ if [ -d "$cache_dir" ]; then
421
+ # Remove plaintext menu-cache.json
422
+ _remove_legacy "$cache_dir/menu-cache.json" "cache plaintext (menu)"
423
+
424
+ # Remove any non-.enc files in cache dir (excluding directories)
425
+ for cache_file in "$cache_dir"/*; do
426
+ [ -f "$cache_file" ] || continue
427
+ case "$cache_file" in
428
+ *.enc) continue ;; # Keep encrypted cache files
429
+ *) _remove_legacy "$cache_file" "cache plaintext" ;;
430
+ esac
431
+ done
432
+ fi
433
+
418
434
  # ─── Resultado ────────────────────────────────────────────────────────
419
435
  if [ $removed -gt 0 ]; then
420
436
  ok "$removed artefato(s) legado(s) removido(s) automaticamente"
@@ -569,12 +585,45 @@ setup_thin_client_config() {
569
585
  mkdir -p "$config_dir" 2>/dev/null
570
586
  mkdir -p "$config_dir/cache" 2>/dev/null
571
587
 
588
+ # Story 61.4 - F4 remediation: restrictive permissions
589
+ chmod 700 "$config_dir" 2>/dev/null
590
+ chmod 700 "$config_dir/cache" 2>/dev/null
591
+
572
592
  if [ -f "$config_file" ]; then
573
593
  # Preservar config existente, atualizar apenas serverUrl se necessario
574
594
  local existing_mode
575
595
  existing_mode=$(grep '"mode"' "$config_file" 2>/dev/null | head -1 | sed 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
576
596
 
577
597
  if [ "$existing_mode" = "active" ] || [ "$existing_mode" = "local" ] || [ "$existing_mode" = "remote" ]; then
598
+ # ─── Config schema migration (Epic 62 - GAP 4) ────────────────
599
+ # Check if config needs schema migration
600
+ local has_config_version
601
+ has_config_version=$(grep '"configVersion"' "$config_file" 2>/dev/null)
602
+ if [ -z "$has_config_version" ]; then
603
+ debug "Migrando schema do config.json (adicionando configVersion)"
604
+ if command -v node >/dev/null 2>&1; then
605
+ node -e "
606
+ const fs = require('fs');
607
+ const path = '$config_file';
608
+ try {
609
+ const cfg = JSON.parse(fs.readFileSync(path, 'utf-8'));
610
+ // Add configVersion
611
+ cfg.configVersion = 1;
612
+ // Remove known obsolete fields from old base template
613
+ delete cfg.version;
614
+ delete cfg.cache;
615
+ // Clean obsolete tier:3 from old base template (preserve real tier values)
616
+ if (cfg.tier === 3 && cfg.mode === 'pending-activation') {
617
+ delete cfg.tier;
618
+ }
619
+ fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
620
+ }" 2>/dev/null
621
+ chmod 600 "$config_file" 2>/dev/null
622
+ debug "Config migrada para configVersion 1"
623
+ fi
624
+ fi
625
+ # Story 61.4 - ensure permissions are always enforced even on existing configs
626
+ chmod 600 "$config_file" 2>/dev/null
578
627
  debug "Config existente preservada (mode=$existing_mode)"
579
628
  return 0
580
629
  fi
@@ -583,15 +632,9 @@ setup_thin_client_config() {
583
632
  # Criar config base para thin client
584
633
  cat > "$config_file" << EOFCONFIG
585
634
  {
586
- "version": "${VERSION}",
635
+ "configVersion": 1,
587
636
  "mode": "pending-activation",
588
637
  "serverUrl": "${NEOCORTEX_SERVER_URL}",
589
- "tier": 3,
590
- "cache": {
591
- "enabled": true,
592
- "directory": "${config_dir}/cache",
593
- "encryption": "AES-256-GCM"
594
- },
595
638
  "resilience": {
596
639
  "circuitBreaker": true,
597
640
  "maxRetries": 3,
@@ -602,6 +645,9 @@ setup_thin_client_config() {
602
645
  }
603
646
  EOFCONFIG
604
647
 
648
+ # Story 61.4 - F4 remediation: config file readable only by owner
649
+ chmod 600 "$config_file" 2>/dev/null
650
+
605
651
  debug "Thin client config criada: $config_file"
606
652
  }
607
653
 
@@ -637,12 +683,45 @@ install_core() {
637
683
  # Thin-client ONLY: zero IP on client, all content from server
638
684
  setup_thin_client_config
639
685
 
640
- # Write version file
686
+ # ─── Version-aware cache purge on upgrade (Epic 62 - GAP 2+3) ───────
641
687
  local pkg_version=""
642
688
  if [ -f "$SOURCE_DIR/package.json" ]; then
643
689
  pkg_version=$(grep '"version"' "$SOURCE_DIR/package.json" 2>/dev/null | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
644
690
  fi
645
- [ -n "$pkg_version" ] && echo "$pkg_version" > "$DEST_DIR/.version"
691
+
692
+ if [ -n "$pkg_version" ]; then
693
+ local old_version=""
694
+ # Read existing .version from either location
695
+ if [ -f "$DEST_DIR/.version" ]; then
696
+ old_version=$(cat "$DEST_DIR/.version" 2>/dev/null | tr -d '[:space:]')
697
+ elif [ -f "$HOME/.neocortex/.version" ]; then
698
+ old_version=$(cat "$HOME/.neocortex/.version" 2>/dev/null | tr -d '[:space:]')
699
+ fi
700
+
701
+ # If version changed, purge all cache files
702
+ if [ -n "$old_version" ] && [ "$old_version" != "$pkg_version" ]; then
703
+ local cache_dir="$HOME/.neocortex/cache"
704
+ if [ -d "$cache_dir" ]; then
705
+ local purged=0
706
+ # Remove all .enc files
707
+ for enc_file in "$cache_dir"/*.enc; do
708
+ [ -f "$enc_file" ] || continue
709
+ rm -f "$enc_file" 2>/dev/null && purged=$((purged + 1))
710
+ done
711
+ # Remove menu-cache.json (redundancy with 62.1)
712
+ [ -f "$cache_dir/menu-cache.json" ] && rm -f "$cache_dir/menu-cache.json" 2>/dev/null && purged=$((purged + 1))
713
+ # Remove any other non-directory files
714
+ for cache_file in "$cache_dir"/*; do
715
+ [ -f "$cache_file" ] || continue
716
+ rm -f "$cache_file" 2>/dev/null && purged=$((purged + 1))
717
+ done
718
+ [ $purged -gt 0 ] && info "Cache purgado: versao alterada de $old_version para $pkg_version ($purged arquivo(s))"
719
+ fi
720
+ fi
721
+
722
+ # Write version file
723
+ echo "$pkg_version" > "$DEST_DIR/.version"
724
+ fi
646
725
 
647
726
  if [ $errors -eq 0 ]; then
648
727
  ok "Core instalado ${DIM}(thin client configurado) [modo remoto]${NC}"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ornexus/neocortex",
3
- "version": "3.9.21",
4
- "description": "Neocortex v3.9.21 - Orquestrador de Desenvolvimento de Epics & Stories para Claude Code",
3
+ "version": "3.9.23",
4
+ "description": "Neocortex v3.9.23 - Orquestrador de Desenvolvimento de Epics & Stories para Claude Code",
5
5
  "keywords": [
6
6
  "claude",
7
7
  "claude-code",
@@ -52,7 +52,10 @@
52
52
  "install.js",
53
53
  "install.sh",
54
54
  "install.ps1",
55
+ "postinstall.js",
55
56
  "packages/client/dist/",
57
+ "!packages/client/dist/**/*.js.map",
58
+ "!packages/client/dist/**/*.d.ts.map",
56
59
  "targets-stubs/"
57
60
  ],
58
61
  "scripts": {
@@ -60,7 +63,7 @@
60
63
  "build:clean": "rm -rf packages/shared/dist packages/client/dist && npm run build",
61
64
  "build:client": "npm run build -w packages/client",
62
65
  "build:shared": "npm run build -w packages/shared",
63
- "postinstall": "node -e \"console.log('\\nNeocortex instalado!\\n\\nUso:\\n @neocortex @docs/stories/1.1.story.md\\n @neocortex @epic-1\\n @neocortex *status\\n')\"",
66
+ "postinstall": "node postinstall.js",
64
67
  "sync": "node sync-version.js",
65
68
  "validate": "bash scripts/validate-pre-publish.sh",
66
69
  "test:e2e": "bash scripts/e2e-smoke-test.sh",
@@ -39,6 +39,26 @@ const STUB_TARGETS = [
39
39
  sourceDir: 'gemini-cli',
40
40
  files: ['agent.md'],
41
41
  },
42
+ {
43
+ destDir: join(homedir(), '.cursor', 'agents'),
44
+ sourceDir: 'cursor',
45
+ files: ['agent.md'],
46
+ },
47
+ {
48
+ destDir: join(homedir(), '.codex'),
49
+ sourceDir: 'codex',
50
+ files: ['agents.md'],
51
+ },
52
+ {
53
+ destDir: join(homedir(), '.vscode'),
54
+ sourceDir: 'vscode',
55
+ files: ['agent.md'],
56
+ },
57
+ {
58
+ destDir: join(homedir(), '.agent', 'skills', 'neocortex'),
59
+ sourceDir: 'antigravity',
60
+ files: ['skill/SKILL.md'],
61
+ },
42
62
  ];
43
63
  // -- Package Root Resolution --------------------------------------------------
44
64
  /**
@@ -127,7 +147,8 @@ export function refreshStubs(cliVersion) {
127
147
  const src = join(sourceDir, file);
128
148
  const dest = join(target.destDir, file);
129
149
  if (existsSync(src)) {
130
- mkdirSync(target.destDir, { recursive: true });
150
+ // Ensure parent directory exists (handles nested paths like skill/SKILL.md)
151
+ mkdirSync(dirname(dest), { recursive: true });
131
152
  copyFileSync(src, dest);
132
153
  copiedAny = true;
133
154
  }
@@ -29,6 +29,7 @@ import { getMachineFingerprint } from '../machine/fingerprint.js';
29
29
  import { updateAgentDescription } from '../agent/update-description.js';
30
30
  import { refreshStubs } from '../agent/refresh-stubs.js';
31
31
  import { updateAgentYaml } from '../agent/update-agent-yaml.js';
32
+ import { saveSecureConfig, setSecureDirPermissions } from '../config/secure-config.js';
32
33
  // ── Version Resolution ────────────────────────────────────────────────────
33
34
  function getInstalledVersion() {
34
35
  try {
@@ -94,11 +95,31 @@ function loadExistingConfig() {
94
95
  }
95
96
  /**
96
97
  * Save user config after successful activation.
97
- * License key is stored to enable LicenseClient re-creation in invoke.
98
+ * License key is encrypted using machine fingerprint (Story 61.2).
99
+ * File permissions set to 600 (Story 61.4).
98
100
  */
99
101
  function saveConfig(config) {
102
+ // Ensure directories exist with secure permissions
100
103
  mkdirSync(CONFIG_DIR, { recursive: true });
101
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
104
+ setSecureDirPermissions(CONFIG_DIR);
105
+ const cacheDir = join(CONFIG_DIR, 'cache');
106
+ mkdirSync(cacheDir, { recursive: true });
107
+ setSecureDirPermissions(cacheDir);
108
+ if (config.licenseKey) {
109
+ // Use secure config writer which encrypts the license key
110
+ saveSecureConfig({
111
+ serverUrl: config.serverUrl,
112
+ mode: config.mode,
113
+ machineId: config.machineId,
114
+ activatedAt: config.activatedAt,
115
+ tier: config.tier,
116
+ licenseKey: config.licenseKey,
117
+ });
118
+ }
119
+ else {
120
+ // Fallback: write without license key (should not happen in normal flow)
121
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
122
+ }
102
123
  }
103
124
  // ── Activate Command ──────────────────────────────────────────────────────
104
125
  /**
@@ -19,17 +19,17 @@
19
19
  *
20
20
  * Story 45.2 - AC1-AC6
21
21
  */
22
- import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
22
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
24
  import { homedir } from 'node:os';
25
25
  import { LicenseClient } from '../license/license-client.js';
26
26
  import { EncryptedCache } from '../cache/encrypted-cache.js';
27
27
  import { NoOpCache } from '../types/index.js';
28
28
  import { TierAwareClient } from '../tier/tier-aware-client.js';
29
+ import { loadSecureConfig } from '../config/secure-config.js';
29
30
  // ── Constants ─────────────────────────────────────────────────────────────
30
31
  const DEFAULT_SERVER_URL = 'https://api.neocortex.ornexus.com';
31
32
  const CONFIG_DIR = join(homedir(), '.neocortex');
32
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
33
33
  const CACHE_DIR = join(CONFIG_DIR, 'cache');
34
34
  const MENU_CACHE_FILE = join(CACHE_DIR, 'menu-cache.json');
35
35
  const MENU_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -115,18 +115,24 @@ export function collectStateSnapshot(projectRoot) {
115
115
  epics,
116
116
  };
117
117
  }
118
- // ── Menu Cache ────────────────────────────────────────────────────────────
119
- function getMenuCache() {
118
+ // ── Menu Cache (Encrypted - Story 61.1) ──────────────────────────────────
119
+ const MENU_CACHE_KEY = 'neocortex:menu:cache';
120
+ /**
121
+ * Read menu cache from EncryptedCache.
122
+ * Falls back gracefully: if decryption fails or data is stale, returns null.
123
+ * Also cleans up legacy plaintext menu-cache.json if it exists.
124
+ */
125
+ async function getMenuCache(encryptedCache) {
120
126
  try {
121
- if (!existsSync(MENU_CACHE_FILE))
127
+ const raw = await encryptedCache.get(MENU_CACHE_KEY);
128
+ if (!raw)
122
129
  return null;
123
- const raw = readFileSync(MENU_CACHE_FILE, 'utf-8');
124
130
  const cache = JSON.parse(raw);
125
131
  // Invalidate on version mismatch (stale cache from previous install)
126
132
  if (cache.version !== CLIENT_VERSION) {
127
133
  return null;
128
134
  }
129
- // Check TTL
135
+ // Check TTL (EncryptedCache also has TTL, but we double-check for version-based invalidation)
130
136
  if (Date.now() - cache.cachedAt > MENU_CACHE_TTL_MS) {
131
137
  return null; // Expired
132
138
  }
@@ -136,33 +142,47 @@ function getMenuCache() {
136
142
  return null;
137
143
  }
138
144
  }
139
- function setMenuCache(instructions, metadata) {
145
+ /**
146
+ * Write menu cache to EncryptedCache.
147
+ * Deletes legacy plaintext menu-cache.json on first encrypted write.
148
+ */
149
+ async function setMenuCache(encryptedCache, instructions, metadata) {
140
150
  try {
141
- mkdirSync(CACHE_DIR, { recursive: true });
142
151
  const cache = {
143
152
  instructions,
144
153
  metadata,
145
154
  cachedAt: Date.now(),
146
155
  version: CLIENT_VERSION,
147
156
  };
148
- writeFileSync(MENU_CACHE_FILE, JSON.stringify(cache), 'utf-8');
157
+ await encryptedCache.set(MENU_CACHE_KEY, JSON.stringify(cache), MENU_CACHE_TTL_MS);
158
+ // Delete legacy plaintext menu-cache.json if it exists (F1 remediation)
159
+ deleteLegacyMenuCache();
149
160
  }
150
161
  catch {
151
162
  // Cache write failure is non-critical
152
163
  }
153
164
  }
154
- // ── Config Loading ────────────────────────────────────────────────────────
155
- function loadConfig() {
165
+ /**
166
+ * Remove legacy plaintext menu-cache.json file.
167
+ * Called after successful encrypted write to prevent IP leakage.
168
+ */
169
+ function deleteLegacyMenuCache() {
156
170
  try {
157
- if (existsSync(CONFIG_FILE)) {
158
- const raw = readFileSync(CONFIG_FILE, 'utf-8');
159
- return JSON.parse(raw);
171
+ if (existsSync(MENU_CACHE_FILE)) {
172
+ unlinkSync(MENU_CACHE_FILE);
160
173
  }
161
174
  }
162
175
  catch {
163
- // Ignore
176
+ // Non-critical: best-effort cleanup
164
177
  }
165
- return null;
178
+ }
179
+ // ── Config Loading (Story 61.2 - Secure) ─────────────────────────────────
180
+ /**
181
+ * Load config with automatic decryption of license key.
182
+ * Handles migration from plaintext licenseKey to encryptedLicenseKey.
183
+ */
184
+ function loadConfig() {
185
+ return loadSecureConfig();
166
186
  }
167
187
  async function getAuthTokenAndClient(serverUrl, licenseKey) {
168
188
  try {
@@ -262,10 +282,14 @@ export async function invoke(options) {
262
282
  const serverUrl = (options.serverUrl ?? config?.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, '');
263
283
  // 2. Collect state snapshot
264
284
  const stateSnapshot = collectStateSnapshot(projectRoot);
285
+ // 2a. Create encrypted cache for menu (uses licenseKey as passphrase)
286
+ const menuCache = config?.licenseKey
287
+ ? new EncryptedCache({ cacheDir: CACHE_DIR, passphrase: config.licenseKey })
288
+ : null;
265
289
  // 3. Check menu cache for empty invocations (AC6)
266
290
  const trimmedArgs = options.args.trim();
267
- if (!trimmedArgs) {
268
- const cachedMenu = getMenuCache();
291
+ if (!trimmedArgs && menuCache) {
292
+ const cachedMenu = await getMenuCache(menuCache);
269
293
  if (cachedMenu) {
270
294
  return {
271
295
  success: true,
@@ -331,9 +355,9 @@ export async function invoke(options) {
331
355
  exitCode,
332
356
  };
333
357
  }
334
- // 6. Cache menu responses (AC6)
335
- if (!trimmedArgs && result.data.metadata?.mode === 'menu') {
336
- setMenuCache(result.data.instructions, result.data.metadata);
358
+ // 6. Cache menu responses (AC6) - encrypted (Story 61.1)
359
+ if (!trimmedArgs && result.data.metadata?.mode === 'menu' && menuCache) {
360
+ setMenuCache(menuCache, result.data.instructions, result.data.metadata).catch(() => { });
337
361
  }
338
362
  // 6a. Update cached quota from server response metadata (Epic 60)
339
363
  if (result.data.metadata) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /** Resolved config with decrypted license key */
14
+ export interface SecureConfig {
15
+ readonly serverUrl?: string;
16
+ readonly mode?: string;
17
+ readonly machineId?: string;
18
+ readonly activatedAt?: string;
19
+ readonly tier?: string;
20
+ readonly licenseKey?: string;
21
+ }
22
+ /**
23
+ * Encrypt a license key using the machine fingerprint as passphrase.
24
+ * Returns the encrypted envelope string.
25
+ */
26
+ export declare function encryptLicenseKey(licenseKey: string): string;
27
+ /**
28
+ * Decrypt a license key using the machine fingerprint as passphrase.
29
+ * Returns the plaintext key or null if decryption fails (hardware changed).
30
+ */
31
+ export declare function decryptLicenseKey(encryptedKey: string): string | null;
32
+ /**
33
+ * Set restrictive file permissions (600) on config files.
34
+ * Skipped on Windows where chmod is not meaningful.
35
+ * Story 61.4 - F4 remediation.
36
+ */
37
+ export declare function setSecureFilePermissions(filePath: string): void;
38
+ /**
39
+ * Set restrictive directory permissions (700) on config directories.
40
+ * Skipped on Windows where chmod is not meaningful.
41
+ * Story 61.4 - F4 remediation.
42
+ */
43
+ export declare function setSecureDirPermissions(dirPath: string): void;
44
+ /**
45
+ * Load config from ~/.neocortex/config.json with automatic migration.
46
+ *
47
+ * If the config contains a plaintext `licenseKey` (old format), it is:
48
+ * 1. Encrypted using machine fingerprint
49
+ * 2. Stored as `encryptedLicenseKey`
50
+ * 3. Old `licenseKey` field removed
51
+ * 4. Config rewritten to disk
52
+ *
53
+ * If decryption of `encryptedLicenseKey` fails (hardware change),
54
+ * returns config with licenseKey = undefined.
55
+ */
56
+ export declare function loadSecureConfig(): SecureConfig | null;
57
+ /**
58
+ * Save config after activation with encrypted license key.
59
+ * This is the primary write path called from activate.ts.
60
+ */
61
+ export declare function saveSecureConfig(config: {
62
+ serverUrl: string;
63
+ mode: string;
64
+ machineId: string;
65
+ activatedAt: string;
66
+ tier?: string;
67
+ licenseKey: string;
68
+ }): void;
69
+ //# sourceMappingURL=secure-config.d.ts.map