@minimalcorp/tsunagi 0.1.30 → 0.1.32

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.
Files changed (51) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  20. package/.next/standalone/apps/web/.next/server/app/index.rsc +1 -1
  21. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  22. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  27. package/.next/standalone/apps/web/.next/server/app/settings.rsc +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  33. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  34. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  35. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  36. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  37. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  38. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  39. package/dist/cli.js +16 -1
  40. package/dist/server/index.js +4 -1
  41. package/dist/server/lib/llm-process.js +9 -5
  42. package/dist/server/lib/python-venv.d.ts +4 -0
  43. package/dist/server/lib/python-venv.js +66 -0
  44. package/dist/server/lib/whisper-process.js +9 -5
  45. package/dist/server/pty-manager.js +33 -1
  46. package/dist/server/routes/terminal.js +3 -3
  47. package/npm-shrinkwrap.json +198 -162
  48. package/package.json +1 -1
  49. /package/.next/standalone/apps/web/.next/static/{TsUWkgyKReTsFW3SydDNd → 70bPr4pgLIE7loeTg62ih}/_buildManifest.js +0 -0
  50. /package/.next/standalone/apps/web/.next/static/{TsUWkgyKReTsFW3SydDNd → 70bPr4pgLIE7loeTg62ih}/_clientMiddlewareManifest.js +0 -0
  51. /package/.next/standalone/apps/web/.next/static/{TsUWkgyKReTsFW3SydDNd → 70bPr4pgLIE7loeTg62ih}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -184,10 +184,25 @@ verifyArtifact(NEXT_STANDALONE_ENTRY, 'Next.js standalone artifact');
184
184
  // Fastify が単一の公開ポート。Next.js は内部ポートで動かし Fastify からプロキシする。
185
185
  const PORT = process.env.PORT ?? '2791';
186
186
  const NEXT_PORT = '2792';
187
+ // Fastify サーバー自身の待受ポートは TSUNAGI_SERVER_PORT で伝える(generic な PORT は上書きしない)。
188
+ // PORT を直接上書きすると、ユーザーが Terminal で明示的に PORT=xxxx と指定していた場合でも
189
+ // tsunagi 内部ターミナル側でその値と tsunagi 自身の値を区別できなくなるため。
190
+ //
191
+ // NODE_ENV は Fastify/Next 等が直接参照するため PORT と同じ専用キー方式が使えず、サーバー
192
+ // プロセス自身には強制的に 'production' を渡す必要がある。上書きする前に外側 Terminal 由来の
193
+ // 元の値(未設定なら undefined)を TSUNAGI_OUTER_NODE_ENV に退避し、tsunagi 内部ターミナル側
194
+ // (apps/server/src/pty-manager.ts)で復元できるようにする。
195
+ const outerNodeEnv = process.env.NODE_ENV;
187
196
  const fastifyChild = spawn(process.execPath, [FASTIFY_ENTRY_JS], {
188
197
  stdio: ['inherit', 'pipe', 'pipe'],
189
198
  cwd: PACKAGE_ROOT,
190
- env: { ...process.env, NODE_ENV: 'production', PORT, TSUNAGI_NEXT_PORT: NEXT_PORT },
199
+ env: {
200
+ ...process.env,
201
+ NODE_ENV: 'production',
202
+ ...(outerNodeEnv !== undefined ? { TSUNAGI_OUTER_NODE_ENV: outerNodeEnv } : {}),
203
+ TSUNAGI_SERVER_PORT: PORT,
204
+ TSUNAGI_NEXT_PORT: NEXT_PORT,
205
+ },
191
206
  });
