@minimalcorp/tsunagi 0.3.3 → 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/{0oricprxoxb_d.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/{053h220fng_68.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 +7 -2
- package/dist/server/pty-manager.js +13 -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 +47 -34
- package/dist/with-plugin.js +3 -3
- package/npm-shrinkwrap.json +12 -12
- 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/{7x81GlNKt9yqIkeI7U1BX → ZcOuTrikuO_ZLaERk7FsW}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → ZcOuTrikuO_ZLaERk7FsW}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → 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
|
}
|
|
@@ -77,4 +77,9 @@ declare class PtyManager {
|
|
|
77
77
|
deleteAllSessions(): void;
|
|
78
78
|
}
|
|
79
79
|
export declare const ptyManager: PtyManager;
|
|
80
|
+
/**
|
|
81
|
+
* PTY の前面がシェルか(エージェントなどのコマンドが動いていないか)。
|
|
82
|
+
* 判定できない環境(Windows)では true を返す
|
|
83
|
+
*/
|
|
84
|
+
export declare function isShellForeground(session: PtySession): boolean;
|
|
80
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
|
/**
|
|
@@ -225,3 +226,15 @@ class PtyManager {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
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 {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { prisma } from '../lib/db.js';
|
|
2
|
-
import {
|
|
2
|
+
import { parseTodos } from '../lib/tab-todos.js';
|
|
3
|
+
import { getAgent } from '../lib/agents/index.js';
|
|
3
4
|
export const hookEvents = [];
|
|
4
5
|
/**
|
|
5
6
|
* status から未読フラグを導出する。
|
|
6
7
|
* 完了(success/error)で未読を立て、再実行・セッション終了で落とす。
|
|
7
|
-
* web 側 (apps/web/src/lib/
|
|
8
|
+
* web 側 (apps/web/src/lib/agent-status.ts の unreadForStatus) と同じルール。
|
|
8
9
|
*/
|
|
9
10
|
function unreadForStatus(status) {
|
|
10
11
|
switch (status) {
|
|
@@ -68,88 +69,97 @@ async function updateTabStatus(sessionId, status, todos) {
|
|
|
68
69
|
return { count: 0 };
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
/** 正規化した hook イベントでタブの状態を更新し、購読中のクライアントに通知する */
|
|
73
|
+
async function applyHookEvent(fastify, io, event) {
|
|
74
|
+
const { tabId } = event;
|
|
75
|
+
const room = `tab:${tabId}`;
|
|
76
|
+
const setStatus = async (status, emitted = status) => {
|
|
77
|
+
await updateTabStatus(tabId, status);
|
|
78
|
+
io.to(room).emit('status-changed', { sessionId: tabId, status: emitted });
|
|
79
|
+
};
|
|
80
|
+
switch (event.kind) {
|
|
81
|
+
case 'session-start':
|
|
82
|
+
// エージェントが起動しただけ(まだプロンプト処理していない)→ idleのまま通知のみ。
|
|
83
|
+
// タブIDと別のセッションIDを振るエージェント(Codex)は、次回の resume 用に保存する
|
|
84
|
+
if (event.agentSessionId) {
|
|
85
|
+
await prisma.tab
|
|
86
|
+
.updateMany({ where: { tabId }, data: { agentSessionId: event.agentSessionId } })
|
|
87
|
+
.catch((err) => fastify.log.warn(err, '[hooks] Failed to save agent session id'));
|
|
88
|
+
}
|
|
89
|
+
io.to(room).emit('status-changed', { sessionId: tabId, status: 'idle' });
|
|
90
|
+
break;
|
|
91
|
+
case 'prompt':
|
|
92
|
+
// プロンプト送信開始 → running
|
|
93
|
+
await setStatus('running');
|
|
94
|
+
break;
|
|
95
|
+
case 'permission':
|
|
96
|
+
// ツール実行許可待ち → waiting
|
|
97
|
+
await setStatus('waiting');
|
|
98
|
+
break;
|
|
99
|
+
case 'tool-post': {
|
|
100
|
+
// ツール実行完了(許可待ち後のAllow含む)→ runningに戻す。todos を更新するツールなら todos も更新する
|
|
101
|
+
const { applyTodos } = event;
|
|
102
|
+
const todosUpdated = applyTodos !== undefined &&
|
|
103
|
+
(await withTodosLock(tabId, async () => {
|
|
104
|
+
const todos = applyTodos(await loadTabTodos(tabId));
|
|
105
|
+
if (!todos) {
|
|
106
|
+
fastify.log.debug({ tabId, tool: event.toolName }, '[hooks] todo tool ignored (unknown task or unexpected payload)');
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const { count } = await updateTabStatus(tabId, 'running', todos);
|
|
110
|
+
fastify.log.info({ tabId, tool: event.toolName, todosCount: todos.length, updatedTabCount: count }, '[hooks] todos updated');
|
|
111
|
+
io.to(room).emit('todos-updated', { sessionId: tabId, todos });
|
|
112
|
+
return true;
|
|
113
|
+
}));
|
|
114
|
+
if (!todosUpdated) {
|
|
115
|
+
await updateTabStatus(tabId, 'running');
|
|
116
|
+
}
|
|
117
|
+
io.to(room).emit('status-changed', { sessionId: tabId, status: 'running' });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'stop':
|
|
121
|
+
// 正常完了 → success
|
|
122
|
+
await setStatus('success');
|
|
123
|
+
break;
|
|
124
|
+
case 'stop-failure':
|
|
125
|
+
// APIエラーでターン終了 → error
|
|
126
|
+
// todos は残す(エージェント側の進捗はセッションに永続化され、resume 後も続きから使われる)
|
|
127
|
+
await setStatus('error', 'failure');
|
|
128
|
+
break;
|
|
129
|
+
case 'interrupt':
|
|
130
|
+
case 'session-end':
|
|
131
|
+
// 中断・セッション終了 → idle(todos は stop-failure と同じ理由で残す)
|
|
132
|
+
await setStatus('idle');
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
71
136
|
export async function hooksRoutes(fastify) {
|
|
72
137
|
const io = fastify.io;
|
|
73
|
-
// POST /hooks
|
|
74
|
-
fastify.post('/hooks
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
138
|
+
// POST /hooks/:agent - エージェント CLI からの hook 受信
|
|
139
|
+
fastify.post('/hooks/:agent', async (request, reply) => {
|
|
140
|
+
const adapter = getAgent(request.params.agent);
|
|
141
|
+
if (!adapter)
|
|
142
|
+
return reply.status(404).send({ error: 'Unknown agent' });
|
|
143
|
+
const body = request.body ?? {};
|
|
144
|
+
const event = adapter.normalizeHook({ body, headers: request.headers });
|
|
145
|
+
const eventName = typeof body.hook_event_name === 'string' ? body.hook_event_name : undefined;
|
|
146
|
+
fastify.log.info({ agent: adapter.id, eventName, tabId: event?.tabId }, 'Agent hook received');
|
|
79
147
|
// メモリに記録(確認用)
|
|
80
148
|
hookEvents.push({
|
|
81
149
|
receivedAt: new Date().toISOString(),
|
|
150
|
+
agent: adapter.id,
|
|
82
151
|
event: eventName,
|
|
83
|
-
|
|
152
|
+
tabId: event?.tabId,
|
|
84
153
|
raw: body,
|
|
85
154
|
});
|
|
86
155
|
// 最新100件のみ保持
|
|
87
156
|
if (hookEvents.length > 100)
|
|
88
157
|
hookEvents.splice(0, hookEvents.length - 100);
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
io.to(room).emit('status-changed', { sessionId, status: 'idle' });
|
|
95
|
-
break;
|
|
96
|
-
case 'UserPromptSubmit':
|
|
97
|
-
// プロンプト送信開始 → running状態に更新
|
|
98
|
-
await updateTabStatus(sessionId, 'running');
|
|
99
|
-
io.to(room).emit('status-changed', { sessionId, status: 'running' });
|
|
100
|
-
break;
|
|
101
|
-
case 'PermissionRequest':
|
|
102
|
-
// ツール実行許可待ち → waiting状態に更新
|
|
103
|
-
await updateTabStatus(sessionId, 'waiting');
|
|
104
|
-
io.to(room).emit('status-changed', { sessionId, status: 'waiting' });
|
|
105
|
-
break;
|
|
106
|
-
case 'PostToolUse': {
|
|
107
|
-
// ツール実行完了(PermissionRequest後のAllow含む)→ runningに戻す
|
|
108
|
-
// TodoWrite / Task 系ツールなら todos も更新する
|
|
109
|
-
const toolName = body.tool_name;
|
|
110
|
-
const todosUpdated = toolName !== undefined &&
|
|
111
|
-
TODO_TOOL_NAMES.has(toolName) &&
|
|
112
|
-
(await withTodosLock(sessionId, async () => {
|
|
113
|
-
const todos = applyToolToTodos(await loadTabTodos(sessionId), toolName, body.tool_input, body.tool_response);
|
|
114
|
-
if (!todos) {
|
|
115
|
-
fastify.log.debug({ sessionId, tool: toolName, input: body.tool_input }, '[hooks] todo tool ignored (unknown task or unexpected payload)');
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
const { count } = await updateTabStatus(sessionId, 'running', todos);
|
|
119
|
-
fastify.log.info({ sessionId, tool: toolName, todosCount: todos.length, updatedTabCount: count }, '[hooks] todos updated');
|
|
120
|
-
io.to(room).emit('todos-updated', { sessionId, todos });
|
|
121
|
-
return true;
|
|
122
|
-
}));
|
|
123
|
-
if (!todosUpdated) {
|
|
124
|
-
await updateTabStatus(sessionId, 'running');
|
|
125
|
-
}
|
|
126
|
-
io.to(room).emit('status-changed', { sessionId, status: 'running' });
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
case 'PreToolUse':
|
|
130
|
-
// ログ記録のみ
|
|
131
|
-
break;
|
|
132
|
-
case 'Stop':
|
|
133
|
-
// 正常完了 → success状態に更新
|
|
134
|
-
await updateTabStatus(sessionId, 'success');
|
|
135
|
-
io.to(room).emit('status-changed', { sessionId, status: 'success' });
|
|
136
|
-
break;
|
|
137
|
-
case 'StopFailure':
|
|
138
|
-
// APIエラーでターン終了 → failure状態に更新
|
|
139
|
-
// todos は残す(Claude 側の Task はセッションに永続化され、--resume 後も続きから使われる)
|
|
140
|
-
await updateTabStatus(sessionId, 'error');
|
|
141
|
-
io.to(room).emit('status-changed', { sessionId, status: 'failure' });
|
|
142
|
-
break;
|
|
143
|
-
case 'SessionEnd':
|
|
144
|
-
// セッション終了(Escキー中断・Ctrl+C・/exit等)→ idle状態に更新(todos は StopFailure と同じ理由で残す)
|
|
145
|
-
// reason: "prompt_input_exit" = ユーザー中断、"other" = プロセスkill等
|
|
146
|
-
fastify.log.info({ sessionId, reason: body.reason }, 'SessionEnd received');
|
|
147
|
-
await updateTabStatus(sessionId, 'idle');
|
|
148
|
-
io.to(room).emit('status-changed', { sessionId, status: 'idle' });
|
|
149
|
-
break;
|
|
150
|
-
default:
|
|
151
|
-
fastify.log.debug({ eventName }, 'Unhandled hook event');
|
|
152
|
-
}
|
|
158
|
+
if (event) {
|
|
159
|
+
await applyHookEvent(fastify, io, event);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
fastify.log.debug({ agent: adapter.id, eventName }, 'Unhandled hook event');
|
|
153
163
|
}
|
|
154
164
|
return reply.status(200).send({ ok: true });
|
|
155
165
|
});
|
|
@@ -22,7 +22,7 @@ const taskIdentifierProperties = {
|
|
|
22
22
|
const STATUS_ENUM = ['backlog', 'planning', 'coding', 'reviewing', 'done'];
|
|
23
23
|
/** status description(LLMバイアス対策の警告文言付き) */
|
|
24
24
|
const STATUS_DESCRIPTION = 'ステータス。backlog → planning → coding → reviewing → done の順で遷移。' +
|
|
25
|
-
'注意: `in_progress` / `completed` / `todo`
|
|
25
|
+
'注意: `in_progress` / `completed` / `todo` は使用不可(エージェント組み込みの TODO 管理ツール(Claude Code の TaskUpdate、Codex の update_plan)の語彙と混同しないこと)。' +
|
|
26
26
|
'実装中 = `coding`、完了 = `done`。';
|
|
27
27
|
/** typo 検出ヒント(schema validation が skip された場合のフォールバック) */
|
|
28
28
|
const TYPO_HINTS = {
|
|
@@ -74,7 +74,7 @@ function createMcpServer(io) {
|
|
|
74
74
|
],
|
|
75
75
|
description: 'ステータスフィルタ。単一値(例: "backlog")または配列(例: ["backlog", "planning"])で指定。' +
|
|
76
76
|
'有効値: backlog / planning / coding / reviewing / done。' +
|
|
77
|
-
'注意: `in_progress` / `completed` / `todo`
|
|
77
|
+
'注意: `in_progress` / `completed` / `todo` は使用不可(エージェント組み込みの TODO 管理ツールの語彙)。',
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
80
|
additionalProperties: false,
|