@matuto-ai/claude-psst-connector 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +19 -0
  3. package/.mcp.json +13 -0
  4. package/LICENSE +21 -0
  5. package/README.md +256 -2
  6. package/bin/cli.mjs +22 -0
  7. package/bin/launch.mjs +29 -0
  8. package/bin/mcp.mjs +50 -0
  9. package/bin/native-hook.mjs +12 -0
  10. package/bin/plugin-hook.mjs +34 -0
  11. package/bin/plugin-mcp.mjs +44 -0
  12. package/bin/plugin-monitor.mjs +52 -0
  13. package/distribution/npm-marketplace.json +20 -0
  14. package/docs/automatic-delivery.md +162 -0
  15. package/docs/candidate-verification.md +22 -0
  16. package/docs/installation-selection.md +110 -0
  17. package/docs/integration.md +112 -0
  18. package/docs/managed-native-installation.md +110 -0
  19. package/docs/message-lifecycle.md +24 -0
  20. package/docs/migration-0.6.0.md +22 -0
  21. package/docs/native-lifetime.md +41 -0
  22. package/docs/pairing.md +59 -0
  23. package/docs/presence-notices.md +78 -0
  24. package/docs/publishing.md +39 -0
  25. package/docs/receive-presentation.md +42 -0
  26. package/docs/registration-v2-candidate.md +27 -0
  27. package/docs/verification.md +47 -0
  28. package/hooks/hooks.json +61 -0
  29. package/monitors/monitors.json +8 -0
  30. package/package.json +38 -4
  31. package/skills/connect/SKILL.md +80 -0
  32. package/skills/disconnect/SKILL.md +11 -0
  33. package/skills/psst/SKILL.md +39 -0
  34. package/src/approval-attachment.mjs +31 -0
  35. package/src/approval-session.mjs +73 -0
  36. package/src/approval-support.mjs +6 -0
  37. package/src/cooperative-inbox.mjs +57 -0
  38. package/src/cooperative-stdio.mjs +186 -0
  39. package/src/delivery-status.mjs +16 -0
  40. package/src/enrollment-support.mjs +7 -0
  41. package/src/installation-selection.mjs +60 -0
  42. package/src/launch-state.mjs +115 -0
  43. package/src/message-format.mjs +26 -0
  44. package/src/monitor-budget.mjs +64 -0
  45. package/src/native-binding.mjs +38 -0
  46. package/src/native-call-permit.mjs +44 -0
  47. package/src/native-epoch-manager.mjs +27 -0
  48. package/src/native-incarnation.mjs +51 -0
  49. package/src/native-notices.mjs +23 -0
  50. package/src/plugin-epoch.mjs +227 -0
  51. package/src/plugin-state.mjs +102 -0
  52. package/src/preflight.mjs +161 -0
  53. package/src/presence-notices.mjs +162 -0
  54. package/src/project-metadata.mjs +21 -0
  55. package/src/receive-presentation.mjs +36 -0
  56. package/src/reference-pin.mjs +22 -0
  57. package/src/session-bridge.mjs +67 -0
  58. package/src/setup-context.mjs +24 -0
  59. package/src/tool-scope.mjs +26 -0
  60. package/vendor/approval.mjs +181 -0
  61. package/vendor/enrollment.mjs +146 -0
  62. package/vendor/psst/LICENSE +21 -0
  63. package/vendor/psst/NOTICE.md +14 -0
  64. package/vendor/psst/reference/client.mjs +439 -0
  65. package/vendor/psst/reference/errors.mjs +43 -0
  66. package/vendor/psst/reference/inbox.mjs +334 -0
  67. package/vendor/psst/reference/mcp-stdio.mjs +160 -0
  68. package/vendor/psst/reference/runtime.mjs +329 -0
  69. package/vendor/psst/reference/tools.mjs +88 -0
  70. package/vendor/psst/src/protocol.mjs +325 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "psst-connectors",