192
207
  const nextChild = spawn(process.execPath, [NEXT_STANDALONE_ENTRY], {
193
208
  stdio: ['inherit', 'pipe', 'pipe'],
@@ -22,7 +22,10 @@ import { llmRoutes } from './routes/llm.js';
22
22
  import { stopLlmServerOnExit } from './lib/llm-process.js';
23
23
  import { createBasicAuth } from './basic-auth.js';
24
24
  // Fastify は単一の公開エンドポイント。Next.js は内部ポートで動かしプロキシする。
25
- const PORT = Number(process.env.PORT) || 2791;
25
+ // PORT ではなく TSUNAGI_SERVER_PORT を見る(apps/cli が注入する専用キー)。generic な PORT
26
+ // 使うと、ユーザーが Terminal で明示的に設定した PORT と衝突し、内部ターミナルへの伝播を
27
+ // 区別できなくなるため(pty-manager.ts 参照)。
28
+ const PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
26
29
  const NEXT_PORT = Number(process.env.TSUNAGI_NEXT_PORT) || 2792;
27
30
  const extraOrigins = (process.env.TSUNAGI_EXTRA_CORS_ORIGINS ?? '')
28
31
  .split(',')
@@ -4,6 +4,7 @@ import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { killProcessOnPort } from './process-port.js';
7
+ import { findPython, isVenvUpToDate, PYTHON_NOT_FOUND_MESSAGE, writeVenvMarker, } from './python-venv.js';
7
8
  const MODEL = 'mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit';
8
9
  const LLM_SERVER_URL = process.env.TSUNAGI_LLM_SERVER_URL || 'http://127.0.0.1:8766';
9
10
  // run.shが待受けるポート固定値(host.docker.internal経由URLと違い、停止処理は
@@ -40,9 +41,6 @@ export function findLlmServerDir() {
40
41
  function venvPython() {
41
42
  return path.join(VENV_DIR, 'bin', 'python3');
42
43
  }
43
- function isVenvReady() {
44
- return fs.existsSync(venvPython());
45
- }
46
44
  function isModelReady() {
47
45
  const snapshotsDir = path.join(MODEL_CACHE_DIR, 'snapshots');
48
46
  if (!fs.existsSync(snapshotsDir))
@@ -135,11 +133,17 @@ async function trackModelDownload(child) {
135
133
  }
136
134
  async function runSetupAndStart(dir) {
137
135
  const hfEnv = { ...process.env, HF_HOME: HF_CACHE_DIR, HF_HUB_DISABLE_XET: '1' };
138
- if (!isVenvReady()) {
136
+ const requirementsFile = path.join(dir, 'requirements.txt');
137
+ if (!isVenvUpToDate(VENV_DIR, requirementsFile)) {
139
138
  currentStep = 'installing_deps';
139
+ const pythonBin = findPython();
140
+ if (!pythonBin)
141
+ throw new Error(PYTHON_NOT_FOUND_MESSAGE);
140
142
  fs.mkdirSync(TSUNAGI_LLM_DIR, { recursive: true });
141
- await runStep('python3', ['-m', 'venv', VENV_DIR], dir);
143
+ fs.rmSync(VENV_DIR, { recursive: true, force: true });
144
+ await runStep(pythonBin, ['-m', 'venv', VENV_DIR], dir);
142
145
  await runStep(venvPython(), ['-m', 'pip', 'install', '-r', 'requirements.txt'], dir);
146
+ writeVenvMarker(VENV_DIR, requirementsFile);
143
147
  }
144
148
  if (!isModelReady()) {
145
149
  currentStep = 'downloading_model';
@@ -0,0 +1,4 @@
1
+ export declare function findPython(): string | null;
2
+ export declare const PYTHON_NOT_FOUND_MESSAGE = "Python 3.10\u4EE5\u4E0A\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002'brew install python@3.12' \u7B49\u3067\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002";
3
+ export declare function isVenvUpToDate(venvDir: string, requirementsFile: string): boolean;
4
+ export declare function writeVenvMarker(venvDir: string, requirementsFile: string): void;
@@ -0,0 +1,66 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ // mlx/mlx-lm/mlx-whisper の固定バージョンが requires-python >=3.10 のため、venv作成に
6
+ // 使う python3 自体も 3.10 以上でなければ pip install がそもそも失敗する。`python3` という
7
+ // 名前が PATH 上のどのバージョンを指すかは環境依存(古い macOS 付属 Python 等)なため、
8
+ // 候補を新しい順に探して最初に見つかった 3.10 以上を使う。
9
+ const MIN_PYTHON_VERSION = '3.10';
10
+ const PYTHON_CANDIDATES = [
11
+ 'python3.14',
12
+ 'python3.13',
13
+ 'python3.12',
14
+ 'python3.11',
15
+ 'python3.10',
16
+ 'python3',
17
+ ];
18
+ function pythonVersion(bin) {
19
+ try {
20
+ return execFileSync(bin, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], {
21
+ encoding: 'utf8',
22
+ }).trim();
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function versionAtLeast(version, min) {
29
+ const [vMajor, vMinor] = version.split('.').map(Number);
30
+ const [mMajor, mMinor] = min.split('.').map(Number);
31
+ return vMajor !== mMajor ? vMajor > mMajor : vMinor >= mMinor;
32
+ }
33
+ export function findPython() {
34
+ for (const cand of PYTHON_CANDIDATES) {
35
+ const version = pythonVersion(cand);
36
+ if (version && versionAtLeast(version, MIN_PYTHON_VERSION))
37
+ return cand;
38
+ }
39
+ return null;
40
+ }
41
+ export const PYTHON_NOT_FOUND_MESSAGE = `Python ${MIN_PYTHON_VERSION}以上が見つかりません。'brew install python@3.12' 等でインストールしてください。`;
42
+ function venvMarkerPath(venvDir) {
43
+ return path.join(venvDir, '.requirements.sha256');
44
+ }
45
+ function requirementsHash(requirementsFile) {
46
+ return crypto.createHash('sha256').update(fs.readFileSync(requirementsFile)).digest('hex');
47
+ }
48
+ // venvが「存在するだけ」でなく、実際に使えるかを検証する。
49
+ // - venv作成に使われた python が 3.10 未満だと mlx 系のインストールに失敗するバージョンのまま放置される
50
+ // - requirements.txt のバージョン指定を後から変更しても、既存venvには反映されない
51
+ // のいずれかが起きていないかを、venv自身のpythonバージョンと記録済みハッシュから判定する。
52
+ export function isVenvUpToDate(venvDir, requirementsFile) {
53
+ const venvPython = path.join(venvDir, 'bin', 'python3');
54
+ if (!fs.existsSync(venvPython))
55
+ return false;
56
+ const version = pythonVersion(venvPython);
57
+ if (!version || !versionAtLeast(version, MIN_PYTHON_VERSION))
58
+ return false;
59
+ const marker = venvMarkerPath(venvDir);
60
+ if (!fs.existsSync(marker))
61
+ return false;
62
+ return fs.readFileSync(marker, 'utf8').trim() === requirementsHash(requirementsFile);
63
+ }
64
+ export function writeVenvMarker(venvDir, requirementsFile) {
65
+ fs.writeFileSync(venvMarkerPath(venvDir), requirementsHash(requirementsFile));
66
+ }
@@ -4,6 +4,7 @@ import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { killProcessOnPort } from './process-port.js';
7
+ import { findPython, isVenvUpToDate, PYTHON_NOT_FOUND_MESSAGE, writeVenvMarker, } from './python-venv.js';
7
8
  const WHISPER_SERVER_URL = process.env.TSUNAGI_WHISPER_SERVER_URL || 'http://127.0.0.1:8765';
8
9
  // run.shが待受けるポート固定値(host.docker.internal経由URLと違い、停止処理は
9
10
  // 必ずこのNodeプロセスと同じホスト上のポートを対象にする必要があるため分けて持つ)。
@@ -34,9 +35,6 @@ export function findWhisperServerDir() {
34
35
  function venvPython() {
35
36
  return path.join(VENV_DIR, 'bin', 'python3');
36
37
  }
37
- function isVenvReady() {
38
- return fs.existsSync(venvPython());
39
- }
40
38
  function isModelReady() {
41
39
  const snapshotsDir = path.join(MODEL_CACHE_DIR, 'snapshots');
42
40
  if (!fs.existsSync(snapshotsDir))
@@ -129,11 +127,17 @@ async function trackModelDownload(child) {
129
127
  }
130
128
  async function runSetupAndStart(dir) {
131
129
  const hfEnv = { ...process.env, HF_HOME: HF_CACHE_DIR, HF_HUB_DISABLE_XET: '1' };
132
- if (!isVenvReady()) {
130
+ const requirementsFile = path.join(dir, 'requirements.txt');
131
+ if (!isVenvUpToDate(VENV_DIR, requirementsFile)) {
133
132
  currentStep = 'installing_deps';
133
+ const pythonBin = findPython();
134
+ if (!pythonBin)
135
+ throw new Error(PYTHON_NOT_FOUND_MESSAGE);
134
136
  fs.mkdirSync(TSUNAGI_WHISPER_DIR, { recursive: true });
135
- await runStep('python3', ['-m', 'venv', VENV_DIR], dir);
137
+ fs.rmSync(VENV_DIR, { recursive: true, force: true });
138
+ await runStep(pythonBin, ['-m', 'venv', VENV_DIR], dir);
136
139
  await runStep(venvPython(), ['-m', 'pip', 'install', '-r', 'requirements.txt'], dir);
140
+ writeVenvMarker(VENV_DIR, requirementsFile);
137
141
  }
138
142
  if (!isModelReady()) {
139
143
  currentStep = 'downloading_model';
@@ -2,6 +2,26 @@ import * as pty from 'node-pty';
2
2
  import * as os from 'os';
3
3
  const SCROLLBACK_MAX_BYTES = 256 * 1024; // 256KB
4
4
  const SESSION_GC_INTERVAL_MS = 30 * 60 * 1000; // 30分
5
+ /**
6
+ * apps/cli が Fastify サーバー起動時に自分で注入する環境変数
7
+ * (TSUNAGI_SERVER_PORT, NODE_ENV, TSUNAGI_NEXT_PORT, TSUNAGI_OUTER_NODE_ENV)。
8
+ * サーバープロセス自身の制御用に後から足された値のため、内部ターミナルには継承しない。
9
+ * 外側 Terminal 由来の環境変数はこのリストと無関係に、これまで通り全て継承する
10
+ * (PORT はここでは扱わない。apps/cli は generic な PORT を上書きせず、専用キー
11
+ * TSUNAGI_SERVER_PORT で自身の待受ポートを伝えるため、ユーザーが Terminal で明示した
12
+ * PORT はそのまま内部ターミナルまで届く)。
13
+ *
14
+ * NODE_ENV は Fastify/Next 等が直接参照するため、PORT と違って専用キーへ逃がせず、
15
+ * サーバープロセス自身には強制的に 'production' が入る。apps/cli はサーバー起動前の
16
+ * 元の値(外側 Terminal 由来、未設定なら undefined)を TSUNAGI_OUTER_NODE_ENV に退避して
17
+ * 渡してくるので、ここで NODE_ENV を一旦除外した後、退避値があれば NODE_ENV として復元する。
18
+ */
19
+ const TSUNAGI_INJECTED_ENV_KEYS = [
20
+ 'TSUNAGI_SERVER_PORT',
21
+ 'NODE_ENV',
22
+ 'TSUNAGI_NEXT_PORT',
23
+ 'TSUNAGI_OUTER_NODE_ENV',
24
+ ];
5
25
  class PtyManager {
6
26
  sessions = new Map();
7
27
  /** sessionId → 最後に input を送信した socket.id */
@@ -22,13 +42,25 @@ class PtyManager {
22
42
  throw new Error(`Session already exists: ${sessionId}`);
23
43
  }
24
44
  const shell = process.env.SHELL || (os.platform() === 'win32' ? 'cmd.exe' : 'bash');
45
+ // tsunagi 自身が注入した環境変数は内部ターミナルに継承しない(TSUNAGI_INJECTED_ENV_KEYS 参照)。
46
+ // /settings 等で明示的に同名キーを設定した場合は env(dbEnv 経由)が後勝ちで上書きするため、
47
+ // カスケードの優先順位・挙動は変わらない。
48
+ const baseEnv = { ...process.env };
49
+ for (const key of TSUNAGI_INJECTED_ENV_KEYS) {
50
+ delete baseEnv[key];
51
+ }
52
+ // 外側 Terminal に元々 NODE_ENV があった場合は復元する。なければ未設定のままにする
53
+ // (tsunagi が強制した 'production' を内部ターミナルに漏らさないため)。
54
+ if (process.env.TSUNAGI_OUTER_NODE_ENV !== undefined) {
55
+ baseEnv.NODE_ENV = process.env.TSUNAGI_OUTER_NODE_ENV;
56
+ }
25
57
  const ptyProcess = pty.spawn(shell, [], {
26
58
  name: 'xterm-256color',
27
59
  cols: 80,
28
60
  rows: 24,
29
61
  cwd,
30
62
  env: {
31
- ...process.env,
63
+ ...baseEnv,
32
64
  ...env,
33
65
  TERM: 'xterm-256color',
34
66
  COLORTERM: 'truecolor',
@@ -7,9 +7,9 @@ import { getEnv } from '../lib/repositories/environment.js';
7
7
  import { ensureClaudeOnboardingCompleted } from '../lib/claude-config-guard.js';
8
8
  // サーバーはプロジェクトルートから起動されるため process.cwd() でルートを取得
9
9
  const TSUNAGI_EDITOR_PATH = path.resolve(process.cwd(), 'scripts/monaco-editor.sh');
10
- // Fastify(API) の公開ポート。index.ts と同じ既定値。monaco-editor.sh など PTY 内の
11
- // プロセスが API を叩く際の同一ホスト向けベース URL に使う。
12
- const SERVER_PORT = Number(process.env.PORT) || 2791;
10
+ // Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
11
+ // monaco-editor.sh など PTY 内のプロセスが API を叩く際の同一ホスト向けベース URL に使う。
12
+ const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
13
13
  /**
14
14
  * 再接続時の画面復元用 scrollback から「端末への問い合わせ(クエリ)」シーケンスを除去する。
15
15
  *