@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.
Files changed (144) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  21. package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
  22. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  26. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  27. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/settings.rsc +3 -3
  33. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  34. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  35. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  36. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  38. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  39. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  40. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/apps/web/.next/server/chunks/ssr/_05n88to._.js +3 -0
  42. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
  43. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0kflgfj._.js → _0pw3_r_._.js} +3 -3
  44. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0g79jqo._.js → _0rb6-mo._.js} +3 -3
  45. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ts4rpp._.js +3 -0
  46. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +2 -2
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +3 -3
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_04pfam-._.js → node_modules_03-rvdo._.js} +1 -1
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0cv326~._.js → node_modules_06jdmu~._.js} +1 -1
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0a57phw._.js → node_modules_0925te2._.js} +2 -2
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_huoqd._.js → node_modules_0e8uni7._.js} +1 -1
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0tod6y8._.js → node_modules_0h4h_aw._.js} +1 -1
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h.vgyn._.js → node_modules_0nzbb9~._.js} +1 -1
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0oip5a1._.js → node_modules_0q8yuvq._.js} +1 -1
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_11-z65a._.js → node_modules_0r6t2tu._.js} +1 -1
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09d2oqj._.js → node_modules_0t6ysh_._.js} +1 -1
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0krwgny._.js → node_modules_0vj~e_v._.js} +1 -1
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0sn0lqb._.js → node_modules_0vq~hzy._.js} +1 -1
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0l23gkf._.js → node_modules_0xzmx-x._.js} +1 -1
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0npujym._.js → node_modules_0yb2sh9._.js} +1 -1
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_5qo~._.js → node_modules_0yc~s8p._.js} +1 -1
  62. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_xfyl._.js → node_modules_0zi-gfw._.js} +1 -1
  63. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ak_3rk._.js → node_modules_0zzdoud._.js} +1 -1
  64. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0xp9nhx._.js → node_modules_0~aiu12._.js} +1 -1
  65. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  66. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  67. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  68. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  69. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  70. package/.next/standalone/apps/web/.next/static/chunks/09b_3duxnyfto.js +7 -0
  71. package/.next/standalone/apps/web/.next/static/chunks/{0z60ewb40i2ce.js → 0gxhy80~c0gvp.js} +1 -1
  72. package/.next/standalone/apps/web/.next/static/chunks/0i9h-39w91ybd.js +6 -0
  73. package/.next/standalone/apps/web/.next/static/chunks/{0b90ys7wq-p.4.css → 0pm0knaijueoc.css} +1 -1
  74. package/.next/standalone/apps/web/.next/static/chunks/{0oricprxoxb_d.js → 0smdykrt42e.5.js} +10 -10
  75. package/.next/standalone/apps/web/.next/static/chunks/{149sia28cskw9.js → 0yp~t_.pozr-d.js} +6 -6
  76. package/.next/standalone/apps/web/.next/static/chunks/{0o6ggm7jrim25.js → 14zh2vf8ljbxo.js} +1 -1
  77. package/.next/standalone/apps/web/.next/static/chunks/{053h220fng_68.js → 1624992q-gpj2.js} +10 -10
  78. package/.next/standalone/apps/web/package.json +1 -0
  79. package/README.md +1 -1
  80. package/dist/app.js +3 -3
  81. package/dist/plugin-lifecycle.d.ts +5 -3
  82. package/dist/plugin-lifecycle.js +40 -16
  83. package/dist/prisma/migrations/20261011000000_add_codex_support/migration.sql +20 -0
  84. package/dist/prisma/schema.prisma +9 -5
  85. package/dist/server/generated/prisma/browser.d.ts +2 -2
  86. package/dist/server/generated/prisma/client.d.ts +2 -2
  87. package/dist/server/generated/prisma/internal/class.d.ts +4 -4
  88. package/dist/server/generated/prisma/internal/class.js +4 -4
  89. package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +41 -39
  90. package/dist/server/generated/prisma/internal/prismaNamespace.js +4 -2
  91. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +5 -3
  92. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +4 -2
  93. package/dist/server/generated/prisma/models/AgentProfile.d.ts +972 -0
  94. package/dist/server/generated/prisma/models/Tab.d.ts +36 -1
  95. package/dist/server/generated/prisma/models.d.ts +1 -1
  96. package/dist/server/index.js +2 -2
  97. package/dist/server/lib/agent-profiles.d.ts +43 -0
  98. package/dist/server/lib/agent-profiles.js +127 -0
  99. package/dist/server/lib/agents/claude.d.ts +2 -0
  100. package/dist/server/lib/agents/claude.js +130 -0
  101. package/dist/server/lib/agents/cli.d.ts +14 -0
  102. package/dist/server/lib/agents/cli.js +57 -0
  103. package/dist/server/lib/agents/codex.d.ts +2 -0
  104. package/dist/server/lib/agents/codex.js +285 -0
  105. package/dist/server/lib/agents/index.d.ts +11 -0
  106. package/dist/server/lib/agents/index.js +19 -0
  107. package/dist/server/lib/agents/login-callback.d.ts +11 -0
  108. package/dist/server/lib/agents/login-callback.js +47 -0
  109. package/dist/server/lib/agents/types.d.ts +77 -0
  110. package/dist/server/lib/agents/types.js +8 -0
  111. package/dist/server/lib/pty-env-sync.js +1 -1
  112. package/dist/server/lib/pty-launch.d.ts +8 -10
  113. package/dist/server/lib/pty-launch.js +20 -58
  114. package/dist/server/lib/repositories/task.js +1 -0
  115. package/dist/server/lib/tab-todos.d.ts +5 -0
  116. package/dist/server/lib/tab-todos.js +13 -0
  117. package/dist/server/pty-manager.d.ts +7 -2
  118. package/dist/server/pty-manager.js +13 -0
  119. package/dist/server/routes/agent-profiles.d.ts +2 -0
  120. package/dist/server/routes/agent-profiles.js +193 -0
  121. package/dist/server/routes/env.js +2 -2
  122. package/dist/server/routes/hooks.d.ts +4 -19
  123. package/dist/server/routes/hooks.js +83 -73
  124. package/dist/server/routes/mcp.js +2 -2
  125. package/dist/server/routes/onboarding.js +19 -10
  126. package/dist/server/routes/tasks.js +4 -4
  127. package/dist/server/routes/terminal.js +47 -34
  128. package/dist/with-plugin.js +3 -3
  129. package/npm-shrinkwrap.json +12 -12
  130. package/package.json +1 -1
  131. package/tsunagi-marketplace/codex/tsunagi-task.md +120 -0
  132. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hnxatt._.js +0 -3
  133. package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +0 -3
  134. package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +0 -6
  135. package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +0 -7
  136. package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +0 -939
  137. package/dist/server/lib/claude-profiles.d.ts +0 -49
  138. package/dist/server/lib/claude-profiles.js +0 -168
  139. package/dist/server/routes/claude-profiles.d.ts +0 -2
  140. package/dist/server/routes/claude-profiles.js +0 -138
  141. /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → ZcOuTrikuO_ZLaERk7FsW}/_buildManifest.js +0 -0
  142. /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → ZcOuTrikuO_ZLaERk7FsW}/_clientMiddlewareManifest.js +0 -0
  143. /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → ZcOuTrikuO_ZLaERk7FsW}/_ssgManifest.js +0 -0
  144. /package/dist/server/generated/prisma/models/{ClaudeProfile.js → AgentProfile.js} +0 -0
