@skillfm/local 2.3.0 → 2.5.1
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/doctor.js.map +1 -1
- package/dist/guard/cli.d.ts.map +1 -1
- package/dist/guard/cli.js.map +1 -1
- package/dist/harness/kernels/deny-pipeline.js +1 -1
- package/dist/harness/kernels/deny-pipeline.js.map +1 -1
- package/dist/harness/writers.d.ts.map +1 -1
- package/dist/harness/writers.js +2 -0
- package/dist/harness/writers.js.map +1 -1
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-stdio/api-client.d.ts.map +1 -1
- package/dist/mcp-stdio/api-client.js +3 -0
- package/dist/mcp-stdio/api-client.js.map +1 -1
- package/dist/mcp-stdio/render-flow.d.ts.map +1 -1
- package/dist/mcp-stdio/render-flow.js.map +1 -1
- package/dist/mcp-stdio/request-context.d.ts.map +1 -1
- package/dist/mcp-stdio/request-context.js +1 -0
- package/dist/mcp-stdio/request-context.js.map +1 -1
- package/dist/mcp-stdio/server.d.ts.map +1 -1
- package/dist/mcp-stdio/server.js +75 -3
- package/dist/mcp-stdio/server.js.map +1 -1
- package/dist/mcp-stdio/sse-progress-client.js.map +1 -1
- package/dist/skill-installer/bundle-fetcher.d.ts +40 -0
- package/dist/skill-installer/bundle-fetcher.d.ts.map +1 -0
- package/dist/skill-installer/bundle-fetcher.js +133 -0
- package/dist/skill-installer/bundle-fetcher.js.map +1 -0
- package/dist/skill-installer/errors.d.ts +12 -0
- package/dist/skill-installer/errors.d.ts.map +1 -0
- package/dist/skill-installer/errors.js +42 -0
- package/dist/skill-installer/errors.js.map +1 -0
- package/dist/skill-installer/index.d.ts +20 -0
- package/dist/skill-installer/index.d.ts.map +1 -0
- package/dist/skill-installer/index.js +193 -0
- package/dist/skill-installer/index.js.map +1 -0
- package/dist/skill-installer/lockfile.d.ts +8 -0
- package/dist/skill-installer/lockfile.d.ts.map +1 -0
- package/dist/skill-installer/lockfile.js +52 -0
- package/dist/skill-installer/lockfile.js.map +1 -0
- package/dist/skill-installer/npm-installer.d.ts +16 -0
- package/dist/skill-installer/npm-installer.d.ts.map +1 -0
- package/dist/skill-installer/npm-installer.js +83 -0
- package/dist/skill-installer/npm-installer.js.map +1 -0
- package/dist/skill-installer/paths.d.ts +4 -0
- package/dist/skill-installer/paths.d.ts.map +1 -0
- package/dist/skill-installer/paths.js +16 -0
- package/dist/skill-installer/paths.js.map +1 -0
- package/dist/skill-installer/tar-extractor.d.ts +15 -0
- package/dist/skill-installer/tar-extractor.d.ts.map +1 -0
- package/dist/skill-installer/tar-extractor.js +56 -0
- package/dist/skill-installer/tar-extractor.js.map +1 -0
- package/dist/skill-md/template.js +2 -2
- package/dist/skill-runner/cli.d.ts +4 -0
- package/dist/skill-runner/cli.d.ts.map +1 -0
- package/dist/skill-runner/cli.js +81 -0
- package/dist/skill-runner/cli.js.map +1 -0
- package/dist/skill-runner/discovery.d.ts +3 -0
- package/dist/skill-runner/discovery.d.ts.map +1 -0
- package/dist/skill-runner/discovery.js +108 -0
- package/dist/skill-runner/discovery.js.map +1 -0
- package/dist/skill-runner/index.d.ts +9 -0
- package/dist/skill-runner/index.d.ts.map +1 -0
- package/dist/skill-runner/index.js +100 -0
- package/dist/skill-runner/index.js.map +1 -0
- package/dist/skill-runner/registry.d.ts +11 -0
- package/dist/skill-runner/registry.d.ts.map +1 -0
- package/dist/skill-runner/registry.js +79 -0
- package/dist/skill-runner/registry.js.map +1 -0
- package/dist/skill-runner/spawner.d.ts +14 -0
- package/dist/skill-runner/spawner.d.ts.map +1 -0
- package/dist/skill-runner/spawner.js +85 -0
- package/dist/skill-runner/spawner.js.map +1 -0
- package/dist/skill-runner/types.d.ts +62 -0
- package/dist/skill-runner/types.d.ts.map +1 -0
- package/dist/skill-runner/types.js +6 -0
- package/dist/skill-runner/types.js.map +1 -0
- package/dist/skill-tunnel/cli.d.ts +5 -0
- package/dist/skill-tunnel/cli.d.ts.map +1 -0
- package/dist/skill-tunnel/cli.js +205 -0
- package/dist/skill-tunnel/cli.js.map +1 -0
- package/dist/skill-tunnel/client.d.ts +56 -0
- package/dist/skill-tunnel/client.d.ts.map +1 -0
- package/dist/skill-tunnel/client.js +260 -0
- package/dist/skill-tunnel/client.js.map +1 -0
- package/dist/skill-tunnel/handshake.d.ts +35 -0
- package/dist/skill-tunnel/handshake.d.ts.map +1 -0
- package/dist/skill-tunnel/handshake.js +61 -0
- package/dist/skill-tunnel/handshake.js.map +1 -0
- package/dist/skill-tunnel/heartbeat.d.ts +34 -0
- package/dist/skill-tunnel/heartbeat.d.ts.map +1 -0
- package/dist/skill-tunnel/heartbeat.js +86 -0
- package/dist/skill-tunnel/heartbeat.js.map +1 -0
- package/dist/skill-tunnel/local-bridge.d.ts +30 -0
- package/dist/skill-tunnel/local-bridge.d.ts.map +1 -0
- package/dist/skill-tunnel/local-bridge.js +224 -0
- package/dist/skill-tunnel/local-bridge.js.map +1 -0
- package/dist/skill-tunnel/reconnect.d.ts +21 -0
- package/dist/skill-tunnel/reconnect.d.ts.map +1 -0
- package/dist/skill-tunnel/reconnect.js +72 -0
- package/dist/skill-tunnel/reconnect.js.map +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HeartbeatTunnelParams } from '@skillfm/contracts/skill-tunnel';
|
|
2
|
+
export interface HeartbeatOptions {
|
|
3
|
+
/** 构造每次 heartbeat 的 local_skill_status 快照 */
|
|
4
|
+
buildStatusSnapshot: () => HeartbeatTunnelParams['local_skill_status'];
|
|
5
|
+
/** 发 heartbeat request(由 client 负责 JSON-RPC 封装 + WS send)*/
|
|
6
|
+
sendHeartbeat: (params: HeartbeatTunnelParams) => Promise<void>;
|
|
7
|
+
/** 心跳超时回调(调用方通常触发重连)*/
|
|
8
|
+
onTimeout: () => void;
|
|
9
|
+
/** 可选: 测试时注入 clock */
|
|
10
|
+
nowMs?: () => number;
|
|
11
|
+
/** 可选: 覆盖 interval(默认 25s)*/
|
|
12
|
+
intervalMs?: number;
|
|
13
|
+
/** 可选: 覆盖 timeout(默认 60s)*/
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class HeartbeatLoop {
|
|
17
|
+
private readonly opts;
|
|
18
|
+
private interval;
|
|
19
|
+
private timeoutTimer;
|
|
20
|
+
private lastAckAt;
|
|
21
|
+
private started;
|
|
22
|
+
constructor(opts: HeartbeatOptions);
|
|
23
|
+
start(): void;
|
|
24
|
+
stop(): void;
|
|
25
|
+
/** 收到 heartbeat ack (JSON-RPC success response) —— 上层 client 调用 */
|
|
26
|
+
onAck(): void;
|
|
27
|
+
/** 暴露给测试: 手动触发一次 tick */
|
|
28
|
+
tick(): Promise<void>;
|
|
29
|
+
/** 暴露给测试: 手动触发一次超时检查 */
|
|
30
|
+
checkTimeout(): void;
|
|
31
|
+
private scheduleTimeoutCheck;
|
|
32
|
+
get lastAck(): number;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/skill-tunnel/heartbeat.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAE7E,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,mBAAmB,EAAE,MAAM,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;IACvE,4DAA4D;IAC5D,aAAa,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,uBAAuB;IACvB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,gBAAgB;IAKlC,KAAK,IAAI,IAAI;IAgBb,IAAI,IAAI,IAAI;IAYZ,mEAAmE;IACnE,KAAK,IAAI,IAAI;IAIb,yBAAyB;IACnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,wBAAwB;IACxB,YAAY,IAAI,IAAI;IAQpB,OAAO,CAAC,oBAAoB;IAY5B,IAAI,OAAO,IAAI,MAAM,CAEpB;CACF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// M2 SkillTunnelClient — Heartbeat 调度
|
|
2
|
+
//
|
|
3
|
+
// 行为 (DELTA §5 + TUNNEL_CONSTANTS):
|
|
4
|
+
// - 每 HEARTBEAT_INTERVAL_MS (25s) 主动发 tunnel.heartbeat JSON-RPC request
|
|
5
|
+
// - 记录 lastHeartbeatAckAt
|
|
6
|
+
// - 若 (now - lastHeartbeatAckAt) > HEARTBEAT_TIMEOUT_MS (60s) → 触发 onTimeout 回调
|
|
7
|
+
// - local_skill_status 快照从外部注入(来自 registry + isAlive)
|
|
8
|
+
import { TUNNEL_CONSTANTS } from '@skillfm/contracts/skill-tunnel';
|
|
9
|
+
export class HeartbeatLoop {
|
|
10
|
+
opts;
|
|
11
|
+
interval = null;
|
|
12
|
+
timeoutTimer = null;
|
|
13
|
+
lastAckAt;
|
|
14
|
+
started = false;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.opts = opts;
|
|
17
|
+
this.lastAckAt = (opts.nowMs ?? Date.now)();
|
|
18
|
+
}
|
|
19
|
+
start() {
|
|
20
|
+
if (this.started)
|
|
21
|
+
return;
|
|
22
|
+
this.started = true;
|
|
23
|
+
this.lastAckAt = (this.opts.nowMs ?? Date.now)();
|
|
24
|
+
const intervalMs = this.opts.intervalMs ?? TUNNEL_CONSTANTS.HEARTBEAT_INTERVAL_MS;
|
|
25
|
+
// 立即触发一次(可选,这里延到 intervalMs 后,避免 connect 瞬间抢跑)
|
|
26
|
+
this.interval = setInterval(() => {
|
|
27
|
+
void this.tick();
|
|
28
|
+
}, intervalMs);
|
|
29
|
+
if (typeof this.interval.unref === 'function') {
|
|
30
|
+
this.interval.unref();
|
|
31
|
+
}
|
|
32
|
+
// 启动 timeout 巡检
|
|
33
|
+
this.scheduleTimeoutCheck();
|
|
34
|
+
}
|
|
35
|
+
stop() {
|
|
36
|
+
this.started = false;
|
|
37
|
+
if (this.interval) {
|
|
38
|
+
clearInterval(this.interval);
|
|
39
|
+
this.interval = null;
|
|
40
|
+
}
|
|
41
|
+
if (this.timeoutTimer) {
|
|
42
|
+
clearTimeout(this.timeoutTimer);
|
|
43
|
+
this.timeoutTimer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** 收到 heartbeat ack (JSON-RPC success response) —— 上层 client 调用 */
|
|
47
|
+
onAck() {
|
|
48
|
+
this.lastAckAt = (this.opts.nowMs ?? Date.now)();
|
|
49
|
+
}
|
|
50
|
+
/** 暴露给测试: 手动触发一次 tick */
|
|
51
|
+
async tick() {
|
|
52
|
+
try {
|
|
53
|
+
const now = (this.opts.nowMs ?? Date.now)();
|
|
54
|
+
await this.opts.sendHeartbeat({
|
|
55
|
+
ts: now,
|
|
56
|
+
local_skill_status: this.opts.buildStatusSnapshot(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// 发送失败不打爆,让 timeout 检测兜底
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** 暴露给测试: 手动触发一次超时检查 */
|
|
64
|
+
checkTimeout() {
|
|
65
|
+
const now = (this.opts.nowMs ?? Date.now)();
|
|
66
|
+
const timeoutMs = this.opts.timeoutMs ?? TUNNEL_CONSTANTS.HEARTBEAT_TIMEOUT_MS;
|
|
67
|
+
if (now - this.lastAckAt > timeoutMs) {
|
|
68
|
+
this.opts.onTimeout();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
scheduleTimeoutCheck() {
|
|
72
|
+
const timeoutMs = this.opts.timeoutMs ?? TUNNEL_CONSTANTS.HEARTBEAT_TIMEOUT_MS;
|
|
73
|
+
// 每 (timeoutMs / 4) 检查一次
|
|
74
|
+
const pollMs = Math.max(1_000, Math.floor(timeoutMs / 4));
|
|
75
|
+
this.timeoutTimer = setInterval(() => {
|
|
76
|
+
this.checkTimeout();
|
|
77
|
+
}, pollMs);
|
|
78
|
+
if (typeof this.timeoutTimer.unref === 'function') {
|
|
79
|
+
this.timeoutTimer.unref();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
get lastAck() {
|
|
83
|
+
return this.lastAckAt;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../src/skill-tunnel/heartbeat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { JsonRpcErrorResponse, JsonRpcSuccessResponse } from '@skillfm/contracts/skill-tunnel';
|
|
2
|
+
export interface LocalBridgeDeps {
|
|
3
|
+
/** 通过 slug 查本地 endpoint */
|
|
4
|
+
resolveEndpoint: (slug: string) => string | null;
|
|
5
|
+
/** 与本地 skill daemon 共享的 HMAC secret(§3 auth)。
|
|
6
|
+
* 若给定,则在发往 127.0.0.1 的 HTTP 请求上加 X-Skill-Auth header。*/
|
|
7
|
+
skillAuthSecret?: string;
|
|
8
|
+
/** 单次 HTTP 请求超时(ms),默认 30s */
|
|
9
|
+
fetchTimeoutMs?: number;
|
|
10
|
+
/** 可注入,方便测试(默认 globalThis.fetch)*/
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
export declare class LocalBridge {
|
|
14
|
+
private readonly deps;
|
|
15
|
+
constructor(deps: LocalBridgeDeps);
|
|
16
|
+
/**
|
|
17
|
+
* 把 server→agent 的 JSON-RPC request 转发到本地 skill daemon。
|
|
18
|
+
* 返回 JsonRpcSuccessResponse 或 JsonRpcErrorResponse(本地不可达等)。
|
|
19
|
+
* 异常不抛出,一律封装成 error response —— 上层 client 按 id 回传。
|
|
20
|
+
*/
|
|
21
|
+
dispatch(id: string, method: string, params: unknown): Promise<JsonRpcSuccessResponse | JsonRpcErrorResponse>;
|
|
22
|
+
private isKnownServerMethod;
|
|
23
|
+
private handleSkillRun;
|
|
24
|
+
private handleToolInvoke;
|
|
25
|
+
private handleSkillPost;
|
|
26
|
+
private handleSkillGet;
|
|
27
|
+
private httpJson;
|
|
28
|
+
private errorResponse;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=local-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-bridge.d.ts","sourceRoot":"","sources":["../../src/skill-tunnel/local-bridge.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EACV,oBAAoB,EACpB,sBAAsB,EAIvB,MAAM,iCAAiC,CAAC;AAIzC,MAAM,WAAW,eAAe;IAC9B,2BAA2B;IAC3B,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD;6DACyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkB;gBAE3B,IAAI,EAAE,eAAe;IAIjC;;;;OAIG;IACG,QAAQ,CACZ,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,sBAAsB,GAAG,oBAAoB,CAAC;IAyDzD,OAAO,CAAC,mBAAmB;YAYb,cAAc;YAkBd,gBAAgB;YA2BhB,eAAe;YAmBf,cAAc;YAmBd,QAAQ;IAsEtB,OAAO,CAAC,aAAa;CAYtB"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// M2 SkillTunnelClient — local bridge
|
|
2
|
+
//
|
|
3
|
+
// 职责: 把 brain → agent 的 JSON-RPC request 转成本地 skill daemon 的 HTTP 调用。
|
|
4
|
+
// 跑在 127.0.0.1 loopback,cookie/profile 全部留在本节点 → 守 BYOK 红线。
|
|
5
|
+
//
|
|
6
|
+
// 映射规则 (对齐 BRAIN-SKILL-CONTRACT §3 HTTP 接口):
|
|
7
|
+
// skill.run → POST /v1/skill/run body = params.request
|
|
8
|
+
// skill.contribute_ctx → POST /v1/skill/contribute-ctx
|
|
9
|
+
// skill.declare_must_relay→ POST /v1/skill/declare-must-relay
|
|
10
|
+
// skill.manifest → GET /v1/skill/manifest
|
|
11
|
+
// skill.tool.invoke → POST /v1/skill/tool/<tool_name>
|
|
12
|
+
// skill.continuation.abort→ POST /v1/skill/continuation/abort
|
|
13
|
+
// skill.health → GET /health
|
|
14
|
+
//
|
|
15
|
+
// 认证: 给本地 skill daemon 发请求时带 X-Skill-Auth (HMAC) 头,
|
|
16
|
+
// 按现有 §3 auth_mode: hmac_shared_secret 规约。
|
|
17
|
+
//
|
|
18
|
+
// 失败路径: fetch 抛 ECONNREFUSED 或 timeout → 返回 TUNNEL_DEGRADED JSON-RPC error,
|
|
19
|
+
// 绝不 hang,绝不吞。
|
|
20
|
+
import { createHmac } from 'node:crypto';
|
|
21
|
+
import { TUNNEL_ERROR_CODES } from '@skillfm/contracts/skill-tunnel';
|
|
22
|
+
export class LocalBridge {
|
|
23
|
+
deps;
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.deps = deps;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 把 server→agent 的 JSON-RPC request 转发到本地 skill daemon。
|
|
29
|
+
* 返回 JsonRpcSuccessResponse 或 JsonRpcErrorResponse(本地不可达等)。
|
|
30
|
+
* 异常不抛出,一律封装成 error response —— 上层 client 按 id 回传。
|
|
31
|
+
*/
|
|
32
|
+
async dispatch(id, method, params) {
|
|
33
|
+
// 1. 白名单校验
|
|
34
|
+
if (!this.isKnownServerMethod(method)) {
|
|
35
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.METHOD_NOT_FOUND, `unknown method: ${method}`);
|
|
36
|
+
}
|
|
37
|
+
// 2. 按 method 路由
|
|
38
|
+
try {
|
|
39
|
+
const m = method;
|
|
40
|
+
switch (m) {
|
|
41
|
+
case 'skill.run':
|
|
42
|
+
return await this.handleSkillRun(id, params);
|
|
43
|
+
case 'skill.contribute_ctx':
|
|
44
|
+
return await this.handleSkillPost(id, params, '/v1/skill/contribute-ctx');
|
|
45
|
+
case 'skill.declare_must_relay':
|
|
46
|
+
return await this.handleSkillPost(id, params, '/v1/skill/declare-must-relay');
|
|
47
|
+
case 'skill.manifest':
|
|
48
|
+
return await this.handleSkillGet(id, params, '/v1/skill/manifest');
|
|
49
|
+
case 'skill.tool.invoke':
|
|
50
|
+
return await this.handleToolInvoke(id, params);
|
|
51
|
+
case 'skill.continuation.abort':
|
|
52
|
+
return await this.handleSkillPost(id, params, '/v1/skill/continuation/abort');
|
|
53
|
+
case 'skill.health':
|
|
54
|
+
return await this.handleSkillGet(id, params, '/health');
|
|
55
|
+
default: {
|
|
56
|
+
// 穷尽检查 —— TS 会在新加 method 时报错
|
|
57
|
+
const _exhaustive = m;
|
|
58
|
+
void _exhaustive;
|
|
59
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.METHOD_NOT_FOUND, `unroutable method: ${method}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const e = err;
|
|
65
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.INTERNAL_ERROR, `local-bridge dispatch error: ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
isKnownServerMethod(method) {
|
|
69
|
+
return [
|
|
70
|
+
'skill.run',
|
|
71
|
+
'skill.contribute_ctx',
|
|
72
|
+
'skill.declare_must_relay',
|
|
73
|
+
'skill.manifest',
|
|
74
|
+
'skill.tool.invoke',
|
|
75
|
+
'skill.continuation.abort',
|
|
76
|
+
'skill.health',
|
|
77
|
+
].includes(method);
|
|
78
|
+
}
|
|
79
|
+
async handleSkillRun(id, params) {
|
|
80
|
+
if (!params?.skill_slug) {
|
|
81
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.INVALID_PARAMS, 'missing skill_slug');
|
|
82
|
+
}
|
|
83
|
+
const endpoint = this.deps.resolveEndpoint(params.skill_slug);
|
|
84
|
+
if (!endpoint) {
|
|
85
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.METHOD_NOT_FOUND, `skill not registered locally: ${params.skill_slug}`);
|
|
86
|
+
}
|
|
87
|
+
return this.httpJson(id, 'POST', `${endpoint}/v1/skill/run`, params.request);
|
|
88
|
+
}
|
|
89
|
+
async handleToolInvoke(id, params) {
|
|
90
|
+
if (!params?.skill_slug || !params?.tool_name) {
|
|
91
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.INVALID_PARAMS, 'missing skill_slug or tool_name');
|
|
92
|
+
}
|
|
93
|
+
const endpoint = this.deps.resolveEndpoint(params.skill_slug);
|
|
94
|
+
if (!endpoint) {
|
|
95
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.METHOD_NOT_FOUND, `skill not registered locally: ${params.skill_slug}`);
|
|
96
|
+
}
|
|
97
|
+
const path = `/v1/skill/tool/${encodeURIComponent(params.tool_name)}`;
|
|
98
|
+
return this.httpJson(id, 'POST', `${endpoint}${path}`, {
|
|
99
|
+
args: params.args,
|
|
100
|
+
context: params.context,
|
|
101
|
+
request_id: params.request_id,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async handleSkillPost(id, params, path) {
|
|
105
|
+
if (!params?.skill_slug) {
|
|
106
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.INVALID_PARAMS, 'missing skill_slug');
|
|
107
|
+
}
|
|
108
|
+
const endpoint = this.deps.resolveEndpoint(params.skill_slug);
|
|
109
|
+
if (!endpoint) {
|
|
110
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.METHOD_NOT_FOUND, `skill not registered locally: ${params.skill_slug}`);
|
|
111
|
+
}
|
|
112
|
+
return this.httpJson(id, 'POST', `${endpoint}${path}`, params);
|
|
113
|
+
}
|
|
114
|
+
async handleSkillGet(id, params, path) {
|
|
115
|
+
if (!params?.skill_slug) {
|
|
116
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.INVALID_PARAMS, 'missing skill_slug');
|
|
117
|
+
}
|
|
118
|
+
const endpoint = this.deps.resolveEndpoint(params.skill_slug);
|
|
119
|
+
if (!endpoint) {
|
|
120
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.METHOD_NOT_FOUND, `skill not registered locally: ${params.skill_slug}`);
|
|
121
|
+
}
|
|
122
|
+
return this.httpJson(id, 'GET', `${endpoint}${path}`, null);
|
|
123
|
+
}
|
|
124
|
+
async httpJson(id, httpMethod, url, body) {
|
|
125
|
+
const fetchImpl = this.deps.fetch ?? globalThis.fetch;
|
|
126
|
+
const timeoutMs = this.deps.fetchTimeoutMs ?? 30_000;
|
|
127
|
+
const headers = {};
|
|
128
|
+
let payload;
|
|
129
|
+
if (httpMethod === 'POST') {
|
|
130
|
+
headers['content-type'] = 'application/json';
|
|
131
|
+
payload = JSON.stringify(body ?? {});
|
|
132
|
+
if (this.deps.skillAuthSecret) {
|
|
133
|
+
// HMAC 对 raw body 签名 —— 与 platform §3 auth 规约保持一致
|
|
134
|
+
headers['x-skill-auth'] = createHmac('sha256', this.deps.skillAuthSecret)
|
|
135
|
+
.update(payload)
|
|
136
|
+
.digest('hex');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetchImpl(url, {
|
|
143
|
+
method: httpMethod,
|
|
144
|
+
headers,
|
|
145
|
+
body: payload,
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
});
|
|
148
|
+
const text = await res.text();
|
|
149
|
+
let data;
|
|
150
|
+
try {
|
|
151
|
+
data = text ? JSON.parse(text) : null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// 非 JSON —— 把 raw 包进 data,不视为致命错
|
|
155
|
+
data = { raw: text };
|
|
156
|
+
}
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.SKILL_APP_ERROR, `local skill returned HTTP ${res.status}`, { http_status: res.status, data });
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
jsonrpc: '2.0',
|
|
162
|
+
id,
|
|
163
|
+
result: data,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const e = err;
|
|
168
|
+
const isAbort = e.name === 'AbortError';
|
|
169
|
+
// undici fetch wraps underlying socket error into `cause`; walk the chain
|
|
170
|
+
// to find ECONNREFUSED / ENOTFOUND / ECONNRESET / EHOSTUNREACH.
|
|
171
|
+
const isUnreachable = detectUnreachable(e);
|
|
172
|
+
const msg = isAbort
|
|
173
|
+
? `local skill timeout after ${timeoutMs}ms (${url})`
|
|
174
|
+
: isUnreachable
|
|
175
|
+
? `local skill unreachable: ${url} (${describeUnreachable(e)})`
|
|
176
|
+
: `local skill unreachable: ${url} (${e.message})`;
|
|
177
|
+
// 所有 fetch 网络层失败都映射为 TUNNEL_DEGRADED —— 契约 §4b 要求不 hang
|
|
178
|
+
return this.errorResponse(id, TUNNEL_ERROR_CODES.TUNNEL_DEGRADED, msg);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
errorResponse(id, code, message, data) {
|
|
185
|
+
return {
|
|
186
|
+
jsonrpc: '2.0',
|
|
187
|
+
id,
|
|
188
|
+
error: { code, message, data },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const UNREACHABLE_CODES = new Set([
|
|
193
|
+
'ECONNREFUSED',
|
|
194
|
+
'ENOTFOUND',
|
|
195
|
+
'ECONNRESET',
|
|
196
|
+
'EHOSTUNREACH',
|
|
197
|
+
'EAI_AGAIN',
|
|
198
|
+
'UND_ERR_SOCKET',
|
|
199
|
+
]);
|
|
200
|
+
function detectUnreachable(err) {
|
|
201
|
+
let cur = err;
|
|
202
|
+
let depth = 0;
|
|
203
|
+
while (cur && depth < 6) {
|
|
204
|
+
const c = cur;
|
|
205
|
+
if (c.code && UNREACHABLE_CODES.has(c.code))
|
|
206
|
+
return true;
|
|
207
|
+
cur = c.cause;
|
|
208
|
+
depth += 1;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
function describeUnreachable(err) {
|
|
213
|
+
let cur = err;
|
|
214
|
+
let depth = 0;
|
|
215
|
+
while (cur && depth < 6) {
|
|
216
|
+
const c = cur;
|
|
217
|
+
if (c.code && UNREACHABLE_CODES.has(c.code))
|
|
218
|
+
return c.code;
|
|
219
|
+
cur = c.cause;
|
|
220
|
+
depth += 1;
|
|
221
|
+
}
|
|
222
|
+
return 'ECONNREFUSED';
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=local-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-bridge.js","sourceRoot":"","sources":["../../src/skill-tunnel/local-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** 不应重连的 close code 集合 */
|
|
2
|
+
export declare const FATAL_CLOSE_CODES: Set<number>;
|
|
3
|
+
export declare function isFatalCloseCode(code: number | null | undefined): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* 给定第 N 次重试(0-indexed),返回延迟 ms。
|
|
6
|
+
* 可注入 jitterFn 方便测试(默认 Math.random)。
|
|
7
|
+
*/
|
|
8
|
+
export declare function computeReconnectDelayMs(attempt: number, jitterFn?: () => number): number;
|
|
9
|
+
/**
|
|
10
|
+
* 简易调度器 —— 包装 setTimeout,便于测试时可注入 fake clock。
|
|
11
|
+
*/
|
|
12
|
+
export declare class ReconnectScheduler {
|
|
13
|
+
private timer;
|
|
14
|
+
private attempt;
|
|
15
|
+
private stopped;
|
|
16
|
+
schedule(fn: () => void): void;
|
|
17
|
+
reset(): void;
|
|
18
|
+
stop(): void;
|
|
19
|
+
get attempts(): number;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=reconnect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnect.d.ts","sourceRoot":"","sources":["../../src/skill-tunnel/reconnect.ts"],"names":[],"mappings":"AAaA,0BAA0B;AAC1B,eAAO,MAAM,iBAAiB,aAK5B,CAAC;AAEH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAM,MAAoB,GACnC,MAAM,CASR;AAED;;GAEG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,OAAO,CAAS;IAExB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAc9B,KAAK,IAAI,IAAI;IAQb,IAAI,IAAI,IAAI;IAQZ,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// M2 SkillTunnelClient — 指数退避重连调度
|
|
2
|
+
//
|
|
3
|
+
// 策略 (DELTA §5 + TUNNEL_CONSTANTS):
|
|
4
|
+
// delay = INITIAL × 2^attempt, 上限 MAX
|
|
5
|
+
// jitter ±30% (避免多 agent 同时重连导致 brain 侧抖动)
|
|
6
|
+
// 致命 close code (4001/4002/4003/4101) 不重连 —— 要么签名错,要么 token 吊销,要么版本不兼容,
|
|
7
|
+
// 这些情况重连只会继续失败,必须人工介入 → 输出到 stderr 供 agent 看到。
|
|
8
|
+
import { TUNNEL_CLOSE_CODES, TUNNEL_CONSTANTS, } from '@skillfm/contracts/skill-tunnel';
|
|
9
|
+
/** 不应重连的 close code 集合 */
|
|
10
|
+
export const FATAL_CLOSE_CODES = new Set([
|
|
11
|
+
TUNNEL_CLOSE_CODES.AUTH_BAD_SIGNATURE,
|
|
12
|
+
TUNNEL_CLOSE_CODES.AUTH_NONCE_REUSED,
|
|
13
|
+
TUNNEL_CLOSE_CODES.AGENT_TOKEN_REVOKED,
|
|
14
|
+
TUNNEL_CLOSE_CODES.VERSION_UNSUPPORTED,
|
|
15
|
+
]);
|
|
16
|
+
export function isFatalCloseCode(code) {
|
|
17
|
+
if (typeof code !== 'number')
|
|
18
|
+
return false;
|
|
19
|
+
return FATAL_CLOSE_CODES.has(code);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 给定第 N 次重试(0-indexed),返回延迟 ms。
|
|
23
|
+
* 可注入 jitterFn 方便测试(默认 Math.random)。
|
|
24
|
+
*/
|
|
25
|
+
export function computeReconnectDelayMs(attempt, jitterFn = Math.random) {
|
|
26
|
+
const base = Math.min(TUNNEL_CONSTANTS.RECONNECT_INITIAL_DELAY_MS * 2 ** attempt, TUNNEL_CONSTANTS.RECONNECT_MAX_DELAY_MS);
|
|
27
|
+
// jitter ±30%
|
|
28
|
+
const jitterRange = base * 0.3;
|
|
29
|
+
const jitter = (jitterFn() * 2 - 1) * jitterRange;
|
|
30
|
+
return Math.max(0, Math.round(base + jitter));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 简易调度器 —— 包装 setTimeout,便于测试时可注入 fake clock。
|
|
34
|
+
*/
|
|
35
|
+
export class ReconnectScheduler {
|
|
36
|
+
timer = null;
|
|
37
|
+
attempt = 0;
|
|
38
|
+
stopped = false;
|
|
39
|
+
schedule(fn) {
|
|
40
|
+
if (this.stopped)
|
|
41
|
+
return;
|
|
42
|
+
const delay = computeReconnectDelayMs(this.attempt);
|
|
43
|
+
this.attempt += 1;
|
|
44
|
+
this.timer = setTimeout(() => {
|
|
45
|
+
this.timer = null;
|
|
46
|
+
if (!this.stopped)
|
|
47
|
+
fn();
|
|
48
|
+
}, delay);
|
|
49
|
+
// 不让 reconnect timer 阻塞 node exit
|
|
50
|
+
if (typeof this.timer.unref === 'function') {
|
|
51
|
+
this.timer.unref();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
reset() {
|
|
55
|
+
this.attempt = 0;
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
this.stopped = true;
|
|
63
|
+
if (this.timer) {
|
|
64
|
+
clearTimeout(this.timer);
|
|
65
|
+
this.timer = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
get attempts() {
|
|
69
|
+
return this.attempt;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=reconnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnect.js","sourceRoot":"","sources":["../../src/skill-tunnel/reconnect.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,oCAAoC;AACpC,wCAAwC;AACxC,6CAA6C;AAC7C,0EAA0E;AAC1E,iDAAiD;AAEjD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,iCAAiC,CAAC;AAEzC,0BAA0B;AAC1B,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS;IAC/C,kBAAkB,CAAC,kBAAkB;IACrC,kBAAkB,CAAC,iBAAiB;IACpC,kBAAkB,CAAC,mBAAmB;IACtC,kBAAkB,CAAC,mBAAmB;CACvC,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,IAA+B;IAC9D,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3C,OAAO,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAe,EACf,WAAyB,IAAI,CAAC,MAAM;IAEpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CACnB,gBAAgB,CAAC,0BAA0B,GAAG,CAAC,IAAI,OAAO,EAC1D,gBAAgB,CAAC,sBAAsB,CACxC,CAAC;IACF,cAAc;IACd,MAAM,WAAW,GAAG,IAAI,GAAG,GAAG,CAAC;IAC/B,MAAM,MAAM,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC;IAClD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,kBAAkB;IACrB,KAAK,GAA0B,IAAI,CAAC;IACpC,OAAO,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,KAAK,CAAC;IAExB,QAAQ,CAAC,EAAc;QACrB,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,EAAE,EAAE,CAAC;QAC1B,CAAC,EAAE,KAAK,CAAC,CAAC;QACV,kCAAkC;QAClC,IAAI,OAAQ,IAAI,CAAC,KAA2C,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACjF,IAAI,CAAC,KAA0C,CAAC,KAAK,EAAE,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;QACjB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillfm/local",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "SkillFM local sidecar — a tiny localhost HTTP proxy that lets any AI agent activate and run SkillFM skills in the current conversation without restarting its MCP runtime. Writes ~/.skillfm/local.json for service discovery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,17 +40,20 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
41
|
"start": "node dist/index.js start",
|
|
42
42
|
"prepublishOnly": "npm run build",
|
|
43
|
-
"test": "tsx --test src/harness/detector.test.ts src/harness/priming.test.ts src/harness/writers.test.ts src/guard/state.test.ts src/soul.test.ts src/soul-security.test.ts src/harness/kernels/registry.test.ts src/harness/kernels/deny-pipeline.test.ts src/mcp-output/builder.test.ts src/mcp-output/deny-review.test.ts src/mcp-output/integration.test.ts src/skill-md/template.test.ts src/skill-md/writer.test.ts src/mcp-stdio/smoke.test.ts src/mcp-stdio/render-flow.contract.test.ts src/mcp-stdio/render-aha.test.ts src/mcp-stdio/sse-progress-client.test.ts src/mcp-stdio/parse-tolerant-json.test.ts src/self-upgrade.test.ts",
|
|
43
|
+
"test": "tsx --test src/harness/detector.test.ts src/harness/priming.test.ts src/harness/writers.test.ts src/guard/state.test.ts src/soul.test.ts src/soul-security.test.ts src/harness/kernels/registry.test.ts src/harness/kernels/deny-pipeline.test.ts src/mcp-output/builder.test.ts src/mcp-output/deny-review.test.ts src/mcp-output/integration.test.ts src/skill-md/template.test.ts src/skill-md/writer.test.ts src/mcp-stdio/smoke.test.ts src/mcp-stdio/render-flow.contract.test.ts src/mcp-stdio/render-aha.test.ts src/mcp-stdio/sse-progress-client.test.ts src/mcp-stdio/parse-tolerant-json.test.ts src/self-upgrade.test.ts src/skill-tunnel/local-bridge.test.ts src/skill-tunnel/client.test.ts src/skill-runner/discovery.test.ts src/skill-installer/index.test.ts",
|
|
44
44
|
"test:acceptance": "npm run build && bash scripts/acceptance-matrix.sh",
|
|
45
45
|
"postpublish": "curl -sS -m 10 -X PUT 'https://registry.npmmirror.com/-/package/@skillfm/local/syncs?sync_upstream=true' > /dev/null && echo '✓ npmmirror sync triggered' || echo '⚠ npmmirror sync failed (非阻塞,可手动触发)'"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
49
|
+
"tar": "^7.5.0",
|
|
50
|
+
"ws": "^8.20.0",
|
|
49
51
|
"zod": "^3.23.0"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@skillfm/contracts": "*",
|
|
53
55
|
"@types/node": "^22.19.17",
|
|
56
|
+
"@types/ws": "^8.5.10",
|
|
54
57
|
"tsx": "^4.0.0",
|
|
55
58
|
"typescript": "^5.4.0"
|
|
56
59
|
},
|