@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 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 sentence like "Join the Station room at
14
- https://rooms.station.site/<id>." From the agent's shell:
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 sentence ends "with token stn_…"; add the token after the
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
- return sameStart(session.procStart, ps(pid, "lstart")) ? session : null;
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 ("Join the Station room at <url>."), so an agent
48
- // may copy the period, or the link inside quotes or angle brackets.
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.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": ["station", "claude-code", "codex", "agents", "cli"],
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",