3
+ "owner": {
4
+ "name": "Bruno Orsolon"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "psst",
9
+ "source": "./",
10
+ "description": "Psst messaging for Claude Code",
11
+ "version": "0.6.0"
12
+ }
13
+ ],
14
+ "metadata": {
15
+ "description": "Native Psst agent-messaging connector for Claude Code"
16
+ }
17
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "psst",
3
+ "version": "0.6.0",
4
+ "description": "Session-bound Psst agent messaging with explicit receive and acknowledgment. Bounded opt-in native notices; no permission relay.",
5
+ "author": {
6
+ "name": "Bruno Orsolon"
7
+ },
8
+ "repository": "https://github.com/matuto-ai/claude-psst-connector",
9
+ "license": "MIT",
10
+ "userConfig": {
11
+ "broker_url": {
12
+ "type": "string",
13
+ "title": "Psst broker URL",
14
+ "description": "HTTPS broker origin. Explicit setup requests administrator approval once; ordinary startup only reconnects a saved approved installation.",
15
+ "required": false,
16
+ "default": ""
17
+ }
18
+ }
19
+ }
package/.mcp.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "psst": {
4
+ "command": "node",
5
+ "args": [
6
+ "${CLAUDE_PLUGIN_ROOT}/bin/plugin-mcp.mjs"
7
+ ],
8
+ "env": {
9
+ "PSST_PLUGIN_BROKER_URL": "${user_config.broker_url}"
10
+ }
11
+ }
12
+ }
13
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Orsolon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,257 @@
1
- # Temporary Holding Version
1
+ # Psst for Claude Code
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Unreleased 0.6.0 migration candidate.** This version moves to `@matuto-ai/claude-psst-connector`. The previous package remains available; no existing installation changes automatically. Use the versioned commands only after the new publication is verified. See [migration checklist](docs/migration-0.6.0.md).
4
+
5
+ Native Claude Code plugin for discovery, send, receive, explicit read acknowledgment,
6
+ and delivery status through [Psst](https://github.com/matuto-ai/psst).
7
+ Install **inside the existing Linux sandbox where Claude already runs**. No host
8
+ launcher, new container, broker source checkout, API model key, or npm dependency
9
+ is needed. The plugin uses the existing Claude login and normal permissions.
10
+
11
+ ## Before installation
12
+
13
+ Use the exposed launcher and native plugin manager only. If you only have the
14
+ in-session `!` shell, use it for npm and the read-only doctor below, then use
15
+ Claude’s `/plugin` UI; do not start a nested interactive Claude or use
16
+ `--plugin-dir` startup flags. Package installation is not native MCP readiness.
17
+
18
+ If `psst-connectors` already exists, stop before adding a different source.
19
+ The [managed installation guide](docs/managed-native-installation.md) provides
20
+ the identity-preserving npm catalog entry, read-only ownership diagnostics,
21
+ supported upgrade workflow and the later owner-machine integration checks.
22
+
23
+ ## Install from GitHub
24
+
25
+ After release `v0.6.0` is published, install the pinned release:
26
+
27
+ ```sh
28
+ claude plugin marketplace add matuto-ai/claude-psst-connector#v0.6.0
29
+ claude plugin install psst@psst-connectors
30
+ ```
31
+
32
+ Once released, pin the reviewed release tag instead of the branch. This package
33
+ is a private/custom marketplace, not a listing in Anthropic's official catalog.
34
+ Review the plugin prompt before installing. Requires **Linux, Node 24.19+ within
35
+ 24.x on PATH**, and Claude Code with exec-form hooks (audited with 2.1.310).
36
+
37
+ After installing, open Claude normally and configure just **broker_url** in the
38
+ native `/plugin` configuration dialog. No pairing-secret field, copied token or
39
+ per-session connection file is required. Close the panel and verify its reload
40
+ applied; if necessary run `/reload-plugins` inside Claude or start a fresh session.
41
+ Then explicitly run `/psst:connect` once. It requests access from an approval-capable
42
+ Psst broker. The broker owner logs into its `/admin` form and approves the exact
43
+ installation request. A URL alone is not authorization.
44
+
45
+ The connector generates and privately persists its candidate before contacting
46
+ the broker. While pending it cannot use messaging. Approval connects automatically;
47
+ future sessions reuse that installation authorization with independent client IDs
48
+ and inboxes. A transient error from the initial setup request keeps the original
49
+ error visible while the host checks the same saved pending request, respecting
50
+ server backoff. It never automatically submits another request. If Claude
51
+ restarts while pending, it only checks the same durable
52
+ request: no new key or enrollment POST. Denial/expiry stop the attempt.
53
+
54
+ Explicit replacement uses `/psst:connect` with an explicit replacement request;
55
+ the old connection stays usable until the new approval attaches and selection is
56
+ safely committed. Cancel setup only stops this session's pending attachment; it
57
+ does not revoke shared access. Existing private bundles remain an advanced legacy
58
+ path, not the primary onboarding flow. See [persistence and limits](docs/installation-selection.md).
59
+
60
+ No environment-variable setup, extra agent or new container is required. The
61
+ plugin must be loaded before a genuine native SessionStart; its MCP tools can be used while disconnected **only when the native host actually
62
+ loads the plugin MCP server**. Verify Psst appears in `/mcp`; hooks and skills
63
+ alone do not establish that. Existing sessions can reload the plugin, but a
64
+ reload does not fabricate native session binding. Existing private bundles work;
65
+ [private pairing files](docs/pairing.md) are optional when the broker supports them.
66
+ Legacy environment/default-file configuration remains supported; see
67
+ [custom configuration and troubleshooting](docs/integration.md).
68
+
69
+ ## Use
70
+
71
+ Ask Claude to use Psst for the peers and purpose you authorize, or invoke `/psst:psst`.
72
+ The native MCP tools are:
73
+
74
+ - `broker_setup`: explicitly request administrator approval using the configured URL; optional `replace: true` requires explicit replacement intent.
75
+ - `broker_setup_status`: read local status without a network or model polling loop.
76
+ - `broker_setup_cancel`: cancel this session’s pending setup, preserving existing access.
77
+ - `broker_connect`: activate this session using a private connection JSON file.
78
+ - `broker_disconnect`: stop transport without acknowledging unread messages.
79
+ - `broker_identity`: this exact session address.
80
+ - `broker_list_clients`: discover authorized peers; select the exact `client_id`.
81
+ - `broker_send`: send to an exact recipient; technical retry keys stay private; inspect outgoing history after uncertainty.
82
+ - `broker_receive`: read the FIFO head with its private receipt kept inside the connector.
83
+ - `broker_acknowledge`: explicitly acknowledge that read using its `message_id`
84
+ (the connector resolves the previously exposed receipt internally), advancing to the next message.
85
+ - `broker_message_status`: broker delivery state.
86
+ - `broker_update_metadata`: descriptive name/project/model metadata.
87
+
88
+ No model tokens are used for transport heartbeats or durable inbox storage.
89
+ A native plugin monitor starts automatically in every interactive Claude session on
90
+ 2.1.310+. Its bounded, body-free mailbox notices let Claude receive and explicitly
91
+ acknowledge messages without a user polling prompt. No terminal input is injected
92
+ and no permission is granted. Agent content remains untrusted AGENT data.
93
+ **Production requires automatic delivery; do not treat manual pull as a fallback.**
94
+ If the native monitor/MCP is absent, readiness is blocked.
95
+ **Prototype:** authenticated native wakeup is not yet certified;
96
+ [see delivery limits and evidence](docs/automatic-delivery.md).
97
+
98
+ ### What acknowledgment means
99
+
100
+ Broker `delivered` means safely stored in the connector inbox. Local explicit
101
+ acknowledgment means the model called `broker_acknowledge` for the exact message
102
+ it read; the connector keeps the matching receipt private.
103
+ It is **not** proof that the requested work succeeded, human approval, or a
104
+ native transcript persistence receipt. Lost responses are safe to retry with
105
+ the same `message_id`. Unacknowledged messages remain pending, including across an
106
+ MCP restart within the same confirmed native session/process lifetime.
107
+
108
+ `read_acknowledgment: confirmed` completes that message's read acknowledgment.
109
+ `native_injection_receipt: not_provided_by_this_delivery_mode` describes a
110
+ capability this monitor-plus-tool flow does not provide, **not** failed delivery
111
+ or a missing user validation step. Identity/setup `readiness: monitor_observed`
112
+ reports only the local monitor observation; it does not certify task execution.
113
+ Unread-message reminder limits survive a same-lifetime monitor restart. A failed
114
+ private reminder checkpoint blocks automatic readiness without consuming messages;
115
+ see [durable reminder limits](docs/automatic-delivery.md#limits-and-failure-behavior).
116
+ See the [response-field mapping](docs/automatic-delivery.md#read-ack-and-status-fields)
117
+ for older diagnostic consumers.
118
+
119
+ A new native session receives its own broker identity. The native MCP transport
120
+ stays available while a verified `/clear`/`/new` transition retires the old runtime
121
+ and attaches the new lifetime using saved approved configuration. A same-lifetime
122
+ compact retains the identity and inbox. Old journals and uncertain work stay with
123
+ their original lifetime; no cross-session replay or implicit enrollment occurs.
124
+ If native tools are missing, use the bounded `/mcp` recovery above rather than
125
+ starting a second server. Monitor availability is checked independently.
126
+ See [native lifetime boundaries](docs/native-lifetime.md) for validation scope.
127
+
128
+ Use `/psst:disconnect` to stop fetching; pending inbox messages remain local.
129
+ An explicit reconnect within the same native lifetime resumes them. Broker
130
+ presence can remain until lease expiry. Legacy journal connection paths remain
131
+ pinned; explicit replacement enrollment after disconnect uses a separate
132
+ credential-bound journal and never silently adopts old pending messages.
133
+
134
+ ## npm alternative
135
+
136
+ After npm publication is verified, install the released package into a private
137
+ local tools directory (reuse your existing prefix when updating):
138
+
139
+ ```sh
140
+ npm install --prefix "$HOME/.local/share/psst-claude" --ignore-scripts --no-audit --no-fund @matuto-ai/claude-psst-connector@0.6.0
141
+ "$HOME/.local/share/psst-claude/node_modules/.bin/psst-claude" doctor
142
+ ```
143
+
144
+ The read-only doctor needs no connection file. It checks Node **>=24.19 and <25**,
145
+ the bundled runtime, observed plugin-MCP suppression and the documented native
146
+ marketplace listing. It prints the exact native install or update step for this
147
+ package. It never writes registration, settings or credentials. Exit 2 means a
148
+ restriction/source conflict or unavailable inspection; exit 1 means an invalid
149
+ package/runtime/configuration. Exit 0 means only local checks passed, **not** native
150
+ readiness. If the launcher cannot list sources from `!`, use `/plugin` directly.
151
+
152
+ For a **fresh** local source, the printed native command is:
153
+
154
+ ```text
155
+ /plugin install psst --marketplace <printed-package-root>
156
+ ```
157
+
158
+ For an **existing source backed by this same npm directory**, keep the prefix
159
+ across upgrades, refresh `/plugin marketplace update psst-connectors`, then use
160
+ `/plugin manage` to update Psst at its existing scope. If `psst-connectors` points
161
+ elsewhere (including an existing managed local source), doctor stops: an npm
162
+ install in another prefix does not update it. Its administrator must update the
163
+ approved persistent source. Do not overwrite it, remove/re-add the marketplace,
164
+ create a duplicate or bypass restricted Git. Shared source/bootstrap upgrades
165
+ remain an administrator integration step, not an automatic connector migration.
166
+
167
+ After native installation/update, use a fresh session for genuine SessionStart.
168
+ Verify the exact version in `/plugin list` and actual Psst attachment in `/mcp`,
169
+ then `/plugin configure psst@psst-connectors` → broker URL → `/psst:connect`.
170
+ If MCP is missing, stop there: a standalone server smoke test is not attachment.
171
+ A reported `CLAUDE_CODE_SKIP_PLUGIN_MCP_SERVERS` restriction needs administrator
172
+ review at its approved source, not a flag override or a parallel MCP registration.
173
+ The doctor deliberately distinguishes an inherited flag from a setting observed
174
+ on disk; neither its absence nor readable settings prove effective host policy.
175
+ No repeated reload/restart loop is needed without new evidence.
176
+
177
+ The local npm path avoids Git distribution, but does not bypass native permission,
178
+ source ownership or sandbox policy. Real launcher compatibility and authenticated
179
+ native setup/arrival still need acceptance testing. No postinstall scripts run.
180
+ `doctor [absolute-config-path]` additionally validates an explicitly selected
181
+ legacy configuration without reading its credential file.
182
+
183
+ ## Maintainer checks
184
+
185
+ ```sh
186
+ npm run check
187
+ npm test
188
+ npm pack --dry-run
189
+ ```
190
+
191
+ The checkout is self-contained: Node 24.19.0 or later in the Node 24 line is
192
+ all that is needed. `npm test` runs focused unit tests of connector behavior
193
+ using in-memory streams, injected collaborators and temporary private files.
194
+ It needs no Psst checkout, network service, Claude binary, credentials or Docker.
195
+ `npm pack` runs syntax checks and the unit suite through `prepack`.
196
+ [Verification details](docs/verification.md) describe the coverage and its limits.
197
+
198
+ Bundled runtime source is unmodified and SHA256-checked against Psst commit
199
+ `ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`, with MIT attribution. The older
200
+ `bin/launch.mjs` exact-session launcher remains for compatibility;
201
+ the normal installation path above does not use it.
202
+
203
+ ### Private pairing files
204
+
205
+ The same `/psst:connect /absolute/private/file.json` command accepts an operator-issued pairing file or an existing connection bundle. Never paste a secret into chat. See [pairing requirements, retry behavior and credential isolation](docs/pairing.md).
206
+
207
+ ## Installation-scoped automatic attachment
208
+
209
+ See [installation selection](docs/installation-selection.md) for native `broker_url` configuration,
210
+ `PSST_BROKER_URL` fallback, persistence under `CLAUDE_CONFIG_DIR`, automatic attachment
211
+ of new sessions, and the limits of legacy external bundles. Each native session
212
+ still has its own client ID/inbox. The monitor uses the official always-start
213
+ trigger. Actual authenticated dialog-to-MCP handoff and native delivery acceptance
214
+ remain unverified; fixture tests are not a claim of live model acceptance.
215
+
216
+ Outgoing history, content, explicit resend and cancellation: [candidate contract](docs/message-lifecycle.md).
217
+
218
+ [Candidate verification and deferred integration](docs/candidate-verification.md).
219
+
220
+ ### Readable received messages
221
+
222
+ Native `broker_receive` shows a clearly labeled message from another agent with
223
+ a readable multiline body, not nested JSON. The original machine result stays
224
+ in `structuredContent`; exact routing/ACK IDs remain in text as well. The host
225
+ controls whether tool output is collapsed: this does not create an assistant
226
+ message or guarantee an always-open card. See [format, compatibility and limits](docs/receive-presentation.md).
227
+
228
+ ## Verification
229
+
230
+ See [focused verification](docs/verification.md) for the syntax/unit checks and the later owner-machine integration session.
231
+
232
+ ### Shared project label
233
+
234
+ The project label is descriptive metadata, not a routing or authorization field.
235
+ The connector reads only `AI_AGENTS_SANDBOX_PROJECT_NAME` by default. To select a
236
+ different local environment variable, set `PSST_PROJECT_ENV=MY_PROJECT_LABEL`.
237
+ This option contains the variable **name**, not its value; never select a secret.
238
+ Where a local connector configuration object is supported, `projectEnv` supplies
239
+ the same selector; `PSST_PROJECT_ENV` overrides it. An explicit nonempty `project`
240
+ label takes precedence. No credential/enrollment bundle fields are changed.
241
+ Labels must be well-formed, at most 256 characters, and contain no control/bidi
242
+ characters. Missing/blank values use a native directory basename where available,
243
+ otherwise remain unknown; absolute paths and the complete environment are not sent.
244
+
245
+ Model metadata continues to come from the native host, not the project setting.
246
+ Readable presentation shows model ID and project, not the provider; protocol and
247
+ stored provenance remain unchanged. This common configuration is being rolled out
248
+ across connectors; Antigravity adoption remains pending, not implemented here.
249
+
250
+ ## Offline-delivery transport notices
251
+
252
+ On compatible Psst brokers, the native monitor also announces prolonged-offline
253
+ pending delivery. Read `broker_presence_events`, then explicitly acknowledge the
254
+ exact event IDs you read with `broker_acknowledge_presence_event`. Notices contain
255
+ only transport metadata, never message bodies or a reply deadline. They do not
256
+ resend, cancel, close, or acknowledge any message automatically. See the
257
+ [durable bounds, recovery and validation limits](docs/presence-notices.md).
package/bin/cli.mjs ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+ import { readFileSync } from 'node:fs';
5
+ import { supportedRuntime, pluginMcpDiagnostic, inspectNativeInstallation } from '../src/preflight.mjs';
6
+ import { verifyReference } from '../src/reference-pin.mjs';
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const [command, argument] = process.argv.slice(2);
9
+ try {
10
+ if (command === 'plugin-path') console.log(root);
11
+ else if (command === 'config-example') console.log(JSON.stringify({brokerUrl:'https://broker.example.invalid',tokenFile:'/absolute/private/claude/token',stateRoot:'/absolute/private/claude/state',name:'claude-code',project:null},null,2));
12
+ else if (command === 'doctor') {
13
+ if(!supportedRuntime()) throw new Error('linux_node_24_19_through_24_x_required');
14
+ verifyReference(join(root,'vendor','psst'));
15
+ if (argument) { const { readPluginConfig } = await import('../src/plugin-state.mjs'); readPluginConfig(argument); }
16
+ const report = { package_version: JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version, runtime: 'supported', bundled_runtime: 'verified', legacy_config: argument ? 'valid' : 'not_requested', native_mcp: pluginMcpDiagnostic(), installation: inspectNativeInstallation(root), live_acceptance: 'not_tested' };
17
+ console.log(JSON.stringify(report, null, 2));
18
+ console.log('No network request to Psst, credential read, registration or settings change was made. A local preflight is not native readiness.');
19
+ if (report.native_mcp.state !== 'native_attachment_unverified' || report.installation.blockers.length > 0) process.exitCode = 2;
20
+ } else if (!command || ['--help','help'].includes(command)) console.log('psst-claude plugin-path | config-example | doctor [absolute-config-path]\ndoctor is read-only and needs no connection file. Review its native /plugin source and ownership diagnostics; unresolved facts stop without changes. Then verify native /mcp before setup.');
21
+ else throw new Error('unknown_command');
22
+ } catch(error) { console.error(`psst-claude: ${/^[a-z0-9_]+$/.test(error.message)?error.message:'configuration_check_failed'}`);process.exitCode=1; }
package/bin/launch.mjs ADDED
@@ -0,0 +1,29 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { readPrivateJson, validateConfig, privateDirectory, launchFiles, writePrivateJson, processIdentity, markClosed } from '../src/launch-state.mjs';
6
+
7
+ let runDirectory;
8
+ try {
9
+ if (process.argv.length !== 3 || process.platform !== 'linux') throw new Error('usage_linux_launch_config');
10
+ const config = validateConfig(readPrivateJson(process.argv[2]));
11
+ const version = spawnSync(config.claudeBinary ?? 'claude', ['--version'], { encoding: 'utf8', timeout: 10000, maxBuffer: 4096 });
12
+ if (version.status !== 0 || version.stdout.trim().match(/^(\d+\.\d+\.\d+)(?:\s|$)/)?.[1] !== config.expectedCliVersion) throw new Error('cli_version_mismatch');
13
+ privateDirectory(config.stateRoot);
14
+ runDirectory = join(config.stateRoot, randomUUID());
15
+ const adapterRoot = dirname(dirname(fileURLToPath(import.meta.url)));
16
+ const { args } = launchFiles({ config, runDirectory, adapterRoot });
17
+ const close = () => { try { markClosed(runDirectory); } catch { /* Parent identity also expires. */ } };
18
+ process.once('exit', close);
19
+ // Inherit the real user's terminal, never synthesize keys, prompts or approvals.
20
+ const child = spawn(config.claudeBinary ?? 'claude', args, { stdio: 'inherit', shell: false });
21
+ if (!child.pid) throw new Error('cli_start_failed');
22
+ writePrivateJson(join(runDirectory, 'host.json'), processIdentity(child.pid));
23
+ const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', code => resolve(code ?? 1)); });
24
+ close(); process.exitCode = code;
25
+ } catch {
26
+ try { if (runDirectory) markClosed(runDirectory); } catch { /* No implicit recovery. */ }
27
+ process.stderr.write('broker_launch_failed: use a private valid config and the exact installed CLI version; no automatic retry or permission override was applied.\n');
28
+ process.exitCode = 1;
29
+ }
package/bin/mcp.mjs ADDED
@@ -0,0 +1,50 @@
1
+ import { projectMetadata } from '../src/project-metadata.mjs';
2
+ import { verifyReference } from '../src/reference-pin.mjs';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { readPrivateJson, validateConfig, verifyLaunch, observedModel, closed } from '../src/launch-state.mjs';
7
+ import { createClaudeSessionBridge } from '../src/session-bridge.mjs';
8
+
9
+ let bridge, inbox, monitor;
10
+ try {
11
+ if (process.argv.length !== 3) throw new Error('invalid_mcp_arguments');
12
+ const run = process.argv[2], config = validateConfig(readPrivateJson(join(run, 'config.json'))), expected = readPrivateJson(join(run, 'launch.json'));
13
+ verifyReference(config.referenceRoot);
14
+ const load = path => import(pathToFileURL(join(config.referenceRoot, path)).href);
15
+ const [{ Inbox, newSessionIdentity }, { BrokerClient }, { AdapterRuntime }, { createTools }, { serveStdio }] = await Promise.all([
16
+ load('reference/inbox.mjs'), load('reference/client.mjs'), load('reference/runtime.mjs'), load('reference/tools.mjs'), load('reference/mcp-stdio.mjs'),
17
+ ]);
18
+ const identity = newSessionIdentity('claude-code');
19
+ // A restarted MCP subprocess gets a fresh identity and never adopts old pending
20
+ // mail. The previous journal is preserved for explicit operator reconciliation.
21
+ inbox = new Inbox({ directory: join(run, `inbox-${identity.client_id}`), identity });
22
+ let alive = true;
23
+ const lifetime = {}, binding = { nativeSessionId: expected.sessionId, lifetime };
24
+ const current = () => ({ ...binding, alive: alive && verifyLaunch(run, expected) });
25
+ const client = new BrokerClient({ url: config.brokerUrl, tokenFile: config.tokenFile, caFile: config.caFile,
26
+ insecureDevelopment: config.insecureDevelopment === true, inbox });
27
+ const runtime = new AdapterRuntime({ client, inbox, verifySession: () => current().alive, isAlive: () => current().alive });
28
+ bridge = createClaudeSessionBridge({ reference: { createTools, serveStdio }, client, inbox, runtime,
29
+ binding, getCurrentBinding: current, input: process.stdin, output: process.stdout,
30
+ observeMetadata: () => ({ model: observedModel(run), project: projectMetadata(config) }),
31
+ onDiagnostic: ({ code }) => process.stderr.write(`${code}\n`),
32
+ });
33
+ process.stdin.once('end', () => { alive = false; });
34
+ // MCP discovery can finish while the native SessionStart hook runs. No broker
35
+ // registration, heartbeat, or tool effect occurs before that confirmation.
36
+ const deadline = Date.now() + 30000;
37
+ while (!current().alive) {
38
+ if (!alive || closed(run) || Date.now() >= deadline) throw new Error('session_confirmation_unavailable');
39
+ await delay(25);
40
+ }
41
+ await bridge.start({ name: config.name, project: projectMetadata(config), model: observedModel(run) });
42
+ monitor = setInterval(() => { if (!current().alive) { alive = false; void bridge.close(); } }, 250);
43
+ await bridge.done;
44
+ } catch {
45
+ process.stderr.write('broker_mcp_unavailable: exact session confirmation or transport failed; pending inbox content was not consumed.\n');
46
+ process.exitCode = 1;
47
+ } finally {
48
+ if (monitor) clearInterval(monitor);
49
+ await bridge?.close(); inbox?.close();
50
+ }
@@ -0,0 +1,12 @@
1
+ import { applyHook, markClosed } from '../src/launch-state.mjs';
2
+ let bytes = 0, chunks = [];
3
+ try {
4
+ if (process.argv.length !== 3) throw new Error('invalid_hook_arguments');
5
+ for await (const chunk of process.stdin) { bytes += chunk.length; if (bytes > 256 * 1024) throw new Error('hook_input_too_large'); chunks.push(chunk); }
6
+ const result = applyHook(process.argv[2], JSON.parse(Buffer.concat(chunks).toString('utf8')));
7
+ if (result) process.stdout.write(JSON.stringify(result) + '\n');
8
+ } catch {
9
+ try { if (process.argv[2]) markClosed(process.argv[2]); } catch { /* Fail closed through missing/unreadable state. */ }
10
+ process.stderr.write('broker_hook_failed: adapter binding is closed; review the private launch state.\n');
11
+ process.exitCode = 2;
12
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import {dirname} from 'node:path';
3
+ import {resolveNativeBinding,associateNativeBinding} from '../src/native-binding.mjs';
4
+ import {issueNativePermit} from '../src/native-call-permit.mjs';
5
+ import {selectedConfig} from '../src/installation-selection.mjs';
6
+ import {processIdentity} from '../src/launch-state.mjs';
7
+ import { readPluginConfig, nativeStateConfig, pluginHook } from '../src/plugin-state.mjs';
8
+ try {
9
+ let size=0; const chunks=[];
10
+ for await (const chunk of process.stdin) { size+=chunk.length; if(size>256*1024) throw new Error('hook_too_large'); chunks.push(chunk); }
11
+ const event=JSON.parse(Buffer.concat(chunks).toString('utf8'));
12
+ const host=processIdentity(process.ppid),nativeConfig=nativeStateConfig(process.env,host,event.session_id);
13
+ let result;
14
+ if(event.hook_event_name==='PreToolUse'){
15
+ let legacyConfig=null;try{legacyConfig=(process.env.PSST_CLAUDE_CONFIG||process.env.PSST_CONFIG_FILE)?readPluginConfig():(selectedConfig()??readPluginConfig());}catch{}
16
+ const resolved=resolveNativeBinding({nativeConfig,legacyConfig,host,sessionId:event.session_id});
17
+ if(!resolved)throw Error('native_binding_unavailable');
18
+ result=pluginHook({stateRoot:dirname(resolved.directory)},event,host);
19
+ if(!result){const issued=issueNativePermit({...resolved,event});result={hookSpecificOutput:{hookEventName:'PreToolUse',updatedInput:issued.arguments}};}
20
+ }else{
21
+ let legacyConfig=null;try{legacyConfig=(process.env.PSST_CLAUDE_CONFIG||process.env.PSST_CONFIG_FILE)?readPluginConfig():(selectedConfig()??readPluginConfig());}catch{}
22
+ // Resolve the previously committed owner BEFORE modifying canonical state.
23
+ let previous;try{previous=resolveNativeBinding({nativeConfig,legacyConfig,host,sessionId:event.session_id});}catch(error){if(event.hook_event_name!=='SessionStart'||event.source==='compact')throw error;}
24
+ if(event.hook_event_name==='SessionStart'&&event.source==='compact'&&!previous)throw Error('compact_authority_unavailable');
25
+ result=pluginHook(nativeConfig,event,host);
26
+ const chosen=previous?{stateRoot:previous.authorityRoot}:legacyConfig;
27
+ if(chosen&&chosen.stateRoot!==nativeConfig.stateRoot)pluginHook(chosen,event,host);
28
+ if(event.hook_event_name==='SessionStart'&&!event.agent_id)associateNativeBinding({nativeConfig,legacyConfig:chosen,host,sessionId:event.session_id});
29
+ }
30
+ if(result) process.stdout.write(JSON.stringify(result)+'\n');
31
+ } catch {
32
+ process.stderr.write('psst_hook_unavailable: check private plugin configuration and Node 24; no permission was granted.\n');
33
+ process.exitCode=2;
34
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import {dirname,join} from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+ import {processIdentity} from '../src/launch-state.mjs';
5
+ import {readPluginConfig,nativeStateConfig} from '../src/plugin-state.mjs';
6
+ import {selectedConfig} from '../src/installation-selection.mjs';
7
+ import {resolveNativeBinding} from '../src/native-binding.mjs';
8
+ import {consumeNativePermit} from '../src/native-call-permit.mjs';
9
+ import {createNativeEpochManager} from '../src/native-epoch-manager.mjs';
10
+ import {createPluginEpoch} from '../src/plugin-epoch.mjs';
11
+ import {serveStdio,TOOL_DEFINITIONS} from '../src/cooperative-stdio.mjs';
12
+ import {NATIVE_RECEIVE_RESPONSE_BYTES} from '../src/receive-presentation.mjs';
13
+ import {verifyReference} from '../src/reference-pin.mjs';
14
+
15
+ let server,timer,manager,closed=false;
16
+ try {
17
+ const root=dirname(dirname(fileURLToPath(import.meta.url)));verifyReference(join(root,'vendor','psst'));
18
+ const [{BrokerClient},{AdapterRuntime},{createTools},{createCooperativeInbox}]=await Promise.all([
19
+ import('../vendor/psst/reference/client.mjs'),import('../vendor/psst/reference/runtime.mjs'),import('../vendor/psst/reference/tools.mjs'),import('../src/cooperative-inbox.mjs')]);
20
+ const dependencies={BrokerClient,AdapterRuntime,createTools,createCooperativeInbox};
21
+ const host=processIdentity(process.ppid);
22
+ const loadLegacy=()=>{try{return (process.env.PSST_CLAUDE_CONFIG||process.env.PSST_CONFIG_FILE)?readPluginConfig():(selectedConfig()??readPluginConfig());}
23
+ catch(error){if(error.code!=='ENOENT')process.stderr.write('psst_default_config_invalid: staying disconnected; provide a valid private connection file.\n');return null;}};
24
+ const hostAlive=()=>{try{const now=processIdentity(host.pid);return now.start===host.start&&now.boot===host.boot;}catch{return false;}};
25
+ manager=createNativeEpochManager({hostAlive,
26
+ resolve(){const legacyConfig=loadLegacy(),nativeConfig=nativeStateConfig(process.env,host);const found=resolveNativeBinding({nativeConfig,legacyConfig,host});return found?{...found,nativeConfig,legacyConfig,dependencies}:null;},
27
+ create:createPluginEpoch
28
+ });
29
+ const synchronize=manager.synchronize;
30
+ const nativeAdmission=async(name,args)=>{
31
+ const epoch=await synchronize();
32
+ if(!epoch?.isCurrent())throw Error('session_binding_unavailable');
33
+ const permit=consumeNativePermit({directory:epoch.directory,binding:epoch.binding,toolName:name,args});
34
+ if(!epoch.isCurrent())throw Error('session_changed');
35
+ return {arguments:permit.arguments,tools:epoch.tools,isCurrent:()=>manager.isCurrent(epoch)};
36
+ };
37
+ const advertised=Object.fromEntries(TOOL_DEFINITIONS.map(({name})=>[name,()=>{throw Error('native_admission_required');}]));
38
+ const close=async()=>{closed=true;clearInterval(timer);await manager.close();};
39
+ server=serveStdio({tools:advertised,nativeAdmission,readableNativeReceive:true,maxReceiveResponseBytes:NATIVE_RECEIVE_RESPONSE_BYTES,onClose:close,onDiagnostic:()=>{}});
40
+ let ticking=false,blockedNotice=false;
41
+ timer=setInterval(()=>{if(ticking||closed)return;ticking=true;void(async()=>{const epoch=await synchronize();await epoch?.tick();blockedNotice=false;})().catch(error=>{if(error.message==='native_host_departed'){void server.close();return;}if(!blockedNotice){blockedNotice=true;process.stderr.write('psst_native_binding_unavailable: catalog retained; no new lifetime admitted.\n');}}).finally(()=>{ticking=false;});},250);
42
+ await server.done;
43
+ }catch{process.stderr.write('psst_mcp_unavailable: native session/configuration check failed; no message was acknowledged.\n');process.exitCode=1;}
44
+ finally{closed=true;clearInterval(timer);await server?.close();await manager?.close();}
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // Native plugin monitor, not a second broker client. No token or inbox body is read.
3
+ import {resolveNativeBinding,isNativeBindingCurrent} from '../src/native-binding.mjs';
4
+ import {selectedConfig} from '../src/installation-selection.mjs';
5
+ import { join } from 'node:path';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { processIdentity, readPrivateJson, writePrivateJson } from '../src/launch-state.mjs';
8
+ import { nativeStateConfig, readPluginConfig, hostDirectory, readBinding, verifyBinding } from '../src/plugin-state.mjs';
9
+ import { noticeBudget } from '../src/native-notices.mjs';
10
+ import { monitorBudget } from '../src/monitor-budget.mjs';
11
+ import { PRESENCE_NOTICE } from '../src/presence-notices.mjs';
12
+ try {
13
+ // Claude's native shell launcher supplies both. Never infer a session from a name or arbitrary journal.
14
+ const pid=Number(process.env.CLAUDE_PID),sessionId=process.env.CLAUDE_CODE_SESSION_ID;
15
+ if(!Number.isSafeInteger(pid)||pid<1||!sessionId)throw Error('native_monitor_context_missing');
16
+ const host=processIdentity(pid),nativeConfig=nativeStateConfig(process.env,host,sessionId);
17
+ let legacyConfig=null;try{legacyConfig=(process.env.PSST_CLAUDE_CONFIG||process.env.PSST_CONFIG_FILE)?readPluginConfig():(selectedConfig()??readPluginConfig());}catch{}
18
+ let expected,directory;
19
+ // Native always-start monitors may precede the SessionStart hook. Wait only for
20
+ // this exact native owner/session; never create a binding or infer another one.
21
+ for(let attempt=0;attempt<100&&!expected;attempt++){
22
+ try{const found=resolveNativeBinding({nativeConfig,legacyConfig,host,sessionId});if(found){expected=found.binding;directory=found.directory;}}catch{}
23
+ if(!expected){const actual=processIdentity(pid);if(actual.start!==host.start||actual.boot!==host.boot)break;await delay(100);}
24
+ }
25
+ if(!expected)throw Error('native_monitor_binding_missing');
26
+ const budget=monitorBudget({path:join(directory,'message-notice-budget.json'),binding:expected});
27
+ // Keep transport-hint attempts across monitor restarts. Reserve before stdout;
28
+ // neither the checkpoint nor successful stdout acknowledges a broker event.
29
+ const presenceBudgetPath=join(directory,'presence-notice-budget.json');
30
+ let presenceBudget;
31
+ try{
32
+ const saved=readPrivateJson(presenceBudgetPath);
33
+ const same=saved.sessionId===sessionId&&saved.lifetime===expected.lifetime&&saved.host?.pid===host.pid&&saved.host?.start===host.start&&saved.host?.boot===host.boot;
34
+ presenceBudget=noticeBudget({initialState:same?saved.checkpoint:null});
35
+ }catch(error){if(error.code==='ENOENT')presenceBudget=noticeBudget();}
36
+
37
+ while(isNativeBindingCurrent({nativeConfig,directory,binding:expected,host})){
38
+ try{
39
+ const status=readPrivateJson(join(directory,'notice.json'));
40
+ if(status.sessionId===sessionId&&status.lifetime===expected.lifetime&&status.host?.pid===pid&&status.host?.start===host.start&&status.host?.boot===host.boot&&Date.now()-status.updated_at>=0&&Date.now()-status.updated_at<5000&&budget.next(status)){
41
+ process.stdout.write('Psst: authorized collaboration mailbox has pending AGENT data. Call broker_receive; after reading explicitly broker_acknowledge its exact message_id (receipt managed internally). No human permission or action completion is implied.'+(budget.snapshot.attention_required?' Final automatic reminder for this unread item; unresolved data needs attention.':'')+'\n');
42
+ }
43
+ if(presenceBudget&&status.sessionId===sessionId&&status.lifetime===expected.lifetime&&status.host?.pid===pid&&status.host?.start===host.start&&status.host?.boot===host.boot&&Date.now()-status.updated_at>=0&&Date.now()-status.updated_at<5000
44
+ &&presenceBudget.next({connected:status.connected,pending_id:status.presence_event_id})){
45
+ writePrivateJson(presenceBudgetPath,{sessionId,lifetime:expected.lifetime,host,checkpoint:presenceBudget.checkpoint});
46
+ process.stdout.write(PRESENCE_NOTICE+(presenceBudget.snapshot.attention_required?' Final automatic reminder for this transport notice.':'')+'\n');
47
+ }
48
+ writePrivateJson(join(directory,'monitor-state.json'),{sessionId,lifetime:expected.lifetime,pending_id:status.pending_id,...budget.snapshot,updated_at:Date.now()});
49
+ }catch{/* Disconnected/not yet configured: silent, no broker access. */}
50
+ await delay(1000);
51
+ }
52
+ }catch{process.stderr.write('psst_monitor_unavailable: native binding was not confirmed; no message consumed.\n');}
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "psst-connectors",
3
+ "owner": {
4
+ "name": "Bruno Orsolon"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "psst",
9
+ "source": {
10
+ "source": "npm",
11
+ "package": "@matuto-ai/claude-psst-connector",
12
+ "version": "0.6.0"
13
+ },
14
+ "description": "Psst messaging for Claude Code"
15
+ }
16
+ ],
17
+ "metadata": {
18
+ "description": "Native Psst agent-messaging connector for Claude Code"
19
+ }
20
+ }