session-peer 0.1.0-preview.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +9 -0
- package/README.ja.md +21 -11
- package/README.ko.md +44 -20
- package/README.md +50 -22
- package/README.zh-CN.md +21 -11
- package/RELEASING.md +103 -138
- package/VALIDATION.md +70 -7
- 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 +12 -6
package/RELEASING.md
CHANGED
|
@@ -1,153 +1,118 @@
|
|
|
1
|
-
# Releasing session-peer
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
After environment approval, a fresh job downloads the artifact without rebuilding,
|
|
73
|
-
rechecks main/CI/registry and validates its contents. Only one `npm publish`
|
|
74
|
-
step receives NPM_TOKEN (`--tag preview --access public --provenance
|
|
75
|
-
--ignore-scripts`). OIDC permission is also granted for provenance. Upload
|
|
76
|
-
network retries are disabled. Do not rerun failed or uncertain publication.
|
|
77
|
-
|
|
78
|
-
Verification compares registry integrity and preview tag, rejects accidental
|
|
79
|
-
latest promotion, requires registry attestation metadata, runs `npm audit
|
|
80
|
-
signatures`, and fresh-installs/uninstalls the published version. Only a passed
|
|
81
|
-
verification completes bootstrap release; a successful upload alone does not.
|
|
82
|
-
Bounded read-only propagation checks are not upload retries.
|
|
83
|
-
|
|
84
|
-
## Register npm Trusted Publisher after the first release
|
|
85
|
-
|
|
86
|
-
npmjs.com → package **session-peer** → Settings → Trusted publishing:
|
|
87
|
-
|
|
88
|
-
| Field | Value |
|
|
89
|
-
| --- | --- |
|
|
90
|
-
| Provider | GitHub Actions |
|
|
91
|
-
| Organization or user | `abruption` |
|
|
92
|
-
| Repository | `session-peer-ts` |
|
|
93
|
-
| Workflow filename | `publish.yml` (no directory prefix) |
|
|
94
|
-
| Environment name | `npm` |
|
|
95
|
-
| Allowed actions | Stage-only: `npm stage publish`, not direct `npm publish` |
|
|
96
|
-
|
|
97
|
-
The workflow must exist on main. npm does not validate the connection on save;
|
|
98
|
-
validate it with the next authorized staged version. GitHub-hosted runners are
|
|
99
|
-
required. Public GitHub + public npm OIDC publishing provides provenance.
|
|
100
|
-
|
|
101
|
-
After verifying bootstrap and configuring trust, revoke its token in npm and
|
|
102
|
-
remove GitHub NPM_TOKEN. Set npm publishing access to **Require two-factor
|
|
103
|
-
authentication and disallow tokens**. These account changes need owner approval
|
|
104
|
-
and are not performed by this workflow. trusted-stage never references NPM_TOKEN;
|
|
105
|
-
an OIDC failure must not cause token fallback. Remove the bootstrap branch in a
|
|
106
|
-
follow-up PR.
|
|
107
|
-
|
|
108
|
-
## Later previews: trusted-stage
|
|
109
|
-
|
|
110
|
-
Prepare a new immutable preview version through a PR. Dispatch main with mode
|
|
111
|
-
`trusted-stage`, its exact version and confirmation
|
|
112
|
-
`session-peer@<version> trusted-stage`. The same gates apply. The package must
|
|
113
|
-
already exist; published versions are refused. One `npm stage publish` runs
|
|
114
|
-
with OIDC and the preview tag.
|
|
115
|
-
|
|
116
|
-
**A successful staged run is pending approval, not a public release.** Record its
|
|
117
|
-
stage ID from npm output. Review its tarball against the retained manifest and
|
|
118
|
-
approve with 2FA in npmjs.com or interactive `npm stage approve`. OIDC cannot
|
|
119
|
-
approve, list or view stages. Public registry metadata cannot reveal pending
|
|
120
|
-
stages, so collisions fail at the registry and need manual inspection, not retries.
|
|
121
|
-
|
|
122
|
-
After approval, check out the exact source commit and download that run artifact
|
|
123
|
-
into `release-dist/`. With Node 24 and npm 11.15.0:
|
|
1
|
+
# Releasing session-peer
|
|
2
|
+
|
|
3
|
+
Publication is manual. Merging, pushing, tagging or creating a GitHub release
|
|
4
|
+
never dispatches `publish.yml`. Python session-peer and its Relay release train
|
|
5
|
+
remain independent. The npm Trusted Publisher for `abruption/session-peer-ts`,
|
|
6
|
+
`publish.yml`, environment `npm` allows `npm stage publish` only. The workflow
|
|
7
|
+
uses OIDC and no npm token. The protected GitHub environment requires a reviewer
|
|
8
|
+
and permits deployments from `main` only.
|
|
9
|
+
|
|
10
|
+
## Verified preview and stable baseline — 2026-09-27
|
|
11
|
+
|
|
12
|
+
`session-peer@0.1.0-preview.1` was staged from main commit
|
|
13
|
+
`f65d3ee99cc26ff4a267aef84e31a308d034ac80` in
|
|
14
|
+
[run 36313884100](https://github.com/abruption/session-peer-ts/actions/runs/36313884100)
|
|
15
|
+
and published after a separate 2FA approval. The staged tarball matched the
|
|
16
|
+
retained run artifact byte for byte. Public verification passed registry
|
|
17
|
+
integrity, provenance metadata, `npm audit signatures`, fresh installation and
|
|
18
|
+
uninstallation. The artifact SHA-256 was
|
|
19
|
+
`d154beac93893b60b0695e5a6296e185beb8dbd05188ffe04befb74d29966110`;
|
|
20
|
+
registry integrity is
|
|
21
|
+
`sha512-h4SMvrQ/LWA9osd4EHIs9rSTqv1u+S3MXQAmn+yG/gZ9+7NwYMutq+Oa5K/0ggIq61Wfwdh11Cv+5YdEffiNMA==`.
|
|
22
|
+
The `preview` tag points to preview.1. The existing `latest` tag still points
|
|
23
|
+
to preview.0. No stable version has been published by this preparation PR.
|
|
24
|
+
|
|
25
|
+
## Prepare 0.1.0 stable
|
|
26
|
+
|
|
27
|
+
1. Merge the reviewed 0.1.0 preparation PR. Require successful `cli` CI on the
|
|
28
|
+
exact merged `main` commit, including the release gate and Windows Node 22/24.
|
|
29
|
+
Match `0.1.0` in package.json, lockfile, src/protocol.ts and all four READMEs.
|
|
30
|
+
Review [VALIDATION.md](VALIDATION.md): fixture CI and the historical real-TUI
|
|
31
|
+
ACKs have different scope. The stable candidate changes the version handshake
|
|
32
|
+
and release controls, not native delivery behavior.
|
|
33
|
+
2. Confirm the `npm` environment still has a required reviewer and permits only
|
|
34
|
+
protected main. Confirm npm account 2FA, package ownership and the Trusted
|
|
35
|
+
Publisher's stage-only mapping above. Do not bypass environment protection.
|
|
36
|
+
3. Read the public registry. `0.1.0` must be absent; `preview` must still point
|
|
37
|
+
to preview.1 and `latest` to preview.0. The published preview.1 integrity and
|
|
38
|
+
provenance must match the verified baseline above. Inspect pending stages with
|
|
39
|
+
`npm stage list session-peer` through an authenticated owner session;
|
|
40
|
+
public registry metadata cannot reveal them.
|
|
41
|
+
4. Obtain final approval for the **exact main SHA**, `0.1.0` version and
|
|
42
|
+
`stable-stage` mode. A merged PR is not publication approval. The approval
|
|
43
|
+
must also cover promotion of npm `latest` to `0.1.0` when the stage is
|
|
44
|
+
approved. GitHub tag, release and fleet installation need separate decisions.
|
|
45
|
+
|
|
46
|
+
Dispatch Actions → **npm staged publication** → Run workflow → **main**:
|
|
47
|
+
|
|
48
|
+
- mode: `stable-stage`
|
|
49
|
+
- version: `0.1.0`
|
|
50
|
+
- confirmation: `session-peer@0.1.0 stable-stage`
|
|
51
|
+
|
|
52
|
+
The prepare job requires exact-main CI and the protected npm environment. It
|
|
53
|
+
checks the registry baseline, tests on Node 24/npm 11.15.0 without publish
|
|
54
|
+
credentials, packs twice, compares hashes, checks the allowlist and installs and
|
|
55
|
+
uninstalls the exact tarball in isolation. The retained `npm-release-<SHA>`
|
|
56
|
+
artifact contains the tarball and manifest.json with commit, SHA-256 and
|
|
57
|
+
SHA-512 integrity. Actions are pinned by commit; no dependency cache or package
|
|
58
|
+
lifecycle scripts are used.
|
|
59
|
+
|
|
60
|
+
After environment review, the publish job downloads that artifact without
|
|
61
|
+
rebuilding and repeats the main/CI/registry and artifact checks. Only one
|
|
62
|
+
`npm stage publish ... --tag latest --provenance --ignore-scripts` step runs with
|
|
63
|
+
OIDC. Network upload retries are disabled. **A successful staged workflow is
|
|
64
|
+
pending npm approval, not a public release.** Record the stage ID. Download its
|
|
65
|
+
tarball, compare it byte for byte with the retained run artifact, and obtain a
|
|
66
|
+
separate final approval before the owner approves that exact stage with 2FA on
|
|
67
|
+
npmjs.com or through interactive `npm stage approve <stage-id>`. OIDC cannot
|
|
68
|
+
approve, list or view stages.
|
|
69
|
+
|
|
70
|
+
After 2FA approval, check out the exact source commit and download the run
|
|
71
|
+
artifact into `release-dist/`. With Node 24 and npm 11.15.0:
|
|
124
72
|
|
|
125
73
|
```sh
|
|
126
74
|
node scripts/release.mjs artifact
|
|
127
75
|
node scripts/release.mjs verify
|
|
128
76
|
```
|
|
129
77
|
|
|
130
|
-
Neither command publishes.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
78
|
+
Neither command publishes. Verification requires the public 0.1.0 integrity,
|
|
79
|
+
registry provenance metadata, `latest: 0.1.0`, unchanged
|
|
80
|
+
`preview: 0.1.0-preview.1`, npm signature audit, and fresh install/uninstall. Announce
|
|
81
|
+
stable availability only after this passes. Review a Git tag, GitHub release or
|
|
82
|
+
fleet rollout separately.
|
|
83
|
+
|
|
84
|
+
## Later previews
|
|
85
|
+
|
|
86
|
+
The manual workflow still accepts `trusted-stage` for a new immutable preview
|
|
87
|
+
version with package `publishConfig.tag: preview`; it stages with OIDC and never
|
|
88
|
+
directly publishes. Preview preflight refuses an already published version and
|
|
89
|
+
requires `latest` to be stable or absent. The historical preview.1 exception
|
|
90
|
+
required its exact legacy `latest: 0.1.0-preview.0` and cannot authorize a
|
|
91
|
+
future version. Every stage needs artifact review and a separate 2FA approval.
|
|
134
92
|
|
|
135
93
|
## Failure handling
|
|
136
94
|
|
|
137
|
-
Missing
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
upload,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
A broken
|
|
95
|
+
Missing environment protection, bad OIDC mapping, stale main, failed CI,
|
|
96
|
+
conflicting version or tag state, an existing stage, upload timeout, hash
|
|
97
|
+
mismatch and non-404 registry errors are stop conditions. Do not automatically
|
|
98
|
+
retry an uncertain upload, change authentication mode, move a tag, unpublish,
|
|
99
|
+
reuse a version or approve a stage. Inspect the run, stage and public registry
|
|
100
|
+
first. Registry propagation may lag a successful approval: `verify` performs
|
|
101
|
+
bounded read-only waits, never a second upload. A broken public release needs a
|
|
102
|
+
reviewed new version and its own approval.
|
|
103
|
+
|
|
104
|
+
The first preview.0 used a bootstrap token, and the registry assigned both
|
|
105
|
+
`preview` and `latest` to it despite `--tag preview`. That bootstrap path is
|
|
106
|
+
closed in this workflow. The repository `NPM_TOKEN` secret and any remaining
|
|
107
|
+
bootstrap credential should be removed through a separate owner-approved account
|
|
108
|
+
operation; this PR neither reads nor changes those credentials.
|
|
144
109
|
|
|
145
110
|
## Sources
|
|
146
111
|
|
|
147
112
|
- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/)
|
|
148
113
|
- [npm staged publishing](https://docs.npmjs.com/staged-publishing)
|
|
149
|
-
- [npm stage CLI
|
|
114
|
+
- [npm stage CLI](https://docs.npmjs.com/cli/v11/commands/npm-stage)
|
|
150
115
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
116
|
+
OIDC needs npm >=11.5.1 and staging needs npm >=11.15.0 with Node >=22.14.0.
|
|
117
|
+
Application support remains Node 22.13+ within 22.x and 24.x; release-tool
|
|
118
|
+
requirements are separate.
|
package/VALIDATION.md
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
# Validation record — 2026-09-27 KST
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This record distinguishes historical live-agent evidence, the verified public
|
|
4
|
+
preview and the unpublished 0.1.0 stable candidate. It does not claim complete
|
|
5
|
+
Python parity.
|
|
4
6
|
Python reference: v1.0.2, commit `47c23713d0a2a3c11ebde6186afd8c43489b8b65`.
|
|
5
7
|
|
|
8
|
+
## Public 0.1.0-preview.1 — 2026-09-27 KST
|
|
9
|
+
|
|
10
|
+
The preview.1 candidate and exact merged main passed macOS/Linux Node 22/24,
|
|
11
|
+
native Windows Node 22/24 and the release gate. A separate gate change preserved
|
|
12
|
+
the existing `latest: 0.1.0-preview.0` while permitting preview.1 staging.
|
|
13
|
+
[Run 36313884100](https://github.com/abruption/session-peer-ts/actions/runs/36313884100)
|
|
14
|
+
staged the exact main commit `f65d3ee99cc26ff4a267aef84e31a308d034ac80`
|
|
15
|
+
through the stage-only OIDC Trusted Publisher. After separate environment and
|
|
16
|
+
npm 2FA approvals, the public registry showed `preview: 0.1.0-preview.1` and
|
|
17
|
+
unchanged `latest: 0.1.0-preview.0`.
|
|
18
|
+
|
|
19
|
+
The npm stage tarball matched the retained run artifact byte for byte. Its
|
|
20
|
+
SHA-256 was `d154beac93893b60b0695e5a6296e185beb8dbd05188ffe04befb74d29966110`.
|
|
21
|
+
The public SHA-512 integrity and provenance metadata matched the manifest;
|
|
22
|
+
`npm audit signatures`, fresh version-specific install, CLI version and empty
|
|
23
|
+
Claude discovery checks, and uninstall passed. This verifies package delivery,
|
|
24
|
+
not a new live Claude/Codex message ACK from the published build.
|
|
25
|
+
|
|
26
|
+
## 0.1.0 stable candidate
|
|
27
|
+
|
|
28
|
+
This preparation updates the version handshake, package metadata, four README
|
|
29
|
+
languages, release gates and tests. Native delivery logic is unchanged. The
|
|
30
|
+
candidate must pass its PR and exact merged-main CI before a separate decision
|
|
31
|
+
to stage `0.1.0`; staging, npm 2FA approval, public verification and any GitHub
|
|
32
|
+
tag or fleet installation remain separate gates.
|
|
33
|
+
|
|
34
|
+
## Historical 0.1.0-preview.1 candidate checks
|
|
35
|
+
|
|
36
|
+
After #7–#10 were merged, the versioned candidate passed the local macOS
|
|
37
|
+
arm64 Node 22 contract suite (23 passed, two Windows-only tests skipped),
|
|
38
|
+
TypeScript test type-check, package/declaration smoke test, four-README
|
|
39
|
+
repository checks and `npm audit` (zero vulnerabilities). The package smoke
|
|
40
|
+
test repeated packing with the same SHA-256 and verified isolated install and
|
|
41
|
+
uninstall. The new candidate was not sent to a live agent and was not published
|
|
42
|
+
by these checks. Windows Node 22/24 and macOS/Linux Node 22/24 must also pass
|
|
43
|
+
on the candidate PR and exact merged `main` commit before publication.
|
|
44
|
+
|
|
6
45
|
## Automated checks
|
|
7
46
|
|
|
8
47
|
- 14 tests pass locally on macOS arm64 / Node 22.14.0.
|
|
@@ -17,6 +56,30 @@ Python reference: v1.0.2, commit `47c23713d0a2a3c11ebde6186afd8c43489b8b65`.
|
|
|
17
56
|
|
|
18
57
|
## Real TUI and SSH checks
|
|
19
58
|
|
|
59
|
+
### Native Windows candidate — 2026-09-27 KST
|
|
60
|
+
|
|
61
|
+
An isolated prefix under the Windows user's Temp directory was installed from
|
|
62
|
+
this branch's local tarball. The existing Python CLI was not removed or
|
|
63
|
+
overwritten. Node 24.16.0 and npm 11.13.0 were present. Windows native
|
|
64
|
+
`list` found the live Claude inbox; Codex dry-run verified the saved thread,
|
|
65
|
+
kernel lock, single same-user `codex.exe` opener and stable start time. Mac
|
|
66
|
+
mini → Windows SSH preflight used an explicit Windows platform, remote `.cmd`
|
|
67
|
+
path and an already authenticated OpenSSH control socket; no host-key bypass
|
|
68
|
+
or new login was introduced.
|
|
69
|
+
|
|
70
|
+
One message per target was submitted from this candidate through Windows SSH.
|
|
71
|
+
The CLI returned Claude `posted` and Codex `queued`, with
|
|
72
|
+
`consumptionConfirmed:false`. The user separately confirmed exact response
|
|
73
|
+
lines in both Windows native TUIs:
|
|
74
|
+
|
|
75
|
+
- Claude: `ACK TS-WIN4-CLAUDE-20260927-A`
|
|
76
|
+
- Codex: `ACK TS-WIN4-CODEX-20260927-B`
|
|
77
|
+
|
|
78
|
+
These ACKs are user-observed responses, not direct terminal-pane capture by
|
|
79
|
+
the test runner. No retry was made. This is one-shot evidence, not a soak or
|
|
80
|
+
all-architecture qualification. Windows x64 was tested; Windows arm64 and
|
|
81
|
+
Node 22 remain untested in this live check. Python remains installed.
|
|
82
|
+
|
|
20
83
|
Dedicated, newly created Claude/Codex test TUIs only. The user approved trusting
|
|
21
84
|
the two otherwise empty test folders. Agent permissions were not changed.
|
|
22
85
|
Each route passed discovery and no-delivery dry-run before exactly one send.
|
|
@@ -69,9 +132,9 @@ package/release fixtures, not claimed as new live ACK evidence.
|
|
|
69
132
|
|
|
70
133
|
## Not covered / not released
|
|
71
134
|
|
|
72
|
-
Windows
|
|
73
|
-
|
|
74
|
-
provenance are
|
|
75
|
-
|
|
76
|
-
and
|
|
77
|
-
on each architecture claimed by a future release.
|
|
135
|
+
Windows arm64, IPv6 literals, Relay/MCP/wake, full Python CLI parity and
|
|
136
|
+
automatic reverse-route detection remain outside this evidence. Registry
|
|
137
|
+
publishing and provenance are verified for preview.1 above, but not by the
|
|
138
|
+
historical live-agent ACKs. Stable 0.1.0 publishing still needs the separate
|
|
139
|
+
approvals and gates in RELEASING.md. Native dependency prebuild availability
|
|
140
|
+
must be tested on each architecture claimed by a future release.
|
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
|
}
|