codexteer 0.16.0 → 0.17.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.
@@ -2,6 +2,7 @@ import { lstat, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { codexHome } from "./runtime.mjs";
4
4
  import { RpcClient } from "./rpc.mjs";
5
+ import { inspectAppServerSocket } from "./socket-endpoint.mjs";
5
6
 
6
7
  const failure = (message, code) => Object.assign(new Error(message), { code });
7
8
  const reads = new Set(["thread/loaded/list", "thread/read", "thread/turns/list", "thread/items/list"]);
@@ -16,16 +17,22 @@ export function desktopReadOnly() {
16
17
  async function identity(home) {
17
18
  const directory = path.join(home, "app-server-control"), socket = path.join(directory, "app-server-control.sock");
18
19
  const fingerprint = [];
19
- for (const [file, type] of [[home, "home"], [directory, "directory"], [socket, "socket"]]) {
20
+ for (const [file, type] of [[home, "home"], [directory, "directory"]]) {
20
21
  const info = await lstat(file);
21
22
  if (info.uid !== process.getuid() || info.isSymbolicLink() || (info.mode & (type === "home" ? 0o022 : 0o077)) !== 0
22
- || (type === "socket" ? !info.isSocket() : !info.isDirectory())) {
23
+ || !info.isDirectory()) {
23
24
  throw failure("Desktop control endpoint has unsafe ownership, permissions, or file type.", "DESKTOP_CONNECTION_UNSAFE");
24
25
  }
25
26
  // Home directory timestamps change for unrelated profile activity.
26
- fingerprint.push([info.dev, info.ino, type === "socket" ? info.ctimeMs : null]);
27
+ fingerprint.push([info.dev, info.ino]);
27
28
  }
28
- return { home, directory, socket, fingerprint: JSON.stringify(fingerprint) };
29
+ let endpoint;
30
+ try { endpoint = await inspectAppServerSocket(socket); }
31
+ catch (error) {
32
+ if (error.code === "SOCKET_UNSAFE") throw failure(error.message, "DESKTOP_CONNECTION_UNSAFE");
33
+ throw error;
34
+ }
35
+ return { home, directory, socket: endpoint.socket, fingerprint: JSON.stringify([...fingerprint, ...endpoint.fingerprint]) };
29
36
  }
30
37
 
31
38
  export async function discoverDesktopRuntime(home = codexHome()) {
@@ -0,0 +1,16 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function desktopMembership(home, threadId, projectId) {
5
+ const state = JSON.parse(await readFile(path.join(home, ".codex-global-state.json"), "utf8"));
6
+ const assignment = state["thread-project-assignments"]?.[threadId];
7
+ return assignment?.projectKind === "local" && assignment.projectId === projectId;
8
+ }
9
+
10
+ export function initialInstructionMatches(history, prompt, sourceId) {
11
+ const item = history.turns?.[0]?.items?.find(item => item.type === "userMessage"
12
+ || (item.type === "functionCallOutput" && item.namespace === "codex_app" && item.name === "create_thread"));
13
+ if (item?.type === "userMessage") return item.content?.filter(part => part.type === "text").map(part => part.text).join("\n") === prompt;
14
+ const expected = `<codex_delegation>\n <source_thread_id>${sourceId}</source_thread_id>\n <input>${prompt}</input>\n</codex_delegation>`;
15
+ return item?.output === expected;
16
+ }
@@ -38,7 +38,7 @@ async function checkAncestors(directory) {
38
38
  }
39
39
  }
40
40
 
41
- async function resolveHome(home) {
41
+ export async function prepareCodexHome(home) {
42
42
  let existing = path.resolve(home), canonical;
43
43
  const missing = [];
44
44
  while (true) {
@@ -53,6 +53,8 @@ async function resolveHome(home) {
53
53
  canonical = path.join(canonical, part);
54
54
  await ownedDirectory(canonical, true);
55
55
  }
56
+ const homeInfo = await lstat(canonical);
57
+ if (!homeInfo.isDirectory() || homeInfo.uid !== process.getuid() || (homeInfo.mode & 0o7022)) throw failure("CODEX_HOME must be an owner-controlled directory.");
56
58
  return canonical;
57
59
  }
58
60
 
@@ -150,9 +152,7 @@ export async function prepareDeployment(home, { sourceRoot = PACKAGE_ROOT } = {}
150
152
  const distribution = await describeDistribution(sourceRoot);
151
153
  // CODEX_HOME may itself be a user-configured symlink; only its canonical
152
154
  // owner-writable directory is accepted. Never follow links below that root.
153
- const canonicalHome = await resolveHome(home);
154
- const homeInfo = await lstat(canonicalHome);
155
- if (!homeInfo.isDirectory() || homeInfo.uid !== process.getuid() || (homeInfo.mode & 0o7022)) throw failure("CODEX_HOME must be an owner-controlled directory.");
155
+ const canonicalHome = await prepareCodexHome(home);
156
156
  const stateRoot = path.join(canonicalHome, "codex-steer"), root = path.join(stateRoot, "runtimes");
157
157
  await ownedDirectory(stateRoot, true); await ownedDirectory(root, true);
158
158
  const target = path.join(root, distribution.id), lock = path.join(root, `.install-${distribution.id}`);
package/src/help.mjs CHANGED
@@ -6,22 +6,23 @@ export { VERSION } from "./version.mjs";
6
6
  import { VERSION } from "./version.mjs";
7
7
 
8
8
  const supervisionExecutionNotes = [
9
+ "--use-path-cliを付けると、本文はPATH上のcodexteerを案内します。CLIの保存・Nodeの固定・CODEX_HOMEの埋め込みを行わず、実行環境のPATHとCODEX_HOMEを使います。CLIの更新は次の実行から反映されます。--supervisor・--connectionと監督の制御は維持します。直接起動ではエージェント引数の区切り -- より前に指定してください。",
9
10
  "各生成に新しい--supervisor IDと--connectionを埋め込みます。直接起動はagent起動前にregisterし、終了時にstopします。本文だけの生成は未登録で、貼り付け先が本文のregisterを実行して開始します。同じ本文の再登録は一時停止を解除せず、停止済みIDは復活しません。",
10
11
  "--connectionの既定はshared(既存wrapper)。desktopは既存の共有制御ソケットを使う観測専用です。送信の実行例を含めず、方針に送信指示があっても送信を許可しません。詳細はhelp connectionを参照してください。",
11
12
  "本文は必須の共通テンプレートと監督方針で構成します。MESSAGEを省略すると標準方針(目的・制約の範囲で必要最小限の介入と結果確認、簡潔な報告)を使い、指定すると標準方針全体をその文に置き換えます。対象ID、実行コマンド、help/read/watch、送信時の確認、停止手順はどちらにも含めます。",
12
13
  "MESSAGEは引用して一つの引数として渡してください。空文字・空白だけはINVALID_SUPERVISION_POLICYで拒否します。改行等はそのまま本文に含め、プレースホルダー展開やシェル実行はしません。指定は今回の生成・起動だけに適用し、既定設定や保存したCLIは変更しません。",
13
- "CLI本体と依存をCODEX_HOME/codex-steer/runtimes/<version>-<sha256>へ検証して保存し、そのコピーへの実行コマンドを本文に埋め込みます。supervise promptもこの保存処理を行います。同じ内容は検証して再利用し、既存のコピーは上書き・自動削除しません。保存・検証に失敗した場合は本文を出力せず、監督エージェントも起動しません。",
14
- "同じPCで使い、監督中は保存したコピーを保持してください。全コマンドの先頭に生成時に検証したCODEX_HOMEの実体パスを指定するため、監督役のCODEX_HOMEが未設定・別設定でも生成元の接続先と履歴を使います。元のnpxキャッシュやチェックアウトを更新・削除しても、監督は保存した内容を使います。ヘルプ中のcodexteer表記も、先頭のCODEX_HOME指定・引用・オプションを含む指定の実行コマンドに置き換えてください。",
15
- "Node本体はコピーせず、開始時の実体への絶対パスと--require-node-versionを各コマンドに埋め込みます。Nodeの版が変われば操作前にNODE_VERSION_MISMATCHで停止します。同じ版のNodeの改変や共有ライブラリまで固定するものではないため、元のNodeは保持してください。保存先やNodeが使えなければ介入を止めて報告し、意図した環境から生成し直すよう指示します。",
14
+ "既定ではCLI本体と依存をCODEX_HOME/codex-steer/runtimes/<version>-<sha256>へ検証して保存し、そのコピーへの実行コマンドを本文に埋め込みます。supervise promptもこの保存処理を行います。同じ内容は検証して再利用し、既存のコピーは上書き・自動削除しません。保存・検証に失敗した場合は本文を出力せず、監督エージェントも起動しません。",
15
+ "保存済みCLIを使う既定モードでは、同じPCで使い、監督中は保存したコピーを保持してください。全コマンドの先頭に生成時に検証したCODEX_HOMEの実体パスを指定するため、監督役のCODEX_HOMEが未設定・別設定でも生成元の接続先と履歴を使います。元のnpxキャッシュやチェックアウトを更新・削除しても、監督は保存した内容を使います。ヘルプ中のcodexteer表記も、先頭のCODEX_HOME指定・引用・オプションを含む指定の実行コマンドに置き換えてください。",
16
+ "既定ではNode本体はコピーせず、開始時の実体への絶対パスと--require-node-versionを各コマンドに埋め込みます。Nodeの版が変われば操作前にNODE_VERSION_MISMATCHで停止します。同じ版のNodeの改変や共有ライブラリまで固定するものではないため、元のNodeは保持してください。保存先やNodeが使えなければ介入を止めて報告し、意図した環境から生成し直すよう指示します。",
16
17
  ];
17
18
 
18
19
  export const topics = {
19
20
  supervise: {
20
21
  title: "指定したCodexタスクの監督役としてClaudeまたはCodex CLIを起動する",
21
22
  when: "対象タスクのIDを指定して、Claude CodeまたはCodex CLIを監督役の対話セッションとして起動するとき。",
22
- usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [-- <AGENT-ARGS...>]", "supervise status <THREAD>", "supervise list", "supervise pause <THREAD>", "supervise resume <THREAD>", "supervise stop <THREAD>", "--supervisor <SESSION-ID> supervise register <THREAD> [--owner NAME]"],
23
+ usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli] [-- <AGENT-ARGS...>]", "supervise status <THREAD>", "supervise list", "supervise pause <THREAD>", "supervise resume <THREAD>", "supervise stop <THREAD>", "--supervisor <SESSION-ID> supervise register <THREAD> [--owner NAME]"],
23
24
  returns: "監督エージェントの標準入力・標準出力・標準エラーと終了コードを引き継ぎます。codexteerの出力は混ぜません。",
24
- examples: ["codexteer supervise <THREAD>", 'codexteer supervise <THREAD> "セキュリティの問題だけを私へ報告し、Codexへは送信しないでください。"', 'codexteer supervise <THREAD> "テスト失敗を優先して監督してください。" --agent claude -- --model <MODEL> --effort <LEVEL>'],
25
+ examples: ["codexteer supervise <THREAD>", "codexteer supervise <THREAD> --use-path-cli", 'codexteer supervise <THREAD> "セキュリティの問題だけを私へ報告し、Codexへは送信しないでください。"', 'codexteer supervise <THREAD> "テスト失敗を優先して監督してください。" --agent claude -- --model <MODEL> --effort <LEVEL>'],
25
26
  notes: [...supervisionExecutionNotes,
26
27
  "status/listは登録状態active/paused/stopped、owner、launcherの生存確認、最後のCLI観測とその経過時間、接続状態、最後の配送を返します。statusは未解決の指摘も要約します。activeは登録状態であり、AIの読了や継続監視の証明ではありません。コピーした本文のlauncher_aliveはnullです。",
27
28
  "pauseは登録済み監督の送信を拒否し、read/watchは続行できます。resumeで介入を再開、stopでその監督の以後の観測・送信を拒否します。Codexタスクやagentプロセス自体は終了しません。必要ならagentへ終了を指示してください。",
@@ -61,9 +62,9 @@ export const topics = {
61
62
  "supervise prompt": {
62
63
  title: "監督役(オーケストレーター)向けのプロンプトを出力する",
63
64
  when: "Claude Codeなどの監督役へ渡す、対象タスク入りの初期プロンプト本文を取得・確認するとき。",
64
- usage: ["supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop]"],
65
- returns: "通常はプロンプト本文だけを標準出力へ返します。--jsonではdata.thread_id、data.prompt、元のホーム(codex_home)・保存先・版・ハッシュ・再利用の有無を示すdata.deployment、Nodeのパスと版を示すdata.node、セッションID・owner・connectionを示すdata.supervisorを返します。",
66
- examples: ["codexteer supervise prompt <THREAD>", 'codexteer supervise prompt <THREAD> "進捗を観測し、完了時だけ私へ報告してください。Codexへの送信は不要です。"', "codexteer supervise prompt codex://threads/<UUID> --json", "codexteer supervise <THREAD>"],
65
+ usage: ["supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli]"],
66
+ returns: "通常はプロンプト本文だけを標準出力へ返します。--jsonではdata.thread_id、data.prompt、元のホーム(codex_home)・保存先・版・ハッシュ・再利用の有無を示すdata.deployment、Nodeのパスと版を示すdata.node、セッションID・owner・connectionを示すdata.supervisor、実行方式を示すdata.cli_mode(savedまたはpath)を返します。--use-path-cliではdeploymentとnodeはnullです。",
67
+ examples: ["codexteer supervise prompt <THREAD>", "codexteer supervise prompt <THREAD> --use-path-cli", 'codexteer supervise prompt <THREAD> "進捗を観測し、完了時だけ私へ報告してください。Codexへの送信は不要です。"', "codexteer supervise prompt codex://threads/<UUID> --json", "codexteer supervise <THREAD>"],
67
68
  notes: [...supervisionExecutionNotes,
68
69
  "出力は監督役(オーケストレーター)へ渡す初期指示です。THREADはUUIDまたはcodex://threads/...。IDの書式を確認して正規化し、本文へ埋め込みます。Desktopへの接続・履歴取得・送信・Claudeの起動・設定変更は行いません。タスクの存在と接続は監督開始時に確認します。", "Claudeを起動する場合はsupervise <THREAD> [MESSAGE]を使います。追加の起動引数は -- の後ろへ渡してください。", "共通の操作手順はhelp monitorと同じです。観測の観点・介入の判断基準・報告方法は選択した監督方針に従います。Monitorが使えない場合は短いwatchで観測します。", "生成した本文を既存のエージェントへ貼り付けても使えます。本文のregisterで開始し、監督停止は端末のsupervise stopかそのセッションへの指示を使います。プロンプトはCLIやOSの権限設定を変更しません。"],
69
70
  },
@@ -108,15 +109,16 @@ export const topics = {
108
109
  overview: {
109
110
  title: "別のAIと、Codexタスクの進行を共有する",
110
111
  when: "タスクを探す → 現状を読む → 指示を送る → 変化を確認する、という順で使います。",
111
- usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [-- <AGENT-ARGS...>]", "supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop]", "supervise status|pause|resume|stop <THREAD>", "supervise list", "threads list [--desktop-only] [--limit N]", "status <THREAD>", "read <THREAD> [--since CURSOR] [--limit N]", "watch <THREAD> [--since CURSOR] [--until change|idle|attention]", "send <THREAD> <MESSAGE...> [--new-turn] [--dry-run] [--no-sound]", "doctor [--backend app-server|ui]", "desktop start [--dry-run]", "thread resolve <THREAD>", "open <THREAD>", "debug-ui <THREAD> [--wait-ms N]", "help <COMMAND>"],
112
+ usage: ["supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli] [-- <AGENT-ARGS...>]", "supervise prompt <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli]", "supervise status|pause|resume|stop <THREAD>", "supervise list", "projects list", "thread create --project ID|NAME [--context-thread THREAD-ID] <MESSAGE>", "threads list [--desktop-only] [--limit N]", "status <THREAD>", "read <THREAD> [--since CURSOR] [--limit N]", "watch <THREAD> [--since CURSOR] [--until change|idle|attention]", "send <THREAD> <MESSAGE...> [--new-turn] [--dry-run] [--no-sound]", "doctor [--backend app-server|ui]", "desktop start [--dry-run]", "thread resolve <THREAD>", "open <THREAD>", "debug-ui <THREAD> [--wait-ms N]", "help <COMMAND>"],
112
113
  returns: "対話起動のsupervise <THREAD> [MESSAGE]を除き、各コマンドに --json を付けると、AIやスクリプトが扱えるJSONを返します。supervise promptもJSONに対応します。",
113
- examples: ["codexteer supervise <THREAD>", "codexteer threads list --desktop-only --json", "codexteer read <THREAD> --json", "codexteer help send"],
114
+ examples: ["codexteer supervise <THREAD>", "codexteer supervise <THREAD> --use-path-cli", "codexteer threads list --desktop-only --json", "codexteer read <THREAD> --json", "codexteer help send"],
114
115
  use_cases: [
115
116
  { need: "ClaudeまたはCodex CLIを監督役にする", command: "help supervise / help monitor" },
116
117
  { need: "監督役(オーケストレーター)向けのプロンプトを取得", command: "help supervise prompt" },
117
118
  { need: "監督状態・一時停止・再開を確認", command: "help supervise" },
118
119
  { need: "指摘と解決条件・評価を追跡", command: "help findings" },
119
120
  { need: "通常起動Desktopの観測接続", command: "help connection" },
121
+ { need: "プロジェクトへ初回指示付きチャットを作成", command: "help projects / help thread" },
120
122
  { need: "今の状態・前回からの変化を確認", command: "help read / help status" },
121
123
  { need: "Claude CodeのMonitorから使う", command: "help monitor" },
122
124
  { need: "根拠や仮説を付けて指示する", command: "help send" },
@@ -125,7 +127,7 @@ export const topics = {
125
127
  { need: "テストしたソースと成果物を照合", command: "help checkpoint" },
126
128
  { need: "画面・Unityなどの利用を調整", command: "help resource" },
127
129
  ],
128
- notes: ["THREADにはUUIDまたはcodex://threads/...を指定します。", "監督役の対話起動: codexteer supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [-- <AGENT-ARGS...>]。superviseの標準入出力と終了コードは監督エージェントのものです。", "コマンド別のhelpには、使い所・出力・例・制約を載せています。help自体も --json に対応します。"],
130
+ notes: ["THREADにはUUIDまたはcodex://threads/...を指定します。", "監督役の対話起動: codexteer supervise <THREAD> [MESSAGE] [--agent claude|codex] [--connection shared|desktop] [--use-path-cli] [-- <AGENT-ARGS...>]。superviseの標準入出力と終了コードは監督エージェントのものです。", "コマンド別のhelpには、使い所・出力・例・制約を載せています。help自体も --json に対応します。"],
129
131
  },
130
132
  read: {
131
133
  title: "直近の発言・コマンド・変更ファイルを読む",
@@ -169,8 +171,23 @@ export const topics = {
169
171
  },
170
172
  doctor: { title: "接続・実行環境・観測APIを診断する", when: "接続できないとき、監督の開始時、またはDesktop更新後。", usage: ["doctor [--backend app-server|ui]", "doctor --thread <THREAD> [--backend app-server]"], returns: "ready、製品バージョン、connection、runtime_compatibility、desktop_subscription、compatibility.api_checks、failureと復旧案内。", examples: ["codexteer doctor --json", "codexteer doctor --thread <THREAD> --json"], notes: ["codex_steer_compatibilityは製品名・版の比較で、違いだけでは停止しません。runtime_compatibilityで通信仕様・機能・操作ごとのsupported/unsupported/unverifiedを返します。新規ターンだけ未対応でも観測と通常送信は継続できます。", "宣言なしの旧環境は読み取りで検証し、未知の送信仕様は試験送信せず停止します。CAPABILITY_UNSUPPORTED/UNVERIFIEDは該当機能、RUNTIME_PROTOCOL_UNSUPPORTED/UNVERIFIEDは共通仕様を確認してください。対象未指定では接続を診断し、観測の互換性はunverifiedです。--threadで指定した対象の読み取り経路を検証します。readyは接続条件と指定された検証の結果です。", "compatibility.statusはverified/unverified/unsupported/failed。verifiedは対象の初回観測に成功したことを表し、履歴全体・送信・画面表示・承認往復の保証ではありません。呼び出していないAPIもunverifiedです。", "--threadはapp-server専用。本文を診断出力に含めず、タスクの再開・送信・承認回答・Desktop再起動・設定変更は行いません。failureにはコードと失敗したメソッドを返します。"] },
171
173
  desktop: { title: "共有接続を有効にしてDesktopを起動する", when: "現在の作業を終え、Desktopを終了した後の起動時。", usage: ["desktop start [--dry-run]"], returns: "起動結果とdeployment(保存先、内容ハッシュ、版、再利用の有無)。", examples: ["codexteer desktop start --dry-run --json", "codexteer desktop start"], notes: ["呼び出した配布物をCODEX_HOME/codex-steer/runtimes/<version>-<sha256>へ検証して配置し、そのwrapperで起動します。保存先の所有者・権限・リンク・内容を検証し、既存の配置は上書きしません。npxの取得元はcodexteerです。通常はバージョン指定なしで使えます。特定の版で開始したい場合は、任意でパッケージ名に@バージョンを付けてください。起動と操作の製品版が異なっても、必要な通信・機能仕様が対応していれば使用できます。", "dry-runは接続・配置・起動を行いません。配置ロックが残っている場合は自動で奪わずDEPLOYMENT_BUSYで停止します。", "起動中のDesktopは終了させません。通常起動への復旧は、終了後にDock等から開き直します。"] },
174
+ projects: { title: "Desktopのローカルプロジェクトを探す", when: "新規チャットの所属とソースフォルダを選ぶとき。", usage: ["projects list"], returns: "App Serverのproject_id、Desktopの旧ID、名前、全ソースフォルダ。", examples: ["codexteer projects list --json"], notes: ["既存shared接続を使います。ローカルプロジェクトのみ。Desktopの状態ファイルはID対応の読取りだけに使います。同名ならproject_idを指定してください。"] },
172
175
  threads: { title: "送信先のタスクを探す", when: "宛先の正確なIDを確認するとき。", usage: ["threads list [--desktop-only] [--limit N]"], returns: "ローカルタスクのID、作業場所、更新日時。", examples: ["codexteer threads list --desktop-only --limit 20 --json"], notes: ["CODEX_HOMEに従います。本文は読みません。"] },
173
- thread: { title: "タスクURLをIDに変換する", when: "コピーしたURLをスクリプトで利用するとき。", usage: ["thread resolve <THREAD>"], returns: "正規化したthread_idとdeep_link。", examples: ["codexteer thread resolve codex://threads/<UUID> --json"], notes: ["IDの書式を確認します。タスクの存在確認はstatusを使ってください。"] },
176
+ thread: {
177
+ title: "プロジェクトへタスクを作成し、IDを解決する",
178
+ when: "新規チャットへ初回指示を渡すとき、URLをIDへ変換するとき。",
179
+ usage: ["thread create --project ID|NAME [--context-thread THREAD-ID] [--timeout-ms N] [--title TEXT] [--model MODEL] [--thinking EFFORT] [--dry-run] <MESSAGE...>|-", "thread resolve <THREAD>"],
180
+ returns: "createは作成ID・所属ID・全フォルダ・初回指示の照合状態・配送状態・receipt_path。",
181
+ examples: ["codexteer thread create --project '競馬' '関連リポジトリを読み、調査結果を報告してください' --json", "codexteer thread create --project '競馬' --dry-run '調査してください' --json", "codexteer projects list --json", "codexteer thread resolve codex://threads/<UUID> --json"],
182
+ notes: ["プロジェクト名と初回指示だけで作成できます。既存チャット・CODEX_THREAD_ID・中継AIは不要です。App Serverで全フォルダ付きチャットを作り、Desktop購読を確立して初回指示を送信後、その新規チャット自身の実在thread/turn IDでnative所属登録を行います。ローカルプロジェクトのみ対応します。",
183
+ "--context-threadは任意の明示的な関連付けです。指定時だけ保存済みターンのある実在チャットを呼出し元にnative create_threadを使います。そのチャットへの送信や追加AIターンはありません。省略時はstandalone作成となり、CODEX_THREAD_IDも参照しません。",
184
+ "モデル・権限・承認設定はプロジェクトのApp Server設定に従います。--model/--thinking/--titleは明示指定可能。cwd・権限・承認設定の上書きと旧--via-threadは非対応。--timeout-msは各RPCの待機期限で既定120000、1000〜600000。CLI終了後も承認はDesktopが担当し、自動回答しません。",
185
+ "standaloneで--titleを省略すると、初回指示の空白をまとめた先頭80文字を名前として保存します。タイトル用AIは起動しません。この名前はJSON/receiptにも含まれます。",
186
+ "作成後にDesktopの所属ストア、canonical projectId、全フォルダ、保存初回指示とタイトルを照合します。履歴の反映待ちは読取りだけで行います。詳細と実機検証はdocs/thread-create.md。",
187
+ "--dry-runは既存接続・プロジェクト・native schemaを読取り確認し、チャット・指示・保存記録は作りません。",
188
+ "作成・初回配送を自動再試行しません。creation_status、thread_id、receipt_path、native_call_idを確認してください。createdなら既存IDを使い、unknown/dispatchingは未作成の証拠ではありません。Desktop/DB/保存履歴を直接編集しません。--connection desktopと--supervisorでのcreateは拒否します。",
189
+ "resolveはID書式だけを検査し、タスク存在確認はstatusを使います。"],
190
+ },
174
191
  open: { title: "タスクをDesktopで開く", when: "ユーザーが画面でタスクを見たいとき。", usage: ["open <THREAD>"], returns: "開いたタスクのURL。", examples: ["codexteer open <THREAD>"], notes: ["画面を操作する明示的なコマンドです。"] },
175
192
  "debug-ui": { title: "UI送信のために画面構造を調べる", when: "明示的なUI方式を診断するとき。", usage: ["debug-ui <THREAD> [--wait-ms N]"], returns: "Accessibilityの診断情報。", examples: ["codexteer debug-ui <THREAD> --wait-ms 1500"], notes: ["画面を操作します。通常のバックグラウンド送信には不要です。"] },
176
193
  };
@@ -0,0 +1,92 @@
1
+ import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
2
+ import { spawnSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { BUNDLED_CLI, BUNDLED_NODE } from "./wrapper.mjs";
8
+ import { nativeRequest } from "./native-rpc.mjs";
9
+
10
+ const LIMIT = 8 * 1024 * 1024;
11
+ const failure = (message, code = "NATIVE_COMMAND_FAILED", uncertain = false) => Object.assign(new Error(message), { code, uncertain });
12
+
13
+ // An internal JSON-RPC error can arrive after a mutation has run. RpcFailure's
14
+ // default uncertain:false only describes the response, not rollback of effects.
15
+ // Only method/argument rejection proves that dispatch could not execute it.
16
+ export function mutationOutcomeUnknown(error) {
17
+ return Number.isSafeInteger(error.rpcCode) ? ![-32601, -32602].includes(error.rpcCode) : error.uncertain !== false;
18
+ }
19
+
20
+ export function validateNativeCommandSchema(params, response) {
21
+ for (const key of ["command", "cwd", "timeoutMs", "outputBytesCap", "sandboxPolicy"]) if (!params?.properties?.[key]) throw failure("Missing native helper command schema.", "NATIVE_COMMAND_UNVERIFIED");
22
+ const readOnly = params.definitions?.SandboxPolicy?.oneOf?.find(policy => policy.properties?.type?.enum?.includes("readOnly"));
23
+ if (readOnly?.properties?.networkAccess?.type !== "boolean" || !["exitCode", "stdout", "stderr"].every(key => response?.properties?.[key])) throw failure("Native helper sandbox/output schema is unverified.", "NATIVE_COMMAND_UNVERIFIED");
24
+ }
25
+
26
+ export function validateStandaloneCreationSchemas(schemas) {
27
+ const required = { ThreadStartParams: ["cwd", "projectId", "runtimeWorkspaceRoots", "model", "config", "ephemeral"],
28
+ ThreadStartResponse: ["thread"], TurnStartParams: ["threadId", "input", "clientUserMessageId", "effort"],
29
+ TurnStartResponse: ["turn"], ThreadSetNameParams: ["threadId", "name"], ThreadMetadataUpdateParams: ["threadId", "projectId"] };
30
+ for (const [name, keys] of Object.entries(required)) {
31
+ if (!keys.every(key => schemas[name]?.properties?.[key])) throw failure(`Missing standalone creation schema: ${name}.`, "NATIVE_COMMAND_UNVERIFIED");
32
+ }
33
+ }
34
+
35
+ export async function inspectNativeCommandProtocol(state, { run = spawnSync, executable = BUNDLED_CLI, standalone = false } = {}) {
36
+ const version = run(executable, ["--version"], { encoding: "utf8", timeout: 5000 });
37
+ const installed = version.stdout?.match(/codex-cli ([\w.+-]+)/)?.[1];
38
+ if (version.status !== 0 || !installed || installed !== state.cli_version) throw failure("Native helper requires the installed bundled schema to match the running server.", "NATIVE_COMMAND_UNVERIFIED");
39
+ const directory = await mkdtemp(path.join(os.tmpdir(), "codexteer-native-schema-"));
40
+ try {
41
+ const generated = run(executable, ["app-server", "generate-json-schema", "--experimental", "--out", directory], { encoding: "utf8", timeout: 15000, maxBuffer: LIMIT });
42
+ if (generated.status !== 0) throw failure("Cannot inspect native helper command schema.", "NATIVE_COMMAND_UNVERIFIED");
43
+ const schemas = await Promise.all(["CommandExecParams", "CommandExecResponse"].map(async name => JSON.parse(await readFile(path.join(directory, "v2", `${name}.json`), "utf8"))));
44
+ validateNativeCommandSchema(...schemas);
45
+ if (standalone) {
46
+ const names = ["ThreadStartParams", "ThreadStartResponse", "TurnStartParams", "TurnStartResponse", "ThreadSetNameParams", "ThreadMetadataUpdateParams"];
47
+ const creation = Object.fromEntries(await Promise.all(names.map(async name => [name, JSON.parse(await readFile(path.join(directory, "v2", `${name}.json`), "utf8"))])));
48
+ validateStandaloneCreationSchemas(creation); schemas.push(creation);
49
+ }
50
+ return { status: "verified", source: "matching-bundled-command-schema", cli_version: installed, schema_sha256: createHash("sha256").update(JSON.stringify(schemas)).digest("hex"), ...(standalone ? { standalone_creation: "verified" } : {}) };
51
+ } catch (error) { if (error.code === "NATIVE_COMMAND_UNVERIFIED") throw error; throw failure("Cannot read native helper command schemas.", "NATIVE_COMMAND_UNVERIFIED"); }
52
+ finally { await rm(directory, { recursive: true, force: true }); }
53
+ }
54
+
55
+ // command/exec is a deterministic App Server API, explicitly independent of
56
+ // threads/turns. It launches only our fixed signed-Node worker, never user code
57
+ // or a shell. The read-only sandbox allows the local native Unix socket; the
58
+ // worker has no HTTP/DNS behavior. No model is invoked.
59
+ export async function nativeOnAppServer(client, endpoint, action, args, context, { timeoutMs = 120000, callId } = {}) {
60
+ const mutation = ["create_thread", "move_thread_to_project"].includes(action);
61
+ const request = nativeRequest(action, args, context, callId);
62
+ const directory = await mkdtemp(path.join(os.tmpdir(), "codexteer-native-request-"));
63
+ let result, error, dispatched = false;
64
+ try {
65
+ const input = JSON.stringify({ endpoint: { socketPath: endpoint.socketPath, fingerprint: endpoint.fingerprint }, action, args, context, timeoutMs, callId: request.params?.callId });
66
+ if (Buffer.byteLength(input) > LIMIT) throw failure("Native helper request exceeds its limit.", "NATIVE_PROTOCOL_ERROR");
67
+ const file = path.join(directory, "request.json");
68
+ await writeFile(file, input, { mode: 0o600, flag: "wx" });
69
+ dispatched = true;
70
+ const output = await client.request("command/exec", {
71
+ command: [BUNDLED_NODE, fileURLToPath(new URL("./native-worker.mjs", import.meta.url)), file],
72
+ cwd: fileURLToPath(new URL("..", import.meta.url)), timeoutMs: timeoutMs + 1000, outputBytesCap: LIMIT * 2,
73
+ sandboxPolicy: { type: "readOnly", networkAccess: true },
74
+ }, { mutation });
75
+ let response;
76
+ try { response = JSON.parse(output.stdout); } catch { throw failure("Native helper returned no valid acknowledgement. Inspect the receipt before retrying.", "NATIVE_PROTOCOL_ERROR", mutation); }
77
+ if (response.ok === true && output.exitCode === 0 && Object.hasOwn(response, "result")) { result = response.result; return result; }
78
+ if (response.ok === false && response.error) {
79
+ const error = failure(response.error.message, response.error.code, !!response.error.uncertain);
80
+ if (Number.isSafeInteger(response.error.rpcCode)) error.rpcCode = response.error.rpcCode;
81
+ throw error;
82
+ }
83
+ throw failure("Native helper did not confirm success.", "NATIVE_PROTOCOL_ERROR", mutation);
84
+ } catch (caught) {
85
+ error = caught;
86
+ if (error.uncertain === undefined || Number.isSafeInteger(error.rpcCode)) error.uncertain = dispatched && mutation && mutationOutcomeUnknown(error);
87
+ throw error;
88
+ } finally {
89
+ try { await rm(directory, { recursive: true, force: true }); }
90
+ catch { if (result) result.native_request_cleanup_required = true; else if (error) error.native_request_cleanup_required = true; }
91
+ }
92
+ }
@@ -0,0 +1,103 @@
1
+ import net from "node:net";
2
+ import os from "node:os";
3
+ import { lstat } from "node:fs/promises";
4
+ import { randomUUID } from "node:crypto";
5
+ import { spawnSync } from "node:child_process";
6
+ import path from "node:path";
7
+ import { BUNDLED_NODE } from "./wrapper.mjs";
8
+ import { normalizeThreadId } from "./thread-id.mjs";
9
+
10
+ const LIMIT = 8 * 1024 * 1024;
11
+ const failure = (message, code = "NATIVE_PROTOCOL_ERROR", uncertain = false) => Object.assign(new Error(message), { code, uncertain });
12
+ const fingerprint = info => [info.dev, info.ino, info.ctimeMs];
13
+
14
+ export async function inspectNativeEndpoint(socketPath, expected) {
15
+ if (typeof socketPath !== "string" || !path.isAbsolute(socketPath) || socketPath.includes("\0")) throw failure("Desktop native endpoint is unavailable.", "NATIVE_UNAVAILABLE");
16
+ const info = await lstat(socketPath);
17
+ if (!info.isSocket() || info.uid !== process.getuid() || (info.mode & 0o077)) throw failure("Unsafe Desktop native endpoint.", "NATIVE_ENDPOINT_UNSAFE");
18
+ const identity = fingerprint(info);
19
+ if (expected && JSON.stringify(expected) !== JSON.stringify(identity)) throw failure("Desktop native endpoint changed. Inspect the creation receipt before retrying.", "NATIVE_ENDPOINT_CHANGED");
20
+ return { socketPath, fingerprint: identity };
21
+ }
22
+
23
+ export async function discoverNativeEndpoint(state, { run = spawnSync } = {}) {
24
+ if (state.node_path !== BUNDLED_NODE) throw failure("Native tools require Desktop's bundled wrapper Node.", "NATIVE_UNAVAILABLE");
25
+ let endpoint = state.native_tools_pipe;
26
+ if (!endpoint) {
27
+ // Compatibility with an already-running wrapper. Read only the selected
28
+ // owner-validated PID, extract one inherited endpoint, and never emit/store
29
+ // its complete environment. New wrappers advertise the endpoint directly.
30
+ const owner = run("/bin/ps", ["-p", String(state.pid), "-o", "uid=", "-o", "comm="], { encoding: "utf8", timeout: 3000 });
31
+ const match = owner.stdout?.trim().match(/^(\d+)\s+(.+)$/);
32
+ if (owner.status !== 0 || !match || Number(match[1]) !== process.getuid() || match[2] !== BUNDLED_NODE) throw failure("Cannot verify the selected Desktop wrapper.", "NATIVE_UNAVAILABLE");
33
+ const environment = run("/bin/ps", ["eww", "-p", String(state.pid), "-o", "command="], { encoding: "utf8", timeout: 3000, maxBuffer: 1024 * 1024 });
34
+ if (environment.status === 0) endpoint = environment.stdout?.match(/(?:^| )CODEX_APP_TOOLS_PIPE_PATH=([^ \n]*)/)?.[1];
35
+ if (!endpoint) throw failure("The selected wrapper did not inherit Desktop native tools. Finish tasks before restarting with codexteer desktop start.", "NATIVE_UNAVAILABLE");
36
+ }
37
+ return inspectNativeEndpoint(endpoint);
38
+ }
39
+
40
+ export function nativeRequest(action, args, context, callId = randomUUID()) {
41
+ if (action === "catalog") return { jsonrpc: "2.0", id: randomUUID(), method: "tools/list", params: { threadStartKind: "all" } };
42
+ if (!["list_projects", "create_thread", "move_thread_to_project"].includes(action)) throw failure("Unsupported native operation.");
43
+ const threadId = normalizeThreadId(context?.threadId);
44
+ if (typeof context.turnId !== "string" || !context.turnId.trim() || context.turnId.length > 150) throw failure("A stored caller turn is required.");
45
+ if (typeof callId !== "string" || !/^[a-zA-Z0-9-]{1,150}$/.test(callId)) throw failure("Invalid native call ID.");
46
+ if (action === "list_projects") args = {};
47
+ else if (action === "move_thread_to_project") {
48
+ // Standalone creation registers only the new task itself. Never use this
49
+ // transport to move another task or grant additional workspace access.
50
+ if (args?.threadId !== threadId || typeof args.projectId !== "string" || !args.projectId.trim()
51
+ || Object.keys(args).some(key => !["threadId", "projectId"].includes(key))) throw failure("Invalid native self-assignment arguments.");
52
+ }
53
+ else if (typeof args?.prompt !== "string" || !args.prompt.trim() || args.target?.type !== "project" || args.target.environment?.type !== "local"
54
+ || typeof args.target.projectId !== "string" || !args.target.projectId.trim()
55
+ || Object.keys(args).some(key => !["prompt", "title", "model", "thinking", "target"].includes(key))
56
+ || Object.keys(args.target).some(key => !["type", "projectId", "environment"].includes(key))
57
+ || Object.keys(args.target.environment).some(key => key !== "type")) throw failure("Invalid local native creation arguments.");
58
+ return { jsonrpc: "2.0", id: randomUUID(), method: "tools/call", params: { arguments: args, callerSource: "codex", callId, hostId: "local", namespace: "codex_app", threadId, turnId: context.turnId, tool: action } };
59
+ }
60
+
61
+ export async function nativeRpc(endpoint, request, { timeoutMs = 30000 } = {}) {
62
+ const mutation = ["create_thread", "move_thread_to_project"].includes(request.params?.tool);
63
+ await inspectNativeEndpoint(endpoint.socketPath, endpoint.fingerprint);
64
+ const body = Buffer.from(JSON.stringify(request));
65
+ if (body.length > LIMIT) throw failure("Native request exceeds the frame limit.");
66
+ const frame = Buffer.alloc(4 + body.length), little = os.endianness() === "LE";
67
+ little ? frame.writeUInt32LE(body.length) : frame.writeUInt32BE(body.length); body.copy(frame, 4);
68
+ return new Promise((resolve, reject) => {
69
+ let bytes = Buffer.alloc(0), settled = false, dispatched = false;
70
+ const socket = net.createConnection(endpoint.socketPath);
71
+ const finish = (error, result) => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); if (error) { error.uncertain ||= mutation && dispatched && ![-32601, -32602].includes(error.rpcCode); reject(error); } else resolve(result); };
72
+ const timer = setTimeout(() => finish(failure("Native request timed out. Do not retry creation automatically.", "NATIVE_TIMEOUT")), timeoutMs);
73
+ socket.on("connect", () => { dispatched = true; socket.write(frame); });
74
+ socket.on("error", () => finish(failure("Desktop native connection failed.", "NATIVE_CONNECTION_FAILED")));
75
+ socket.on("close", () => finish(failure("Desktop native connection closed before acknowledgement.", "NATIVE_CONNECTION_CLOSED")));
76
+ socket.on("data", chunk => {
77
+ bytes = Buffer.concat([bytes, chunk]);
78
+ if (bytes.length > LIMIT + 4) return finish(failure("Native response exceeds the frame limit."));
79
+ if (bytes.length < 4) return;
80
+ const length = little ? bytes.readUInt32LE(0) : bytes.readUInt32BE(0);
81
+ if (!length || length > LIMIT) return finish(failure("Invalid native frame length."));
82
+ if (bytes.length < length + 4) return;
83
+ try {
84
+ const response = JSON.parse(bytes.subarray(4, length + 4));
85
+ if (response.id !== request.id || response.jsonrpc !== "2.0" || bytes.length !== length + 4) throw failure("Invalid native response identity/frame.");
86
+ if (response.error) {
87
+ const error = failure("Desktop rejected the native request. Inspect the receipt before retrying.", "NATIVE_REJECTED");
88
+ if (Number.isSafeInteger(response.error.code)) error.rpcCode = response.error.code;
89
+ return finish(error);
90
+ }
91
+ if (!Object.hasOwn(response, "result")) throw failure("Native response has no result.");
92
+ finish(null, response.result);
93
+ } catch (error) { finish(error.code ? error : failure("Invalid native response JSON.")); }
94
+ });
95
+ });
96
+ }
97
+
98
+ export function nativeToolJson(result) {
99
+ if (result?.success !== true || !Array.isArray(result.contentItems)) throw failure("Desktop native tool did not confirm success. Inspect the receipt before retrying.", "NATIVE_TOOL_FAILED", true);
100
+ const texts = result.contentItems.filter(item => item.type === "inputText");
101
+ if (texts.length !== 1 || typeof texts[0].text !== "string") throw failure("Invalid native tool receipt.", "NATIVE_PROTOCOL_ERROR", true);
102
+ try { return JSON.parse(texts[0].text); } catch { throw failure("Invalid native tool receipt JSON.", "NATIVE_PROTOCOL_ERROR", true); }
103
+ }
@@ -0,0 +1,25 @@
1
+ import { nativeRequest, nativeRpc } from "./native-rpc.mjs";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+
4
+ // Spawned by the existing signed App Server through command/exec, without a
5
+ // thread or turn. Desktop authenticates this signed child using its unchanged
6
+ // peer policy. The helper opens only the owner-validated local native socket.
7
+ process.stdin.setEncoding("utf8");
8
+ let body = "";
9
+ try {
10
+ if (process.argv[2]) {
11
+ const info = await lstat(process.argv[2]);
12
+ if (!info.isFile() || info.uid !== process.getuid() || (info.mode & 0o077) || info.size > 8 * 1024 * 1024) throw new Error("Unsafe native worker request file.");
13
+ body = await readFile(process.argv[2], "utf8");
14
+ } else for await (const chunk of process.stdin) {
15
+ body += chunk;
16
+ if (Buffer.byteLength(body) > 8 * 1024 * 1024) throw new Error("Native worker request exceeds its limit.");
17
+ }
18
+ const { endpoint, action, args, context, timeoutMs, callId } = JSON.parse(body);
19
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 600000) throw new Error("Invalid native timeout.");
20
+ const result = await nativeRpc(endpoint, nativeRequest(action, args, context, callId), { timeoutMs });
21
+ process.stdout.write(JSON.stringify({ ok: true, result }));
22
+ } catch (error) {
23
+ process.stdout.write(JSON.stringify({ ok: false, error: { message: error.message, code: error.code ?? "NATIVE_PROTOCOL_ERROR", uncertain: !!error.uncertain, rpcCode: Number.isSafeInteger(error.rpcCode) ? error.rpcCode : undefined } }));
24
+ process.exitCode = 1;
25
+ }
@@ -0,0 +1,79 @@
1
+ import { readFile, realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { discoverRuntime } from "./runtime.mjs";
4
+ import { RpcClient } from "./rpc.mjs";
5
+ import { assertRuntimeOperation, compatibleClient } from "./compatibility.mjs";
6
+
7
+ const failure = (message, code = "PROJECT_PROTOCOL_ERROR") => Object.assign(new Error(message), { code });
8
+
9
+ export function validateProject(project) {
10
+ if (!project || typeof project.id !== "string" || !project.id || typeof project.name !== "string"
11
+ || !Array.isArray(project.roots) || !project.roots.length
12
+ || project.roots.some(root => typeof root?.path !== "string" || !path.isAbsolute(root.path) || root.path.includes("\0"))) {
13
+ throw failure("App Server returned an invalid local project.");
14
+ }
15
+ return project;
16
+ }
17
+
18
+ export async function projectsOnClient(client) {
19
+ const projects = [], cursors = new Set(), ids = new Set(); let cursor;
20
+ for (let page = 0; page < 100; page++) {
21
+ const result = await client.request("project/list", { limit: 100, ...(cursor ? { cursor } : {}) });
22
+ if (!Array.isArray(result?.data)) throw failure("Invalid App Server project list.");
23
+ for (const project of result.data) {
24
+ validateProject(project);
25
+ if (ids.has(project.id)) throw failure("Duplicate project ID in App Server project list.");
26
+ ids.add(project.id); projects.push(project);
27
+ }
28
+ cursor = result.nextCursor;
29
+ if (cursor == null) return projects;
30
+ if (typeof cursor !== "string" || !cursor || cursors.has(cursor)) throw failure("Invalid project pagination cursor.");
31
+ cursors.add(cursor);
32
+ }
33
+ throw failure("Project list exceeded the pagination limit.");
34
+ }
35
+
36
+ // Desktop keeps its legacy public IDs while App Server owns canonical IDs.
37
+ // Read this mapping only; never edit Desktop's state or infer membership by cwd.
38
+ export async function projectAliases(home) {
39
+ try {
40
+ const state = JSON.parse(await readFile(path.join(home, ".codex-global-state.json"), "utf8"));
41
+ const mapping = state["app-server-project-id-by-legacy-project-id-by-host"]?.[`local:${home}`] ?? {};
42
+ return Object.fromEntries(Object.entries(mapping).filter(([key, value]) => typeof key === "string" && typeof value === "string"));
43
+ } catch (error) { if (error.code === "ENOENT") return {}; throw error; }
44
+ }
45
+
46
+ export function resolveProject(projects, selector, aliases = {}) {
47
+ if (typeof selector !== "string" || !selector.trim()) throw failure("--project requires a project ID or exact name.", "PROJECT_REQUIRED");
48
+ const id = aliases[selector] ?? selector;
49
+ const exact = projects.find(project => project.id === id);
50
+ if (exact) return exact;
51
+ const matches = projects.filter(project => project.name === selector);
52
+ if (matches.length !== 1) throw failure(matches.length ? "Multiple projects have this name. Select the ID from projects list." : "Local project not found. Select the ID or exact name from projects list.", matches.length ? "PROJECT_AMBIGUOUS" : "PROJECT_NOT_FOUND");
53
+ return matches[0];
54
+ }
55
+
56
+ export async function projectWorkspace(project, requestedCwd) {
57
+ validateProject(project);
58
+ const roots = [...new Set(await Promise.all(project.roots.map(async root => {
59
+ const resolved = await realpath(root.path);
60
+ if (!(await stat(resolved)).isDirectory()) throw failure("Project root is not a directory.", "PROJECT_ROOT_UNAVAILABLE");
61
+ return resolved;
62
+ })))];
63
+ const cwd = requestedCwd === undefined ? roots[0] : await realpath(path.resolve(requestedCwd));
64
+ if (!(await stat(cwd)).isDirectory() || !roots.some(root => {
65
+ const relative = path.relative(root, cwd);
66
+ return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
67
+ })) throw failure("--cwd must be a directory inside the selected project's source folders.", "PROJECT_CWD_OUTSIDE_ROOTS");
68
+ return { cwd, runtimeWorkspaceRoots: roots };
69
+ }
70
+
71
+ export async function listProjects({ discover = discoverRuntime, connect = RpcClient.connect, aliases = projectAliases } = {}) {
72
+ const { paths, state } = await discover();
73
+ assertRuntimeOperation(state, "read");
74
+ const client = compatibleClient(await connect(paths.socket), state);
75
+ try {
76
+ const projects = await projectsOnClient(client), mapping = await aliases(paths.home);
77
+ return projects.map(project => ({ project_id: project.id, desktop_project_ids: Object.keys(mapping).filter(id => mapping[id] === project.id), name: project.name, roots: project.roots.map(root => root.path) }));
78
+ } finally { client.close(); }
79
+ }
package/src/prompt.mjs CHANGED
@@ -2,8 +2,8 @@ import { normalizeThreadId } from "./thread-id.mjs";
2
2
 
3
3
  export const DEFAULT_SUPERVISION_POLICY = [
4
4
  "ユーザーの最新の目的・制約の範囲内で、観測に基づく必要最小限の軌道修正と、その結果の確認を委任します。委任された範囲では送信内容とタイミングを判断し、介入のたびに確認を求めません。",
5
- "次の3点を観測してください。(1) 過剰な設計: 今の要件に不要な抽象化・汎用化、依頼外へのスコープ拡大、より小さな変更で達成できる余地。(2) コードの責務: 変更が既存のモジュールや層の役割分担を越えていないか、同じ判定や状態の持ち主を増やしていないか。(3) 作業の焦点: 依頼の達成に効かない作業へ時間を使っていないか、同じ原因の失敗を新しい根拠なく繰り返していないか。",
6
- "変更量や一般論だけで断定せず、必要性が示されているかを、そのリポジトリの既存の構造と文書に照らして判断します。責務の指摘は今回の変更が触れた範囲に限り、どの責務をどこが持ち、どこが越えたかをファイルと箇所で示せる場合だけ送ります。以前からある問題は介入せず、ユーザーへの報告にとどめます。",
5
+ "次の3点を観測してください。(1) 設計上の責務: 事実・計算・判定のそれぞれを、設計がどの層やシステム(サーバー、公開API、アプリなど)の担当と定めているかを先に確認し、変更がその分担を崩していないかを見ます。特に、ある層が正として持つ計算や判定を別の層でも実装していないか(検算、再計算、空のデータからの推測、派生値の複製保存、念のための二重チェック)、同じ事実の持ち主が二つになっていないかを見ます。モジュール間の役割分担も同じ基準で見ます。(2) 過剰な設計: 今の要件に不要な抽象化・汎用化、依頼外へのスコープ拡大、より小さな変更で達成できる余地。(3) 作業の焦点: 依頼の達成に効かない作業へ時間を使っていないか、同じ原因の失敗を新しい根拠なく繰り返していないか。",
6
+ "変更量や一般論だけで断定せず、そのプロジェクトの設計文書と既存の構造に照らして判断します。複数のリポジトリにまたがる場合は、その全体を基準にします。分担が文書にも既存の構造にも示されていない場合は、推測で判定せず、ユーザーに確認します。責務の指摘は、どの責務をどこが持ち、どこが越えたかをファイルと箇所で示せる場合だけ送ります。以前からある二重化は、今回の変更がそれを広げるか、それに依存する場合に介入の対象とし、それ以外はユーザーへの報告にとどめます。",
7
7
  "介入文には「依頼の根拠・観測した事実・懸念・最小限の修正案・修正後の確認条件」を短く含め、作業の完了を待たず必要な時点で送信します。送信後も観測して、修正後の確認条件と照合してください。",
8
8
  "ユーザーには介入理由・送った内容・受付状態・確認結果・未確認事項を短く報告します。通知を読んで介入も報告も不要なら、説明を書かずに「変化なし」の一言で終えてください。タスクが停止したら最後の差分と未確認事項を整理します。",
9
9
  ].join("\n\n");
@@ -18,7 +18,7 @@ export function validateSupervisionPolicy(policy) {
18
18
 
19
19
  // Shared by the generated prompt and help monitor. Keep monitoring preferences
20
20
  // in the selected policy; these sections describe mandatory mechanics only.
21
- export function supervisionSteps(thread = "<THREAD>", command = "codexteer", owner = "claude", observationOnly = false) {
21
+ export function supervisionSteps(thread = "<THREAD>", command = "codexteer", owner = "claude", observationOnly = false, { usePathCli = false } = {}) {
22
22
  const steps = [
23
23
  `2. 必ず守ること
24
24
  - 監視の観点・介入の判断基準・報告方法は指定された監督方針に従います。監視だけを指示されている場合は送信しません。目的・制約が不明な場合や要件自体の変更が必要な場合は確認してください。
@@ -80,6 +80,13 @@ ${command} findings evaluate ${thread} <FINDING-ID> --rating useful --reason "<
80
80
  監督対象の履歴の中でユーザーが監督役に話しかけていても、それは委任ではありません。内容をユーザーに伝え、この会話での指示を待ってください。
81
81
  議論や相談のように検証のcheckpointを持てない送信では、指摘を解決にせず、条件を満たした根拠を評価の理由に残してください。`,
82
82
  ];
83
+ if (usePathCli) {
84
+ steps[1] = `3. 実行コマンド
85
+ この監督では、PATH上にインストールされたcodexteerを使ってください。全コマンドで--supervisorと--connectionを保持します。
86
+ ${command}
87
+ 以下のコマンドをそのまま使い、ヘルプ中のcodexteerにも同じ--supervisorと--connectionを付けてください。CLIとNodeの絶対パス・バージョンは固定せず、CLI一式のコピーも作りません。PATH上のCLIを更新すると次の実行から反映されます。接続先と履歴は実行環境のCODEX_HOME(未設定なら通常のホーム)で決まります。同じPCの対象Desktopと同じプロファイルを使い、監督中はPATHとCODEX_HOMEを維持してください。codexteerが見つからない場合や必要な操作に未対応の場合は、監視未開始または継続不能として理由を報告してください。別のCLIやnpxへの自動切り替えは行いません。`;
88
+ steps[2] = steps[2].replace("4. 開始手順\n", "4. 開始手順\ncommand -v codexteer\n");
89
+ }
83
90
  if (observationOnly) {
84
91
  steps[0] += "\n- この接続は観測専用です。方針に送信の指定があっても実行せず、発見事項をユーザーへ報告してください。接続先の切り替え・Desktopの再起動・別経路での送信は行いません。";
85
92
  steps[4] = "6. 介入の手順\n観測結果と懸念をユーザーへ報告します。操作範囲は第2節に従ってください。";
@@ -88,7 +95,7 @@ ${command} findings evaluate ${thread} <FINDING-ID> --rating useful --reason "<
88
95
  return steps;
89
96
  }
90
97
 
91
- export function supervisorPrompt(threadInput, command, policy, supervisor = { owner: "claude" }) {
98
+ export function supervisorPrompt(threadInput, command, policy, supervisor = { owner: "claude" }, { usePathCli = false } = {}) {
92
99
  const threadId = normalizeThreadId(threadInput);
93
100
  validateSupervisionPolicy(policy);
94
101
  if (typeof command !== "string" || !command) throw new Error("A prepared supervision command is required.");
@@ -98,7 +105,7 @@ export function supervisorPrompt(threadInput, command, policy, supervisor = { ow
98
105
  "1. 役割と監督方針\nあなたはCodex Desktopタスクの監督役(オーケストレーター)です。codexteerで次のタスクを継続監督してください。",
99
106
  `対象タスク: ${threadId}`,
100
107
  policy ?? (supervisor.connection === "desktop" ? "最新のユーザーの目的・制約と進捗を観測し、問題・確認できた結果・未確認事項を短く報告してください。変化のない定期報告は控え、タスク停止時に最後の差分を整理してください。" : DEFAULT_SUPERVISION_POLICY),
101
- ...supervisionSteps(threadId, command, supervisor.owner, supervisor.connection === "desktop"),
108
+ ...supervisionSteps(threadId, command, supervisor.owner, supervisor.connection === "desktop", { usePathCli }),
102
109
  ].join("\n\n"),
103
110
  };
104
111
  }
package/src/runtime.mjs CHANGED
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { lstat, mkdir, readFile, rename, rm, writeFile, realpath } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { inspectAppServerSocket } from "./socket-endpoint.mjs";
5
6
 
6
7
  const runtimeFailure = (message, code) => Object.assign(new Error(message), { code });
7
8
 
@@ -75,9 +76,10 @@ export async function discoverRuntime(home) {
75
76
  if (!isAlive(state.pid) || !isAlive(state.server_pid) || !state.desktop_connected) {
76
77
  throw runtimeFailure("Shared App Server is not ready. Finish current tasks, quit Desktop, then run: codexteer desktop start", "RUNTIME_NOT_READY");
77
78
  }
78
- await checkOwned(paths.socket, "socket");
79
- return { paths, state };
79
+ const endpoint = await inspectAppServerSocket(paths.socket);
80
+ return { paths: { ...paths, socket: endpoint.socket }, state };
80
81
  } catch (error) {
82
+ if (error.code === "SOCKET_UNSAFE") throw runtimeFailure(error.message, "RUNTIME_UNSAFE");
81
83
  if (error.code === "ENOENT") throw runtimeFailure("Shared App Server is unavailable. Finish current tasks, quit Desktop, then run: codexteer desktop start", "RUNTIME_UNAVAILABLE");
82
84
  if (["EACCES", "EPERM"].includes(error.code)) throw runtimeFailure("Cannot inspect the shared App Server runtime. Check access permissions.", "PERMISSION_DENIED");
83
85
  throw error;