@@ -0,0 +1,57 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execFileAsync = promisify(execFile);
4
+ /**
5
+ * エージェント CLI を実行するときの環境変数。プロファイルのログインを見るよう設定ディレクトリを付け、
6
+ * ログインより優先される認証情報の環境変数は取り除く。システム既定は外側 Terminal の環境のまま
7
+ * (普段ターミナルで CLI を使うときと同じ)。
8
+ */
9
+ export function agentCliEnv(profile, configDir) {
10
+ const env = { ...process.env };
11
+ if (configDir) {
12
+ env[profile.configDirEnvKey] = configDir;
13
+ for (const key of profile.authEnvKeys)
14
+ delete env[key];
15
+ }
16
+ return env;
17
+ }
18
+ export async function runAgentCli(cli, args, env) {
19
+ return execFileAsync(cli, args, { env, timeout: 15_000 });
20
+ }
21
+ function isAtLeast(version, min) {
22
+ for (let i = 0; i < min.length; i++) {
23
+ if (version[i] !== min[i])
24
+ return version[i] > min[i];
25
+ }
26
+ return true;
27
+ }
28
+ /** CLI の更新を拾えるよう、短時間だけ結果を使い回す */
29
+ const AVAILABILITY_TTL_MS = 30_000;
30
+ const availabilityCache = new Map();
31
+ async function detectAvailability(adapter) {
32
+ const minVersion = adapter.minVersion.join('.');
33
+ try {
34
+ const { stdout } = await runAgentCli(adapter.cli, ['--version'], process.env);
35
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout);
36
+ const version = match ? match.slice(1).map(Number) : null;
37
+ return {
38
+ agent: adapter.id,
39
+ installed: true,
40
+ version: version?.join('.'),
41
+ supported: version !== null && isAtLeast(version, adapter.minVersion),
42
+ minVersion,
43
+ };
44
+ }
45
+ catch {
46
+ return { agent: adapter.id, installed: false, supported: false, minVersion };
47
+ }
48
+ }
49
+ /** `<cli> --version` で導入状況を確認する */
50
+ export function getAgentAvailability(adapter) {
51
+ const cached = availabilityCache.get(adapter.id);
52
+ if (cached && Date.now() - cached.at < AVAILABILITY_TTL_MS)
53
+ return cached.value;
54
+ const value = detectAvailability(adapter);
55
+ availabilityCache.set(adapter.id, { at: Date.now(), value });
56
+ return value;
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentAdapter } from './types.js';
2
+ export declare const codexAdapter: AgentAdapter;
@@ -0,0 +1,285 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'fs/promises';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { getStateDir } from '../data-path.js';
6
+ import { prisma } from '../db.js';
7
+ import { planToTodos } from '../tab-todos.js';
8
+ import { agentCliEnv, runAgentCli } from './cli.js';
9
+ // Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数を見る
10
+ const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
11
+ const SERVER_URL = `http://localhost:${SERVER_PORT}`;
12
+ /** hook の送信元タブを示すヘッダー(Codex のセッションIDはタブIDと別のため PTY の env から渡す) */
13
+ const TAB_ID_HEADER = 'x-tsunagi-tab-id';
14
+ /**
15
+ * hook のコマンド。タブごとの値は PTY の env(TSUNAGI_API_BASE / TSUNAGI_SESSION_ID)で渡し、
16
+ * 文字列自体は固定にする(trust のハッシュが変わらないように)。
17
+ * Codex は一部の hook の stdout を JSON の指示として読むため捨て、送信に失敗しても 0 で終える。
18
+ * `${VAR}` 形式は Codex が置換するため使わない
19
+ */
20
+ const HOOK_COMMAND = `curl -s -m 5 -X POST "$TSUNAGI_API_BASE/api/hooks/codex"` +
21
+ ` -H "X-Tsunagi-Tab-Id: $TSUNAGI_SESSION_ID" -H 'Content-Type: application/json'` +
22
+ ` --data-binary @- >/dev/null 2>&1 || true`;
23
+ /**
24
+ * 登録する hook と、その trust 用ラベル(hooks.state のキーとハッシュに使う)。
25
+ * SessionEnd / Interrupt は Codex 側で timeout が最大 3 秒に制限される
26
+ */
27
+ const HOOK_EVENTS = [
28
+ { event: 'SessionStart', label: 'session_start', timeout: 10 },
29
+ { event: 'UserPromptSubmit', label: 'user_prompt_submit', timeout: 10 },
30
+ { event: 'PermissionRequest', label: 'permission_request', timeout: 10 },
31
+ { event: 'PostToolUse', label: 'post_tool_use', timeout: 10 },
32
+ { event: 'Stop', label: 'stop', timeout: 10 },
33
+ { event: 'Interrupt', label: 'interrupt', timeout: 3 },
34
+ { event: 'SessionEnd', label: 'session_end', timeout: 3 },
35
+ ];
36
+ const SIMPLE_EVENTS = {
37
+ UserPromptSubmit: 'prompt',
38
+ PermissionRequest: 'permission',
39
+ Stop: 'stop',
40
+ // Esc での中断
41
+ Interrupt: 'interrupt',
42
+ // /exit・Ctrl+C 等
43
+ SessionEnd: 'session-end',
44
+ };
45
+ function shellQuote(value) {
46
+ return `'${value.replace(/'/g, `'\\''`)}'`;
47
+ }
48
+ /** TOML のキー・文字列(JSON の文字列表現は TOML の basic string として読める) */
49
+ const tomlString = (value) => JSON.stringify(value);
50
+ function sortKeys(value) {
51
+ if (Array.isArray(value))
52
+ return value.map(sortKeys);
53
+ if (value && typeof value === 'object') {
54
+ return Object.fromEntries(Object.keys(value)
55
+ .sort()
56
+ .map((key) => [key, sortKeys(value[key])]));
57
+ }
58
+ return value;
59
+ }
60
+ /**
61
+ * `-c` で渡した hook を承認済みにするハッシュ。Codex は正規化した hook 定義(キーを整列した JSON)の
62
+ * sha256 を trust の照合に使う(codex-rs/hooks/src/engine/discovery.rs の hook_hash)
63
+ */
64
+ function hookTrustHash(label, timeout) {
65
+ const identity = {
66
+ event_name: label,
67
+ hooks: [{ type: 'command', command: HOOK_COMMAND, timeout, async: false }],
68
+ };
69
+ return `sha256:${createHash('sha256')
70
+ .update(JSON.stringify(sortKeys(identity)))
71
+ .digest('hex')}`;
72
+ }
73
+ /**
74
+ * tsunagi 連携用の `-c` 引数。ユーザーの config.toml には書き込まない(このプロセスだけに効く)
75
+ * - hooks: 状態遷移・todos の通知。`-c` で渡した hook の trust は `<session-flags>` のキーで照合される。
76
+ * キーにドットを含むため、`hooks.state` はインラインテーブルで丸ごと渡す
77
+ * - tools.update_plan: 計画ツール(todos の元)を有効にする
78
+ * - mcp_servers.tsunagi: タスク管理の MCP
79
+ */
80
+ function tsunagiConfigArgs(extra) {
81
+ const args = ['--dangerously-bypass-approvals-and-sandbox'];
82
+ const state = [];
83
+ for (const { event, label, timeout } of HOOK_EVENTS) {
84
+ args.push('-c', `hooks.${event}=[{hooks=[{type="command",command=${tomlString(HOOK_COMMAND)},timeout=${timeout}}]}]`);
85
+ state.push(`${tomlString(`/<session-flags>/config.toml:${label}:0:0`)}={trusted_hash=${tomlString(hookTrustHash(label, timeout))}}`);
86
+ }
87
+ args.push('-c', `hooks.state={${state.join(',')}}`);
88
+ args.push('-c', 'tools.update_plan.enabled=true');
89
+ args.push('-c', `mcp_servers.tsunagi.url=${tomlString(`${SERVER_URL}/api/mcp`)}`);
90
+ if (extra.developerInstructions) {
91
+ args.push('-c', `developer_instructions=${tomlString(extra.developerInstructions)}`);
92
+ }
93
+ if (extra.agentRoleFile) {
94
+ args.push('-c', `agents.tsunagi-task-curator={description=${tomlString(CURATOR_DESCRIPTION)},config_file=${tomlString(extra.agentRoleFile)}}`);
95
+ }
96
+ return args;
97
+ }
98
+ const CURATOR_DESCRIPTION = 'tsunagiタスクの並び替え(優先度変更)・棚卸し(backlog整理)を専門に行うエージェント。' +
99
+ '「優先度を並び替えて」「棚卸しして」「backlogを整理して」等の要望があった時に使用する。';
100
+ /**
101
+ * Claude Code 用のプラグイン(TSUNAGI_PLUGIN_DIR = <marketplace>/plugins/tsunagi-plugin)からの相対パスで
102
+ * Codex 向けの本文を読む。skill は Codex 向けに書き直したもの(<marketplace>/codex)、
103
+ * subagent は vendor 中立のためプラグインのものを共用する
104
+ */
105
+ async function readPluginFile(...segments) {
106
+ const pluginDir = process.env.TSUNAGI_PLUGIN_DIR;
107
+ if (!pluginDir)
108
+ return undefined;
109
+ try {
110
+ return await readFile(path.resolve(pluginDir, ...segments), 'utf-8');
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ }
116
+ /** Markdown 先頭の YAML frontmatter を外す */
117
+ function stripFrontmatter(markdown) {
118
+ return markdown.replace(/^---\n[\s\S]*?\n---\n/, '').trimStart();
119
+ }
120
+ /** 複数タブの同時起動で書きかけのファイルを読まないよう、書き終えてから置き換える */
121
+ async function writeAtomic(file, content) {
122
+ await mkdir(path.dirname(file), { recursive: true });
123
+ const tmp = `${file}.${randomUUID()}.tmp`;
124
+ await writeFile(tmp, content);
125
+ await rename(tmp, file);
126
+ }
127
+ /** subagent の role ファイル(Codex の設定レイヤー)を state ディレクトリに書き出す */
128
+ async function writeCuratorRole() {
129
+ const markdown = await readPluginFile('agents', 'tsunagi-task-curator.md');
130
+ if (!markdown)
131
+ return undefined;
132
+ const file = path.join(getStateDir(), 'codex', 'tsunagi-task-curator.toml');
133
+ await writeAtomic(file, `developer_instructions = ${tomlString(stripFrontmatter(markdown))}\n`);
134
+ return file;
135
+ }
136
+ /**
137
+ * codex の起動スクリプトを書き出し、そのパスを返す。`sh <script> [<codex-session-id>]` で起動する。
138
+ * Codex にはセッションIDを指定して新規作成する手段がないため、タブには前回の Codex のセッションIDを
139
+ * 保存しておき(SessionStart hook)、その会話記録が CODEX_HOME にあれば resume、なければ新規作成する。
140
+ * 引数は長く、PTY の1行上限(MAX_CANON)で切り捨てられるためスクリプトに閉じ込める
141
+ */
142
+ async function writeLauncher() {
143
+ const [developerInstructions, agentRoleFile] = await Promise.all([
144
+ readPluginFile('..', '..', 'codex', 'tsunagi-task.md'),
145
+ writeCuratorRole(),
146
+ ]);
147
+ const args = tsunagiConfigArgs({ developerInstructions, agentRoleFile }).map(shellQuote);
148
+ const script = `#!/bin/sh
149
+ # usage: launch-codex.sh [<codex-session-id>]
150
+ session_id=$1
151
+ # 作業フォルダの信頼確認を出さない(Claude の --dangerously-skip-permissions と揃える)。
152
+ # -c のキーはドットで区切られパスを書けないため、projects をインラインテーブルで渡す
153
+ cwd_key=$(pwd -P | sed 's/[\\\\"]/\\\\&/g')
154
+ set -- ${args.join(' \\\n ')} \\
155
+ -c "projects={\\"$cwd_key\\"={trust_level=\\"trusted\\"}}"
156
+ codex_home=\${CODEX_HOME:-$HOME/.codex}
157
+ if [ -n "$session_id" ] &&
158
+ [ -n "$(find "$codex_home/sessions" -name "rollout-*-$session_id.jsonl" 2>/dev/null | head -n 1)" ]; then
159
+ exec codex resume "$@" "$session_id"
160
+ fi
161
+ exec codex "$@"
162
+ `;
163
+ const file = path.join(getStateDir(), 'launch-codex.sh');
164
+ await writeAtomic(file, script);
165
+ return file;
166
+ }
167
+ /** JWT の payload を読む(署名は検証しない。表示用) */
168
+ function decodeJwtPayload(token) {
169
+ if (typeof token !== 'string')
170
+ return undefined;
171
+ const payload = token.split('.')[1];
172
+ if (!payload)
173
+ return undefined;
174
+ try {
175
+ return JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
176
+ }
177
+ catch {
178
+ return undefined;
179
+ }
180
+ }
181
+ /** ChatGPT ログインのアカウント情報(auth.json の id_token)。keyring 保存などで読めなければ空 */
182
+ async function readChatgptAccount(codexHome) {
183
+ try {
184
+ const auth = JSON.parse(await readFile(path.join(codexHome, 'auth.json'), 'utf-8'));
185
+ const claims = decodeJwtPayload(auth.tokens?.id_token);
186
+ if (!claims)
187
+ return {};
188
+ const openai = claims['https://api.openai.com/auth'];
189
+ return {
190
+ ...(typeof claims.email === 'string' && { email: claims.email }),
191
+ ...(typeof openai?.chatgpt_plan_type === 'string' && {
192
+ subscriptionType: openai.chatgpt_plan_type,
193
+ }),
194
+ };
195
+ }
196
+ catch {
197
+ return {};
198
+ }
199
+ }
200
+ const codexProfile = {
201
+ envKey: 'TSUNAGI_CODEX_PROFILE',
202
+ configDirEnvKey: 'CODEX_HOME',
203
+ // auth.json のログインより優先される認証情報
204
+ authEnvKeys: ['CODEX_API_KEY', 'CODEX_ACCESS_TOKEN'],
205
+ defaultConfigDir: '~/.codex',
206
+ // ブラウザでの OAuth(127.0.0.1:1455 で待ち受け)。戻れない環境ではユーザーが --device-auth に切り替える
207
+ loginCommand: 'codex login',
208
+ // codex login はブラウザでの認証の戻りを 127.0.0.1:1455(使用中なら 1457)で待つ
209
+ loginCallbackPorts: [1455, 1457],
210
+ // `codex login status` は終了コードでログイン状態を返し、stderr に1行だけ方式を出す
211
+ async getAuthStatus(configDir) {
212
+ const env = agentCliEnv(codexProfile, configDir);
213
+ const codexHome = configDir ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
214
+ try {
215
+ const { stdout, stderr } = await runAgentCli('codex', ['login', 'status'], env);
216
+ const line = `${stderr}\n${stdout}`.trim().split('\n').pop() ?? '';
217
+ const method = /^Logged in using (.+?)(?: - .*)?$/.exec(line)?.[1];
218
+ return {
219
+ loggedIn: true,
220
+ authMethod: method,
221
+ ...(method === 'ChatGPT' && (await readChatgptAccount(codexHome))),
222
+ };
223
+ }
224
+ catch (err) {
225
+ // 未ログインは終了コード 1("Not logged in")
226
+ const code = err.code;
227
+ if (typeof code === 'number')
228
+ return { loggedIn: false };
229
+ return { loggedIn: false, error: err instanceof Error ? err.message : String(err) };
230
+ }
231
+ },
232
+ async logout(configDir) {
233
+ await runAgentCli('codex', ['logout'], agentCliEnv(codexProfile, configDir));
234
+ },
235
+ };
236
+ export const codexAdapter = {
237
+ id: 'codex',
238
+ label: 'Codex',
239
+ cli: 'codex',
240
+ // tsunagi が使う `-c` キー(hooks.state / tools.update_plan 等)を確認したバージョン
241
+ minVersion: [0, 162, 0],
242
+ profile: codexProfile,
243
+ async resolveLaunchEnv() {
244
+ return { env: {}, unsetKeys: [] };
245
+ },
246
+ // CODEX_HOME は存在しないと codex が起動しない(プロファイル作成時に作るが、消されていた場合に備える)
247
+ async prepareLaunch(env) {
248
+ if (env.CODEX_HOME)
249
+ await mkdir(env.CODEX_HOME, { recursive: true });
250
+ },
251
+ async buildCommand(tabId) {
252
+ const [launcher, tab] = await Promise.all([
253
+ writeLauncher(),
254
+ prisma.tab.findUnique({ where: { tabId }, select: { agentSessionId: true } }),
255
+ ]);
256
+ const sessionId = tab?.agentSessionId;
257
+ return sessionId
258
+ ? `sh ${shellQuote(launcher)} ${shellQuote(sessionId)}`
259
+ : `sh ${shellQuote(launcher)}`;
260
+ },
261
+ normalizeHook({ body, headers }) {
262
+ const header = headers[TAB_ID_HEADER];
263
+ const tabId = Array.isArray(header) ? header[0] : header;
264
+ const { hook_event_name: eventName, session_id: sessionId } = body;
265
+ if (!tabId || !eventName)
266
+ return null;
267
+ if (eventName === 'SessionStart') {
268
+ return { tabId, kind: 'session-start', ...(sessionId && { agentSessionId: sessionId }) };
269
+ }
270
+ const kind = SIMPLE_EVENTS[eventName];
271
+ if (kind)
272
+ return { tabId, kind };
273
+ if (eventName === 'PostToolUse') {
274
+ const { tool_name: toolName, tool_input: input } = body;
275
+ return {
276
+ tabId,
277
+ kind: 'tool-post',
278
+ toolName,
279
+ // update_plan は毎回計画全体が渡されるため全置換する
280
+ ...(toolName === 'update_plan' && { applyTodos: () => planToTodos(input) }),
281
+ };
282
+ }
283
+ return null;
284
+ },
285
+ };
@@ -0,0 +1,11 @@
1
+ import type { TabMode } from '@minimalcorp/tsunagi-shared';
2
+ import type { AgentAdapter } from './types.js';
3
+ export * from './types.js';
4
+ export { getAgentAvailability } from './cli.js';
5
+ export declare const AGENTS: readonly AgentAdapter[];
6
+ export declare function getAgent(id: string): AgentAdapter | undefined;
7
+ /**
8
+ * タブの mode で起動するエージェント。local(旧 ollama / lmstudio 含む)はローカルLLMに繋いだ claude。
9
+ * terminal は自動では起動しないが、Run ボタンで起動するのは claude
10
+ */
11
+ export declare function getAgentForMode(mode: TabMode | undefined): AgentAdapter;
@@ -0,0 +1,19 @@
1
+ import { claudeAdapter } from './claude.js';
2
+ import { codexAdapter } from './codex.js';
3
+ export * from './types.js';
4
+ export { getAgentAvailability } from './cli.js';
5
+ const ADAPTERS = {
6
+ claude: claudeAdapter,
7
+ codex: codexAdapter,
8
+ };
9
+ export const AGENTS = Object.values(ADAPTERS);
10
+ export function getAgent(id) {
11
+ return Object.hasOwn(ADAPTERS, id) ? ADAPTERS[id] : undefined;
12
+ }
13
+ /**
14
+ * タブの mode で起動するエージェント。local(旧 ollama / lmstudio 含む)はローカルLLMに繋いだ claude。
15
+ * terminal は自動では起動しないが、Run ボタンで起動するのは claude
16
+ */
17
+ export function getAgentForMode(mode) {
18
+ return mode === 'codex' ? codexAdapter : claudeAdapter;
19
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ブラウザでの OAuth の戻り先(CLI が 127.0.0.1 で待ち受ける)にブラウザから届かない環境か。
3
+ * コンテナ内の CLI にはホストのブラウザから届かない(ホストのポートを公開すると、ホストで動く CLI の
4
+ * ログインとポートを取り合うため公開しない)
5
+ */
6
+ export declare function needsLoginCallbackPaste(): boolean;
7
+ /**
8
+ * ブラウザが開けなかった戻り先の URL(http://127.0.0.1:<port>/auth/callback?code=…)を、
9
+ * 同じマシンで待ち受けている CLI に渡す。失敗したらエラーメッセージを返す
10
+ */
11
+ export declare function forwardLoginCallback(rawUrl: string, ports: number[]): Promise<string | null>;
@@ -0,0 +1,47 @@
1
+ import { existsSync } from 'fs';
2
+ /**
3
+ * ブラウザでの OAuth の戻り先(CLI が 127.0.0.1 で待ち受ける)にブラウザから届かない環境か。
4
+ * コンテナ内の CLI にはホストのブラウザから届かない(ホストのポートを公開すると、ホストで動く CLI の
5
+ * ログインとポートを取り合うため公開しない)
6
+ */
7
+ export function needsLoginCallbackPaste() {
8
+ return existsSync('/.dockerenv');
9
+ }
10
+ /**
11
+ * ブラウザが開けなかった戻り先の URL(http://127.0.0.1:<port>/auth/callback?code=…)を、
12
+ * 同じマシンで待ち受けている CLI に渡す。失敗したらエラーメッセージを返す
13
+ */
14
+ export async function forwardLoginCallback(rawUrl, ports) {
15
+ let url;
16
+ try {
17
+ url = new URL(rawUrl.trim());
18
+ }
19
+ catch {
20
+ return 'URL の形式が正しくありません';
21
+ }
22
+ const port = Number(url.port);
23
+ if (url.protocol !== 'http:' ||
24
+ !['127.0.0.1', 'localhost'].includes(url.hostname) ||
25
+ !ports.includes(port)) {
26
+ return `http://127.0.0.1:${ports[0]}/… の URL を貼り付けてください`;
27
+ }
28
+ const get = (target) => fetch(target, { redirect: 'manual', signal: AbortSignal.timeout(30_000) });
29
+ try {
30
+ const res = await get(`http://127.0.0.1:${port}${url.pathname}${url.search}`);
31
+ if (res.status < 400) {
32
+ // 成功すると完了ページへリダイレクトされる。CLI はローカルの完了ページが開かれてから終了するため、
33
+ // 同じ待ち受け先へのリダイレクトだけ辿る(外部のページは開かない)
34
+ const location = res.headers.get('location');
35
+ const next = location ? new URL(location, `http://127.0.0.1:${port}`) : null;
36
+ if (next && ['127.0.0.1', 'localhost'].includes(next.hostname) && next.port === url.port) {
37
+ await get(`http://127.0.0.1:${port}${next.pathname}${next.search}`).catch(() => undefined);
38
+ }
39
+ return null;
40
+ }
41
+ const body = (await res.text().catch(() => '')).replace(/<[^>]+>/g, ' ').trim();
42
+ return `ログインに失敗しました (${res.status})${body ? `: ${body.slice(0, 200)}` : ''}`;
43
+ }
44
+ catch (err) {
45
+ return `ログインの待ち受けに接続できません: ${err instanceof Error ? err.message : String(err)}`;
46
+ }
47
+ }
@@ -0,0 +1,77 @@
1
+ import type { AgentAuthStatus, AgentId, TabMode, Todo } from '@minimalcorp/tsunagi-shared';
2
+ export type { AgentId };
3
+ /** エージェントの起動に必要な設定が足りないなど、起動前に分かる失敗 */
4
+ export declare class AgentLaunchError extends Error {
5
+ readonly statusCode: number;
6
+ constructor(message: string, statusCode: number);
7
+ }
8
+ /**
9
+ * vendor ごとの hook を tsunagi の状態遷移に正規化したもの。
10
+ * tabId は tsunagi のタブID(Claude はセッションID = タブID、それ以外は PTY の TSUNAGI_SESSION_ID)
11
+ */
12
+ export type NormalizedHookEvent = {
13
+ tabId: string;
14
+ kind: 'session-start';
15
+ /** エージェント側のセッションID(タブIDと別の ID を振るエージェントのみ)。resume 用に保存する */
16
+ agentSessionId?: string;
17
+ } | {
18
+ tabId: string;
19
+ kind: 'prompt' | 'permission' | 'stop' | 'stop-failure' | 'interrupt' | 'session-end';
20
+ } | {
21
+ tabId: string;
22
+ kind: 'tool-post';
23
+ toolName?: string;
24
+ /** todos を更新するツールなら、現在の todos に適用する関数(解釈できなければ null を返す) */
25
+ applyTodos?: (current: Todo[]) => Todo[] | null;
26
+ };
27
+ export interface HookRequest {
28
+ body: Record<string, unknown>;
29
+ headers: Record<string, string | string[] | undefined>;
30
+ }
31
+ /** PTY に渡すエージェント固有の環境変数 */
32
+ export interface AgentLaunchEnv {
33
+ env: Record<string, string>;
34
+ unsetKeys: string[];
35
+ }
36
+ /** プロファイル(エージェントの設定ディレクトリ)の扱い */
37
+ export interface AgentProfileSpec {
38
+ /** タブで使うプロファイルの割り当てを保存する予約キー(global → owner → repo で継承) */
39
+ envKey: string;
40
+ /** 設定ディレクトリを指す環境変数(CLAUDE_CONFIG_DIR / CODEX_HOME) */
41
+ configDirEnvKey: string;
42
+ /**
43
+ * プロファイルのログインより優先される認証情報の環境変数。
44
+ * プロファイルを選んだタブでは外側 Terminal から継承した値も取り除く
45
+ */
46
+ authEnvKeys: string[];
47
+ /** システム既定の設定ディレクトリ(表示用) */
48
+ defaultConfigDir: string;
49
+ /** ログイン用 PTY に書き込むコマンド */
50
+ loginCommand: string;
51
+ /**
52
+ * ブラウザでの OAuth の戻り先として CLI が 127.0.0.1 で待ち受けるポート(使用中のときの予備を含む)。
53
+ * ブラウザから届かない環境(コンテナ内)では、ブラウザで開けなかった URL を貼り付けて CLI に渡す(login-callback.ts)
54
+ */
55
+ loginCallbackPorts?: number[];
56
+ /** configDir が undefined ならシステム既定 */
57
+ getAuthStatus(configDir: string | undefined): Promise<AgentAuthStatus>;
58
+ logout(configDir: string): Promise<void>;
59
+ }
60
+ export interface AgentAdapter {
61
+ id: AgentId;
62
+ /** 表示名 */
63
+ label: string;
64
+ /** CLI のコマンド名 */
65
+ cli: string;
66
+ /** tsunagi が必要とする最低バージョン */
67
+ minVersion: readonly [number, number, number];
68
+ profile: AgentProfileSpec;
69
+ /** タブの mode に応じた追加の環境変数(DB の環境変数より後に適用する) */
70
+ resolveLaunchEnv(mode: TabMode): Promise<AgentLaunchEnv>;
71
+ /** 起動前の補正(設定ファイルの修正など)。env は PTY に渡す環境変数 */
72
+ prepareLaunch(env: Record<string, string>): Promise<void>;
73
+ /** PTY に書き込む起動コマンド(既存セッションがあれば resume) */
74
+ buildCommand(tabId: string, mode: TabMode): Promise<string>;
75
+ /** hook の受信内容を正規化する。状態遷移に関係しないイベントは null */
76
+ normalizeHook(request: HookRequest): NormalizedHookEvent | null;
77
+ }
@@ -0,0 +1,8 @@
1
+ /** エージェントの起動に必要な設定が足りないなど、起動前に分かる失敗 */
2
+ export class AgentLaunchError extends Error {
3
+ statusCode;
4
+ constructor(message, statusCode) {
5
+ super(message);
6
+ this.statusCode = statusCode;
7
+ }
8
+ }
@@ -70,7 +70,7 @@ async function respawn(session) {
70
70
  try {
71
71
  await startPtySession(sessionId, launch, {
72
72
  request: session.options.request ?? {},
73
- launchClaude: session.options.launchClaude ?? false,
73
+ launchAgent: session.options.launchAgent ?? false,
74
74
  });
75
75
  }
76
76
  catch (err) {
@@ -1,5 +1,6 @@
1
1
  import type { TabMode } from '@minimalcorp/tsunagi-shared';
2
2
  import { type PtySession } from '../pty-manager.js';
3
+ import { AgentLaunchError as PtyLaunchError } from './agents/index.js';
3
4
  /** PTY 起動時にクライアントが指定する値(環境変数の自動反映で作り直すときも同じ値を使う) */
4
5
  export interface PtyLaunchRequest {
5
6
  cwd?: string;
@@ -13,24 +14,21 @@ export interface PtyLaunch {
13
14
  env: Record<string, string>;
14
15
  unsetKeys: string[];
15
16
  }
16
- export declare class PtyLaunchError extends Error {
17
- readonly statusCode: number;
18
- constructor(message: string, statusCode: number);
19
- }
17
+ export { PtyLaunchError };
20
18
  /**
21
19
  * sessionId(=tab_id) から PTY の作業ディレクトリと環境変数を解決する。
22
- * 環境変数の優先順位: リクエストで渡されたenv > ローカルLLMのprovider env >
23
- * DB env(global/owner/repo、Claude プロファイルを CLAUDE_CONFIG_DIR に変換) > tsunagi独自のEDITOR設定
20
+ * 環境変数の優先順位: リクエストで渡されたenv > エージェント固有のenv(ローカルLLMの provider 等) >
21
+ * DB env(global/owner/repo、エージェントのプロファイルを設定ディレクトリに変換) > tsunagi独自のEDITOR設定
24
22
  */
25
23
  export declare function resolvePtyLaunch(sessionId: string, request: PtyLaunchRequest): Promise<PtyLaunch>;
26
24
  /**
27
- * 解決済みの launch で PTY を作り、claude を起動する。
25
+ * 解決済みの launch で PTY を作り、エージェントを起動する。
28
26
  * 新規作成(POST /terminal/sessions)と環境変数変更時の作り直し(pty-env-sync)で共用する。
29
27
  */
30
28
  export declare function startPtySession(sessionId: string, launch: PtyLaunch, options: {
31
29
  request: PtyLaunchRequest;
32
- launchClaude: boolean;
30
+ launchAgent: boolean;
33
31
  command?: string;
34
32
  }): Promise<PtySession>;
35
- /** 既存 PTY で claude を(再)起動する前の補正。PTY 作成時の環境変数のプロファイルを対象にする */
36
- export declare function prepareClaudeConfigForSession(session: PtySession): Promise<void>;
33
+ /** 既存 PTY でエージェントを(再)起動する前の補正。PTY 作成時の環境変数のプロファイルを対象にする */
34
+ export declare function prepareAgentLaunchForSession(session: PtySession, mode: TabMode | undefined): Promise<void>;