pi-requesty-discover 1.2.0 → 2.0.0

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/README.adoc CHANGED
@@ -16,7 +16,8 @@ This is a fork of https://github.com/requestyai/pi-requesty[pi-requesty] with ad
16
16
 
17
17
  * Use `REQUESTY_API_KEY` from the environment
18
18
  * Health-check available models and write a log about them
19
- * Provide a dry-run mode that does not change anything
19
+ * Brief information about what was found, including added/removed models, health summary
20
+ * Confirm before writing `models.json`, with a dry-run mode that only previews
20
21
 
21
22
  == When and why use it
22
23
 
@@ -25,7 +26,7 @@ Use this extension if
25
26
  - you are on Requesty.ai
26
27
  - you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
27
28
  - you don't want to handcraft your own curated list of models constantly
28
- - you want to health check the list to complain about non-working models via a dry-run
29
+ - you want to health-check models and preview the diff before writing
29
30
 
30
31
  == Installation
31
32
 
@@ -124,12 +125,20 @@ Inside Pi:
124
125
  /requesty-discover
125
126
  ----
126
127
 
127
- The command fetches Requesty models using `~/.pi/agent/models.json` and writes the discovered model IDs back to the same file.
128
- Run `/reload` after syncing.
128
+ The command:
129
+
130
+ - discovers Requesty models (and optional health checks)
131
+ - shows a summary notification (added/removed IDs, health summary)
132
+ - asks for confirmation before writing `~/.pi/agent/models.json`
133
+ - reports whether the file was updated
134
+
135
+ If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
136
+
137
+ After a successful write, run `/reload`.
129
138
 
130
139
  [source,text]
131
140
  ----
132
141
  /requesty-discover --dry-run
133
142
  ----
134
143
 
