diff --git a/.gitignore b/.gitignore index 8a702ff..560d16a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ src-tauri/gen/ # Machine-local cargo override (see src-tauri/.cargo/config.toml.example) src-tauri/.cargo/config.toml # Seeded design canvas — 2.2MB of editor payload, regenerated from design/*.dc.html -design/liplus-chat-ui.html +design/pullcept-ui.html diff --git a/NOTICE.txt b/NOTICE.txt index 71e18ef..b9c95e2 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,2 +1,2 @@ -liplus-chat +Pullcept Copyright 2026 Yoshiharu Uematsu diff --git a/README.md b/README.md index 9b57ec9..ec54fad 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# liplus-chat +# Pullcept > [!IMPORTANT] > 部屋の一往復(人間の発言 → channel → セッションの返信 → 部屋への表示)は 2026-08-21 に実機で確認しました。配布は開発者環境向けの第一段階のみで、起動時に警告バナーが出ます。 ## 概要 -liplus-chat は、人間と複数の独立した AI / Li+ セッションが、一つのローカルな会話面を共有するためのデスクトップアプリを目指しています。 +Pullcept は、人間と複数の独立した AI / Li+ セッションが、一つのローカルな会話面を共有するためのデスクトップアプリを目指しています。 各 AI セッションの文脈と人格は独立したまま保ち、発言者を識別できる共通の場で対話する構想です。特定の AI に他のセッションの内部文脈を統合するのではなく、それぞれが自分の文脈から同じ会話へ参加する形を取ります。 @@ -58,7 +58,7 @@ npm run tauri dev 「名前」と「色」は、そのアカウントが部屋で名乗る名前と、発言に付く色です。**いつでも変えられます。** 同一性は名前ではなく内部の id にあるため、改名しても稼働中のセッションの登録は追随します。色を選ばずに参加することもでき、その場合は名前から色が決まります。タイトルバーの「名前」「色」は同じものの人間側です(人間はアカウントではなく、この画面の話者として参加します)。 -「起動オプション」には `--dangerously-skip-permissions` のように、CLI へ渡したいオプションをそのまま書けます。アプリは部屋の channel エントリ(`server:liplus-chat-room--`)をここへ統合するので、別の channel サーバを指定しても部屋の入力路は残ります。実際に起動する行は入力欄の右に表示されます。 +「起動オプション」には `--dangerously-skip-permissions` のように、CLI へ渡したいオプションをそのまま書けます。アプリは部屋の channel エントリ(`server:pullcept-room--`)をここへ統合するので、別の channel サーバを指定しても部屋の入力路は残ります。実際に起動する行は入力欄の右に表示されます。 作業ディレクトリの入力欄があります。初回はホームディレクトリが入っているので、セッションを動かしたいディレクトリへ変更してください。 @@ -70,8 +70,8 @@ npm run tauri dev アカウントを選び「セッション参加」を押すと、次の 2 つが行われます。 -1. そのアカウントの作業ディレクトリの `.mcp.json` へ、部屋のサイドカーを `liplus-chat-room--` という名前で登録します。**既存の内容はマージして保持します**が、あなたのリポジトリのファイルを書き換える操作です。登録鍵はアカウント id から決まります。アカウントごとに分けているのは同じ作業ディレクトリへ 2 つのセッションを立てたときに互いの登録を潰さないためで、名前ではなく id から導くのは改名で鍵が動かないようにするためです。前回の起動で書かれたエントリ(もう繋がらないポートを指しているもの)は、このとき掃除します。 -2. `--dangerously-load-development-channels server:liplus-chat-room--` を付けて CLI を PTY 上の対話セッションとして起動します。 +1. そのアカウントの作業ディレクトリの `.mcp.json` へ、部屋のサイドカーを `pullcept-room--` という名前で登録します。**既存の内容はマージして保持します**が、あなたのリポジトリのファイルを書き換える操作です。登録鍵はアカウント id から決まります。アカウントごとに分けているのは同じ作業ディレクトリへ 2 つのセッションを立てたときに互いの登録を潰さないためで、名前ではなく id から導くのは改名で鍵が動かないようにするためです。前回の起動で書かれたエントリ(もう繋がらないポートを指しているもの)は、このとき掃除します。 +2. `--dangerously-load-development-channels server:pullcept-room--` を付けて CLI を PTY 上の対話セッションとして起動します。 同じアカウントを二重に起動することはできません。一つのアカウントが持てる席は、一つの部屋につき一つです。 @@ -156,6 +156,6 @@ portable-pty-patch/ Windows 対応を含む portable-pty のローカルパッ ## ライセンス -liplus-chat は [Apache License 2.0](LICENSE) のもとで提供されます。著作権表示は [NOTICE.txt](NOTICE.txt) を参照してください。 +Pullcept は [Apache License 2.0](LICENSE) のもとで提供されます。著作権表示は [NOTICE.txt](NOTICE.txt) を参照してください。 `portable-pty-patch/` は MIT License のコードを含みます。詳細は [`portable-pty-patch/LICENSE.md`](portable-pty-patch/LICENSE.md) を参照してください。 diff --git a/crates/mcp-config/Cargo.toml b/crates/mcp-config/Cargo.toml index 2684c8d..0749cec 100644 --- a/crates/mcp-config/Cargo.toml +++ b/crates/mcp-config/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "mcp-config" version = "0.1.0" -description = "Registration of the liplus-chat room sidecar in a project's .mcp.json" +description = "Registration of the Pullcept room sidecar in a project's .mcp.json" authors = ["Liplus Project Contributors"] license = "Apache-2.0" edition = "2021" # Deliberately free of tauri. This crate writes into the user's own project -# directory, which is the part of liplus-chat that most needs test coverage, +# directory, which is the part of Pullcept that most needs test coverage, # and a test binary that links the tauri dependency tree does not load on the # GNU target (see the CI note in docs/0-requirements.md). diff --git a/crates/mcp-config/src/lib.rs b/crates/mcp-config/src/lib.rs index e67f921..a2e33f8 100644 --- a/crates/mcp-config/src/lib.rs +++ b/crates/mcp-config/src/lib.rs @@ -11,7 +11,7 @@ //! registers the same server twice and takes the whole room down. //! //! This crate holds no tauri: it writes into the user's own project directory, -//! which is the part of liplus-chat that most needs test coverage, and a test +//! which is the part of Pullcept that most needs test coverage, and a test //! binary linking the tauri tree does not load on the GNU target. use serde_json::{json, Map, Value}; @@ -24,7 +24,7 @@ use std::path::{Path, PathBuf}; /// a single key means the second launch overwrites the first one's name, hue /// and room address — the identity the first session was launched with is gone /// while that session is still running (#40). -pub const SERVER_PREFIX: &str = "liplus-chat-room"; +pub const SERVER_PREFIX: &str = "pullcept-room"; /// The `.mcp.json` key, and the `server:` tag, for one account. /// @@ -41,7 +41,7 @@ pub const SERVER_PREFIX: &str = "liplus-chat-room"; /// The slug half is legibility and the hash half is what makes the key total, /// as it was under the name. An id is opaque, so the slug reads less well than /// a name did; which account an entry belongs to is read from -/// `LIPLUS_AGENT_NAME` in its own env instead. Legibility loses to identity +/// `PULLCEPT_AGENT_NAME` in its own env instead. Legibility loses to identity /// here — a key that reads oddly costs one lookup, and a key that moves is a /// registration nobody can find. pub fn server_name_for(account_id: &str) -> String { @@ -204,7 +204,7 @@ pub fn split_launch_options(text: &str) -> Vec { /// /// Absolute paths and nothing to resolve. The CLI spawns this from the user's /// own project directory, so anything looked up by name is looked up there: -/// `npx tsx` searched for a package that lives in liplus-chat and asked to +/// `npx tsx` searched for a package that lives in Pullcept and asked to /// install it, from a process with no way to answer (#22). `node` is an /// executable rather than a shell script, so no shell wrapper is needed either. fn spawn_form(runner: &Path, entry: &Path) -> (&'static str, Vec) { @@ -261,26 +261,26 @@ pub fn register_sidecar(dir: &Path, room: &RoomRegistration<'_>) -> Result url == room.room_url, _ => true, } }); let mut env = Map::new(); - env.insert("LIPLUS_ROOM_URL".into(), json!(room.room_url)); - env.insert("LIPLUS_ROOM_TOKEN".into(), json!(room.token)); - env.insert("LIPLUS_AGENT_NAME".into(), json!(room.agent_name)); - env.insert("LIPLUS_ROOM_ID".into(), json!("liplus-chat")); + env.insert("PULLCEPT_ROOM_URL".into(), json!(room.room_url)); + env.insert("PULLCEPT_ROOM_TOKEN".into(), json!(room.token)); + env.insert("PULLCEPT_AGENT_NAME".into(), json!(room.agent_name)); + env.insert("PULLCEPT_ROOM_ID".into(), json!("pullcept")); // Only when declared. An undeclared participant is a participant the room // derives a hue for, which is not the same state as one who chose that hue. if let Some(hue) = room.agent_hue { - env.insert("LIPLUS_AGENT_HUE".into(), json!(format!("{hue:.1}"))); + env.insert("PULLCEPT_AGENT_HUE".into(), json!(format!("{hue:.1}"))); } servers.insert( @@ -308,7 +308,7 @@ mod tests { impl Scratch { fn new() -> Self { - let dir = std::env::temp_dir().join(format!("liplus-chat-test-{}", Uuid::new_v4())); + let dir = std::env::temp_dir().join(format!("pullcept-test-{}", Uuid::new_v4())); std::fs::create_dir_all(&dir).expect("scratch dir"); Scratch(dir) } @@ -323,8 +323,8 @@ mod tests { } } - const ENTRY: &str = "C:/liplus-chat/sidecar/src/index.ts"; - const RUNNER: &str = "C:/liplus-chat/node_modules/tsx/dist/cli.mjs"; + const ENTRY: &str = "C:/pullcept/sidecar/src/index.ts"; + const RUNNER: &str = "C:/pullcept/node_modules/tsx/dist/cli.mjs"; /// The account `Lin` is launched from. Opaque here as it is in the app: a /// test that read a name out of it would be testing the wrong key. const LIN: &str = "8f14e45f-ceea-467a-b160-6f14e45fceea"; @@ -356,17 +356,17 @@ mod tests { let json = read(&path); let server = &json["mcpServers"][server_name_for(LIN)]; - assert_eq!(server["env"]["LIPLUS_ROOM_URL"], "ws://127.0.0.1:1234"); - assert_eq!(server["env"]["LIPLUS_ROOM_TOKEN"], "tok"); - assert_eq!(server["env"]["LIPLUS_AGENT_NAME"], "Lin"); - assert_eq!(server["env"]["LIPLUS_ROOM_ID"], "liplus-chat"); + assert_eq!(server["env"]["PULLCEPT_ROOM_URL"], "ws://127.0.0.1:1234"); + assert_eq!(server["env"]["PULLCEPT_ROOM_TOKEN"], "tok"); + assert_eq!(server["env"]["PULLCEPT_AGENT_NAME"], "Lin"); + assert_eq!(server["env"]["PULLCEPT_ROOM_ID"], "pullcept"); // Undeclared is the key absent, not a default value: a hue written here // would be a declaration this participant never made. assert!( !server["env"] .as_object() .expect("env") - .contains_key("LIPLUS_AGENT_HUE"), + .contains_key("PULLCEPT_AGENT_HUE"), "an undeclared hue must leave no key behind" ); @@ -425,8 +425,8 @@ mod tests { let json = read(&path); let server = &json["mcpServers"][server_name_for(LIN)]; - assert_eq!(server["env"]["LIPLUS_ROOM_TOKEN"], "tok2"); - assert_eq!(server["env"]["LIPLUS_AGENT_HUE"], "145.0"); + assert_eq!(server["env"]["PULLCEPT_ROOM_TOKEN"], "tok2"); + assert_eq!(server["env"]["PULLCEPT_AGENT_HUE"], "145.0"); assert_eq!( json["mcpServers"].as_object().expect("servers").len(), 1, @@ -461,7 +461,7 @@ mod tests { let servers = json["mcpServers"].as_object().expect("servers"); assert_eq!(servers.len(), 1, "a rename must not open a second entry"); assert_eq!( - json["mcpServers"][server_name_for(LIN)]["env"]["LIPLUS_AGENT_NAME"], + json["mcpServers"][server_name_for(LIN)]["env"]["PULLCEPT_AGENT_NAME"], "リン", "the new name belongs in the entry the id already had" ); @@ -491,12 +491,12 @@ mod tests { let json = read(&path); assert_eq!( - json["mcpServers"][server_name_for(LIN)]["env"]["LIPLUS_AGENT_NAME"], + json["mcpServers"][server_name_for(LIN)]["env"]["PULLCEPT_AGENT_NAME"], "Lin", "the first session's identity must survive the second launch" ); assert_eq!( - json["mcpServers"][server_name_for(LAY)]["env"]["LIPLUS_AGENT_NAME"], + json["mcpServers"][server_name_for(LAY)]["env"]["PULLCEPT_AGENT_NAME"], "Lay" ); assert_eq!(json["mcpServers"].as_object().expect("servers").len(), 2); @@ -512,9 +512,9 @@ mod tests { std::fs::write( scratch.path().join(".mcp.json"), r#"{"mcpServers":{ - "liplus-chat-room": {"env":{"LIPLUS_ROOM_URL":"ws://127.0.0.1:1"}}, - "liplus-chat-room-lay-00000000": {"env":{"LIPLUS_ROOM_URL":"ws://127.0.0.1:1234"}}, - "liplus-chat-room-theirs": {"command":"not-ours"}, + "pullcept-room": {"env":{"PULLCEPT_ROOM_URL":"ws://127.0.0.1:1"}}, + "pullcept-room-lay-00000000": {"env":{"PULLCEPT_ROOM_URL":"ws://127.0.0.1:1234"}}, + "pullcept-room-theirs": {"command":"not-ours"}, "theirs": {"command":"their-server"} }}"#, ) @@ -528,15 +528,15 @@ mod tests { let json = read(&path); let servers = json["mcpServers"].as_object().expect("servers"); assert!( - !servers.contains_key("liplus-chat-room"), + !servers.contains_key("pullcept-room"), "an entry from a previous run must go" ); assert!( - servers.contains_key("liplus-chat-room-lay-00000000"), + servers.contains_key("pullcept-room-lay-00000000"), "a live sibling of this run must stay" ); assert!( - servers.contains_key("liplus-chat-room-theirs"), + servers.contains_key("pullcept-room-theirs"), "an entry with no room address of ours is not ours to remove" ); assert!(servers.contains_key("theirs")); @@ -552,7 +552,7 @@ mod tests { // input that slugs to nothing still gets a key of its own, and two that // slug alike still get two. The uniqueness lives in the hash, and the // ids the app mints do not lean on the slug for it. - assert!(server_name_for("マスター").starts_with("liplus-chat-room-")); + assert!(server_name_for("マスター").starts_with("pullcept-room-")); assert_ne!(server_name_for("マスター"), server_name_for("ますたー")); assert_ne!(server_name_for("Lin"), server_name_for("lin!")); // Nothing outside the set a console and a JSON key both leave alone. diff --git a/crates/room-floor/Cargo.toml b/crates/room-floor/Cargo.toml index ecc3584..b2f08fd 100644 --- a/crates/room-floor/Cargo.toml +++ b/crates/room-floor/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "room-floor" version = "0.1.0" -description = "The floor of the liplus-chat room: what has been said, and who had seen it before speaking" +description = "The floor of the Pullcept room: what has been said, and who had seen it before speaking" authors = ["Liplus Project Contributors"] license = "Apache-2.0" edition = "2021" diff --git a/design/Main.dc.html b/design/Main.dc.html index 530e722..b9b594d 100644 --- a/design/Main.dc.html +++ b/design/Main.dc.html @@ -19,7 +19,7 @@
- Li+ Chat + Pullcept Claude Code / Lin - claude --dangerously-skip-permissions --dangerously-load-development-channels server:liplus-chat-room server:github-webhook-mcp + claude --dangerously-skip-permissions --dangerously-load-development-channels server:pullcept-room server:github-webhook-mcp
diff --git a/design/README.md b/design/README.md index c1d57a6..410cdba 100644 --- a/design/README.md +++ b/design/README.md @@ -8,6 +8,6 @@ UI 設計キャンバスの元ファイル。公開先は `docs/0-requirements.m 色と寸法は `src/styles.css` の実値をそのまま使っている。実装側のトークンを変えたら、こちらも合わせること。 -公開版は Claude Code の `/design` スキルから再生成する。`liplus-chat-ui.html`(約 2.2MB、エディタ本体を含む)は生成物なので追跡しない。 +公開版は Claude Code の `/design` スキルから再生成する。`pullcept-ui.html`(約 2.2MB、エディタ本体を含む)は生成物なので追跡しない。 キャンバス側で編集された場合は、先に公開版を読み戻してからこちらを直す。手元の古い版で上書きしないため。 diff --git a/design/Vision.dc.html b/design/Vision.dc.html index 53a6410..9141dba 100644 --- a/design/Vision.dc.html +++ b/design/Vision.dc.html @@ -20,7 +20,7 @@
- Li+ Chat + Pullcept
diff --git a/docs/0-requirements.md b/docs/0-requirements.md index 49b92f8..f9da47f 100644 --- a/docs/0-requirements.md +++ b/docs/0-requirements.md @@ -1,4 +1,4 @@ -# liplus-chat 要求仕様 +# Pullcept 要求仕様 ## purpose @@ -40,7 +40,7 @@ - `claude/channel` を宣言するのは**サーバ側**であり、ホストはそれを消費する側。ホストが initialize で申告する capability はフラグの有無に関わらず変化しない。 - `--channels` / `--dangerously-load-development-channels` はいずれも `--help` 非掲載(`2.1.209`)。 -### liplus-chat で追加確認したこと(2026-08-21) +### Pullcept で追加確認したこと(2026-08-21) - **複数の channel サーバを同時に有効化できる。** 部屋のサイドカーと `github-webhook-mcp` の双方が同一セッションで機能した。#90 の premise は `--channels` 併記の失敗のみを確定させており、この点は扱っていなかった。 - 上の「単独指定」は `--channels` を併記しないという意味であって、`server:` エントリを一つに限る意味ではない。 @@ -73,7 +73,7 @@ ## architecture ``` -liplus-chat (Tauri app) = 部屋の壁 +Pullcept (Tauri app) = 部屋の壁 ├── UI Layer (WebView) │ ├── チャットルーム(メッセージ一覧 / 発言者表示 / 入力欄) │ └── アカウントとセッション制御(作成 / 編集 / 削除 / 起動) @@ -158,9 +158,9 @@ push の形(参照実装 `Liplus-Project/github-webhook-mcp` `local-mcp/src/in `.mcp.json` に書く起動コマンドは、絶対パスのみで構成し、名前による解決を含めない。 - command = `node` -- args = [`/node_modules/tsx/dist/cli.mjs`, ``] +- args = [`/node_modules/tsx/dist/cli.mjs`, ``] -サイドカーを spawn するのは CLI であり、その作業ディレクトリはユーザーのプロジェクトである。名前で引くものはそこで引かれる。`npx tsx` は liplus-chat 側にしか無い `tsx` をユーザーのディレクトリで探し、見つからずインストールの確認を出す——非対話で spawn されたプロセスにその確認へ答える経路は無い。 +サイドカーを spawn するのは CLI であり、その作業ディレクトリはユーザーのプロジェクトである。名前で引くものはそこで引かれる。`npx tsx` は Pullcept 側にしか無い `tsx` をユーザーのディレクトリで探し、見つからずインストールの確認を出す——非対話で spawn されたプロセスにその確認へ答える経路は無い。 ### 部屋の作法 @@ -168,7 +168,7 @@ push の形(参照実装 `Liplus-Project/github-webhook-mcp` `local-mcp/src/in 判定材料(`meta.to`)と判定規律(`instructions`)は同時に変える。片方だけ動かすと、宛先を知らないまま宛先を判断させる状態になる。 -- サイドカーは自分の部屋での名前(`LIPLUS_AGENT_NAME`)を `instructions` へ埋め込む。埋めなければエージェントは `meta.to` を自分と照合できない。 +- サイドカーは自分の部屋での名前(`PULLCEPT_AGENT_NAME`)を `instructions` へ埋め込む。埋めなければエージェントは `meta.to` を自分と照合できない。 - `instructions` は「人間と AI を区別しない」ことを明示する。区別しない前提はフレームだけでなく作法にも要る。フレームから区別を消しても、作法が「人間に答えよ」と言っていれば、エージェントはもう無い区別を読むことになる。 - `meta.to` が自分の名前なら自分宛、他の参加者の名前なら自分宛ではない、無ければ部屋全体宛。全体宛に誰が答えるかはエージェントの判断に委ねる。 - 宛先による配送の絞り込みはアプリ側で行わない。部屋は全員へ配り、答えるかどうかをエージェントが決める。絞れば部屋が「誰に届いたか」を持つことになり、アプリが会話の中身へ関与し始める。 @@ -253,7 +253,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre ### 実機で確認済み(2026-08-21) - **1 往復が成立した。** 部屋の入力欄からの発言が channel notification としてセッションへ届き、セッションが `say_to_room` を呼び返し、その内容が部屋のメッセージ一覧へ並んだ。CLI 側へ追加入力は与えていない。 -- **channel サーバを 2 つ同時に読み込んだ状態で成立した。** 部屋のサイドカー(`liplus-chat-room`)と `github-webhook-mcp` の双方が同一セッションで機能した。#90 の premise は `--channels` 併記による二重登録の失敗のみを確定させており、複数の channel サーバを同時に有効化できることはここで初めて実測された。 +- **channel サーバを 2 つ同時に読み込んだ状態で成立した。** 部屋のサイドカー(`pullcept-room`)と `github-webhook-mcp` の双方が同一セッションで機能した。#90 の premise は `--channels` 併記による二重登録の失敗のみを確定させており、複数の channel サーバを同時に有効化できることはここで初めて実測された。 未計測: 往復の所要時間。成立の可否のみを確認しており、秒数は取っていない。参考値として #90 の実測では push から返信 tool 呼び出しまで約 10 秒(モデル思考時間込み)。 @@ -325,7 +325,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre セッションの起動オプションはアカウントの属性であり、画面で編集したその場で保存する。 -アプリ自身の channel エントリ(`server:liplus-chat-room--`)は、利用者が書いたものへ**統合**する。利用者が `--dangerously-load-development-channels` を書いていればその直後へ挿入し、書いていなければフラグごと足す。第二のフラグとしては足さない。同一フラグの二重指定は `--channels` 併記と同じ形であり、成立条件が壊れる。 +アプリ自身の channel エントリ(`server:pullcept-room--`)は、利用者が書いたものへ**統合**する。利用者が `--dangerously-load-development-channels` を書いていればその直後へ挿入し、書いていなければフラグごと足す。第二のフラグとしては足さない。同一フラグの二重指定は `--channels` 併記と同じ形であり、成立条件が壊れる。 統合であって上書きではない。利用者が別の channel サーバを指定しても、部屋のエントリは落ちない。落ちれば部屋の入力路そのものが消える。 @@ -363,7 +363,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre 同じアカウントの二重起動は拒否する。アカウントは「誰か」であり、同じ誰かが一つの部屋に二つの接続で居ることを認めない。名簿に同じ同一性が二度並び、その名前への宛先が二つの着地点を持つことになる。 -**この規則は部屋に対して閉じている。** 現在ルームは 1 つ(サイドカーの `LIPLUS_ROOM_ID` は `liplus-chat` 固定)であるため、実装上は「アカウントが一度だけ動く」と見分けが付かないが、同じ規則ではない。ルームを複数にすることは検討事項として残っており(「決まっていないこと」)、一つのアカウントが二つのルームでそれぞれ席を持つのは意図した形であって違反ではない。機構だけからはこの射程が読めないため、ここに書いておく。 +**この規則は部屋に対して閉じている。** 現在ルームは 1 つ(サイドカーの `PULLCEPT_ROOM_ID` は `pullcept` 固定)であるため、実装上は「アカウントが一度だけ動く」と見分けが付かないが、同じ規則ではない。ルームを複数にすることは検討事項として残っており(「決まっていないこと」)、一つのアカウントが二つのルームでそれぞれ席を持つのは意図した形であって違反ではない。機構だけからはこの射程が読めないため、ここに書いておく。 判定は席の台帳(`src-tauri/src/session.rs` の `RoomSeats`)が持つ。掃除と確保は一度のロック取得の中で行う——先に確認して後から確保する形では、二つの起動が同じ空席を通り抜ける。 @@ -375,7 +375,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre ### `.mcp.json` の登録単位 -`.mcp.json` の登録鍵はアカウントごとに分ける。名前は `liplus-chat-room--` であり、**アカウント id だけから決まる**。 +`.mcp.json` の登録鍵はアカウントごとに分ける。名前は `pullcept-room--` であり、**アカウント id だけから決まる**。 鍵が 1 つに固定されていたときは、同じ作業ディレクトリを指す 2 セッションが同じ鍵を上書きした。2 つめの起動が 1 つめの名前・色・部屋アドレスを書き換え、1 つめのセッションがまだ動いている間にその識別が消える(#40)。 @@ -383,9 +383,9 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre 鍵を起動ごとの id にもしない。それでは起動のたびに 1 つ増え、ユーザーのリポジトリのファイルが際限なく伸びる。アカウント id の関数にすることで、同じアカウントで立て直したときは同じエントリを使い回す。 -slug は読みやすさのためだけにあり、ASCII 英数字と `-` に落とす。一意性は hash が担う。id は不透明であるため、名前を鍵にしていたときより読みにくい——どのアカウントのエントリかは、そのエントリ自身の `LIPLUS_AGENT_NAME` から読む。読みやすさより同一性を採る。鍵が読みにくいのは 1 回の参照で済み、鍵が動くのは登録が行方不明になることであるから。 +slug は読みやすさのためだけにあり、ASCII 英数字と `-` に落とす。一意性は hash が担う。id は不透明であるため、名前を鍵にしていたときより読みにくい——どのアカウントのエントリかは、そのエントリ自身の `PULLCEPT_AGENT_NAME` から読む。読みやすさより同一性を採る。鍵が読みにくいのは 1 回の参照で済み、鍵が動くのは登録が行方不明になることであるから。 -登録のたびに、`liplus-chat-room` で始まり `LIPLUS_ROOM_URL` が現在の部屋アドレスと異なるエントリを削除する。部屋は起動ごとに新しいポートを取るため、それらは前回起動の残りであり、どのサイドカーも到達できない。残しておくと、この作業ディレクトリで CLI を起動するたびに死んだポートへ再試行するサイドカーが 1 つずつ増える。現在のアドレスを持つエントリは同一起動の別セッションであるから残す。`LIPLUS_ROOM_URL` を持たないエントリは、名前が前方一致しても他人のものとして触らない。 +登録のたびに、`pullcept-room` で始まり `PULLCEPT_ROOM_URL` が現在の部屋アドレスと異なるエントリを削除する。部屋は起動ごとに新しいポートを取るため、それらは前回起動の残りであり、どのサイドカーも到達できない。残しておくと、この作業ディレクトリで CLI を起動するたびに死んだポートへ再試行するサイドカーが 1 つずつ増える。現在のアドレスを持つエントリは同一起動の別セッションであるから残す。`PULLCEPT_ROOM_URL` を持たないエントリは、名前が前方一致しても他人のものとして触らない。 ## テストの配置 @@ -414,13 +414,13 @@ CI が実行するもの: 2. **セッションが生きているか。** 同じパネルのセッション行を見る。`終了(終了コード N)` なら CLI が落ちている。理由はタイトルバーの「診断」を開いた先の端末に残る。CLI が確認プロンプトで止まっている場合は、端末へ直接答える。 3. **サイドカーが接続したか。** パネルの名簿で、そのアカウントの行から「未起動」が消える。名簿にはアカウントが起動していなくても並ぶため、名前が出ていること自体は接続の根拠にならない。自分(人間)も発言前から載っている。接続していない場合は端末に `room socket: connected as ""` があるかを見る。 4. **サイドカー単体が壊れていないか。** `npm run sidecar:test` を実行する。偽の部屋ソケットを立てて両面を駆動するため、通ればサイドカーではなくアプリ側の問題に絞れる。 -5. **`.mcp.json` の登録が効いているか。** セッションの作業ディレクトリの `.mcp.json` に `liplus-chat-room--` が入っているかを見る。鍵はアカウント id から決まるため名前では引けない。どのアカウントのものかはエントリ内の `LIPLUS_AGENT_NAME` で読む。固定の `liplus-chat-room` は前回起動の残りとして掃除される側である。`--mcp-config` によるファイル渡しでは channel 側が名前を解決できないため、正式登録以外の経路は成立しない。 -6. **起動フラグが単独指定になっているか。** `--dangerously-load-development-channels server:liplus-chat-room--` 以外に `--channels` が付いていると、同一サーバが二重登録され全体が不通になる。アカウントの起動オプションに `--channels` / `--print` / `--input-format` / `--output-format` がある場合、セッション起動はエラーで止まる(黙って外すと動いたように見えるため)。 +5. **`.mcp.json` の登録が効いているか。** セッションの作業ディレクトリの `.mcp.json` に `pullcept-room--` が入っているかを見る。鍵はアカウント id から決まるため名前では引けない。どのアカウントのものかはエントリ内の `PULLCEPT_AGENT_NAME` で読む。固定の `pullcept-room` は前回起動の残りとして掃除される側である。`--mcp-config` によるファイル渡しでは channel 側が名前を解決できないため、正式登録以外の経路は成立しない。 +6. **起動フラグが単独指定になっているか。** `--dangerously-load-development-channels server:pullcept-room--` 以外に `--channels` が付いていると、同一サーバが二重登録され全体が不通になる。アカウントの起動オプションに `--channels` / `--print` / `--input-format` / `--output-format` がある場合、セッション起動はエラーで止まる(黙って外すと動いたように見えるため)。 7. **対話セッションになっているか。** `--print` 系では push が届かない。PTY 経由の対話起動であることを確認する。 ## UI の方向(2026-08-21 時点、暫定) -設計キャンバス: [liplus-chat UI 設計](https://claude.ai/code/artifact/aab6bbbd-0ebc-485e-9538-f63a8e6f40e5) +設計キャンバス: [Pullcept UI 設計](https://claude.ai/code/artifact/aab6bbbd-0ebc-485e-9538-f63a8e6f40e5) 2 枚のアートボードを並べ、その差分をロードマップとして読む形にしてある。 diff --git a/index.html b/index.html index cf0ecf4..46cada0 100644 --- a/index.html +++ b/index.html @@ -3,14 +3,14 @@ - Li+ Chat + Pullcept
- Li+ Chat + Pullcept diff --git a/package-lock.json b/package-lock.json index 9770347..c044ba2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "liplus-chat", + "name": "pullcept", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "liplus-chat", + "name": "pullcept", "version": "0.1.0", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 51a26b0..506139b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "liplus-chat", + "name": "pullcept", "private": true, "version": "0.1.0", "license": "Apache-2.0", diff --git a/sidecar/src/index.ts b/sidecar/src/index.ts index 26259b5..bad3c2b 100644 --- a/sidecar/src/index.ts +++ b/sidecar/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * liplus-chat room sidecar + * Pullcept room sidecar * * A stdio MCP server that puts one CLI session into the room. * @@ -33,10 +33,10 @@ import { import WebSocket from "ws"; import { randomUUID } from "node:crypto"; -const ROOM_URL = process.env.LIPLUS_ROOM_URL ?? ""; -const AGENT_NAME = process.env.LIPLUS_AGENT_NAME ?? "session"; -const ROOM_TOKEN = process.env.LIPLUS_ROOM_TOKEN ?? ""; -const CHAT_ID = process.env.LIPLUS_ROOM_ID ?? "liplus-chat"; +const ROOM_URL = process.env.PULLCEPT_ROOM_URL ?? ""; +const AGENT_NAME = process.env.PULLCEPT_AGENT_NAME ?? "session"; +const ROOM_TOKEN = process.env.PULLCEPT_ROOM_TOKEN ?? ""; +const CHAT_ID = process.env.PULLCEPT_ROOM_ID ?? "pullcept"; /** * The hue this session was launched under, in oklch degrees, or null when it @@ -46,7 +46,7 @@ const CHAT_ID = process.env.LIPLUS_ROOM_ID ?? "liplus-chat"; * a colour for from their name, and a number invented here would be indelible: * the room cannot tell a declaration from a fallback once it is on the wire. */ -const AGENT_HUE = readHue(process.env.LIPLUS_AGENT_HUE); +const AGENT_HUE = readHue(process.env.PULLCEPT_AGENT_HUE); function readHue(raw: string | undefined): number | null { if (raw === undefined || raw.trim() === "") return null; @@ -67,7 +67,7 @@ const PROTOCOL_VERSION = 4; const POST_RESULT_TIMEOUT = 15_000; function log(line: string): void { - process.stderr.write(`[liplus-chat sidecar] ${line}\n`); + process.stderr.write(`[pullcept sidecar] ${line}\n`); } // ── Room frames ────────────────────────────────────────────────────────────── @@ -90,7 +90,7 @@ function log(line: string): void { // // `hello` is where this session says who it is: the name it answers to and, // when it was launched with one, the hue it is drawn in. Both arrive from the -// launch (`LIPLUS_AGENT_NAME` / `LIPLUS_AGENT_HUE`) rather than from anything +// launch (`PULLCEPT_AGENT_NAME` / `PULLCEPT_AGENT_HUE`) rather than from anything // this file decides, because both are the person's declaration, made at the // moment of joining. // @@ -144,14 +144,14 @@ interface PostResultFrame { // ── MCP server ─────────────────────────────────────────────────────────────── const INSTRUCTIONS = [ - "あなたは liplus-chat の部屋に参加しています。", + "あなたは Pullcept の部屋に参加しています。", `この部屋でのあなたの名前は「${AGENT_NAME}」です。`, "", "この部屋は、人間と AI を区別しません。参加者は全員が同じ参加者であり、", "違いは名前だけです。発言もひとつの行為で、誰が出しても同じ形で届きます。", "相手が人間か別のセッションかを気にする必要はありません。", "", - '部屋の発言は として届きます。', + '部屋の発言は として届きます。', "発言するときは say_to_room ツールを呼んでください。ターミナルへの出力は", "部屋には届きません。", "", @@ -192,7 +192,7 @@ const INSTRUCTIONS = [ ].join("\n"); const mcp = new Server( - { name: "liplus-chat-room", version: "0.1.0" }, + { name: "pullcept-room", version: "0.1.0" }, { capabilities: { tools: {}, @@ -206,7 +206,7 @@ const TOOLS = [ { name: "say_to_room", description: - "Post a message to the liplus-chat room. This is the only way to be heard " + + "Post a message to the Pullcept room. This is the only way to be heard " + "by the room; terminal output is not read by anyone.", inputSchema: { type: "object" as const, @@ -360,7 +360,7 @@ const MAX_RETRY_DELAY = 30_000; const PING_INTERVAL = 25_000; function roomStatus(): string { - if (!ROOM_URL) return "LIPLUS_ROOM_URL is not set"; + if (!ROOM_URL) return "PULLCEPT_ROOM_URL is not set"; if (ws && ws.readyState === WebSocket.OPEN) return "connected"; return lastError ? `disconnected: ${lastError}` : "disconnected"; } @@ -524,5 +524,5 @@ if (ROOM_URL) { // Serving MCP without a room is a degraded but legible state: the agent can // still call the tool and gets told why nothing was delivered. Exiting here // would surface to the user as a bare MCP connection failure instead. - log("room socket: LIPLUS_ROOM_URL is not set, staying offline"); + log("room socket: PULLCEPT_ROOM_URL is not set, staying offline"); } diff --git a/sidecar/test/round-trip.test.mjs b/sidecar/test/round-trip.test.mjs index a80fefe..1d84ea7 100644 --- a/sidecar/test/round-trip.test.mjs +++ b/sidecar/test/round-trip.test.mjs @@ -186,10 +186,10 @@ test("a room post reaches the channel, and say_to_room reaches the room", async cwd: REPO, env: { ...process.env, - LIPLUS_ROOM_URL: `ws://127.0.0.1:${port}`, - LIPLUS_AGENT_NAME: "test-agent", - LIPLUS_AGENT_HUE: "145", - LIPLUS_ROOM_ID: "test-room", + PULLCEPT_ROOM_URL: `ws://127.0.0.1:${port}`, + PULLCEPT_AGENT_NAME: "test-agent", + PULLCEPT_AGENT_HUE: "145", + PULLCEPT_ROOM_ID: "test-room", }, stdio: ["pipe", "pipe", "pipe"], }, @@ -583,10 +583,10 @@ test("a session launched without a declared hue says so by omission", async (t) cwd: REPO, env: { ...process.env, - LIPLUS_ROOM_URL: `ws://127.0.0.1:${port}`, - LIPLUS_AGENT_NAME: "no-colour", - LIPLUS_AGENT_HUE: "", - LIPLUS_ROOM_ID: "test-room", + PULLCEPT_ROOM_URL: `ws://127.0.0.1:${port}`, + PULLCEPT_AGENT_NAME: "no-colour", + PULLCEPT_AGENT_HUE: "", + PULLCEPT_ROOM_ID: "test-room", }, stdio: ["pipe", "pipe", "pipe"], }, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 691cc56..16e4845 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2131,26 +2131,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "liplus-chat" -version = "0.1.0" -dependencies = [ - "futures-util", - "mcp-config", - "parking_lot", - "portable-pty", - "room-floor", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-clipboard-manager", - "tauri-plugin-opener", - "tokio", - "tokio-tungstenite", - "uuid", -] - [[package]] name = "litemap" version = "0.8.3" @@ -2916,6 +2896,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pullcept" +version = "0.1.0" +dependencies = [ + "futures-util", + "mcp-config", + "parking_lot", + "portable-pty", + "room-floor", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-clipboard-manager", + "tauri-plugin-opener", + "tokio", + "tokio-tungstenite", + "uuid", +] + [[package]] name = "pxfm" version = "0.1.30" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4da5c63..489001c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "liplus-chat" +name = "pullcept" version = "0.1.0" description = "Local chat room hosting multiple AI agents over a claude/channel MCP surface" authors = ["Liplus Project Contributors"] @@ -11,7 +11,7 @@ edition = "2021" # The `_lib` suffix may seem redundant but it is necessary # to make the lib name unique and wouldn't conflict with the bin name. # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 -name = "liplus_chat_lib" +name = "pullcept_lib" crate-type = ["staticlib", "rlib"] [build-dependencies] diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index ec852af..abde0c1 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -139,9 +139,9 @@ pub fn load_config(app: AppHandle) -> Result { // nothing left for one to do — the person's own working directory and // launch options are what would have been lost, and they carry over. // - // No path exists for anything older than that. liplus-chat has never + // No path exists for anything older than that. Pullcept has never // shipped a release, and its app data directory is keyed to its own - // identifier (org.liplus-project.liplus-chat), so no config in the older + // identifier (org.liplus-project.pullcept), so no config in the older // left/right pane format from liplus-desktop can reach this app. serde_json::from_str::(&content) .map_err(|e| format!("Failed to parse config: {e}")) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 78eeefd..192be9e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - liplus_chat_lib::run() + pullcept_lib::run() } diff --git a/src-tauri/src/session.rs b/src-tauri/src/session.rs index b8fafbf..8e7a504 100644 --- a/src-tauri/src/session.rs +++ b/src-tauri/src/session.rs @@ -45,8 +45,8 @@ fn resolve_sidecar_paths() -> Result<(PathBuf, PathBuf), String> { } } - let entry_override = from_env("LIPLUS_SIDECAR_ENTRY")?; - let runner_override = from_env("LIPLUS_SIDECAR_RUNNER")?; + let entry_override = from_env("PULLCEPT_SIDECAR_ENTRY")?; + let runner_override = from_env("PULLCEPT_SIDECAR_RUNNER")?; if let (Some(entry), Some(runner)) = (&entry_override, &runner_override) { return Ok((entry.clone(), runner.clone())); } @@ -71,7 +71,7 @@ fn resolve_sidecar_paths() -> Result<(PathBuf, PathBuf), String> { } } Err("Could not find sidecar/src/index.ts next to node_modules/tsx. \ - Run npm install, or set LIPLUS_SIDECAR_ENTRY and LIPLUS_SIDECAR_RUNNER." + Run npm install, or set PULLCEPT_SIDECAR_ENTRY and PULLCEPT_SIDECAR_RUNNER." .to_string()) } @@ -83,7 +83,7 @@ fn resolve_sidecar_paths() -> Result<(PathBuf, PathBuf), String> { /// name would have two places to land. /// /// The rule is scoped to the room, not to the account. There is one room today -/// — the sidecar's `LIPLUS_ROOM_ID` is fixed to `liplus-chat` — so this map +/// — the sidecar's `PULLCEPT_ROOM_ID` is fixed to `pullcept` — so this map /// needs no room in its key yet, and with one room the refusal is /// indistinguishable from "an account runs once". They are not the same rule. /// Rooms are meant to become plural (`design/Vision.dc.html`), and one account diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 45505e8..52fa135 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "liplus-chat", + "productName": "pullcept", "version": "0.1.0", - "identifier": "org.liplus-project.liplus-chat", + "identifier": "org.liplus-project.pullcept", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", @@ -13,7 +13,7 @@ "withGlobalTauri": true, "windows": [ { - "title": "Li+ Chat", + "title": "Pullcept", "width": 1200, "height": 800 } diff --git a/src/main.ts b/src/main.ts index 84d8de4..fd9bb15 100644 --- a/src/main.ts +++ b/src/main.ts @@ -100,8 +100,8 @@ interface StartedSession { started_at: string; } -const NAME_KEY = "liplus-chat.display-name"; -const HUE_KEY = "liplus-chat.display-hue"; +const NAME_KEY = "pullcept.display-name"; +const HUE_KEY = "pullcept.display-hue"; /** * The hues a participant can declare.