@futdevpro/fdp-agent-memory 1.1.30 → 1.1.113

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 (81) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/_cli/_commands/find-duplicates.command.js +41 -16
  3. package/build/src/_cli/_commands/scan-projects.command.js +113 -113
  4. package/build/src/_cli/_commands/scan.command.js +2 -0
  5. package/build/src/_cli/_commands/serve.command.js +110 -10
  6. package/build/src/_cli/_services/fam-client.service.js +25 -1
  7. package/build/src/_cli/register-commands.js +28 -0
  8. package/build/src/_collections/config-catalog.const.js +160 -6
  9. package/build/src/_collections/error-codes.const.js +2 -0
  10. package/build/src/_collections/fam-console.util.js +38 -260
  11. package/build/src/_collections/fam-db-models.const.js +2 -0
  12. package/build/src/_collections/fam-entry-bootstrap.util.js +20 -6
  13. package/build/src/_collections/fam-error-context.util.js +1 -0
  14. package/build/src/_collections/fam-mcp-bridge.util.js +15 -1
  15. package/build/src/_collections/fam-operation-queue.service.js +85 -0
  16. package/build/src/_collections/fam-project-discovery.util.js +148 -0
  17. package/build/src/_collections/fam-request-origin.util.js +41 -0
  18. package/build/src/_collections/fam-retry.util.js +48 -0
  19. package/build/src/_collections/fam-scan-progress-sink.service.js +26 -0
  20. package/build/src/_enums/fam-rule-scope.type-enum.js +25 -0
  21. package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
  22. package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
  23. package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
  24. package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
  25. package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
  26. package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
  27. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
  28. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
  29. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +31 -5
  30. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
  31. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +9 -0
  32. package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
  33. package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
  34. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
  35. package/build/src/_modules/embedding/index.js +5 -1
  36. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
  37. package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
  38. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
  39. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
  40. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
  41. package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
  42. package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
  43. package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
  44. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
  45. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
  46. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +173 -37
  47. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
  48. package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
  49. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
  50. package/build/src/_modules/ingest/index.js +6 -1
  51. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +151 -8
  52. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +70 -0
  53. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
  54. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
  55. package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
  56. package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
  57. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
  58. package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
  59. package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
  60. package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
  61. package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
  62. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
  63. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
  64. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +216 -5
  65. package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
  66. package/build/src/_modules/retrieval/index.js +12 -1
  67. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +76 -0
  68. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +233 -0
  69. package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
  70. package/build/src/_routes/server/api/api.controller.js +270 -0
  71. package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
  72. package/build/src/app.server.js +13 -10
  73. package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
  74. package/client-dist/chunk-NMUCMQY6.js +1 -0
  75. package/client-dist/favicon.ico +0 -0
  76. package/client-dist/index.html +3 -3
  77. package/client-dist/main-YK4YIVAQ.js +2 -0
  78. package/package.json +1 -1
  79. package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
  80. package/client-dist/chunk-I77GXVAQ.js +0 -1
  81. package/client-dist/main-PJPEDVJT.js +0 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.30",
3
+ "version": "1.1.113",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -10,6 +10,9 @@ const fam_cli_const_1 = require("../_collections/fam-cli.const");
10
10
  * 2026-06-21). Egy tár vektor-pool-jában megkeresi a NAGYON HASONLÓ (koszinusz ≥ `--threshold`) entry-ket és
11
11
  * cluster-ekbe vonja — **NEM töröl**, csak megmutatja az ismétlődéseket (a user dönt). A `GET /api/duplicates/:table`
12
12
  * REST-tükre. `--threshold` (0..1, default 0.95), `--neighbors`, `--max-entries` (O(n²)-cap), `--max-clusters`.
13
+ * CROSS-PROJECT mód (user-FR 2026-07-15): a `--path-regex`/`--exclude-path-regex`/`--exclude-root-regex`/
14
+ * `--projects`/`--exclude-projects` Mongo-prefilterrel szűkíti a jelölt-halmazt (célzott scan a nagy táron),
15
+ * a `--min-projects` csak a legalább ennyi KÜLÖNBÖZŐ projektet átfedő clustereket adja.
13
16
  */
