opencoding-agent 1.0.3 → 1.0.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.
- package/dist/src/hooks/auto-update/cache.d.ts +6 -0
- package/dist/src/hooks/auto-update/cache.js +24 -0
- package/dist/src/hooks/auto-update/checker.d.ts +18 -0
- package/dist/src/hooks/auto-update/checker.js +128 -0
- package/dist/src/hooks/auto-update/constants.d.ts +9 -0
- package/dist/src/hooks/auto-update/constants.js +23 -0
- package/dist/src/hooks/auto-update/index.d.ts +15 -0
- package/dist/src/hooks/auto-update/index.js +52 -0
- package/dist/src/index.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invalidates the current package by removing its directory from OpenCode's cache.
|
|
3
|
+
* This forces a clean state so OpenCode re-downloads the plugin on next run.
|
|
4
|
+
* @param packageName The name of the package to invalidate.
|
|
5
|
+
*/
|
|
6
|
+
export declare function invalidatePackage(packageName?: string): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { CACHE_DIR, PACKAGE_NAME } from './constants';
|
|
4
|
+
/**
|
|
5
|
+
* Invalidates the current package by removing its directory from OpenCode's cache.
|
|
6
|
+
* This forces a clean state so OpenCode re-downloads the plugin on next run.
|
|
7
|
+
* @param packageName The name of the package to invalidate.
|
|
8
|
+
*/
|
|
9
|
+
export function invalidatePackage(packageName = PACKAGE_NAME) {
|
|
10
|
+
try {
|
|
11
|
+
const pkgDir = path.join(CACHE_DIR, 'node_modules', packageName);
|
|
12
|
+
if (fs.existsSync(pkgDir)) {
|
|
13
|
+
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
14
|
+
console.log(`[auto-update] Package cache removed: ${pkgDir}`);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
console.log(`[auto-update] Package not found in cache: ${packageName}`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error('[auto-update] Failed to invalidate package:', err);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PackageJson {
|
|
2
|
+
name?: string;
|
|
3
|
+
version?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface NpmDistTags {
|
|
7
|
+
latest: string;
|
|
8
|
+
[key: string]: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getCachedVersion(): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Fetches the latest version for a specific channel from the NPM registry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getLatestVersion(channel?: string): Promise<string | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Checks if the plugin is running in local development mode (e.g. from a file path).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isLocalDevMode(directory: string): boolean;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { INSTALLED_PACKAGE_JSON, NPM_FETCH_TIMEOUT, NPM_REGISTRY_URL, PACKAGE_NAME, USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC } from './constants';
|
|
5
|
+
/**
|
|
6
|
+
* Strips single and multi-line comments from a JSON string.
|
|
7
|
+
*/
|
|
8
|
+
function stripJsonComments(text) {
|
|
9
|
+
return text.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the installed version from node_modules, with memoization.
|
|
13
|
+
*/
|
|
14
|
+
let cachedPackageVersion = null;
|
|
15
|
+
export function getCachedVersion() {
|
|
16
|
+
if (cachedPackageVersion)
|
|
17
|
+
return cachedPackageVersion;
|
|
18
|
+
try {
|
|
19
|
+
if (fs.existsSync(INSTALLED_PACKAGE_JSON)) {
|
|
20
|
+
const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, 'utf-8');
|
|
21
|
+
const pkg = JSON.parse(content);
|
|
22
|
+
if (pkg.version) {
|
|
23
|
+
cachedPackageVersion = pkg.version;
|
|
24
|
+
return pkg.version;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* empty */
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const pkgPath = findPackageJsonUp(currentDir);
|
|
34
|
+
if (pkgPath) {
|
|
35
|
+
const content = fs.readFileSync(pkgPath, 'utf-8');
|
|
36
|
+
const pkg = JSON.parse(content);
|
|
37
|
+
if (pkg.version) {
|
|
38
|
+
cachedPackageVersion = pkg.version;
|
|
39
|
+
return pkg.version;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.warn('[auto-update] Failed to resolve version from current directory:', err);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Recursively searches upwards for a package.json belonging to this plugin.
|
|
50
|
+
*/
|
|
51
|
+
function findPackageJsonUp(startPath) {
|
|
52
|
+
try {
|
|
53
|
+
const stat = fs.statSync(startPath);
|
|
54
|
+
let dir = stat.isDirectory() ? startPath : path.dirname(startPath);
|
|
55
|
+
for (let i = 0; i < 10; i++) {
|
|
56
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
57
|
+
if (fs.existsSync(pkgPath)) {
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(pkgPath, 'utf-8');
|
|
60
|
+
const pkg = JSON.parse(content);
|
|
61
|
+
if (pkg.name === PACKAGE_NAME)
|
|
62
|
+
return pkgPath;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* empty */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const parent = path.dirname(dir);
|
|
69
|
+
if (parent === dir)
|
|
70
|
+
break;
|
|
71
|
+
dir = parent;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* empty */
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Fetches the latest version for a specific channel from the NPM registry.
|
|
81
|
+
*/
|
|
82
|
+
export async function getLatestVersion(channel = 'latest') {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
headers: { Accept: 'application/json' },
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
return null;
|
|
92
|
+
const data = (await response.json());
|
|
93
|
+
return data[channel] ?? data.latest ?? null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timeoutId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Checks if the plugin is running in local development mode (e.g. from a file path).
|
|
104
|
+
*/
|
|
105
|
+
export function isLocalDevMode(directory) {
|
|
106
|
+
const configPaths = [
|
|
107
|
+
path.join(directory, '.opencode', 'opencode.json'),
|
|
108
|
+
path.join(directory, '.opencode', 'opencode.jsonc'),
|
|
109
|
+
USER_OPENCODE_CONFIG,
|
|
110
|
+
USER_OPENCODE_CONFIG_JSONC,
|
|
111
|
+
];
|
|
112
|
+
for (const configPath of configPaths) {
|
|
113
|
+
try {
|
|
114
|
+
if (fs.existsSync(configPath)) {
|
|
115
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
116
|
+
const json = JSON.parse(stripJsonComments(content));
|
|
117
|
+
const plugins = json.plugin ?? [];
|
|
118
|
+
if (plugins.some((p) => p.startsWith('/') || p.startsWith('file://'))) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME = "opencoding-agent";
|
|
2
|
+
export declare const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/opencoding-agent/dist-tags";
|
|
3
|
+
export declare const NPM_FETCH_TIMEOUT = 3000;
|
|
4
|
+
/** The directory used by OpenCode to cache node_modules for plugins. */
|
|
5
|
+
export declare const CACHE_DIR: string;
|
|
6
|
+
/** Path to this plugin's package.json within the OpenCode cache. */
|
|
7
|
+
export declare const INSTALLED_PACKAGE_JSON: string;
|
|
8
|
+
export declare const USER_OPENCODE_CONFIG: string;
|
|
9
|
+
export declare const USER_OPENCODE_CONFIG_JSONC: string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export const PACKAGE_NAME = 'opencoding-agent';
|
|
4
|
+
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
5
|
+
export const NPM_FETCH_TIMEOUT = 3000;
|
|
6
|
+
function getCacheDir() {
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return path.join(process.env.LOCALAPPDATA ?? os.homedir(), 'opencode');
|
|
9
|
+
}
|
|
10
|
+
return path.join(os.homedir(), '.cache', 'opencode');
|
|
11
|
+
}
|
|
12
|
+
/** The directory used by OpenCode to cache node_modules for plugins. */
|
|
13
|
+
export const CACHE_DIR = getCacheDir();
|
|
14
|
+
/** Path to this plugin's package.json within the OpenCode cache. */
|
|
15
|
+
export const INSTALLED_PACKAGE_JSON = path.join(CACHE_DIR, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
16
|
+
function getConfigDir() {
|
|
17
|
+
const userConfigDir = process.env.XDG_CONFIG_HOME
|
|
18
|
+
? process.env.XDG_CONFIG_HOME
|
|
19
|
+
: path.join(os.homedir(), '.config');
|
|
20
|
+
return path.join(userConfigDir, 'opencode');
|
|
21
|
+
}
|
|
22
|
+
export const USER_OPENCODE_CONFIG = path.join(getConfigDir(), 'opencode.json');
|
|
23
|
+
export const USER_OPENCODE_CONFIG_JSONC = path.join(getConfigDir(), 'opencode.jsonc');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an OpenCode hook that checks for plugin updates and invalidates cache if available.
|
|
4
|
+
*
|
|
5
|
+
* @param ctx The plugin input context.
|
|
6
|
+
* @returns A hook object for the session.created event.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAutoUpdateHook(ctx: PluginInput): {
|
|
9
|
+
event: ({ event }: {
|
|
10
|
+
event: {
|
|
11
|
+
type: string;
|
|
12
|
+
properties?: unknown;
|
|
13
|
+
};
|
|
14
|
+
}) => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getCachedVersion, getLatestVersion, isLocalDevMode } from './checker';
|
|
2
|
+
import { invalidatePackage } from './cache';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an OpenCode hook that checks for plugin updates and invalidates cache if available.
|
|
5
|
+
*
|
|
6
|
+
* @param ctx The plugin input context.
|
|
7
|
+
* @returns A hook object for the session.created event.
|
|
8
|
+
*/
|
|
9
|
+
export function createAutoUpdateHook(ctx) {
|
|
10
|
+
let hasChecked = false;
|
|
11
|
+
return {
|
|
12
|
+
event: ({ event }) => {
|
|
13
|
+
// Only run once per process when a session is created
|
|
14
|
+
if (event.type !== 'session.created')
|
|
15
|
+
return;
|
|
16
|
+
if (hasChecked)
|
|
17
|
+
return;
|
|
18
|
+
hasChecked = true;
|
|
19
|
+
// Run check in the background
|
|
20
|
+
setTimeout(async () => {
|
|
21
|
+
// Skip check if in local development mode
|
|
22
|
+
if (isLocalDevMode(ctx.directory)) {
|
|
23
|
+
console.log('[auto-update] Local development mode detected. Skipping check.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const currentVersion = getCachedVersion();
|
|
27
|
+
const latestVersion = await getLatestVersion();
|
|
28
|
+
if (!currentVersion || !latestVersion) {
|
|
29
|
+
console.log('[auto-update] Could not determine versions. Skipping.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (currentVersion !== latestVersion) {
|
|
33
|
+
console.log(`[auto-update] Update available: ${currentVersion} → ${latestVersion}`);
|
|
34
|
+
// Show toast notification
|
|
35
|
+
ctx.client.tui.showToast({
|
|
36
|
+
body: {
|
|
37
|
+
title: 'opencoding-agent Update!',
|
|
38
|
+
message: `v${latestVersion} is available. Restart OpenCode to apply.`,
|
|
39
|
+
variant: 'info',
|
|
40
|
+
duration: 8000,
|
|
41
|
+
},
|
|
42
|
+
}).catch(() => { });
|
|
43
|
+
// Invalidate cache
|
|
44
|
+
invalidatePackage();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(`[auto-update] Already on latest version: ${currentVersion}`);
|
|
48
|
+
}
|
|
49
|
+
}, 0);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { injectAgents } from "./agents";
|
|
|
2
2
|
import { catalogTools } from "./tools/catalog";
|
|
3
3
|
import { createBuiltinMcps } from "./mcp";
|
|
4
4
|
import { loadPluginConfig } from "./config";
|
|
5
|
+
import { createAutoUpdateHook } from "./hooks/auto-update";
|
|
5
6
|
/**
|
|
6
7
|
* opencoding-agent Plugin
|
|
7
8
|
*
|
|
@@ -12,6 +13,8 @@ const OpencodingAgentPlugin = async (ctx) => {
|
|
|
12
13
|
const pluginConfig = loadPluginConfig(ctx.directory);
|
|
13
14
|
const mcps = createBuiltinMcps(pluginConfig.disabled_mcps);
|
|
14
15
|
const mcpNames = Object.keys(mcps);
|
|
16
|
+
// Initialize auto-update hook
|
|
17
|
+
const autoUpdateHook = createAutoUpdateHook(ctx);
|
|
15
18
|
return {
|
|
16
19
|
name: "opencoding-agent",
|
|
17
20
|
// Config hook: Injected once during initialization
|
|
@@ -52,6 +55,10 @@ const OpencodingAgentPlugin = async (ctx) => {
|
|
|
52
55
|
}
|
|
53
56
|
});
|
|
54
57
|
},
|
|
58
|
+
// Session events
|
|
59
|
+
event: async (input) => {
|
|
60
|
+
await autoUpdateHook.event(input);
|
|
61
|
+
},
|
|
55
62
|
// Register custom tools
|
|
56
63
|
tool: {
|
|
57
64
|
...catalogTools,
|