prism-mcp-server 20.21.26 → 20.21.27
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 +20 -0
- package/dist/utils/safetyGate.js +7 -1
- package/package.json +1 -1
- package/scripts/dev/browse.py +70 -19
package/README.md
CHANGED
|
@@ -170,6 +170,26 @@ or by re-enabling after each run.
|
|
|
170
170
|
<details>
|
|
171
171
|
<summary>Release history (optional)</summary>
|
|
172
172
|
|
|
173
|
+
## What's New in v20.21.27
|
|
174
|
+
|
|
175
|
+
**Why it matters:** a script that feeds Prism Browser through a pipe can
|
|
176
|
+
now pause between blocks of commands. Before, commands that had already
|
|
177
|
+
arrived waited for the next write, and never ran if the 600 s idle timeout
|
|
178
|
+
came first, so a script that checked a result before sending more stalled
|
|
179
|
+
and then failed.
|
|
180
|
+
|
|
181
|
+
- **Every piped command runs when it arrives.** The reader waited on
|
|
182
|
+
`select()` for new pipe data while `readline()` had already buffered the
|
|
183
|
+
commands sent so far. It now reads the descriptor itself and decodes it as
|
|
184
|
+
the text stream did (its encoding, universal newlines). Measured with two
|
|
185
|
+
commands written together and a third written only after the reader hands
|
|
186
|
+
over the second: the second came out only with the third before, and
|
|
187
|
+
before the third is written now.
|
|
188
|
+
- **Unchanged:** the idle timeout, `\` line continuation (CRLF endings
|
|
189
|
+
included), comment lines, and streams `select()` cannot watch, now read
|
|
190
|
+
blocking. At the idle timeout, a command still being written is dropped,
|
|
191
|
+
never run cut short.
|
|
192
|
+
|
|
173
193
|
## What's New in v20.21.26
|
|
174
194
|
|
|
175
195
|
**Why it matters:** routine wording no longer lets a mixed request skip
|
package/dist/utils/safetyGate.js
CHANGED
|
@@ -31,7 +31,13 @@ const CRISIS_INPUT_RE = [
|
|
|
31
31
|
// Swift copy); the generator's other patterns drift from this file —
|
|
32
32
|
// reported; apply single lines, and keep the portal copy in step.
|
|
33
33
|
/(?:(?<!\b(?:a|an|the|this|that|good|great|solid|useful|our|your|my|as)\s{1,4})\bjump(?:ing)?[\s-]+off\b|\bjump(?:ing)?[\s-]+off\b(?![\s-]+points?\b))/i,
|
|
34
|
-
|
|
34
|
+
// "die", "dies", "died" and "dieing" as whole words: "how much is a diet
|
|
35
|
+
// coke" and "how to get a hoodie" matched (2026-10-15). Only the "it is to
|
|
36
|
+
// die for" idiom (ending its clause, or before and/but/so/too) and
|
|
37
|
+
// "to-die-for" are exempt; "how to die for the insurance" still matches.
|
|
38
|
+
// "starve myself to death" is named because "diet" no longer stands in for
|
|
39
|
+
// it. Same pattern as the portal copy.
|
|
40
|
+
/how\s+(?:many|much|to).*(?:pills|overdose|starv\w*\s+(?:my)?self\s+to\s+death|(?<!\b(?:is|are|was|were|s|re)\s{1,3}to\s{1,3})(?<!\bto-)\bdie(?:[sd]|ing)?\b|\bdie(?:[sd]|ing)?\b(?!\s+for(?:\s*[.!?,;:—–-]|\s*$|\s+(?:and|but|so|too)\b)|-for\b))/i,
|
|
35
41
|
// Spanish — block hyperbole nouns only (NOT hunger/thirst/cold — may be literal for neglected child)
|
|
36
42
|
/quiero\s+morir(?!\s+de\s+(?:risa|la\s+risa|vergüenza|ganas|envidia|aburrimiento)\b)/i,
|
|
37
43
|
/(?:voy\s+a\s+)?matarme(?!\s+(?:estudiando|trabajando|riendo|de\s+(?:risa|la\s+risa))\b)/i,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.21.
|
|
3
|
+
"version": "20.21.27",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Persistent session memory for AI coding agents that never leaves your machine — including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
|
|
6
6
|
"module": "index.ts",
|
package/scripts/dev/browse.py
CHANGED
|
@@ -42,8 +42,10 @@ MODES:
|
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
44
|
import argparse
|
|
45
|
+
import codecs
|
|
45
46
|
import datetime
|
|
46
47
|
import hashlib
|
|
48
|
+
import io
|
|
47
49
|
import json
|
|
48
50
|
import os
|
|
49
51
|
import random
|
|
@@ -2198,10 +2200,17 @@ def run_repl(session, sanitize=False, cleanup=False, fail_fast=False):
|
|
|
2198
2200
|
# ---------------------------------------------------------------------------
|
|
2199
2201
|
# Pipe/batch mode
|
|
2200
2202
|
# ---------------------------------------------------------------------------
|
|
2203
|
+
class _PipeIdleTimeout(Exception):
|
|
2204
|
+
"""The producer held the pipe open past the idle timeout."""
|
|
2205
|
+
|
|
2206
|
+
|
|
2201
2207
|
def _stdin_lines_with_idle_timeout(timeout_sec):
|
|
2202
2208
|
"""
|
|
2203
2209
|
Yield stdin lines, giving up if the producer holds the pipe open without
|
|
2204
|
-
sending anything.
|
|
2210
|
+
sending anything. Iterates the stream itself when it has no descriptor,
|
|
2211
|
+
and reads blocking when select() cannot watch the descriptor. Raises
|
|
2212
|
+
_PipeIdleTimeout at the timeout, so a command still being written is
|
|
2213
|
+
dropped rather than run cut short.
|
|
2205
2214
|
"""
|
|
2206
2215
|
stream = sys.stdin
|
|
2207
2216
|
try:
|
|
@@ -2209,31 +2218,73 @@ def _stdin_lines_with_idle_timeout(timeout_sec):
|
|
|
2209
2218
|
except Exception:
|
|
2210
2219
|
yield from stream
|
|
2211
2220
|
return
|
|
2221
|
+
# Read the descriptor directly and split lines here. stream.readline()
|
|
2222
|
+
# pulled a whole chunk into Python's own buffer and returned one line; the
|
|
2223
|
+
# rest sat there while select() watched the empty pipe, so commands that
|
|
2224
|
+
# had already arrived waited for the producer's next write, or for this
|
|
2225
|
+
# timeout. A producer that pauses between blocks (waiting on a result the
|
|
2226
|
+
# held-back commands would produce) deadlocked until then (2026-10-16).
|
|
2227
|
+
#
|
|
2228
|
+
# Decode as the text stream did: one incremental decoder for the whole
|
|
2229
|
+
# stream, in its encoding and error policy, with universal newlines. A
|
|
2230
|
+
# character split across two reads stays whole, and CRLF and a bare CR
|
|
2231
|
+
# both end a line, so a backslash continuation written from Windows
|
|
2232
|
+
# still joins.
|
|
2233
|
+
decoder = io.IncrementalNewlineDecoder(
|
|
2234
|
+
codecs.getincrementaldecoder(getattr(stream, "encoding", None) or "utf-8")(
|
|
2235
|
+
getattr(stream, "errors", None) or "strict"),
|
|
2236
|
+
translate=True,
|
|
2237
|
+
)
|
|
2238
|
+
pending = ""
|
|
2239
|
+
selectable = True
|
|
2212
2240
|
while True:
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
yield
|
|
2217
|
-
|
|
2218
|
-
if
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2241
|
+
newline = pending.find("\n")
|
|
2242
|
+
if newline >= 0:
|
|
2243
|
+
line, pending = pending[:newline + 1], pending[newline + 1:]
|
|
2244
|
+
yield line
|
|
2245
|
+
continue
|
|
2246
|
+
if selectable:
|
|
2247
|
+
try:
|
|
2248
|
+
ready, _, _ = select.select([fileno], [], [], timeout_sec)
|
|
2249
|
+
except Exception:
|
|
2250
|
+
# A descriptor select() cannot watch (a Windows pipe): read it
|
|
2251
|
+
# blocking from here on, keeping any partial line already read.
|
|
2252
|
+
selectable = False
|
|
2253
|
+
ready = True
|
|
2254
|
+
if not ready:
|
|
2255
|
+
audit_log("pipe_idle_timeout", "", f"timeout={timeout_sec}s")
|
|
2256
|
+
print(json.dumps({
|
|
2257
|
+
"status": "error",
|
|
2258
|
+
"action": "pipe",
|
|
2259
|
+
"message": f"No input for {timeout_sec}s. Closing browser to prevent zombie process.",
|
|
2260
|
+
}))
|
|
2261
|
+
sys.stdout.flush()
|
|
2262
|
+
raise _PipeIdleTimeout()
|
|
2263
|
+
chunk = os.read(fileno, 65536)
|
|
2264
|
+
if not chunk:
|
|
2265
|
+
pending += decoder.decode(b"", final=True)
|
|
2266
|
+
if pending:
|
|
2267
|
+
yield pending
|
|
2229
2268
|
return
|
|
2230
|
-
|
|
2269
|
+
pending += decoder.decode(chunk)
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
def _pipe_commands(timeout_sec):
|
|
2273
|
+
"""
|
|
2274
|
+
The pipe's commands, continuations joined. At the idle timeout the reader
|
|
2275
|
+
raises before the joiner can flush, so a continued command whose tail never
|
|
2276
|
+
came is dropped, not run as its head alone.
|
|
2277
|
+
"""
|
|
2278
|
+
try:
|
|
2279
|
+
yield from _join_continuations(_stdin_lines_with_idle_timeout(timeout_sec))
|
|
2280
|
+
except _PipeIdleTimeout:
|
|
2281
|
+
return
|
|
2231
2282
|
|
|
2232
2283
|
|
|
2233
2284
|
def run_pipe(session, sanitize=False, cleanup=False, fail_fast=False, cleanup_files=None):
|
|
2234
2285
|
"""Read commands from stdin, one per line. Returns True when all succeeded."""
|
|
2235
2286
|
had_error = False
|
|
2236
|
-
for line in
|
|
2287
|
+
for line in _pipe_commands(PIPE_IDLE_TIMEOUT):
|
|
2237
2288
|
line = line.strip()
|
|
2238
2289
|
if not line or line.startswith('#'):
|
|
2239
2290
|
continue
|