• .claude/skills/javascript

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sunday, May 24, 2026 14:31:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b36324bc482299535a4f3f25
    Modified Files:
    .claude/skills/javascript/SKILL.md .claude/skills/jsexec/SKILL.md .claude/skills/logs/SKILL.md .claude/skills/menus/SKILL.md .claude/skills/mqtt/SKILL.md .claude/skills/smbutils/SKILL.md .claude/skills/text/SKILL.md
    Log Message:
    .claude/skills: replace invented $SBBS env var with <sbbs> placeholder

    $SBBS isn't a standard Synchronet env var (the real ones are SBBSCTRL, SBBSNODE, SBBSEXEC) and isn't set on user systems. The shorthand
    misled another Claude session into emitting `gmake $(SBBS)` as if it
    were a real make variable. Use the literal `<sbbs>` placeholder
    instead -- visually obvious as a fill-in, no false implication of a
    shell var.

    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sunday, May 24, 2026 15:52:00
    https://gitlab.synchro.net/main/sbbs/-/commit/f27b2e8270997abb2ada9598
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    .claude/skills/javascript: document constant namespaces and terminal-capability checks

    Two new subsections after load()/require(): one breaks down where
    constants come from (host-injected globals like LOG_*/INVALID_SOCKET, class-static properties like FileBase.DETAIL/SORT and CryptContext.ALGO_*,
    and JS-library files under exec/load/ that need load()/require()); the
    other distinguishes console.term_supports(USER_UTF8) (live connection capability) from user.settings & USER_UTF8 (stored preference) and
    explains when to use which.

    Triggered by C-vs-JS namespace confusion (bare UTF8 in sbbsdefs.h vs
    USER_UTF8 in exec/load/userdefs.js, same (1<<29) value) hit during
    LLM-guru language-output work.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Friday, May 29, 2026 15:31:00
    https://gitlab.synchro.net/main/sbbs/-/commit/30e601ade1688cc1a957bb92
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    skills/javascript: note the chat_llm LLM-Guru module family

    Add the chat_llm engine, chat_llm_irc adapter, llm_tools, and llm_index to
    the stock exec/*.js inventory, plus wiki links to the new module/config/
    howto pages now published on wiki.synchro.net.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Friday, May 29, 2026 23:59:00
    https://gitlab.synchro.net/main/sbbs/-/commit/d4613a65265ef3fe028b36a7
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: how to syntax-check/test a side-effecting module

    Document the guard-and-load() idiom: gate a module's load-time entry
    point (server loop, socket connect, greeting) behind a NO_MAIN-style
    sentinel so it can be load()ed under jsexec for syntax-checking and unit-testing its helpers without running the side effect. Cites the
    stock CHAT_LLM_NO_STANDALONE / CHAT_LLM_IRC_NO_MAIN guards. Notes that syncjslint.js is a style linter (jslint) with false positives on valid SpiderMonkey regex, not a reliable syntax gate.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Friday, May 29, 2026 23:59:00
    https://gitlab.synchro.net/main/sbbs/-/commit/a923481d60d196ea331efafd
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    skills/javascript: document INI trailing-comment type-dependency

    Synchronet .ini value parsing strips a trailing "; comment" only for single-token value types (boolean via isTrue, enum via parseEnum, and integer/float/datetime via the numeric parse). STRING values keep the
    whole post-'=' text (truncsp only), so an inline comment after a
    string-valued key becomes part of the value -- the trap that corrupts URL/path/name settings. File.iniGetObject() reads every value as a raw
    string and thus never strips inline comments.

    Add a "Reading INI files (and the trailing-comment trap)" section with
    the per-type table, the rationale (single-token values have no
    delimiter ambiguity; strings do), and the history behind the
    single-token support (cceb1fbb8 isTrue after FozzTexx's sexpots.ini
    report; 7346893d6 enum-after-comment).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sunday, May 31, 2026 19:06:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8e4089207c7f521c8f2fdec0
    Modified Files:
    .claude/skills/javascript/SKILL.md ctrl/chat_llm.ini exec/chat_llm.js chat_llm_irc.js llm_index.js exec/llm_tools/relay_message.js
    Log Message:
    chat_llm: relay opt-out + caps, deterministic relay reply, ini root-section defaults

    Relay (IRC):
    - Recipients can opt out of relays ("don't relay messages to me" and
    variants; "relay me" opts back in). State in
    data/chat/<bot>_norelay.json; the relay_message tool refuses to queue
    for an opted-out nick and deliver_pending() drops anything already
    queued if the recipient opts out later.
    - Cap pending relays per recipient AND per sender (anti-flood),
    configurable via relay_max_pending (default 5), passed to the tool
    via env.
    - Deterministic relay reply: the pre-classifier now speaks the
    relay_message tool's own result text verbatim (.error / .note) and
    skips the model turn. qwen2.5:7b was observed discarding a refusal
    and fabricating a delivery promise ("I'll make sure X knows") for a
    relay it actually refused (e.g. to an opted-out recipient); the
    short-circuit removes any chance of misreporting whether the relay
    happened. Scoped to relay_message only.
    - De-hardcode the tool's queue/opt-out paths: chat_llm_irc.js passes
    relay_path/norelay_path through ctx -> env so the tool and bot share
    the same persona-derived files.
    - Move bot state (relay/mute/seen/norelay) under data/chat/.

    Config / persona:
    - chat_llm.ini defaults now live in the root (unnamed) section; a named
    [<persona>] section overrides. "default" is the reserved fallback
    persona (the root defaults) and must not be a section or a guru code.
    - ctx_from_user() canonicalizes the persona code to lowercase and maps
    blank/missing -> "default"; PERSONA in chat_llm_irc.js is lowercased
    at the source.
    - Sanitize code-derived filenames: load_index key and BOT_FILE_BASE
    parts run through safe_id() so an unfortunate code can't escape
    data/chat/ or collide via path characters.

    Docs:
    - javascript SKILL.md: document the root (unnamed) section as global
    defaults (iniGetObject(null)) and the case-fold caveat.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sunday, May 31, 2026 19:06:00
    https://gitlab.synchro.net/main/sbbs/-/commit/cffaaed443ae7d78c8c2ecdb
    Added Files:
    .claude/skills/javascript/scripts/jsobjs.py
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    skills/javascript: add jsobjs.py native-API lookup + guidance

    Parses the in-tree docs/jsobjs.html (generated by jsexec jsdocs.js) and greps the object model, so finding a native method (e.g. strftime) is one command away instead of a hand-rolled reinvention. Reads the file live each run -- auto-discovered by walking up from the script/CWD -- so it never goes stale against the checkout; falls back to a regenerate hint if missing.

    SKILL.md gains a "Before hand-rolling a utility, look for a native one" section pointing at the script and the js_global.cpp grep fallback.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thursday, June 04, 2026 19:01:00
    https://gitlab.synchro.net/main/sbbs/-/commit/ee6689ca9a399aa3c5ad1cc6
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: document js.on_exit() scope + forced-termination semantics

    js.on_exit() evaluates its handler string in the script's GLOBAL scope (it compiles the string against the global object), so a handler function nested in another function is unreachable and throws silently at exit. It also fires on forced termination (operator-terminate / SIGTERM), which -- unlike a thrown exception -- does NOT run catch/finally (verified empirically via SIGTERM + file markers). New 'Script lifecycle: exit handlers' section covers the global-scope requirement, the finally-vs-on_exit distinction, and the out-of-process testing caveat.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thursday, June 04, 2026 19:01:00
    https://gitlab.synchro.net/main/sbbs/-/commit/a915d2d944ed7b316e36e030
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: correct js.on_exit scope (door child-scope vs jsexec)

    The earlier note said on_exit evaluates "in the GLOBAL scope" -- true only for jsexec/login/timed-event modules (host evaluates against js_glob and recurses child scopes). Doors and most bbs.exec/;exec/js.exec invocations run in a fresh child js_scope (exec.cpp:595) and the host evaluates on_exit against THAT scope (exec.cpp:701) with no recursion. Registration captures the scope where js.on_exit is called -- top level = js_scope, inside a function = that function's call object -- so a handler registered inside main() is filed where a
    door's EvalOnExit(js_scope) never looks and silently never runs, even though the
    same code passes under jsexec (which recurses). This shipped as a live bug.

    Rewrite: register on_exit at top level (not inside a function); prefer try/finally for clean-unwind cleanup (a door disconnect is a clean unwind) and use on_exit only as the forced-terminate backstop; note that a jsexec wrapper does not reproduce a door's on_exit scope, so finally is the dependable path.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Saturday, June 06, 2026 15:23:00
    https://gitlab.synchro.net/main/sbbs/-/commit/90570a43c9e774e639c3ab04
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    skills/javascript: document inkey timed input + inactivity model

    Add a "Timed / non-blocking key input, and the inactivity model"
    subsection to the input/output coverage: console.inkey(mode, timeout)
    semantics (timeout in ms; K_NONE returns "" vs K_NUL returns null on
    timeout; 1-char string on a key), the getkey-enforces-idle /
    inkey-doesn't trap, and the live console inactivity properties (max_getkey_inactivity, getkey_inactivity_warning, last_getkey_activity). Verified against inkey.cpp, getkey.cpp, js_console.cpp, scfglib1.c (NOINP=0x0100, default max_getkey_inactivity=300).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sunday, June 07, 2026 18:41:00
    https://gitlab.synchro.net/main/sbbs/-/commit/e9e5884dba659e4eec2e8ee0
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    skills/javascript: load() caller-scope trap + uselect/printfile/directory gotchas

    Lessons captured from Synchronet door work:
    - load('file') runs in the CALLER's scope, so its top-level vars become locals of
    whatever function called load() -- a top-level / on_exit handler can't see a
    value load()ed inside main(); capture it into a reachable scope (the silent
    "quetzal is not defined" ReferenceError).
    - console.uselect: the title is auto-prefixed with "Select " (pass "a Game", not
    "Select a Game"); the display call's number argument is the DEFAULT item index.
    console.line_counter = 0 discards a pending auto-pager prompt before a clear. - console.printfile renders Ctrl-A codes + ANSI/CP437 by default; P_PCBOARD is
    only for PCBoard @X codes (it would misread a literal @).
    - directory() defaults to GLOB_MARK, so directory entries come back with a trailing
    '/' (self-identifying). The CWD is process-global (always ctrl/, since sbbs is
    multi-threaded) -- which is why no chdir is exposed to BBS JS; build absolute
    paths from js.exec_dir / system.*_dir.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thursday, June 11, 2026 15:38:00
    https://gitlab.synchro.net/main/sbbs/-/commit/4804fef3b8d8cbf70a850169
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: document terminal-control abstraction + K_CTRLKEYS vs K_EXTKEYS

    Two lessons from the Z-Machine v6 door work:

    - Terminal control sequences (cursor show/hide, etc.) are abstracted in
    ansiterm_lib.js (CSI ?25h/l via ansiterm.send) -- reach for that before
    hand-writing escape codes.

    - Function/cursor key input: inkey/getkey pre-translate the arrow/Home/End
    escapes into control codes (TERM_KEY_*), conflating them with Ctrl-letters.
    K_EXTKEYS still yields those conflated codes and has no F-key codes; K_CTRLKEYS
    passes control keys through AND leaves the escape sequences RAW so you can parse
    the cterm forms yourself (ESC[A/B/C/D, ESC[11~..24~) and keep arrows/F-keys
    distinct. getbyte() is the raw alternative but drops idle-disconnect + UTF-8.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Monday, July 06, 2026 20:44:00
    https://gitlab.synchro.net/main/sbbs/-/commit/371c3e49b542335fac8e210c
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: five API gotchas learned from real module work

    - MsgBase.remove_msg(): passing a header object silently returns false
    with an empty last_error; pass the message number/offset/ID.
    - bbs.text() accepts the string identifier (via text_id_map), and a
    text.dat format string can carry more %-specifiers than the name
    suggests (NodeMsgFmt has three, including the message body) -- count
    them in ctrl/text.dat before format()ing, or the leftover %s renders
    literally.
    - bbs.compare_ars() evaluates the current user only; use the User
    object's compare_ars() to test any user (e.g. other nodes' useron),
    plus how to find a program's execution_ars and enumerate in-use nodes.
    - Emit line breaks with console.newline() rather than a hand-written
    "\r\n", same terminal-correctness principle as console.creturn().
    - Don't copy P_NOPAUSE onto printfile() calls reflexively: it disables
    the default pagination, which is only right when the surrounding flow
    provides the pause.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sunday, July 12, 2026 05:11:00
    https://gitlab.synchro.net/main/sbbs/-/commit/d3c494572acac3b4fa607dbc
    Modified Files:
    .claude/skills/javascript/SKILL.md .claude/skills/jsexec/SKILL.md exec/load/door_deploy.js
    Log Message:
    skills: an install-xtrn [exec:] child must use only the CONTEXT INTERSECTION

    The `env` object I documented a moment ago has a sharper caveat than "jsexec-only", and it is a live trap rather than a curiosity.

    install-xtrn.js runs an installer's [exec:<file>.js] steps with js.exec() --
    IN THE SAME PROCESS -- and install-xtrn itself is invoked either from the command line (jsexec install-xtrn ...) or from INSIDE THE BBS (xtrn-setup.js, the sysop's Auto-install menu). So a door's getcore.js / getwads.js / getdata.js / getroms.js runs under whichever context the sysop happened to choose, and can rely on neither:

    * no `env`, `uifc`, `conio` -- absent under the BBS
    * no `bbs`, `console` -- absent under jsexec
    * `system.*`, `js.*`, `file_*`, `print()`, `argv` -- present in both

    print() is how such a script talks to whoever is running it.

    Checked: none of the six [exec:] children in the door installers (download.js, fetch-assets.js, getcore.js, getdata.js, getroms.js, getwads.js) touches any
    of the forbidden globals, and door_deploy.js resolves the live install from system.ctrl_dir rather than env.SBBSCTRL -- which also means a deploy step COULD be wired into an installer later without breaking. Its comment now says why, so the next reader does not "improve" it back to the environment.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sunday, July 12, 2026 05:11:00
    https://gitlab.synchro.net/main/sbbs/-/commit/35892c00ce4292ea1c9c4c11
    Modified Files:
    .claude/skills/javascript/SKILL.md .claude/skills/jsexec/SKILL.md
    Log Message:
    skills: correct two wrong claims about running in both contexts

    I wrote, one commit ago, that a script runnable from both jsexec and the BBS "must use only the intersection", and that "print() is the safe way to talk
    to whoever is running it". Both are wrong, and the tree says so.

    A SCRIPT CAN DETECT ITS CONTEXT, and the stock code does -- with js.global.<name>, not a bare identifier. That is the whole point of the
    idiom: a bare `console` under jsexec throws ReferenceError, while js.global.console is an ordinary property lookup that simply reads
    undefined. exec/logonlist.js gates on js.global.bbs, exec/load/gettext.js on js.global.console, and install-xtrn.js itself does
    `js.global.console && console.aborted`. The rule is not "avoid", it is
    "probe, then branch".

    AND print() IS NOT THE ONLY SAFE OUTPUT. Verified against the running binary: print, write, writeln, printf, alert, log, read, readln, prompt, confirm and deny are ALL present -- eleven functions, in a BBS session and on a command line alike. Only console.* / bbs / user are terminal-bound; uifc, env and
    conio are jsexec-only. So an installer's child script has a real vocabulary: confirm() to ask, alert() to warn, prompt() to collect, print() to narrate.

    Both skills now carry the table (from https://wiki.synchro.net/custom:javascript#output, checked against the
    binary) and the js.global probe, with the stock scripts cited so the next reader can see it done rather than take my word for it.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tuesday, July 21, 2026 21:53:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8c2fba616c53be965f97ca60
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: document the 512-byte line cap on readAll/readln

    File.readAll() and File.readln() both default to maxlen=512. Because readAll
    is a loop over readln, and readln is fgets(buf, len+1, fp), an over-long line is SPLIT rather than truncated: fgets leaves the remainder in the stream, so
    it comes back as the next array element. The array quietly desynchronizes
    from the file's lines, nothing throws, and f.error stays 0.

    Hit this reading a 20MB data/gitpush.jsonl whose largest payload is ~160KB: a scan for the longest line reported exactly 512 bytes, and JSON.parse threw on the fragments. Only files whose lines all fit in 512 bytes behave correctly, which is what makes it look intermittent.

    Also note that load('json_lines.js') is the right tool for .jsonl and
    .ndjson, but that its own max_line_len default is 4096 -- still too small for records this size, so it has to be passed explicitly.

    Three baseline agents given the un-amended skill all got this right, but each did it by reading js_file.cpp first; the section is here to save that detour, and to carry the json_lines.js default, which none of them had reason to hit.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tuesday, July 28, 2026 02:32:00
    https://gitlab.synchro.net/main/sbbs/-/commit/74911bb1379078e52a52f4ca
    Modified Files:
    .claude/skills/javascript/SKILL.md
    Log Message:
    javascript skill: document the ini's "key : value" string literals

    A key separated from its value by a COLON rather than an equals sign makes
    the value a C string literal: xpdev/ini_file.c's key_name() runs it through c_unescape_str(). So an .ini can carry Ctrl-A attribute codes, CP437 bytes
    and control characters directly, spelled as ctrl/text.dat spells them, and
    no JS-side unescape function is needed. Written up because I was about to hand-roll one.

    Records the escape set (\xNN hex, \NNN decimal -- not octal, unless built
    with C_UNESCAPE_OCTAL_SUPPORT), which readers unescape (iniGetString and
    the named-string-list readers behind File.iniGetObject()), that the
    separator is whichever of = or : comes first on the line -- so dir = C:\roms
    is unaffected and the two forms mix freely -- that typed reads still split
    on : but skip the unescape, and that : preserves trailing whitespace where
    = trims it. Verified against a live install with jsexec.

    Sits beside the existing trailing-comment trap in the same section, which
    still applies: a ; comment after a string value is not stripped under
    either separator.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net