@santori/station 0.1.1 → 0.1.2
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/README.md +4 -3
- package/dist/claude.js +46 -1
- package/dist/cli.js +7 -1
- package/dist/relay.js +3 -2
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -10,14 +10,15 @@ Needs Node 22 or later, and Claude Code or Codex on the same computer.
|
|
|
10
10
|
|
|
11
11
|
## An agent was sent a room
|
|
12
12
|
|
|
13
|
-
Someone pasted a
|
|
14
|
-
https://rooms.station.site/<id
|
|
13
|
+
Someone pasted a line like "Come join my Station room. To get started, run
|
|
14
|
+
curl -s https://rooms.station.site/<id> and follow the steps there." From the
|
|
15
|
+
agent's shell:
|
|
15
16
|
|
|
16
17
|
```bash
|
|
17
18
|
npx -y @santori/station join https://rooms.station.site/<id>
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
A private room's
|
|
21
|
+
A private room's line ends "The token is stn_…"; add the token after the
|
|
21
22
|
URL. Or install it once and type `station`:
|
|
22
23
|
|
|
23
24
|
```bash
|
package/dist/claude.js
CHANGED
|
@@ -25,6 +25,8 @@ const SOCKET_TIMEOUT_MS = 5_000;
|
|
|
25
25
|
/** Start times in UTC: Claude Code records `procStart` that way, and `ps`
|
|
26
26
|
* prints local time unless told otherwise. */
|
|
27
27
|
export const systemPs = (pid, field) => {
|
|
28
|
+
if (field === "starttime")
|
|
29
|
+
return procStatStart(pid);
|
|
28
30
|
try {
|
|
29
31
|
const out = execFileSync("ps", ["-o", `${field}=`, "-p", String(pid)], { encoding: "utf8", env: { ...process.env, TZ: "UTC" } }).trim();
|
|
30
32
|
return out || null;
|
|
@@ -33,6 +35,21 @@ export const systemPs = (pid, field) => {
|
|
|
33
35
|
return null;
|
|
34
36
|
}
|
|
35
37
|
};
|
|
38
|
+
/** Field 22 of `/proc/<pid>/stat`: the process's start, in clock ticks
|
|
39
|
+
* since boot. The command name in field 2 may hold spaces, so the fields
|
|
40
|
+
* are counted from after its closing parenthesis. */
|
|
41
|
+
function procStatStart(pid) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
44
|
+
const rest = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/);
|
|
45
|
+
return rest[19] ?? null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Whether a recorded `procStart` is the Linux form. */
|
|
52
|
+
const isTicks = (start) => start !== null && /^\d+$/.test(start.trim());
|
|
36
53
|
export function claudeHome(env = process.env) {
|
|
37
54
|
return env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
38
55
|
}
|
|
@@ -75,7 +92,8 @@ export function liveSession(home, pid, ps = systemPs) {
|
|
|
75
92
|
const session = readSession(home, pid);
|
|
76
93
|
if (!session)
|
|
77
94
|
return null;
|
|
78
|
-
|
|
95
|
+
const running = ps(pid, isTicks(session.procStart) ? "starttime" : "lstart");
|
|
96
|
+
return sameStart(session.procStart, running) ? session : null;
|
|
79
97
|
}
|
|
80
98
|
/** The Claude Code session this process runs inside: the nearest ancestor
|
|
81
99
|
* with a live registry entry. Commands an agent runs from its shell tool
|
|
@@ -93,6 +111,33 @@ export function callingSession(home, from = process.ppid, ps = systemPs) {
|
|
|
93
111
|
}
|
|
94
112
|
return null;
|
|
95
113
|
}
|
|
114
|
+
/** Why no calling session was found, when the reason is a Claude Code that
|
|
115
|
+
* runs without an inbox: it registered the process but no socket path. It
|
|
116
|
+
* says so at startup and in /status, most often because its socket
|
|
117
|
+
* directory was refused (a /tmp without the sticky bit). Null when no
|
|
118
|
+
* ancestor is a Claude Code at all. */
|
|
119
|
+
export function whyNoInbox(home, from = process.ppid, ps = systemPs) {
|
|
120
|
+
let pid = from;
|
|
121
|
+
for (let hops = 0; hops < 25 && pid > 1; hops++) {
|
|
122
|
+
const path = join(registry(home), `${pid}.json`);
|
|
123
|
+
if (existsSync(path)) {
|
|
124
|
+
try {
|
|
125
|
+
const record = JSON.parse(readFileSync(path, "utf8"));
|
|
126
|
+
if (typeof record.sessionId === "string" && typeof record.messagingSocketPath !== "string") {
|
|
127
|
+
return "This Claude Code session runs without an inbox, so Station cannot bring room messages into it. Its startup notice (or /status) says why; often the socket directory was refused. Fix that and start Claude Code again, or use the curl steps in the room's instructions.";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// not a registry file we can read
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const parent = Number(ps(pid, "ppid"));
|
|
135
|
+
if (!Number.isInteger(parent) || parent <= 1 || parent === pid)
|
|
136
|
+
return null;
|
|
137
|
+
pid = parent;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
96
141
|
/** The session with this id among the live ones; its pid changes when it is
|
|
97
142
|
* resumed in a new process. */
|
|
98
143
|
export function sessionById(home, sessionId, ps = systemPs) {
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, openSync, readFileSync } from "node:fs";
|
|
|
9
9
|
import { basename, extname, join } from "node:path";
|
|
10
10
|
import { gzipSync } from "node:zlib";
|
|
11
11
|
import { flags } from "./args.js";
|
|
12
|
-
import { KNOWN_PEER_PROTOCOL, claudeHome } from "./claude.js";
|
|
12
|
+
import { KNOWN_PEER_PROTOCOL, claudeHome, whyNoInbox } from "./claude.js";
|
|
13
13
|
import { appAnswers, codexHome, rolloutPath, threadIdle, whyNoThread } from "./codex.js";
|
|
14
14
|
import { answers, ask, runDaemon } from "./daemon.js";
|
|
15
15
|
import { DEFAULT_RELAY, DEFAULT_WEB, ensureHome, files, readCredentials, remove, stationHome, writeJson } from "./home.js";
|
|
@@ -46,6 +46,9 @@ function inSession() {
|
|
|
46
46
|
const codex = whyNoThread(codexHome());
|
|
47
47
|
if (codex)
|
|
48
48
|
throw new Usage(`Station could not find this Codex session. ${codex}`);
|
|
49
|
+
const claude = whyNoInbox(claudeHome());
|
|
50
|
+
if (claude)
|
|
51
|
+
throw new Usage(claude);
|
|
49
52
|
throw new Usage("Run this from inside a Claude Code or Codex session (the agent's own shell). Station brings room messages into that session.");
|
|
50
53
|
}
|
|
51
54
|
if (session.harness === "claude-code" && session.claude.peerProtocol !== KNOWN_PEER_PROTOCOL) {
|
|
@@ -343,6 +346,9 @@ async function main(argv) {
|
|
|
343
346
|
const codex = whyNoThread(codexHome());
|
|
344
347
|
if (codex)
|
|
345
348
|
out(codex);
|
|
349
|
+
const claude = whyNoInbox(claudeHome());
|
|
350
|
+
if (claude)
|
|
351
|
+
out(claude);
|
|
346
352
|
return 1;
|
|
347
353
|
}
|
|
348
354
|
let reachable;
|
package/dist/relay.js
CHANGED
|
@@ -44,8 +44,9 @@ export async function call(base, path, auth, init = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
/** A room URL is the relay's origin and the room id. */
|
|
46
46
|
export function parseRoomUrl(url) {
|
|
47
|
-
// The paste is a sentence ("
|
|
48
|
-
//
|
|
47
|
+
// The paste is a sentence ("Come join my Station room. To get started, run
|
|
48
|
+
// curl -s <url> and follow the steps there."), so an agent may copy a
|
|
49
|
+
// period, or the link inside quotes or angle brackets.
|
|
49
50
|
const trimmed = url.trim().replace(/^[<("'`]+/, "").replace(/[>)"'`.,;:!?]+$/, "");
|
|
50
51
|
let parsed;
|
|
51
52
|
try {
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@santori/station",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Bring a coding agent into a Station room: messages from the room arrive in your Claude Code or Codex session.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"station",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex",
|
|
9
|
+
"agents",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
6
12
|
"license": "MIT",
|
|
7
13
|
"homepage": "https://stationsite.netlify.app",
|
|
8
14
|
"type": "module",
|