14
17
  class FAM_FindDuplicates_Util {
15
18
  /** A `find-duplicates` parancs-deszkriptor. */
@@ -22,6 +25,12 @@ class FAM_FindDuplicates_Util {
22
25
  .option('--neighbors <n>', 'szomszéd-szám/entry (default 10)')
23
26
  .option('--max-entries <n>', 'entry-cap az O(n²) ellen (default 3000; a capped jelzi, ha több)')
24
27
  .option('--max-clusters <n>', 'a visszaadott cluster-ek max száma (default 100)')
28
+ .option('--path-regex <regex>', 'a sourceFilePath include-regexe (case-insensitive; célzott scan)')
29
+ .option('--exclude-path-regex <regex>', 'a sourceFilePath exclude-regexe (pl. spec/vendor zaj)')
30
+ .option('--exclude-root-regex <regex>', 'a source.root exclude-regexe (pl. STALE-projects)')
31
+ .option('--projects <list>', 'csak ezek a projektek (vessző-szeparált canonicalName lista)')
32
+ .option('--exclude-projects <list>', 'kizárt projektek (vessző-szeparált; fork-zaj elnyomása)')
33
+ .option('--min-projects <n>', 'csak a legalább ennyi KÜLÖNBÖZŐ projektet átfedő clusterek')
25
34
  .action(async () => {
26
35
  fam_output_util_1.FAM_Output_Util.exit(await FAM_FindDuplicates_Util.run(command));
27
36
  });
@@ -37,6 +46,12 @@ class FAM_FindDuplicates_Util {
37
46
  neighbors: local.neighbors,
38
47
  maxEntries: local.maxEntries,
39
48
  maxClusters: local.maxClusters,
49
+ pathRegex: local.pathRegex,
50
+ excludePathRegex: local.excludePathRegex,
51
+ excludeRootRegex: local.excludeRootRegex,
52
+ projects: local.projects,
53
+ excludeProjects: local.excludeProjects,
54
+ minProjects: local.minProjects,
40
55
  });
41
56
  const path = `/api/duplicates/${encodeURIComponent(local.table)}${query}`;
42
57
  const result = await client.get(path);
@@ -57,19 +72,22 @@ class FAM_FindDuplicates_Util {
57
72
  }
58
73
  /** A query-string összeállítása a megadott (definiált) opciókból. */
59
74
  static buildQuery(options) {
60
- const parts = [];
61
- if (options.threshold !== undefined) {
62
- parts.push(`threshold=${encodeURIComponent(options.threshold)}`);
63
- }
64
- if (options.neighbors !== undefined) {
65
- parts.push(`neighbors=${encodeURIComponent(options.neighbors)}`);
66
- }
67
- if (options.maxEntries !== undefined) {
68
- parts.push(`maxEntries=${encodeURIComponent(options.maxEntries)}`);
69
- }
70
- if (options.maxClusters !== undefined) {
71
- parts.push(`maxClusters=${encodeURIComponent(options.maxClusters)}`);
72
- }
75
+ // A CLI kebab-opciók → REST camelCase query-kulcsok leképezése (a definiáltakból).
76
+ const mapping = [
77
+ ['threshold', options.threshold],
78
+ ['neighbors', options.neighbors],
79
+ ['maxEntries', options.maxEntries],
80
+ ['maxClusters', options.maxClusters],
81
+ ['pathRegex', options.pathRegex],
82
+ ['excludePathRegex', options.excludePathRegex],
83
+ ['excludeRootRegex', options.excludeRootRegex],
84
+ ['projects', options.projects],
85
+ ['excludeProjects', options.excludeProjects],
86
+ ['minProjects', options.minProjects],
87
+ ];
88
+ const parts = mapping
89
+ .filter(([, value]) => value !== undefined)
90
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`);
73
91
  return parts.length ? `?${parts.join('&')}` : '';
74
92
  }
75
93
  /** Ember-olvasható összegzés: scan-fejléc (+ capped-figyelmeztetés) + a top cluster-ek tagokkal. */
@@ -77,7 +95,8 @@ class FAM_FindDuplicates_Util {
77
95
  const lines = [];
78
96
  lines.push(`Near-duplikátum scan — '${report.table}' (küszöb ${report.threshold}):`);
79
97
  lines.push(` vizsgált ${report.scanned} / ${report.totalInPool} entry`
80
- + `${report.capped ? ' ⚠ CAPPED (a tár nagyobb — emeld a --max-entries-t a teljes scan-hez)' : ''}`);
98
+ + `${report.filtered ? ' (szűrt jelölt-halmaz)' : ''}`
99
+ + `${report.capped ? ' ⚠ CAPPED (a jelölt-halmaz nagyobb — emeld a --max-entries-t vagy szűkíts tovább)' : ''}`);
81
100
  lines.push(` ${report.clusterCount} duplikátum-cluster, összesen ${report.duplicateEntryCount} érintett entry`);
82
101
  if (!report.clusterCount) {
83
102
  lines.push(' ✓ nincs a küszöb feletti near-duplikátum.');
@@ -85,10 +104,16 @@ class FAM_FindDuplicates_Util {
85
104
  }
86
105
  for (const [index, cluster] of report.clusters.entries()) {
87
106
  lines.push('');
88
- lines.push(` #${index + 1} — ${cluster.size} elem (score ${cluster.minScore.toFixed(3)}…${cluster.maxScore.toFixed(3)}):`);
107
+ const projectInfo = cluster.projects?.length
108
+ ? ` — ${cluster.projectCount} projekt: ${cluster.projects.join(', ')}`
109
+ : '';
110
+ lines.push(` #${index + 1} — ${cluster.size} elem (score ${cluster.minScore.toFixed(3)}…${cluster.maxScore.toFixed(3)})${projectInfo}:`);
89
111
  for (const member of cluster.members) {
112
+ const location = member.sourceFilePath
113
+ ? ` ${member.project ? `[${member.project}] ` : ''}${member.sourceFilePath}`
114
+ : '';
90
115
  const snippet = member.snippet ? `: ${member.snippet}` : '';
91
- lines.push(` • [${member.id}]${member.kind ? ` (${member.kind})` : ''}${snippet}`);
116
+ lines.push(` • [${member.id}]${member.kind ? ` (${member.kind})` : ''}${location}${snippet}`);
92
117
  }
93
118
  }
94
119
  return lines.join('\n');
@@ -1,159 +1,159 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_ScanProjects_Util = void 0;
4
- const fs_1 = require("fs");
4
+ const path_1 = require("path");
5
5
  const commander_1 = require("commander");
6
6
  const fam_cooldown_util_1 = require("../../_collections/fam-cooldown.util");
7
7
  const fam_output_util_1 = require("../_collections/fam-output.util");
8
8
  const fam_client_service_1 = require("../_services/fam-client.service");
9
- const fam_cli_const_1 = require("../_collections/fam-cli.const");
10
- const fam_project_discovery_util_1 = require("../_collections/fam-project-discovery.util");
11
9
  /**
12
- * `fam scan-projects` — több projekt **SOROS** scan-orchestrátora (robusztus, NINCS konkurrens embed-thrash).
13
- * A projekt-listát `--config` (explicit JSON) VAGY `--root` (auto-felderítés: `package.json`/`.git` markerek,
14
- * a scope-név a package.json `name`-jéből) adja. Projektenként: előbb `dryRun` (idő-becslés), majd a VALÓS scan,
15
- * és VÁRJA a teljes választ, mielőtt a következőre lép. Ez a megfelelő minta a Packages / teljes CURSOR scanhez.
10
+ * `fam scan-projects` (FEAT-008, THIN-CLI) — több projekt scan-je **SZERVER-OLDALI vezényléssel**. A CLI itt már
11
+ * CSAK egy vékony eszköz: elindítja a job-ot (`POST /api/scan/start`) és poll-ozza az állapotát
12
+ * (`GET /api/scan/status/:jobId`), kiírva a haladást. A tényleges terhet (felderítés + soros feldolgozás + queue +
13
+ * cooldown + progress) a SZERVER viszi nála van minden (adat, hidratáció). A `--config` fájlt is a SZERVER olvassa.
16
14
  */
17
15
  class FAM_ScanProjects_Util {
18
16
  /** A `scan-projects` parancs-deszkriptor. */
19
17
  static command() {
20
18
  const command = new commander_1.Command('scan-projects');
21
19
  command
22
- .description('Több projekt SOROS scan-je (auto-felderítés --root-tal vagy explicit --config-gal; nincs '
23
- + 'konkurrens thrash). ⚠️ MINDEN projekt-gyökérbe előbb `.fdpfamignore` + `--dry-run` (lásd lent).')
24
- .option('--root <dir>', 'projekt-gyökerek auto-felderítése e mappa alatt (package.json/.git markerek)')
25
- .option('--config <file.json>', 'explicit projekt-lista JSON — elemenként `path` + `project` (+ opcionális `org`, `table`)')
20
+ .description('Több projekt SZERVER-OLDALI scan-je (a szerver vezényel: felderítés + soros feldolgozás + '
21
+ + 'queue + cooldown). A CLI csak elindítja a job-ot és poll-ozza az állapotát.')
22
+ .option('--root <dir>', 'projekt-gyökerek auto-felderítése e mappa alatt (a SZERVEREN, package.json/.git markerek)')
23
+ .option('--config <file.json>', 'explicit projekt-lista JSON — a SZERVER olvassa (elemenként `path` + `project`)')
26
24
  .option('--depth <n>', 'auto-felderítés max mélysége (default 3)', '3')
27
25
  .option('--org <name>', 'default organization az auto-felderítéshez', 'FutDevPro')
28
26
  .option('--table <documents|codebase>', 'default cél-tár', 'codebase')
29
- .option('--dry-run', 'csak becslés MINDEN projektre, NEM ír', false)
30
- .option('--cooldown <ms>', 'cooldown a projektek KÖZÉ (ms) gép-tehermentesítő szünet nagy multi-scanhez; '
31
- + 'felülírja a `scan.projectCooldownMs` configot (default: a config, ami 0 = ki)')
32
- .option('--stop-on-error', 'az első hibánál leáll (default: folytatja a többivel)', false)
33
- .addHelpText('before', '\n⚠️ TISZTA MULTI-SCAN ELŐBB minden projektbe IGNORE-FÁJL:\n'
34
- + ' Mivel ez TÖBB projektet szkennel egyszerre, a junk gyorsan elszaporodik. MINDEN érintett\n'
35
- + ' projekt-gyökérbe tegyél `.fdpfamignore`-t (a default fedi a node_modules/Library/__pycache__/\n'
36
- + ' site-packages/dist/build/Logs-féle junkot; a projekt-specifikusat ott add hozzá), és FUTTASD\n'
37
- + ' ELŐSZÖR `--dry-run`-nal (per-projekt fájl/chunk-becslés). Utólag kitakarítani (törlés +\n'
38
- + ' re-embed minden projekten) sokkal drágább, mint eleve tisztán szkennelni.\n')
27
+ .option('--cooldown <ms>', 'projekt-cooldown override (ms) felülírja a szerver `scan.projectCooldownMs` configot')
28
+ .option('--cooldown-min-chunks <n>', 'a feltételes cooldown küszöbe ( ennyi embed-chunk vár)')
29
+ .option('--poll <ms>', 'a status-poll periódusa (ms)', '2000')
30
+ .option('--memory-path <dir>', '„SCAN-ALL": EXTRA cél a workspace mellé az agent-memória mappa ADDITÍV '
31
+ + 'szkenje (preserveHistory: a kompaktáláskor kieső *.md-k MEGMARADNAK) a `memory` tárba, '
32
+ + '`project=agent-memory` scope-ra. Önállóan is futtatható (csak a memória).')
33
+ .option('--memory-scope <name>', 'a memória-cél projekt-scope neve (default `agent-memory`)', 'agent-memory')
39
34
  .action(async () => {
40
35
  fam_output_util_1.FAM_Output_Util.exit(await FAM_ScanProjects_Util.run(command));
41
36
  });
42
37
  return command;
43
38
  }
44
- /** A `scan-projects` futtatása: projekt-lista feloldás SOROS dryRun+scan összesítő. */
39
+ /** Indít egy szerver-oldali scan-job-ot, majd poll-ozza az állapotát a befejezésig (progress a stderr-re). */
45
40
  static async run(command) {
46
41
  const globals = command.optsWithGlobals();
47
42
  const local = command.opts();
48
- let specs;
49
- try {
50
- if (local.config) {
51
- specs = fam_project_discovery_util_1.FAM_ProjectDiscovery_Util.parseConfig((0, fs_1.readFileSync)(local.config, 'utf-8'));
52
- }
53
- else if (local.root) {
54
- specs = fam_project_discovery_util_1.FAM_ProjectDiscovery_Util.discover(local.root, Number(local.depth) || 3, { org: local.org, table: local.table });
55
- }
56
- else {
57
- return fam_output_util_1.FAM_Output_Util.failure({
58
- options: globals, command: 'scan-projects',
59
- error: {
60
- errorCode: 'FAM-CLI-SCANP-001',
61
- message: '`--root <dir>` (auto-felderítés) VAGY `--config <file.json>` (explicit lista) kötelező.',
62
- fixHint: 'Pl.: fam scan-projects --root E:/.../NPM-packages vagy --config projects.json',
63
- },
64
- });
65
- }
66
- }
67
- catch (error) {
43
+ if (!local.root && !local.config && !local.memoryPath) {
68
44
  return fam_output_util_1.FAM_Output_Util.failure({
69
45
  options: globals, command: 'scan-projects',
70
- error: { errorCode: 'FAM-CLI-SCANP-002', message: `A projekt-lista feloldása sikertelen: ${error?.message}` },
46
+ error: {
47
+ errorCode: 'FAM-CLI-SCANP-001',
48
+ message: '`--root <dir>` (auto-felderítés) VAGY `--config <file.json>` (explicit lista) VAGY `--memory-path <dir>` kötelező.',
49
+ fixHint: 'Pl.: fam scan-projects --root E:/.../NPM-packages vagy --config projects.json vagy SCAN-ALL: --root <workspace> --memory-path <mem-dir>',
50
+ },
71
51
  });
72
52
  }
73
- if (!specs.length) {
53
+ // A start-body — a felderítést/olvasást a SZERVER végzi (thin-CLI). A `sourceLocation` (a CLI cwd-je) MINDIG
54
+ // megy, hogy a szerver a relatív útvonalakat a HÍVÓ helyéhez oldja fel (a szerver cwd-je eltér). A `--root`/
55
+ // `--config` abszolútként is átmegy (belt-and-suspenders).
56
+ const body = { sourceLocation: process.cwd() };
57
+ if (local.config) {
58
+ body.configPath = (0, path_1.resolve)(local.config);
59
+ }
60
+ else if (local.root) {
61
+ body.root = (0, path_1.resolve)(local.root);
62
+ body.depth = Number(local.depth) || 3;
63
+ body.org = local.org;
64
+ body.table = local.table;
65
+ }
66
+ // „SCAN-ALL": a memória-mappa ADDITÍV cél a fő lista UTÁN appendelve (root-szintű `project=<scope>` scope, `memory`
67
+ // tár, preserveHistory → a kompaktáláskor kieső *.md-k megmaradnak). Önállóan is mehet (csak ez, root/config nélkül).
68
+ if (local.memoryPath) {
69
+ body.extraTargets = [{
70
+ path: (0, path_1.resolve)(local.memoryPath),
71
+ project: local.memoryScope ?? 'agent-memory',
72
+ table: 'memory',
73
+ preserveHistory: true,
74
+ scopePath: [{ layer: 'project', rawName: local.memoryScope ?? 'agent-memory' }],
75
+ }];
76
+ }
77
+ if (local.cooldown !== undefined) {
78
+ body.cooldownMs = Number(local.cooldown);
79
+ }
80
+ if (local.cooldownMinChunks !== undefined) {
81
+ body.cooldownMinChunks = Number(local.cooldownMinChunks);
82
+ }
83
+ const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
84
+ // A `start` a szerver FAST-PATH-ja miatt MÁR a (rész)job-ot adja vissza: ha a job ≤ a szerver wait-ablakán
85
+ // belül végzett → kész eredmény poll NÉLKÜL; különben futó job → poll-ozunk.
86
+ const start = await client.post('/api/scan/start', body);
87
+ if (!start.ok || !start.data) {
74
88
  return fam_output_util_1.FAM_Output_Util.failure({
75
89
  options: globals, command: 'scan-projects',
76
- error: { errorCode: 'FAM-CLI-SCANP-003', message: 'Nincs felderített/megadott projekt.' },
77
- exitCode: fam_cli_const_1.FAM_CliExitCode.operationError,
90
+ error: start.error ?? { errorCode: 'FAM-CLI-SCANP-002', message: 'A scan-job indítása sikertelen.' },
78
91
  });
79
92
  }
80
- const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
81
- const projects = [];
82
- let okCount = 0;
83
- // Projekt-cooldown (user-FR): a `--cooldown <ms>` felülírja a szerver `scan.projectCooldownMs` configját
84
- // (default 0 = ki). dryRun-on nincs cooldown (nincs valódi embed-munka). A projektek KÖZÉ vár, nem az utolsó után.
85
- const projectCooldownMs = local.dryRun ? 0 : await FAM_ScanProjects_Util.resolveCooldownMs(client, local.cooldown);
86
- FAM_ScanProjects_Util.progress(globals, `=== ${specs.length} projekt SOROS scan-je${local.dryRun ? ' (dryRun)' : ''}`
87
- + `${projectCooldownMs > 0 ? `, cooldown ${projectCooldownMs}ms/projekt` : ''} ===`);
88
- for (let i = 0; i < specs.length; i++) {
89
- const spec = specs[i];
90
- FAM_ScanProjects_Util.progress(globals, `[${i + 1}/${specs.length}] ${spec.project} (${spec.path})`);
91
- // 1) dryRun — idő-becslés
92
- const dry = await client.post('/api/write', FAM_ScanProjects_Util.body(spec, true, local));
93
- const dryScan = dry.ok ? dry.data?.scan : undefined;
94
- if (dryScan) {
95
- FAM_ScanProjects_Util.progress(globals, ` becslés: ${dryScan.filesProcessed} fájl, ${dryScan.chunks} chunk, ~${dryScan.estimate?.estimatedSeconds ?? '?'}s embed`);
96
- }
97
- if (local.dryRun) {
98
- projects.push({ project: spec.project, dryRun: dryScan });
99
- okCount += dry.ok ? 1 : 0;
100
- continue;
101
- }
102
- // 2) VALÓS scan VÁRJUK a teljes választ (soros, nincs konkurrencia)
103
- const startedAt = Date.now();
104
- const real = await client.post('/api/write', FAM_ScanProjects_Util.body(spec, false, local));
105
- const seconds = Math.round((Date.now() - startedAt) / 1000);
106
- const realScan = real.ok ? real.data?.scan : undefined;
107
- if (real.ok && realScan) {
108
- okCount++;
109
- FAM_ScanProjects_Util.progress(globals, ` kész: ${realScan.filesProcessed} fájl, ${realScan.chunks} chunk, ${JSON.stringify(realScan.verdicts)}, ${realScan.status} (${seconds}s)`);
110
- projects.push({ project: spec.project, scan: realScan, seconds: seconds });
111
- }
112
- else {
113
- FAM_ScanProjects_Util.progress(globals, ` HIBA: ${real.error?.message ?? 'ismeretlen scan-hiba'}`);
114
- projects.push({ project: spec.project, error: real.error });
115
- if (local.stopOnError) {
93
+ const jobId = start.data.jobId;
94
+ const printed = new Set();
95
+ FAM_ScanProjects_Util.progress(globals, `=== scan-job a SZERVEREN: ${jobId} (${start.data.total} projekt) ===`);
96
+ let last = start.data;
97
+ FAM_ScanProjects_Util.printNew(globals, last, printed);
98
+ if (!FAM_ScanProjects_Util.isTerminal(last.status)) {
99
+ // Nem fért a fast-path-ba poll a befejezésig (a frissen kész projekteket egyszer írjuk ki). ETA-jelzés.
100
+ const eta = last.etaSeconds !== undefined ? `, ETA ~${last.etaSeconds}s` : '';
101
+ FAM_ScanProjects_Util.progress(globals, ` …fut (${last.currentIndex}/${last.total}${eta}) poll-ozok`);
102
+ const pollMs = Number(local.poll) || 2000;
103
+ let lastSubProgress = '';
104
+ for (;;) {
105
+ await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(pollMs);
106
+ const status = await client.get(`/api/scan/status/${jobId}`);
107
+ if (!status.ok || !status.data) {
108
+ return fam_output_util_1.FAM_Output_Util.failure({
109
+ options: globals, command: 'scan-projects',
110
+ error: status.error ?? { errorCode: 'FAM-CLI-SCANP-003', message: 'A scan-job állapota nem lekérdezhető.' },
111
+ });
112
+ }
113
+ last = status.data;
114
+ FAM_ScanProjects_Util.printNew(globals, last, printed);
115
+ // Within-project sub-progress (user-FR): egy HOSSZÚ projekt KÖZBEN is lássunk haladást (nem csak a
116
+ // per-projekt befejezést). Dedup: csak változásnál írunk.
117
+ const sub = last.currentProgress;
118
+ if (sub && last.status === 'running') {
119
+ const line = ` ↳ ${sub.scopeLabel}: ${sub.processed}/${sub.total} fájl (${sub.pct}%)`;
120
+ if (line !== lastSubProgress) {
121
+ lastSubProgress = line;
122
+ FAM_ScanProjects_Util.progress(globals, line);
123
+ }
124
+ }
125
+ if (FAM_ScanProjects_Util.isTerminal(last.status)) {
116
126
  break;
117
127
  }
118
128
  }
119
- // Projekt-cooldown: a projektek KÖZÉ (NEM az utolsó után) — gép-tehermentesítő szünet nagy multi-scanhez.
120
- if (projectCooldownMs > 0 && i < specs.length - 1) {
121
- FAM_ScanProjects_Util.progress(globals, ` cooldown ${projectCooldownMs}ms…`);
122
- await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(projectCooldownMs);
123
- }
124
129
  }
125
- FAM_ScanProjects_Util.progress(globals, `=== KÉSZ: ${okCount}/${specs.length} ok ===`);
130
+ FAM_ScanProjects_Util.progress(globals, `=== ${last.status.toUpperCase()}: ${last.okCount}/${last.total} ok ===`);
126
131
  return fam_output_util_1.FAM_Output_Util.success({
127
132
  options: globals, command: 'scan-projects',
128
- data: { total: specs.length, ok: okCount, projects: projects },
133
+ data: { jobId: jobId, status: last.status, total: last.total, ok: last.okCount, projects: last.projects },
129
134
  });
130
135
  }
131
- /**
132
- * A projekt-cooldown ms feloldása: a CLI `--cooldown` felülírja a szerver `scan.projectCooldownMs` configját.
133
- * Nincs CLI-érték a szerver effektív configját kérdezi (`GET /api/config`); a config-lekérés hibája → 0 (a
134
- * cooldown sosem buktatja a scant). Minden út a `FAM_Cooldown_Util.resolveMs`-en megy át (clamp + biztonság).
135
- */
136
- static async resolveCooldownMs(client, cliValue) {
137
- if (cliValue !== undefined) {
138
- return fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs(cliValue);
139
- }
140
- const result = await client.get('/api/config');
141
- const value = result.ok ? result.data?.effective?.['scan.projectCooldownMs'] : 0;
142
- return fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs(value);
136
+ /** Terminál-e a job-státusz (kész/bukott/megszakítva). */
137
+ static isTerminal(status) {
138
+ return status === 'completed' || status === 'failed' || status === 'cancelled';
143
139
  }
144
- /** A scan-request body egy projekt-spec-hez (dryRun vagy valós). */
145
- static body(spec, dryRun, defaults) {
146
- return {
147
- table: spec.table ?? defaults.table ?? 'codebase',
148
- operation: 'scan-project',
149
- scopePath: [
150
- { layer: 'organization', rawName: spec.org ?? defaults.org ?? 'FutDevPro' },
151
- { layer: 'project', rawName: spec.project },
152
- ],
153
- scan: { path: spec.path, dryRun: dryRun },
154
- };
140
+ /** A frissen (ok/error) befejezett projekteket EGYSZER írja ki (a `printed` halmaz dedupol). */
141
+ static printNew(globals, job, printed) {
142
+ for (const project of job.projects) {
143
+ if ((project.status === 'ok' || project.status === 'error') && !printed.has(project.project)) {
144
+ printed.add(project.project);
145
+ if (project.status === 'ok') {
146
+ FAM_ScanProjects_Util.progress(globals, ` [${printed.size}/${job.total}] ${project.project}: `
147
+ + `${project.filesProcessed ?? '?'} fájl, ${project.chunks ?? '?'} chunk, `
148
+ + `${JSON.stringify(project.verdicts ?? {})} (${project.seconds ?? '?'}s)`);
149
+ }
150
+ else {
151
+ FAM_ScanProjects_Util.progress(globals, ` [${printed.size}/${job.total}] ${project.project}: HIBA — ${project.error?.message ?? 'ismeretlen'}`);
152
+ }
153
+ }
154
+ }
155
155
  }
156
- /** Ember-olvasható haladás-sor a stderr-re (a `--json` stdout-ot nem szennyezi; `--quiet` esetén néma). */
156
+ /** Ember-olvasható haladás-sor a stderr-re (`--json` stdout-ot nem szennyezi; `--quiet` esetén néma). */
157
157
  static progress(globals, line) {
158
158
  if (!globals.quiet) {
159
159
  process.stderr.write(`${line}\n`);
@@ -26,6 +26,7 @@ class FAM_Scan_Util {
26
26
  .option('--exclude <glob,...>', 'exclude glob-szűrők (a secret-guard mellé)')
27
27
  .option('--scope <layer=name,...>', 'scope-lánc (KÖTELEZŐ)')
28
28
  .option('--dry-run', 'nem ír; tervezett fájl/chunk/verdikt-szám', false)
29
+ .option('--preserve-history', 'ADDITÍV: a forrásból eltűnt fájlok bejegyzéseit NEM törli (superset; pl. kompaktálódó memória)', false)
29
30
  .addHelpText('before', '\n⚠️ TISZTA SCAN — ELŐBB az IGNORE-FÁJL:\n'
30
31
  + ' 1) Hozz létre `.fdpfamignore`-t a scan-gyökérben (a `.gitignore` mintájára) a projekt-\n'
31
32
  + ' specifikus junk-mappákra (a default már fedi: node_modules/Library/__pycache__/\n'
@@ -80,6 +81,7 @@ class FAM_Scan_Util {
80
81
  include: fam_arg_util_1.FAM_Arg_Util.parseList(local.include),
81
82
  exclude: exclude,
82
83
  dryRun: local.dryRun === true,
84
+ preserveHistory: local.preserveHistory === true,
83
85
  },
84
86
  };
85
87
  const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
@@ -23,9 +23,8 @@ const fam_client_service_1 = require("../_services/fam-client.service");
23
23
  * - **FEAT-002** („egy szerver, sok bridge"): ha már fut egy egészséges primary a porton → NEM indítunk
24
24
  * másodikat (idempotens reuse). Az MCP-kliensek külön `fam start`-ja úgyis ehhez a REST-primary-hoz
25
25
  * bridge-el.
26
- * - **FEAT-001** (activity-konzol): a detach-olt szerver a saját, látható activity-ablakát nyitja (a
27
- * `fam-activity-<port>.log` tail-je) ez a „fixen megnyíló ablak, ami mindig fut". A tee `fs`-sel a
28
- * fájlba ír, így `stdio:'ignore'` mellett is működik.
26
+ * - **Saját látható ablak:** a detach-olt szerver a SAJÁT, látható konzol-ablakában fut (`spawnServerWindow`) —
27
+ * az ablak KÖZVETLENÜL a node-ot futtatja, a szerver élő stdout/stderr-jét mutatja, és túléli a CLI/agent kilépését.
29
28
  *
30
29
  * A boot **REST-only** (a belépő argumentum-nélküli ága): tiszta HTTP-primary, MCP-stdio nélkül (a detach-olt
31
30
  * processnek nincs stdin-kliense). A heap-plafon a nagy korpusz boot-hidratálásához emelt (`--max-old-space-size`,
@@ -43,6 +42,109 @@ class FAM_Serve_Util {
43
42
  static HEALTH_POLL_MS = 1000;
44
43
  /** Az utolsó `fam serve` indítás állapot-fájlja (port + db + heap) — a `--restart`/auto-restart innen tudja, mit. */
45
44
  static STATE_FILE = (0, path_1.join)((0, os_1.tmpdir)(), 'fam-serve-state.json');
45
+ /**
46
+ * Az `ensureRunning()` egyidejű-spawn védő in-flight promise-e: ha több párhuzamos hívó (pl. több MCP-tool-hívás
47
+ * vagy bridge + CLI) EGYSZERRE találja le-állva a szervert, NE indítson mindenki külön példányt — az első spawn
48
+ * promise-ét osztjuk (process-en belül). A cross-process verseny benign (a port-preflight a 2. spawnt elejti).
49
+ */
50
+ static ensureInFlight = null;
51
+ /** Az utolsó (process-en belüli) ensure-spawn epoch-ms-e — a sorozatos tool-hívások ne nyissanak több ablakot, amíg az előző primary bootol. */
52
+ static lastEnsureSpawnAt = 0;
53
+ /** Az ensure-spawn cooldown-ja ms-ban: ennyin belül egy újabb le-állt-észlelés NEM spawn-ol újra (a bootoló primary-t várjuk). */
54
+ static ENSURE_SPAWN_COOLDOWN_MS = 15000;
55
+ /**
56
+ * `ensureRunning()` — **ÚJRAHASZNÁLHATÓ ensure-szemantika MINDEN FAM-funkcióhoz** (user-FR 2026-06-22: „minden
57
+ * fam funkciónak automatikusan gondoskodnia kell róla, hogy a fam el legyen indítva, ha nincs"). Health-check a
58
+ * (FAM_SERVER_URL/default `127.0.0.1:39265`) primary-n; ha fut → no-op (`already-running`). Ha NEM fut ÉS a cél
59
+ * **loopback** → leválasztott (detached) REST-primary spawn (a `fam serve` magjával: saját ablak, túléli a hívót)
60
+ * + health-bevárás. Remote cél → `remote-unreachable` (nem indíthatunk távoli szervert). Egyidejűség-védett.
61
+ *
62
+ * A hívók: az **MCP bridge** (unreachable → ensure + retry, önheal), a **`fam start` no-primary ág** (detached
63
+ * primary + bridge), és a **CLI data-parancsok** (pre-action). Best-effort, nem dob — a `reason` viszi a kimenetet.
64
+ */
65
+ static async ensureRunning(options) {
66
+ const client = new fam_client_service_1.FAM_CliClient_Service();
67
+ const baseUrl = client.getBaseUrl();
68
+ const port = FAM_Serve_Util.resolvePortFromUrl(baseUrl) ?? FAM_Serve_Util.resolvePort();
69
+ if (await client.isReachable()) {
70
+ return { running: true, spawned: false, url: baseUrl, port: port, reason: 'already-running' };
71
+ }
72
+ if (!FAM_Serve_Util.isLoopbackUrl(baseUrl)) {
73
+ // Távoli (nem-loopback) primary-t nem tudunk lokálisan felhúzni — csak jelezzük.
74
+ return { running: false, spawned: false, url: baseUrl, port: port, reason: 'remote-unreachable' };
75
+ }
76
+ if (FAM_Serve_Util.ensureInFlight) {
77
+ return FAM_Serve_Util.ensureInFlight;
78
+ }
79
+ // Spawn-spam védelem: ha az imént (cooldown-on belül) MÁR spawn-oltunk egy primary-t, az még BOOTOL-hat
80
+ // (pool-hidratálás) — ne nyissunk újabb ablakot minden tool-hívásra; jelezzük, hogy „indul" (a hívó retry-zik).
81
+ if (Date.now() - FAM_Serve_Util.lastEnsureSpawnAt < FAM_Serve_Util.ENSURE_SPAWN_COOLDOWN_MS) {
82
+ return { running: false, spawned: false, url: baseUrl, port: port, reason: 'boot-timeout' };
83
+ }
84
+ FAM_Serve_Util.lastEnsureSpawnAt = Date.now();
85
+ FAM_Serve_Util.ensureInFlight = FAM_Serve_Util.spawnDetachedForEnsure({
86
+ port: port, baseUrl: baseUrl, client: client, waitMs: options?.waitMs,
87
+ });
88
+ try {
89
+ return await FAM_Serve_Util.ensureInFlight;
90
+ }
91
+ finally {
92
+ FAM_Serve_Util.ensureInFlight = null;
93
+ }
94
+ }
95
+ /**
96
+ * Az `ensureRunning` spawn-magja (NEM CLI — nincs `FAM_Output`): a `serve` detached-spawn + health-wait logikájának
97
+ * újrahasznált, riport-mentes változata. Felhúzza a REST-only primary-t a saját ablakában, és bevárja a `/api/health`-et.
98
+ */
99
+ static async spawnDetachedForEnsure(set) {
100
+ const entry = FAM_Serve_Util.resolveServerEntry();
101
+ if (!entry) {
102
+ return { running: false, spawned: false, url: set.baseUrl, port: set.port, reason: 'no-entry' };
103
+ }
104
+ const envOverrides = {
105
+ FAM_SERVER_PORT: String(set.port),
106
+ FAM_LOG_FILE: (0, path_1.join)((0, os_1.tmpdir)(), `fam-server-${set.port}.log`),
107
+ };
108
+ const heapMb = FAM_Serve_Util.resolveHeapMb();
109
+ const nodeArgs = heapMb > 0 ? [`--max-old-space-size=${heapMb}`] : [];
110
+ try {
111
+ fam_console_util_1.FAM_Console_Util.spawnServerWindow({
112
+ command: process.execPath, args: [...nodeArgs, entry], envOverrides: envOverrides, port: set.port,
113
+ });
114
+ }
115
+ catch {
116
+ return { running: false, spawned: false, url: set.baseUrl, port: set.port, reason: 'spawn-failed' };
117
+ }
118
+ const waitMs = set.waitMs ?? FAM_Serve_Util.DEFAULT_WAIT_SEC * 1000;
119
+ const healthy = await FAM_Serve_Util.waitForHealthy(set.client, waitMs);
120
+ if (healthy) {
121
+ FAM_Serve_Util.writeServeState({ port: set.port, heapMb: heapMb });
122
+ }
123
+ return {
124
+ running: healthy, spawned: true, url: set.baseUrl, port: set.port,
125
+ reason: healthy ? 'spawned-healthy' : 'boot-timeout',
126
+ };
127
+ }
128
+ /** A port kinyerése egy base-URL-ből (`http://host:port`); nincs explicit port / hiba → `undefined` (a hívó default-ol). */
129
+ static resolvePortFromUrl(url) {
130
+ try {
131
+ const parsedPort = Number(new URL(url).port);
132
+ return Number.isInteger(parsedPort) && parsedPort > 0 ? parsedPort : undefined;
133
+ }
134
+ catch {
135
+ return undefined;
136
+ }
137
+ }
138
+ /** Loopback-cél-e az URL (`127.0.0.1`/`localhost`/`::1`/`0.0.0.0`)? Csak ilyet húzhatunk fel lokálisan. */
139
+ static isLoopbackUrl(url) {
140
+ try {
141
+ const host = new URL(url).hostname.toLowerCase().replace(/^\[|\]$/g, '');
142
+ return host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '0.0.0.0';
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
46
148
  /** A `serve` parancs-deszkriptor. */
47
149
  static command() {
48
150
  const command = new commander_1.Command('serve');
@@ -173,13 +275,11 @@ class FAM_Serve_Util {
173
275
  },
174
276
  });
175
277
  }
176
- // A window-/foreground-szerver env-override-jai (a többit a szerver a saját `.env`-jéből olvassa):
177
- // FAM_SHOW_CONSOLE=false NINCS külön FEAT-001 tail-ablak (a serve-ablak MAGA a látható konzol → nincs
178
- // dupla ablak; a user-direktíva szerint az Activity Monitor így redundáns).
278
+ // A detached-szerver env-override-jai (a többit a szerver a saját `.env`-jéből olvassa). A szerver a SAJÁT
279
+ // látható ablakában fut (`spawnServerWindow`) az ablak a szerver konzolja (nincs külön monitor-ablak).
179
280
  const envOverrides = {
180
281
  FAM_SERVER_PORT: String(set.port),
181
- FAM_SHOW_CONSOLE: 'false',
182
- // File-log → a detached szerver logjai (import-progress is) FÁJLBÓL OLVASHATÓK (tail), ablak nélkül.
282
+ // File-log → a detached szerver logjai (import-progress is) FÁJLBÓL is OLVASHATÓK (tail) a látható ablak MELLETT.
183
283
  FAM_LOG_FILE: (0, path_1.join)((0, os_1.tmpdir)(), `fam-server-${set.port}.log`),
184
284
  };
185
285
  if (set.local.mongodbUri) {
@@ -198,8 +298,8 @@ class FAM_Serve_Util {
198
298
  return FAM_Serve_Util.runForeground([...nodeArgs, entry], { ...process.env, ...envOverrides });
199
299
  }
200
300
  // DETACHED: a szervert egy KÜLÖN, LÁTHATÓ konzol-ablakban indítjuk (a `start`/terminál teljesen leválasztja
201
- // a CLI-ről → túléli; az ablak a szerver élő stdout/stderr-jét mutatja). REST-only primary (a belépő
202
- // argumentum-nélküli ága). A FEAT-001 tail-monitort kikapcsoljuk (FAM_SHOW_CONSOLE=false) nincs dupla ablak.
301
+ // a CLI-ről → túléli a hívót; az ablak a szerver élő stdout/stderr-jét mutatja). REST-only primary (a belépő
302
+ // argumentum-nélküli ága). Egy szerver = egy látható ablak (nincs külön activity-monitor).
203
303
  try {
204
304
  fam_console_util_1.FAM_Console_Util.spawnServerWindow({
205
305
  command: process.execPath,
@@ -64,6 +64,23 @@ class FAM_CliClient_Service {
64
64
  async put(path, body) {
65
65
  return this.request('PUT', path, body);
66
66
  }
67
+ /**
68
+ * EVENT-SIGNING aláírás-headerek (user-FR 2026-06-25): a CLI minden REST-hívása `fam-cli`-ként alá van írva; ha az
69
+ * agent beállítja a `FAM_SESSION_ID` env-et, az a hívó session-azonosítója (több egyidejű workspace-session
70
+ * elkülönítéséhez). A `FAM_WORKSPACE` env vagy a CLI `cwd` adja a workspace-kontextust.
71
+ */
72
+ signingHeaders() {
73
+ const headers = { 'x-fam-agent': 'fam-cli' };
74
+ const sessionId = process.env.FAM_SESSION_ID;
75
+ if (sessionId && sessionId.trim()) {
76
+ headers['x-fam-session'] = sessionId.trim();
77
+ }
78
+ const workspace = process.env.FAM_WORKSPACE || process.cwd();
79
+ if (workspace) {
80
+ headers['x-fam-workspace'] = workspace;
81
+ }
82
+ return headers;
83
+ }
67
84
  /**
68
85
  * A közös HTTP-réteg: a server-választ `FAM_ClientResult`-tá fordítja. Elérhetetlen szerver →
69
86
  * `unreachable:true`; a `{ ok:false, error }` test → `error` átvitel; egyéb non-2xx → szintetikus
@@ -71,11 +88,18 @@ class FAM_CliClient_Service {
71
88
  */
72
89
  async request(method, path, body) {
73
90
  const url = `${this.baseUrl}${path}`;
91
+ // EVENT-SIGNING (user-FR 2026-06-25): a CLI MINDEN hívását aláírjuk → a `fam scan-projects` (és minden más)
92
+ // beazonosíthatóan `fam-cli`-ként szerepel a job-okon. A `FAM_SESSION_ID` env (ha az agent beállítja) a hívó
93
+ // session-azonosítója. Egy helyen, a közös request-rétegben → minden CLI-parancs aláírt.
94
+ const headers = this.signingHeaders();
95
+ if (body !== undefined) {
96
+ headers['content-type'] = 'application/json';
97
+ }
74
98
  let response;
75
99
  try {
76
100
  response = await fetch(url, {
77
101
  method: method,
78
- headers: body === undefined ? {} : { 'content-type': 'application/json' },
102
+ headers: headers,
79
103
  body: body === undefined ? undefined : JSON.stringify(body),
80
104
  });
81
105
  }