Skip to content

Commit 4cf3a47

Browse files
authored
Merge pull request #13 from Tarquinen/feat/version-checker
feat: add update checker notification
2 parents 4c1e08e + 97c1f7e commit 4cf3a47

File tree

8 files changed

+685
-45
lines changed

8 files changed

+685
-45
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ Thumbs.db
2727

2828
# OpenCode
2929
.opencode/
30+
31+
# Tests (local development only)
32+
tests/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ If you want to ensure a specific version is always used or update your version,
7878
```json
7979
{
8080
"plugin": [
81-
"@tarquinen/[email protected].10"
81+
"@tarquinen/[email protected].12"
8282
]
8383
}
8484
```

index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Plugin } from "@opencode-ai/plugin"
33
import { getConfig } from "./lib/config"
44
import { Logger } from "./lib/logger"
55
import { Janitor, type SessionStats } from "./lib/janitor"
6+
import { checkForUpdates } from "./lib/version-checker"
67

78
/**
89
* Checks if a session is a subagent (child session)
@@ -145,6 +146,9 @@ const plugin: Plugin = (async (ctx) => {
145146
model: config.model || "auto"
146147
})
147148

149+
// Check for updates on launch (fire and forget)
150+
checkForUpdates(ctx.client).catch(() => {})
151+
148152
return {
149153
/**
150154
* Event Hook: Triggers janitor analysis when session becomes idle

lib/deduplicator.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,35 @@ export function detectDuplicates(
2727
protectedTools: string[]
2828
): DuplicateDetectionResult {
2929
const signatureMap = new Map<string, string[]>()
30-
30+
3131
// Filter out protected tools before processing
3232
const deduplicatableIds = unprunedToolCallIds.filter(id => {
3333
const metadata = toolMetadata.get(id)
3434
return !metadata || !protectedTools.includes(metadata.tool)
3535
})
36-
36+
3737
// Build map of signature -> [ids in chronological order]
3838
for (const id of deduplicatableIds) {
3939
const metadata = toolMetadata.get(id)
4040
if (!metadata) continue
41-
41+
4242
const signature = createToolSignature(metadata.tool, metadata.parameters)
4343
if (!signatureMap.has(signature)) {
4444
signatureMap.set(signature, [])
4545
}
4646
signatureMap.get(signature)!.push(id)
4747
}
48-
48+
4949
// Identify duplicates (keep only last occurrence)
5050
const duplicateIds: string[] = []
5151
const deduplicationDetails = new Map()
52-
52+
5353
for (const [signature, ids] of signatureMap.entries()) {
5454
if (ids.length > 1) {
5555
const metadata = toolMetadata.get(ids[0])!
5656
const idsToRemove = ids.slice(0, -1) // All except last
5757
duplicateIds.push(...idsToRemove)
58-
58+
5959
deduplicationDetails.set(signature, {
6060
toolName: metadata.tool,
6161
parameterKey: extractParameterKey(metadata),
@@ -65,7 +65,7 @@ export function detectDuplicates(
6565
})
6666
}
6767
}
68-
68+
6969
return { duplicateIds, deduplicationDetails }
7070
}
7171

@@ -75,7 +75,7 @@ export function detectDuplicates(
7575
*/
7676
function createToolSignature(tool: string, parameters?: any): string {
7777
if (!parameters) return tool
78-
78+
7979
// Normalize parameters for consistent comparison
8080
const normalized = normalizeParameters(parameters)
8181
const sorted = sortObjectKeys(normalized)
@@ -91,7 +91,7 @@ function createToolSignature(tool: string, parameters?: any): string {
9191
function normalizeParameters(params: any): any {
9292
if (typeof params !== 'object' || params === null) return params
9393
if (Array.isArray(params)) return params
94-
94+
9595
const normalized: any = {}
9696
for (const [key, value] of Object.entries(params)) {
9797
if (value !== undefined && value !== null) {
@@ -107,7 +107,7 @@ function normalizeParameters(params: any): any {
107107
function sortObjectKeys(obj: any): any {
108108
if (typeof obj !== 'object' || obj === null) return obj
109109
if (Array.isArray(obj)) return obj.map(sortObjectKeys)
110-
110+
111111
const sorted: any = {}
112112
for (const key of Object.keys(obj).sort()) {
113113
sorted[key] = sortObjectKeys(obj[key])
@@ -136,9 +136,9 @@ function sortObjectKeys(obj: any): any {
136136
*/
137137
export function extractParameterKey(metadata: { tool: string, parameters?: any }): string {
138138
if (!metadata.parameters) return ''
139-
139+
140140
const { tool, parameters } = metadata
141-
141+
142142
// ===== File Operation Tools =====
143143
if (tool === "read" && parameters.filePath) {
144144
return parameters.filePath
@@ -149,7 +149,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
149149
if (tool === "edit" && parameters.filePath) {
150150
return parameters.filePath
151151
}
152-
152+
153153
// ===== Directory/Search Tools =====
154154
if (tool === "list") {
155155
// path is optional, defaults to current directory
@@ -170,17 +170,17 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
170170
}
171171
return '(unknown pattern)'
172172
}
173-
173+
174174
// ===== Execution Tools =====
175175
if (tool === "bash") {
176176
if (parameters.description) return parameters.description
177177
if (parameters.command) {
178-
return parameters.command.length > 50
178+
return parameters.command.length > 50
179179
? parameters.command.substring(0, 50) + "..."
180180
: parameters.command
181181
}
182182
}
183-
183+
184184
// ===== Web Tools =====
185185
if (tool === "webfetch" && parameters.url) {
186186
return parameters.url
@@ -191,7 +191,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
191191
if (tool === "codesearch" && parameters.query) {
192192
return `"${parameters.query}"`
193193
}
194-
194+
195195
// ===== Todo Tools =====
196196
// Note: Todo tools are stateful and in protectedTools by default
197197
if (tool === "todowrite") {
@@ -200,7 +200,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
200200
if (tool === "todoread") {
201201
return "read todo list"
202202
}
203-
203+
204204
// ===== Agent/Task Tools =====
205205
// Note: task is in protectedTools by default
206206
if (tool === "task" && parameters.description) {
@@ -210,7 +210,7 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
210210
if (tool === "batch") {
211211
return `${parameters.tool_calls?.length || 0} parallel tools`
212212
}
213-
213+
214214
// ===== Fallback =====
215215
// For unknown tools, custom tools, or tools without extractable keys
216216
// Check if parameters is empty or only has empty values

lib/janitor.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export class Janitor {
228228
// Filter LLM results to only include IDs that were actually candidates
229229
// (LLM sometimes returns duplicate IDs that were already filtered out)
230230
const rawLlmPrunedIds = result.object.pruned_tool_call_ids
231-
llmPrunedIds = rawLlmPrunedIds.filter(id =>
231+
llmPrunedIds = rawLlmPrunedIds.filter(id =>
232232
prunableToolCallIds.includes(id.toLowerCase())
233233
)
234234

@@ -263,7 +263,7 @@ export class Janitor {
263263

264264
// Calculate which IDs are actually NEW (not already pruned)
265265
const finalNewlyPrunedIds = Array.from(expandedPrunedIds).filter(id => !alreadyPrunedIds.includes(id))
266-
266+
267267
// finalPrunedIds includes everything (new + already pruned) for logging
268268
const finalPrunedIds = Array.from(expandedPrunedIds)
269269

@@ -338,16 +338,16 @@ export class Janitor {
338338
const shortenedPath = this.shortenSinglePath(pathPart)
339339
return `${prefix} in ${shortenedPath}`
340340
}
341-
341+
342342
return this.shortenSinglePath(input)
343343
}
344-
344+
345345
/**
346346
* Shorten a single path string
347347
*/
348348
private shortenSinglePath(path: string): string {
349349
const homeDir = require('os').homedir()
350-
350+
351351
// Strip working directory FIRST (before ~ replacement) for cleaner relative paths
352352
if (this.workingDirectory) {
353353
if (path.startsWith(this.workingDirectory + '/')) {
@@ -358,7 +358,7 @@ export class Janitor {
358358
return '.'
359359
}
360360
}
361-
361+
362362
// Replace home directory with ~
363363
if (path.startsWith(homeDir)) {
364364
path = '~' + path.slice(homeDir.length)
@@ -375,7 +375,7 @@ export class Janitor {
375375
const workingDirWithTilde = this.workingDirectory.startsWith(homeDir)
376376
? '~' + this.workingDirectory.slice(homeDir.length)
377377
: null
378-
378+
379379
if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) {
380380
return path.slice(workingDirWithTilde.length + 1)
381381
}
@@ -396,15 +396,15 @@ export class Janitor {
396396
if (prunedIds.length === 0) return messages
397397

398398
const prunedIdsSet = new Set(prunedIds.map(id => id.toLowerCase()))
399-
399+
400400
return messages.map(msg => {
401401
if (!msg.parts) return msg
402-
402+
403403
return {
404404
...msg,
405405
parts: msg.parts.map((part: any) => {
406-
if (part.type === 'tool' &&
407-
part.callID &&
406+
if (part.type === 'tool' &&
407+
part.callID &&
408408
prunedIdsSet.has(part.callID.toLowerCase()) &&
409409
part.state?.output) {
410410
// Replace with the same placeholder used by the global fetch wrapper
@@ -427,20 +427,20 @@ export class Janitor {
427427
*/
428428
private async calculateTokensSaved(prunedIds: string[], toolOutputs: Map<string, string>): Promise<number> {
429429
const outputsToTokenize: string[] = []
430-
430+
431431
for (const prunedId of prunedIds) {
432432
const output = toolOutputs.get(prunedId)
433433
if (output) {
434434
outputsToTokenize.push(output)
435435
}
436436
}
437-
437+
438438
if (outputsToTokenize.length > 0) {
439439
// Use batch tokenization for efficiency (lazy loads gpt-tokenizer)
440440
const tokenCounts = await estimateTokensBatch(outputsToTokenize)
441441
return tokenCounts.reduce((sum, count) => sum + count, 0)
442442
}
443-
443+
444444
return 0
445445
}
446446

@@ -501,7 +501,7 @@ export class Janitor {
501501
const toolText = totalPruned === 1 ? 'tool' : 'tools'
502502

503503
let message = `🧹 DCP: Saved ~${tokensFormatted} tokens (${totalPruned} ${toolText} pruned)`
504-
504+
505505
// Add session totals if there's been more than one pruning run
506506
if (sessionStats.totalToolsPruned > totalPruned) {
507507
message += ` │ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`
@@ -536,15 +536,15 @@ export class Janitor {
536536

537537
const toolText = deduplicatedIds.length === 1 ? 'tool' : 'tools'
538538
let message = `🧹 DCP: Saved ~${tokensFormatted} tokens (${deduplicatedIds.length} duplicate ${toolText} removed)`
539-
539+
540540
// Add session totals if there's been more than one pruning run
541541
if (sessionStats.totalToolsPruned > deduplicatedIds.length) {
542542
message += ` │ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`
543543
}
544544
message += '\n'
545545

546546
// Group by tool type
547-
const grouped = new Map<string, Array<{count: number, key: string}>>()
547+
const grouped = new Map<string, Array<{ count: number, key: string }>>()
548548

549549
for (const [_, details] of deduplicationDetails) {
550550
const { toolName, parameterKey, duplicateCount } = details
@@ -603,7 +603,7 @@ export class Janitor {
603603
const tokensFormatted = formatTokenCount(tokensSaved)
604604

605605
let message = `🧹 DCP: Saved ~${tokensFormatted} tokens (${totalPruned} tool${totalPruned > 1 ? 's' : ''} pruned)`
606-
606+
607607
// Add session totals if there's been more than one pruning run
608608
if (sessionStats.totalToolsPruned > totalPruned) {
609609
message += ` │ Session: ~${formatTokenCount(sessionStats.totalTokensSaved)} tokens, ${sessionStats.totalToolsPruned} tools`
@@ -615,7 +615,7 @@ export class Janitor {
615615
message += `\n📦 Duplicates removed (${deduplicatedIds.length}):\n`
616616

617617
// Group by tool type
618-
const grouped = new Map<string, Array<{count: number, key: string}>>()
618+
const grouped = new Map<string, Array<{ count: number, key: string }>>()
619619

620620
for (const [_, details] of deduplicationDetails) {
621621
const { toolName, parameterKey, duplicateCount } = details
@@ -652,15 +652,15 @@ export class Janitor {
652652
}
653653
}
654654
}
655-
655+
656656
// Handle any tools that weren't found in metadata (edge case)
657657
const foundToolNames = new Set(toolsSummary.keys())
658658
const missingTools = llmPrunedIds.filter(id => {
659659
const normalizedId = id.toLowerCase()
660660
const metadata = toolMetadata.get(normalizedId)
661661
return !metadata || !foundToolNames.has(metadata.tool)
662662
})
663-
663+
664664
if (missingTools.length > 0) {
665665
message += ` (${missingTools.length} tool${missingTools.length > 1 ? 's' : ''} with unknown metadata)\n`
666666
}

0 commit comments

Comments
 (0)