@minimalcorp/tsunagi 0.3.2 → 0.4.0
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/_05n88to._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0kflgfj._.js → _0pw3_r_._.js} +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0g79jqo._.js → _0rb6-mo._.js} +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ts4rpp._.js +3 -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 +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_04pfam-._.js → node_modules_03-rvdo._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0cv326~._.js → node_modules_06jdmu~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0a57phw._.js → node_modules_0925te2._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_huoqd._.js → node_modules_0e8uni7._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0tod6y8._.js → node_modules_0h4h_aw._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h.vgyn._.js → node_modules_0nzbb9~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0oip5a1._.js → node_modules_0q8yuvq._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_11-z65a._.js → node_modules_0r6t2tu._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09d2oqj._.js → node_modules_0t6ysh_._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0krwgny._.js → node_modules_0vj~e_v._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0sn0lqb._.js → node_modules_0vq~hzy._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0l23gkf._.js → node_modules_0xzmx-x._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0npujym._.js → node_modules_0yb2sh9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_5qo~._.js → node_modules_0yc~s8p._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_xfyl._.js → node_modules_0zi-gfw._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ak_3rk._.js → node_modules_0zzdoud._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0xp9nhx._.js → node_modules_0~aiu12._.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/09b_3duxnyfto.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0z60ewb40i2ce.js → 0gxhy80~c0gvp.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0i9h-39w91ybd.js +6 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0b90ys7wq-p.4.css → 0pm0knaijueoc.css} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{16cbfrtkn0.kw.js → 0smdykrt42e.5.js} +10 -10
- package/.next/standalone/apps/web/.next/static/chunks/{149sia28cskw9.js → 0yp~t_.pozr-d.js} +6 -6
- package/.next/standalone/apps/web/.next/static/chunks/{0o6ggm7jrim25.js → 14zh2vf8ljbxo.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{06yhoqamnoyq-.js → 1624992q-gpj2.js} +10 -10
- package/.next/standalone/apps/web/package.json +1 -0
- package/README.md +1 -1
- package/dist/app.js +3 -3
- package/dist/plugin-lifecycle.d.ts +5 -3
- package/dist/plugin-lifecycle.js +40 -16
- package/dist/prisma/migrations/20261011000000_add_codex_support/migration.sql +20 -0
- package/dist/prisma/schema.prisma +9 -5
- package/dist/server/generated/prisma/browser.d.ts +2 -2
- package/dist/server/generated/prisma/client.d.ts +2 -2
- package/dist/server/generated/prisma/internal/class.d.ts +4 -4
- package/dist/server/generated/prisma/internal/class.js +4 -4
- package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +41 -39
- package/dist/server/generated/prisma/internal/prismaNamespace.js +4 -2
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +5 -3
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +4 -2
- package/dist/server/generated/prisma/models/AgentProfile.d.ts +972 -0
- package/dist/server/generated/prisma/models/Tab.d.ts +36 -1
- package/dist/server/generated/prisma/models.d.ts +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/lib/agent-profiles.d.ts +43 -0
- package/dist/server/lib/agent-profiles.js +127 -0
- package/dist/server/lib/agents/claude.d.ts +2 -0
- package/dist/server/lib/agents/claude.js +130 -0
- package/dist/server/lib/agents/cli.d.ts +14 -0
- package/dist/server/lib/agents/cli.js +57 -0
- package/dist/server/lib/agents/codex.d.ts +2 -0
- package/dist/server/lib/agents/codex.js +285 -0
- package/dist/server/lib/agents/index.d.ts +11 -0
- package/dist/server/lib/agents/index.js +19 -0
- package/dist/server/lib/agents/login-callback.d.ts +11 -0
- package/dist/server/lib/agents/login-callback.js +47 -0
- package/dist/server/lib/agents/types.d.ts +77 -0
- package/dist/server/lib/agents/types.js +8 -0
- package/dist/server/lib/pty-env-sync.js +1 -1
- package/dist/server/lib/pty-launch.d.ts +8 -10
- package/dist/server/lib/pty-launch.js +20 -58
- package/dist/server/lib/repositories/task.js +1 -0
- package/dist/server/lib/tab-todos.d.ts +5 -0
- package/dist/server/lib/tab-todos.js +13 -0
- package/dist/server/pty-manager.d.ts +19 -2
- package/dist/server/pty-manager.js +38 -0
- package/dist/server/routes/agent-profiles.d.ts +2 -0
- package/dist/server/routes/agent-profiles.js +193 -0
- package/dist/server/routes/env.js +2 -2
- package/dist/server/routes/hooks.d.ts +4 -19
- package/dist/server/routes/hooks.js +83 -73
- package/dist/server/routes/mcp.js +2 -2
- package/dist/server/routes/onboarding.js +19 -10
- package/dist/server/routes/tasks.js +4 -4
- package/dist/server/routes/terminal.js +84 -37
- package/dist/with-plugin.js +3 -3
- package/npm-shrinkwrap.json +31 -31
- package/package.json +1 -1
- package/tsunagi-marketplace/codex/tsunagi-task.md +120 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hnxatt._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +0 -6
- package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +0 -7
- package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +0 -939
- package/dist/server/lib/claude-profiles.d.ts +0 -49
- package/dist/server/lib/claude-profiles.js +0 -168
- package/dist/server/routes/claude-profiles.d.ts +0 -2
- package/dist/server/routes/claude-profiles.js +0 -138
- /package/.next/standalone/apps/web/.next/static/{kCD_Mhp9UYT63vnxJPuK4 → ZcOuTrikuO_ZLaERk7FsW}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{kCD_Mhp9UYT63vnxJPuK4 → ZcOuTrikuO_ZLaERk7FsW}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{kCD_Mhp9UYT63vnxJPuK4 → ZcOuTrikuO_ZLaERk7FsW}/_ssgManifest.js +0 -0
- /package/dist/server/generated/prisma/models/{ClaudeProfile.js → AgentProfile.js} +0 -0
|
@@ -6,11 +6,8 @@ import { ptyManager } from '../pty-manager.js';
|
|
|
6
6
|
import { prisma } from './db.js';
|
|
7
7
|
import { getWorktreePath } from './worktree-manager.js';
|
|
8
8
|
import { getEnv } from './repositories/environment.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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';
|
|
9
|
+
import { resolveAgentProfileEnv } from './agent-profiles.js';
|
|
10
|
+
import { AgentLaunchError as PtyLaunchError, getAgentForMode } from './agents/index.js';
|
|
14
11
|
// このファイル基準で解決する(cwd 非依存):
|
|
15
12
|
// dev: apps/server/src/lib → apps/server/scripts
|
|
16
13
|
// build: apps/server/dist/lib → apps/server/scripts
|
|
@@ -19,17 +16,11 @@ const TSUNAGI_EDITOR_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.
|
|
|
19
16
|
// Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
|
|
20
17
|
// monaco-editor.sh など PTY 内のプロセスが API を叩く際の同一ホスト向けベース URL に使う。
|
|
21
18
|
const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
|
|
22
|
-
export
|
|
23
|
-
statusCode;
|
|
24
|
-
constructor(message, statusCode) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.statusCode = statusCode;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
19
|
+
export { PtyLaunchError };
|
|
29
20
|
/**
|
|
30
21
|
* sessionId(=tab_id) から PTY の作業ディレクトリと環境変数を解決する。
|
|
31
|
-
* 環境変数の優先順位: リクエストで渡されたenv >
|
|
32
|
-
* DB env(global/owner/repo
|
|
22
|
+
* 環境変数の優先順位: リクエストで渡されたenv > エージェント固有のenv(ローカルLLMの provider 等) >
|
|
23
|
+
* DB env(global/owner/repo、エージェントのプロファイルを設定ディレクトリに変換) > tsunagi独自のEDITOR設定
|
|
33
24
|
*/
|
|
34
25
|
export async function resolvePtyLaunch(sessionId, request) {
|
|
35
26
|
// Taskに紐づかないタブは Tab レコードが存在しないため null になる。
|
|
@@ -57,24 +48,12 @@ export async function resolvePtyLaunch(sessionId, request) {
|
|
|
57
48
|
// Taskに紐づかないタブは global のみになる。
|
|
58
49
|
// 読み込みに失敗したら起動しない(空の環境変数で起動・作り直しすると設定が抜け落ちるため)
|
|
59
50
|
const dbEnv = tab ? await getEnv('repo', tab.task.owner, tab.task.repo) : await getEnv('global');
|
|
60
|
-
const profile =
|
|
61
|
-
//
|
|
62
|
-
// DB の環境変数より後に適用し、Anthropic の認証トークンは環境変数ごと取り除く。
|
|
51
|
+
const profile = resolveAgentProfileEnv(dbEnv);
|
|
52
|
+
// エージェント固有の環境変数は DB の環境変数より後に適用する
|
|
63
53
|
const mode = tab?.mode;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
}
|
|
54
|
+
const agentEnv = await getAgentForMode(mode).resolveLaunchEnv(mode ?? 'claude');
|
|
55
|
+
const providerEnv = agentEnv.env;
|
|
56
|
+
const unsetKeys = [...new Set([...profile.unsetKeys, ...agentEnv.unsetKeys])];
|
|
78
57
|
// tsunagi-editor.sh をデフォルトにすることで Ctrl+G が Monaco Modal を開く。
|
|
79
58
|
// DB / リクエストで EDITOR が明示設定されている場合はそちらが優先される。
|
|
80
59
|
const tsunagiDefaultEnv = {
|
|
@@ -93,41 +72,24 @@ export async function resolvePtyLaunch(sessionId, request) {
|
|
|
93
72
|
};
|
|
94
73
|
}
|
|
95
74
|
/**
|
|
96
|
-
*
|
|
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 を起動する。
|
|
75
|
+
* 解決済みの launch で PTY を作り、エージェントを起動する。
|
|
110
76
|
* 新規作成(POST /terminal/sessions)と環境変数変更時の作り直し(pty-env-sync)で共用する。
|
|
111
77
|
*/
|
|
112
78
|
export async function startPtySession(sessionId, launch, options) {
|
|
113
79
|
await fs.mkdir(launch.workingDir, { recursive: true });
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
ensureActiveLoaded().catch(() => undefined);
|
|
119
|
-
}
|
|
120
|
-
command = await buildClaudeCommand(sessionId, launch.mode ?? 'claude');
|
|
121
|
-
}
|
|
80
|
+
const agent = options.launchAgent ? getAgentForMode(launch.mode) : undefined;
|
|
81
|
+
const command = agent
|
|
82
|
+
? await agent.buildCommand(sessionId, launch.mode ?? 'claude')
|
|
83
|
+
: options.command;
|
|
122
84
|
const session = ptyManager.createSession(sessionId, launch.workingDir, launch.env, launch.unsetKeys, {
|
|
123
85
|
request: options.request,
|
|
124
|
-
|
|
86
|
+
launchAgent: options.launchAgent,
|
|
125
87
|
syncEnv: true,
|
|
126
88
|
});
|
|
127
89
|
// コマンドが指定されていればPTY起動後にシェルへ書き込む
|
|
128
90
|
if (command) {
|
|
129
91
|
const cmd = command.endsWith('\n') ? command : command + '\n';
|
|
130
|
-
await
|
|
92
|
+
await agent?.prepareLaunch(launch.env);
|
|
131
93
|
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
132
94
|
setTimeout(() => {
|
|
133
95
|
session.pty.write(cmd);
|
|
@@ -135,7 +97,7 @@ export async function startPtySession(sessionId, launch, options) {
|
|
|
135
97
|
}
|
|
136
98
|
return session;
|
|
137
99
|
}
|
|
138
|
-
/** 既存 PTY
|
|
139
|
-
export async function
|
|
140
|
-
await
|
|
100
|
+
/** 既存 PTY でエージェントを(再)起動する前の補正。PTY 作成時の環境変数のプロファイルを対象にする */
|
|
101
|
+
export async function prepareAgentLaunchForSession(session, mode) {
|
|
102
|
+
await getAgentForMode(mode)?.prepareLaunch(session.appliedEnv);
|
|
141
103
|
}
|
|
@@ -243,6 +243,7 @@ function mapTab(tab) {
|
|
|
243
243
|
mode: tab.mode,
|
|
244
244
|
unread: tab.unread,
|
|
245
245
|
todos: tab.todos ? JSON.parse(tab.todos) : [],
|
|
246
|
+
agentSessionId: tab.agentSessionId ?? undefined,
|
|
246
247
|
startedAt: tab.startedAt.toISOString(),
|
|
247
248
|
completedAt: tab.completedAt?.toISOString(),
|
|
248
249
|
updatedAt: tab.updatedAt.toISOString(),
|
|
@@ -10,3 +10,8 @@ export declare function parseTodos(raw: string | null | undefined): Todo[];
|
|
|
10
10
|
* tsunagi 側も DB 上の todos を起点に差分を当てる(サーバー再起動でも進捗を失わない)
|
|
11
11
|
*/
|
|
12
12
|
export declare function applyToolToTodos(current: Todo[], toolName: string | undefined, input: Record<string, unknown> | undefined, response: Record<string, unknown> | undefined): Todo[] | null;
|
|
13
|
+
/**
|
|
14
|
+
* Codex の update_plan(PostToolUse の tool_input: { plan: [{ step, status }] })を todos に変換する。
|
|
15
|
+
* 毎回計画全体が渡されるため全置換する。解釈できなければ null
|
|
16
|
+
*/
|
|
17
|
+
export declare function planToTodos(input: unknown): Todo[] | null;
|
|
@@ -137,3 +137,16 @@ export function applyToolToTodos(current, toolName, input, response) {
|
|
|
137
137
|
return null;
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Codex の update_plan(PostToolUse の tool_input: { plan: [{ step, status }] })を todos に変換する。
|
|
142
|
+
* 毎回計画全体が渡されるため全置換する。解釈できなければ null
|
|
143
|
+
*/
|
|
144
|
+
export function planToTodos(input) {
|
|
145
|
+
const plan = isRecord(input) ? input.plan : undefined;
|
|
146
|
+
if (!Array.isArray(plan))
|
|
147
|
+
return null;
|
|
148
|
+
return plan.filter(isRecord).map((item) => ({
|
|
149
|
+
content: String(item.step ?? ''),
|
|
150
|
+
status: isStatus(item.status) ? item.status : 'pending',
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
@@ -6,8 +6,8 @@ export interface PtySessionOptions {
|
|
|
6
6
|
cwd?: string;
|
|
7
7
|
env?: Record<string, string>;
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/** 作成時にエージェントを起動したか(作り直し後もエージェントを resume で起動する) */
|
|
10
|
+
launchAgent?: boolean;
|
|
11
11
|
/** tsunagi の環境変数の変更を自動で反映する対象か(Claude のログイン用 PTY 等は対象外) */
|
|
12
12
|
syncEnv?: boolean;
|
|
13
13
|
}
|
|
@@ -35,6 +35,10 @@ export interface PtySession {
|
|
|
35
35
|
/** GCインターバルタイマー */
|
|
36
36
|
gcTimer: ReturnType<typeof setInterval> | null;
|
|
37
37
|
}
|
|
38
|
+
export interface TerminalSize {
|
|
39
|
+
cols: number;
|
|
40
|
+
rows: number;
|
|
41
|
+
}
|
|
38
42
|
declare class PtyManager {
|
|
39
43
|
private sessions;
|
|
40
44
|
/** sessionId → 最後に input を送信した socket.id */
|
|
@@ -42,6 +46,14 @@ declare class PtyManager {
|
|
|
42
46
|
setActiveSocket(sessionId: string, socketId: string): void;
|
|
43
47
|
getActiveSocket(sessionId: string): string | undefined;
|
|
44
48
|
clearActiveSocket(sessionId: string, socketId: string): void;
|
|
49
|
+
/** sessionId → (socket.id → その socket の xterm サイズ)。挿入順 = 最後に resize/claim した順 */
|
|
50
|
+
private terminalSizes;
|
|
51
|
+
setTerminalSize(sessionId: string, socketId: string, size: TerminalSize): void;
|
|
52
|
+
/** socket の登録を外し、残っている socket のうち最後に使われたものを返す */
|
|
53
|
+
removeTerminalSocket(sessionId: string, socketId: string): {
|
|
54
|
+
socketId: string;
|
|
55
|
+
size: TerminalSize;
|
|
56
|
+
} | undefined;
|
|
45
57
|
/**
|
|
46
58
|
* @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
|
|
47
59
|
* (例: Ollama タブでは Anthropic の認証トークンを取り除く)
|
|
@@ -65,4 +77,9 @@ declare class PtyManager {
|
|
|
65
77
|
deleteAllSessions(): void;
|
|
66
78
|
}
|
|
67
79
|
export declare const ptyManager: PtyManager;
|
|
80
|
+
/**
|
|
81
|
+
* PTY の前面がシェルか(エージェントなどのコマンドが動いていないか)。
|
|
82
|
+
* 判定できない環境(Windows)では true を返す
|
|
83
|
+
*/
|
|
84
|
+
export declare function isShellForeground(session: PtySession): boolean;
|
|
68
85
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as pty from 'node-pty';
|
|
2
2
|
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
3
4
|
const SCROLLBACK_MAX_BYTES = 256 * 1024; // 256KB
|
|
4
5
|
const SESSION_GC_INTERVAL_MS = 30 * 60 * 1000; // 30分
|
|
5
6
|
/**
|
|
@@ -59,6 +60,31 @@ class PtyManager {
|
|
|
59
60
|
this.activeSocketIds.delete(sessionId);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
/** sessionId → (socket.id → その socket の xterm サイズ)。挿入順 = 最後に resize/claim した順 */
|
|
64
|
+
terminalSizes = new Map();
|
|
65
|
+
setTerminalSize(sessionId, socketId, size) {
|
|
66
|
+
let sizes = this.terminalSizes.get(sessionId);
|
|
67
|
+
if (!sizes) {
|
|
68
|
+
sizes = new Map();
|
|
69
|
+
this.terminalSizes.set(sessionId, sizes);
|
|
70
|
+
}
|
|
71
|
+
// 末尾に移して「最後に使われた socket」を末尾で表す
|
|
72
|
+
sizes.delete(socketId);
|
|
73
|
+
sizes.set(socketId, size);
|
|
74
|
+
}
|
|
75
|
+
/** socket の登録を外し、残っている socket のうち最後に使われたものを返す */
|
|
76
|
+
removeTerminalSocket(sessionId, socketId) {
|
|
77
|
+
const sizes = this.terminalSizes.get(sessionId);
|
|
78
|
+
if (!sizes)
|
|
79
|
+
return undefined;
|
|
80
|
+
sizes.delete(socketId);
|
|
81
|
+
const last = Array.from(sizes.entries()).at(-1);
|
|
82
|
+
if (!last) {
|
|
83
|
+
this.terminalSizes.delete(sessionId);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return { socketId: last[0], size: last[1] };
|
|
87
|
+
}
|
|
62
88
|
/**
|
|
63
89
|
* @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
|
|
64
90
|
* (例: Ollama タブでは Anthropic の認証トークンを取り除く)
|
|
@@ -200,3 +226,15 @@ class PtyManager {
|
|
|
200
226
|
}
|
|
201
227
|
}
|
|
202
228
|
export const ptyManager = new PtyManager();
|
|
229
|
+
/** 前面にいればシェルのプロンプトとみなすプロセス名(ログインシェルの先頭の `-` は外して比べる) */
|
|
230
|
+
const SHELL_NAMES = new Set(['bash', 'zsh', 'sh', 'dash', 'fish', 'ksh']);
|
|
231
|
+
/**
|
|
232
|
+
* PTY の前面がシェルか(エージェントなどのコマンドが動いていないか)。
|
|
233
|
+
* 判定できない環境(Windows)では true を返す
|
|
234
|
+
*/
|
|
235
|
+
export function isShellForeground(session) {
|
|
236
|
+
if (os.platform() === 'win32')
|
|
237
|
+
return true;
|
|
238
|
+
const name = path.basename(session.pty.process).replace(/^-/, '');
|
|
239
|
+
return SHELL_NAMES.has(name) || name === path.basename(process.env.SHELL ?? '');
|
|
240
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { mkdir } from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import { ptyManager } from '../pty-manager.js';
|
|
4
|
+
import { prisma } from '../lib/db.js';
|
|
5
|
+
import { deleteEnv, getEnv, setEnv } from '../lib/repositories/environment.js';
|
|
6
|
+
import { AGENTS, getAgent, getAgentAvailability } from '../lib/agents/index.js';
|
|
7
|
+
import { DEFAULT_PROFILE, LOGIN_SESSION_PREFIX, createProfile, deleteProfile, getProfileAuthStatus, listProfiles, loginPtyEnv, profileExists, } from '../lib/agent-profiles.js';
|
|
8
|
+
import { notifyEnvChanged } from '../lib/pty-env-sync.js';
|
|
9
|
+
import { forwardLoginCallback, needsLoginCallbackPaste } from '../lib/agents/login-callback.js';
|
|
10
|
+
/** ログイン用 PTY の sessionId からエージェントを得る(`agent-login-<agent>-<slug>-<uuid>`) */
|
|
11
|
+
function agentOfLoginSession(sessionId) {
|
|
12
|
+
return AGENTS.find((agent) => sessionId.startsWith(`${LOGIN_SESSION_PREFIX}${agent.id}-`));
|
|
13
|
+
}
|
|
14
|
+
function validateScope(scope, owner, repo) {
|
|
15
|
+
if (scope !== 'global' && scope !== 'owner' && scope !== 'repo')
|
|
16
|
+
return 'Invalid scope';
|
|
17
|
+
if (scope === 'owner' && !owner)
|
|
18
|
+
return 'owner is required for scope=owner';
|
|
19
|
+
if (scope === 'repo' && (!owner || !repo))
|
|
20
|
+
return 'owner and repo are required for scope=repo';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
async function getAssignment(agent, scope, owner, repo) {
|
|
24
|
+
const { envKey } = agent.profile;
|
|
25
|
+
const row = await prisma.environmentVariable.findFirst({
|
|
26
|
+
where: {
|
|
27
|
+
key: envKey,
|
|
28
|
+
scope,
|
|
29
|
+
...(scope !== 'global' && { owner }),
|
|
30
|
+
...(scope === 'repo' && { repo }),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const merged = await getEnv(scope, owner, repo);
|
|
34
|
+
const assignment = {
|
|
35
|
+
assigned: row?.value ?? null,
|
|
36
|
+
effective: merged[envKey] ?? DEFAULT_PROFILE,
|
|
37
|
+
};
|
|
38
|
+
return assignment;
|
|
39
|
+
}
|
|
40
|
+
function syncPtyEnv(fastify) {
|
|
41
|
+
notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
|
|
42
|
+
}
|
|
43
|
+
/** :agent を adapter に解決する。未知なら 404 を返して undefined */
|
|
44
|
+
function resolveAgent(id, reply) {
|
|
45
|
+
const agent = getAgent(id);
|
|
46
|
+
if (!agent)
|
|
47
|
+
void reply.status(404).send({ error: 'Unknown agent' });
|
|
48
|
+
return agent;
|
|
49
|
+
}
|
|
50
|
+
export async function agentProfilesRoutes(fastify) {
|
|
51
|
+
// GET /agents - エージェント CLI の導入状況
|
|
52
|
+
fastify.get('/agents', async (_request, reply) => {
|
|
53
|
+
const agents = await Promise.all(AGENTS.map((agent) => getAgentAvailability(agent)));
|
|
54
|
+
return reply.send({ data: { agents } });
|
|
55
|
+
});
|
|
56
|
+
// GET /agent-profiles/:agent - システム既定 + 登録済みプロファイルとログイン状態
|
|
57
|
+
fastify.get('/agent-profiles/:agent', async (request, reply) => {
|
|
58
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
59
|
+
if (!agent)
|
|
60
|
+
return;
|
|
61
|
+
const profiles = await listProfiles(agent);
|
|
62
|
+
const data = await Promise.all([
|
|
63
|
+
getProfileAuthStatus(agent, DEFAULT_PROFILE).then((status) => ({
|
|
64
|
+
agent: agent.id,
|
|
65
|
+
slug: DEFAULT_PROFILE,
|
|
66
|
+
name: 'System default',
|
|
67
|
+
// 設定ディレクトリの環境変数を付けない(普段ターミナルで CLI を使うときと同じ設定)
|
|
68
|
+
configDir: null,
|
|
69
|
+
status,
|
|
70
|
+
})),
|
|
71
|
+
...profiles.map(async (profile) => ({
|
|
72
|
+
...profile,
|
|
73
|
+
status: await getProfileAuthStatus(agent, profile.slug),
|
|
74
|
+
})),
|
|
75
|
+
]);
|
|
76
|
+
return reply.send({ data: { profiles: data } });
|
|
77
|
+
});
|
|
78
|
+
// GET /agent-profiles/:agent/:slug/status - ログイン状態(ログイン用ダイアログで完了を確認する)
|
|
79
|
+
fastify.get('/agent-profiles/:agent/:slug/status', async (request, reply) => {
|
|
80
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
81
|
+
if (!agent)
|
|
82
|
+
return;
|
|
83
|
+
const { slug } = request.params;
|
|
84
|
+
if (!(await profileExists(agent, slug))) {
|
|
85
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
86
|
+
}
|
|
87
|
+
return reply.send({ data: { status: await getProfileAuthStatus(agent, slug) } });
|
|
88
|
+
});
|
|
89
|
+
// POST /agent-profiles/:agent - プロファイル作成
|
|
90
|
+
fastify.post('/agent-profiles/:agent', async (request, reply) => {
|
|
91
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
92
|
+
if (!agent)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
const profile = await createProfile(agent, request.body?.name ?? '');
|
|
96
|
+
return reply.status(201).send({ data: { profile } });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return reply.status(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// DELETE /agent-profiles/:agent/:slug - プロファイル削除(logout してディレクトリごと消す)
|
|
103
|
+
fastify.delete('/agent-profiles/:agent/:slug', async (request, reply) => {
|
|
104
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
105
|
+
if (!agent)
|
|
106
|
+
return;
|
|
107
|
+
const { slug } = request.params;
|
|
108
|
+
if (slug === DEFAULT_PROFILE || !(await profileExists(agent, slug))) {
|
|
109
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
110
|
+
}
|
|
111
|
+
await deleteProfile(agent, slug);
|
|
112
|
+
syncPtyEnv(fastify);
|
|
113
|
+
return reply.status(204).send();
|
|
114
|
+
});
|
|
115
|
+
// POST /agent-profiles/:agent/:slug/login - ログイン用 PTY を起動しログインコマンドを実行する
|
|
116
|
+
fastify.post('/agent-profiles/:agent/:slug/login', async (request, reply) => {
|
|
117
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
118
|
+
if (!agent)
|
|
119
|
+
return;
|
|
120
|
+
const { slug } = request.params;
|
|
121
|
+
if (!(await profileExists(agent, slug))) {
|
|
122
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
123
|
+
}
|
|
124
|
+
// 古いログイン用 PTY はここでは閉じない(同時に届いた要求どうしで、相手が表示中の PTY を閉じてしまう)。
|
|
125
|
+
// ダイアログを閉じたときにクライアントが DELETE する
|
|
126
|
+
const sessionId = `${LOGIN_SESSION_PREFIX}${agent.id}-${slug}-${crypto.randomUUID()}`;
|
|
127
|
+
const { env, unsetKeys } = loginPtyEnv(agent, slug);
|
|
128
|
+
const configDir = env[agent.profile.configDirEnvKey];
|
|
129
|
+
// CODEX_HOME は存在しないと codex が起動しない
|
|
130
|
+
if (configDir)
|
|
131
|
+
await mkdir(configDir, { recursive: true });
|
|
132
|
+
const session = ptyManager.createSession(sessionId, os.homedir(), env, unsetKeys);
|
|
133
|
+
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
134
|
+
setTimeout(() => session.pty.write(`${agent.profile.loginCommand}\n`), 300);
|
|
135
|
+
// ブラウザから CLI の待ち受けに届かない環境では、開けなかった URL を貼り付けてもらう
|
|
136
|
+
const callbackPaste = Boolean(agent.profile.loginCallbackPorts) && needsLoginCallbackPaste();
|
|
137
|
+
return reply.status(201).send({ data: { sessionId, callbackPaste } });
|
|
138
|
+
});
|
|
139
|
+
// DELETE /agent-profiles/login/:sessionId - ログイン用 PTY を閉じる
|
|
140
|
+
fastify.delete('/agent-profiles/login/:sessionId', async (request, reply) => {
|
|
141
|
+
const { sessionId } = request.params;
|
|
142
|
+
if (!sessionId.startsWith(LOGIN_SESSION_PREFIX)) {
|
|
143
|
+
return reply.status(400).send({ error: 'Not a login session' });
|
|
144
|
+
}
|
|
145
|
+
ptyManager.deleteSession(sessionId);
|
|
146
|
+
return reply.status(204).send();
|
|
147
|
+
});
|
|
148
|
+
// POST /agent-profiles/login/:sessionId/callback - ブラウザで開けなかった戻り先の URL を CLI に渡す
|
|
149
|
+
fastify.post('/agent-profiles/login/:sessionId/callback', async (request, reply) => {
|
|
150
|
+
const { sessionId } = request.params;
|
|
151
|
+
const ports = agentOfLoginSession(sessionId)?.profile.loginCallbackPorts;
|
|
152
|
+
if (!ports || !ptyManager.getSession(sessionId)) {
|
|
153
|
+
return reply.status(404).send({ error: 'Login session not found' });
|
|
154
|
+
}
|
|
155
|
+
const error = await forwardLoginCallback(request.body?.url ?? '', ports);
|
|
156
|
+
if (error)
|
|
157
|
+
return reply.status(400).send({ error });
|
|
158
|
+
return reply.send({ data: { forwarded: true } });
|
|
159
|
+
});
|
|
160
|
+
// GET /agent-profiles/:agent/assignment - スコープのプロファイル割り当て
|
|
161
|
+
fastify.get('/agent-profiles/:agent/assignment', async (request, reply) => {
|
|
162
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
163
|
+
if (!agent)
|
|
164
|
+
return;
|
|
165
|
+
const { scope, owner, repo } = request.query;
|
|
166
|
+
const error = validateScope(scope, owner, repo);
|
|
167
|
+
if (error)
|
|
168
|
+
return reply.status(400).send({ error });
|
|
169
|
+
return reply.send({ data: { assignment: await getAssignment(agent, scope, owner, repo) } });
|
|
170
|
+
});
|
|
171
|
+
// PUT /agent-profiles/:agent/assignment - スコープのプロファイル割り当てを変更する
|
|
172
|
+
fastify.put('/agent-profiles/:agent/assignment', async (request, reply) => {
|
|
173
|
+
const agent = resolveAgent(request.params.agent, reply);
|
|
174
|
+
if (!agent)
|
|
175
|
+
return;
|
|
176
|
+
const { scope, owner, repo, slug } = request.body ?? {};
|
|
177
|
+
const error = validateScope(scope, owner, repo);
|
|
178
|
+
if (error)
|
|
179
|
+
return reply.status(400).send({ error });
|
|
180
|
+
if (slug !== null && !(await profileExists(agent, slug))) {
|
|
181
|
+
return reply.status(400).send({ error: 'Profile not found' });
|
|
182
|
+
}
|
|
183
|
+
const { envKey } = agent.profile;
|
|
184
|
+
if (slug === null) {
|
|
185
|
+
await deleteEnv(envKey, scope, owner, repo);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
await setEnv(envKey, slug, scope, owner, repo);
|
|
189
|
+
}
|
|
190
|
+
syncPtyEnv(fastify);
|
|
191
|
+
return reply.send({ data: { assignment: await getAssignment(agent, scope, owner, repo) } });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as envRepo from '../lib/repositories/environment.js';
|
|
2
|
-
import { RESERVED_ENV_KEYS } from '../lib/
|
|
2
|
+
import { RESERVED_ENV_KEYS } from '../lib/agent-profiles.js';
|
|
3
3
|
import { notifyEnvChanged } from '../lib/pty-env-sync.js';
|
|
4
|
-
const RESERVED_KEY_ERROR = '
|
|
4
|
+
const RESERVED_KEY_ERROR = 'エージェントの認証は Settings の Claude Profile / Codex Profile で設定してください';
|
|
5
5
|
/** 変更後、起動中のターミナルに反映する(失敗しても API 自体は成功扱い) */
|
|
6
6
|
function syncPtyEnv(fastify) {
|
|
7
7
|
notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import type { FastifyInstance } from 'fastify';
|
|
2
|
-
interface ClaudeHookBody {
|
|
3
|
-
hook_event_name: string;
|
|
4
|
-
session_id?: string;
|
|
5
|
-
transcript_path?: string;
|
|
6
|
-
cwd?: string;
|
|
7
|
-
permission_mode?: string;
|
|
8
|
-
prompt?: string;
|
|
9
|
-
tool_name?: string;
|
|
10
|
-
tool_use_id?: string;
|
|
11
|
-
tool_input?: Record<string, unknown>;
|
|
12
|
-
tool_response?: Record<string, unknown>;
|
|
13
|
-
error?: string;
|
|
14
|
-
error_details?: string;
|
|
15
|
-
reason?: string;
|
|
16
|
-
}
|
|
17
2
|
export interface HookEvent {
|
|
18
3
|
receivedAt: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
4
|
+
agent: string;
|
|
5
|
+
event?: string;
|
|
6
|
+
tabId?: string;
|
|
7
|
+
raw: Record<string, unknown>;
|
|
22
8
|
}
|
|
23
9
|
export declare const hookEvents: HookEvent[];
|
|
24
10
|
export declare function hooksRoutes(fastify: FastifyInstance): Promise<void>;
|
|
25
|
-
export {};
|