session-peer 0.1.0-preview.0 → 0.1.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +9 -0
- package/README.ja.md +10 -6
- package/README.ko.md +26 -7
- package/README.md +30 -8
- package/README.zh-CN.md +10 -6
- package/RELEASING.md +31 -0
- package/VALIDATION.md +36 -1
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +57 -10
- package/dist/discovery.d.ts +10 -0
- package/dist/discovery.js +19 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/process.d.ts +16 -0
- package/dist/process.js +14 -6
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.js +1 -1
- package/dist/send.d.ts +10 -0
- package/dist/send.js +5 -1
- package/dist/windows.d.ts +9 -0
- package/dist/windows.js +171 -0
- package/dist/writer.d.ts +8 -0
- package/dist/writer.js +26 -8
- package/package.json +10 -4
package/CONTRIBUTING.md
CHANGED
|
@@ -35,6 +35,15 @@ npm run test:package
|
|
|
35
35
|
npm audit
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
Tests live in `test/*.ts`. `npm test` builds declarations, runs strict test
|
|
39
|
+
type-checking, then executes the tests with Node's type-stripping flag (including
|
|
40
|
+
on Node 22.13). Release/repository automation remains JavaScript.
|
|
41
|
+
`npm run test:package` checks that the packed declarations resolve from a
|
|
42
|
+
separate TypeScript consumer. The package root exposes only pure protocol helpers
|
|
43
|
+
(`VERSION`, `VERSION_LINE`, `reply`, `envelope`); importing it does not run the CLI.
|
|
44
|
+
The npm TypeScript indicator requires publishing these declarations in a new
|
|
45
|
+
version; it does not retroactively alter `0.1.0-preview.0`.
|
|
46
|
+
|
|
38
47
|
Python reference commit: `47c23713d0a2a3c11ebde6186afd8c43489b8b65`.
|
|
39
48
|
CI checks it out separately. Python is a development oracle, never a runtime
|
|
40
49
|
fallback. Match Node 22/24, macOS/Linux and the native dependency requirements.
|
package/README.ja.md
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
- インストール済みの同じクライアントへの SSH 接続、構造化 Reply-To URI、JSON 出力。
|
|
16
16
|
- 宛先や所有者が曖昧なら拒否。不確実な提出を自動再送しません。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Relay 通信、MCP、wake/resume、非アクティブ queue、Antigravity、自動更新、暗黙の全エージェント検出、人間向けテキスト出力は未実装です。未対応コマンドは明示的に失敗します。汎用オーケストレーターではありません。
|
|
19
19
|
|
|
20
20
|
## 必要条件
|
|
21
21
|
|
|
22
|
-
macOS / Linux、Node **22.x の 22.13 以上、または 24.x**。Node 26
|
|
22
|
+
macOS / Linux / Windows native、Node **22.x の 22.13 以上、または 24.x**。Node 26 は対象外です。ネイティブロック依存には対応する x64/arm64 バイナリが必要で、純 JavaScript パッケージではありません。Codex 送信には `codex` が必要で、macOS/Linux では `lsof`・`ps` も必要です。Windows はネイティブロックと Restart Manager で所有者を確認します。Claude にはアクセス可能な inbox を持つ稼働中 TUI が必要です。SSH は既存の鍵・ホスト信頼と、接続先の**同一バージョン**のクライアントを使います。
|
|
23
23
|
|
|
24
24
|
## インストール
|
|
25
25
|
|
|
@@ -33,16 +33,18 @@ npm run build
|
|
|
33
33
|
node dist/cli.js --version
|
|
34
34
|
npm pack --ignore-scripts
|
|
35
35
|
# 任意のグローバルインストール前に PATH の既存コマンドを確認
|
|
36
|
-
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.
|
|
36
|
+
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.1.tgz
|
|
37
37
|
session-peer --version
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
期待値は `session-peer 0.1.0-preview.
|
|
40
|
+
期待値は `session-peer 0.1.0-preview.1 (typescript)`。`./...tgz` を省略しないでください。これは未確認のレジストリではなくローカル成果物を選択します。別途承認された npm 公開後も、パッケージ名とコマンド名は上記のままです。そのリリースのバージョン / dist-tag を確認してください。
|
|
41
41
|
|
|
42
42
|
### 既存インストールとの共存
|
|
43
43
|
|
|
44
44
|
インストール前後に `type -a session-peer` と `command -v session-peer` を確認します。他の実装も同名コマンドを提供するため、PATH 上の一つを選ぶか `node /absolute/path/dist/cli.js` を使ってください。`--force` で他の管理ツールのファイルを上書きしないでください。Python パッケージ・スキル・サービスを自動変更しません。削除は `npm uninstall --global session-peer` を使い、PATH を再確認します。
|
|
45
45
|
|
|
46
|
+
Windows PowerShell では `Get-Command session-peer -All` で既存コマンドを確認します。Python CLI を置換せずに試すには、`npm ci --ignore-scripts`、`npm run build`、`npm pack --ignore-scripts` の後、`npm install --prefix "$env:TEMP\session-peer-ts-preview" --ignore-scripts .\session-peer-0.1.0-preview.1.tgz` を実行し、`& "$env:TEMP\session-peer-ts-preview\node_modules\.bin\session-peer.cmd" --version` で確認します。同じ prefix の `npm uninstall --prefix "$env:TEMP\session-peer-ts-preview" session-peer` で削除します。
|
|
47
|
+
|
|
46
48
|
## 使い方
|
|
47
49
|
|
|
48
50
|
```sh
|
|
@@ -63,6 +65,8 @@ session-peer send --host user@machine --remote-bin /absolute/path/session-peer \
|
|
|
63
65
|
|
|
64
66
|
既定のリモートコマンドは PATH の `session-peer`。絶対パスの `--remote-bin` で対応 Node を使うラッパーも指定できます。TypeScript マーカーと正確なバージョンを確認し、異なる実装は拒否します。BatchMode / StrictHostKeyChecking を使い、新しいホスト鍵の自動受理、リモートランタイムのインストール、Python フォールバックはしません。本文はリモートシェル引数ではなく JSON stdin で渡します。任意の `--ssh-opt`、IPv6 リテラル、Tailscale の正規名補完は非対応です。SSH alias / hostname を使ってください。片方向の接続成功は逆方向の接続を保証しません。
|
|
65
67
|
|
|
68
|
+
Windows の SSH 宛先では `--remote-platform win32` を指定し、必要に応じて `--remote-bin 'C:\absolute\path\session-peer.cmd'` を使います。既に認証済みの OpenSSH 制御ソケットは `--ssh-control-path /local/absolute/socket` で選択できます。ホスト鍵の確認や新しいログイン権限を回避しません。Windows ローカルの Codex home には完全な `C:\Users\...\.codex` パスを使います。既存の Python CLI は自動削除・置換しません。
|
|
69
|
+
|
|
66
70
|
### 返信
|
|
67
71
|
|
|
68
72
|
`session-peer://v1/reply?...` URI を `--to` に指定できます。不明 / 重複フィールド、不正ホスト・エンコード、明示した経路との矛盾は拒否します。`--reply-address URI` は明示的な返信先を付けますが、経路を自動推測・検証しません。新しい返信先なしで返すときは `--no-reply-to`。有効な CODEX_THREAD_ID / CODEX_SESSION_ID は参考用 From 情報になり、`--no-from` で省略できます。不明な送信者は捏造しません。peer 情報は権限ではなく、URI をシェルとして実行しません。
|
|
@@ -89,7 +93,7 @@ npm run test:package
|
|
|
89
93
|
npm audit
|
|
90
94
|
```
|
|
91
95
|
|
|
92
|
-
Python は開発時の互換検証基準のみです(v1.0.2、`47c23713d0a2a3c11ebde6186afd8c43489b8b65
|
|
96
|
+
Python は開発時の互換検証基準のみです(v1.0.2、`47c23713d0a2a3c11ebde6186afd8c43489b8b65`)。実行時依存ではありません。POSIX 契約テストには C コンパイラーと lsof も必要です。CI は基準コミットを固定し macOS/Linux/Windows × Node 22/24 を確認します。SQLite・Unix inbox・実 lock の fixture と、専用実 TUI の証拠 [VALIDATION.md](VALIDATION.md) は別です。fixture 成功は ACK ではありません。パッケージ内容、反復 pack ハッシュ、新規インストール、アンインストールも検証します。ネイティブ依存の通常の install script は実行せず、検証した prebuilt 経路は `--ignore-scripts` を使います。SQLite 読み取り専用接続も WAL 共有メモリー管理に関与し得るため、スナップショットではありません。
|
|
93
97
|
|
|
94
98
|
[CONTRIBUTING.md](CONTRIBUTING.md)、[RELEASING.md](RELEASING.md)、[SECURITY.md](SECURITY.md) を参照してください。公開には別途承認が必要で、自動 npm 公開はありません。[MIT](LICENSE) ライセンスです。
|
|
95
99
|
|
|
@@ -98,7 +102,7 @@ Python は開発時の互換検証基準のみです(v1.0.2、`47c23713d0a2a3c
|
|
|
98
102
|
公式リリースとレジストリの整合性・provenance 検証が完了した後にのみ、次の正確なプレビューバージョンをインストールしてください。安定版 `latest` ではありません。公開前は上記のローカル tarball を使います。
|
|
99
103
|
|
|
100
104
|
```sh
|
|
101
|
-
npm install --global --ignore-scripts session-peer@0.1.0-preview.
|
|
105
|
+
npm install --global --ignore-scripts session-peer@0.1.0-preview.1
|
|
102
106
|
session-peer --version
|
|
103
107
|
```
|
|
104
108
|
|
package/README.ko.md
CHANGED
|
@@ -19,15 +19,16 @@ TypeScript 클라이언트입니다. Python 없이 Node.js로 실행합니다.
|
|
|
19
19
|
- 구조화된 Reply-To URI와 JSON 결과
|
|
20
20
|
- 모호한 대상·확인할 수 없는 소유권은 거부하고, 불확실한 제출은 자동 재시도하지 않음
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Relay 전송, MCP, wake/resume, 비활성 세션 queue, Antigravity, 자동 업데이트,
|
|
23
23
|
암묵적인 전체 에이전트 탐색, 일반 텍스트 출력은 미지원입니다. 미지원 옵션은 명시적으로
|
|
24
24
|
거부하며 범용 오케스트레이터를 지향하지 않습니다.
|
|
25
25
|
|
|
26
26
|
## 요구사항
|
|
27
27
|
|
|
28
|
-
macOS/Linux와 Node **22.x의 22.13 이상 또는 24.x**가 필요합니다. Node 26은
|
|
28
|
+
macOS/Linux/Windows native와 Node **22.x의 22.13 이상 또는 24.x**가 필요합니다. Node 26은
|
|
29
29
|
미지원입니다. 네이티브 flock 의존성에 맞는 사전 빌드 바이너리(x64/arm64)가 필요하므로
|
|
30
|
-
순수 JavaScript 패키지는 아닙니다. Codex 전송에는 `codex
|
|
30
|
+
순수 JavaScript 패키지는 아닙니다. Codex 전송에는 `codex`가 필요하고 macOS/Linux에서는
|
|
31
|
+
`lsof`, `ps`도 필요합니다. Windows는 네이티브 잠금과 Restart Manager로 소유자를 확인합니다. 저장된
|
|
31
32
|
스레드의 유일하고 안정적인 live writer가 필요합니다. Claude는 접근 가능한 inbox가
|
|
32
33
|
있는 실행 중 TUI가 필요합니다. SSH에는 OpenSSH, 기존 키·호스트 신뢰 설정과 원격의
|
|
33
34
|
**동일 버전 클라이언트**가 필요합니다.
|
|
@@ -46,11 +47,11 @@ npm run build
|
|
|
46
47
|
node dist/cli.js --version
|
|
47
48
|
npm pack --ignore-scripts
|
|
48
49
|
# 선택 사항: PATH에서 사용할 구현을 명시적으로 선택한 뒤 전역 설치
|
|
49
|
-
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.
|
|
50
|
+
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.1.tgz
|
|
50
51
|
session-peer --version
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
예상 출력은 `session-peer 0.1.0-preview.
|
|
54
|
+
예상 출력은 `session-peer 0.1.0-preview.1 (typescript)`입니다. 설치 명령의
|
|
54
55
|
`./...tgz`는 검증되지 않은 레지스트리 패키지가 아닌 로컬 산출물을 지정하므로 생략하지
|
|
55
56
|
마세요. 별도 승인된 npm 발행 이후에도 패키지명은 `session-peer`, 명령은
|
|
56
57
|
`session-peer`로 유지하며 해당 릴리스의 버전·dist-tag 안내를 따릅니다.
|
|
@@ -64,6 +65,18 @@ session-peer --version
|
|
|
64
65
|
않습니다. npm 설치본 제거는 `npm uninstall --global session-peer`로 하고 PATH를
|
|
65
66
|
다시 확인하세요.
|
|
66
67
|
|
|
68
|
+
Windows PowerShell에서는 `Get-Command session-peer -All`로 기존 설치본을 확인하세요.
|
|
69
|
+
Python CLI를 덮어쓰지 않으려면 격리 prefix에 설치·제거할 수 있습니다.
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
npm ci --ignore-scripts
|
|
73
|
+
npm run build
|
|
74
|
+
npm pack --ignore-scripts
|
|
75
|
+
npm install --prefix "$env:TEMP\session-peer-ts-preview" --ignore-scripts .\session-peer-0.1.0-preview.1.tgz
|
|
76
|
+
& "$env:TEMP\session-peer-ts-preview\node_modules\.bin\session-peer.cmd" --version
|
|
77
|
+
# 이후 제거: npm uninstall --prefix "$env:TEMP\session-peer-ts-preview" session-peer
|
|
78
|
+
```
|
|
79
|
+
|
|
67
80
|
## 사용법
|
|
68
81
|
|
|
69
82
|
```sh
|
|
@@ -95,6 +108,12 @@ Node를 선택하는 래퍼를 지정할 수 있습니다. TypeScript 표시와
|
|
|
95
108
|
Tailscale 정규 이름 보강은 미지원이므로 SSH 별칭·호스트명을 사용하세요. 정방향 접속이
|
|
96
109
|
역방향 접속을 보장하지 않습니다.
|
|
97
110
|
|
|
111
|
+
Windows SSH 대상에는 `--remote-platform win32`를 명시하고, 원격 PATH에 없다면
|
|
112
|
+
`--remote-bin 'C:\절대\경로\session-peer.cmd'`를 지정하세요. 이미 인증된 OpenSSH
|
|
113
|
+
제어 소켓은 `--ssh-control-path /로컬/절대/소켓`으로 선택할 수 있습니다. 이는 호스트 키
|
|
114
|
+
검증을 우회하거나 새 로그인을 허용하지 않습니다. Windows 로컬 Codex 홈은
|
|
115
|
+
`C:\Users\...\.codex`처럼 전체 경로를 사용합니다. 기존 Python CLI는 자동 제거·교체하지 않습니다.
|
|
116
|
+
|
|
98
117
|
### 회신
|
|
99
118
|
|
|
100
119
|
`session-peer://v1/reply?...` URI를 `--to`로 사용할 수 있습니다. 알 수 없거나 중복된
|
|
@@ -134,7 +153,7 @@ npm audit
|
|
|
134
153
|
|
|
135
154
|
Python은 개발용 호환성 기준(v1.0.2 커밋
|
|
136
155
|
`47c23713d0a2a3c11ebde6186afd8c43489b8b65`)일 뿐 런타임 의존성이 아닙니다. 테스트에는
|
|
137
|
-
C 컴파일러와 lsof
|
|
156
|
+
C 컴파일러와 lsof는 POSIX 계약 테스트에 필요합니다. CI는 참조 커밋을 고정하고 macOS/Linux/Windows × Node 22/24를
|
|
138
157
|
검사합니다. 임시 SQLite·Unix inbox·실제 잠금 fixture와 [VALIDATION.md](VALIDATION.md)의
|
|
139
158
|
실제 TUI 증거는 구분합니다. fixture 통과는 ACK가 아닙니다. 패키지 내용·반복 빌드 해시·
|
|
140
159
|
새 환경 설치·삭제도 검사합니다. 네이티브 의존성에는 설치 스크립트가 있지만 검증한 사전
|
|
@@ -152,7 +171,7 @@ C 컴파일러와 lsof도 필요합니다. CI는 참조 커밋을 고정하고 m
|
|
|
152
171
|
로컬 tarball 설치를 사용합니다.
|
|
153
172
|
|
|
154
173
|
```sh
|
|
155
|
-
npm install --global --ignore-scripts session-peer@0.1.0-preview.
|
|
174
|
+
npm install --global --ignore-scripts session-peer@0.1.0-preview.1
|
|
156
175
|
session-peer --version
|
|
157
176
|
```
|
|
158
177
|
|
package/README.md
CHANGED
|
@@ -21,15 +21,16 @@ a Relay server or hosted service.
|
|
|
21
21
|
- Refuse ambiguous targets and uncertain ownership; never automatically retry
|
|
22
22
|
an uncertain submission.
|
|
23
23
|
|
|
24
|
-
Not implemented:
|
|
24
|
+
Not implemented: Relay transport, MCP, wake/resume, inactive queueing,
|
|
25
25
|
Antigravity, automatic updates, implicit multi-agent discovery, or human text
|
|
26
26
|
output. Unsupported commands fail explicitly; this is not a general orchestrator.
|
|
27
27
|
|
|
28
28
|
## Requirements
|
|
29
29
|
|
|
30
|
-
macOS or
|
|
30
|
+
macOS, Linux or native Windows; Node **22.13+ within 22.x or 24.x**. Node 26 is not supported.
|
|
31
31
|
The native flock dependency needs a matching prebuilt binary (x64/arm64); this
|
|
32
|
-
is not a pure-JavaScript package. Codex sends
|
|
32
|
+
is not a pure-JavaScript package. Codex sends need `codex`; macOS/Linux also
|
|
33
|
+
need `lsof` and `ps`. Windows uses native lock and Restart Manager inspection.
|
|
33
34
|
Claude needs a live TUI with an accessible native inbox. SSH requires OpenSSH,
|
|
34
35
|
existing key/host trust and the **same client version** on the destination.
|
|
35
36
|
|
|
@@ -47,11 +48,11 @@ npm run build
|
|
|
47
48
|
node dist/cli.js --version
|
|
48
49
|
npm pack --ignore-scripts
|
|
49
50
|
# Optional global install: first check which session-peer your PATH selects.
|
|
50
|
-
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.
|
|
51
|
+
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.1.tgz
|
|
51
52
|
session-peer --version
|
|
52
53
|
```
|
|
53
54
|
|
|
54
|
-
Expected: `session-peer 0.1.0-preview.
|
|
55
|
+
Expected: `session-peer 0.1.0-preview.1 (typescript)`. Keep the `./...tgz` path;
|
|
55
56
|
it selects the locally built artifact, not an unverified registry package.
|
|
56
57
|
After a separately approved npm release, the package will remain `session-peer`
|
|
57
58
|
while the command remains `session-peer`; follow that release's version/dist-tag.
|
|
@@ -65,6 +66,18 @@ or call this build explicitly with `node /absolute/path/dist/cli.js`. Do not use
|
|
|
65
66
|
is installed, removed or reconfigured automatically. To remove this npm install,
|
|
66
67
|
run `npm uninstall --global session-peer` and recheck PATH.
|
|
67
68
|
|
|
69
|
+
On Windows PowerShell, inspect competing commands with
|
|
70
|
+
`Get-Command session-peer -All`. To avoid replacing the Python CLI, use an isolated prefix:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
npm ci --ignore-scripts
|
|
74
|
+
npm run build
|
|
75
|
+
npm pack --ignore-scripts
|
|
76
|
+
npm install --prefix "$env:TEMP\session-peer-ts-preview" --ignore-scripts .\session-peer-0.1.0-preview.1.tgz
|
|
77
|
+
& "$env:TEMP\session-peer-ts-preview\node_modules\.bin\session-peer.cmd" --version
|
|
78
|
+
# Later: npm uninstall --prefix "$env:TEMP\session-peer-ts-preview" session-peer
|
|
79
|
+
```
|
|
80
|
+
|
|
68
81
|
## Use
|
|
69
82
|
|
|
70
83
|
```sh
|
|
@@ -97,6 +110,14 @@ JSON stdin request, not remote shell arguments. Arbitrary `--ssh-opt`, IPv6
|
|
|
97
110
|
literals and Tailscale canonical-name enrichment are not supported; use an SSH
|
|
98
111
|
alias/hostname. Forward access does not establish reverse access.
|
|
99
112
|
|
|
113
|
+
For a Windows SSH destination, add `--remote-platform win32` and select a
|
|
114
|
+
Windows `--remote-bin 'C:\absolute\path\session-peer.cmd'` if needed. A previously
|
|
115
|
+
authenticated OpenSSH control socket can be selected with
|
|
116
|
+
`--ssh-control-path /absolute/local/socket`; this does not bypass host-key
|
|
117
|
+
verification or grant a new login. For local Windows Codex commands, use a full
|
|
118
|
+
`C:\Users\...\.codex` path for `--codex-home`. The Python CLI, if present,
|
|
119
|
+
is not removed or replaced by this preview.
|
|
120
|
+
|
|
100
121
|
### Replies
|
|
101
122
|
|
|
102
123
|
Use a `session-peer://v1/reply?...` URI as `--to`. Unknown/duplicate fields,
|
|
@@ -137,8 +158,9 @@ npm audit
|
|
|
137
158
|
|
|
138
159
|
Python is only the development conformance oracle (v1.0.2 commit
|
|
139
160
|
`47c23713d0a2a3c11ebde6186afd8c43489b8b65`), never a runtime dependency. Tests also
|
|
140
|
-
need a C compiler and lsof. CI pins the reference
|
|
141
|
-
|
|
161
|
+
need a C compiler and lsof for the POSIX contract suite. CI pins the reference
|
|
162
|
+
and covers macOS/Linux/Windows × Node 22/24. Temporary
|
|
163
|
+
SQLite, Unix inbox and real lock fixtures are distinct from
|
|
142
164
|
the dedicated real-TUI evidence in [VALIDATION.md](VALIDATION.md). A green fixture
|
|
143
165
|
test is not an ACK. Package tests inspect contents, repeat-pack hashes, clean
|
|
144
166
|
install and uninstall. The native dependency normally has an install script;
|
|
@@ -156,7 +178,7 @@ install the exact preview below. It is not the stable `latest` channel. Before
|
|
|
156
178
|
publication, keep using the local tarball instructions above.
|
|
157
179
|
|
|
158
180
|
```sh
|
|
159
|
-
npm install --global --ignore-scripts session-peer@0.1.0-preview.
|
|
181
|
+
npm install --global --ignore-scripts session-peer@0.1.0-preview.1
|
|
160
182
|
session-peer --version
|
|
161
183
|
```
|
|
162
184
|
|
package/README.zh-CN.md
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
- 通过 SSH 调用远端已安装的同版本客户端,支持结构化 Reply-To URI 和 JSON 输出。
|
|
16
16
|
- 目标或进程归属不明确时拒绝操作;不会自动重试结果不确定的提交。
|
|
17
17
|
|
|
18
|
-
未实现
|
|
18
|
+
未实现 Relay 传输、MCP、wake/resume、非活跃会话排队、Antigravity、自动更新、隐式多代理发现及面向人的文本输出。不支持的命令会明确失败;这不是通用编排器。
|
|
19
19
|
|
|
20
20
|
## 环境要求
|
|
21
21
|
|
|
22
|
-
macOS 或
|
|
22
|
+
macOS、Linux 或 Windows native;Node **22.x 中的 22.13 及以上,或 24.x**。不支持 Node 26。原生锁依赖需要匹配的 x64/arm64 预编译二进制,本包不是纯 JavaScript 实现。Codex 发送需要 `codex`,macOS/Linux 还需要 `lsof`、`ps`;Windows 使用原生锁和 Restart Manager 验证所有者。Claude 需要运行中的 TUI 及可访问的原生 inbox。SSH 使用已有密钥和主机信任,远端必须安装**同版本**客户端。
|
|
23
23
|
|
|
24
24
|
## 安装
|
|
25
25
|
|
|
@@ -33,16 +33,18 @@ npm run build
|
|
|
33
33
|
node dist/cli.js --version
|
|
34
34
|
npm pack --ignore-scripts
|
|
35
35
|
# 可选:全局安装前先检查 PATH 选择的现有命令
|
|
36
|
-
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.
|
|
36
|
+
npm install --global --ignore-scripts ./session-peer-0.1.0-preview.1.tgz
|
|
37
37
|
session-peer --version
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
预期输出:`session-peer 0.1.0-preview.
|
|
40
|
+
预期输出:`session-peer 0.1.0-preview.1 (typescript)`。保留 `./...tgz` 路径,确保安装的是本地构建产物,而非未验证的注册表包。将来另行批准 npm 发布后,包名和命令名仍如上所示,请按该次发布的版本 / dist-tag 安装。
|
|
41
41
|
|
|
42
42
|
### 与已有安装共存
|
|
43
43
|
|
|
44
44
|
安装前后用 `type -a session-peer` 和 `command -v session-peer` 检查实际执行项。其他实现也可能提供同名命令;请选择 PATH 上的一种,或显式运行 `node /absolute/path/dist/cli.js`。不要用 `--force` 覆盖其他管理器的文件。本包不会自动安装、删除或调整 Python 包、技能或服务。卸载使用 `npm uninstall --global session-peer`,随后再次检查 PATH。
|
|
45
45
|
|
|
46
|
+
在 Windows PowerShell 中用 `Get-Command session-peer -All` 检查已有命令。为了不替换 Python CLI,可先执行 `npm ci --ignore-scripts`、`npm run build`、`npm pack --ignore-scripts`,再用 `npm install --prefix "$env:TEMP\session-peer-ts-preview" --ignore-scripts .\session-peer-0.1.0-preview.1.tgz` 安装到隔离目录。以 `& "$env:TEMP\session-peer-ts-preview\node_modules\.bin\session-peer.cmd" --version` 验证,并可用 `npm uninstall --prefix "$env:TEMP\session-peer-ts-preview" session-peer` 卸载。
|
|
47
|
+
|
|
46
48
|
## 使用
|
|
47
49
|
|
|
48
50
|
```sh
|
|
@@ -63,6 +65,8 @@ session-peer send --host user@machine --remote-bin /absolute/path/session-peer \
|
|
|
63
65
|
|
|
64
66
|
默认远程命令是 PATH 中的 `session-peer`。绝对路径 `--remote-bin` 也可选择使用受支持 Node 的包装器。握手检查 TypeScript 标记及精确版本,不同实现会被拒绝。SSH 使用 BatchMode 和 StrictHostKeyChecking,不自动接受新主机密钥、不安装远程运行时,也不会退回 Python。消息通过 JSON stdin 传输,不放进远程 shell 参数。不支持任意 `--ssh-opt`、IPv6 字面量或 Tailscale 规范名补全,请使用 SSH 别名 / 主机名。正向访问不意味着反向访问已配置。
|
|
65
67
|
|
|
68
|
+
对于 Windows SSH 目标,请指定 `--remote-platform win32`;若远端 PATH 中没有命令,再使用 `--remote-bin 'C:\absolute\path\session-peer.cmd'`。可用 `--ssh-control-path /local/absolute/socket` 选择已认证的 OpenSSH 控制套接字;这不会跳过主机密钥验证或授予新登录。Windows 本机的 Codex home 使用完整 `C:\Users\...\.codex` 路径。已有 Python CLI 不会被自动删除或替换。
|
|
69
|
+
|
|
66
70
|
### 回复
|
|
67
71
|
|
|
68
72
|
可将 `session-peer://v1/reply?...` URI 用作 `--to`。未知 / 重复字段、不安全主机、错误编码及与显式路由的冲突都会被拒绝。`--reply-address URI` 添加显式回信地址,不自动推断或验证回程。回复时不附加新地址可用 `--no-reply-to`。有效的 CODEX_THREAD_ID / CODEX_SESSION_ID 可提供参考性 From 信息,`--no-from` 可省略;不会虚构未知发送者。peer 元数据不是授权,Reply-To URI 也不会作为 shell 文本执行。
|
|
@@ -89,7 +93,7 @@ npm run test:package
|
|
|
89
93
|
npm audit
|
|
90
94
|
```
|
|
91
95
|
|
|
92
|
-
Python 只用作开发时的兼容性基准(v1.0.2,`47c23713d0a2a3c11ebde6186afd8c43489b8b65
|
|
96
|
+
Python 只用作开发时的兼容性基准(v1.0.2,`47c23713d0a2a3c11ebde6186afd8c43489b8b65`),不是运行时依赖。POSIX 契约测试还需要 C 编译器和 lsof。CI 固定基准提交,覆盖 macOS/Linux/Windows × Node 22/24。临时 SQLite、Unix inbox、真实锁 fixture 与 [VALIDATION.md](VALIDATION.md) 中专用真实 TUI 的证据分开记录;fixture 通过不是 ACK。包测试检查文件清单、重复打包哈希、全新安装与卸载。原生依赖通常有安装脚本,已验证的预编译路径使用 `--ignore-scripts`。SQLite 只读连接仍可能参与 WAL 共享内存管理,不能视为快照。
|
|
93
97
|
|
|
94
98
|
参见 [CONTRIBUTING.md](CONTRIBUTING.md)、[RELEASING.md](RELEASING.md) 和 [SECURITY.md](SECURITY.md)。发布需单独批准,未启用自动 npm 发布。采用 [MIT](LICENSE) 许可证。
|
|
95
99
|
|
|
@@ -98,7 +102,7 @@ Python 只用作开发时的兼容性基准(v1.0.2,`47c23713d0a2a3c11ebde618
|
|
|
98
102
|
仅在官方发布以及注册表完整性、provenance 验证完成后,安装以下精确预览版本。它不是稳定版 `latest`。公开前继续使用上述本地 tarball 安装。
|
|
99
103
|
|
|
100
104
|
```sh
|
|
101
|
-
npm install --global --ignore-scripts session-peer@0.1.0-preview.
|
|
105
|
+
npm install --global --ignore-scripts session-peer@0.1.0-preview.1
|
|
102
106
|
session-peer --version
|
|
103
107
|
```
|
|
104
108
|
|
package/RELEASING.md
CHANGED
|
@@ -113,6 +113,21 @@ Prepare a new immutable preview version through a PR. Dispatch main with mode
|
|
|
113
113
|
already exist; published versions are refused. One `npm stage publish` runs
|
|
114
114
|
with OIDC and the preview tag.
|
|
115
115
|
|
|
116
|
+
### 0.1.0-preview.1 candidate
|
|
117
|
+
|
|
118
|
+
This candidate includes the merged TypeScript declaration, SSH preflight,
|
|
119
|
+
publication-verification and native Windows changes (#7–#10). Its publication
|
|
120
|
+
mode is `trusted-stage`; `bootstrap-token` is reserved for preview.0 and must
|
|
121
|
+
not be reused. The existing `latest` tag is retained. For this
|
|
122
|
+
exact candidate, preflight requires both `latest` and `preview` to point to
|
|
123
|
+
preview.0, confirms the registry has no `0.1.0-preview.1` version, and requires
|
|
124
|
+
the exact-main CI gate. No npm tag action is part of this release. After npm
|
|
125
|
+
stage approval, verification requires `preview` to point to preview.1 while
|
|
126
|
+
`latest` still points to preview.0. An unversioned install continues to select
|
|
127
|
+
preview.0; use the explicit `session-peer@0.1.0-preview.1` version for testing.
|
|
128
|
+
Other prerelease `latest` states remain blocked, including future candidates.
|
|
129
|
+
Do not infer publication from local packaging, a green PR, or an approved stage.
|
|
130
|
+
|
|
116
131
|
**A successful staged run is pending approval, not a public release.** Record its
|
|
117
132
|
stage ID from npm output. Review its tarball against the retained manifest and
|
|
118
133
|
approve with 2FA in npmjs.com or interactive `npm stage approve`. OIDC cannot
|
|
@@ -142,6 +157,22 @@ checks. Inspect registry/staged state and retained evidence first. Read-only
|
|
|
142
157
|
verification of the exact artifact may be repeated; publication may not.
|
|
143
158
|
A broken/partial public release requires a reviewed new version and approval.
|
|
144
159
|
|
|
160
|
+
### First publication propagation and tag state
|
|
161
|
+
|
|
162
|
+
The first public `0.1.0-preview.0` upload returned success, while its workflow
|
|
163
|
+
verification initially saw registry 404 responses. The exact tarball, provenance
|
|
164
|
+
and isolated install were later verified, but the run remains failed. Verification
|
|
165
|
+
now waits for up to 30 read attempts (two seconds between reads) before reporting
|
|
166
|
+
that a published version is still invisible. Never re-upload that version.
|
|
167
|
+
|
|
168
|
+
The registry also assigned both `preview` and `latest` to the initial preview
|
|
169
|
+
despite the explicit `--tag preview`. The preview.1 gate preserves this exact
|
|
170
|
+
legacy `latest` value and refuses any change to it; the exception does not apply
|
|
171
|
+
to later versions. Review the current tags and agree on a separate operator
|
|
172
|
+
action in npm before changing a tag; the workflow never does so. A verification
|
|
173
|
+
failure from the tag check does not mean the upload failed. Record upload,
|
|
174
|
+
registry propagation, tag state and install verification separately.
|
|
175
|
+
|
|
145
176
|
## Sources
|
|
146
177
|
|
|
147
178
|
- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/)
|
package/VALIDATION.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
This is preview evidence, not an npm release or a claim of complete Python parity.
|
|
4
4
|
Python reference: v1.0.2, commit `47c23713d0a2a3c11ebde6186afd8c43489b8b65`.
|
|
5
5
|
|
|
6
|
+
## 0.1.0-preview.1 candidate
|
|
7
|
+
|
|
8
|
+
After #7–#10 were merged, the versioned candidate passed the local macOS
|
|
9
|
+
arm64 Node 22 contract suite (23 passed, two Windows-only tests skipped),
|
|
10
|
+
TypeScript test type-check, package/declaration smoke test, four-README
|
|
11
|
+
repository checks and `npm audit` (zero vulnerabilities). The package smoke
|
|
12
|
+
test repeated packing with the same SHA-256 and verified isolated install and
|
|
13
|
+
uninstall. The new candidate was not sent to a live agent and was not published
|
|
14
|
+
by these checks. Windows Node 22/24 and macOS/Linux Node 22/24 must also pass
|
|
15
|
+
on the candidate PR and exact merged `main` commit before publication.
|
|
16
|
+
|
|
6
17
|
## Automated checks
|
|
7
18
|
|
|
8
19
|
- 14 tests pass locally on macOS arm64 / Node 22.14.0.
|
|
@@ -17,6 +28,30 @@ Python reference: v1.0.2, commit `47c23713d0a2a3c11ebde6186afd8c43489b8b65`.
|
|
|
17
28
|
|
|
18
29
|
## Real TUI and SSH checks
|
|
19
30
|
|
|
31
|
+
### Native Windows candidate — 2026-09-27 KST
|
|
32
|
+
|
|
33
|
+
An isolated prefix under the Windows user's Temp directory was installed from
|
|
34
|
+
this branch's local tarball. The existing Python CLI was not removed or
|
|
35
|
+
overwritten. Node 24.16.0 and npm 11.13.0 were present. Windows native
|
|
36
|
+
`list` found the live Claude inbox; Codex dry-run verified the saved thread,
|
|
37
|
+
kernel lock, single same-user `codex.exe` opener and stable start time. Mac
|
|
38
|
+
mini → Windows SSH preflight used an explicit Windows platform, remote `.cmd`
|
|
39
|
+
path and an already authenticated OpenSSH control socket; no host-key bypass
|
|
40
|
+
or new login was introduced.
|
|
41
|
+
|
|
42
|
+
One message per target was submitted from this candidate through Windows SSH.
|
|
43
|
+
The CLI returned Claude `posted` and Codex `queued`, with
|
|
44
|
+
`consumptionConfirmed:false`. The user separately confirmed exact response
|
|
45
|
+
lines in both Windows native TUIs:
|
|
46
|
+
|
|
47
|
+
- Claude: `ACK TS-WIN4-CLAUDE-20260927-A`
|
|
48
|
+
- Codex: `ACK TS-WIN4-CODEX-20260927-B`
|
|
49
|
+
|
|
50
|
+
These ACKs are user-observed responses, not direct terminal-pane capture by
|
|
51
|
+
the test runner. No retry was made. This is one-shot evidence, not a soak or
|
|
52
|
+
all-architecture qualification. Windows x64 was tested; Windows arm64 and
|
|
53
|
+
Node 22 remain untested in this live check. Python remains installed.
|
|
54
|
+
|
|
20
55
|
Dedicated, newly created Claude/Codex test TUIs only. The user approved trusting
|
|
21
56
|
the two otherwise empty test folders. Agent permissions were not changed.
|
|
22
57
|
Each route passed discovery and no-delivery dry-run before exactly one send.
|
|
@@ -69,7 +104,7 @@ package/release fixtures, not claimed as new live ACK evidence.
|
|
|
69
104
|
|
|
70
105
|
## Not covered / not released
|
|
71
106
|
|
|
72
|
-
Windows
|
|
107
|
+
Windows arm64, IPv6 literals, Relay/MCP/wake, full Python
|
|
73
108
|
CLI parity, automatic reverse-route detection, npm registry publishing and
|
|
74
109
|
provenance are outside this historical evidence. Publication preparation now
|
|
75
110
|
removes the private flag, but actual publishing still needs separate approval
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type Done } from './process.js';
|
|
3
|
+
type Options = {
|
|
4
|
+
command: 'list' | 'send';
|
|
5
|
+
values: Map<string, string>;
|
|
6
|
+
flags: Set<string>;
|
|
7
|
+
};
|
|
8
|
+
export declare function parse(args: string[]): Options;
|
|
9
|
+
export declare function sshPreflightFailure(result: Done, expected: string): string | undefined;
|
|
10
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
|
+
import { lstatSync } from 'node:fs';
|
|
3
4
|
import { claude, codex, Refusal } from './discovery.js';
|
|
4
5
|
import { executable, run, UnknownOutcome } from './process.js';
|
|
5
6
|
import { checkMessage, send } from './send.js';
|
|
@@ -18,7 +19,7 @@ export function parse(args) {
|
|
|
18
19
|
throw new Refusal('invalid_option');
|
|
19
20
|
flags.add(key);
|
|
20
21
|
}
|
|
21
|
-
else if (['--agent', '--codex-home', '--codex-bin', '--output-format', '--to', '--message', '-m', '--host', '--remote-bin', '--reply-address'].includes(key)) {
|
|
22
|
+
else if (['--agent', '--codex-home', '--codex-bin', '--output-format', '--to', '--message', '-m', '--host', '--remote-bin', '--remote-platform', '--ssh-control-path', '--reply-address'].includes(key)) {
|
|
22
23
|
const name = key === '-m' ? '--message' : key;
|
|
23
24
|
const value = split > 0 ? token.slice(split + 1) : args[++i];
|
|
24
25
|
if (value === undefined || values.has(name))
|
|
@@ -60,8 +61,23 @@ export function parse(args) {
|
|
|
60
61
|
}
|
|
61
62
|
if (values.has('--host'))
|
|
62
63
|
host(values.get('--host'));
|
|
64
|
+
if (values.has('--ssh-control-path')) {
|
|
65
|
+
if (!values.has('--host'))
|
|
66
|
+
throw new Refusal('inapplicable_option');
|
|
67
|
+
try {
|
|
68
|
+
if (!lstatSync(values.get('--ssh-control-path')).isSocket())
|
|
69
|
+
throw new Error('not_socket');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Refusal('invalid_ssh_control_path');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (values.has('--remote-platform') && (!values.has('--host') || !['posix', 'win32'].includes(values.get('--remote-platform'))))
|
|
76
|
+
throw new Refusal('invalid_remote_platform');
|
|
63
77
|
if (values.has('--remote-bin')) {
|
|
64
|
-
|
|
78
|
+
const binary = values.get('--remote-bin');
|
|
79
|
+
if (!values.has('--host') || (values.get('--remote-platform') === 'win32' ?
|
|
80
|
+
!/^[A-Za-z]:\\[^\r\n]+$/.test(binary) : !/^\/[\x20-\x7e]+$/.test(binary)))
|
|
65
81
|
throw new Refusal('invalid_remote_bin');
|
|
66
82
|
}
|
|
67
83
|
return { command, values, flags };
|
|
@@ -84,15 +100,46 @@ async function input() {
|
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
const quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
103
|
+
// Classify only the no-message version preflight. Never expose SSH stderr,
|
|
104
|
+
// which can contain user paths or agent output.
|
|
105
|
+
export function sshPreflightFailure(result, expected) {
|
|
106
|
+
if (result.interrupted)
|
|
107
|
+
return 'ssh_preflight_timeout';
|
|
108
|
+
if (!result.spawned)
|
|
109
|
+
return 'ssh_unavailable';
|
|
110
|
+
const detail = result.stderr;
|
|
111
|
+
if (/host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|no [^\n]*host key is known/i.test(detail))
|
|
112
|
+
return 'ssh_host_key_untrusted';
|
|
113
|
+
if (/permission denied \(|authentication failed|too many authentication failures|tailnet policy does not permit/i.test(detail))
|
|
114
|
+
return 'ssh_authentication_refused';
|
|
115
|
+
if (/could not resolve hostname|name or service not known|network is unreachable|no route to host|connection timed out|connection refused|operation timed out/i.test(detail))
|
|
116
|
+
return 'ssh_unreachable';
|
|
117
|
+
if (result.code === 127 && /not found|no such file|not recognized/i.test(detail))
|
|
118
|
+
return 'remote_cli_missing';
|
|
119
|
+
if (result.code === 0 && result.stdout.trim() !== expected)
|
|
120
|
+
return 'remote_version_mismatch';
|
|
121
|
+
if (result.code !== 0)
|
|
122
|
+
return 'ssh_preflight_failed';
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
87
125
|
async function remote(options, message) {
|
|
88
126
|
const { values, flags, command } = options;
|
|
89
127
|
const target = host(values.get('--host'));
|
|
90
128
|
const binary = values.get('--remote-bin') ?? 'session-peer';
|
|
129
|
+
const windows = values.get('--remote-platform') === 'win32';
|
|
130
|
+
const invoke = (flag) => {
|
|
131
|
+
if (!windows)
|
|
132
|
+
return `${quote(binary)} ${flag}`;
|
|
133
|
+
const code = `& '${binary.replace(/'/g, "''")}' ${flag}`;
|
|
134
|
+
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(code, 'utf16le').toString('base64')}`;
|
|
135
|
+
};
|
|
91
136
|
const ssh = executable('ssh');
|
|
92
|
-
const base = ['-T', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-o', 'ConnectTimeout=10',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
const base = ['-T', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes', '-o', 'ConnectTimeout=10',
|
|
138
|
+
...(values.has('--ssh-control-path') ? ['-S', values.get('--ssh-control-path')] : []), '--', target];
|
|
139
|
+
const preflight = await run(ssh, [...base, invoke('--version')], { timeout: 15000 });
|
|
140
|
+
const preflightError = sshPreflightFailure(preflight, VERSION_LINE);
|
|
141
|
+
if (preflightError)
|
|
142
|
+
throw new Refusal(preflightError, 1);
|
|
96
143
|
const args = [command, '--json'];
|
|
97
144
|
for (const [key, value] of values)
|
|
98
145
|
if (['--agent', '--codex-home', '--codex-bin', '--to'].includes(key))
|
|
@@ -103,7 +150,7 @@ async function remote(options, message) {
|
|
|
103
150
|
args.push('--dry-run');
|
|
104
151
|
if (command === 'send')
|
|
105
152
|
args.push('--message', message, '--no-from', '--no-reply-to');
|
|
106
|
-
const done = await run(ssh, [...base,
|
|
153
|
+
const done = await run(ssh, [...base, invoke('--stdio-request')], {
|
|
107
154
|
timeout: 90000, input: JSON.stringify({ schemaVersion: 1, args })
|
|
108
155
|
});
|
|
109
156
|
const uncertain = () => { if (command === 'send' && !flags.has('--dry-run') && done.spawned)
|
|
@@ -132,13 +179,13 @@ try {
|
|
|
132
179
|
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
133
180
|
if (!((major === 22 && minor >= 13) || major === 24))
|
|
134
181
|
throw new Refusal('unsupported_node_version');
|
|
135
|
-
if (!['darwin', 'linux'].includes(process.platform))
|
|
182
|
+
if (!['darwin', 'linux', 'win32'].includes(process.platform))
|
|
136
183
|
throw new Refusal('unsupported_platform');
|
|
137
184
|
let args = process.argv.slice(2);
|
|
138
185
|
if (args.length === 1 && args[0] === '--version')
|
|
139
186
|
console.log(VERSION_LINE);
|
|
140
187
|
else if (args.length === 1 && ['--help', '-h'].includes(args[0]))
|
|
141
|
-
console.log('session-peer (TypeScript preview): list --agent claude|codex --json; send --to TARGET --message TEXT --json [--dry-run] [--host HOST] [--remote-bin ABSOLUTE_PATH]. Codex requires --codex-home. Relay/MCP/wake
|
|
188
|
+
console.log('session-peer (TypeScript preview): list --agent claude|codex --json; send --to TARGET --message TEXT --json [--dry-run] [--host HOST] [--remote-bin ABSOLUTE_PATH]. Codex requires --codex-home. Relay/MCP/wake unsupported.');
|
|
142
189
|
else {
|
|
143
190
|
const wire = args.length === 1 && args[0] === '--stdio-request';
|
|
144
191
|
if (wire) {
|
|
@@ -156,7 +203,7 @@ try {
|
|
|
156
203
|
}
|
|
157
204
|
command = ['list', 'send'].includes(args[0] ?? '') ? args[0] : 'unknown';
|
|
158
205
|
const options = parse(args);
|
|
159
|
-
if (wire && (options.values.has('--host') || options.values.has('--remote-bin')))
|
|
206
|
+
if (wire && (options.values.has('--host') || options.values.has('--remote-bin') || options.values.has('--remote-platform') || options.values.has('--ssh-control-path')))
|
|
160
207
|
throw new Refusal('nested_transport_forbidden');
|
|
161
208
|
let message;
|
|
162
209
|
if (command === 'send') {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Row = Record<string, unknown>;
|
|
2
|
+
export declare class Refusal extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
constructor(code: string, exitCode?: number);
|
|
6
|
+
}
|
|
7
|
+
export declare function canonical(path: string): string;
|
|
8
|
+
export declare function claude(all: boolean): Row;
|
|
9
|
+
export declare function codex(path: string, all: boolean): Promise<Row>;
|
|
10
|
+
export {};
|
package/dist/discovery.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { windowsProcessStart } from './windows.js';
|
|
5
6
|
export class Refusal extends Error {
|
|
6
7
|
code;
|
|
7
8
|
exitCode;
|
|
@@ -33,6 +34,8 @@ export function canonical(path) {
|
|
|
33
34
|
function alive(pid) {
|
|
34
35
|
if (pid <= 1)
|
|
35
36
|
return false;
|
|
37
|
+
if (process.platform === 'win32')
|
|
38
|
+
return windowsProcessStart(pid) !== undefined;
|
|
36
39
|
try {
|
|
37
40
|
process.kill(pid, 0);
|
|
38
41
|
return true;
|
|
@@ -71,20 +74,31 @@ export function claude(all) {
|
|
|
71
74
|
if (typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid > 2147483647)
|
|
72
75
|
continue;
|
|
73
76
|
const matches = pid === Number(entry.slice(0, -5));
|
|
74
|
-
|
|
77
|
+
let live = matches && alive(pid);
|
|
78
|
+
if (live && process.platform === 'win32') {
|
|
79
|
+
const started = typeof record.startedAt === 'number' && Number.isSafeInteger(record.startedAt) && record.startedAt > 0 ? record.startedAt : NaN;
|
|
80
|
+
const ticks = windowsProcessStart(pid);
|
|
81
|
+
const actual = ticks === undefined ? NaN : Number((ticks - 116444736000000000n) / 10000n);
|
|
82
|
+
live = Number.isFinite(started) && Number.isFinite(actual) && actual <= started + 2000;
|
|
83
|
+
}
|
|
75
84
|
const socket = typeof record.messagingSocketPath === 'string' ? record.messagingSocketPath : '';
|
|
76
85
|
let inbox = false;
|
|
77
86
|
if (socket) {
|
|
78
|
-
|
|
79
|
-
inbox =
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
if (process.platform === 'win32')
|
|
88
|
+
inbox = /^\\\\\.\\pipe\\[^\r\n]+$/i.test(socket);
|
|
89
|
+
else
|
|
90
|
+
try {
|
|
91
|
+
inbox = statSync(socket).isSocket();
|
|
92
|
+
}
|
|
93
|
+
catch { /* Inaccessible is not reachable. */ }
|
|
82
94
|
}
|
|
83
95
|
const row = { agent: 'claude', pid, socket, alive: live, reachable: live && inbox };
|
|
84
96
|
for (const key of ['name', 'status', 'cwd', 'kind', 'version', 'tmux'])
|
|
85
97
|
row[key] = record[key] ?? null;
|
|
86
98
|
if (!matches)
|
|
87
99
|
row.staleReason = 'record_pid_mismatch';
|
|
100
|
+
else if (!live && process.platform === 'win32')
|
|
101
|
+
row.staleReason = 'process_unverified';
|
|
88
102
|
if (all || row.reachable)
|
|
89
103
|
sessions.push(row);
|
|
90
104
|
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VERSION, VERSION_LINE, reply, envelope } from './protocol.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class UnknownOutcome extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare function executable(name: string): string;
|
|
4
|
+
export type Done = {
|
|
5
|
+
code: number | null;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
spawned: boolean;
|
|
9
|
+
interrupted: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function run(binary: string, args: string[], options?: {
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
input?: string;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}): Promise<Done>;
|
package/dist/process.js
CHANGED
|
@@ -5,8 +5,10 @@ import { Refusal } from './discovery.js';
|
|
|
5
5
|
export class UnknownOutcome extends Error {
|
|
6
6
|
}
|
|
7
7
|
export function executable(name) {
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const windows = process.platform === 'win32';
|
|
9
|
+
const names = windows && !/\.(?:exe|cmd|bat)$/i.test(name) ? [name + '.exe', name + '.cmd', name] : [name];
|
|
10
|
+
const paths = isAbsolute(name) ? [name] : name.includes('/') || name.includes('\\') ? [] :
|
|
11
|
+
(process.env.PATH ?? '').split(delimiter).filter(Boolean).flatMap(p => names.map(n => join(p, n)));
|
|
10
12
|
for (const path of paths) {
|
|
11
13
|
try {
|
|
12
14
|
accessSync(path, constants.X_OK);
|
|
@@ -19,7 +21,7 @@ export function executable(name) {
|
|
|
19
21
|
}
|
|
20
22
|
export function run(binary, args, options = {}) {
|
|
21
23
|
return new Promise(resolve => {
|
|
22
|
-
let stdout = '', bytes = 0, spawned = false, interrupted = false;
|
|
24
|
+
let stdout = '', stderr = '', bytes = 0, spawned = false, interrupted = false;
|
|
23
25
|
const child = spawn(binary, args, { shell: false, env: options.env ?? process.env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
24
26
|
const stop = () => { interrupted = true; child.kill('SIGKILL'); };
|
|
25
27
|
const timer = setTimeout(stop, options.timeout ?? 3000);
|
|
@@ -34,10 +36,16 @@ export function run(binary, args, options = {}) {
|
|
|
34
36
|
stdout += part;
|
|
35
37
|
});
|
|
36
38
|
// Drain but never report native stderr (it may contain message text/credentials).
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
// Retain stderr only for allowlisted error classification. Never include it in CLI output.
|
|
40
|
+
child.stderr.on('data', (part) => {
|
|
41
|
+
bytes += part.length;
|
|
42
|
+
if (stderr.length < 4096)
|
|
43
|
+
stderr += part.toString('utf8').slice(0, 4096 - stderr.length);
|
|
44
|
+
if (bytes > (options.limit ?? 1024 * 1024))
|
|
45
|
+
stop();
|
|
46
|
+
});
|
|
39
47
|
child.once('error', () => { interrupted = true; });
|
|
40
|
-
child.once('close', code => { clearTimeout(timer); resolve({ code, stdout, spawned, interrupted }); });
|
|
48
|
+
child.once('close', code => { clearTimeout(timer); resolve({ code, stdout, stderr, spawned, interrupted }); });
|
|
41
49
|
child.stdin.end(options.input);
|
|
42
50
|
});
|
|
43
51
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const VERSION = "0.1.0-preview.1";
|
|
2
|
+
export declare const VERSION_LINE = "session-peer 0.1.0-preview.1 (typescript)";
|
|
3
|
+
export declare function host(value: string): string;
|
|
4
|
+
export declare function reply(value: string): {
|
|
5
|
+
to: string;
|
|
6
|
+
host?: string;
|
|
7
|
+
home?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function envelope(text: string, noFrom: boolean, address?: string): string;
|
package/dist/protocol.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Refusal } from './discovery.js';
|
|
2
2
|
import { uuid } from './writer.js';
|
|
3
|
-
export const VERSION = '0.1.0-preview.
|
|
3
|
+
export const VERSION = '0.1.0-preview.1';
|
|
4
4
|
export const VERSION_LINE = `session-peer ${VERSION} (typescript)`;
|
|
5
5
|
export function host(value) {
|
|
6
6
|
if (value.length > 255 || !/^(?:[A-Za-z0-9_][A-Za-z0-9_.-]*@)?[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value))
|
package/dist/send.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SendOptions = {
|
|
2
|
+
to: string;
|
|
3
|
+
home?: string;
|
|
4
|
+
codexBin?: string;
|
|
5
|
+
message: string;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function checkMessage(text: string, codex?: boolean): void;
|
|
9
|
+
export declare function postSocket(path: string, text: string): Promise<void>;
|
|
10
|
+
export declare function send(options: SendOptions): Promise<Record<string, unknown>>;
|
package/dist/send.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createConnection } from 'node:net';
|
|
|
2
2
|
import { canonical, claude, Refusal } from './discovery.js';
|
|
3
3
|
import { executable, run, UnknownOutcome } from './process.js';
|
|
4
4
|
import { resolveWriter, uuid } from './writer.js';
|
|
5
|
+
import { postWindowsPipe } from './windows.js';
|
|
5
6
|
export function checkMessage(text, codex = false) {
|
|
6
7
|
if (!text.trim() || text.includes('\0') || [...text].length > 1_000_000 ||
|
|
7
8
|
(codex && Buffer.byteLength(text, 'utf8') > 32768))
|
|
@@ -83,6 +84,9 @@ export async function send(options) {
|
|
|
83
84
|
const rechecked = select();
|
|
84
85
|
if (row.pid !== rechecked.pid || row.socket !== rechecked.socket)
|
|
85
86
|
throw new Refusal('target_changed', 1);
|
|
86
|
-
|
|
87
|
+
if (process.platform === 'win32')
|
|
88
|
+
await postWindowsPipe(String(row.socket), Number(row.pid), options.message);
|
|
89
|
+
else
|
|
90
|
+
await postSocket(String(row.socket), options.message);
|
|
87
91
|
return result;
|
|
88
92
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type WindowsProcess = {
|
|
2
|
+
pid: number;
|
|
3
|
+
uid: string;
|
|
4
|
+
command: string;
|
|
5
|
+
start: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function inspectWindows(mode: 'identity' | 'openers', value: string): WindowsProcess[];
|
|
8
|
+
export declare function windowsProcessStart(pid: number): bigint | undefined;
|
|
9
|
+
export declare function postWindowsPipe(pipe: string, pid: number, text: string): Promise<void>;
|
package/dist/windows.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createConnection } from 'node:net';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { Refusal } from './discovery.js';
|
|
6
|
+
import { UnknownOutcome } from './process.js';
|
|
7
|
+
// Get-CimInstance and Restart Manager are Windows read-only APIs. The script
|
|
8
|
+
// emits only PID, SID, executable basename and creation time. No arguments,
|
|
9
|
+
// command line, message body, token or file contents are returned.
|
|
10
|
+
const inspectScript = String.raw `
|
|
11
|
+
$ErrorActionPreference = 'Stop'
|
|
12
|
+
Add-Type -TypeDefinition @'
|
|
13
|
+
using System;
|
|
14
|
+
using System.Text;
|
|
15
|
+
using System.Runtime.InteropServices;
|
|
16
|
+
public static class Reader {
|
|
17
|
+
[StructLayout(LayoutKind.Sequential)] public struct Time { public uint Lo; public uint Hi; }
|
|
18
|
+
[StructLayout(LayoutKind.Sequential)] public struct Unique { public uint Pid; public Time Start; }
|
|
19
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
20
|
+
public struct Info {
|
|
21
|
+
public Unique Process;
|
|
22
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public string App;
|
|
23
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)] public string Service;
|
|
24
|
+
public uint Kind; public uint Status; public uint Session;
|
|
25
|
+
[MarshalAs(UnmanagedType.Bool)] public bool Restartable;
|
|
26
|
+
}
|
|
27
|
+
[DllImport("Rstrtmgr.dll", CharSet=CharSet.Unicode)] public static extern int RmStartSession(out uint session, uint flags, StringBuilder key);
|
|
28
|
+
[DllImport("Rstrtmgr.dll", CharSet=CharSet.Unicode)] public static extern int RmRegisterResources(uint session, uint files, string[] paths, uint apps, IntPtr app, uint services, IntPtr svc);
|
|
29
|
+
[DllImport("Rstrtmgr.dll")] public static extern int RmGetList(uint session, out uint needed, ref uint count, [In,Out] Info[] info, ref uint reasons);
|
|
30
|
+
[DllImport("Rstrtmgr.dll")] public static extern int RmEndSession(uint session);
|
|
31
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
|
|
32
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool GetProcessTimes(IntPtr handle, out Time creation, out Time exit, out Time kernel, out Time user);
|
|
33
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
34
|
+
public static ulong Start(uint pid) {
|
|
35
|
+
var handle = OpenProcess(0x1000, false, pid);
|
|
36
|
+
if (handle == IntPtr.Zero) throw new Exception("process_unavailable");
|
|
37
|
+
try {
|
|
38
|
+
Time created, exited, kernel, user;
|
|
39
|
+
if (!GetProcessTimes(handle, out created, out exited, out kernel, out user)) throw new Exception("process_unavailable");
|
|
40
|
+
return ((ulong)created.Hi << 32) | created.Lo;
|
|
41
|
+
} finally { CloseHandle(handle); }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
'@
|
|
45
|
+
function Identity([uint32]$identifier, [string]$expected = '') {
|
|
46
|
+
$item = Get-CimInstance Win32_Process -Filter "ProcessId = $identifier"
|
|
47
|
+
if (!$item) { throw 'process_unavailable' }
|
|
48
|
+
$start = [string][Reader]::Start($identifier)
|
|
49
|
+
if ($expected -and $start -ne $expected) { throw 'process_replaced' }
|
|
50
|
+
$owner = Invoke-CimMethod -InputObject $item -MethodName GetOwnerSid
|
|
51
|
+
if (!$owner.Sid) { throw 'owner_unavailable' }
|
|
52
|
+
return @{pid=[int]$identifier; uid=[string]$owner.Sid; command=[string]$item.Name; start=$start}
|
|
53
|
+
}
|
|
54
|
+
function Openers([string]$location) {
|
|
55
|
+
[uint32]$session = 0
|
|
56
|
+
$key = New-Object System.Text.StringBuilder 33
|
|
57
|
+
if ([Reader]::RmStartSession([ref]$session, 0, $key) -ne 0) { throw 'rm_start_failed' }
|
|
58
|
+
try {
|
|
59
|
+
$paths = [string[]]@($location)
|
|
60
|
+
if ([Reader]::RmRegisterResources($session, 1, $paths, 0, [IntPtr]::Zero, 0, [IntPtr]::Zero) -ne 0) { throw 'rm_register_failed' }
|
|
61
|
+
[uint32]$needed = 0; [uint32]$count = 128; [uint32]$reasons = 0
|
|
62
|
+
$rows = New-Object 'Reader+Info[]' 128
|
|
63
|
+
if ([Reader]::RmGetList($session, [ref]$needed, [ref]$count, $rows, [ref]$reasons) -ne 0 -or $count -gt 128) { throw 'rm_list_failed' }
|
|
64
|
+
$result = @()
|
|
65
|
+
for ($i=0; $i -lt $count; $i++) {
|
|
66
|
+
$ticks = ([uint64]$rows[$i].Process.Start.Hi -shl 32) -bor $rows[$i].Process.Start.Lo
|
|
67
|
+
$result += Identity $rows[$i].Process.Pid ([string]$ticks)
|
|
68
|
+
}
|
|
69
|
+
return ,$result
|
|
70
|
+
} finally { [void][Reader]::RmEndSession($session) }
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
export function inspectWindows(mode, value) {
|
|
74
|
+
if (process.platform !== 'win32')
|
|
75
|
+
throw new Refusal('windows_inspection_unavailable', 1);
|
|
76
|
+
const encodedValue = Buffer.from(value, 'utf8').toString('base64');
|
|
77
|
+
const code = inspectScript + `
|
|
78
|
+
$value = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedValue}'))
|
|
79
|
+
try {
|
|
80
|
+
$result = if ('${mode}' -eq 'identity') { @(Identity ([uint32]$value)) } else { @(Openers $value) }
|
|
81
|
+
ConvertTo-Json -InputObject @($result) -Compress -Depth 4
|
|
82
|
+
} catch { exit 1 }
|
|
83
|
+
`;
|
|
84
|
+
const done = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
85
|
+
'$script = [Console]::In.ReadToEnd(); Invoke-Expression $script'], { input: code, encoding: 'utf8', timeout: 8000, maxBuffer: 65536, windowsHide: true });
|
|
86
|
+
if (done.status !== 0 || done.error)
|
|
87
|
+
throw new Refusal('windows_owner_inspection_failed', 1);
|
|
88
|
+
let rows;
|
|
89
|
+
try {
|
|
90
|
+
rows = JSON.parse(done.stdout);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Refusal('windows_owner_inspection_failed', 1);
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(rows) || rows.length > 128 || rows.some(row => !row || !Number.isSafeInteger(row.pid) || row.pid <= 0 ||
|
|
96
|
+
typeof row.uid !== 'string' || !row.uid.startsWith('S-') ||
|
|
97
|
+
typeof row.command !== 'string' || !row.command ||
|
|
98
|
+
typeof row.start !== 'string' || !/^\d+$/.test(row.start))) {
|
|
99
|
+
throw new Refusal('windows_owner_inspection_failed', 1);
|
|
100
|
+
}
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
export function windowsProcessStart(pid) {
|
|
104
|
+
try {
|
|
105
|
+
const rows = inspectWindows('identity', String(pid));
|
|
106
|
+
return rows.length === 1 ? BigInt(rows[0].start) : undefined;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function authLine(pid) {
|
|
113
|
+
const directory = join(process.env.CLAUDE_CONFIG_DIR || process.env.ANTHROPIC_CONFIG_DIR ||
|
|
114
|
+
join(process.env.USERPROFILE || '', '.claude'), 'sessions');
|
|
115
|
+
let names;
|
|
116
|
+
try {
|
|
117
|
+
names = readdirSync(directory).filter(name => new RegExp(`^${pid}\\.[^.]+\\.key$`).test(name));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Refusal('windows_inbox_auth_unavailable', 1);
|
|
121
|
+
}
|
|
122
|
+
if (names.length !== 1)
|
|
123
|
+
throw new Refusal('windows_inbox_auth_unavailable', 1);
|
|
124
|
+
const file = join(directory, names[0]);
|
|
125
|
+
try {
|
|
126
|
+
const entry = lstatSync(file);
|
|
127
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 16384)
|
|
128
|
+
throw new Error('invalid_key');
|
|
129
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
130
|
+
const token = value?.peerToken;
|
|
131
|
+
if (typeof token !== 'string' || !token.trim())
|
|
132
|
+
throw new Error('invalid_key');
|
|
133
|
+
return JSON.stringify({ type: 'auth', token }) + '\n';
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Refusal('windows_inbox_auth_unavailable', 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function postWindowsPipe(pipe, pid, text) {
|
|
140
|
+
if (!/^\\\\\.\\pipe\\[^\r\n]+$/i.test(pipe))
|
|
141
|
+
throw new Refusal('invalid_windows_pipe', 1);
|
|
142
|
+
const auth = authLine(pid);
|
|
143
|
+
const payload = auth + JSON.stringify({ type: 'user', message: { role: 'user', content: text } }) + '\n';
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let connected = false, finished = false;
|
|
146
|
+
const socket = createConnection(pipe);
|
|
147
|
+
const timeout = setTimeout(() => finish(new Error('timeout')), 10000);
|
|
148
|
+
const finish = (error) => {
|
|
149
|
+
if (finished)
|
|
150
|
+
return;
|
|
151
|
+
finished = true;
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
socket.destroy();
|
|
154
|
+
if (error)
|
|
155
|
+
reject(connected ? new UnknownOutcome() : new Refusal('inbox_unreachable', 1));
|
|
156
|
+
else
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
socket.once('error', error => finish(error));
|
|
160
|
+
socket.once('connect', () => {
|
|
161
|
+
connected = true;
|
|
162
|
+
socket.write(payload, error => {
|
|
163
|
+
if (error)
|
|
164
|
+
return finish(error);
|
|
165
|
+
const drain = setTimeout(() => finish(), 2000);
|
|
166
|
+
socket.once('close', () => { clearTimeout(drain); finish(); });
|
|
167
|
+
socket.once('data', () => { clearTimeout(drain); finish(); });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const uuid: (value: string) => boolean;
|
|
2
|
+
export declare function probeLock(path: string): Promise<'free' | 'held' | 'absent' | 'unknown'>;
|
|
3
|
+
export declare function inspectWriter(home: string, id: string): Promise<{
|
|
4
|
+
activity: string;
|
|
5
|
+
identity: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function homes(selected: string): string[];
|
|
8
|
+
export declare function resolveWriter(selected: string, id: string): Promise<string>;
|
package/dist/writer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isAbsolute, join } from 'node:path';
|
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
5
|
import { canonical, Refusal } from './discovery.js';
|
|
6
6
|
import { executable, run } from './process.js';
|
|
7
|
+
import { inspectWindows } from './windows.js';
|
|
7
8
|
const missing = (error) => ['ENOENT', 'ENOTDIR'].includes(error.code ?? '');
|
|
8
9
|
export const uuid = (value) => /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
|
|
9
10
|
function snapshot(path) {
|
|
@@ -16,18 +17,30 @@ export async function probeLock(path) {
|
|
|
16
17
|
const before = lstatSync(path);
|
|
17
18
|
if (!before.isFile() || before.isSymbolicLink())
|
|
18
19
|
return 'unknown';
|
|
19
|
-
const
|
|
20
|
-
fd = openSync(path, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
20
|
+
const locks = await import('fs-ext-extra-prebuilt');
|
|
21
|
+
fd = openSync(path, constants.O_RDWR | (process.platform === 'win32' ? 0 : constants.O_NOFOLLOW));
|
|
21
22
|
const after = fstatSync(fd);
|
|
22
23
|
if (before.dev !== after.dev || before.ino !== after.ino)
|
|
23
24
|
return 'unknown';
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
try {
|
|
27
|
+
locks.lockFileExSync(fd, locks.constants.LOCKFILE_EXCLUSIVE_LOCK | locks.constants.LOCKFILE_FAIL_IMMEDIATELY, 0, 0, 0xffffffff, 0xffffffff);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return ['EAGAIN', 'EACCES', 'EWOULDBLOCK'].includes(error.code ?? '') ||
|
|
31
|
+
error.errno === 33 ? 'held' : 'unknown';
|
|
32
|
+
}
|
|
33
|
+
locks.unlockFileExSync(fd, 0, 0, 0xffffffff, 0xffffffff);
|
|
26
34
|
}
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
else {
|
|
36
|
+
try {
|
|
37
|
+
locks.flockSync(fd, 'exnb');
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return ['EAGAIN', 'EACCES', 'EWOULDBLOCK'].includes(error.code ?? '') ? 'held' : 'unknown';
|
|
41
|
+
}
|
|
42
|
+
locks.flockSync(fd, 'un');
|
|
29
43
|
}
|
|
30
|
-
flockSync(fd, 'un');
|
|
31
44
|
return 'free';
|
|
32
45
|
}
|
|
33
46
|
catch (error) {
|
|
@@ -43,6 +56,10 @@ async function sample(path) {
|
|
|
43
56
|
if (state !== 'held')
|
|
44
57
|
return { state, fingerprint: '', owners: [], valid: true };
|
|
45
58
|
const fingerprint = snapshot(path);
|
|
59
|
+
if (process.platform === 'win32') {
|
|
60
|
+
const owners = inspectWindows('openers', path);
|
|
61
|
+
return { state, fingerprint, owners, valid: true };
|
|
62
|
+
}
|
|
46
63
|
const listed = await run(executable('lsof'), ['-nP', '-F0pcu', '--', path]);
|
|
47
64
|
const owners = [];
|
|
48
65
|
let current;
|
|
@@ -76,9 +93,10 @@ export async function inspectWriter(home, id) {
|
|
|
76
93
|
await delay(250);
|
|
77
94
|
const after = await sample(path);
|
|
78
95
|
const owner = after.owners[0];
|
|
96
|
+
const ownId = process.platform === 'win32' ? inspectWindows('identity', String(process.pid))[0]?.uid : process.getuid?.();
|
|
79
97
|
if (!before.valid || !after.valid || before.state !== after.state || before.fingerprint !== after.fingerprint ||
|
|
80
98
|
before.owners.length !== 1 || after.owners.length !== 1 || JSON.stringify(before.owners) !== JSON.stringify(after.owners) ||
|
|
81
|
-
!owner?.start || owner.uid !==
|
|
99
|
+
!owner?.start || !ownId || owner.uid !== ownId || !/^codex(?:\.exe|-|$)/i.test(owner.command ?? '')) {
|
|
82
100
|
throw new Refusal('active_writer_unverified', 1);
|
|
83
101
|
}
|
|
84
102
|
return { activity: 'live_writer', identity: JSON.stringify({ fingerprint: after.fingerprint, ...owner }) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-peer",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
"tag": "preview"
|
|
9
9
|
},
|
|
10
10
|
"repository": { "type": "git", "url": "git+https://github.com/abruption/session-peer-ts.git" },
|
|
11
|
-
"os": ["darwin", "linux"],
|
|
11
|
+
"os": ["darwin", "linux", "win32"],
|
|
12
12
|
"description": "Python-free local and SSH client for Claude Code and Codex sessions (preview)",
|
|
13
13
|
"type": "module",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
|
|
17
|
+
},
|
|
14
18
|
"license": "MIT",
|
|
15
19
|
"engines": {
|
|
16
20
|
"node": "^22.13.0 || ^24.0.0"
|
|
@@ -31,8 +35,10 @@
|
|
|
31
35
|
],
|
|
32
36
|
"scripts": {
|
|
33
37
|
"build": "tsc -p tsconfig.json",
|
|
34
|
-
"test": "npm run build && node --test test/*.test.
|
|
35
|
-
"test:
|
|
38
|
+
"test": "npm run build && npm run test:types && node --experimental-strip-types --test test/*.test.ts",
|
|
39
|
+
"test:types": "tsc -p tsconfig.test.json",
|
|
40
|
+
"test:windows": "npm run build && npm run test:types && node --experimental-strip-types --test test/windows.test.ts",
|
|
41
|
+
"test:package": "npm run build && node --experimental-strip-types test/package-smoke.ts"
|
|
36
42
|
},
|
|
37
43
|
"devDependencies": {
|
|
38
44
|
"@types/node": "22.13.10",
|