135
- This will not overwrite the `models.json` file, but will do everything else, including the health checks.
144
+ Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
package/README.md CHANGED
@@ -8,7 +8,8 @@ This is a fork of [pi-requesty](https://github.com/requestyai/pi-requesty) with
8
8
 
9
9
  * Use `REQUESTY_API_KEY` from the environment
10
10
  * Health-check available models and write a log about them
11
- * Provide a dry-run mode that does not change anything
11
+ * Brief information about what was found, including added/removed models, health summary
12
+ * Confirm before writing `models.json`, with a dry-run mode that only previews
12
13
 
13
14
  ## When and why use it
14
15
 
@@ -17,7 +18,7 @@ Use this extension if
17
18
  * you are on Requesty.ai
18
19
  * you are limited to certain models by a company policy (for instance, for data residency/protection reasons)
19
20
  * you don’t want to handcraft your own curated list of models constantly
20
- * you want to health check the list to complain about non-working models via a dry-run
21
+ * you want to health-check models and preview the diff before writing
21
22
 
22
23
  ## Installation
23
24
 
@@ -91,11 +92,19 @@ Inside Pi:
91
92
  /requesty-discover
92
93
  ```
93
94
 
94
- The command fetches Requesty models using `~/.pi/agent/models.json` and writes the discovered model IDs back to the same file.
95
- Run `/reload` after syncing.
95
+ The command:
96
+
97
+ * discovers Requesty models (and optional health checks)
98
+ * shows a summary notification (added/removed IDs, health summary)
99
+ * asks for confirmation before writing `~/.pi/agent/models.json`
100
+ * reports whether the file was updated
101
+
102
+ If there are no model ID changes, confirmation still offers a metadata refresh write, which might have changed as well.
103
+
104
+ After a successful write, run `/reload`.
96
105
 
97
106
  ```text
98
107
  /requesty-discover --dry-run
99
108
  ```
100
109
 
101
- This will not overwrite the `models.json` file, but will do everything else, including the health checks.
110
+ Same discovery and health checks, but never prompts to write and leaves `models.json` unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-requesty-discover",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Discover available Requesty models for the Pi Coding Agent",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { ExtensionAPI, ExtensionCommandContext, ProviderModelConfig } from '@earendil-works/pi-coding-agent'
2
- import { getEnv } from './env'
2
+ import { type Env, getEnv } from './env'
3
3
  import {
4
4
  diffModels,
5
5
  formatModelsDiffSummary,
6
6
  getRequestyConfig,
7
- updateModelsJson,
8
7
  type ModelsDiff,
8
+ updateModelsJson,
9
9
  } from './models-json'
10
10
  import { discoverModels } from './requesty-api'
11
11
  import { checkModels, formatHealthSummary, writeHealthCheckLog } from './health-check'
@@ -26,105 +26,214 @@ export type Notifier = {
26
26
  notify(message: string, level: NotificationLevel): void
27
27
  }
28
28
 
29
+ export type Confirmer = {
30
+ confirm(title: string, message: string): Promise<boolean>
31
+ }
32
+
29
33
  export type StatusReporter = {
30
34
  set(message: string): void
31
- clear(): void
32
35
  }
33
36
 
37
+ type ModelsJsonData = ReturnType<typeof getRequestyConfig>['data']
38
+
39
+ type DiscoveryEvaluation = {
40
+ dryRun: boolean
41
+ modelCount: number
42
+ failedCount: number
43
+ passing: ProviderModelConfig[]
44
+ diff: ModelsDiff
45
+ healthCheckSummary: string
46
+ logNote: string
47
+ data: ModelsJsonData
48
+ env: Env
49
+ }
50
+
51
+ type EvaluationResult =
52
+ | {
53
+ ok: true
54
+ value: DiscoveryEvaluation
55
+ }
56
+ | { ok: false; error: unknown }
57
+
34
58
  // noinspection JSUnusedGlobalSymbols
35
59
  export default function (pi: ExtensionAPI) {
36
60
  pi.registerCommand(COMMAND_NAME, {
37
61
  description: 'Dynamically discover Requesty models, run health checks, and update the local models.json.',
38
62
  getArgumentCompletions,
39
63
  handler: async (args, ctx) => {
40
- await runWithStatusUi(ctx, 'Discovering models...', (status, notifier) => runCommand(args, status, notifier))
64
+ await runDiscoveryWorkflow(ctx, args)
41
65
  },
42
66
  })
43
67
  }
44
68
 
69
+ export async function runDiscoveryWorkflow(ctx: ExtensionCommandContext, args: string) {
70
+ // Interactive TUI command; no print/json/rpc path.
71
+ if (ctx.mode !== 'tui') {
72
+ return
73
+ }
74
+
75
+ const notifier = createUiNotifier(ctx)
76
+ const confirmer = createUiConfirmer(ctx)
77
+
78
+ // Phase A: progress UI only. Loader must close before confirm (Phase B).
79
+ // Errors are wrapped because ctx.ui.custom resolves via done() and does not reject.
80
+ const evaluationResult: EvaluationResult = await runWithStatusUi(ctx, 'Discovering models...', async status => {
81
+ try {
82
+ const evaluation = await evaluateDiscovery(args, status)
83
+ return { ok: true, value: evaluation }
84
+ } catch (error) {
85
+ return { ok: false, error }
86
+ }
87
+ })
88
+
89
+ if (!evaluationResult.ok) {
90
+ notifier.notify(formatDiscoveryFailure(evaluationResult.error), 'error')
91
+ return
92
+ }
93
+
94
+ // Phases B + C: decide, optional write, final notify — outside the loader.
95
+ await finalizeDiscovery(evaluationResult.value, confirmer, notifier)
96
+ }
97
+
45
98
  async function runWithStatusUi<T>(
46
99
  ctx: ExtensionCommandContext,
47
100
  initialMessage: string,
48
- fn: (status: StatusReporter, notifier: Notifier) => Promise<T>,
101
+ fn: (status: StatusReporter) => Promise<T>,
49
102
  ): Promise<T> {
50
- if (ctx.mode !== 'tui') {
51
- return fn(createNoopStatusReporter(), createNoopNotifier())
52
- }
53
-
54
103
  return ctx.ui.custom<T>((_tui, theme, _kb, done) => {
55
104
  const loader = new RequestyStatusLoader(_tui, theme, initialMessage)
56
- const notifier = createUiNotifier(ctx)
57
105
  const status = createLoaderStatusReporter(loader)
58
106
  void Promise.resolve()
59
- .then(() => fn(status, notifier))
107
+ .then(() => fn(status))
60
108
  .then(done)
61
109
  .catch(done)
62
110
  return loader
63
111
  })
64
112
  }
65
113
 
66
- export async function runCommand(args: string, status: StatusReporter, notifier: Notifier): Promise<void> {
114
+ function formatDiscoveryFailure(error: unknown): string {
115
+ const detail = error instanceof Error ? error.message : String(error)
116
+ return `Discovery failed: ${detail}`
117
+ }
118
+
119
+ async function evaluateDiscovery(args: string, status: StatusReporter): Promise<DiscoveryEvaluation> {
67
120
  status.set('Discovering Requesty models...')
68
- const parts = args.split(' ')
69
- const dryRun = parts.includes(DRY_RUN_ARG)
70
- if (dryRun) {
71
- notifier.notify('running in dry mode, no changes will be done', 'info')
121
+ const dryRun = args.split(' ').includes(DRY_RUN_ARG)
122
+
123
+ const env = getEnv()
124
+ const { data, provider, existingModelIds } = getRequestyConfig(env)
125
+ const models = await discoverModels(provider)
126
+ const modelsMap = new Map(models.map(m => [m.id, m]))
127
+
128
+ let diff: ModelsDiff
129
+ let failedCount = 0
130
+ let passing: ProviderModelConfig[]
131
+ let logNote = ''
132
+ let healthCheckSummary = ''
133
+
134
+ if (env.health_check_mode !== 'off') {
135
+ status.set(`Checking models 0/${models.length}...`)
136
+ const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
137
+ onProgress: ({ completed, total }) => {
138
+ status.set(`Checking models ${completed}/${total}...`)
139
+ },
140
+ })
141
+ const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
142
+ failedCount = sortedResults.filter(r => !r.ok).length
143
+ passing = sortedResults.flatMap(r => {
144
+ const model = modelsMap.get(r.modelId)
145
+ return r.ok && model ? [model] : []
146
+ })
147
+ diff = diffModels(existingModelIds, passing)
148
+ healthCheckSummary = formatHealthSummary(sortedResults)
149
+ writeHealthCheckLog(provider, sortedResults, diff, env)
150
+ logNote = `Full health check log: ${env.health_check_log_path}\n`
151
+ } else {
152
+ passing = models
153
+ diff = diffModels(existingModelIds, passing)
72
154
  }
73
155
 
74
- try {
75
- const env = getEnv()
76
- const { data, provider, existingModelIds } = getRequestyConfig(env)
77
- const models = await discoverModels(provider)
78
- const modelsMap = new Map(models.map(m => [m.id, m]))
79
-
80
- let diff: ModelsDiff
81
- let failed = []
82
- let passing: ProviderModelConfig[]
83
- let logNote = ''
84
- let healthCheckSummary = ''
85
-
86
- if (env.health_check_mode !== 'off') {
87
- status.set(`Checking models 0/${models.length}...`)
88
- const healthResults = await checkModels(provider, models, env.health_check_mode === 'full', {
89
- onProgress: ({ completed, total }) => {
90
- status.set(`Checking models ${completed}/${total}...`)
91
- },
92
- })
93
- const sortedResults = healthResults.toSorted((a, b) => a.modelId.localeCompare(b.modelId))
94
- failed = sortedResults.filter(r => !r.ok)
95
- passing = sortedResults.flatMap(r => {
96
- const model = modelsMap.get(r.modelId)
97
- return r.ok && model ? [model] : []
98
- })
99
- diff = diffModels(existingModelIds, passing)
100
- healthCheckSummary = formatHealthSummary(sortedResults)
101
- writeHealthCheckLog(provider, sortedResults, diff, env)
102
- logNote = `Full health check log: ${env.health_check_log_path}\n`
103
- } else {
104
- passing = models
105
- diff = diffModels(existingModelIds, passing)
106
- }
156
+ return {
157
+ dryRun,
158
+ modelCount: models.length,
159
+ failedCount,
160
+ passing,
161
+ diff,
162
+ healthCheckSummary,
163
+ logNote,
164
+ data,
165
+ env,
166
+ }
167
+ }
107
168
 
108
- const shouldUpdate = passing.length > 0 && !dryRun
109
- if (shouldUpdate) {
110
- updateModelsJson(data, passing, env)
111
- }
169
+ async function finalizeDiscovery(
170
+ evaluation: DiscoveryEvaluation,
171
+ confirmer: Confirmer,
172
+ notifier: Notifier,
173
+ ): Promise<void> {
174
+ const level = notificationLevel(evaluation)
175
+ const summary = buildDiscoverySummary(evaluation)
176
+
177
+ // Always surface the discovery result first (toast styling), then decide.
178
+ if (evaluation.dryRun) {
179
+ notifier.notify(
180
+ `${summary}
181
+ Dry run: left models.json unchanged.`,
182
+ level,
183
+ )
184
+ return
185
+ }
186
+
187
+ if (evaluation.passing.length === 0) {
188
+ notifier.notify(
189
+ `${summary}
190
+ Left models.json unchanged.`,
191
+ level,
192
+ )
193
+ return
194
+ }
195
+
196
+ notifier.notify(summary, level)
197
+ const { title, message } = buildConfirmPrompt(evaluation)
198
+ const shouldUpdate = await confirmer.confirm(title, message)
199
+ if (shouldUpdate) {
200
+ updateModelsJson(evaluation.data, evaluation.passing, evaluation.env)
201
+ notifier.notify('Updated models.json. Run /reload to use the changes.', 'info')
202
+ return
203
+ }
112
204
 
113
- const writeNote = shouldUpdate ? 'Run /reload to use models.json changes.' : 'models.json was not updated.'
114
- const message = `Discovered ${models.length} Requesty model(s).\n${healthCheckSummary}${formatModelsDiffSummary(diff)}\n${logNote}${writeNote}`
205
+ notifier.notify('Left models.json unchanged.', 'info')
206
+ }
207
+
208
+ function buildDiscoverySummary(evaluation: DiscoveryEvaluation): string {
209
+ return [
210
+ `Discovered ${evaluation.modelCount} Requesty model(s).`,
211
+ evaluation.healthCheckSummary.trimEnd(),
212
+ formatModelsDiffSummary(evaluation.diff),
213
+ evaluation.logNote.trimEnd(),
214
+ ]
215
+ .filter(part => part.length > 0)
216
+ .join('\n')
217
+ }
115
218
 
116
- if (failed.length === 0) {
117
- notifier.notify(message, 'info')
118
- } else if (failed.length < models.length) {
119
- notifier.notify(message, 'warning')
120
- } else {
121
- notifier.notify(message, 'error')
219
+ function buildConfirmPrompt(evaluation: DiscoveryEvaluation): { title: string; message: string } {
220
+ const hasIdChanges = evaluation.diff.added.length > 0 || evaluation.diff.removed.length > 0
221
+ if (hasIdChanges) {
222
+ return {
223
+ title: `Write ${evaluation.passing.length} model(s)?`,
224
+ message: 'Update models.json with the discovery result above.',
122
225
  }
123
- } catch (error) {
124
- notifier.notify(`Discovery failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
125
- } finally {
126
- status.clear()
127
226
  }
