heroku 10.4.0 → 10.4.1-alpha.4

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.
@@ -0,0 +1,619 @@
1
+ // do not use the older node:readline module
2
+ // else things will break
3
+ const readline = require('node:readline/promises')
4
+ const yargs = require('yargs-parser')
5
+ const util = require('util')
6
+ const path = require('node:path')
7
+ const fs = require('node:fs')
8
+ const {ux} = require('@oclif/core')
9
+
10
+ const historyFile = path.join(process.env.HOME || process.env.USERPROFILE, '.heroku_repl_history')
11
+ const stateFile = path.join(process.env.HOME || process.env.USERPROFILE, '.heroku_repl_state')
12
+
13
+ const maxHistory = 1000
14
+ const mcpMode = process.env.HEROKU_MCP_MODE === 'true'
15
+ /**
16
+ * Map of commands used to provide completion
17
+ * data. The key is the flag or arg name to
18
+ * get data for and the value is an array containing
19
+ * the command name and an array of arguments to
20
+ * pass to the command if needed.
21
+ *
22
+ * @example
23
+ * heroku > pipelines:create --app <tab><tab>
24
+ * heroku > spaces:create --team <tab><tab>
25
+ */
26
+ const completionCommandByName = new Map([
27
+ ['app', ['apps', ['--all', '--json']]],
28
+ ['org', ['orgs', ['--json']]],
29
+ ['team', ['teams', ['--json']]],
30
+ ['space', ['spaces', ['--json']]],
31
+ ['pipeline', ['pipelines', ['--json']]],
32
+ ['addon', ['addons', ['--json']]],
33
+ ['domain', ['domains', ['--json']]],
34
+ ['dyno', ['ps', ['--json']]],
35
+ ['release', ['releases', ['--json']]],
36
+ ['stack', ['apps:stacks', ['--json']]],
37
+ ])
38
+
39
+ /**
40
+ * Map of completion data by flag or arg name.
41
+ * This is used as a cache for completion data
42
+ * that is retrieved from a remote source.
43
+ *
44
+ * No attempt is made to invalidate these caches
45
+ * at runtime but they are not preserved between
46
+ * sessions.
47
+ */
48
+ const completionResultsByName = new Map()
49
+
50
+ class HerokuRepl {
51
+ /**
52
+ * The OClif config object containing
53
+ * the command metadata and the means
54
+ * to execute commands
55
+ */
56
+ #config
57
+
58
+ /**
59
+ * A map of key/value pairs used for
60
+ * the 'set' and 'unset' command
61
+ */
62
+ #setValues = new Map()
63
+
64
+ /**
65
+ * The history of the REPL commands used
66
+ */
67
+ #history = []
68
+
69
+ /**
70
+ * The write stream for the history file
71
+ */
72
+ #historyStream
73
+
74
+ /**
75
+ * The readline interface used for the REPL
76
+ */
77
+ #rl = readline.createInterface({
78
+ input: process.stdin,
79
+ output: process.stdout,
80
+ prompt: 'heroku > ',
81
+ removeHistoryDuplicates: true,
82
+ historySize: maxHistory,
83
+ completer: async line => {
84
+ if (mcpMode) {
85
+ return [[], line]
86
+ }
87
+
88
+ const [command, ...parts] = line.split(' ')
89
+ if (command === 'set') {
90
+ return this.#buildSetCompletions(parts)
91
+ }
92
+
93
+ const commandMeta = this.#config.findCommand(command)
94
+ if (!commandMeta) {
95
+ const matches = this.#config.commands.filter(({id}) => id.startsWith(command))
96
+ return [matches.map(({id}) => id).sort(), line]
97
+ }
98
+
99
+ return this.#buildCompletions(commandMeta, parts)
100
+ },
101
+ })
102
+
103
+ /**
104
+ * Constructs a new instance of the HerokuRepl class.
105
+ *
106
+ * @param {Config} config The oclif core config object
107
+ */
108
+ constructor(config) {
109
+ if (!mcpMode) {
110
+ this.#prepareHistory()
111
+ this.#loadState()
112
+ }
113
+
114
+ this.#config = config
115
+ }
116
+
117
+ /**
118
+ * Prepares the REPL history by loading
119
+ * the previous history from the history file
120
+ * and opening a write stream for new entries.
121
+ *
122
+ * @returns {Promise<void>} a promise that resolves when the history has been loaded
123
+ */
124
+ #prepareHistory() {
125
+ this.#historyStream = fs.createWriteStream(historyFile, {
126
+ flags: 'a',
127
+ encoding: 'utf8',
128
+ })
129
+
130
+ // Load existing history first
131
+ if (fs.existsSync(historyFile)) {
132
+ this.#history = fs.readFileSync(historyFile, 'utf8')
133
+ .split('\n')
134
+ .filter(line => line.trim())
135
+ .reverse()
136
+ .splice(0, maxHistory)
137
+
138
+ this.#rl.history.push(...this.#history)
139
+ this.#rl.history
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Loads the previous session state from the state file.
145
+ * @returns {void}
146
+ */
147
+ #loadState() {
148
+ if (fs.existsSync(stateFile)) {
149
+ try {
150
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'))
151
+ for (const entry of Object.entries(state)) {
152
+ this.#updateFlagsByName('set', entry, true)
153
+ }
154
+
155
+ process.stdout.write('session restored')
156
+ } catch {
157
+ // noop
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Waits for the REPL to finish and then
164
+ * writes the current session state to the state file.
165
+ *
166
+ * @returns {Promise<void>} a promise that resolves when the REPL is done
167
+ */
168
+ async done() {
169
+ await new Promise(resolve => {
170
+ this.#rl.once('close', () => {
171
+ this.#historyStream?.close()
172
+ fs.writeFileSync(stateFile, JSON.stringify(Object.fromEntries(this.#setValues)), 'utf8')
173
+ resolve()
174
+ })
175
+ })
176
+ }
177
+
178
+ /**
179
+ * Process a line of input.
180
+ * This method will parse the input
181
+ * and run the command if it is valid.
182
+ * If the command is invalid, an error
183
+ * message will be displayed.
184
+ *
185
+ * @param {string} line the line to process
186
+ * @returns void
187
+ */
188
+ start() {
189
+ this.#rl.on('line', this.#processLine)
190
+ this.#rl.prompt()
191
+ }
192
+
193
+ /**
194
+ * Processes the line received from the terminal stdin
195
+ *
196
+ * @param {string} input the line to process
197
+ * @returns {Promise<void>} a promise that resolves when the command has been executed
198
+ */
199
+ #processLine = async input => {
200
+ this.#history.push(input)
201
+ this.#historyStream?.write(input + '\n')
202
+
203
+ const {_: [command, ...positionalArgs], ...flags} = yargs(input, {
204
+ configuration: {
205
+ 'camel-case-expansion': false,
206
+ 'boolean-negation': false,
207
+ },
208
+ })
209
+ const args = Object.entries(flags).flatMap(([key, value]) => {
210
+ if (typeof value === 'string') {
211
+ return [`--${key}`, value]
212
+ }
213
+
214
+ return [`--${key}`]
215
+ }).concat(positionalArgs)
216
+
217
+ if (command === 'exit') {
218
+ process.exit(0)
219
+ }
220
+
221
+ if (command === 'history') {
222
+ process.stdout.write(this.#history.join('\n'))
223
+ this.#rl.prompt()
224
+ return
225
+ }
226
+
227
+ if (command === 'set' || command === 'unset') {
228
+ this.#updateFlagsByName(command, args)
229
+ this.#rl.prompt()
230
+ return
231
+ }
232
+
233
+ const cmd = this.#config.findCommand(command)
234
+
235
+ if (!cmd) {
236
+ console.error(`"${command}" is not a valid command`)
237
+ this.#rl.prompt()
238
+ return
239
+ }
240
+
241
+ try {
242
+ const {flags} = cmd
243
+ for (const [key, value] of this.#setValues) {
244
+ if (Reflect.has(flags, key)) {
245
+ args.push(`--${key}`, value)
246
+ }
247
+ }
248
+
249
+ // Any commands that prompt the user will cause
250
+ // the REPL to enter an invalid state. We need
251
+ // to pause the readline interface and restore
252
+ // it when the command is done.
253
+ if (process.stdin.isTTY) {
254
+ process.stdin.setRawMode(false)
255
+ }
256
+
257
+ this.#rl.pause()
258
+ this.#rl.off('line', this.#processLine)
259
+ if (mcpMode) {
260
+ process.stdout.write('<<<BEGIN RESULTS>>>\n')
261
+ }
262
+
263
+ await this.#config.runCommand(command, args.filter(Boolean))
264
+ } catch (error) {
265
+ if (mcpMode) {
266
+ process.stderr.write(`<<<ERROR>>>\n${error.message}\n<<<END ERROR>>>\n`)
267
+ } else {
268
+ console.error(error.message)
269
+ }
270
+ } finally {
271
+ if (process.stdin.isTTY) {
272
+ process.stdin.setRawMode(true)
273
+ }
274
+
275
+ if (mcpMode) {
276
+ process.stdout.write('<<<END RESULTS>>>\n')
277
+ }
278
+
279
+ this.#rl.resume()
280
+ this.#rl.on('line', this.#processLine)
281
+ // Force readline to refresh the current line
282
+ this.#rl.write(null, {ctrl: true, name: 'u'})
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Updates the session state based on the command and args.
288
+ *
289
+ * @param {'set'|'unset'} command either 'set' or 'unset'
290
+ * @param {string[]} args an array of arg names
291
+ * @param {boolean} omitConfirmation when false. no confirmation is printed to stdout
292
+ * @returns {void}
293
+ */
294
+ #updateFlagsByName(command, args, omitConfirmation) {
295
+ if (command === 'set') {
296
+ const [key, value] = args
297
+ if (key && value) {
298
+ this.#setValues.set(key, value)
299
+
300
+ if (!omitConfirmation) {
301
+ process.stdout.write(`setting --${key} to ${value}\n`)
302
+ }
303
+
304
+ if (key === 'app') {
305
+ this.#rl.setPrompt(`${value} > `)
306
+ }
307
+ } else {
308
+ const values = []
309
+ for (const [flag, value] of this.#setValues) {
310
+ values.push({flag, value})
311
+ }
312
+
313
+ if (values.length === 0) {
314
+ return console.info('no flags set')
315
+ }
316
+
317
+ ux.table(values, {
318
+ flag: {header: 'Flag'},
319
+ value: {header: 'Value'},
320
+ })
321
+ }
322
+ }
323
+
324
+ if (command === 'unset') {
325
+ const [key] = args
326
+
327
+ if (!omitConfirmation) {
328
+ process.stdout.write(`unsetting --${key}\n`)
329
+ }
330
+
331
+ this.#setValues.delete(key)
332
+ if (key === 'app') {
333
+ this.#rl.setPrompt('heroku > ')
334
+ }
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Build completions for a command.
340
+ * The completions are based on the
341
+ * metadata for the command and the
342
+ * user input.
343
+ *
344
+ * @param {Record<string, unknown>} commandMeta the metadata for the command
345
+ * @param {string[]} flagsOrArgs the flags or args for the command
346
+ * @returns {Promise<[string[], string]>} the completions and the current input
347
+ */
348
+ async #buildCompletions(commandMeta, flagsOrArgs = []) {
349
+ const {args, flags} = commandMeta
350
+ const {requiredInputs: requiredFlags, optionalInputs: optionalFlags} = this.#collectInputsFromManifest(flags)
351
+ const {requiredInputs: requiredArgs, optionalInputs: optionalArgs} = this.#collectInputsFromManifest(args)
352
+
353
+ const {_: userArgs, ...userFlags} = yargs(flagsOrArgs, {
354
+ configuration: {
355
+ 'camel-case-expansion': false,
356
+ 'boolean-negation': false,
357
+ },
358
+ })
359
+ const current = flagsOrArgs[flagsOrArgs.length - 1] ?? ''
360
+
361
+ // Order of precedence:
362
+ // 1. Required flags
363
+ // 2. Required args
364
+ // 3. Optional flags
365
+ // 4. Optional args
366
+ // 5. End of line
367
+ // Flags *must* occur first since they may influence
368
+ // the completions for args.
369
+ return await this.#getCompletionsForFlag(current, requiredFlags, userFlags, commandMeta) ||
370
+ await this.#getCompletionsForArg(current, requiredArgs, userArgs, commandMeta) ||
371
+ await this.#getCompletionsForFlag(current, optionalFlags, userFlags, commandMeta) ||
372
+ await this.#getCompletionsForArg(current, optionalArgs, userArgs, commandMeta) ||
373
+ this.#getCompletionsForEndOfLine(flags, userFlags)
374
+ }
375
+
376
+ /**
377
+ * Get completions for a command.
378
+ * The completions are based on the
379
+ * metadata for the command and the
380
+ * user input.
381
+ *
382
+ * @param {[string, string]} parts the parts for a line to get completions for
383
+ * @returns {[string[], string]} the completions and the current input
384
+ */
385
+ async #buildSetCompletions(parts) {
386
+ const [name, current] = parts
387
+ if (parts.length > 0 && completionCommandByName.has(name)) {
388
+ return [await this.#getCompletion(name, current), current]
389
+ }
390
+
391
+ // Critical to completions operating as expected;
392
+ // the completions must be filtered to omit keys
393
+ // that do not match our name (if a name exists).
394
+ const completions = [...completionCommandByName.keys()]
395
+ .filter(c => !name || c.startsWith(name))
396
+
397
+ return [completions, name ?? current]
398
+ }
399
+
400
+ /**
401
+ * Get completions for the end of the line.
402
+ *
403
+ * @param {Record<string, unknown>} flags the flags for the command
404
+ * @param {Record<string, unknown>} userFlags the flags that have already been used
405
+ * @returns {[string[], string]} the completions and the current input
406
+ */
407
+ #getCompletionsForEndOfLine(flags, userFlags) {
408
+ const flagKeys = Object.keys(userFlags)
409
+ // If there are no more flags to complete,
410
+ // return an empty array.
411
+ return flagKeys.length < Object.keys(flags).length ? [[' --'], ''] : [[], '']
412
+ }
413
+
414
+ /**
415
+ * Get completions for a flag or flag value.
416
+ *
417
+ * @param {string} current the current input
418
+ * @param {string[]} flags the flags for the command
419
+ * @param {string[]} userFlags the flags that have already been used
420
+ * @param {Record<string, unknown>} commandMeta the metadata for the command
421
+ * @return {Promise<[string[], string]>} the completions and the current input
422
+ */
423
+ async #getCompletionsForFlag(current, flags, userFlags, commandMeta) {
424
+ // flag completion for long and short flags.
425
+ // flags that have already been used are
426
+ // not included in the completions.
427
+ const isFlag = current.startsWith('-')
428
+ if (isFlag) {
429
+ const isLongFlag = current.startsWith('--')
430
+ const rawFlag = isLongFlag ? current.slice(2) : current.slice(1)
431
+ const matched = flags
432
+ .map(f => isLongFlag ? f.long : f.short)
433
+ .filter(flag => !Reflect.has(userFlags, flag) && (!rawFlag || flag.startsWith(rawFlag)))
434
+
435
+ if (matched.length > 0) {
436
+ return [matched, rawFlag]
437
+ }
438
+ }
439
+
440
+ // Does the flag have a value?
441
+ const flagKeys = Object.keys(userFlags)
442
+ const flag = flagKeys[flagKeys.length - 1]
443
+ if (!flag || !current) {
444
+ return null
445
+ }
446
+
447
+ const {options, type} = commandMeta.flags[flag] ?? {}
448
+ // Options are defined in the metadata
449
+ // for the command. If the flag has options
450
+ // defined, we will attempt to complete
451
+ // based on the options.
452
+ if (type === 'option') {
453
+ if (options?.length > 0) {
454
+ const optionComplete = options.includes(current)
455
+ const matched = options.filter(o => o.startsWith(current))
456
+
457
+ if (!optionComplete) {
458
+ return matched.length > 0 ? [matched, current] : [options, current]
459
+ }
460
+ }
461
+
462
+ return [await this.#getCompletion(flag, isFlag ? '' : current), current]
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Get completions for a flag.
468
+ *
469
+ * @param {string} flag the flag to get the completion for
470
+ * @param {string} startsWith the string to match against
471
+ * @returns {Promise<[string[]]>} the completions
472
+ */
473
+ async #getCompletion(flag, startsWith) {
474
+ // attempt to retrieve the options from the
475
+ // Heroku API. If the options have already
476
+ // been retrieved, they will be cached.
477
+ if (completionCommandByName.has(flag)) {
478
+ let result
479
+ if (completionResultsByName.has(flag)) {
480
+ result = completionResultsByName.get(flag)
481
+ }
482
+
483
+ if (!result || result.length === 0) {
484
+ const [command, args] = completionCommandByName.get(flag)
485
+ const completionsStr = await this.#captureStdout(() => this.#config.runCommand(command, args)) ?? '[]'
486
+ result = JSON.parse(util.stripVTControlCharacters(completionsStr))
487
+ completionResultsByName.set(flag, result)
488
+ }
489
+
490
+ const matched = result
491
+ .map(obj => obj.name ?? obj.id)
492
+ .filter(name => !startsWith || name.startsWith(startsWith))
493
+ .sort()
494
+
495
+ return matched
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Capture stdout by deflecting it to a
501
+ * trap function and returning the output.
502
+ *
503
+ * This is useful for silently capturing the output
504
+ * of a command that normally prints to stdout.
505
+ *
506
+ * @param {CallableFunction} fn the function to capture stdout for
507
+ * @returns {Promise<string>} the output from stdout
508
+ */
509
+ async #captureStdout(fn) {
510
+ const output = []
511
+ const originalWrite = process.stdout.write
512
+ // Replace stdout.write temporarily
513
+ process.stdout.write = chunk => {
514
+ output.push(typeof chunk === 'string' ? chunk : chunk.toString())
515
+ return true
516
+ }
517
+
518
+ try {
519
+ await fn()
520
+ return output.join('')
521
+ } finally {
522
+ // Restore original stdout
523
+ process.stdout.write = originalWrite
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Get completions for an arg.
529
+ *
530
+ * @param {string} current the current input
531
+ * @param {({long: string}[])} args the args for the command
532
+ * @param {string[]} userArgs the args that have already been used
533
+ * @returns {Promise<[string[], string] | null>} the completions and the current input
534
+ */
535
+ async #getCompletionsForArg(current, args = [], userArgs = []) {
536
+ if (userArgs.length <= args.length) {
537
+ const arg = args[userArgs.length - 1]
538
+ if (arg) {
539
+ const {long} = arg
540
+ if (completionCommandByName.has(long)) {
541
+ const completions = await this.#getCompletion(long, current)
542
+ if (completions.length > 0) {
543
+ return [completions, current]
544
+ }
545
+ }
546
+
547
+ return [[`<${long}>`], current]
548
+ }
549
+ }
550
+
551
+ return null
552
+ }
553
+
554
+ /**
555
+ * Collect inputs from the command manifest and sorts
556
+ * them by type and then by required status.
557
+ *
558
+ * @param {Record<string, unknown>} commandMeta the metadata from the command manifest
559
+ * @returns {{requiredInputs: {long: string, short: string}[], optionalInputs: {long: string, short: string}[]}} the inputs from the command manifest
560
+ */
561
+ #collectInputsFromManifest(commandMeta) {
562
+ const requiredInputs = []
563
+ const optionalInputs = []
564
+
565
+ // Prioritize options over booleans
566
+ const keysByType = Object.keys(commandMeta).sort((a, b) => {
567
+ const {type: aType} = commandMeta[a]
568
+ const {type: bType} = commandMeta[b]
569
+ if (aType === bType) {
570
+ return 0
571
+ }
572
+
573
+ if (aType === 'option') {
574
+ return -1
575
+ }
576
+
577
+ if (bType === 'option') {
578
+ return 1
579
+ }
580
+
581
+ return 0
582
+ })
583
+
584
+ keysByType.forEach(long => {
585
+ const {required: isRequired, char: short} = commandMeta[long]
586
+ if (isRequired) {
587
+ requiredInputs.push({long, short})
588
+ return
589
+ }
590
+
591
+ optionalInputs.push({long, short})
592
+ })
593
+ // Prioritize required inputs
594
+ // over optional inputs
595
+ // required inputs are sorted
596
+ // alphabetically. optional
597
+ // inputs are sorted alphabetically
598
+ // and then pushed to the end of
599
+ // the list.
600
+ requiredInputs.sort((a, b) => {
601
+ if (a.long < b.long) {
602
+ return -1
603
+ }
604
+
605
+ if (a.long > b.long) {
606
+ return 1
607
+ }
608
+
609
+ return 0
610
+ })
611
+
612
+ return {requiredInputs, optionalInputs}
613
+ }
614
+ }
615
+ module.exports.herokuRepl = async function (config) {
616
+ const repl = new HerokuRepl(config)
617
+ repl.start()
618
+ return repl.done()
619
+ }
package/bin/run CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const {Config} = require('@oclif/core')
4
+ const root = require.resolve('../package.json')
5
+ const config = new Config({root})
6
+
3
7
  process.env.HEROKU_UPDATE_INSTRUCTIONS = process.env.HEROKU_UPDATE_INSTRUCTIONS || 'update with: "npm update -g heroku"'
4
8
 
5
9
  const now = new Date()
6
10
  const cliStartTime = now.getTime()
7
11
  const globalTelemetry = require('../lib/global_telemetry')
12
+ const yargs = require('yargs-parser')(process.argv.slice(2))
8
13
 
9
14
  process.once('beforeExit', async code => {
10
15
  // capture as successful exit
@@ -38,13 +43,37 @@ process.on('SIGTERM', async () => {
38
43
  globalTelemetry.initializeInstrumentation()
39
44
 
40
45
  const oclif = require('@oclif/core')
46
+ const oclifFlush = require('@oclif/core/flush')
47
+ const oclifError = require('@oclif/core/handle')
48
+ const {promptUser} = require('./heroku-prompts')
49
+ const {herokuRepl} = require('./heroku-repl')
41
50
 
42
- oclif.run().then(require('@oclif/core/flush')).catch(async error => {
43
- // capture any errors raised by oclif
44
- const cliError = error
45
- cliError.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
46
- await globalTelemetry.sendTelemetry(cliError)
51
+ const main = async () => {
52
+ try {
53
+ await config.load()
54
+ const {_: [commandName, ...args], ...flags} = yargs
55
+ if (!commandName && args.length === 0 && Object.keys(flags).length === 0) {
56
+ await herokuRepl(config)
57
+ return
58
+ }
47
59
 
48
- return require('@oclif/core/handle')(error)
49
- })
60
+ if (flags.prompt) {
61
+ delete flags.prompt
62
+ await promptUser(config, commandName, args, flags)
63
+ await oclif.run([commandName, ...args], config)
64
+ } else {
65
+ await oclif.run(undefined, config)
66
+ }
67
+
68
+ await oclifFlush()
69
+ } catch (error) {
70
+ // capture any errors raised by oclif
71
+ const cliError = error
72
+ cliError.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
73
+ await globalTelemetry.sendTelemetry(cliError)
74
+
75
+ oclifError(error)
76
+ }
77
+ }
50
78
 
79
+ main()
package/lib/analytics.js CHANGED
@@ -15,6 +15,8 @@ class AnalyticsCommand {
15
15
  }
16
16
  async record(opts) {
17
17
  await this.initialize;
18
+ const mcpMode = process.env.HEROKU_MCP_MODE === 'true';
19
+ const mcpServerVersion = process.env.HEROKU_MCP_SERVER_VERSION || 'unknown';
18
20
  const plugin = opts.Command.plugin;
19
21
  if (!plugin) {
20
22
  debug('no plugin found for analytics');
@@ -29,7 +31,7 @@ class AnalyticsCommand {
29
31
  cli: this.config.name,
30
32
  command: opts.Command.id,
31
33
  completion: await this._acAnalytics(opts.Command.id),
32
- version: this.config.version,
34
+ version: `${this.config.version}${mcpMode ? ` (MCP ${mcpServerVersion})` : ''}`,
33
35
  plugin: plugin.name,
34
36
  plugin_version: plugin.version,
35
37
  os: this.config.platform,
@@ -25,7 +25,7 @@ export declare function initializeInstrumentation(): void;
25
25
  export declare function setupTelemetry(config: any, opts: any): {
26
26
  command: any;
27
27
  os: any;
28
- version: any;
28
+ version: string;
29
29
  exitCode: number;
30
30
  exitState: string;
31
31
  cliRunDuration: number;