@enterpriseai/cli 3.15.9 → 3.15.11

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 (37) hide show
  1. package/README.md +45 -11
  2. package/dist/commands/classifier.d.ts.map +1 -1
  3. package/dist/commands/classifier.js +4 -1
  4. package/dist/commands/classifier.js.map +1 -1
  5. package/dist/commands/docs.d.ts.map +1 -1
  6. package/dist/commands/docs.js +88 -31
  7. package/dist/commands/docs.js.map +1 -1
  8. package/dist/commands/init.d.ts +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +15 -10
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/start.d.ts +15 -0
  13. package/dist/commands/start.d.ts.map +1 -1
  14. package/dist/commands/start.js +111 -29
  15. package/dist/commands/start.js.map +1 -1
  16. package/dist/lib/agent-guide.js +2 -2
  17. package/dist/lib/agent-guide.js.map +1 -1
  18. package/dist/lib/ai-surface-installer.d.ts +38 -0
  19. package/dist/lib/ai-surface-installer.d.ts.map +1 -0
  20. package/dist/lib/ai-surface-installer.js +160 -0
  21. package/dist/lib/ai-surface-installer.js.map +1 -0
  22. package/dist/lib/ai-surfaces.d.ts +257 -6
  23. package/dist/lib/ai-surfaces.d.ts.map +1 -1
  24. package/dist/lib/ai-surfaces.js +3182 -170
  25. package/dist/lib/ai-surfaces.js.map +1 -1
  26. package/dist/lib/api.d.ts +16 -2
  27. package/dist/lib/api.d.ts.map +1 -1
  28. package/dist/lib/api.js +43 -8
  29. package/dist/lib/api.js.map +1 -1
  30. package/dist/lib/gofer-installer.js +1 -1
  31. package/package.json +2 -2
  32. package/resources/gofer/.gofer-version +17 -1
  33. package/resources/gofer/bash-scripts/install-optional-tools.sh +357 -16
  34. package/resources/gofer/bash-scripts/sync-implementation-status.sh +3 -4
  35. package/resources/gofer/powershell-scripts/install-optional-tools.ps1 +419 -11
  36. package/resources/gofer/references/platform/eai-service-patterns.md +84 -1
  37. package/resources/gofer/templates/visuals/capability-heatmap-template.md +2 -2
