Skip to content

Commit 6401f13

Browse files
authored
Merge pull request #91 from Tarquinen/feat/auto-update
Add auto-update feature
2 parents 72a535c + 132e0bb commit 6401f13

File tree

5 files changed

+125
-37
lines changed

5 files changed

+125
-37
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Add to your OpenCode config:
1313
```jsonc
1414
// opencode.jsonc
1515
{
16-
"plugin": ["@tarquinen/opencode-dcp@0.4.2"],
16+
"plugin": ["@tarquinen/opencode-dcp"],
1717
"experimental": {
1818
"primary_tools": ["prune"]
1919
}
@@ -22,7 +22,7 @@ Add to your OpenCode config:
2222

2323
The `experimental.primary_tools` setting ensures the `prune` tool is only available to the primary agent (not subagents).
2424

25-
When a new version is available, DCP will show a toast notification. Update by changing the version number in your config.
25+
DCP automatically updates itself in the background when new versions are available. You'll see a toast notification when an update is downloaded—just restart OpenCode to apply it. To disable auto-updates, set `"autoUpdate": false` in your DCP config.
2626

2727
Restart OpenCode. The plugin will automatically start optimizing your sessions.
2828

@@ -63,6 +63,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
6363
| `model` | (session) | Model for analysis (e.g., `"anthropic/claude-haiku-4-5"`) |
6464
| `showModelErrorToasts` | `true` | Show notifications on model fallback |
6565
| `showUpdateToasts` | `true` | Show notifications when a new version is available |
66+
| `autoUpdate` | `true` | Automatically download new versions (restart to apply) |
6667
| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) |
6768
| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
6869
| `nudge_freq` | `10` | How often to remind AI to prune (lower = more frequent) |

index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ const plugin: Plugin = (async (ctx) => {
5353

5454
// Check for updates after a delay
5555
setTimeout(() => {
56-
checkForUpdates(ctx.client, logger, config.showUpdateToasts ?? true).catch(() => { })
56+
checkForUpdates(ctx.client, logger, {
57+
showToast: config.showUpdateToasts ?? true,
58+
autoUpdate: config.autoUpdate ?? true
59+
}).catch(() => { })
5760
}, 5000)
5861

5962
// Show migration toast if there were config migrations

lib/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface PluginConfig {
1414
model?: string
1515
showModelErrorToasts?: boolean
1616
showUpdateToasts?: boolean
17+
autoUpdate?: boolean
1718
strictModelSelection?: boolean
1819
pruning_summary: "off" | "minimal" | "detailed"
1920
nudge_freq: number
@@ -34,6 +35,7 @@ const defaultConfig: PluginConfig = {
3435
protectedTools: ['task', 'todowrite', 'todoread', 'prune', 'batch', 'edit', 'write'],
3536
showModelErrorToasts: true,
3637
showUpdateToasts: true,
38+
autoUpdate: true,
3739
strictModelSelection: false,
3840
pruning_summary: 'detailed',
3941
nudge_freq: 10,
@@ -50,6 +52,7 @@ const VALID_CONFIG_KEYS = new Set([
5052
'model',
5153
'showModelErrorToasts',
5254
'showUpdateToasts',
55+
'autoUpdate',
5356
'strictModelSelection',
5457
'pruning_summary',
5558
'nudge_freq',
@@ -115,6 +118,8 @@ function createDefaultConfig(): void {
115118
"showModelErrorToasts": true,
116119
// Show toast notifications when a new version is available
117120
"showUpdateToasts": true,
121+
// Automatically update to new versions (restart required to apply)
122+
"autoUpdate": true,
118123
// Only run AI analysis with session model or configured model (disables fallback models)
119124
"strictModelSelection": false,
120125
// AI analysis strategies (deduplication runs automatically on every request)
@@ -205,6 +210,7 @@ export function getConfig(ctx?: PluginInput): ConfigResult {
205210
model: globalConfig.model ?? config.model,
206211
showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts,
207212
showUpdateToasts: globalConfig.showUpdateToasts ?? config.showUpdateToasts,
213+
autoUpdate: globalConfig.autoUpdate ?? config.autoUpdate,
208214
strictModelSelection: globalConfig.strictModelSelection ?? config.strictModelSelection,
209215
strategies: mergeStrategies(config.strategies, globalConfig.strategies as any),
210216
pruning_summary: globalConfig.pruning_summary ?? config.pruning_summary,
@@ -237,6 +243,7 @@ export function getConfig(ctx?: PluginInput): ConfigResult {
237243
model: projectConfig.model ?? config.model,
238244
showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts,
239245
showUpdateToasts: projectConfig.showUpdateToasts ?? config.showUpdateToasts,
246+
autoUpdate: projectConfig.autoUpdate ?? config.autoUpdate,
240247
strictModelSelection: projectConfig.strictModelSelection ?? config.strictModelSelection,
241248
strategies: mergeStrategies(config.strategies, projectConfig.strategies as any),
242249
pruning_summary: projectConfig.pruning_summary ?? config.pruning_summary,

lib/prompts/synthetic.txt

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
<system-reminder>
2-
The <prunable-tools> list and any pruning nudges are injected by the environment as assistant messages, use this information when deciding what to prune.
3-
</system-reminder>
4-
5-
<instruction name=context_management_protocol>
6-
You are operating in a context-constrained environment. You must actively manage your context window using the `prune` tool.
2+
<instruction name=context_management_protocol policy_level=critical>
73

8-
## Prune Early, Prune Often
9-
Every tool call adds to your context debt. You MUST pay this down regularly by pruning. Do not wait until context is "full" - by then it's too late. Evaluate what can be pruned after every few tool calls.
4+
ENVIRONMENT
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the `prune` tool. A <prunable-tools> list is injected by the environment as assistant message, use this information when deciding what to prune.
106

11-
## When to Prune (Triggers)
12-
You SHOULD use the prune tool when ANY of these are true:
13-
- You just completed a task or sub-task
14-
- You read files that turned out to be unhelpful or only partially useful
15-
- You have gathered enough information to answer a question or make a decision
16-
- You ran commands whose output you have already processed
17-
- Newer tool outputs have made older ones obsolete
18-
- You are about to start a new phase of work
7+
PRUNE EARLY, PRUNE OFTEN - BUT PRUNE METHODICALLY
8+
Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Evaluate what SHOULD be pruned before jumping the gun.
199

20-
When in doubt, prune. It is better to prune aggressively than to run out of context.
10+
WHEN TO PRUNE? THE THREE SCENARIOS TO CONSIDER
11+
1. TASK COMPLETION: When work is done, quietly prune the tools that aren't needed anymore
12+
2. NOISE REMOVAL: If outputs are irrelevant, unhelpful, or superseded by newer info, prune IMMEDIATELY. No distillation - gun it down
13+
3. CONTEXT CONSOLIDATION: When pruning valuable context to the task at hand, you MUST ALWAYS distill key findings into your narrative BEFORE pruning. Be surgical and strategic in what you extract. THINK: high signal, low noise
2114

22-
## Three Pruning Modes
23-
Apply the correct mode for each situation:
15+
You WILL use the `prune` tool when ANY of these are true:
16+
- Task or sub-task is complete
17+
- You are about to start a new phase of work
18+
- You have distilled enough information in your messages to prune related tools
19+
- Context contains tools output that are unhelpful, noise, or made obsolete by newer outputs
2420

25-
1. TASK COMPLETION: When work is done, prune the tools used. No distillation needed - just state the task is complete.
26-
2. NOISE REMOVAL: If outputs are irrelevant, unhelpful, or outdated (superseded by newer info), prune IMMEDIATELY. No distillation - just cut it out.
27-
3. CONTEXT CONSOLIDATION: When pruning useful research, you MUST distill key findings into your narrative *before* pruning. Extract only what matters (e.g., a specific function signature from a large file).
21+
NOTES
22+
When in doubt, prune out. Prune often yet remain strategic about it.
23+
FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES.
2824

29-
FAILURE TO PRUNE will result in context overflow and degraded performance.
3025
</instruction>
26+
</system-reminder>

lib/version-checker.ts

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { readFileSync } from 'fs'
22
import { join, dirname } from 'path'
33
import { fileURLToPath } from 'url'
4+
import { spawn } from 'child_process'
5+
import { homedir } from 'os'
46

57
export const PACKAGE_NAME = '@tarquinen/opencode-dcp'
68
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
@@ -50,7 +52,65 @@ export function isOutdated(local: string, remote: string): boolean {
5052
return false
5153
}
5254

53-
export async function checkForUpdates(client: any, logger?: { info: (component: string, message: string, data?: any) => void }, showToast: boolean = true): Promise<void> {
55+
export async function performUpdate(targetVersion: string, logger?: { info: (component: string, message: string, data?: any) => void }): Promise<boolean> {
56+
// OpenCode installs packages to ~/.cache/opencode/node_modules/
57+
const cacheDir = join(homedir(), '.cache', 'opencode')
58+
const packageSpec = `${PACKAGE_NAME}@${targetVersion}`
59+
60+
logger?.info("version", "Starting auto-update", { targetVersion, cacheDir })
61+
62+
return new Promise((resolve) => {
63+
let resolved = false
64+
65+
const proc = spawn('npm', ['install', '--legacy-peer-deps', packageSpec], {
66+
cwd: cacheDir,
67+
stdio: 'pipe'
68+
})
69+
70+
let stderr = ''
71+
proc.stderr?.on('data', (data) => {
72+
stderr += data.toString()
73+
})
74+
75+
proc.on('close', (code) => {
76+
if (resolved) return
77+
resolved = true
78+
clearTimeout(timeoutId)
79+
if (code === 0) {
80+
logger?.info("version", "Auto-update succeeded", { targetVersion })
81+
resolve(true)
82+
} else {
83+
logger?.info("version", "Auto-update failed", { targetVersion, code, stderr: stderr.slice(0, 500) })
84+
resolve(false)
85+
}
86+
})
87+
88+
proc.on('error', (err) => {
89+
if (resolved) return
90+
resolved = true
91+
clearTimeout(timeoutId)
92+
logger?.info("version", "Auto-update error", { targetVersion, error: err.message })
93+
resolve(false)
94+
})
95+
96+
// Timeout after 60 seconds
97+
const timeoutId = setTimeout(() => {
98+
if (resolved) return
99+
resolved = true
100+
proc.kill()
101+
logger?.info("version", "Auto-update timed out", { targetVersion })
102+
resolve(false)
103+
}, 60000)
104+
})
105+
}
106+
107+
export async function checkForUpdates(
108+
client: any,
109+
logger?: { info: (component: string, message: string, data?: any) => void },
110+
options: { showToast?: boolean; autoUpdate?: boolean } = {}
111+
): Promise<void> {
112+
const { showToast = true, autoUpdate = false } = options
113+
54114
try {
55115
const local = getLocalVersion()
56116
const npm = await getNpmVersion()
@@ -65,20 +125,41 @@ export async function checkForUpdates(client: any, logger?: { info: (component:
65125
return
66126
}
67127

68-
logger?.info("version", "Update available", { local, npm })
128+
logger?.info("version", "Update available", { local, npm, autoUpdate })
69129

70-
if (!showToast) {
71-
return
72-
}
130+
if (autoUpdate) {
131+
// Attempt auto-update
132+
const success = await performUpdate(npm, logger)
73133

74-
await client.tui.showToast({
75-
body: {
76-
title: "DCP: Update available",
77-
message: `v${local} → v${npm}\nUpdate opencode.jsonc: ${PACKAGE_NAME}@${npm}`,
78-
variant: "info",
79-
duration: 6000
134+
if (success && showToast) {
135+
await client.tui.showToast({
136+
body: {
137+
title: "DCP: Updated!",
138+
message: `v${local} → v${npm}\nRestart OpenCode to apply`,
139+
variant: "success",
140+
duration: 6000
141+
}
142+
})
143+
} else if (!success && showToast) {
144+
await client.tui.showToast({
145+
body: {
146+
title: "DCP: Update failed",
147+
message: `v${local} → v${npm}\nManual: npm install ${PACKAGE_NAME}@${npm}`,
148+
variant: "warning",
149+
duration: 6000
150+
}
151+
})
80152
}
81-
})
153+
} else if (showToast) {
154+
await client.tui.showToast({
155+
body: {
156+
title: "DCP: Update available",
157+
message: `v${local} → v${npm}`,
158+
variant: "info",
159+
duration: 6000
160+
}
161+
})
162+
}
82163
} catch {
83164
}
84165
}

0 commit comments

Comments
 (0)