@minimalcorp/tsunagi 0.1.23 → 0.1.25
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/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0cwejmp._.js → [root-of-the-server]__053-iaj._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__07kf5h-._.js +13 -13
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_gamc9._.js +13 -13
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0a7t~wn._.js → [root-of-the-server]__0tq9wv6._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0ie-fls._.js → _03hz06e._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0gpi5iq._.js → _0836rl_._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_06gug4~._.js → _0amxree._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +17 -17
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s64~o2._.js → node_modules_00b-qxe._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0-dhyl6._.js → node_modules_046ca89._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0w5pf1p._.js → node_modules_065xsw3._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0-ecrjy._.js → node_modules_0b547kr._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0.7pxhy._.js → node_modules_0c_-8tt._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0efb87~._.js → node_modules_0dmu62y._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0of7.-5._.js → node_modules_0gs4d2e._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0j3gfu0._.js → node_modules_0jcrj-2._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0uwlz8.._.js → node_modules_0kmpnnk._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0126g83._.js → node_modules_0llz_9l._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zmomoe._.js → node_modules_0pi.005._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0.6~k__._.js → node_modules_0v86kpj._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ybihhp._.js → node_modules_0vm.f2d._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_11gk90a._.js → node_modules_0y3g5lf._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0r6ewmn._.js → node_modules_0z0orm9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06iewnh._.js → node_modules_0~n91z-._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0592-patkjl3_.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/08dtfekvxgpfu.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0915n6rwhsagp.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0922av8h192l7.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0e9140l311w8t.js → 0dpeys_k2-mrk.js} +15 -15
- package/.next/standalone/apps/web/.next/static/chunks/0hvtxos.brr01.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0kw1z8z_ld6s0.js → 0nh1-0lee-1pk.js} +15 -15
- package/.next/standalone/apps/web/.next/static/chunks/0wxrfu5la_s94.js +64 -0
- package/.next/standalone/apps/web/.next/static/chunks/{10ildl1-mw9ez.js → 0xxgdzi2vipaj.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0zu48yvkl8pyj.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/13twvr.qg3khh.js +1 -0
- package/dist/server/index.js +8 -1
- package/dist/server/lib/claude-config-guard.d.ts +9 -0
- package/dist/server/lib/claude-config-guard.js +43 -0
- package/dist/server/lib/llm-process.d.ts +29 -0
- package/dist/server/lib/llm-process.js +235 -0
- package/dist/server/lib/process-port.d.ts +1 -0
- package/dist/server/lib/process-port.js +28 -0
- package/dist/server/lib/whisper-process.d.ts +2 -2
- package/dist/server/lib/whisper-process.js +19 -5
- package/dist/server/routes/llm.d.ts +2 -0
- package/dist/server/routes/llm.js +23 -0
- package/dist/server/routes/terminal.js +5 -0
- package/dist/server/routes/whisper.js +36 -2
- package/llm-server/README.md +27 -0
- package/llm-server/download_model.py +14 -0
- package/llm-server/requirements.txt +1 -0
- package/llm-server/run.sh +26 -0
- package/npm-shrinkwrap.json +24 -40
- package/package.json +2 -1
- package/whisper-server/server.py +25 -4
- package/.next/standalone/apps/web/.next/static/chunks/0-nditm2kmxy3.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/00vlzdkbq6_w3.js +0 -64
- package/.next/standalone/apps/web/.next/static/chunks/0krkukgs6hg9c.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0sy8tzk6r-pb~.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0vfrsmi1vl53y.css +0 -4
- /package/.next/standalone/apps/web/.next/static/{S24zAyBAidp_ISALDwbn4 → qMzGt_O2oG-7wQXWYfQUB}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{S24zAyBAidp_ISALDwbn4 → qMzGt_O2oG-7wQXWYfQUB}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{S24zAyBAidp_ISALDwbn4 → qMzGt_O2oG-7wQXWYfQUB}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { killProcessOnPort } from './process-port.js';
|
|
7
|
+
const MODEL = 'mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit';
|
|
8
|
+
const LLM_SERVER_URL = process.env.TSUNAGI_LLM_SERVER_URL || 'http://127.0.0.1:8766';
|
|
9
|
+
// run.shが待受けるポート固定値(host.docker.internal経由URLと違い、停止処理は
|
|
10
|
+
// 必ずこのNodeプロセスと同じホスト上のポートを対象にする必要があるため分けて持つ)。
|
|
11
|
+
const LLM_PORT = 8766;
|
|
12
|
+
export function getLlmServerUrl() {
|
|
13
|
+
return LLM_SERVER_URL;
|
|
14
|
+
}
|
|
15
|
+
// venv・モデルキャッシュとも ~/.tsunagi/llm 配下にまとめる(run.shと同じ場所)。
|
|
16
|
+
const TSUNAGI_LLM_DIR = path.join(os.homedir(), '.tsunagi', 'llm');
|
|
17
|
+
const VENV_DIR = path.join(TSUNAGI_LLM_DIR, 'venv');
|
|
18
|
+
const HF_CACHE_DIR = path.join(TSUNAGI_LLM_DIR, 'cache');
|
|
19
|
+
const MODEL_CACHE_DIR = path.join(HF_CACHE_DIR, 'hub', 'models--mlx-community--Qwen3-30B-A3B-Instruct-2507-4bit');
|
|
20
|
+
// 実測値(2026-07時点、4bit量子化された重み一式の合計)。多少の変動はあるがETA計算の目安として使う。
|
|
21
|
+
const EXPECTED_MODEL_BYTES = 17_200_000_000;
|
|
22
|
+
// このファイルは apps/server/src/lib (dev) または apps/cli/dist/server/lib
|
|
23
|
+
// (npm配布物) のいずれかにいる。どちらの場合も3階層上に llm-server が
|
|
24
|
+
// 兄弟ディレクトリとして存在するようレイアウトを揃えている
|
|
25
|
+
// (apps/server/dist/lib → apps/server → apps → apps/llm-server,
|
|
26
|
+
// apps/cli/dist/server/lib → apps/cli/dist → apps/cli → apps/cli/llm-server)。
|
|
27
|
+
export function findLlmServerDir() {
|
|
28
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const candidate = path.join(here, '..', '..', '..', 'llm-server');
|
|
30
|
+
return fs.existsSync(path.join(candidate, 'run.sh')) ? candidate : null;
|
|
31
|
+
}
|
|
32
|
+
function venvPython() {
|
|
33
|
+
return path.join(VENV_DIR, 'bin', 'python3');
|
|
34
|
+
}
|
|
35
|
+
function isVenvReady() {
|
|
36
|
+
return fs.existsSync(venvPython());
|
|
37
|
+
}
|
|
38
|
+
function isModelReady() {
|
|
39
|
+
const snapshotsDir = path.join(MODEL_CACHE_DIR, 'snapshots');
|
|
40
|
+
if (!fs.existsSync(snapshotsDir))
|
|
41
|
+
return false;
|
|
42
|
+
return fs.readdirSync(snapshotsDir).length > 0;
|
|
43
|
+
}
|
|
44
|
+
let currentStep = 'not_running';
|
|
45
|
+
let downloadProgress;
|
|
46
|
+
let lastError;
|
|
47
|
+
let managedProcess = null;
|
|
48
|
+
let setupPromise = null;
|
|
49
|
+
function sleep(ms) {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
}
|
|
52
|
+
async function checkHealth() {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${LLM_SERVER_URL}/health`, {
|
|
55
|
+
signal: AbortSignal.timeout(2000),
|
|
56
|
+
});
|
|
57
|
+
return response.ok;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function getLlmServerStatus() {
|
|
64
|
+
const serverDir = findLlmServerDir();
|
|
65
|
+
// セットアップ/起動フロー進行中はそのステップをそのまま報告する。
|
|
66
|
+
if (setupPromise) {
|
|
67
|
+
return { step: currentStep, serverDir, downloadProgress, error: lastError };
|
|
68
|
+
}
|
|
69
|
+
const healthy = await checkHealth();
|
|
70
|
+
if (healthy) {
|
|
71
|
+
return { step: managedProcess ? 'running' : 'running_external', serverDir };
|
|
72
|
+
}
|
|
73
|
+
return { step: lastError ? 'error' : 'not_running', serverDir, error: lastError };
|
|
74
|
+
}
|
|
75
|
+
function runStep(cmd, args, cwd, env) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const child = spawn(cmd, args, { cwd, stdio: 'ignore', env: env ?? process.env });
|
|
78
|
+
child.on('exit', (code) => {
|
|
79
|
+
if (code === 0)
|
|
80
|
+
resolve();
|
|
81
|
+
else
|
|
82
|
+
reject(new Error(`${path.basename(cmd)} ${args.join(' ')} exited with code ${code}`));
|
|
83
|
+
});
|
|
84
|
+
child.on('error', reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// モデルDL中(huggingface_hubがblobs/配下に *.incomplete を書き続ける)のファイルサイズを
|
|
88
|
+
// 定期的にポーリングし、進捗(%)と直近の転送速度からのETAを算出する。
|
|
89
|
+
async function trackModelDownload(child) {
|
|
90
|
+
const blobsDir = path.join(MODEL_CACHE_DIR, 'blobs');
|
|
91
|
+
let lastBytes = 0;
|
|
92
|
+
let lastTime = Date.now();
|
|
93
|
+
let stopped = false;
|
|
94
|
+
child.on('exit', () => {
|
|
95
|
+
stopped = true;
|
|
96
|
+
});
|
|
97
|
+
while (!stopped) {
|
|
98
|
+
await sleep(1000);
|
|
99
|
+
let downloaded = 0;
|
|
100
|
+
try {
|
|
101
|
+
for (const f of fs.readdirSync(blobsDir)) {
|
|
102
|
+
if (!f.endsWith('.incomplete'))
|
|
103
|
+
continue;
|
|
104
|
+
downloaded += fs.statSync(path.join(blobsDir, f)).size;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// blobsDir がまだ作られていない場合は0のまま次のポーリングへ
|
|
109
|
+
}
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const elapsedSec = (now - lastTime) / 1000;
|
|
112
|
+
const bytesPerSec = elapsedSec > 0 ? (downloaded - lastBytes) / elapsedSec : 0;
|
|
113
|
+
const remaining = EXPECTED_MODEL_BYTES - downloaded;
|
|
114
|
+
const etaSeconds = bytesPerSec > 0 ? Math.max(0, Math.round(remaining / bytesPerSec)) : null;
|
|
115
|
+
downloadProgress = {
|
|
116
|
+
downloadedBytes: downloaded,
|
|
117
|
+
totalBytes: EXPECTED_MODEL_BYTES,
|
|
118
|
+
etaSeconds,
|
|
119
|
+
};
|
|
120
|
+
lastBytes = downloaded;
|
|
121
|
+
lastTime = now;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function runSetupAndStart(dir) {
|
|
125
|
+
const hfEnv = { ...process.env, HF_HOME: HF_CACHE_DIR, HF_HUB_DISABLE_XET: '1' };
|
|
126
|
+
if (!isVenvReady()) {
|
|
127
|
+
currentStep = 'installing_deps';
|
|
128
|
+
fs.mkdirSync(TSUNAGI_LLM_DIR, { recursive: true });
|
|
129
|
+
await runStep('python3', ['-m', 'venv', VENV_DIR], dir);
|
|
130
|
+
await runStep(venvPython(), ['-m', 'pip', 'install', '-r', 'requirements.txt'], dir);
|
|
131
|
+
}
|
|
132
|
+
if (!isModelReady()) {
|
|
133
|
+
currentStep = 'downloading_model';
|
|
134
|
+
downloadProgress = { downloadedBytes: 0, totalBytes: EXPECTED_MODEL_BYTES, etaSeconds: null };
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
const child = spawn(venvPython(), ['download_model.py'], {
|
|
137
|
+
cwd: dir,
|
|
138
|
+
stdio: 'ignore',
|
|
139
|
+
env: hfEnv,
|
|
140
|
+
});
|
|
141
|
+
void trackModelDownload(child);
|
|
142
|
+
child.on('exit', (code) => {
|
|
143
|
+
if (code === 0)
|
|
144
|
+
resolve();
|
|
145
|
+
else
|
|
146
|
+
reject(new Error(`download_model.py exited with code ${code}`));
|
|
147
|
+
});
|
|
148
|
+
child.on('error', reject);
|
|
149
|
+
});
|
|
150
|
+
downloadProgress = undefined;
|
|
151
|
+
}
|
|
152
|
+
currentStep = 'starting_server';
|
|
153
|
+
const child = spawn(venvPython(), ['-m', 'mlx_lm.server', '--model', MODEL, '--host', '127.0.0.1', '--port', String(LLM_PORT)], { cwd: dir, stdio: 'ignore', env: hfEnv });
|
|
154
|
+
child.on('exit', () => {
|
|
155
|
+
if (managedProcess === child)
|
|
156
|
+
managedProcess = null;
|
|
157
|
+
});
|
|
158
|
+
child.on('error', () => {
|
|
159
|
+
if (managedProcess === child)
|
|
160
|
+
managedProcess = null;
|
|
161
|
+
});
|
|
162
|
+
managedProcess = child;
|
|
163
|
+
// MoEで実計算はアクティブパラメータ(~3B)分のみだが、4bit量子化でも~17GBの
|
|
164
|
+
// 重みファイル自体はメモリへ展開する必要があるため、whisperより長めに待つ。
|
|
165
|
+
const start = Date.now();
|
|
166
|
+
while (Date.now() - start < 90000) {
|
|
167
|
+
if (await checkHealth())
|
|
168
|
+
return;
|
|
169
|
+
await sleep(1000);
|
|
170
|
+
}
|
|
171
|
+
throw new Error('Server did not become healthy within 90s of starting');
|
|
172
|
+
}
|
|
173
|
+
export function startLlmServer() {
|
|
174
|
+
if (setupPromise || managedProcess) {
|
|
175
|
+
return { started: false, error: 'Already starting/running' };
|
|
176
|
+
}
|
|
177
|
+
const dir = findLlmServerDir();
|
|
178
|
+
if (!dir) {
|
|
179
|
+
return { started: false, error: 'llm-server directory not found' };
|
|
180
|
+
}
|
|
181
|
+
lastError = undefined;
|
|
182
|
+
setupPromise = runSetupAndStart(dir)
|
|
183
|
+
.catch((error) => {
|
|
184
|
+
currentStep = 'error';
|
|
185
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
186
|
+
})
|
|
187
|
+
.finally(() => {
|
|
188
|
+
setupPromise = null;
|
|
189
|
+
});
|
|
190
|
+
return { started: true };
|
|
191
|
+
}
|
|
192
|
+
export async function stopLlmServer() {
|
|
193
|
+
if (managedProcess) {
|
|
194
|
+
managedProcess.kill();
|
|
195
|
+
managedProcess = null;
|
|
196
|
+
currentStep = 'not_running';
|
|
197
|
+
return { stopped: true };
|
|
198
|
+
}
|
|
199
|
+
// tsunagi外(make llm等)で起動された場合はchild_processのハンドルを
|
|
200
|
+
// 持たないため、ポート番号を手がかりにOS側から見つけて停止する。
|
|
201
|
+
const killed = await killProcessOnPort(LLM_PORT);
|
|
202
|
+
if (!killed) {
|
|
203
|
+
return {
|
|
204
|
+
stopped: false,
|
|
205
|
+
error: `ポート${LLM_PORT}で待ち受けているプロセスが見つかりませんでした`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
currentStep = 'not_running';
|
|
209
|
+
return { stopped: true };
|
|
210
|
+
}
|
|
211
|
+
// tsunagi本体プロセスの終了時(Ctrl+C等)に、自分が起動したllm-serverも
|
|
212
|
+
// 道連れで停止する。ユーザーが手動で起動したもの(running_external)には触れない。
|
|
213
|
+
export function stopLlmServerOnExit() {
|
|
214
|
+
managedProcess?.kill();
|
|
215
|
+
managedProcess = null;
|
|
216
|
+
}
|
|
217
|
+
// mlx_lm.serverへストリーミングなしで1回だけ問い合わせ、最終テキストのみを返す。
|
|
218
|
+
// 音声入力の文字起こし結果の整形など、対話UIを介さずLLMの出力だけ欲しい場面で使う。
|
|
219
|
+
export async function generateLlmCompletion(messages, maxTokens) {
|
|
220
|
+
const response = await fetch(`${LLM_SERVER_URL}/v1/chat/completions`, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'content-type': 'application/json' },
|
|
223
|
+
body: JSON.stringify({ messages, stream: false, max_tokens: maxTokens }),
|
|
224
|
+
signal: AbortSignal.timeout(60_000),
|
|
225
|
+
});
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new Error(`llm-server responded with ${response.status}`);
|
|
228
|
+
}
|
|
229
|
+
const data = (await response.json());
|
|
230
|
+
const content = data.choices?.[0]?.message?.content;
|
|
231
|
+
if (typeof content !== 'string') {
|
|
232
|
+
throw new Error('Unexpected response shape from llm-server');
|
|
233
|
+
}
|
|
234
|
+
return content;
|
|
235
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function killProcessOnPort(port: number): Promise<boolean>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
// 指定ポートでLISTENしているプロセスを探して停止する。tsunagi自身が起動した
|
|
3
|
+
// 場合はchild_processのハンドルで直接killできるが、`make whisper`/`make llm`
|
|
4
|
+
// のようにtsunagi外で手動起動された場合はハンドルを持たないため、
|
|
5
|
+
// ポート番号を手がかりにOS側から見つけて停止する。
|
|
6
|
+
export function killProcessOnPort(port) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
execFile('lsof', ['-ti', `:${port}`, '-sTCP:LISTEN'], (_error, stdout) => {
|
|
9
|
+
const pids = (stdout || '')
|
|
10
|
+
.split('\n')
|
|
11
|
+
.map((s) => s.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (pids.length === 0) {
|
|
14
|
+
resolve(false);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const pid of pids) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// 既に終了している等は無視
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
resolve(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -16,8 +16,8 @@ export declare function startWhisperServer(): {
|
|
|
16
16
|
started: boolean;
|
|
17
17
|
error?: string;
|
|
18
18
|
};
|
|
19
|
-
export declare function stopWhisperServer(): {
|
|
19
|
+
export declare function stopWhisperServer(): Promise<{
|
|
20
20
|
stopped: boolean;
|
|
21
21
|
error?: string;
|
|
22
|
-
}
|
|
22
|
+
}>;
|
|
23
23
|
export declare function stopWhisperServerOnExit(): void;
|
|
@@ -3,7 +3,11 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { killProcessOnPort } from './process-port.js';
|
|
6
7
|
const WHISPER_SERVER_URL = process.env.TSUNAGI_WHISPER_SERVER_URL || 'http://127.0.0.1:8765';
|
|
8
|
+
// run.shが待受けるポート固定値(host.docker.internal経由URLと違い、停止処理は
|
|
9
|
+
// 必ずこのNodeプロセスと同じホスト上のポートを対象にする必要があるため分けて持つ)。
|
|
10
|
+
const WHISPER_PORT = 8765;
|
|
7
11
|
// venv・モデルキャッシュとも ~/.tsunagi/whisper 配下にまとめる(run.shと同じ場所)。
|
|
8
12
|
const TSUNAGI_WHISPER_DIR = path.join(os.homedir(), '.tsunagi', 'whisper');
|
|
9
13
|
const VENV_DIR = path.join(TSUNAGI_WHISPER_DIR, 'venv');
|
|
@@ -179,12 +183,22 @@ export function startWhisperServer() {
|
|
|
179
183
|
});
|
|
180
184
|
return { started: true };
|
|
181
185
|
}
|
|
182
|
-
export function stopWhisperServer() {
|
|
183
|
-
if (
|
|
184
|
-
|
|
186
|
+
export async function stopWhisperServer() {
|
|
187
|
+
if (managedProcess) {
|
|
188
|
+
managedProcess.kill();
|
|
189
|
+
managedProcess = null;
|
|
190
|
+
currentStep = 'not_running';
|
|
191
|
+
return { stopped: true };
|
|
192
|
+
}
|
|
193
|
+
// tsunagi外(make whisper等)で起動された場合はchild_processのハンドルを
|
|
194
|
+
// 持たないため、ポート番号を手がかりにOS側から見つけて停止する。
|
|
195
|
+
const killed = await killProcessOnPort(WHISPER_PORT);
|
|
196
|
+
if (!killed) {
|
|
197
|
+
return {
|
|
198
|
+
stopped: false,
|
|
199
|
+
error: `ポート${WHISPER_PORT}で待ち受けているプロセスが見つかりませんでした`,
|
|
200
|
+
};
|
|
185
201
|
}
|
|
186
|
-
managedProcess.kill();
|
|
187
|
-
managedProcess = null;
|
|
188
202
|
currentStep = 'not_running';
|
|
189
203
|
return { stopped: true };
|
|
190
204
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getLlmServerStatus, startLlmServer, stopLlmServer } from '../lib/llm-process.js';
|
|
2
|
+
export async function llmRoutes(fastify) {
|
|
3
|
+
// GET /llm/server/status
|
|
4
|
+
fastify.get('/llm/server/status', async (_request, reply) => {
|
|
5
|
+
return reply.status(200).send(await getLlmServerStatus());
|
|
6
|
+
});
|
|
7
|
+
// POST /llm/server/start
|
|
8
|
+
fastify.post('/llm/server/start', async (_request, reply) => {
|
|
9
|
+
const result = startLlmServer();
|
|
10
|
+
if (!result.started) {
|
|
11
|
+
return reply.status(409).send({ error: result.error });
|
|
12
|
+
}
|
|
13
|
+
return reply.status(202).send(await getLlmServerStatus());
|
|
14
|
+
});
|
|
15
|
+
// POST /llm/server/stop
|
|
16
|
+
fastify.post('/llm/server/stop', async (_request, reply) => {
|
|
17
|
+
const result = await stopLlmServer();
|
|
18
|
+
if (!result.stopped) {
|
|
19
|
+
return reply.status(409).send({ error: result.error });
|
|
20
|
+
}
|
|
21
|
+
return reply.status(200).send(await getLlmServerStatus());
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import { ptyManager } from '../pty-manager.js';
|
|
5
5
|
import { prisma } from '../lib/db.js';
|
|
6
6
|
import { getEnv } from '../lib/repositories/environment.js';
|
|
7
|
+
import { ensureClaudeOnboardingCompleted } from '../lib/claude-config-guard.js';
|
|
7
8
|
// サーバーはプロジェクトルートから起動されるため process.cwd() でルートを取得
|
|
8
9
|
const TSUNAGI_EDITOR_PATH = path.resolve(process.cwd(), 'scripts/monaco-editor.sh');
|
|
9
10
|
// Fastify(API) の公開ポート。index.ts と同じ既定値。monaco-editor.sh など PTY 内の
|
|
@@ -230,6 +231,10 @@ export async function terminalRoutes(fastify) {
|
|
|
230
231
|
// コマンドが指定されていればPTY起動後にシェルへ書き込む
|
|
231
232
|
if (command) {
|
|
232
233
|
const cmd = command.endsWith('\n') ? command : command + '\n';
|
|
234
|
+
// claude logout で ~/.claude.json の hasCompletedOnboarding がリセットされて
|
|
235
|
+
// いると、次回 claude 起動時にオンボーディングウィザードが表示され --resume/
|
|
236
|
+
// --session-id を前提にした自動起動フローが止まるため、起動前に補正する。
|
|
237
|
+
await ensureClaudeOnboardingCompleted();
|
|
233
238
|
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
234
239
|
setTimeout(() => {
|
|
235
240
|
session.pty.write(cmd);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { getWhisperServerStatus, startWhisperServer, stopWhisperServer, } from '../lib/whisper-process.js';
|
|
2
|
+
import { generateLlmCompletion } from '../lib/llm-process.js';
|
|
2
3
|
// ユーザーが依存関係(pipパッケージ)をセットアップ済みのローカル常駐サーバー
|
|
3
4
|
// (apps/whisper-server)を指す。依存関係のインストール自体はtsunagi側では行わず、
|
|
4
5
|
// プロセスの起動・停止のみ管理する。
|
|
5
6
|
const WHISPER_SERVER_URL = process.env.TSUNAGI_WHISPER_SERVER_URL || 'http://127.0.0.1:8765';
|
|
7
|
+
// 文字起こし結果のLLM整形に使うシステムプロンプト。検証で
|
|
8
|
+
// 「原文の言葉遣いを維持しつつ句読点・誤字だけ直す」設定が有効と確認できたもの。
|
|
9
|
+
const TRANSCRIPTION_CORRECTION_SYSTEM_PROMPT = '以下は音声認識による文字起こし結果です。元の言葉遣いや言い回し、文章構成はできるだけそのまま維持してください。行ってよいのは句読点の追加と、明らかな誤字(音声認識の誤変換)の修正のみです。要約・言い換え・フィラーの除去・文の並び替えなどは行わないでください。修正後の文章のみを出力し、前置きや説明は不要です。';
|
|
6
10
|
export async function whisperRoutes(fastify) {
|
|
7
11
|
// GET /whisper/server/status
|
|
8
12
|
fastify.get('/whisper/server/status', async (_request, reply) => {
|
|
@@ -18,7 +22,7 @@ export async function whisperRoutes(fastify) {
|
|
|
18
22
|
});
|
|
19
23
|
// POST /whisper/server/stop
|
|
20
24
|
fastify.post('/whisper/server/stop', async (_request, reply) => {
|
|
21
|
-
const result = stopWhisperServer();
|
|
25
|
+
const result = await stopWhisperServer();
|
|
22
26
|
if (!result.stopped) {
|
|
23
27
|
return reply.status(409).send({ error: result.error });
|
|
24
28
|
}
|
|
@@ -30,9 +34,19 @@ export async function whisperRoutes(fastify) {
|
|
|
30
34
|
if (!file) {
|
|
31
35
|
return reply.status(400).send({ error: 'No audio file provided' });
|
|
32
36
|
}
|
|
37
|
+
// 音声ファイルと同じmultipartペイロード内の`prompt`フィールド(あれば)を
|
|
38
|
+
// whisper-serverのinitial_promptとしてそのまま転送する。
|
|
39
|
+
const promptField = file.fields.prompt;
|
|
40
|
+
const prompt = typeof promptField?.value === 'string' ? promptField.value : undefined;
|
|
41
|
+
// ローカルLLMによる整形を使うかどうかはクライアント(Settingsの有効/無効フラグ)から渡される。
|
|
42
|
+
const useLlmField = file.fields.useLlm;
|
|
43
|
+
const useLlm = useLlmField?.value === 'true';
|
|
33
44
|
const buffer = await file.toBuffer();
|
|
34
45
|
const body = new FormData();
|
|
35
46
|
body.append('file', new Blob([buffer]), file.filename);
|
|
47
|
+
if (prompt)
|
|
48
|
+
body.append('prompt', prompt);
|
|
49
|
+
let whisperText;
|
|
36
50
|
try {
|
|
37
51
|
const response = await fetch(`${WHISPER_SERVER_URL}/transcribe`, {
|
|
38
52
|
method: 'POST',
|
|
@@ -42,7 +56,7 @@ export async function whisperRoutes(fastify) {
|
|
|
42
56
|
throw new Error(`whisper-server responded with ${response.status}`);
|
|
43
57
|
}
|
|
44
58
|
const result = (await response.json());
|
|
45
|
-
|
|
59
|
+
whisperText = result.text;
|
|
46
60
|
}
|
|
47
61
|
catch (error) {
|
|
48
62
|
fastify.log.error(error, 'Whisper transcription error');
|
|
@@ -52,5 +66,25 @@ export async function whisperRoutes(fastify) {
|
|
|
52
66
|
: 'Transcription failed. Is apps/whisper-server running?',
|
|
53
67
|
});
|
|
54
68
|
}
|
|
69
|
+
if (!useLlm || !whisperText) {
|
|
70
|
+
return reply.status(200).send({ text: whisperText });
|
|
71
|
+
}
|
|
72
|
+
// LLMによる整形が有効な場合のみ追加で問い合わせる。文字起こし自体は既に
|
|
73
|
+
// 成功しているため、ここで失敗しても文字起こし結果はそのまま返す
|
|
74
|
+
// (ユーザーの発話内容を無駄にしないことを、整形の成否より優先する)。
|
|
75
|
+
try {
|
|
76
|
+
const corrected = await generateLlmCompletion([
|
|
77
|
+
{ role: 'system', content: TRANSCRIPTION_CORRECTION_SYSTEM_PROMPT },
|
|
78
|
+
{ role: 'user', content: whisperText },
|
|
79
|
+
], Math.min(4096, Math.max(512, whisperText.length * 4)));
|
|
80
|
+
return reply.status(200).send({ text: corrected.trim() });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
fastify.log.error(error, 'LLM correction failed, falling back to raw transcription');
|
|
84
|
+
return reply.status(200).send({
|
|
85
|
+
text: whisperText,
|
|
86
|
+
warning: 'ローカルLLMによる整形に失敗したため、文字起こし結果をそのまま使用しました',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
55
89
|
});
|
|
56
90
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# llm-server
|
|
2
|
+
|
|
3
|
+
音声入力の文字起こし結果をローカルLLMで整形するためのサーバー。mlx-lm (Apple Silicon GPU) で応答を生成する。
|
|
4
|
+
tsunagi本体はこのサーバーにHTTPでプロキシするだけで、セットアップ・起動は行わない。
|
|
5
|
+
|
|
6
|
+
## 要件
|
|
7
|
+
|
|
8
|
+
- macOS (Apple Silicon: M1/M2/M3/M4)
|
|
9
|
+
- Python 3.9+ (Xcode Command Line Tools または Homebrew 経由で入手可能)
|
|
10
|
+
- 空きディスク容量 約18GB(モデル本体)
|
|
11
|
+
|
|
12
|
+
## セットアップ・起動
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd apps/llm-server
|
|
16
|
+
./run.sh
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
初回は`venv`を作成し`requirements.txt`の依存関係(mlx-lm)をインストールしてから起動する。
|
|
20
|
+
`http://127.0.0.1:8766`で待受。初回起動時にモデル(mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit,
|
|
21
|
+
約17.2GB)を自動ダウンロードする。MoE構成(総30B・実計算に使うアクティブパラメータは約3B)で、
|
|
22
|
+
denseな32B級モデルよりメモリ帯域あたりの生成速度が出やすい。
|
|
23
|
+
|
|
24
|
+
`mlx_lm.server`が提供するOpenAI互換の`/v1/chat/completions`をそのまま公開する。
|
|
25
|
+
|
|
26
|
+
起動している間、tsunagiのSettingsでローカルLLMを有効にすると、音声入力の文字起こし結果がこの
|
|
27
|
+
サーバーで自動整形されるようになる。停止すると文字起こし結果はそのまま(未整形)で返る。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""LLMモデルの重みを事前ダウンロードするだけのスクリプト。
|
|
2
|
+
|
|
3
|
+
サーバー起動(mlx_lm.server)より前に呼ぶことで、初回のチャットリクエストで
|
|
4
|
+
突然数分〜数十分待たされる、という事態を避ける。tsunagi(Node)側からはこの
|
|
5
|
+
プロセスの実行中、キャッシュディレクトリ内の.incompleteファイルのサイズを
|
|
6
|
+
定期的に見て進捗を計算する想定なので、ここでは特別な進捗出力はしない。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from huggingface_hub import snapshot_download
|
|
10
|
+
|
|
11
|
+
MODEL_REPO = "mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit"
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
snapshot_download(repo_id=MODEL_REPO)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mlx-lm
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
4
|
+
|
|
5
|
+
# venv・モデルキャッシュとも ~/.tsunagi/llm 配下にまとめる。
|
|
6
|
+
# npmインストール先(apps/llm-server自体)が書き込み不可な場所でも動くようにする狙いもある。
|
|
7
|
+
TSUNAGI_LLM_DIR="${HOME}/.tsunagi/llm"
|
|
8
|
+
VENV_DIR="${TSUNAGI_LLM_DIR}/venv"
|
|
9
|
+
export HF_HOME="${TSUNAGI_LLM_DIR}/cache"
|
|
10
|
+
# xet転送は進捗の観測が難しい独自プロトコルのため無効化し、素直なHTTPダウンロードに固定する。
|
|
11
|
+
export HF_HUB_DISABLE_XET=1
|
|
12
|
+
|
|
13
|
+
MODEL="mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit"
|
|
14
|
+
|
|
15
|
+
if [ ! -d "$VENV_DIR" ]; then
|
|
16
|
+
mkdir -p "$TSUNAGI_LLM_DIR"
|
|
17
|
+
python3 -m venv "$VENV_DIR"
|
|
18
|
+
"$VENV_DIR/bin/python3" -m pip install -r requirements.txt
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# サーバー起動前にモデルを予めダウンロードしておく(初回チャットで待たされないように)。
|
|
22
|
+
"$VENV_DIR/bin/python3" download_model.py
|
|
23
|
+
|
|
24
|
+
# venvを ~/.tsunagi 配下へ移設する場合があるため、絶対パスが焼き込まれる
|
|
25
|
+
# コンソールスクリプト(bin/mlx_lm.server)ではなく `python3 -m` 経由で呼び出す。
|
|
26
|
+
exec "$VENV_DIR/bin/python3" -m mlx_lm.server --model "$MODEL" --host 127.0.0.1 --port 8766
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minimalcorp/tsunagi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@minimalcorp/tsunagi",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.25",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE",
|
|
11
11
|
"os": [
|
|
12
12
|
"darwin",
|
|
@@ -277,9 +277,9 @@
|
|
|
277
277
|
}
|
|
278
278
|
},
|
|
279
279
|
"node_modules/@fastify/reply-from": {
|
|
280
|
-
"version": "12.6.
|
|
281
|
-
"resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-12.6.
|
|
282
|
-
"integrity": "sha512-
|
|
280
|
+
"version": "12.6.3",
|
|
281
|
+
"resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-12.6.3.tgz",
|
|
282
|
+
"integrity": "sha512-oP8V1jo1SVL2MtagitoOGK7voP88VID0sGsm2Ob1lEzD0h9GBWq36zAe8JmYjllj0ybdHcMTQEhE/EQzVAjWsQ==",
|
|
283
283
|
"funding": [
|
|
284
284
|
{
|
|
285
285
|
"type": "github",
|
|
@@ -296,27 +296,11 @@
|
|
|
296
296
|
"end-of-stream": "^1.4.4",
|
|
297
297
|
"fast-content-type-parse": "^3.0.0",
|
|
298
298
|
"fast-querystring": "^1.1.2",
|
|
299
|
-
"fastify-plugin": "^
|
|
299
|
+
"fastify-plugin": "^6.0.0",
|
|
300
300
|
"toad-cache": "^3.7.0",
|
|
301
301
|
"undici": "^7.0.0"
|
|
302
302
|
}
|
|
303
303
|
},
|
|
304
|
-
"node_modules/@fastify/reply-from/node_modules/fastify-plugin": {
|
|
305
|
-
"version": "5.1.0",
|
|
306
|
-
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
|
|
307
|
-
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
|
|
308
|
-
"funding": [
|
|
309
|
-
{
|
|
310
|
-
"type": "github",
|
|
311
|
-
"url": "https://github.com/sponsors/fastify"
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"type": "opencollective",
|
|
315
|
-
"url": "https://opencollective.com/fastify"
|
|
316
|
-
}
|
|
317
|
-
],
|
|
318
|
-
"license": "MIT"
|
|
319
|
-
},
|
|
320
304
|
"node_modules/@hono/node-server": {
|
|
321
305
|
"version": "1.19.14",
|
|
322
306
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
|
@@ -1200,9 +1184,9 @@
|
|
|
1200
1184
|
}
|
|
1201
1185
|
},
|
|
1202
1186
|
"node_modules/avvio": {
|
|
1203
|
-
"version": "9.
|
|
1204
|
-
"resolved": "https://registry.npmjs.org/avvio/-/avvio-9.
|
|
1205
|
-
"integrity": "sha512-
|
|
1187
|
+
"version": "9.3.0",
|
|
1188
|
+
"resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz",
|
|
1189
|
+
"integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==",
|
|
1206
1190
|
"funding": [
|
|
1207
1191
|
{
|
|
1208
1192
|
"type": "github",
|
|
@@ -2186,9 +2170,9 @@
|
|
|
2186
2170
|
"license": "ISC"
|
|
2187
2171
|
},
|
|
2188
2172
|
"node_modules/grammex": {
|
|
2189
|
-
"version": "3.1.
|
|
2190
|
-
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.
|
|
2191
|
-
"integrity": "sha512-
|
|
2173
|
+
"version": "3.1.13",
|
|
2174
|
+
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz",
|
|
2175
|
+
"integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==",
|
|
2192
2176
|
"license": "MIT"
|
|
2193
2177
|
},
|
|
2194
2178
|
"node_modules/graphmatch": {
|
|
@@ -2222,9 +2206,9 @@
|
|
|
2222
2206
|
}
|
|
2223
2207
|
},
|
|
2224
2208
|
"node_modules/hono": {
|
|
2225
|
-
"version": "4.12.
|
|
2226
|
-
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.
|
|
2227
|
-
"integrity": "sha512-
|
|
2209
|
+
"version": "4.12.30",
|
|
2210
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz",
|
|
2211
|
+
"integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==",
|
|
2228
2212
|
"license": "MIT",
|
|
2229
2213
|
"engines": {
|
|
2230
2214
|
"node": ">=16.9.0"
|
|
@@ -2333,9 +2317,9 @@
|
|
|
2333
2317
|
}
|
|
2334
2318
|
},
|
|
2335
2319
|
"node_modules/js-base64": {
|
|
2336
|
-
"version": "3.
|
|
2337
|
-
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.
|
|
2338
|
-
"integrity": "sha512-
|
|
2320
|
+
"version": "3.9.1",
|
|
2321
|
+
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.1.tgz",
|
|
2322
|
+
"integrity": "sha512-U73qptcvf/HIOauFOmqT3a0mDUp0MYlfd15oqoe9kqZt5XhiXVb+HG09sLvI9PQ9tZIBFS4nlErai8zbWazP0g==",
|
|
2339
2323
|
"license": "BSD-3-Clause"
|
|
2340
2324
|
},
|
|
2341
2325
|
"node_modules/json-schema-ref-resolver": {
|
|
@@ -3331,9 +3315,9 @@
|
|
|
3331
3315
|
}
|
|
3332
3316
|
},
|
|
3333
3317
|
"node_modules/socket.io-parser": {
|
|
3334
|
-
"version": "4.2.
|
|
3335
|
-
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.
|
|
3336
|
-
"integrity": "sha512-
|
|
3318
|
+
"version": "4.2.7",
|
|
3319
|
+
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
|
|
3320
|
+
"integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
|
|
3337
3321
|
"license": "MIT",
|
|
3338
3322
|
"dependencies": {
|
|
3339
3323
|
"@socket.io/component-emitter": "~3.1.0",
|
|
@@ -3592,9 +3576,9 @@
|
|
|
3592
3576
|
"license": "ISC"
|
|
3593
3577
|
},
|
|
3594
3578
|
"node_modules/ws": {
|
|
3595
|
-
"version": "8.21.
|
|
3596
|
-
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.
|
|
3597
|
-
"integrity": "sha512
|
|
3579
|
+
"version": "8.21.1",
|
|
3580
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
|
3581
|
+
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
|
3598
3582
|
"license": "MIT",
|
|
3599
3583
|
"engines": {
|
|
3600
3584
|
"node": ">=10.0.0"
|