mcp-efficiency-engine 0.1.5 → 0.1.7

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/.vscode/mcp.json CHANGED
@@ -7,16 +7,23 @@
7
7
  "command": "codebase-memory-mcp"
8
8
  },
9
9
  "codegraph": {
10
- "command": "codegraph",
10
+ "command": "pwsh",
11
11
  "args": [
12
- "serve",
13
- "--mcp"
12
+ "-NoProfile",
13
+ "-ExecutionPolicy",
14
+ "Bypass",
15
+ "-File",
16
+ "scripts/ops/start-codegraph-mcp.ps1"
14
17
  ]
15
18
  },
16
19
  "gitnexus": {
17
- "command": "gitnexus",
20
+ "command": "pwsh",
18
21
  "args": [
19
- "mcp"
22
+ "-NoProfile",
23
+ "-ExecutionPolicy",
24
+ "Bypass",
25
+ "-File",
26
+ "scripts/ops/start-gitnexus-mcp.ps1"
20
27
  ],
21
28
  "env": {
22
29
  "GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT": "50000",
@@ -95,6 +95,14 @@ npm install mcp-efficiency-engine
95
95
 
96
96
  Si el entorno bloquea scripts de npm (`allow-scripts`), ejecutar manualmente:
97
97
 
98
+ ```powershell
99
+ npm install mcp-efficiency-engine
100
+ npm approve-scripts mcp-efficiency-engine
101
+ npm rebuild mcp-efficiency-engine
102
+ ```
103
+
104
+ Alternativa manual equivalente:
105
+
98
106
  ```powershell
99
107
  npx mcp-efficiency-engine install
100
108
  npx mcp-efficiency-engine validate -PortableMode
package/README.md CHANGED
@@ -190,13 +190,21 @@ Comportamiento esperado:
190
190
 
191
191
  - copia al proyecto host los artefactos canonicos del engine (`scripts`, `.github`, `.vscode`, `repo-intake`, `orchestrator`, `policies`, `observability`, `autodocs/schema`, `memory`, etc.)
192
192
  - instala motores y herramientas via bootstrap portable
193
- - si no existe `repo-registry/repos.yml`, pregunta por owner/prefix y si quieres registrar un repo inicial para intake
194
- - si no añades repos en ese momento, deja el registry plantilla listo para añadirlos despues y rerun de intake
193
+ - si no existe `repo-registry/repos.yml`, en modo interactivo pregunta por owner/prefix y repo inicial para intake
194
+ - si la instalacion corre en modo no interactivo (comun en lifecycle scripts de npm), crea automaticamente un repo inicial por defecto: dominio `dev`, location `.`
195
195
 
196
196
  Nota npm (entornos con politicas de scripts):
197
197
 
198
- - si `npm` bloquea `install`/`postinstall` (por ejemplo con `allow-scripts`), el scaffold/bootstrapping puede no ejecutarse automaticamente
199
- - en ese caso, aprueba scripts (`npm approve-scripts`) o ejecuta manualmente:
198
+ - si `npm` bloquea `install`/`postinstall` (por ejemplo con `allow-scripts`), el scaffold/bootstrapping no se ejecuta automaticamente
199
+ - flujo recomendado en dos pasos:
200
+
201
+ ```powershell
202
+ npm install mcp-efficiency-engine
203
+ npm approve-scripts mcp-efficiency-engine
204
+ npm rebuild mcp-efficiency-engine
205
+ ```
206
+
207
+ - alternativa manual equivalente:
200
208
 
201
209
  ```powershell
202
210
  npx mcp-efficiency-engine install
@@ -217,6 +225,33 @@ npm install -g mcp-efficiency-engine
217
225
  mcpee install
218
226
  ```
219
227
 
228
+ ### Cuando se conectan boosts/repos auxiliares
229
+
230
+ Resumen rapido:
231
+
232
+ - `npm install` + `rebuild` instala/configura el engine.
233
+ - La conexion real de boosts/repos ocurre al ejecutar intake.
234
+ - Si no configuras repos adicionales, se usa el repo inicial por defecto.
235
+
236
+ Pasos recomendados despues de instalar:
237
+
238
+ ```powershell
239
+ # 1) (Opcional) editar repos auxiliares/boosts
240
+ notepad .\repo-registry\repos.yml
241
+
242
+ # 2) materializar capacidades de todos los repos del registry
243
+ .\scripts\intake\run-repo-intake.cmd
244
+
245
+ # 3) validar que el router ya los ve
246
+ .\scripts\ops\hi.ps1
247
+ ```
248
+
249
+ Si quieres que te pregunte por owner/prefix/repo inicial en modo asistido, ejecuta:
250
+
251
+ ```powershell
252
+ npx mcp-efficiency-engine install
253
+ ```
254
+
220
255
  ### 2) Validación mínima
221
256
 
222
257
  ```powershell
@@ -233,6 +268,17 @@ py -3 .\scripts\intake\run-routing-evals.py
233
268
  .\scripts\ops\bye.ps1
234
269
  ```
235
270
 
271
+ Telemetría de terminal (PowerShell, opcional):
272
+
273
+ ```powershell
274
+ mcpee observe-on
275
+ # ... comandos interactivos ...
276
+ mcpee observe-off
277
+ ```
278
+
279
+ - `mcpee observe-on` instala un hook global de perfil PowerShell para emitir un evento de telemetría por comando usando `scripts/ops/emit-terminal-command-telemetry.py`.
280
+ - `mcpee observe-off` elimina el hook global y restaura el perfil sin instrumentación.
281
+
236
282
  Validación extendida recomendada:
237
283
 
238
284
  ```powershell
@@ -238,6 +238,15 @@ function deriveRepoPrefix(targetRoot, options) {
238
238
  return `${path.basename(targetRoot).replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase()}_`;
239
239
  }
240
240
 
241
+ function deriveDefaultRepoName(targetRoot, options) {
242
+ if (options.initialRepoName) {
243
+ return options.initialRepoName;
244
+ }
245
+
246
+ const safeBaseName = path.basename(targetRoot).replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
247
+ return `${deriveRepoPrefix(targetRoot, options)}${safeBaseName}`;
248
+ }
249
+
241
250
  function initializeTemplateRegistry(targetRoot, options) {
242
251
  const registryPath = path.join(targetRoot, "repo-registry", "repos.yml");
243
252
  if (fs.existsSync(registryPath)) {
@@ -245,14 +254,28 @@ function initializeTemplateRegistry(targetRoot, options) {
245
254
  }
246
255
 
247
256
  const initScript = path.join(targetRoot, "scripts", "intake", "init-template-registry.ps1");
257
+ const resolvedPrefix = deriveRepoPrefix(targetRoot, options);
248
258
  const args = [
249
259
  "-Owner",
250
260
  options.owner || "your-team",
251
261
  "-RepoNamePrefix",
252
- deriveRepoPrefix(targetRoot, options),
253
- "-SkipInitialRepo",
262
+ resolvedPrefix,
254
263
  ];
255
264
 
265
+ if (options.skipInitialRepo) {
266
+ args.push("-SkipInitialRepo");
267
+ }
268
+ else {
269
+ args.push(
270
+ "-InitialRepoName",
271
+ deriveDefaultRepoName(targetRoot, options),
272
+ "-InitialRepoDomain",
273
+ options.initialRepoDomain || "dev",
274
+ "-InitialRepoLocation",
275
+ options.initialRepoLocation || ".",
276
+ );
277
+ }
278
+
256
279
  return runPowerShell(initScript, args, targetRoot);
257
280
  }
258
281
 
@@ -314,7 +337,8 @@ function runHostInstall(rawOptions) {
314
337
  return hookStatus;
315
338
  }
316
339
 
317
- process.stdout.write("[mcpee] Modo no interactivo detectado. Se inicializo el registry plantilla y se omitio bootstrap interactivo.\n");
340
+ process.stdout.write("[mcpee] Modo no interactivo detectado. Se inicializo repos.yml con un repo inicial por defecto y se omitio bootstrap interactivo.\n");
341
+ process.stdout.write("[mcpee] Si npm bloqueo scripts, ejecuta: npm approve-scripts mcp-efficiency-engine ; npm rebuild mcp-efficiency-engine\n");
318
342
  return 0;
319
343
  }
320
344
 
package/bin/mcpee.js CHANGED
@@ -12,8 +12,77 @@ const commandMap = {
12
12
  hi: "scripts/ops/hi.ps1",
13
13
  bye: "scripts/ops/bye.ps1",
14
14
  intake: "scripts/intake/run-repo-intake.ps1",
15
+ "observe-on": "scripts/ops/install-terminal-telemetry-hook.ps1",
16
+ "observe-off": "scripts/ops/uninstall-terminal-telemetry-hook.ps1",
15
17
  };
16
18
 
19
+ function resolveSessionId() {
20
+ const explicitSessionId = (process.env.MCP_SESSION_ID || "").trim();
21
+ if (explicitSessionId) {
22
+ return explicitSessionId;
23
+ }
24
+
25
+ const sessionLogHint = (process.env.VSCODE_TARGET_SESSION_LOG || "").trim();
26
+ if (sessionLogHint) {
27
+ const match = sessionLogHint.match(/[0-9a-fA-F-]{36}/);
28
+ if (match && match[0]) {
29
+ return match[0];
30
+ }
31
+ }
32
+
33
+ return "mcpee-cli";
34
+ }
35
+
36
+ function resolvePythonCandidates() {
37
+ if (process.platform === "win32") {
38
+ return [
39
+ ["py", ["-3"]],
40
+ ["python", []],
41
+ ["python3", []],
42
+ ];
43
+ }
44
+
45
+ return [
46
+ ["python3", []],
47
+ ["python", []],
48
+ ];
49
+ }
50
+
51
+ function runCopilotUsageIngest(repoRootPath) {
52
+ const sessionLogHint = (process.env.VSCODE_TARGET_SESSION_LOG || "").trim();
53
+ if (!sessionLogHint) {
54
+ return;
55
+ }
56
+
57
+ const ingestScriptPath = path.join(repoRootPath, "scripts", "learning", "ingest-copilot-session-usage.py");
58
+ if (!fs.existsSync(ingestScriptPath)) {
59
+ return;
60
+ }
61
+
62
+ for (const [pythonCommand, pythonPrefixArgs] of resolvePythonCandidates()) {
63
+ const ingestResult = spawnSync(
64
+ pythonCommand,
65
+ [
66
+ ...pythonPrefixArgs,
67
+ ingestScriptPath,
68
+ "--session-log",
69
+ sessionLogHint,
70
+ ],
71
+ {
72
+ cwd: repoRootPath,
73
+ stdio: "ignore",
74
+ },
75
+ );
76
+
77
+ if (ingestResult.error && ingestResult.error.code === "ENOENT") {
78
+ continue;
79
+ }
80
+
81
+ // La ingesta es best-effort: si falla no bloquea el comando principal.
82
+ return;
83
+ }
84
+ }
85
+
17
86
  function resolveExecutionRoot(scriptRelativePath) {
18
87
  const configuredTarget = process.env.MCPEE_TARGET_DIR ? path.resolve(process.env.MCPEE_TARGET_DIR) : "";
19
88
  const cwdRoot = process.cwd();
@@ -41,6 +110,8 @@ function printHelp() {
41
110
  " hi Ejecuta el preflight diario.",
42
111
  " bye Ejecuta el cierre diario.",
43
112
  " intake Ejecuta el repo intake.",
113
+ " observe-on Activa telemetria global del terminal (hook PowerShell).",
114
+ " observe-off Desactiva telemetria global del terminal.",
44
115
  "",
45
116
  "Ejemplos:",
46
117
  " npx mcp-efficiency-engine bootstrap",
@@ -61,6 +132,10 @@ function runPowerShellScript(scriptRelativePath, forwardedArgs) {
61
132
  const executionRoot = resolveExecutionRoot(scriptRelativePath);
62
133
  const scriptPath = path.join(executionRoot, scriptRelativePath);
63
134
  const tracerScriptPath = path.join(repoRoot, "scripts", "ops", "trace-command.py");
135
+ const sessionId = resolveSessionId();
136
+
137
+ runCopilotUsageIngest(repoRoot);
138
+
64
139
  if (!fs.existsSync(scriptPath)) {
65
140
  process.stderr.write(`Script no encontrado: ${scriptRelativePath}\n`);
66
141
  return 1;
@@ -70,37 +145,42 @@ function runPowerShellScript(scriptRelativePath, forwardedArgs) {
70
145
  for (const shellCommand of shellCandidates) {
71
146
  if (fs.existsSync(tracerScriptPath)) {
72
147
  const operation = `mcpee.${path.basename(scriptRelativePath, path.extname(scriptRelativePath))}`;
73
- const tracedResult = spawnSync(
74
- "py",
75
- [
76
- "-3",
77
- tracerScriptPath,
78
- "--operation",
79
- operation,
80
- "--session-id",
81
- "mcpee-cli",
82
- "--cwd",
83
- executionRoot,
84
- "--",
85
- shellCommand,
86
- "-NoProfile",
87
- "-ExecutionPolicy",
88
- "Bypass",
89
- "-File",
90
- scriptPath,
91
- ...forwardedArgs,
92
- ],
93
- {
94
- cwd: repoRoot,
95
- stdio: "inherit",
96
- },
97
- );
98
-
99
- if (!tracedResult.error || tracedResult.error.code !== "ENOENT") {
148
+ for (const [pythonCommand, pythonPrefixArgs] of resolvePythonCandidates()) {
149
+ const tracedResult = spawnSync(
150
+ pythonCommand,
151
+ [
152
+ ...pythonPrefixArgs,
153
+ tracerScriptPath,
154
+ "--operation",
155
+ operation,
156
+ "--session-id",
157
+ sessionId,
158
+ "--cwd",
159
+ executionRoot,
160
+ "--",
161
+ shellCommand,
162
+ "-NoProfile",
163
+ "-ExecutionPolicy",
164
+ "Bypass",
165
+ "-File",
166
+ scriptPath,
167
+ ...forwardedArgs,
168
+ ],
169
+ {
170
+ cwd: repoRoot,
171
+ stdio: "inherit",
172
+ },
173
+ );
174
+
175
+ if (tracedResult.error && tracedResult.error.code === "ENOENT") {
176
+ continue;
177
+ }
178
+
100
179
  if (tracedResult.error) {
101
180
  process.stderr.write(`${tracedResult.error.message}\n`);
102
181
  return 1;
103
182
  }
183
+
104
184
  return tracedResult.status ?? 0;
105
185
  }
106
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-efficiency-engine",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Motor de orquestacion para agentes MCP con routing por dominio y bootstrap portable.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import hashlib
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def _bootstrap_repo_root() -> Path:
10
+ repo_root = Path(__file__).resolve().parents[2]
11
+ if str(repo_root) not in sys.path:
12
+ sys.path.insert(0, str(repo_root))
13
+ return repo_root
14
+
15
+
16
+ def _hash_command(command: str) -> str:
17
+ return hashlib.sha256(command.encode("utf-8")).hexdigest()[:16]
18
+
19
+
20
+ def main() -> int:
21
+ parser = argparse.ArgumentParser(description="Emit telemetry for an already executed terminal command.")
22
+ parser.add_argument("--session-id", default="terminal-session", help="Session identifier")
23
+ parser.add_argument("--cwd", default=".", help="Current working directory")
24
+ parser.add_argument("--command", default="", help="Executed command text")
25
+ parser.add_argument("--success", default="true", help="Whether command succeeded (true/false)")
26
+ parser.add_argument("--exit-code", type=int, default=0, help="Exit code for native commands")
27
+ args = parser.parse_args()
28
+
29
+ command_text = (args.command or "").strip()
30
+ if not command_text:
31
+ return 0
32
+
33
+ repo_root = _bootstrap_repo_root()
34
+ from telemetry import build_telemetry_collector
35
+
36
+ collector = build_telemetry_collector(repo_root)
37
+
38
+ success = str(args.success).strip().lower() in {"1", "true", "yes", "ok"}
39
+ normalized_cwd = str(Path(args.cwd).resolve())
40
+
41
+ with collector.start_execution(
42
+ operation="terminal.command",
43
+ session_id=str(args.session_id).strip() or "terminal-session",
44
+ model="powershell",
45
+ provider="local-shell",
46
+ ):
47
+ with collector.start_span(
48
+ name="terminal.executed_command",
49
+ kind="TOOL",
50
+ attributes={
51
+ "cwd": normalized_cwd,
52
+ "command_hash": _hash_command(command_text),
53
+ "success": success,
54
+ "exit_code": int(args.exit_code),
55
+ },
56
+ ):
57
+ collector.record_event(
58
+ "RoutingResolved",
59
+ {
60
+ "source": "terminal-hook",
61
+ "cwd": normalized_cwd,
62
+ "success": success,
63
+ "exit_code": int(args.exit_code),
64
+ },
65
+ level="INFO",
66
+ )
67
+ collector.record_metric("terminal_command_count", 1.0, unit="count")
68
+ collector.record_metric("terminal_last_exit_code", float(int(args.exit_code)), unit="code")
69
+ if not success:
70
+ collector.record_event(
71
+ "ExceptionThrown",
72
+ {
73
+ "source": "terminal-hook",
74
+ "message": "command_reported_failure",
75
+ "exit_code": int(args.exit_code),
76
+ },
77
+ level="ERROR",
78
+ )
79
+
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ raise SystemExit(main())
@@ -0,0 +1,152 @@
1
+ param(
2
+ [switch]$AllHosts,
3
+ [switch]$CurrentProcess,
4
+ [string]$SessionId = ''
5
+ )
6
+
7
+ Set-StrictMode -Version Latest
8
+ $ErrorActionPreference = 'Stop'
9
+
10
+ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
11
+ $sessionIdResolved = if ([string]::IsNullOrWhiteSpace($SessionId)) {
12
+ if (-not [string]::IsNullOrWhiteSpace($env:MCP_SESSION_ID)) { $env:MCP_SESSION_ID }
13
+ elseif (-not [string]::IsNullOrWhiteSpace($env:VSCODE_TARGET_SESSION_LOG) -and $env:VSCODE_TARGET_SESSION_LOG -match '([0-9a-fA-F-]{36})') { $Matches[1] }
14
+ else { 'terminal-session' }
15
+ }
16
+ else {
17
+ $SessionId
18
+ }
19
+
20
+ $hookBlock = @"
21
+ # >>> MCPEE_TERMINAL_TELEMETRY_START >>>
22
+ `$global:MCPEE_TELEMETRY_REPO_ROOT = '$repoRoot'
23
+ `$global:MCPEE_TELEMETRY_SESSION_ID = '$sessionIdResolved'
24
+ `$global:MCPEE_TELEMETRY_LAST_HISTORY_ID = -1
25
+ `$global:MCPEE_TELEMETRY_LAST_STAMP = ''
26
+
27
+ function global:Get-McpeeTelemetryPythonCommand {
28
+ if (Get-Command py -ErrorAction SilentlyContinue) {
29
+ return @('py', '-3')
30
+ }
31
+ if (Get-Command python -ErrorAction SilentlyContinue) {
32
+ return @('python')
33
+ }
34
+ if (Get-Command python3 -ErrorAction SilentlyContinue) {
35
+ return @('python3')
36
+ }
37
+ return @()
38
+ }
39
+
40
+ function global:Invoke-McpeeTerminalTelemetry {
41
+ try {
42
+ `$last = Get-History -Count 1 -ErrorAction SilentlyContinue
43
+ if (`$null -eq `$last) { return }
44
+
45
+ `$historyId = [int]`$last.Id
46
+ if (`$historyId -le [int]`$global:MCPEE_TELEMETRY_LAST_HISTORY_ID) { return }
47
+
48
+ `$commandText = [string]`$last.CommandLine
49
+ if ([string]::IsNullOrWhiteSpace(`$commandText)) { return }
50
+
51
+ `$commandTrim = `$commandText.Trim()
52
+ if (`$commandTrim -match 'Invoke-McpeeTerminalTelemetry|emit-terminal-command-telemetry\.py') {
53
+ `$global:MCPEE_TELEMETRY_LAST_HISTORY_ID = `$historyId
54
+ return
55
+ }
56
+
57
+ `$stamp = ('{0}:{1}' -f `$historyId, `$commandTrim)
58
+ if (`$stamp -eq `$global:MCPEE_TELEMETRY_LAST_STAMP) {
59
+ `$global:MCPEE_TELEMETRY_LAST_HISTORY_ID = `$historyId
60
+ return
61
+ }
62
+
63
+ `$py = Get-McpeeTelemetryPythonCommand
64
+ if (`$py.Count -eq 0) {
65
+ `$global:MCPEE_TELEMETRY_LAST_HISTORY_ID = `$historyId
66
+ `$global:MCPEE_TELEMETRY_LAST_STAMP = `$stamp
67
+ return
68
+ }
69
+
70
+ `$scriptPath = Join-Path `$global:MCPEE_TELEMETRY_REPO_ROOT 'scripts/ops/emit-terminal-command-telemetry.py'
71
+ if (-not (Test-Path `$scriptPath)) {
72
+ `$global:MCPEE_TELEMETRY_LAST_HISTORY_ID = `$historyId
73
+ `$global:MCPEE_TELEMETRY_LAST_STAMP = `$stamp
74
+ return
75
+ }
76
+
77
+ `$success = if (`$?) { 'true' } else { 'false' }
78
+ `$nativeExit = if (`$null -eq `$LASTEXITCODE) { 0 } else { [int]`$LASTEXITCODE }
79
+
80
+ `$pyArgs = @()
81
+ if (`$py.Count -gt 1) {
82
+ `$pyArgs = `$py[1..(`$py.Count-1)]
83
+ }
84
+
85
+ & `$py[0] @(`$pyArgs) `$scriptPath --session-id `$global:MCPEE_TELEMETRY_SESSION_ID --cwd `$PWD.Path --command `$commandTrim --success `$success --exit-code `$nativeExit *> `$null
86
+
87
+ `$global:MCPEE_TELEMETRY_LAST_HISTORY_ID = `$historyId
88
+ `$global:MCPEE_TELEMETRY_LAST_STAMP = `$stamp
89
+ }
90
+ catch {
91
+ # Hook de telemetria best-effort: nunca romper la consola.
92
+ }
93
+ }
94
+
95
+ if (-not (Test-Path Function:\global:MCPEE_ORIGINAL_PROMPT)) {
96
+ `$originalPromptBody = if (Test-Path Function:\global:prompt) { (Get-Command prompt -CommandType Function).ScriptBlock.ToString() } else { '"PS " + `$executionContext.SessionState.Path.CurrentLocation + "> "' }
97
+ Set-Item -Path Function:\global:MCPEE_ORIGINAL_PROMPT -Value ([ScriptBlock]::Create(`$originalPromptBody)) -Force
98
+ }
99
+
100
+ function global:prompt {
101
+ Invoke-McpeeTerminalTelemetry
102
+ & global:MCPEE_ORIGINAL_PROMPT
103
+ }
104
+ # <<< MCPEE_TERMINAL_TELEMETRY_END <<<
105
+ "@
106
+
107
+ function Install-InProfile {
108
+ param([string]$ProfilePath)
109
+
110
+ $dir = Split-Path $ProfilePath -Parent
111
+ if (-not (Test-Path $dir)) {
112
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
113
+ }
114
+
115
+ if (-not (Test-Path $ProfilePath)) {
116
+ New-Item -ItemType File -Path $ProfilePath -Force | Out-Null
117
+ }
118
+
119
+ $content = Get-Content -Path $ProfilePath -Raw -ErrorAction SilentlyContinue
120
+ if ($null -eq $content) { $content = '' }
121
+
122
+ if ($content -match 'MCPEE_TERMINAL_TELEMETRY_START') {
123
+ $updated = [regex]::Replace(
124
+ $content,
125
+ '(?s)# >>> MCPEE_TERMINAL_TELEMETRY_START >>>.*?# <<< MCPEE_TERMINAL_TELEMETRY_END <<<',
126
+ $hookBlock
127
+ )
128
+ Set-Content -Path $ProfilePath -Value $updated -Encoding UTF8
129
+ }
130
+ else {
131
+ if (-not [string]::IsNullOrWhiteSpace($content) -and -not $content.EndsWith("`n")) {
132
+ $content += "`r`n"
133
+ }
134
+ $content += "`r`n$hookBlock`r`n"
135
+ Set-Content -Path $ProfilePath -Value $content -Encoding UTF8
136
+ }
137
+
138
+ Write-Host "[ok] Hook instalado/actualizado en: $ProfilePath"
139
+ }
140
+
141
+ if ($CurrentProcess) {
142
+ Invoke-Expression $hookBlock
143
+ Write-Host '[ok] Hook cargado en la sesion actual.'
144
+ }
145
+
146
+ Install-InProfile -ProfilePath $PROFILE.CurrentUserCurrentHost
147
+ if ($AllHosts) {
148
+ Install-InProfile -ProfilePath $PROFILE.CurrentUserAllHosts
149
+ }
150
+
151
+ Write-Host '[ok] Telemetria global de terminal activada (best-effort).'
152
+ Write-Host '[hint] Para desinstalar usa: mcpee observe-off'
@@ -0,0 +1,98 @@
1
+ param(
2
+ [string]$SourceConnectionString = 'Server=(localdb)\MSSQLLocalDB;Database=TechRidersDev;Trusted_Connection=True;MultipleActiveResultSets=True;TrustServerCertificate=True',
3
+ [string]$TargetServer = 'sql-techriders-dev-ohbnpr4l4fihc.database.windows.net',
4
+ [string]$TargetDatabase = 'sqldb-techriders-dev',
5
+ [string]$TargetUser = 'sqladminuser'
6
+ )
7
+
8
+ $ErrorActionPreference = 'Stop'
9
+ Add-Type -AssemblyName System.Data
10
+
11
+ if ([string]::IsNullOrWhiteSpace($env:SQL_ADMIN_PASSWORD)) {
12
+ throw 'Missing SQL_ADMIN_PASSWORD environment variable.'
13
+ }
14
+
15
+ $targetConnectionString = "Server=tcp:$TargetServer,1433;Initial Catalog=$TargetDatabase;Persist Security Info=False;User ID=$TargetUser;Password=$($env:SQL_ADMIN_PASSWORD);MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
16
+
17
+ $sourceConn = New-Object System.Data.SqlClient.SqlConnection $SourceConnectionString
18
+ $targetConn = New-Object System.Data.SqlClient.SqlConnection $targetConnectionString
19
+
20
+ try {
21
+ $sourceConn.Open()
22
+ $targetConn.Open()
23
+
24
+ $tableCmd = $sourceConn.CreateCommand()
25
+ $tableCmd.CommandText = @"
26
+ SELECT s.name AS SchemaName, t.name AS TableName
27
+ FROM sys.tables t
28
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
29
+ WHERE t.is_ms_shipped = 0
30
+ ORDER BY s.name, t.name;
31
+ "@
32
+
33
+ $reader = $tableCmd.ExecuteReader()
34
+ $tables = New-Object System.Collections.Generic.List[object]
35
+ while ($reader.Read()) {
36
+ $schema = $reader.GetString(0)
37
+ $table = $reader.GetString(1)
38
+ if ($table -ne '__EFMigrationsHistory') {
39
+ $tables.Add([PSCustomObject]@{ Schema = $schema; Table = $table })
40
+ }
41
+ }
42
+ $reader.Close()
43
+
44
+ if ($tables.Count -eq 0) {
45
+ throw 'No source user tables found.'
46
+ }
47
+
48
+ foreach ($t in $tables) {
49
+ $fullName = "[$($t.Schema)].[$($t.Table)]"
50
+ $disableCmd = $targetConn.CreateCommand()
51
+ $disableCmd.CommandText = "ALTER TABLE $fullName NOCHECK CONSTRAINT ALL;"
52
+ [void]$disableCmd.ExecuteNonQuery()
53
+ }
54
+
55
+ $summary = New-Object System.Collections.Generic.List[object]
56
+
57
+ foreach ($t in $tables) {
58
+ $fullName = "[$($t.Schema)].[$($t.Table)]"
59
+
60
+ $deleteCmd = $targetConn.CreateCommand()
61
+ $deleteCmd.CommandText = "DELETE FROM $fullName;"
62
+ [void]$deleteCmd.ExecuteNonQuery()
63
+
64
+ $selectCmd = $sourceConn.CreateCommand()
65
+ $selectCmd.CommandText = "SELECT * FROM $fullName;"
66
+
67
+ $adapter = New-Object System.Data.SqlClient.SqlDataAdapter $selectCmd
68
+ $dataTable = New-Object System.Data.DataTable
69
+ [void]$adapter.Fill($dataTable)
70
+
71
+ if ($dataTable.Rows.Count -gt 0) {
72
+ $bulkCopy = New-Object System.Data.SqlClient.SqlBulkCopy($targetConn, [System.Data.SqlClient.SqlBulkCopyOptions]::KeepIdentity, $null)
73
+ $bulkCopy.DestinationTableName = $fullName
74
+ $bulkCopy.BulkCopyTimeout = 0
75
+ foreach ($col in $dataTable.Columns) {
76
+ [void]$bulkCopy.ColumnMappings.Add($col.ColumnName, $col.ColumnName)
77
+ }
78
+ $bulkCopy.WriteToServer($dataTable)
79
+ $bulkCopy.Close()
80
+ }
81
+
82
+ $summary.Add([PSCustomObject]@{ Table = $fullName; Rows = $dataTable.Rows.Count })
83
+ }
84
+
85
+ foreach ($t in $tables) {
86
+ $fullName = "[$($t.Schema)].[$($t.Table)]"
87
+ $enableCmd = $targetConn.CreateCommand()
88
+ $enableCmd.CommandText = "ALTER TABLE $fullName WITH CHECK CHECK CONSTRAINT ALL;"
89
+ [void]$enableCmd.ExecuteNonQuery()
90
+ }
91
+
92
+ $summary | Sort-Object Table | Format-Table -AutoSize | Out-String | Write-Output
93
+ Write-Output 'MIGRATION_COMPLETED'
94
+ }
95
+ finally {
96
+ if ($sourceConn.State -eq 'Open') { $sourceConn.Close() }
97
+ if ($targetConn.State -eq 'Open') { $targetConn.Close() }
98
+ }
@@ -0,0 +1,67 @@
1
+ param(
2
+ [switch]$DryRun
3
+ )
4
+
5
+ $ErrorActionPreference = 'Stop'
6
+
7
+ function Resolve-RepoRoot {
8
+ return (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
9
+ }
10
+
11
+ function Has-CodegraphIndex([string]$path) {
12
+ return (Test-Path (Join-Path $path '.codegraph'))
13
+ }
14
+
15
+ function Resolve-TargetPath {
16
+ param([string]$RepoRoot)
17
+
18
+ $envPath = [Environment]::GetEnvironmentVariable('CODEGRAPH_PROJECT_PATH')
19
+ if (-not [string]::IsNullOrWhiteSpace($envPath)) {
20
+ $resolvedEnvPath = $null
21
+ try {
22
+ $resolvedEnvPath = (Resolve-Path $envPath).Path
23
+ }
24
+ catch {
25
+ $resolvedEnvPath = $null
26
+ }
27
+
28
+ if ($resolvedEnvPath -and (Has-CodegraphIndex $resolvedEnvPath)) {
29
+ return $resolvedEnvPath
30
+ }
31
+ }
32
+
33
+ $projectCandidates = @()
34
+ $projectsRoot = Join-Path $RepoRoot 'projects'
35
+ if (Test-Path $projectsRoot) {
36
+ foreach ($projectDir in Get-ChildItem -Path $projectsRoot -Directory -ErrorAction SilentlyContinue) {
37
+ if (Has-CodegraphIndex $projectDir.FullName) {
38
+ $projectCandidates += $projectDir.FullName
39
+ }
40
+ }
41
+ }
42
+
43
+ if ($projectCandidates.Count -eq 1) {
44
+ return $projectCandidates[0]
45
+ }
46
+
47
+ if (Has-CodegraphIndex $RepoRoot) {
48
+ return $RepoRoot
49
+ }
50
+
51
+ if ($projectCandidates.Count -gt 1) {
52
+ return $projectCandidates[0]
53
+ }
54
+
55
+ return $RepoRoot
56
+ }
57
+
58
+ $repoRoot = Resolve-RepoRoot
59
+ $targetPath = Resolve-TargetPath -RepoRoot $repoRoot
60
+
61
+ if ($DryRun) {
62
+ Write-Output $targetPath
63
+ exit 0
64
+ }
65
+
66
+ & codegraph serve --mcp --path $targetPath
67
+ exit $LASTEXITCODE
@@ -0,0 +1,85 @@
1
+ param(
2
+ [switch]$DryRun
3
+ )
4
+
5
+ $ErrorActionPreference = 'Stop'
6
+
7
+ function Resolve-RepoRoot {
8
+ return (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
9
+ }
10
+
11
+ function Is-GitRepo([string]$path) {
12
+ return (Test-Path (Join-Path $path '.git'))
13
+ }
14
+
15
+ function Has-GitNexusIndex([string]$path) {
16
+ return (Test-Path (Join-Path $path '.gitnexus'))
17
+ }
18
+
19
+ function Resolve-TargetPath {
20
+ param([string]$RepoRoot)
21
+
22
+ $envPath = [Environment]::GetEnvironmentVariable('GITNEXUS_PROJECT_PATH')
23
+ if (-not [string]::IsNullOrWhiteSpace($envPath)) {
24
+ $resolvedEnvPath = $null
25
+ try {
26
+ $resolvedEnvPath = (Resolve-Path $envPath).Path
27
+ }
28
+ catch {
29
+ $resolvedEnvPath = $null
30
+ }
31
+
32
+ if ($resolvedEnvPath -and (Is-GitRepo $resolvedEnvPath)) {
33
+ return $resolvedEnvPath
34
+ }
35
+ }
36
+
37
+ $projectCandidates = @()
38
+ $indexedProjectCandidates = @()
39
+ $projectsRoot = Join-Path $RepoRoot 'projects'
40
+
41
+ if (Test-Path $projectsRoot) {
42
+ foreach ($projectDir in Get-ChildItem -Path $projectsRoot -Directory -ErrorAction SilentlyContinue) {
43
+ if (Is-GitRepo $projectDir.FullName) {
44
+ $projectCandidates += $projectDir.FullName
45
+ if (Has-GitNexusIndex $projectDir.FullName) {
46
+ $indexedProjectCandidates += $projectDir.FullName
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ if ($indexedProjectCandidates.Count -eq 1) {
53
+ return $indexedProjectCandidates[0]
54
+ }
55
+
56
+ if ((Has-GitNexusIndex $RepoRoot) -and (Is-GitRepo $RepoRoot)) {
57
+ return $RepoRoot
58
+ }
59
+
60
+ if ($indexedProjectCandidates.Count -gt 1) {
61
+ return $indexedProjectCandidates[0]
62
+ }
63
+
64
+ if ($projectCandidates.Count -eq 1) {
65
+ return $projectCandidates[0]
66
+ }
67
+
68
+ if ($projectCandidates.Count -gt 1) {
69
+ return $projectCandidates[0]
70
+ }
71
+
72
+ return $RepoRoot
73
+ }
74
+
75
+ $repoRoot = Resolve-RepoRoot
76
+ $targetPath = Resolve-TargetPath -RepoRoot $repoRoot
77
+
78
+ if ($DryRun) {
79
+ Write-Output $targetPath
80
+ exit 0
81
+ }
82
+
83
+ Set-Location $targetPath
84
+ & gitnexus mcp
85
+ exit $LASTEXITCODE
@@ -0,0 +1,53 @@
1
+ param(
2
+ [switch]$AllHosts,
3
+ [switch]$CurrentProcess
4
+ )
5
+
6
+ Set-StrictMode -Version Latest
7
+ $ErrorActionPreference = 'Stop'
8
+
9
+ function Remove-FromProfile {
10
+ param([string]$ProfilePath)
11
+
12
+ if (-not (Test-Path $ProfilePath)) {
13
+ return
14
+ }
15
+
16
+ $content = Get-Content -Path $ProfilePath -Raw -ErrorAction SilentlyContinue
17
+ if ($null -eq $content) {
18
+ return
19
+ }
20
+
21
+ if ($content -match 'MCPEE_TERMINAL_TELEMETRY_START') {
22
+ $updated = [regex]::Replace(
23
+ $content,
24
+ '(?s)\r?\n?# >>> MCPEE_TERMINAL_TELEMETRY_START >>>.*?# <<< MCPEE_TERMINAL_TELEMETRY_END <<<\r?\n?',
25
+ "`r`n"
26
+ )
27
+ Set-Content -Path $ProfilePath -Value $updated -Encoding UTF8
28
+ Write-Host "[ok] Hook eliminado de: $ProfilePath"
29
+ }
30
+ }
31
+
32
+ if ($CurrentProcess) {
33
+ Remove-Item Function:\global:prompt -ErrorAction SilentlyContinue
34
+ if (Test-Path Function:\global:MCPEE_ORIGINAL_PROMPT) {
35
+ Set-Item -Path Function:\global:prompt -Value ((Get-Command MCPEE_ORIGINAL_PROMPT -CommandType Function).ScriptBlock) -Force
36
+ Remove-Item Function:\global:MCPEE_ORIGINAL_PROMPT -ErrorAction SilentlyContinue
37
+ }
38
+
39
+ Remove-Item Function:\global:Invoke-McpeeTerminalTelemetry -ErrorAction SilentlyContinue
40
+ Remove-Item Function:\global:Get-McpeeTelemetryPythonCommand -ErrorAction SilentlyContinue
41
+ Remove-Variable MCPEE_TELEMETRY_REPO_ROOT -Scope Global -ErrorAction SilentlyContinue
42
+ Remove-Variable MCPEE_TELEMETRY_SESSION_ID -Scope Global -ErrorAction SilentlyContinue
43
+ Remove-Variable MCPEE_TELEMETRY_LAST_HISTORY_ID -Scope Global -ErrorAction SilentlyContinue
44
+ Remove-Variable MCPEE_TELEMETRY_LAST_STAMP -Scope Global -ErrorAction SilentlyContinue
45
+ Write-Host '[ok] Hook desactivado en la sesion actual.'
46
+ }
47
+
48
+ Remove-FromProfile -ProfilePath $PROFILE.CurrentUserCurrentHost
49
+ if ($AllHosts) {
50
+ Remove-FromProfile -ProfilePath $PROFILE.CurrentUserAllHosts
51
+ }
52
+
53
+ Write-Host '[ok] Telemetria global de terminal desactivada.'
@@ -143,6 +143,27 @@ function Ensure-PythonModuleInstalled {
143
143
  }
144
144
  }
145
145
 
146
+ function Sync-GraphifyArtifactsFromScripts {
147
+ $scriptsGraphPath = 'scripts/graphify-out/graph.json'
148
+ $contextGraphDir = 'context/graphify-out'
149
+ $contextGraphPath = Join-Path $contextGraphDir 'graph.json'
150
+
151
+ if (-not (Test-Path $scriptsGraphPath)) {
152
+ return $false
153
+ }
154
+
155
+ New-Item -ItemType Directory -Path $contextGraphDir -Force | Out-Null
156
+ Copy-Item -Path $scriptsGraphPath -Destination $contextGraphPath -Force
157
+
158
+ $scriptsManifestPath = 'scripts/graphify-out/manifest.json'
159
+ $contextManifestPath = Join-Path $contextGraphDir 'manifest.json'
160
+ if (Test-Path $scriptsManifestPath) {
161
+ Copy-Item -Path $scriptsManifestPath -Destination $contextManifestPath -Force
162
+ }
163
+
164
+ return $true
165
+ }
166
+
146
167
  Write-Host '== MCP platform prerequisites setup =='
147
168
 
148
169
  $toolingManifest = Get-ToolingManifest -Path $toolingManifestPath
@@ -212,8 +233,15 @@ if (-not $SkipGraphify) {
212
233
  }
213
234
 
214
235
  if (-not (Test-Path 'context/graphify-out/graph.json')) {
215
- Write-Host '[setup] graphify extract scripts --no-cluster --out context'
216
- & $pyCmd @pyArgs -m graphify extract scripts --no-cluster --out context
236
+ Write-Host '[setup] graphify update scripts --no-cluster'
237
+ & $pyCmd @pyArgs -m graphify update scripts --no-cluster
238
+ if ($LASTEXITCODE -ne 0) {
239
+ throw "graphify update failed with exit code $LASTEXITCODE"
240
+ }
241
+
242
+ if (Sync-GraphifyArtifactsFromScripts) {
243
+ Write-Host '[ok] graphify output synced from scripts/graphify-out to context/graphify-out'
244
+ }
217
245
  }
218
246
  else {
219
247
  Write-Host '[ok] context/graphify-out/graph.json already exists'
@@ -69,6 +69,24 @@ function Write-SetupValidationReport {
69
69
  $Report | ConvertTo-Json -Depth 20 | Set-Content -Path $Path -Encoding utf8
70
70
  }
71
71
 
72
+ function Sync-GraphifyArtifactsIfNeeded {
73
+ $contextGraphDir = Join-Path $repoRoot 'context/graphify-out'
74
+ $contextGraphPath = Join-Path $contextGraphDir 'graph.json'
75
+ $scriptsGraphDir = Join-Path $repoRoot 'scripts/graphify-out'
76
+ $scriptsGraphPath = Join-Path $scriptsGraphDir 'graph.json'
77
+
78
+ if ((-not (Test-Path $contextGraphPath)) -and (Test-Path $scriptsGraphPath)) {
79
+ New-Item -ItemType Directory -Path $contextGraphDir -Force | Out-Null
80
+ Copy-Item -Path $scriptsGraphPath -Destination $contextGraphPath -Force
81
+
82
+ $scriptsManifestPath = Join-Path $scriptsGraphDir 'manifest.json'
83
+ $contextManifestPath = Join-Path $contextGraphDir 'manifest.json'
84
+ if ((-not (Test-Path $contextManifestPath)) -and (Test-Path $scriptsManifestPath)) {
85
+ Copy-Item -Path $scriptsManifestPath -Destination $contextManifestPath -Force
86
+ }
87
+ }
88
+ }
89
+
72
90
  # Ensure report directory exists
73
91
  $reportDir = Split-Path -Parent $setupValidationReportPath
74
92
  if (-not (Test-Path $reportDir)) {
@@ -192,8 +210,9 @@ if ($toolingManifest -and ($toolingManifest.PSObject.Properties.Name -contains '
192
210
 
193
211
  try {
194
212
  $mcpRaw = Get-Content -Raw -Path '.vscode/mcp.json' | ConvertFrom-Json -Depth 20
195
- if ($mcpRaw.servers.gitnexus.command -ne 'gitnexus') {
196
- $errors += 'MCP gitnexus command should be local `gitnexus` (avoid npx startup latency).'
213
+ $gitnexusCommand = [string]$mcpRaw.servers.gitnexus.command
214
+ if ($gitnexusCommand -eq 'npx') {
215
+ $errors += 'MCP gitnexus command should avoid `npx` (startup latency). Use local `gitnexus` or local wrapper script.'
197
216
  }
198
217
  $scanLimit = 0
199
218
  $gitnexusHasEnv = ($mcpRaw.servers.gitnexus.PSObject.Properties.Name -contains 'env')
@@ -215,6 +234,8 @@ catch {
215
234
  $errors += "Unable to parse .vscode/mcp.json: $($_.Exception.Message)"
216
235
  }
217
236
 
237
+ Sync-GraphifyArtifactsIfNeeded
238
+
218
239
  $specFiles = @(
219
240
  "specs/architecture.spec.md",
220
241
  "specs/azure-rag.spec.md",
@@ -332,9 +353,9 @@ else {
332
353
  }
333
354
  }
334
355
 
335
- if (-not (Test-Path "context/graphify-out/graph.json")) {
356
+ if ((-not (Test-Path "context/graphify-out/graph.json")) -and (-not (Test-Path "scripts/graphify-out/graph.json"))) {
336
357
  if (-not ($CIMode)) {
337
- $errors += "Missing context/graphify-out/graph.json. Run: py -3.14 -m graphify extract scripts --no-cluster --out context"
358
+ $errors += "Missing graphify graph output. Run: py -3.14 -m graphify update scripts --no-cluster"
338
359
  }
339
360
  }
340
361