@minimalcorp/tsunagi 0.2.0 → 0.3.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/.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.js.nft.json +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 +14 -14
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +16 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +9 -9
- 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 +17 -16
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +17 -16
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +3 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +5 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- 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]__00qk65u._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__02o_btn._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_03vftq7._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0835zmv._.js +14 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g79jqo._.js +20 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0kflgfj._.js +20 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0w2dm3r._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_053072t._.js → _139_l.u._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_13w5ytx._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +4 -21
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0bfjoue._.js → node_modules_09d2oqj._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_0a57phw._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0a9pqes._.js → node_modules_0ak_3rk._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0mpkxmv._.js → node_modules_0cv326~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zstt1y._.js → node_modules_0h.vgyn._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0~2ubpz._.js → node_modules_0npujym._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_1re~9._.js → node_modules_0q_5qo~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0e.hm5q._.js → node_modules_0sn0lqb._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0-g_j5j._.js → node_modules_11-z65a._.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/0.gcmzq82~36i.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/06gfh91..uydi.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/08e.dbdz18nd2.js +18 -0
- package/.next/standalone/apps/web/.next/static/chunks/0b90ys7wq-p.4.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +6 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0s7g4uwmm49t..js → 0l83q95s~tih4.js} +2 -2
- package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/0lxvbkhh7amha.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0mqupo29wme26.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/0rn1kcmsfzjmz.js +12 -0
- package/.next/standalone/apps/web/.next/static/chunks/0whl1y16cvn5q.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0z60ewb40i2ce.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0~qk-51yuw4mm.js +18 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0v3hfoggxwsov.js → 149sia28cskw9.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/17~93fal1fw_d.js +5 -0
- package/dist/prisma/migrations/20261007000000_add_claude_profiles/migration.sql +10 -0
- package/dist/prisma/schema.prisma +9 -0
- package/dist/server/generated/prisma/browser.d.ts +5 -0
- package/dist/server/generated/prisma/client.d.ts +5 -0
- package/dist/server/generated/prisma/internal/class.d.ts +11 -0
- package/dist/server/generated/prisma/internal/class.js +4 -4
- package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +83 -1
- package/dist/server/generated/prisma/internal/prismaNamespace.js +6 -0
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +7 -0
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +6 -0
- package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +939 -0
- package/dist/server/generated/prisma/models/ClaudeProfile.js +1 -0
- package/dist/server/generated/prisma/models.d.ts +1 -0
- package/dist/server/index.js +17 -0
- package/dist/server/lib/claude-config-guard.d.ts +7 -5
- package/dist/server/lib/claude-config-guard.js +31 -12
- package/dist/server/lib/claude-profiles.d.ts +49 -0
- package/dist/server/lib/claude-profiles.js +168 -0
- package/dist/server/lib/pty-env-sync.d.ts +12 -0
- package/dist/server/lib/pty-env-sync.js +139 -0
- package/dist/server/lib/pty-launch.d.ts +36 -0
- package/dist/server/lib/pty-launch.js +141 -0
- package/dist/server/lib/repositories/environment.js +8 -34
- package/dist/server/pty-manager.d.ts +23 -1
- package/dist/server/pty-manager.js +7 -1
- package/dist/server/routes/claude-profiles.d.ts +2 -0
- package/dist/server/routes/claude-profiles.js +138 -0
- package/dist/server/routes/env.js +25 -1
- package/dist/server/routes/onboarding.js +15 -6
- package/dist/server/routes/terminal.js +34 -120
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__03~.ax1._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_07i72iz._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_09eooml._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0bru-z.._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i8ojdw._.js +0 -14
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mtw02l._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_109pmo2._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_12aa-fs._.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/0.qz1zt1c.y.n.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/01mfp5lo0gy.q.js +0 -23
- package/.next/standalone/apps/web/.next/static/chunks/0_by0zpp68~r7.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0ajw1mjy3owa4.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0fe4v5dh3whem.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0gzt~9euv6u2y.js +0 -12
- package/.next/standalone/apps/web/.next/static/chunks/0jcorj3-aambm.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0wnto9g4i0s3h.css +0 -4
- package/.next/standalone/apps/web/.next/static/chunks/0wzu~340lh7o~.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/0xa_n_zp.qw-b.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0~_c.mwboavrv.js +0 -7
- /package/.next/standalone/apps/web/.next/static/{OXWK1FxsZh6lcEQawkL6h → Qv73X7lHL0PkQ5DtK6UEF}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{OXWK1FxsZh6lcEQawkL6h → Qv73X7lHL0PkQ5DtK6UEF}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{OXWK1FxsZh6lcEQawkL6h → Qv73X7lHL0PkQ5DtK6UEF}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { ptyManager } from '../pty-manager.js';
|
|
6
|
+
import { prisma } from './db.js';
|
|
7
|
+
import { getWorktreePath } from './worktree-manager.js';
|
|
8
|
+
import { getEnv } from './repositories/environment.js';
|
|
9
|
+
import { ensureClaudeOnboardingCompleted, removeLocalModelFromUserSettings, watchUserSettingsForLocalModel, } from './claude-config-guard.js';
|
|
10
|
+
import { LOCAL_LLM_UNSET_ENV_KEYS, LOCAL_MODEL_ALIAS, buildLocalLlmEnv } from './local-llm-env.js';
|
|
11
|
+
import { ensureActiveLoaded, getLocalLlmSettings } from './local-llm.js';
|
|
12
|
+
import { LOCAL_LLM_PROXY_URL, buildClaudeCommand, isLocalLlmMode } from './claude-command.js';
|
|
13
|
+
import { resolveClaudeProfileEnv } from './claude-profiles.js';
|
|
14
|
+
// このファイル基準で解決する(cwd 非依存):
|
|
15
|
+
// dev: apps/server/src/lib → apps/server/scripts
|
|
16
|
+
// build: apps/server/dist/lib → apps/server/scripts
|
|
17
|
+
// npm bundle: <pkg>/dist/server/lib → <pkg>/dist/scripts
|
|
18
|
+
const TSUNAGI_EDITOR_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/monaco-editor.sh');
|
|
19
|
+
// Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
|
|
20
|
+
// monaco-editor.sh など PTY 内のプロセスが API を叩く際の同一ホスト向けベース URL に使う。
|
|
21
|
+
const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
|
|
22
|
+
export class PtyLaunchError extends Error {
|
|
23
|
+
statusCode;
|
|
24
|
+
constructor(message, statusCode) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.statusCode = statusCode;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* sessionId(=tab_id) から PTY の作業ディレクトリと環境変数を解決する。
|
|
31
|
+
* 環境変数の優先順位: リクエストで渡されたenv > ローカルLLMのprovider env >
|
|
32
|
+
* DB env(global/owner/repo、Claude プロファイルを CLAUDE_CONFIG_DIR に変換) > tsunagi独自のEDITOR設定
|
|
33
|
+
*/
|
|
34
|
+
export async function resolvePtyLaunch(sessionId, request) {
|
|
35
|
+
// Taskに紐づかないタブは Tab レコードが存在しないため null になる。
|
|
36
|
+
const tab = await prisma.tab.findUnique({
|
|
37
|
+
where: { tabId: sessionId },
|
|
38
|
+
include: { task: true },
|
|
39
|
+
});
|
|
40
|
+
// cwd は Task があればサーバー側で worktree パスを導出する(クライアントの state に依存しない)。
|
|
41
|
+
// クライアント指定の cwd は Task に紐づかないタブのフォールバックとしてのみ使う。
|
|
42
|
+
const defaultDir = path.join(os.homedir(), '.tsunagi', 'workspaces');
|
|
43
|
+
let workingDir = tab
|
|
44
|
+
? getWorktreePath(tab.task.owner, tab.task.repo, tab.task.branch)
|
|
45
|
+
: (request.cwd ?? defaultDir);
|
|
46
|
+
// cwd が存在するか確認。Task のタブは worktree 必須(無ければ起動しない)、
|
|
47
|
+
// それ以外はデフォルトにフォールバック
|
|
48
|
+
try {
|
|
49
|
+
await fs.access(workingDir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
if (tab)
|
|
53
|
+
throw new PtyLaunchError(`Worktree not found at ${workingDir}`, 409);
|
|
54
|
+
workingDir = defaultDir;
|
|
55
|
+
}
|
|
56
|
+
// repo スコープまで階層マージした環境変数を取得する(global → owner → repo、後勝ちで上書き)。
|
|
57
|
+
// Taskに紐づかないタブは global のみになる。
|
|
58
|
+
// 読み込みに失敗したら起動しない(空の環境変数で起動・作り直しすると設定が抜け落ちるため)
|
|
59
|
+
const dbEnv = tab ? await getEnv('repo', tab.task.owner, tab.task.repo) : await getEnv('global');
|
|
60
|
+
const profile = resolveClaudeProfileEnv(dbEnv);
|
|
61
|
+
// ローカルLLMタブ: Anthropic の代わりに tsunagi の中継口(→ Ollama / LM Studio)で claude を動かす。
|
|
62
|
+
// DB の環境変数より後に適用し、Anthropic の認証トークンは環境変数ごと取り除く。
|
|
63
|
+
const mode = tab?.mode;
|
|
64
|
+
let providerEnv = {};
|
|
65
|
+
let unsetKeys = profile.unsetKeys;
|
|
66
|
+
if (isLocalLlmMode(mode)) {
|
|
67
|
+
const { active, extraEnv } = await getLocalLlmSettings();
|
|
68
|
+
if (!active) {
|
|
69
|
+
throw new PtyLaunchError('ローカルLLMのモデルが未設定です。Settings で使用するモデルを選んでください', 400);
|
|
70
|
+
}
|
|
71
|
+
providerEnv = buildLocalLlmEnv({
|
|
72
|
+
proxyUrl: LOCAL_LLM_PROXY_URL,
|
|
73
|
+
contextTokens: active.contextTokens,
|
|
74
|
+
extraEnv,
|
|
75
|
+
});
|
|
76
|
+
unsetKeys = [...new Set([...unsetKeys, ...LOCAL_LLM_UNSET_ENV_KEYS])];
|
|
77
|
+
}
|
|
78
|
+
// tsunagi-editor.sh をデフォルトにすることで Ctrl+G が Monaco Modal を開く。
|
|
79
|
+
// DB / リクエストで EDITOR が明示設定されている場合はそちらが優先される。
|
|
80
|
+
const tsunagiDefaultEnv = {
|
|
81
|
+
EDITOR: TSUNAGI_EDITOR_PATH,
|
|
82
|
+
TSUNAGI_SESSION_ID: sessionId,
|
|
83
|
+
// monaco-editor.sh が API(Fastify) を叩くためのベース URL。サーバーと同一
|
|
84
|
+
// ホストなので localhost。単一ポート化で API は SERVER_PORT(既定 2791)。
|
|
85
|
+
TSUNAGI_API_BASE: `http://localhost:${SERVER_PORT}`,
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
workingDir,
|
|
89
|
+
mode,
|
|
90
|
+
hasTab: Boolean(tab),
|
|
91
|
+
env: { ...tsunagiDefaultEnv, ...profile.env, ...providerEnv, ...request.env },
|
|
92
|
+
unsetKeys,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* claude 起動前の補正。claude logout で .claude.json の hasCompletedOnboarding がリセットされて
|
|
97
|
+
* いると、次回 claude 起動時にオンボーディングウィザードが表示され --resume/--session-id を前提にした
|
|
98
|
+
* 自動起動フローが止まるため補正する。監視の取りこぼしに備え、中継口の名前が既定モデルに残って
|
|
99
|
+
* いないかも補正する。対象はタブが使う Claude プロファイルの設定ディレクトリ。
|
|
100
|
+
*/
|
|
101
|
+
async function prepareClaudeConfig(env) {
|
|
102
|
+
// PTY は外側 Terminal の環境を継承するため、プロファイル未指定なら外側の CLAUDE_CONFIG_DIR に従う
|
|
103
|
+
const configDir = env.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR;
|
|
104
|
+
await ensureClaudeOnboardingCompleted(configDir);
|
|
105
|
+
await removeLocalModelFromUserSettings(LOCAL_MODEL_ALIAS, configDir);
|
|
106
|
+
watchUserSettingsForLocalModel(LOCAL_MODEL_ALIAS, configDir);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 解決済みの launch で PTY を作り、claude を起動する。
|
|
110
|
+
* 新規作成(POST /terminal/sessions)と環境変数変更時の作り直し(pty-env-sync)で共用する。
|
|
111
|
+
*/
|
|
112
|
+
export async function startPtySession(sessionId, launch, options) {
|
|
113
|
+
await fs.mkdir(launch.workingDir, { recursive: true });
|
|
114
|
+
let command = options.command;
|
|
115
|
+
if (options.launchClaude) {
|
|
116
|
+
if (isLocalLlmMode(launch.mode)) {
|
|
117
|
+
// 最初の発言を待たせないよう読み込みを始めておく(完了は待たない)
|
|
118
|
+
ensureActiveLoaded().catch(() => undefined);
|
|
119
|
+
}
|
|
120
|
+
command = await buildClaudeCommand(sessionId, launch.mode ?? 'claude');
|
|
121
|
+
}
|
|
122
|
+
const session = ptyManager.createSession(sessionId, launch.workingDir, launch.env, launch.unsetKeys, {
|
|
123
|
+
request: options.request,
|
|
124
|
+
launchClaude: options.launchClaude,
|
|
125
|
+
syncEnv: true,
|
|
126
|
+
});
|
|
127
|
+
// コマンドが指定されていればPTY起動後にシェルへ書き込む
|
|
128
|
+
if (command) {
|
|
129
|
+
const cmd = command.endsWith('\n') ? command : command + '\n';
|
|
130
|
+
await prepareClaudeConfig(launch.env);
|
|
131
|
+
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
session.pty.write(cmd);
|
|
134
|
+
}, 300);
|
|
135
|
+
}
|
|
136
|
+
return session;
|
|
137
|
+
}
|
|
138
|
+
/** 既存 PTY で claude を(再)起動する前の補正。PTY 作成時の環境変数のプロファイルを対象にする */
|
|
139
|
+
export async function prepareClaudeConfigForSession(session) {
|
|
140
|
+
await prepareClaudeConfig(session.appliedEnv);
|
|
141
|
+
}
|
|
@@ -21,48 +21,22 @@ export async function getAllEnv(scope, owner, repo) {
|
|
|
21
21
|
// 環境変数取得(優先順位: repo > owner > global)
|
|
22
22
|
export async function getEnv(scope, owner, repo) {
|
|
23
23
|
const result = {};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
CLAUDE_TOKEN_KEYS.forEach((key) => delete result[key]);
|
|
29
|
-
};
|
|
30
|
-
// Helper: 環境変数リストにClaude認証トークンが含まれるか
|
|
31
|
-
const hasClaudeToken = (envs) => {
|
|
32
|
-
return envs.some((env) => CLAUDE_TOKEN_KEYS.includes(env.key));
|
|
24
|
+
const apply = (envs) => {
|
|
25
|
+
envs.forEach((env) => {
|
|
26
|
+
result[env.key] = env.value;
|
|
27
|
+
});
|
|
33
28
|
};
|
|
34
29
|
// グローバル変数を先に適用(有効なもののみ)
|
|
35
|
-
|
|
36
|
-
where: { scope: 'global', enabled: true },
|
|
37
|
-
});
|
|
38
|
-
globalEnvs.forEach((env) => {
|
|
39
|
-
result[env.key] = env.value;
|
|
40
|
-
});
|
|
30
|
+
apply(await prisma.environmentVariable.findMany({ where: { scope: 'global', enabled: true } }));
|
|
41
31
|
// owner変数を適用(上書き、有効なもののみ)
|
|
42
32
|
if (scope === 'owner' || scope === 'repo') {
|
|
43
|
-
|
|
44
|
-
where: { scope: 'owner', owner, enabled: true },
|
|
45
|
-
});
|
|
46
|
-
// Ownerスコープで新しいClaude認証トークンがあれば、Globalのものをクリア
|
|
47
|
-
if (hasClaudeToken(ownerEnvs)) {
|
|
48
|
-
clearClaudeTokens();
|
|
49
|
-
}
|
|
50
|
-
ownerEnvs.forEach((env) => {
|
|
51
|
-
result[env.key] = env.value;
|
|
52
|
-
});
|
|
33
|
+
apply(await prisma.environmentVariable.findMany({ where: { scope: 'owner', owner, enabled: true } }));
|
|
53
34
|
}
|
|
54
35
|
// repo変数を適用(上書き、有効なもののみ)
|
|
55
36
|
if (scope === 'repo') {
|
|
56
|
-
|
|
37
|
+
apply(await prisma.environmentVariable.findMany({
|
|
57
38
|
where: { scope: 'repo', owner, repo, enabled: true },
|
|
58
|
-
});
|
|
59
|
-
// Repoスコープで新しいClaude認証トークンがあれば、以前のものをクリア
|
|
60
|
-
if (hasClaudeToken(repoEnvs)) {
|
|
61
|
-
clearClaudeTokens();
|
|
62
|
-
}
|
|
63
|
-
repoEnvs.forEach((env) => {
|
|
64
|
-
result[env.key] = env.value;
|
|
65
|
-
});
|
|
39
|
+
}));
|
|
66
40
|
}
|
|
67
41
|
return result;
|
|
68
42
|
}
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import * as pty from 'node-pty';
|
|
2
|
+
/** PTY 作成時の付随情報(環境変数の変更を反映するために作り直すときに使う) */
|
|
3
|
+
export interface PtySessionOptions {
|
|
4
|
+
/** クライアントが指定した cwd / env */
|
|
5
|
+
request?: {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
/** 作成時に claude を起動したか(作り直し後も claude を --resume で起動する) */
|
|
10
|
+
launchClaude?: boolean;
|
|
11
|
+
/** tsunagi の環境変数の変更を自動で反映する対象か(Claude のログイン用 PTY 等は対象外) */
|
|
12
|
+
syncEnv?: boolean;
|
|
13
|
+
}
|
|
2
14
|
export interface PtySession {
|
|
3
15
|
pty: pty.IPty;
|
|
4
16
|
sessionId: string;
|
|
5
17
|
cwd: string;
|
|
18
|
+
/** 作成時に渡した環境変数と取り除いたキー(変更検知の比較元) */
|
|
19
|
+
appliedEnv: Record<string, string>;
|
|
20
|
+
appliedUnsetKeys: string[];
|
|
21
|
+
options: PtySessionOptions;
|
|
22
|
+
/** tsunagi の環境変数が変わり、作り直し待ちの状態 */
|
|
23
|
+
envPending: boolean;
|
|
24
|
+
/** 環境変数の反映のために kill 中。onExit で「セッション終了」として扱わない */
|
|
25
|
+
respawning: boolean;
|
|
26
|
+
/** 最後にユーザー入力があった時刻(入力中の作り直しを避けるため) */
|
|
27
|
+
lastInputAt: number;
|
|
6
28
|
/** PTY出力のリングバッファ(再接続時にまとめて送信) */
|
|
7
29
|
scrollback: string[];
|
|
8
30
|
scrollbackSize: number;
|
|
@@ -24,7 +46,7 @@ declare class PtyManager {
|
|
|
24
46
|
* @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
|
|
25
47
|
* (例: Ollama タブでは Anthropic の認証トークンを取り除く)
|
|
26
48
|
*/
|
|
27
|
-
createSession(sessionId: string, cwd: string, env?: Record<string, string>, unsetKeys?: string[]): PtySession;
|
|
49
|
+
createSession(sessionId: string, cwd: string, env?: Record<string, string>, unsetKeys?: string[], options?: PtySessionOptions): PtySession;
|
|
28
50
|
getSession(sessionId: string): PtySession | undefined;
|
|
29
51
|
deleteSession(sessionId: string): void;
|
|
30
52
|
/**
|
|
@@ -63,7 +63,7 @@ class PtyManager {
|
|
|
63
63
|
* @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
|
|
64
64
|
* (例: Ollama タブでは Anthropic の認証トークンを取り除く)
|
|
65
65
|
*/
|
|
66
|
-
createSession(sessionId, cwd, env, unsetKeys = []) {
|
|
66
|
+
createSession(sessionId, cwd, env = {}, unsetKeys = [], options = {}) {
|
|
67
67
|
if (this.sessions.has(sessionId)) {
|
|
68
68
|
throw new Error(`Session already exists: ${sessionId}`);
|
|
69
69
|
}
|
|
@@ -102,6 +102,12 @@ class PtyManager {
|
|
|
102
102
|
pty: ptyProcess,
|
|
103
103
|
sessionId,
|
|
104
104
|
cwd,
|
|
105
|
+
appliedEnv: env,
|
|
106
|
+
appliedUnsetKeys: unsetKeys,
|
|
107
|
+
options,
|
|
108
|
+
envPending: false,
|
|
109
|
+
respawning: false,
|
|
110
|
+
lastInputAt: 0,
|
|
105
111
|
scrollback: [],
|
|
106
112
|
scrollbackSize: 0,
|
|
107
113
|
lastOutputAt: now,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import { ptyManager } from '../pty-manager.js';
|
|
3
|
+
import { prisma } from '../lib/db.js';
|
|
4
|
+
import { deleteEnv, getEnv, setEnv } from '../lib/repositories/environment.js';
|
|
5
|
+
import { CLAUDE_PROFILE_ENV_KEY, DEFAULT_CLAUDE_PROFILE, buildClaudeLoginCommand, claudeLoginPtyEnv, claudeProfileExists, createClaudeProfile, deleteClaudeProfile, getClaudeAuthStatus, listClaudeProfiles, } from '../lib/claude-profiles.js';
|
|
6
|
+
import { notifyEnvChanged } from '../lib/pty-env-sync.js';
|
|
7
|
+
/** ログイン用 PTY の sessionId の接頭辞(タブの PTY と区別する) */
|
|
8
|
+
const LOGIN_SESSION_PREFIX = 'claude-login-';
|
|
9
|
+
function validateScope(scope, owner, repo) {
|
|
10
|
+
if (scope !== 'global' && scope !== 'owner' && scope !== 'repo')
|
|
11
|
+
return 'Invalid scope';
|
|
12
|
+
if (scope === 'owner' && !owner)
|
|
13
|
+
return 'owner is required for scope=owner';
|
|
14
|
+
if (scope === 'repo' && (!owner || !repo))
|
|
15
|
+
return 'owner and repo are required for scope=repo';
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
async function getAssignment(scope, owner, repo) {
|
|
19
|
+
const row = await prisma.environmentVariable.findFirst({
|
|
20
|
+
where: {
|
|
21
|
+
key: CLAUDE_PROFILE_ENV_KEY,
|
|
22
|
+
scope,
|
|
23
|
+
...(scope !== 'global' && { owner }),
|
|
24
|
+
...(scope === 'repo' && { repo }),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const merged = await getEnv(scope, owner, repo);
|
|
28
|
+
const assignment = {
|
|
29
|
+
assigned: row?.value ?? null,
|
|
30
|
+
effective: merged[CLAUDE_PROFILE_ENV_KEY] ?? DEFAULT_CLAUDE_PROFILE,
|
|
31
|
+
};
|
|
32
|
+
return assignment;
|
|
33
|
+
}
|
|
34
|
+
function syncPtyEnv(fastify) {
|
|
35
|
+
notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
|
|
36
|
+
}
|
|
37
|
+
export async function claudeProfilesRoutes(fastify) {
|
|
38
|
+
// GET /claude-profiles - システム既定 + 登録済みプロファイルとログイン状態
|
|
39
|
+
fastify.get('/claude-profiles', async (_request, reply) => {
|
|
40
|
+
const profiles = await listClaudeProfiles();
|
|
41
|
+
const data = await Promise.all([
|
|
42
|
+
getClaudeAuthStatus(DEFAULT_CLAUDE_PROFILE).then((status) => ({
|
|
43
|
+
slug: DEFAULT_CLAUDE_PROFILE,
|
|
44
|
+
name: 'System default',
|
|
45
|
+
// CLAUDE_CONFIG_DIR を付けない(普段ターミナルで claude を使うときと同じ設定)
|
|
46
|
+
configDir: null,
|
|
47
|
+
status,
|
|
48
|
+
})),
|
|
49
|
+
...profiles.map(async (profile) => ({
|
|
50
|
+
...profile,
|
|
51
|
+
status: await getClaudeAuthStatus(profile.slug),
|
|
52
|
+
})),
|
|
53
|
+
]);
|
|
54
|
+
return reply.send({ data: { profiles: data } });
|
|
55
|
+
});
|
|
56
|
+
// GET /claude-profiles/:slug/status - ログイン状態(ログイン用ダイアログで完了を確認する)
|
|
57
|
+
fastify.get('/claude-profiles/:slug/status', async (request, reply) => {
|
|
58
|
+
const { slug } = request.params;
|
|
59
|
+
if (!(await claudeProfileExists(slug))) {
|
|
60
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
61
|
+
}
|
|
62
|
+
return reply.send({ data: { status: await getClaudeAuthStatus(slug) } });
|
|
63
|
+
});
|
|
64
|
+
// POST /claude-profiles - プロファイル作成
|
|
65
|
+
fastify.post('/claude-profiles', async (request, reply) => {
|
|
66
|
+
try {
|
|
67
|
+
const profile = await createClaudeProfile(request.body?.name ?? '');
|
|
68
|
+
return reply.status(201).send({ data: { profile } });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return reply.status(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// DELETE /claude-profiles/:slug - プロファイル削除(logout してディレクトリごと消す)
|
|
75
|
+
fastify.delete('/claude-profiles/:slug', async (request, reply) => {
|
|
76
|
+
const { slug } = request.params;
|
|
77
|
+
if (slug === DEFAULT_CLAUDE_PROFILE || !(await claudeProfileExists(slug))) {
|
|
78
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
79
|
+
}
|
|
80
|
+
await deleteClaudeProfile(slug);
|
|
81
|
+
syncPtyEnv(fastify);
|
|
82
|
+
return reply.status(204).send();
|
|
83
|
+
});
|
|
84
|
+
// POST /claude-profiles/:slug/login - ログイン用 PTY を起動し `claude auth login` を実行する
|
|
85
|
+
fastify.post('/claude-profiles/:slug/login', async (request, reply) => {
|
|
86
|
+
const { slug } = request.params;
|
|
87
|
+
if (!(await claudeProfileExists(slug))) {
|
|
88
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
89
|
+
}
|
|
90
|
+
// 同じプロファイルのログイン用 PTY が残っていれば閉じる
|
|
91
|
+
const prefix = `${LOGIN_SESSION_PREFIX}${slug}-`;
|
|
92
|
+
for (const id of ptyManager.listSessions()) {
|
|
93
|
+
if (id.startsWith(prefix))
|
|
94
|
+
ptyManager.deleteSession(id);
|
|
95
|
+
}
|
|
96
|
+
const sessionId = `${prefix}${crypto.randomUUID()}`;
|
|
97
|
+
const { env, unsetKeys } = claudeLoginPtyEnv(slug);
|
|
98
|
+
const session = ptyManager.createSession(sessionId, os.homedir(), env, unsetKeys);
|
|
99
|
+
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
100
|
+
setTimeout(() => session.pty.write(`${buildClaudeLoginCommand()}\n`), 300);
|
|
101
|
+
return reply.status(201).send({ data: { sessionId } });
|
|
102
|
+
});
|
|
103
|
+
// DELETE /claude-profiles/login/:sessionId - ログイン用 PTY を閉じる
|
|
104
|
+
fastify.delete('/claude-profiles/login/:sessionId', async (request, reply) => {
|
|
105
|
+
const { sessionId } = request.params;
|
|
106
|
+
if (!sessionId.startsWith(LOGIN_SESSION_PREFIX)) {
|
|
107
|
+
return reply.status(400).send({ error: 'Not a login session' });
|
|
108
|
+
}
|
|
109
|
+
ptyManager.deleteSession(sessionId);
|
|
110
|
+
return reply.status(204).send();
|
|
111
|
+
});
|
|
112
|
+
// GET /claude-profiles/assignment - スコープのプロファイル割り当て
|
|
113
|
+
fastify.get('/claude-profiles/assignment', async (request, reply) => {
|
|
114
|
+
const { scope, owner, repo } = request.query;
|
|
115
|
+
const error = validateScope(scope, owner, repo);
|
|
116
|
+
if (error)
|
|
117
|
+
return reply.status(400).send({ error });
|
|
118
|
+
return reply.send({ data: { assignment: await getAssignment(scope, owner, repo) } });
|
|
119
|
+
});
|
|
120
|
+
// PUT /claude-profiles/assignment - スコープのプロファイル割り当てを変更する
|
|
121
|
+
fastify.put('/claude-profiles/assignment', async (request, reply) => {
|
|
122
|
+
const { scope, owner, repo, slug } = request.body ?? {};
|
|
123
|
+
const error = validateScope(scope, owner, repo);
|
|
124
|
+
if (error)
|
|
125
|
+
return reply.status(400).send({ error });
|
|
126
|
+
if (slug !== null && !(await claudeProfileExists(slug))) {
|
|
127
|
+
return reply.status(400).send({ error: 'Profile not found' });
|
|
128
|
+
}
|
|
129
|
+
if (slug === null) {
|
|
130
|
+
await deleteEnv(CLAUDE_PROFILE_ENV_KEY, scope, owner, repo);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await setEnv(CLAUDE_PROFILE_ENV_KEY, slug, scope, owner, repo);
|
|
134
|
+
}
|
|
135
|
+
syncPtyEnv(fastify);
|
|
136
|
+
return reply.send({ data: { assignment: await getAssignment(scope, owner, repo) } });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import * as envRepo from '../lib/repositories/environment.js';
|
|
2
|
+
import { RESERVED_ENV_KEYS } from '../lib/claude-profiles.js';
|
|
3
|
+
import { notifyEnvChanged } from '../lib/pty-env-sync.js';
|
|
4
|
+
const RESERVED_KEY_ERROR = 'Claude の認証は Settings の Claude Profile で設定してください';
|
|
5
|
+
/** 変更後、起動中のターミナルに反映する(失敗しても API 自体は成功扱い) */
|
|
6
|
+
function syncPtyEnv(fastify) {
|
|
7
|
+
notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
|
|
8
|
+
}
|
|
2
9
|
export async function envRoutes(fastify) {
|
|
3
10
|
// GET /env
|
|
4
11
|
fastify.get('/env', async (request, reply) => {
|
|
@@ -30,6 +37,9 @@ export async function envRoutes(fastify) {
|
|
|
30
37
|
if (!key || !value || !scope) {
|
|
31
38
|
return reply.status(400).send({ error: 'Missing required fields: key, value, scope' });
|
|
32
39
|
}
|
|
40
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
41
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
42
|
+
}
|
|
33
43
|
if (scope === 'owner' && !owner) {
|
|
34
44
|
return reply.status(400).send({ error: 'Missing required field: owner for scope=owner' });
|
|
35
45
|
}
|
|
@@ -39,6 +49,7 @@ export async function envRoutes(fastify) {
|
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
await envRepo.setEnv(key, value, scope, owner, repo);
|
|
52
|
+
syncPtyEnv(fastify);
|
|
42
53
|
return reply.status(201).send({ data: { success: true } });
|
|
43
54
|
}
|
|
44
55
|
catch (error) {
|
|
@@ -53,6 +64,9 @@ export async function envRoutes(fastify) {
|
|
|
53
64
|
if (!key || !value || !scope) {
|
|
54
65
|
return reply.status(400).send({ error: 'Missing required fields: key, value, scope' });
|
|
55
66
|
}
|
|
67
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
68
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
69
|
+
}
|
|
56
70
|
if (scope === 'owner' && !owner) {
|
|
57
71
|
return reply.status(400).send({ error: 'Missing required field: owner for scope=owner' });
|
|
58
72
|
}
|
|
@@ -62,6 +76,7 @@ export async function envRoutes(fastify) {
|
|
|
62
76
|
});
|
|
63
77
|
}
|
|
64
78
|
await envRepo.setEnv(key, value, scope, owner, repo);
|
|
79
|
+
syncPtyEnv(fastify);
|
|
65
80
|
return reply.status(200).send({ data: { success: true } });
|
|
66
81
|
}
|
|
67
82
|
catch (error) {
|
|
@@ -80,6 +95,9 @@ export async function envRoutes(fastify) {
|
|
|
80
95
|
error: 'Missing required query parameters: key, scope',
|
|
81
96
|
});
|
|
82
97
|
}
|
|
98
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
99
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
100
|
+
}
|
|
83
101
|
if (scope === 'owner' && !owner) {
|
|
84
102
|
return reply.status(400).send({
|
|
85
103
|
error: 'Missing required query parameter: owner for scope=owner',
|
|
@@ -94,6 +112,7 @@ export async function envRoutes(fastify) {
|
|
|
94
112
|
if (!success) {
|
|
95
113
|
return reply.status(404).send({ error: 'Environment variable not found' });
|
|
96
114
|
}
|
|
115
|
+
syncPtyEnv(fastify);
|
|
97
116
|
return reply.status(200).send({ data: { success: true } });
|
|
98
117
|
}
|
|
99
118
|
catch (error) {
|
|
@@ -116,7 +135,8 @@ export async function envRoutes(fastify) {
|
|
|
116
135
|
error: 'Missing required query parameters: owner, repo for scope=repo',
|
|
117
136
|
});
|
|
118
137
|
}
|
|
119
|
-
|
|
138
|
+
// 予約キー(Claude プロファイルの割り当て)は専用 UI で扱うため一覧に含めない
|
|
139
|
+
const envVars = (await envRepo.getAllEnv(scope, owner || undefined, repo || undefined)).filter((env) => !RESERVED_ENV_KEYS.includes(env.key));
|
|
120
140
|
return reply.status(200).send({ data: { envVars } });
|
|
121
141
|
}
|
|
122
142
|
catch (error) {
|
|
@@ -131,7 +151,11 @@ export async function envRoutes(fastify) {
|
|
|
131
151
|
if (!key || !scope || typeof enabled !== 'boolean') {
|
|
132
152
|
return reply.status(400).send({ error: 'key, scope, and enabled are required' });
|
|
133
153
|
}
|
|
154
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
155
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
156
|
+
}
|
|
134
157
|
await envRepo.toggleEnv(key, scope, enabled, owner, repo);
|
|
158
|
+
syncPtyEnv(fastify);
|
|
135
159
|
return reply.status(200).send({ data: { success: true } });
|
|
136
160
|
}
|
|
137
161
|
catch (error) {
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { getEnv } from '../lib/repositories/environment.js';
|
|
2
1
|
import { isLocalLlmReady } from '../lib/local-llm.js';
|
|
2
|
+
import { DEFAULT_CLAUDE_PROFILE, getClaudeAuthStatus, listClaudeProfiles, } from '../lib/claude-profiles.js';
|
|
3
|
+
/** システム既定かいずれかの Claude プロファイルがログイン済みか */
|
|
4
|
+
async function isClaudeLoggedIn() {
|
|
5
|
+
if ((await getClaudeAuthStatus(DEFAULT_CLAUDE_PROFILE)).loggedIn)
|
|
6
|
+
return true;
|
|
7
|
+
const profiles = await listClaudeProfiles();
|
|
8
|
+
const statuses = await Promise.all(profiles.map((p) => getClaudeAuthStatus(p.slug)));
|
|
9
|
+
return statuses.some((status) => status.loggedIn);
|
|
10
|
+
}
|
|
3
11
|
export async function onboardingRoutes(fastify) {
|
|
4
12
|
// GET /onboarding/status
|
|
5
13
|
fastify.get('/onboarding/status', async (_request, reply) => {
|
|
6
14
|
try {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const [claudeLoggedIn, localLlmReady] = await Promise.all([
|
|
16
|
+
isClaudeLoggedIn(),
|
|
17
|
+
// 実験的機能のローカルLLM(Ollama / LM Studio)を設定済みなら、Claude にログインしていなくても claude を起動できる
|
|
18
|
+
isLocalLlmReady(),
|
|
19
|
+
]);
|
|
11
20
|
return reply.status(200).send({
|
|
12
|
-
data: { completed:
|
|
21
|
+
data: { completed: claudeLoggedIn || localLlmReady, claudeLoggedIn, localLlmReady },
|
|
13
22
|
});
|
|
14
23
|
}
|
|
15
24
|
catch (error) {
|