更新依赖项版本,添加文件哈希功能,优化远程应用连接逻辑
This commit is contained in:
@@ -51,7 +51,7 @@
|
|||||||
"@kevisual/router": "^0.0.62",
|
"@kevisual/router": "^0.0.62",
|
||||||
"@kevisual/types": "^0.0.12",
|
"@kevisual/types": "^0.0.12",
|
||||||
"@kevisual/use-config": "^1.0.28",
|
"@kevisual/use-config": "^1.0.28",
|
||||||
"@opencode-ai/plugin": "^1.1.35",
|
"@opencode-ai/plugin": "^1.1.36",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.6",
|
||||||
"@types/node": "^25.0.10",
|
"@types/node": "^25.0.10",
|
||||||
"@types/send": "^1.2.1",
|
"@types/send": "^1.2.1",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"@kevisual/js-filter": "^0.0.5",
|
"@kevisual/js-filter": "^0.0.5",
|
||||||
"@kevisual/oss": "^0.0.16",
|
"@kevisual/oss": "^0.0.16",
|
||||||
"@kevisual/video-tools": "^0.0.13",
|
"@kevisual/video-tools": "^0.0.13",
|
||||||
"@opencode-ai/sdk": "^1.1.35",
|
"@opencode-ai/sdk": "^1.1.36",
|
||||||
"es-toolkit": "^1.44.0",
|
"es-toolkit": "^1.44.0",
|
||||||
"eventemitter3": "^5.0.4",
|
"eventemitter3": "^5.0.4",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class AssistantApp extends Manager {
|
|||||||
logger.debug('链接到了远程应用服务器');
|
logger.debug('链接到了远程应用服务器');
|
||||||
const appId = id;
|
const appId = id;
|
||||||
const username = config?.auth.username || 'unknown';
|
const username = config?.auth.username || 'unknown';
|
||||||
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
const url = new URL(`/${username}/v1/${appId}`, config?.registry || 'https://kevisual.cn/');
|
||||||
this.remoteUrl = url.toString();
|
this.remoteUrl = url.toString();
|
||||||
console.log('远程地址', this.remoteUrl);
|
console.log('远程地址', this.remoteUrl);
|
||||||
} else {
|
} else {
|
||||||
@@ -190,6 +190,10 @@ export class AssistantApp extends Manager {
|
|||||||
console.log('重新连接到远程应用服务器...', this.attemptedConnectTimes);
|
console.log('重新连接到远程应用服务器...', this.attemptedConnectTimes);
|
||||||
const remoteApp = this.remoteApp;;
|
const remoteApp = this.remoteApp;;
|
||||||
if (remoteApp) {
|
if (remoteApp) {
|
||||||
|
// 先关闭旧的 WebSocket,防止竞态条件
|
||||||
|
if (remoteApp.ws) {
|
||||||
|
remoteApp.ws.close();
|
||||||
|
}
|
||||||
remoteApp.init();
|
remoteApp.init();
|
||||||
this.attemptedConnectTimes += 1;
|
this.attemptedConnectTimes += 1;
|
||||||
const isConnect = await remoteApp.isConnect();
|
const isConnect = await remoteApp.isConnect();
|
||||||
|
|||||||
16
assistant/src/module/file-hash.ts
Normal file
16
assistant/src/module/file-hash.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
export const getHash = (file: string) => {
|
||||||
|
if (!fs.existsSync(file)) return '';
|
||||||
|
const buffer = fs.readFileSync(file); // 不指定编码,返回 Buffer
|
||||||
|
return crypto.createHash('md5').update(buffer).digest('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBufferHash = (buffer: Buffer) => {
|
||||||
|
return crypto.createHash('md5').update(buffer).digest('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStringHash = (str: string) => {
|
||||||
|
return crypto.createHash('md5').update(str).digest('hex');
|
||||||
|
}
|
||||||
@@ -1,70 +1,169 @@
|
|||||||
import { App, QueryRouterServer } from '@kevisual/router';
|
import { App, QueryRouterServer } from '@kevisual/router';
|
||||||
import { AssistantConfig } from '../assistant/index.ts';
|
import { AssistantInit } from '../../services/init/index.ts';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs, { write } from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import { runCode } from './run.ts';
|
import { runCode } from './run.ts';
|
||||||
import { pick } from 'es-toolkit';
|
|
||||||
const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2'
|
const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2'
|
||||||
import { filter } from "@kevisual/js-filter";
|
import { filter } from "@kevisual/js-filter";
|
||||||
const codeDemo = `// 这是一个示例代码文件
|
import { getHash, getStringHash } from '../file-hash.ts';
|
||||||
import {Mini} from '@kevisual/router';
|
|
||||||
|
|
||||||
const app = new Mini();
|
const codeDemo = `// 这是一个示例代码文件
|
||||||
|
import {App} from '@kevisual/router';
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
|
||||||
app.route({
|
app.route({
|
||||||
path: 'hello',
|
path: 'hello',
|
||||||
describetion: 'LightCode 示例路由',
|
description: 'LightCode 示例路由',
|
||||||
|
metadata: {
|
||||||
|
tags: ['light-code', 'example'],
|
||||||
|
},
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
|
console.log('tokenUser:', ctx.query?.tokenUser);
|
||||||
ctx.body = 'Hello from LightCode!';
|
ctx.body = 'Hello from LightCode!';
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
|
|
||||||
app.wait();
|
app.wait();
|
||||||
`;
|
`;
|
||||||
|
const writeCodeDemo = async (appDir: string) => {
|
||||||
|
const lightcodeDir = path.join(appDir, 'light-code', 'code');
|
||||||
|
const demoPath = path.join(lightcodeDir, `${codeDemoId}.ts`);
|
||||||
|
fs.writeFileSync(demoPath, codeDemo, 'utf-8');
|
||||||
|
}
|
||||||
|
// writeCodeDemo(path.join(os.homedir(), 'kevisual', 'assistant-app', 'apps'));
|
||||||
|
|
||||||
type opts = {
|
type opts = {
|
||||||
router: QueryRouterServer | App
|
router: QueryRouterServer | App
|
||||||
config: AssistantConfig
|
config: AssistantInit
|
||||||
|
sync?: boolean
|
||||||
|
}
|
||||||
|
type LightCodeFile = {
|
||||||
|
id?: string, code?: string, hash?: string, filepath: string
|
||||||
}
|
}
|
||||||
export const initLightCode = async (opts: opts) => {
|
export const initLightCode = async (opts: opts) => {
|
||||||
// 注册 lightcode 路由
|
// 注册 light-code 路由
|
||||||
console.log('初始化 lightcode 路由');
|
console.log('初始化 light-code 路由');
|
||||||
const config = opts.config;
|
const config = opts.config;
|
||||||
|
const app = opts.router;
|
||||||
|
const token = config.getConfig()?.token || '';
|
||||||
|
const query = config.query;
|
||||||
|
const sync = opts.sync ?? true;
|
||||||
|
if (!config || !app) {
|
||||||
|
console.error('initLightCode 缺少必要参数, config 或 app');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const appDir = config.configPath.appsDir;
|
const appDir = config.configPath.appsDir;
|
||||||
const lightcodeDir = path.join(appDir, 'light-code', 'code');
|
const lightcodeDir = path.join(appDir, 'light-code', 'code');
|
||||||
if (!fs.existsSync(lightcodeDir)) {
|
if (!fs.existsSync(lightcodeDir)) {
|
||||||
fs.mkdirSync(lightcodeDir, { recursive: true });
|
fs.mkdirSync(lightcodeDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
let diffList: LightCodeFile[] = [];
|
||||||
|
|
||||||
const codeFiles = glob.sync(['**/*.ts', '**/*.js'], {
|
const codeFiles = glob.sync(['**/*.ts', '**/*.js'], {
|
||||||
cwd: lightcodeDir,
|
cwd: lightcodeDir,
|
||||||
onlyFiles: true,
|
onlyFiles: true,
|
||||||
|
}).map(file => {
|
||||||
|
return {
|
||||||
|
filepath: path.join(lightcodeDir, file),
|
||||||
|
// hash: getHash(path.join(lightcodeDir, file))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (codeFiles.length === 0) {
|
|
||||||
// 如果没有代码文件,创建一个示例文件
|
if (sync) {
|
||||||
const demoPath = path.join(lightcodeDir, `${codeDemoId}.ts`);
|
const queryRes = await query.post({
|
||||||
fs.writeFileSync(demoPath, codeDemo, 'utf-8');
|
path: 'light-code',
|
||||||
|
key: 'list',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
if (queryRes.code === 200) {
|
||||||
|
const lightQueryList = queryRes.data?.list || [];
|
||||||
|
for (const item of lightQueryList) {
|
||||||
|
const codeHash = getStringHash(item.code || '');
|
||||||
|
diffList.push({ id: item.id!, code: item.code || '', hash: codeHash, filepath: path.join(lightcodeDir, `${item.id}.ts`) });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of codeFiles) {
|
|
||||||
const tsPath = path.join(lightcodeDir, file);
|
const codeFileSet = new Set(codeFiles.map(f => f.filepath));
|
||||||
|
// 需要新增的文件 (在 diffList 中但不在 codeFiles 中)
|
||||||
|
const toAdd = diffList.filter(d => !codeFileSet.has(d.filepath));
|
||||||
|
// 需要删除的文件 (在 codeFiles 中但不在 diffList 中)
|
||||||
|
const toDelete = codeFiles.filter(f => !diffList.some(d => d.filepath === f.filepath));
|
||||||
|
// 需要更新的文件 (两边都有但 hash 不同)
|
||||||
|
const toUpdate = diffList.filter(d => codeFileSet.has(d.filepath) && d.hash !== getHash(d.filepath));
|
||||||
|
const unchanged = diffList.filter(d => codeFileSet.has(d.filepath) && d.hash === getHash(d.filepath));
|
||||||
|
|
||||||
|
// 执行新增
|
||||||
|
for (const item of toAdd) {
|
||||||
|
fs.writeFileSync(item.filepath, item.code, 'utf-8');
|
||||||
|
// console.log(`新增 light-code 文件: ${item.filepath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行删除
|
||||||
|
for (const filepath of toDelete) {
|
||||||
|
fs.unlinkSync(filepath.filepath);
|
||||||
|
// console.log(`删除 light-code 文件: ${filepath.filepath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行更新
|
||||||
|
for (const item of toUpdate) {
|
||||||
|
fs.writeFileSync(item.filepath, item.code, 'utf-8');
|
||||||
|
// console.log(`更新 light-code 文件: ${item.filepath}`);
|
||||||
|
}
|
||||||
|
// 记录未更新的文件
|
||||||
|
// const lightCodeList = [...toAdd, ...unchanged].map(d => ({
|
||||||
|
// filepath: d.filepath,
|
||||||
|
// hash: d.hash
|
||||||
|
// }));
|
||||||
|
} else {
|
||||||
|
console.error('light-code 同步失败', queryRes.message);
|
||||||
|
diffList = codeFiles;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
diffList = codeFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const file of diffList) {
|
||||||
|
const tsPath = file.filepath;
|
||||||
const runRes = await runCode(tsPath, { path: 'router', key: 'list' }, { timeout: 10000 });
|
const runRes = await runCode(tsPath, { path: 'router', key: 'list' }, { timeout: 10000 });
|
||||||
if (runRes.success) {
|
if (runRes.success) {
|
||||||
const res = runRes.data;
|
const res = runRes.data;
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const list = res.data?.list || [];
|
const list = res.data?.list || [];
|
||||||
for (const routerItem of list) {
|
for (const routerItem of list) {
|
||||||
const pickValues = pick(routerItem, ['path', 'id', 'description', 'metadata']);
|
|
||||||
if (routerItem.path?.includes('auth') || routerItem.path?.includes('router') || routerItem.path?.includes('call')) {
|
if (routerItem.path?.includes('auth') || routerItem.path?.includes('router') || routerItem.path?.includes('call')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
console.log('lightcode 路由必须包含 path 和 id', pickValues);
|
// console.log(`注册 light-code 路由: [${routerItem.path}] ${routerItem.id} 来自文件: ${file.filepath}`);
|
||||||
console.log(`注册 lightcode 路由: ${routerItem.path} ${routerItem.id} 来自文件: ${file}`);
|
app.route({
|
||||||
|
id: routerItem.id,
|
||||||
|
path: routerItem.id!,
|
||||||
|
description: routerItem.description || '',
|
||||||
|
metadata: routerItem.metadata || {},
|
||||||
|
middleware: ['auth'],
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const tokenUser = ctx.state?.tokenUser || {};
|
||||||
|
const query = { ...ctx.query, tokenUser }
|
||||||
|
const runRes2 = await runCode(tsPath, query, { timeout: 30000 });
|
||||||
|
if (runRes2.success) {
|
||||||
|
const res2 = runRes2.data;
|
||||||
|
if (res2.code === 200) {
|
||||||
|
ctx.body = res2.data;
|
||||||
|
} else {
|
||||||
|
ctx.throw(res2.code, res2.message || 'Lightcode 路由执行失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.throw(runRes2.error || 'Lightcode 路由执行失败');
|
||||||
|
}
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
const runRes2 = await runCode(tsPath, { path: routerItem.path, key: routerItem.key, id: routerItem.id }, { timeout: 10000 });
|
|
||||||
console.log('lightcode 路由执行结果', runRes2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('lightcode 路由执行失败', runRes.error);
|
console.error('light-code 路由执行失败', runRes.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(`light-code 路由注册成功`, `注册${diffList.length}个路由`);
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ type RunCode = {
|
|||||||
};
|
};
|
||||||
error?: any
|
error?: any
|
||||||
timestamp?: string
|
timestamp?: string
|
||||||
|
output?: string
|
||||||
}
|
}
|
||||||
export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: RunCodeOptions): Promise<RunCode> => {
|
export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: RunCodeOptions): Promise<RunCode> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -42,7 +43,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let output = ''
|
||||||
const timeoutMs = opts?.timeout || 30000; // 默认30秒超时
|
const timeoutMs = opts?.timeout || 30000; // 默认30秒超时
|
||||||
|
|
||||||
let child
|
let child
|
||||||
@@ -69,6 +70,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
|||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
resolved = true
|
resolved = true
|
||||||
cleanup()
|
cleanup()
|
||||||
|
result.output = output
|
||||||
resolve(result)
|
resolve(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +78,11 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
|||||||
try {
|
try {
|
||||||
// 使用 Bun 的 fork 模式启动子进程
|
// 使用 Bun 的 fork 模式启动子进程
|
||||||
child = fork(tsPath, [], {
|
child = fork(tsPath, [], {
|
||||||
silent: true // 启用 stdio 重定向
|
silent: true, // 启用 stdio 重定向
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_CHILD_PROCESS: 'true' // 标记为子进程
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// 监听来自子进程的消息
|
// 监听来自子进程的消息
|
||||||
child.on('message', (msg: RunCode) => {
|
child.on('message', (msg: RunCode) => {
|
||||||
@@ -93,6 +99,11 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (child.stdout) {
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
output += data.toString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 监听子进程退出事件
|
// 监听子进程退出事件
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ export class RemoteApp {
|
|||||||
throw new Error('No id provided for remote app');
|
throw new Error('No id provided for remote app');
|
||||||
}
|
}
|
||||||
this.isError = false;
|
this.isError = false;
|
||||||
|
// 关闭已有连接
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
const ws = new WebSocket(this.getWsURL(this.url));
|
const ws = new WebSocket(this.getWsURL(this.url));
|
||||||
const that = this;
|
const that = this;
|
||||||
ws.onopen = function () {
|
ws.onopen = function () {
|
||||||
|
|||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -139,8 +139,8 @@ importers:
|
|||||||
specifier: ^0.0.13
|
specifier: ^0.0.13
|
||||||
version: 0.0.13(dotenv@17.2.3)(supports-color@10.2.2)
|
version: 0.0.13(dotenv@17.2.3)(supports-color@10.2.2)
|
||||||
'@opencode-ai/sdk':
|
'@opencode-ai/sdk':
|
||||||
specifier: ^1.1.35
|
specifier: ^1.1.36
|
||||||
version: 1.1.35
|
version: 1.1.36
|
||||||
es-toolkit:
|
es-toolkit:
|
||||||
specifier: ^1.44.0
|
specifier: ^1.44.0
|
||||||
version: 1.44.0
|
version: 1.44.0
|
||||||
@@ -191,8 +191,8 @@ importers:
|
|||||||
specifier: ^1.0.28
|
specifier: ^1.0.28
|
||||||
version: 1.0.28(dotenv@17.2.3)
|
version: 1.0.28(dotenv@17.2.3)
|
||||||
'@opencode-ai/plugin':
|
'@opencode-ai/plugin':
|
||||||
specifier: ^1.1.35
|
specifier: ^1.1.36
|
||||||
version: 1.1.35
|
version: 1.1.36
|
||||||
'@types/bun':
|
'@types/bun':
|
||||||
specifier: ^1.3.6
|
specifier: ^1.3.6
|
||||||
version: 1.3.6
|
version: 1.3.6
|
||||||
@@ -1468,12 +1468,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
'@opencode-ai/plugin@1.1.35':
|
'@opencode-ai/plugin@1.1.36':
|
||||||
resolution: {integrity: sha512-mn96oPcPxAjBcRuG/ivtJAOujJeyUPmL+D+/79Fs29MqIkfxJ/x+SVfNf8IXTFfkyt8FzZ3gF+Vuk1z/QjTkPA==}
|
resolution: {integrity: sha512-b2XWeFZN7UzgwkkzTIi6qSntkpEA9En2zvpqakQzZAGQm6QBdGAlv6r1u5hEnmF12Gzyj5umTMWr5GzVbP/oAA==}
|
||||||
|
|
||||||
'@opencode-ai/sdk@1.1.35':
|
'@opencode-ai/sdk@1.1.35':
|
||||||
resolution: {integrity: sha512-1RfjXvc5nguurpGXyKk8aJ4Rb3ix1IZ5V7itPB3SMq7c6OkmbE/5wzN2KUT9zATWj7ZDjmShkxEjvkRsOhodtw==}
|
resolution: {integrity: sha512-1RfjXvc5nguurpGXyKk8aJ4Rb3ix1IZ5V7itPB3SMq7c6OkmbE/5wzN2KUT9zATWj7ZDjmShkxEjvkRsOhodtw==}
|
||||||
|
|
||||||
|
'@opencode-ai/sdk@1.1.36':
|
||||||
|
resolution: {integrity: sha512-feNHWnbxhg03TI2QrWnw3Chc0eYrWSDSmHIy/ejpSVfcKlfXREw1Tpg0L4EjrpeSc4jB1eM673dh+WM/Ko2SFQ==}
|
||||||
|
|
||||||
'@oslojs/encoding@1.1.0':
|
'@oslojs/encoding@1.1.0':
|
||||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||||
|
|
||||||
@@ -6814,13 +6817,15 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.17.1
|
fastq: 1.17.1
|
||||||
|
|
||||||
'@opencode-ai/plugin@1.1.35':
|
'@opencode-ai/plugin@1.1.36':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opencode-ai/sdk': 1.1.35
|
'@opencode-ai/sdk': 1.1.36
|
||||||
zod: 4.1.8
|
zod: 4.1.8
|
||||||
|
|
||||||
'@opencode-ai/sdk@1.1.35': {}
|
'@opencode-ai/sdk@1.1.35': {}
|
||||||
|
|
||||||
|
'@opencode-ai/sdk@1.1.36': {}
|
||||||
|
|
||||||
'@oslojs/encoding@1.1.0': {}
|
'@oslojs/encoding@1.1.0': {}
|
||||||
|
|
||||||
'@peculiar/asn1-cms@2.6.0':
|
'@peculiar/asn1-cms@2.6.0':
|
||||||
|
|||||||
Reference in New Issue
Block a user