bacluc-opencode-completion-check-command 0.5.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bacluc-opencode-completion-check-command",
3
- "version": "v0.5.0",
3
+ "version": "v0.7.0",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "opencode",
@@ -36,12 +36,12 @@
36
36
  "format:check": "prettier --check ."
37
37
  },
38
38
  "dependencies": {
39
- "@opencode-ai/plugin": "1.16.2"
39
+ "@opencode-ai/plugin": "1.17.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bun": "1.3.14",
43
43
  "@types/node": "25.9.2",
44
- "prettier": "3.8.3",
44
+ "prettier": "3.8.4",
45
45
  "typescript": "6.0.3",
46
46
  "vitest": "4.1.8"
47
47
  }
@@ -1,4 +1,4 @@
1
- import type { Hooks, Plugin } from '@opencode-ai/plugin'
1
+ import type { Hooks, Plugin, PluginInput } from '@opencode-ai/plugin'
2
2
  import type { Event } from '@opencode-ai/sdk'
3
3
  import { promises as fs } from 'fs'
4
4
  import { exec } from 'child_process'
@@ -123,6 +123,60 @@ export function buildFailureMessage(result: CommandResult): string {
123
123
  return message
124
124
  }
125
125
 
126
+ /**
127
+ * Detects whether an error attached to an assistant message means the provider's
128
+ * usage limit was used up (rate limit, quota or credits exhausted). When that
129
+ * happens the agent did not actually finish its task — it was cut off — so the
130
+ * completion check must not run and the agent must not be re-prompted (which
131
+ * would immediately hit the same limit again and burn the retry budget).
132
+ */
133
+ export function isUsageLimitError(error: unknown): boolean {
134
+ if (!error || typeof error !== 'object') {
135
+ return false
136
+ }
137
+ const { name, data } = error as { name?: string; data?: Record<string, unknown> }
138
+ const statusCode = typeof data?.statusCode === 'number' ? data.statusCode : undefined
139
+
140
+ // HTTP 429 from the provider always means rate / usage limit reached.
141
+ if (name === 'APIError' && statusCode === 429) {
142
+ return true
143
+ }
144
+
145
+ // Otherwise fall back to matching the human-readable message / response body,
146
+ // which is how quota/credit exhaustion surfaces across providers.
147
+ const message = typeof data?.message === 'string' ? data.message : ''
148
+ const responseBody = typeof data?.responseBody === 'string' ? data.responseBody : ''
149
+ const haystack = `${message} ${responseBody}`.toLowerCase()
150
+
151
+ return /usage limit|rate limit|quota|too many requests|credit balance|out of credits|insufficient (?:credit|balance|funds|quota)/.test(
152
+ haystack,
153
+ )
154
+ }
155
+
156
+ /**
157
+ * Returns true when the session's most recent assistant message ended with a
158
+ * usage-limit error, i.e. the model ran out of usage rather than finishing.
159
+ */
160
+ export async function sessionHitUsageLimit(client: PluginInput['client'], sessionID: string): Promise<boolean> {
161
+ try {
162
+ const response = await client.session.messages({ path: { id: sessionID } })
163
+ const messages = response?.data
164
+ if (!Array.isArray(messages)) {
165
+ return false
166
+ }
167
+ for (let i = messages.length - 1; i >= 0; i--) {
168
+ const info = messages[i]?.info
169
+ if (info?.role === 'assistant') {
170
+ return isUsageLimitError(info.error)
171
+ }
172
+ }
173
+ return false
174
+ } catch {
175
+ // If we cannot determine the state, fall back to the normal behaviour.
176
+ return false
177
+ }
178
+ }
179
+
126
180
  export async function readDefaultCommandFromAgentsMd(directory: string): Promise<string | null> {
127
181
  try {
128
182
  const content = await fs.readFile(`${directory}/AGENTS.md`, 'utf-8')
@@ -239,6 +293,26 @@ export const CompletionCheckCommandPlugin: Plugin = async (input, options) => {
239
293
  processing.add(sessionID)
240
294
 
241
295
  try {
296
+ if (await sessionHitUsageLimit(client, sessionID)) {
297
+ // The agent was cut off by the provider's usage limit rather than
298
+ // finishing. Skip the completion check (and the re-prompt) so we don't
299
+ // immediately hit the limit again. The command stays registered, so the
300
+ // check still runs once the session is able to continue.
301
+ try {
302
+ await client.tui.showToast({
303
+ body: {
304
+ title: 'Completion Check',
305
+ message: 'Skipped the completion check because the usage limit was reached.',
306
+ variant: 'warning',
307
+ duration: 10000,
308
+ },
309
+ })
310
+ } catch {
311
+ // Ignore feedback errors
312
+ }
313
+ return
314
+ }
315
+
242
316
  const result = await executeCommand(command, input.directory)
243
317
 
244
318
  if (result.exitCode === 0) {
package/src/index.ts CHANGED
@@ -5,5 +5,7 @@ export {
5
5
  CompletionCheckStore,
6
6
  executeCommand,
7
7
  buildFailureMessage,
8
+ isUsageLimitError,
9
+ sessionHitUsageLimit,
8
10
  } from './completion-check-command.js'
9
11
  export type { CommandResult } from './completion-check-command.js'