227
+ return {
228
+ title: 'Refresh models.json?',
229
+ message: 'No model ID changes. Rewrite the file to refresh metadata anyway?',
230
+ }
231
+ }
232
+
233
+ function notificationLevel(evaluation: DiscoveryEvaluation): NotificationLevel {
234
+ if (evaluation.failedCount === 0) return 'info'
235
+ if (evaluation.failedCount < evaluation.modelCount) return 'warning'
236
+ return 'error'
128
237
  }
129
238
 
130
239
  function getArgumentCompletions(prefix: string): AutocompleteItem[] {
@@ -132,37 +241,26 @@ function getArgumentCompletions(prefix: string): AutocompleteItem[] {
132
241
  {
133
242
  value: DRY_RUN_ARG,
134
243
  label: DRY_RUN_ARG,
135
- description: 'Preview without writing into the new model.json file',
244
+ description: 'Preview discovery without offering to write models.json',
136
245
  },
137
246
  ]
138
247
  if (!prefix) return options
139
248
  return options.filter(o => o.value.toLowerCase().startsWith(prefix.toLowerCase()))
140
249
  }
141
250
 
142
- function createNoopStatusReporter(): StatusReporter {
143
- return {
144
- set() {
145
- // Status output is only rendered in TUI mode.
146
- },
147
- clear() {
148
- // Status output is only rendered in TUI mode.
149
- },
150
- }
151
- }
152
-
153
- function createNoopNotifier(): Notifier {
251
+ function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
154
252
  return {
155
- notify() {
156
- // Notifications are only rendered in TUI mode.
253
+ notify(message: string, level?: NotificationLevel) {
254
+ const prefixedMessage = `${COMMAND_NAME}: ${message}`
255
+ ctx.ui.notify(prefixedMessage, level)
157
256
  },
158
257
  }
159
258
  }
160
259
 
161
- function createUiNotifier(ctx: ExtensionCommandContext): Notifier {
260
+ function createUiConfirmer(ctx: ExtensionCommandContext): Confirmer {
162
261
  return {
163
- notify(message: string, level?: NotificationLevel) {
164
- const prefixedMessage = `${COMMAND_NAME}: ${message}`
165
- ctx.ui.notify(prefixedMessage, level)
262
+ confirm(title, message) {
263
+ return ctx.ui.confirm(title, message)
166
264
  },
167
265
  }
168
266
  }
@@ -172,8 +270,5 @@ function createLoaderStatusReporter(loader: RequestyStatusLoader): StatusReporte
172
270
  set(message: string) {
173
271
  loader.setMessage(message)
174
272
  },
175
- clear() {
176
- // Nothing to clear: the loader closes when ctx.ui.custom() resolves.
177
- },
178
273
  }
179
274
  }