@tryinget/pi-activity-strip 0.2.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.
@@ -0,0 +1,202 @@
1
+ import { execFile } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { promisify } from "node:util";
5
+ /** @typedef {import("@earendil-works/pi-coding-agent").ExtensionAPI} ExtensionAPI */
6
+ /** @typedef {import("../src/common/contracts.ts").BeforeAgentStartEventLike} BeforeAgentStartEventLike */
7
+ /** @typedef {import("../src/common/contracts.ts").MessageUpdateEventLike} MessageUpdateEventLike */
8
+ /** @typedef {import("../src/common/contracts.ts").SessionStartContextLike} SessionStartContextLike */
9
+ /** @typedef {import("../src/common/contracts.ts").ToolExecutionEventLike} ToolExecutionEventLike */
10
+ /** @typedef {import("../src/common/contracts.ts").TurnStartEventLike} TurnStartEventLike */
11
+ import { getBrokerStatus, requestBrokerShutdown } from "../src/client/broker-client.mjs";
12
+ import { ensureActivityStripRunning } from "../src/client/launcher.mjs";
13
+ import { createSessionTelemetry } from "../src/client/session-telemetry.mjs";
14
+ import {
15
+ formatBrokerRuntimeStatus,
16
+ summarizeBrokerRuntimeStatus,
17
+ } from "../src/common/status-report.mjs";
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+ const packageRoot = path.resolve(__dirname, "..");
22
+ const launcherPath = path.join(packageRoot, "bin", "pi-activity-strip.mjs");
23
+ const execFileAsync = promisify(execFile);
24
+
25
+ /** @typedef {SessionStartContextLike & { hasUI?: boolean; ui?: { notify?: (message: string, level?: "info" | "warning" | "error") => void; editor?: (title: string, text: string) => Promise<unknown> } }} UiContextLike */
26
+
27
+ function wantsAutostart() {
28
+ return process.env.PI_ACTIVITY_STRIP_AUTO_START !== "0";
29
+ }
30
+
31
+ /** @param {ExtensionAPI} pi */
32
+ export default function activityStripExtension(pi) {
33
+ const telemetry = createSessionTelemetry({
34
+ pi,
35
+ cwd: process.cwd(),
36
+ sessionName: "",
37
+ });
38
+
39
+ /** @param {UiContextLike} ctx @param {boolean} [announce] */
40
+ async function ensureOpen(ctx, announce = true) {
41
+ const result = await ensureActivityStripRunning(launcherPath);
42
+ if (!ctx?.hasUI || !announce) return result;
43
+ if (!result.ok) {
44
+ ctx.ui?.notify?.(result.error ?? "Activity strip did not start", "error");
45
+ return result;
46
+ }
47
+ ctx.ui?.notify?.(
48
+ result.started ? "Activity strip opened" : "Activity strip already running",
49
+ "info",
50
+ );
51
+ return result;
52
+ }
53
+
54
+ pi.registerCommand("activity-strip", {
55
+ description: "Open or check the top-row activity strip",
56
+ /** @param {string} args @param {UiContextLike} ctx */
57
+ handler: async (args, ctx) => {
58
+ const action = String(args ?? "")
59
+ .trim()
60
+ .toLowerCase();
61
+ if (action === "status") {
62
+ try {
63
+ const result = await getBrokerStatus();
64
+ if (ctx.hasUI && typeof ctx.ui?.editor === "function") {
65
+ await ctx.ui.editor("Activity Strip Status", formatBrokerRuntimeStatus(result));
66
+ } else if (ctx.hasUI) {
67
+ const summary = summarizeBrokerRuntimeStatus(result);
68
+ ctx.ui?.notify?.(summary.headline, summary.level);
69
+ }
70
+ } catch {
71
+ if (ctx.hasUI && typeof ctx.ui?.editor === "function") {
72
+ await ctx.ui.editor("Activity Strip Status", "stopped");
73
+ } else if (ctx.hasUI) {
74
+ ctx.ui?.notify?.("Activity strip is stopped", "warning");
75
+ }
76
+ }
77
+ return;
78
+ }
79
+
80
+ if (action === "doctor") {
81
+ try {
82
+ const { stdout } = await execFileAsync(process.execPath, [launcherPath, "doctor"], {
83
+ env: process.env,
84
+ });
85
+ if (ctx.hasUI && typeof ctx.ui?.editor === "function") {
86
+ await ctx.ui.editor("Activity Strip Doctor", stdout.trim() || "No output");
87
+ } else if (ctx.hasUI) {
88
+ ctx.ui?.notify?.("Opened activity strip compatibility report", "info");
89
+ }
90
+ } catch (error) {
91
+ const message = error instanceof Error ? error.message : "Activity strip doctor failed";
92
+ if (ctx.hasUI && typeof ctx.ui?.editor === "function") {
93
+ await ctx.ui.editor("Activity Strip Doctor", message);
94
+ } else if (ctx.hasUI) {
95
+ ctx.ui?.notify?.(message, "error");
96
+ }
97
+ }
98
+ return;
99
+ }
100
+
101
+ if (action === "fix-top") {
102
+ try {
103
+ await execFileAsync(process.execPath, [launcherPath, "fix-top"], {
104
+ env: process.env,
105
+ });
106
+ if (ctx.hasUI) ctx.ui?.notify?.("Moved activity strip to the top edge", "info");
107
+ } catch (error) {
108
+ if (ctx.hasUI) {
109
+ ctx.ui?.notify?.(
110
+ error instanceof Error ? error.message : "Failed to move activity strip",
111
+ "error",
112
+ );
113
+ }
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (action === "stop") {
119
+ try {
120
+ const result = await requestBrokerShutdown();
121
+ if (ctx.hasUI)
122
+ ctx.ui?.notify?.(
123
+ result?.ok ? "Stopping activity strip" : "Activity strip is not running",
124
+ result?.ok ? "info" : "warning",
125
+ );
126
+ } catch {
127
+ if (ctx.hasUI) ctx.ui?.notify?.("Activity strip is not running", "warning");
128
+ }
129
+ return;
130
+ }
131
+
132
+ await ensureOpen(ctx, true);
133
+ },
134
+ });
135
+
136
+ pi.registerCommand("activity-strip-stop", {
137
+ description: "Stop the running activity strip broker/window",
138
+ /** @param {string} _args @param {UiContextLike} ctx */
139
+ handler: async (_args, ctx) => {
140
+ try {
141
+ const result = await requestBrokerShutdown();
142
+ if (ctx.hasUI)
143
+ ctx.ui?.notify?.(
144
+ result?.ok ? "Stopping activity strip" : "Activity strip is not running",
145
+ result?.ok ? "info" : "warning",
146
+ );
147
+ } catch {
148
+ if (ctx.hasUI) ctx.ui?.notify?.("Activity strip is not running", "warning");
149
+ }
150
+ },
151
+ });
152
+
153
+ /** @param {unknown} _event @param {UiContextLike} ctx */
154
+ pi.on("session_start", async (_event, ctx) => {
155
+ await telemetry.onSessionStart(ctx);
156
+ if (ctx.hasUI && wantsAutostart()) {
157
+ await ensureOpen(ctx, false);
158
+ }
159
+ });
160
+
161
+ /** @param {BeforeAgentStartEventLike} event */
162
+ pi.on("before_agent_start", async (event) => {
163
+ telemetry.onBeforeAgentStart(event);
164
+ });
165
+
166
+ /** @param {TurnStartEventLike} event */
167
+ pi.on("turn_start", async (event) => {
168
+ telemetry.onTurnStart(event);
169
+ });
170
+
171
+ /** @param {MessageUpdateEventLike} event */
172
+ pi.on("message_update", async (event) => {
173
+ telemetry.onMessageUpdate(event);
174
+ });
175
+
176
+ /** @param {ToolExecutionEventLike} event */
177
+ pi.on("tool_execution_start", async (event) => {
178
+ telemetry.onToolExecutionStart(event);
179
+ });
180
+
181
+ /** @param {ToolExecutionEventLike} event */
182
+ pi.on("tool_execution_update", async (event) => {
183
+ telemetry.onToolExecutionUpdate(event);
184
+ });
185
+
186
+ /** @param {ToolExecutionEventLike} event */
187
+ pi.on("tool_execution_end", async (event) => {
188
+ telemetry.onToolExecutionEnd(event);
189
+ });
190
+
191
+ pi.on("turn_end", async () => {
192
+ telemetry.onTurnEnd();
193
+ });
194
+
195
+ pi.on("agent_settled", async () => {
196
+ telemetry.onAgentSettled();
197
+ });
198
+
199
+ pi.on("session_shutdown", async () => {
200
+ await telemetry.shutdown();
201
+ });
202
+ }
@@ -0,0 +1 @@
1
+ export { default } from "./activity-strip.js";
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@tryinget/pi-activity-strip",
3
+ "version": "0.2.0",
4
+ "description": "pi extension package for activity-strip workflows in monorepo runtime",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tryingET/pi-extensions.git",
10
+ "directory": "packages/pi-activity-strip"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/tryingET/pi-extensions/issues"
14
+ },
15
+ "homepage": "https://github.com/tryingET/pi-extensions/tree/main/packages/pi-activity-strip",
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi-extension",
19
+ "activity-strip",
20
+ "monorepo"
21
+ ],
22
+ "publishConfig": {
23
+ "registry": "https://registry.npmjs.org/",
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.19.0"
28
+ },
29
+ "scripts": {
30
+ "fix": "bash ./scripts/quality-gate.sh fix",
31
+ "lint": "bash ./scripts/quality-gate.sh lint",
32
+ "typecheck": "bash ./scripts/quality-gate.sh typecheck",
33
+ "quality:pre-commit": "bash ./scripts/quality-gate.sh pre-commit",
34
+ "quality:pre-push": "bash ./scripts/quality-gate.sh pre-push",
35
+ "quality:ci": "bash ./scripts/quality-gate.sh ci",
36
+ "check": "npm run quality:ci",
37
+ "test": "npm run quality:ci",
38
+ "docs:list": "bash ./scripts/docs-list.sh",
39
+ "docs:list:workspace": "bash ./scripts/docs-list.sh --workspace --discover",
40
+ "docs:list:json": "bash ./scripts/docs-list.sh --json",
41
+ "release:check": "bash ./scripts/release-check.sh",
42
+ "release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh",
43
+ "strip:open": "node ./bin/pi-activity-strip.mjs open",
44
+ "strip:status": "node ./bin/pi-activity-strip.mjs status",
45
+ "strip:doctor": "node ./bin/pi-activity-strip.mjs doctor",
46
+ "strip:snapshot": "node ./bin/pi-activity-strip.mjs snapshot",
47
+ "strip:fix-top": "node ./bin/pi-activity-strip.mjs fix-top",
48
+ "strip:stop": "node ./bin/pi-activity-strip.mjs stop",
49
+ "capture:strip": "bash ./scripts/capture-strip-window.sh",
50
+ "capture:top": "bash ./scripts/capture-top-band.sh 180",
51
+ "demo:simulate": "node ./examples/simulate-sessions.mjs",
52
+ "smoke:headless-live": "bash ./scripts/live-headless-smoke.sh"
53
+ },
54
+ "files": [
55
+ "bin",
56
+ "extensions/activity-strip.js",
57
+ "extensions/activity-strip.ts",
58
+ "src",
59
+ "prompts",
60
+ "examples",
61
+ "policy/security-policy.json"
62
+ ],
63
+ "pi": {
64
+ "extensions": [
65
+ "./extensions/activity-strip.js"
66
+ ],
67
+ "prompts": [
68
+ "./prompts"
69
+ ]
70
+ },
71
+ "x-pi-template": {
72
+ "scaffoldMode": "simple-package",
73
+ "workspacePath": "packages/pi-activity-strip",
74
+ "releaseComponent": "pi-activity-strip",
75
+ "releaseConfigMode": "component"
76
+ },
77
+ "devDependencies": {
78
+ "@biomejs/biome": "2.4.4",
79
+ "@earendil-works/pi-ai": "^0.80.6",
80
+ "@earendil-works/pi-coding-agent": "^0.80.6",
81
+ "@types/node": "22.14.0",
82
+ "@typescript/native-preview": "7.0.0-dev.20260417.1",
83
+ "typebox": "^1.0.0",
84
+ "typescript": "5.9.2"
85
+ },
86
+ "overrides": {
87
+ "fast-xml-parser": "5.3.6"
88
+ },
89
+ "peerDependencies": {
90
+ "@earendil-works/pi-ai": "*",
91
+ "@earendil-works/pi-coding-agent": "*",
92
+ "typebox": "*"
93
+ }
94
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "default": {
3
+ "maxRiskScore": 70,
4
+ "requireIntegrity": true,
5
+ "requireSignatures": false,
6
+ "allowNpmDiffFallback": false,
7
+ "minReleaseAgeHours": 0
8
+ },
9
+ "packages": {}
10
+ }
@@ -0,0 +1,21 @@
1
+ ---
2
+ summary: "Draft an implementation plan for a requested pi-activity-strip change"
3
+ read_when:
4
+ - "Using or maintaining this package prompt template."
5
+ - "Checking prompt metadata for pi package discovery or docs-list validation."
6
+ description: Draft an implementation plan for a requested pi-activity-strip change
7
+ system4d:
8
+ container: "Prompt template for implementation planning."
9
+ compass: "Turn requests into actionable, risk-aware plans."
10
+ engine: "Scope -> tasks -> validation -> rollout."
11
+ fog: "Hidden constraints unless assumptions are surfaced."
12
+ ---
13
+
14
+ Create a pi-activity-strip implementation plan for this request: $@
15
+
16
+ Include:
17
+ - Scope and non-goals
18
+ - Key risks and mitigations
19
+ - Step-by-step implementation tasks
20
+ - Validation commands and expected outcomes
21
+ - Rollout and rollback notes
@@ -0,0 +1,21 @@
1
+ ---
2
+ summary: "Review a pi-activity-strip change for security risks and mitigations"
3
+ read_when:
4
+ - "Using or maintaining this package prompt template."
5
+ - "Checking prompt metadata for pi package discovery or docs-list validation."
6
+ description: Review a pi-activity-strip change for security risks and mitigations
7
+ system4d:
8
+ container: "Prompt template for security-focused review."
9
+ compass: "Identify practical vulnerabilities before release."
10
+ engine: "Threats -> impact -> mitigations -> verification."
11
+ fog: "Partial context can hide exploit paths."
12
+ ---
13
+
14
+ Review this pi-activity-strip change for security concerns: $@
15
+
16
+ Focus on:
17
+ - Input validation and injection risk
18
+ - Privilege boundaries and secret handling
19
+ - Dependency and supply-chain risk
20
+ - Safe failure modes and logging
21
+ - Concrete remediations with priority
@@ -0,0 +1,165 @@
1
+ import { EventEmitter } from "node:events";
2
+ import fs from "node:fs";
3
+ import net from "node:net";
4
+ /** @typedef {import("../common/contracts.ts").ActivityStripBrokerOptions} ActivityStripBrokerOptions */
5
+ import {
6
+ ACTIVITY_STRIP_BROADCAST_TICK_MS,
7
+ ACTIVITY_STRIP_SOCKET_DIR,
8
+ ACTIVITY_STRIP_SOCKET_PATH,
9
+ } from "../common/constants.mjs";
10
+ import { SessionStore } from "./session-store.mjs";
11
+
12
+ /** @param {string} filePath */
13
+ function safeUnlink(filePath) {
14
+ try {
15
+ fs.unlinkSync(filePath);
16
+ } catch (error) {
17
+ if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
18
+ throw error;
19
+ }
20
+ }
21
+ }
22
+
23
+ /** @param {unknown} error */
24
+ function isExpectedClientDisconnect(error) {
25
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
26
+ return code === "ECONNRESET" || code === "EPIPE" || code === "ERR_STREAM_DESTROYED";
27
+ }
28
+
29
+ /** @param {string} line */
30
+ function parseLine(line) {
31
+ const trimmed = String(line ?? "").trim();
32
+ if (!trimmed) return null;
33
+ return JSON.parse(trimmed);
34
+ }
35
+
36
+ export class ActivityStripBroker extends EventEmitter {
37
+ /** @param {ActivityStripBrokerOptions} [options] */
38
+ constructor(options = {}) {
39
+ super();
40
+ this.socketPath = options.socketPath ?? ACTIVITY_STRIP_SOCKET_PATH;
41
+ this.socketDir = options.socketDir ?? ACTIVITY_STRIP_SOCKET_DIR;
42
+ this.store = options.store ?? new SessionStore();
43
+ this.getRuntimeStatus = options.getRuntimeStatus ?? (() => undefined);
44
+ this.server = net.createServer((socket) => this.handleConnection(socket));
45
+ this.tick = null;
46
+ }
47
+
48
+ async start() {
49
+ fs.mkdirSync(this.socketDir, { recursive: true });
50
+ safeUnlink(this.socketPath);
51
+
52
+ await new Promise((resolve, reject) => {
53
+ this.server.once("error", reject);
54
+ this.server.listen(this.socketPath, () => {
55
+ this.server.off("error", reject);
56
+ resolve(undefined);
57
+ });
58
+ });
59
+
60
+ this.tick = setInterval(() => {
61
+ this.emitSnapshot();
62
+ }, ACTIVITY_STRIP_BROADCAST_TICK_MS);
63
+ this.tick.unref?.();
64
+ this.emitSnapshot();
65
+ }
66
+
67
+ emitSnapshot() {
68
+ this.emit("snapshot", this.store.snapshot());
69
+ }
70
+
71
+ /** @param {import("node:net").Socket} socket @param {Record<string, unknown>} message */
72
+ reply(socket, message) {
73
+ if (socket.destroyed || !socket.writable) return;
74
+ try {
75
+ socket.write(`${JSON.stringify(message)}\n`, (error) => {
76
+ if (error && !isExpectedClientDisconnect(error)) this.emit("client-error", error);
77
+ });
78
+ } catch (error) {
79
+ if (!isExpectedClientDisconnect(error)) this.emit("client-error", error);
80
+ }
81
+ }
82
+
83
+ /** @param {import("node:net").Socket} socket */
84
+ handleConnection(socket) {
85
+ let buffer = "";
86
+ socket.setEncoding("utf8");
87
+ socket.on("error", (error) => {
88
+ if (!isExpectedClientDisconnect(error)) this.emit("client-error", error);
89
+ });
90
+
91
+ socket.on("data", (chunk) => {
92
+ buffer += chunk;
93
+ let newlineIndex = buffer.indexOf("\n");
94
+ while (newlineIndex >= 0) {
95
+ const line = buffer.slice(0, newlineIndex);
96
+ buffer = buffer.slice(newlineIndex + 1);
97
+ this.handleMessage(socket, line);
98
+ newlineIndex = buffer.indexOf("\n");
99
+ }
100
+ });
101
+ }
102
+
103
+ /** @param {import("node:net").Socket} socket @param {string} line */
104
+ handleMessage(socket, line) {
105
+ let message;
106
+ try {
107
+ message = parseLine(line);
108
+ } catch (error) {
109
+ this.reply(socket, {
110
+ ok: false,
111
+ error: error instanceof Error ? error.message : String(error),
112
+ });
113
+ return;
114
+ }
115
+
116
+ if (!message) return;
117
+
118
+ switch (message.type) {
119
+ case "ping":
120
+ this.reply(socket, {
121
+ ok: true,
122
+ type: "pong",
123
+ snapshot: this.store.snapshot(),
124
+ runtimeStatus: this.getRuntimeStatus(),
125
+ });
126
+ return;
127
+ case "shutdown":
128
+ this.reply(socket, { ok: true, type: "shutdown" });
129
+ setTimeout(() => {
130
+ this.emit("shutdown-requested");
131
+ }, 20);
132
+ return;
133
+ case "remove":
134
+ this.store.remove(message.sessionId);
135
+ this.emitSnapshot();
136
+ return;
137
+ case "upsert":
138
+ this.store.upsert(message.session);
139
+ this.emitSnapshot();
140
+ return;
141
+ default:
142
+ this.reply(socket, { ok: false, error: `Unsupported message type: ${message.type}` });
143
+ }
144
+ }
145
+
146
+ async stop() {
147
+ if (this.tick) {
148
+ clearInterval(this.tick);
149
+ this.tick = null;
150
+ }
151
+
152
+ await new Promise((resolve) => {
153
+ this.server.close(() => resolve(undefined));
154
+ });
155
+
156
+ safeUnlink(this.socketPath);
157
+ }
158
+ }
159
+
160
+ /** @param {ActivityStripBrokerOptions} [options] */
161
+ export async function createActivityStripBroker(options = {}) {
162
+ const broker = new ActivityStripBroker(options);
163
+ await broker.start();
164
+ return broker;
165
+ }
@@ -0,0 +1,49 @@
1
+ import { ACTIVITY_STRIP_STALE_AFTER_MS } from "../common/constants.mjs";
2
+ import { normalizeSessionSnapshot, sortSessions } from "../common/protocol.mjs";
3
+
4
+ /** @typedef {import("../common/contracts.ts").SessionSnapshot} SessionSnapshot */
5
+ /** @typedef {import("../common/contracts.ts").SessionStoreOptions} SessionStoreOptions */
6
+ /** @typedef {import("../common/contracts.ts").BrokerSnapshot} BrokerSnapshot */
7
+
8
+ export class SessionStore {
9
+ /** @param {SessionStoreOptions} [options] */
10
+ constructor(options = {}) {
11
+ this.staleAfterMs = options.staleAfterMs ?? ACTIVITY_STRIP_STALE_AFTER_MS;
12
+ /** @type {Map<string, SessionSnapshot>} */
13
+ this.sessions = new Map();
14
+ }
15
+
16
+ /** @param {Partial<SessionSnapshot> | Record<string, unknown>} session */
17
+ upsert(session) {
18
+ const normalized = normalizeSessionSnapshot(session);
19
+ if (!normalized.sessionId) return false;
20
+ this.sessions.set(normalized.sessionId, normalized);
21
+ return true;
22
+ }
23
+
24
+ /** @param {string} sessionId */
25
+ remove(sessionId) {
26
+ return this.sessions.delete(String(sessionId ?? ""));
27
+ }
28
+
29
+ /** @param {number} [now] */
30
+ purge(now = Date.now()) {
31
+ let changed = false;
32
+ for (const [sessionId, session] of this.sessions.entries()) {
33
+ if (now - session.updatedAt > this.staleAfterMs) {
34
+ this.sessions.delete(sessionId);
35
+ changed = true;
36
+ }
37
+ }
38
+ return changed;
39
+ }
40
+
41
+ /** @param {number} [now] @returns {BrokerSnapshot} */
42
+ snapshot(now = Date.now()) {
43
+ this.purge(now);
44
+ return {
45
+ generatedAt: now,
46
+ sessions: sortSessions([...this.sessions.values()]),
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,115 @@
1
+ import net from "node:net";
2
+ /** @typedef {import("../common/contracts.ts").BrokerClientOptions} BrokerClientOptions */
3
+ /** @typedef {import("../common/contracts.ts").BrokerResponse} BrokerResponse */
4
+ /** @typedef {import("../common/contracts.ts").SessionSnapshot} SessionSnapshot */
5
+ import {
6
+ ACTIVITY_STRIP_CONNECT_TIMEOUT_MS,
7
+ ACTIVITY_STRIP_SOCKET_PATH,
8
+ } from "../common/constants.mjs";
9
+ import { makeMessage } from "../common/protocol.mjs";
10
+
11
+ /** @param {Record<string, unknown>} message @param {BrokerClientOptions} [options] @returns {Promise<BrokerResponse>} */
12
+ export function sendBrokerMessage(message, options = {}) {
13
+ const expectReply = Boolean(options.expectReply);
14
+ const timeoutMs =
15
+ Number(options.timeoutMs ?? ACTIVITY_STRIP_CONNECT_TIMEOUT_MS) ||
16
+ ACTIVITY_STRIP_CONNECT_TIMEOUT_MS;
17
+ const socketPath = options.socketPath ?? ACTIVITY_STRIP_SOCKET_PATH;
18
+
19
+ return new Promise((resolve, reject) => {
20
+ const socket = net.createConnection(socketPath);
21
+ let settled = false;
22
+ let buffer = "";
23
+
24
+ /** @param {BrokerResponse} value */
25
+ const finishResolve = (value) => {
26
+ if (settled) return;
27
+ settled = true;
28
+ clearTimeout(timeoutId);
29
+ socket.destroy();
30
+ resolve(value);
31
+ };
32
+
33
+ /** @param {Error} error */
34
+ const finishReject = (error) => {
35
+ if (settled) return;
36
+ settled = true;
37
+ clearTimeout(timeoutId);
38
+ socket.destroy();
39
+ reject(error);
40
+ };
41
+
42
+ const timeoutId = setTimeout(() => {
43
+ finishReject(new Error(`Timed out contacting activity strip broker after ${timeoutMs}ms`));
44
+ }, timeoutMs);
45
+
46
+ socket.setEncoding("utf8");
47
+
48
+ socket.on("connect", () => {
49
+ socket.write(`${JSON.stringify(message)}\n`, () => {
50
+ if (!expectReply) {
51
+ finishResolve({ ok: true });
52
+ }
53
+ });
54
+ });
55
+
56
+ socket.on("data", (chunk) => {
57
+ if (!expectReply || settled) return;
58
+ buffer += chunk;
59
+ const newlineIndex = buffer.indexOf("\n");
60
+ if (newlineIndex < 0) return;
61
+ const line = buffer.slice(0, newlineIndex).trim();
62
+ if (!line) return;
63
+ try {
64
+ const parsed = JSON.parse(line);
65
+ finishResolve(
66
+ parsed && typeof parsed === "object"
67
+ ? /** @type {BrokerResponse} */ (parsed)
68
+ : { ok: false, error: "Invalid broker response payload" },
69
+ );
70
+ } catch (error) {
71
+ finishReject(error instanceof Error ? error : new Error(String(error)));
72
+ }
73
+ });
74
+
75
+ socket.on("error", (error) => {
76
+ finishReject(error instanceof Error ? error : new Error(String(error)));
77
+ });
78
+ });
79
+ }
80
+
81
+ /** @param {BrokerClientOptions} [options] */
82
+ export async function getBrokerStatus(options = {}) {
83
+ return await sendBrokerMessage(makeMessage("ping"), {
84
+ ...options,
85
+ expectReply: true,
86
+ });
87
+ }
88
+
89
+ /** @param {BrokerClientOptions} [options] */
90
+ export async function isBrokerAlive(options = {}) {
91
+ try {
92
+ const result = await getBrokerStatus(options);
93
+ return result?.ok === true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /** @param {BrokerClientOptions} [options] @returns {Promise<BrokerResponse>} */
100
+ export async function requestBrokerShutdown(options = {}) {
101
+ return await sendBrokerMessage(makeMessage("shutdown"), {
102
+ ...options,
103
+ expectReply: true,
104
+ });
105
+ }
106
+
107
+ /** @param {SessionSnapshot} session @param {BrokerClientOptions} [options] */
108
+ export async function publishSessionSnapshot(session, options = {}) {
109
+ await sendBrokerMessage(makeMessage("upsert", { session }), options);
110
+ }
111
+
112
+ /** @param {string} sessionId @param {BrokerClientOptions} [options] */
113
+ export async function removeSession(sessionId, options = {}) {
114
+ await sendBrokerMessage(makeMessage("remove", { sessionId }), options);
115
+ }