@@ -5,6 +5,7 @@ param(
5
5
 
6
6
  $ErrorActionPreference = 'Stop'
7
7
  $HasFailure = $false
8
+ $LastStepSucceeded = $true
8
9
 
9
10
  function Write-Info {
10
11
  param([string]$Message)
@@ -16,6 +17,41 @@ function Write-Warn {
16
17
  Write-Warning "[gofer] $Message"
17
18
  }
18
19
 
20
+ function ConvertTo-SafeDiagnostic {
21
+ param([string]$Message)
22
+
23
+ if ([string]::IsNullOrEmpty($Message)) {
24
+ return 'No diagnostic details were provided.'
25
+ }
26
+
27
+ $safeMessage = $Message
28
+ foreach ($localRoot in @(
29
+ $WorkspacePath,
30
+ $HOME,
31
+ $env:USERPROFILE,
32
+ [System.IO.Path]::GetTempPath()
33
+ )) {
34
+ if (-not [string]::IsNullOrWhiteSpace($localRoot)) {
35
+ $safeMessage = [regex]::Replace(
36
+ $safeMessage,
37
+ [regex]::Escape($localRoot),
38
+ '<local-path>',
39
+ [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
40
+ )
41
+ }
42
+ }
43
+ $safeMessage = $safeMessage `
44
+ -replace '(?i)\b((?:api[-_ ]?key|token|secret|password|credential)\s*[:=]\s*)[^\s,;]+', '$1<redacted>' `
45
+ -replace '(?i)(https?://)[^\s/@:]+:[^\s/@]+@', '$1<redacted>@' `
46
+ -replace '(?i)\b[A-Z]:\\Users\\[^\\\s"'']+', '<home>' `
47
+ -replace '/(?:Users|home)/[^/\s"'']+', '<home>' `
48
+ -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', ''
49
+ if ($safeMessage.Length -gt 4096) {
50
+ return $safeMessage.Substring(0, 4096) + '...<truncated>'
51
+ }
52
+ return $safeMessage
53
+ }
54
+
19
55
  function Invoke-Step {
20
56
  param(
21
57
  [string]$Description,
@@ -23,12 +59,14 @@ function Invoke-Step {
23
59
  )
24
60
 
25
61
  Write-Info $Description
62
+ $script:LastStepSucceeded = $true
26
63
  try {
27
64
  & $ScriptBlock
28
65
  } catch {
66
+ $script:LastStepSucceeded = $false
29
67
  $script:HasFailure = $true
30
68
  Write-Warn "Failed: $Description"
31
- Write-Warn $_.Exception.Message
69
+ Write-Warn (ConvertTo-SafeDiagnostic $_.Exception.Message)
32
70
  }
33
71
  }
34
72
 
@@ -37,6 +75,36 @@ function Test-CommandExists {
37
75
  return $null -ne (Get-Command $CommandName -ErrorAction SilentlyContinue)
38
76
  }
39
77
 
78
+ function Stop-InstallerProcess {
79
+ param([System.Diagnostics.Process]$Process)
80
+
81
+ if ($null -eq $Process) { return }
82
+ try {
83
+ if (-not $Process.HasExited) {
84
+ try {
85
+ # Kill the complete process tree where the current runtime supports it.
86
+ $Process.Kill($true)
87
+ } catch {
88
+ # Windows PowerShell 5.1 exposes only Kill() without the tree overload.
89
+ # taskkill is the operating-system fallback for terminating descendants.
90
+ $taskkillPath = if ($env:SystemRoot) {
91
+ Join-Path $env:SystemRoot 'System32\taskkill.exe'
92
+ } else {
93
+ $null
94
+ }
95
+ if ($taskkillPath -and [System.IO.File]::Exists($taskkillPath)) {
96
+ & $taskkillPath /PID $Process.Id /T /F *> $null
97
+ } else {
98
+ $Process.Kill()
99
+ }
100
+ }
101
+ [void]$Process.WaitForExit(5000)
102
+ }
103
+ } catch {
104
+ # Best-effort cleanup must not hide the original interruption or failure.
105
+ }
106
+ }
107
+
40
108
  function Get-JsPackageManager {
41
109
  if (Test-Path (Join-Path $WorkspacePath 'bun.lockb')) {
42
110
  return 'bun'
@@ -109,6 +177,301 @@ function Install-NpmGlobalPackage {
109
177
  Invoke-Step "Installing $PackageName globally with npm" { npm install --global $PackageName }
110
178
  }
111
179
 
180
+ function Test-InstalledCli {
181
+ param(
182
+ [string]$ToolName,
183
+ [string[]]$ExpectedPaths
184
+ )
185
+
186
+ Invoke-Step "Validating the installed $ToolName executable path" {
187
+ foreach ($candidatePath in $ExpectedPaths) {
188
+ if ([string]::IsNullOrWhiteSpace($candidatePath) -or
189
+ -not [System.IO.Path]::IsPathRooted($candidatePath)) {
190
+ continue
191
+ }
192
+ $fullPath = [System.IO.Path]::GetFullPath($candidatePath)
193
+ if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
194
+ $item = Get-Item -LiteralPath $fullPath -Force
195
+ if ($item.Length -gt 0) {
196
+ Write-Info "$ToolName installer produced its provider-defined executable"
197
+ return
198
+ }
199
+ }
200
+ }
201
+ throw "$ToolName installation did not produce its expected executable."
202
+ }
203
+ }
204
+
205
+ function Install-PinnedCopilotCli {
206
+ $packageSpec = '@github/copilot@1.0.83'
207
+ $expectedIntegrity = 'sha512-M8uZI0V0dahYV1KZij3nGDxaXEGG7I7YUZzQPI7NEZkL/83Nl/tNTbPdxKtdWZbOmWoXsPKXty/eEYoj6RHDhA=='
208
+ $registry = 'https://registry.npmjs.org/'
209
+
210
+ $npmCommand = Get-Command -Name 'npm' -CommandType Application -All -ErrorAction SilentlyContinue |
211
+ Select-Object -First 1
212
+ if ($null -eq $npmCommand -or -not [System.IO.File]::Exists($npmCommand.Source)) {
213
+ $script:HasFailure = $true
214
+ Write-Warn "npm is required to install $packageSpec globally"
215
+ return
216
+ }
217
+
218
+ $script:CopilotCommandPaths = @()
219
+ Invoke-Step 'Verifying and installing the pinned GitHub Copilot CLI' {
220
+ $integrityOutput = (& $npmCommand.Source view $packageSpec dist.integrity --json --registry=$registry 2>$null | Out-String).Trim()
221
+ if ($LASTEXITCODE -ne 0) {
222
+ throw 'Could not resolve the pinned GitHub Copilot CLI package.'
223
+ }
224
+ $actualIntegrity = $integrityOutput.Trim('"')
225
+ if ($actualIntegrity -cne $expectedIntegrity) {
226
+ throw 'Refusing GitHub Copilot CLI because its npm integrity changed.'
227
+ }
228
+
229
+ & $npmCommand.Source install --global $packageSpec --registry=$registry `
230
+ --ignore-scripts --no-audit --no-fund *> $null
231
+ if ($LASTEXITCODE -ne 0) {
232
+ throw 'The pinned GitHub Copilot CLI installation failed.'
233
+ }
234
+
235
+ & $npmCommand.Source list --global --depth=0 $packageSpec *> $null
236
+ if ($LASTEXITCODE -ne 0) {
237
+ throw 'The installed GitHub Copilot CLI package did not match the pinned version.'
238
+ }
239
+
240
+ $npmPrefix = (& $npmCommand.Source prefix --global 2>$null | Out-String).Trim()
241
+ if ($LASTEXITCODE -ne 0 -or -not [System.IO.Path]::IsPathRooted($npmPrefix)) {
242
+ throw 'Could not resolve an absolute npm prefix for GitHub Copilot CLI.'
243
+ }
244
+ $script:CopilotCommandPaths = @(
245
+ (Join-Path $npmPrefix 'copilot.cmd'),
246
+ (Join-Path $npmPrefix 'copilot.exe'),
247
+ (Join-Path $npmPrefix 'bin\copilot')
248
+ )
249
+ }
250
+ if ($script:LastStepSucceeded) {
251
+ Test-InstalledCli -ToolName 'GitHub Copilot CLI' -ExpectedPaths $script:CopilotCommandPaths
252
+ }
253
+ }
254
+
255
+ function Get-OfficialInstallerPayload {
256
+ param(
257
+ [string]$ToolName,
258
+ [string]$ScriptUrl,
259
+ [string]$AllowedFinalOrigin
260
+ )
261
+
262
+ $initialUri = [System.Uri]$ScriptUrl
263
+ $finalOriginUri = [System.Uri]$AllowedFinalOrigin
264
+ if (-not $initialUri.IsAbsoluteUri -or $initialUri.Scheme -cne 'https' -or $initialUri.UserInfo) {
265
+ throw "$ToolName installer URL is not an absolute HTTPS URL."
266
+ }
267
+ if (-not $finalOriginUri.IsAbsoluteUri -or $finalOriginUri.Scheme -cne 'https' -or
268
+ $finalOriginUri.UserInfo -or $finalOriginUri.AbsolutePath -cne '/' -or
269
+ $finalOriginUri.Query -or $finalOriginUri.Fragment) {
270
+ throw "$ToolName final installer origin is invalid."
271
+ }
272
+
273
+ Add-Type -AssemblyName System.Net.Http -ErrorAction Stop
274
+ $handler = [System.Net.Http.HttpClientHandler]::new()
275
+ $handler.AllowAutoRedirect = $false
276
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
277
+ $httpClient.Timeout = [TimeSpan]::FromMinutes(2)
278
+ $httpClient.MaxResponseContentBufferSize = 1MB
279
+ $allowedOrigins = @(
280
+ $initialUri.GetLeftPart([System.UriPartial]::Authority),
281
+ $finalOriginUri.GetLeftPart([System.UriPartial]::Authority)
282
+ ) | Select-Object -Unique
283
+
284
+ $currentUri = $initialUri
285
+ $response = $null
286
+ try {
287
+ for ($redirectCount = 0; $redirectCount -le 5; $redirectCount += 1) {
288
+ $currentOrigin = $currentUri.GetLeftPart([System.UriPartial]::Authority)
289
+ if ($currentUri.Scheme -cne 'https' -or $currentUri.UserInfo -or
290
+ $allowedOrigins -cnotcontains $currentOrigin) {
291
+ throw "$ToolName installer redirected to an unapproved origin."
292
+ }
293
+
294
+ $request = [System.Net.Http.HttpRequestMessage]::new(
295
+ [System.Net.Http.HttpMethod]::Get,
296
+ $currentUri
297
+ )
298
+ try {
299
+ $response = $httpClient.SendAsync(
300
+ $request,
301
+ [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead
302
+ ).GetAwaiter().GetResult()
303
+ } finally {
304
+ $request.Dispose()
305
+ }
306
+
307
+ $statusCode = [int]$response.StatusCode
308
+ if ($statusCode -in @(301, 302, 303, 307, 308)) {
309
+ $redirectLocation = $response.Headers.Location
310
+ if ($null -eq $redirectLocation) {
311
+ throw "$ToolName installer redirect omitted its destination."
312
+ }
313
+ $nextUri = if ($redirectLocation.IsAbsoluteUri) {
314
+ $redirectLocation
315
+ } else {
316
+ [System.Uri]::new($currentUri, $redirectLocation)
317
+ }
318
+ $response.Dispose()
319
+ $response = $null
320
+ $currentUri = $nextUri
321
+ continue
322
+ }
323
+
324
+ [void]$response.EnsureSuccessStatusCode()
325
+ if ($currentOrigin -cne $finalOriginUri.GetLeftPart([System.UriPartial]::Authority)) {
326
+ throw "$ToolName installer did not finish at its approved provider origin."
327
+ }
328
+ if ($response.Content.Headers.ContentLength.HasValue -and
329
+ $response.Content.Headers.ContentLength.Value -gt 1MB) {
330
+ throw "$ToolName installer exceeded the allowed download size."
331
+ }
332
+ $contentStream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
333
+ $buffer = New-Object byte[] 8192
334
+ $memory = [System.IO.MemoryStream]::new()
335
+ try {
336
+ while (($bytesRead = $contentStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
337
+ if (($memory.Length + $bytesRead) -gt 1MB) {
338
+ throw "$ToolName installer exceeded the allowed download size."
339
+ }
340
+ $memory.Write($buffer, 0, $bytesRead)
341
+ }
342
+ $installerBytes = $memory.ToArray()
343
+ } finally {
344
+ $memory.Dispose()
345
+ $contentStream.Dispose()
346
+ }
347
+ if ($installerBytes.Length -eq 0) {
348
+ throw "$ToolName installer was empty."
349
+ }
350
+ return [pscustomobject]@{
351
+ Bytes = $installerBytes
352
+ EffectiveUrl = $currentUri.AbsoluteUri
353
+ }
354
+ }
355
+ throw "$ToolName installer exceeded the redirect limit."
356
+ } finally {
357
+ if ($null -ne $response) { $response.Dispose() }
358
+ $httpClient.Dispose()
359
+ $handler.Dispose()
360
+ }
361
+ }
362
+
363
+ function Install-FromOfficialPowerShellScript {
364
+ param(
365
+ [string]$ToolName,
366
+ [string]$ScriptUrl,
367
+ [string]$ExpectedSha256,
368
+ [string]$AllowedFinalOrigin
369
+ )
370
+
371
+ Invoke-Step "Installing or updating $ToolName from $ScriptUrl" {
372
+ $installerBytes = $null
373
+ $installerText = $null
374
+ try {
375
+ $payload = Get-OfficialInstallerPayload -ToolName $ToolName `
376
+ -ScriptUrl $ScriptUrl -AllowedFinalOrigin $AllowedFinalOrigin
377
+ $installerBytes = [byte[]]$payload.Bytes
378
+ $sha256 = [System.Security.Cryptography.SHA256]::Create()
379
+ try {
380
+ $actualSha256 = [System.BitConverter]::ToString(
381
+ $sha256.ComputeHash($installerBytes)
382
+ ).Replace('-', '').ToLowerInvariant()
383
+ } finally {
384
+ $sha256.Dispose()
385
+ }
386
+ if ($actualSha256 -cne $ExpectedSha256) {
387
+ throw "$ToolName installer SHA-256 digest changed."
388
+ }
389
+
390
+ $strictUtf8 = [System.Text.UTF8Encoding]::new($false, $true)
391
+ $installerText = $strictUtf8.GetString($installerBytes)
392
+ if ($installerText.Length -gt 0 -and $installerText[0] -eq [char]0xFEFF) {
393
+ $installerText = $installerText.Substring(1)
394
+ }
395
+
396
+ $powerShell = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
397
+ if (-not [System.IO.File]::Exists($powerShell)) {
398
+ throw 'Could not resolve the current PowerShell executable.'
399
+ }
400
+
401
+ $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
402
+ $startInfo.FileName = $powerShell
403
+ # Feed the verified bytes over a private pipe. No pathname exists for a
404
+ # second process to replace between digest verification and execution.
405
+ $startInfo.Arguments = '-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'
406
+ $startInfo.UseShellExecute = $false
407
+ $startInfo.CreateNoWindow = $true
408
+ $startInfo.RedirectStandardInput = $true
409
+ # Provider output is intentionally drained without relaying it. Even a
410
+ # verified installer can print local paths or account data.
411
+ $startInfo.RedirectStandardOutput = $true
412
+ $startInfo.RedirectStandardError = $true
413
+ $startInfo.EnvironmentVariables.Clear()
414
+ foreach ($environmentName in @(
415
+ 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'HOME',
416
+ 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA', 'PROGRAMDATA', 'PROGRAMFILES',
417
+ 'PROGRAMFILES(X86)', 'ProgramW6432', 'HOMEDRIVE', 'HOMEPATH', 'USERNAME', 'LANG',
418
+ 'OS', 'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'NUMBER_OF_PROCESSORS',
419
+ 'DOTNET_ROOT', 'DOTNET_ROOT_ARM64', 'DOTNET_ROOT_X64'
420
+ )) {
421
+ $environmentValue = [System.Environment]::GetEnvironmentVariable($environmentName)
422
+ if (-not [string]::IsNullOrEmpty($environmentValue)) {
423
+ $startInfo.EnvironmentVariables[$environmentName] = $environmentValue
424
+ }
425
+ }
426
+ $systemPath = @(
427
+ (Join-Path $env:SystemRoot 'System32'),
428
+ $env:SystemRoot,
429
+ (Split-Path -Parent $powerShell)
430
+ ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
431
+ $startInfo.EnvironmentVariables['PATH'] = $systemPath -join ';'
432
+ $startInfo.EnvironmentVariables['CODEX_NON_INTERACTIVE'] = '1'
433
+
434
+ $installerProcess = [System.Diagnostics.Process]::new()
435
+ $installerProcess.StartInfo = $startInfo
436
+ try {
437
+ if (-not $installerProcess.Start()) {
438
+ throw "Could not start the isolated $ToolName installer process."
439
+ }
440
+ $stdoutDrain = $installerProcess.StandardOutput.BaseStream.CopyToAsync([System.IO.Stream]::Null)
441
+ $stderrDrain = $installerProcess.StandardError.BaseStream.CopyToAsync([System.IO.Stream]::Null)
442
+ $stdinWrite = $installerProcess.StandardInput.WriteAsync($installerText)
443
+ if (-not $stdinWrite.Wait([TimeSpan]::FromSeconds(15))) {
444
+ throw "$ToolName installer input pipe timed out."
445
+ }
446
+ $installerProcess.StandardInput.Close()
447
+
448
+ $deadline = [DateTime]::UtcNow.AddMinutes(15)
449
+ while (-not $installerProcess.WaitForExit(250)) {
450
+ if ([DateTime]::UtcNow -ge $deadline) {
451
+ throw "$ToolName installer exceeded its bounded execution time."
452
+ }
453
+ }
454
+ if (-not [System.Threading.Tasks.Task]::WaitAll(
455
+ [System.Threading.Tasks.Task[]]@($stdoutDrain, $stderrDrain),
456
+ [TimeSpan]::FromSeconds(5)
457
+ )) {
458
+ throw "$ToolName installer output pipes did not close."
459
+ }
460
+ if ($installerProcess.ExitCode -ne 0) {
461
+ throw "$ToolName installer exited with code $($installerProcess.ExitCode)"
462
+ }
463
+ } finally {
464
+ Stop-InstallerProcess -Process $installerProcess
465
+ $installerProcess.Dispose()
466
+ }
467
+ } finally {
468
+ $installerText = $null
469
+ $installerBytes = $null
470
+ }
471
+ }
472
+ return $script:LastStepSucceeded
473
+ }
474
+
112
475
  function Install-GitHubCli {
113
476
  if (Test-CommandExists 'gh') {
114
477
  Write-Info 'GitHub CLI is already installed'
@@ -158,11 +521,23 @@ if ([string]::IsNullOrWhiteSpace($Tools)) {
158
521
  exit 0
159
522
  }
160
523
 
161
- $toolList = $Tools.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)
524
+ $toolList = @()
525
+ foreach ($rawTool in $Tools.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)) {
526
+ $normalizedTool = $rawTool.Trim().ToLowerInvariant()
527
+ if ($normalizedTool -in @('agy', 'gemini')) {
528
+ $normalizedTool = 'antigravity'
529
+ }
530
+ if ($normalizedTool -notmatch '^[a-z0-9-]+$') {
531
+ $normalizedTool = 'invalid-tool-id'
532
+ }
533
+ if ($normalizedTool -and $normalizedTool -notin $toolList) {
534
+ $toolList += $normalizedTool
535
+ }
536
+ }
162
537
  $packageManager = Get-JsPackageManager
163
538
 
164
- Write-Info "Workspace: $WorkspacePath"
165
- Write-Info "Selected tools: $Tools"
539
+ Write-Info 'Workspace selected'
540
+ Write-Info "Selected tools: $($toolList -join ',')"
166
541
 
167
542
  foreach ($tool in $toolList) {
168
543
  switch ($tool.Trim()) {
@@ -174,16 +549,48 @@ foreach ($tool in $toolList) {
174
549
  Install-PlaywrightBrowsers
175
550
  }
176
551
  'claude' {
177
- Install-NpmGlobalPackage -PackageName '@anthropic-ai/claude-code'
552
+ if (Install-FromOfficialPowerShellScript -ToolName 'Claude Code' `
553
+ -ScriptUrl 'https://claude.ai/install.ps1' `
554
+ -ExpectedSha256 'cd17c6b555f761d60373659824bf805e1510538226e4c7028e19d7494937a333' `
555
+ -AllowedFinalOrigin 'https://downloads.claude.ai') {
556
+ Test-InstalledCli -ToolName 'Claude Code' -ExpectedPaths @(
557
+ (Join-Path $env:USERPROFILE '.local\bin\claude.exe'),
558
+ (Join-Path $env:USERPROFILE '.local\bin\claude')
559
+ )
560
+ }
178
561
  }
179
562
  'codex' {
180
- Install-NpmGlobalPackage -PackageName '@openai/codex-cli'
563
+ if (Install-FromOfficialPowerShellScript -ToolName 'Codex CLI' `
564
+ -ScriptUrl 'https://chatgpt.com/codex/install.ps1' `
565
+ -ExpectedSha256 '391f247de2c70c7e99041979ec02dae7e76be27ac9cfc1dfe7c1eb21d48d8b97' `
566
+ -AllowedFinalOrigin 'https://releases.openai.com') {
567
+ Test-InstalledCli -ToolName 'Codex CLI' -ExpectedPaths @(
568
+ (Join-Path $env:LOCALAPPDATA 'Programs\OpenAI\Codex\bin\codex.exe')
569
+ )
570
+ }
181
571
  }
182
572
  'copilot' {
183
- Install-NpmGlobalPackage -PackageName '@github/copilot'
573
+ Install-PinnedCopilotCli
574
+ }
575
+ 'antigravity' {
576
+ if (Install-FromOfficialPowerShellScript -ToolName 'Antigravity CLI (agy)' `
577
+ -ScriptUrl 'https://antigravity.google/cli/install.ps1' `
578
+ -ExpectedSha256 '51c2cb4fada22ce0228da71b9506370383d6544bfebcec85fe7616a52b805344' `
579
+ -AllowedFinalOrigin 'https://antigravity.google') {
580
+ Test-InstalledCli -ToolName 'Antigravity CLI' -ExpectedPaths @(
581
+ (Join-Path $env:LOCALAPPDATA 'agy\bin\agy.exe')
582
+ )
583
+ }
184
584
  }
185
- 'gemini' {
186
- Install-NpmGlobalPackage -PackageName '@google/gemini-cli'
585
+ 'grok' {
586
+ if (Install-FromOfficialPowerShellScript -ToolName 'Grok Build' `
587
+ -ScriptUrl 'https://x.ai/cli/install.ps1' `
588
+ -ExpectedSha256 '3a4ee2b1d744252c00827abbdeb2589f6b3dae80e73d88e0a81e08dc0ee747e7' `
589
+ -AllowedFinalOrigin 'https://x.ai') {
590
+ Test-InstalledCli -ToolName 'Grok Build' -ExpectedPaths @(
591
+ (Join-Path $env:USERPROFILE '.grok\bin\grok.exe')
592
+ )
593
+ }
187
594
  }
188
595
  'gh' {
189
596
  Install-GitHubCli
@@ -199,10 +606,11 @@ foreach ($tool in $toolList) {
199
606
  }
200
607
 
201
608
  Write-Info 'Suggested next steps:'
202
- Write-Info ' claude login'
609
+ Write-Info ' claude auth login'
203
610
  Write-Info ' codex login'
204
611
  Write-Info ' copilot login'
205
- Write-Info ' gemini auth login'
612
+ Write-Info ' agy'
613
+ Write-Info ' grok'
206
614
  Write-Info ' gh auth login'
207
615
  Write-Info ' az login'
208
616
 
@@ -25,10 +25,93 @@ patterns in `eai-app-template/docs/platform/eai-service-patterns.md`.
25
25
  | Resource actions | `client.resources.executeAction(type, id, action)` | named resources command if available; otherwise `eai publicapi post /v4/data/resources/...` | Actions enforce object-type rules. |
26
26
  | Resource search | local helper around `/v4/data/resources/{tenant}/search` if SDK support is absent | `eai resources storage doctor --format json`, then `eai resources search "query" --fulltext`; use `--hybrid` or `--vector` only when doctor reports those modes ready | V4 passive ResourceAPI search is a projection over canonical data. Fulltext can be usable before semantic search modes are ready. |
27
27
  | Resource files | local helper around resource file routes | `eai resources file upload/get/delete` | Use when the file is attached to a typed ResourceAPI object property. |
28
- | Documents | `useDocuments().upload/classify/ragIndex` | `eai docs upload`, `eai docs classify`, `eai docs index` | Use when the file should be processed, classified, indexed, or exposed to AI/RAG context. |
28
+ | Documents | One `useDocuments().upload(file, context)` OR `classify([file], context)` | One `eai docs upload` OR `eai docs classify`, with authorized context | Queued ResourceAPI upload; follow the document lifecycle rules below. |
29
29
  | Chat | `useChat(workflowId, stage).send/stream` | `eai chat send`, `eai chat stream` | Use v4 chat shape with `message`, `conversation_id`, and `params`. |
30
30
  | Advanced PublicAPI | BFF/server helper | `eai publicapi <method> /v4/...` | Use only when named SDK/CLI support is missing. |
31
31
 
32
+ ## Document Lifecycle Rules
33
+
34
+ The #3453 standalone lifecycle is a candidate, not deployed capability or live
35
+ acceptance evidence. Before generating or enabling an upload, verify the
36
+ installed CLI/SDK supports the context contract and the target runtime supports
37
+ the configured lifecycle. Provision the business document/analysis schemas and
38
+ Admin Portal lifecycle binding first. Storage readiness and classifier
39
+ readiness are separate checks; a published classifier alone is not enough.
40
+
41
+ - Use one `POST /v4/data/documents/upload` for durable upload and queued
42
+ classification, with `storage_target=resourceapi`. Browser callers use the
43
+ local BFF `/api/eai/v4/data/documents/upload`; tokens stay server-side.
44
+ - For standalone documents, send both `verticalKey` and `workflowKey`. PublicAPI
45
+ validates the authorised app/workflow and resolves optional
46
+ `config.documentLifecycle` on the existing classifier-target
47
+ `vertical-product-config` binding, never from the upload body.
48
+ - Allowed binding values are `planning-assist-v1`, `planning-assess-v1`, and
49
+ `business-document-v1`. Omission preserves existing behaviour and does not
50
+ erase a saved selection on reassociation. Unsupported values fail validation.
51
+ Missing planning fields do not imply business mode.
52
+ - Preserve working DAISY/Assess requests, planning/case relationships, rules,
53
+ stored records and in-flight jobs. Retain real authorised
54
+ `planning_application_id`, `business_request_id` and applicable
55
+ `assess_case_id` context (SDK: `planningApplicationId`, `businessRequestId`,
56
+ `assessCaseId`). Never fabricate a planning application, business request,
57
+ case or form submission for a standalone document. Optional real parents
58
+ must be supported by the selected lifecycle and authorised.
59
+ - Call `classify([file], context)` for `processing_mode=classification`, OR
60
+ `upload(file, context)` for full requested processing. Do not upload and then
61
+ classify the same bytes again. The SDK supplies `storage_target=resourceapi`.
62
+ Do not generate context-free legacy file classification or a fallback to it.
63
+ - Do not send lifecycle mappings, target collections, permissions, provider
64
+ credentials or worker pins from the client. Missing schema, storage,
65
+ service, app/workflow or classifier readiness must fail before upload-side
66
+ writes, not fall back to legacy storage or an unscoped classifier.
67
+ - Retain the returned job/document IDs. Poll
68
+ `GET /v4/data/documents/jobs/{job_id}` with authorised context and read back
69
+ saved requested stages and provenance. Acknowledgement and provider success
70
+ are not completed classification, extraction, rule validation or persistence.
71
+ A polling timeout is incomplete; never re-upload automatically.
72
+ - Indexing is an optional derived stage only where supported and requested.
73
+ Use authorised lifecycle file retrieval and cleanup. Invalidate stale work
74
+ and clean only owned outputs according to retention, never shared parents.
75
+ - Direct `POST /v4/data/documents/classify-by-url` is analysis, not a durable
76
+ queued upload/save/readback/cleanup replacement. New workflow-selected URL
77
+ callers also supply the app/workflow pair. Preserve established unscoped
78
+ DAISY/Assess classifier behaviour without making it a new business-app default.
79
+
80
+ Candidate example after provisioning and version checks, submitted once:
81
+
82
+ ```tsx
83
+ const { classify } = useDocuments(tenantId);
84
+ const response = await classify([file], {
85
+ verticalKey: appKey,
86
+ workflowKey,
87
+ });
88
+ if (!response.ok) throw new Error("Document submission failed.");
89
+ const accepted = await response.json();
90
+ ```
91
+
92
+ Retain the returned IDs and implement bounded status polling plus saved-result
93
+ readback before reporting success. The equivalent CLI submission is:
94
+
95
+ ```bash
96
+ eai docs classify ./document.pdf --tenant-id <tenant-id> \
97
+ --storage-target resourceapi --vertical-key <app-key> --workflow-key <workflow-key>
98
+ ```
99
+
100
+ An authorised administrator selects the binding with
101
+ `eai classifier target <classifier-key> --app <app-key> --workflow <workflow-key>
102
+ --document-lifecycle business-document-v1` after schema/storage and published
103
+ classifier readiness checks. Verify this syntax with the installed command's
104
+ `--help`. `--document-lifecycle` is a target-administration option, not an upload
105
+ option.
106
+
107
+ Scope is document-use migration, not all v3 retirement. Existing-record
108
+ read/download/delete/status, old queued callbacks, reference files and real
109
+ form attachments remain compatibility obligations. Replacement, client and
110
+ tenant-setup proof must precede enforcement against approved new legacy
111
+ admissions. Track package/plugin/docs publication and installed adoption
112
+ separately from local source or generator checks; do not claim live acceptance.
113
+
114
+
32
115
  ## Storage Backend Rules
33
116
 
34
117
  Keep Object Type identifiers in their correct layer. Configuration/model
@@ -30,9 +30,9 @@ quadrantChart
30
30
  x-axis Low Maturity --> High Maturity
31
31
  y-axis Low Value --> High Value
32
32
  quadrant-1 Strategic Investment
33
- quadrant-2 Optimize
33
+ quadrant-2 Quick Wins
34
34
  quadrant-3 Deprecate
35
- quadrant-4 Quick Wins
35
+ quadrant-4 Optimize
36
36
  {{CAPABILITY_1}}: [{{X1}}, {{Y1}}]
37
37
  {{CAPABILITY_2}}: [{{X2}}, {{Y2}}]
38
38
  {{CAPABILITY_3}}: [{{X3}}, {{Y3}}]