threadwire 0.1.18 → 0.1.19

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Resolve native Kimi through the normal `PATH` (`kimi`) instead of requiring
6
+ `/usr/local/bin/kimi`; `THREADWIRE_KIMI_BIN` remains the explicit override.
7
+ - Resolve native Codex through `THREADWIRE_CODEX_BIN` or normal `PATH` lookup,
8
+ falling back to `codex` rather than a Threadwire-specific structural adapter.
9
+
10
+ - Add an optional trusted-controller Telegram relay handoff: the generated host launcher can securely read one bounded controller-only bot-token file after argument validation, export it only to Threadwire, and scrub its pointer before controller execution. Existing provider-boundary filtering keeps the token and pointer out of Docker provider argv/environment, task containers, source, prompts, and logs; omitting the option preserves the existing dotenv behavior.
11
+
5
12
  ## 0.1.18 - 2026-08-06
6
13
 
7
14
  - Autodiscover the direct Codex CLI for both worker runs and live capacity probes: after a valid `THREADWIRE_CODEX_BIN` and executable structural `/opt/data/libexec/threadwire/codex` adapter, resolve executable `codex` candidates in supplied `PATH` order while rejecting the `/opt/data/bin/codex` Threadwire front door and continuing lookup. Stale overrides name the selected fallback, and no safe candidate fails clearly without unchecked bare-command lookup.
package/README.md CHANGED
@@ -39,16 +39,14 @@ npx --yes threadwire@latest run \
39
39
 
40
40
  Provide `THREADWIRE_TELEGRAM_BOT_TOKEN` through a trusted environment or secret
41
41
  manager; do not put the token in the command line. Codex first uses a valid
42
- `THREADWIRE_CODEX_BIN`, then the executable structural adapter at
43
- `/opt/data/libexec/threadwire/codex`, then the first executable `codex` found by
44
- left-to-right Unix lookup in the supplied `PATH`. Relative and empty PATH
42
+ `THREADWIRE_CODEX_BIN`, then the first executable `codex` found by left-to-right
43
+ Unix lookup in the supplied `PATH`; if no safe candidate is found, it runs the
44
+ normal `codex` command. Relative and empty PATH
45
45
  entries resolve from the worker's `--cwd`. Candidates are canonically resolved
46
46
  and inspected before use; known Threadwire front doors such as
47
47
  `/opt/data/bin/codex`, including aliases to them, are rejected while lookup
48
48
  continues, preventing recursive relay. A stale override warns with the actual
49
- selected fallback, and no safe candidate retains the explicit structural path
50
- for the normal unavailable failure rather than spawning an unchecked bare
51
- command. Use
49
+ selected fallback. Use
52
50
  `THREADWIRE_CODEX_BIN` only for an intentional custom/container adapter. For
53
51
  Claude or OpenCode, use `THREADWIRE_CLAUDE_BIN` or `THREADWIRE_OPENCODE_BIN`
54
52
  respectively; those overrides must name direct provider executables, never
@@ -80,9 +78,9 @@ The target has the form `telegram:<chat-id>:<thread-id>` for topics, `telegram:<
80
78
 
81
79
  `--cwd` is the complete workspace contract for every provider. It may name any caller-accessible directory, including a non-Git directory. Threadwire passes it unchanged to the native provider and performs no profile, repository, source-identity, task-binding, or root-allowlist lookup. `--workspace-profile` has been removed and is rejected; migrate invocations by replacing it with `--cwd <directory>`.
82
80
 
83
- Provider tool-start and tool-finish events do not produce Telegram messages by default. Pass the boolean `--tool-messages` flag to opt in, as shown above. When enabled, each tool is one concise, silent status line rather than a pair of noisy start/finish notices: when a tool starts, Threadwire sends `🛠 [P42] Tool: <safe command or tool description>` (for example `🛠 [P42] Tool: command — git status --short`), and when that same tool finishes, it edits the original message in place so it begins `✅` instead — no second completion message is sent, and Telegram edits raise no notification. The command/description shows a safe CLI-style preview with credential-bearing values (tokens, passwords, keys, authorization/cookie forms, credentials in URLs) redacted and control characters stripped; only normalized Codex command-completion output may also be appended to that same edited status as a Telegram-native expandable HTML blockquote with the visible label `Output →`; it is credential-redacted, HTML-escaped, and Unicode-safe bounded. Stderr, reasoning, and every other raw provider result remain excluded. Use `--max-output-length <positive-integer>` to truncate only that preview Unicode-safely; omit the option for unlimited tool detail. A prompt may instead come from `--prompt-file` or piped stdin, and every source rejects blank/whitespace content. Arguments following `--` pass to the selected provider, except output-stream and session options owned by Threadwire. Native Kimi uses the caller's compatible configuration. Threadwire maps its public `--resume-session <exact-id>` option to Kimi's native `--session <exact-id>` option and owns `--prompt` plus `--output-format stream-json`. The caller environment or image must provide the Kimi executable at `/usr/local/bin/kimi` or set `THREADWIRE_KIMI_BIN`; a missing executable fails normally.
81
+ Provider tool-start and tool-finish events do not produce Telegram messages by default. Pass the boolean `--tool-messages` flag to opt in, as shown above. When enabled, each tool is one concise, silent status line rather than a pair of noisy start/finish notices: when a tool starts, Threadwire sends `🛠 [P42] Tool: <safe command or tool description>` (for example `🛠 [P42] Tool: command — git status --short`), and when that same tool finishes, it edits the original message in place so it begins `✅` instead — no second completion message is sent, and Telegram edits raise no notification. The command/description shows a safe CLI-style preview with credential-bearing values (tokens, passwords, keys, authorization/cookie forms, credentials in URLs) redacted and control characters stripped; only normalized Codex command-completion output may also be appended to that same edited status as a Telegram-native expandable HTML blockquote with the visible label `Output →`; it is credential-redacted, HTML-escaped, and Unicode-safe bounded. Stderr, reasoning, and every other raw provider result remain excluded. Use `--max-output-length <positive-integer>` to truncate only that preview Unicode-safely; omit the option for unlimited tool detail. A prompt may instead come from `--prompt-file` or piped stdin, and every source rejects blank/whitespace content. Arguments following `--` pass to the selected provider, except output-stream and session options owned by Threadwire. Native Kimi uses the caller's compatible configuration. Threadwire maps its public `--resume-session <exact-id>` option to Kimi's native `--session <exact-id>` option and owns `--prompt` plus `--output-format stream-json`. The caller environment or image must provide `kimi` on `PATH` or set `THREADWIRE_KIMI_BIN`; a missing executable fails normally.
84
82
 
85
- Threadwire directly spawns the structurally separate terminal adapters `/opt/data/libexec/threadwire/codex`, `/opt/data/libexec/threadwire/claude`, `/usr/local/bin/kimi`, or `/opt/data/libexec/threadwire/opencode-local-fleet` with an argv array and `shell: false`. These libexec adapters hold the real provider-CLI logic and are never on a worker's `PATH`, so relaying can never recurse back into the front-door shims. Each spawn also sets `THREADWIRE_ACTIVE=1` in the provider child environment. The executable can be overridden per provider with `THREADWIRE_CODEX_BIN`, `THREADWIRE_CLAUDE_BIN`, `THREADWIRE_KIMI_BIN`, or `THREADWIRE_OPENCODE_BIN` for staged cutover and rollback. It does not impose a worker timeout. `SIGINT` and `SIGTERM` are forwarded to the worker as explicit cancellation. Terminal completion is explicit and provider-owned rather than derived only from process exit: native Kimi confirms a completed run with a final assistant response (assistant content without further tool calls) plus its session handoff record in the stream. Once both records are confirmed, Threadwire preserves the session continuation handle, terminates the attempt-owned provider process group (`SIGTERM`), and emits exactly one `delegated_result`. Success settles only once the entire owned group is gone: members that survive `SIGTERM` — including attempt-owned grandchildren — keep the grace-period escalation running and receive `SIGKILL` against the exact group before settlement, so no descendant is leaked even when the direct provider exits first. A provider that exits nonzero before its protocol confirms completion keeps its failure status, and long-running tool activity remains unbounded: there is no generic idle timeout, only caller cancellation or an explicit deadline. Parent `SIGINT`/`SIGTERM` is owned cancellation, not just forwarding: the signal reaches the exact attempt-owned group, surviving members are escalated to `SIGKILL` after the grace period, and settlement keeps the forwarded signal's exit status (143/130 on Linux) only after the owned tree is gone. Relay/consumer failure cleans up the same owned tree — even when the failure lands after the direct child closed and only descendants survive — and the worker runner also accepts an explicit `AbortSignal`: an abort terminates the exact attempt-owned group with the same grace/escalation and settles with the abort failure, never success. After any `SIGKILL`, settlement waits for bounded cleanup confirmation — the exact group is re-probed, or on platforms without process groups the direct child's close is required — and cleanup that cannot be confirmed within the bounded window settles as failure, never success; this confirmation bound is not an idle or tool-activity timeout.
83
+ Threadwire directly spawns the structurally separate terminal adapters `/opt/data/libexec/threadwire/codex`, `/opt/data/libexec/threadwire/claude`, `kimi` resolved from `PATH`, or `/opt/data/libexec/threadwire/opencode-local-fleet` with an argv array and `shell: false`. These libexec adapters hold the real provider-CLI logic and are never on a worker's `PATH`, so relaying can never recurse back into the front-door shims. Each spawn also sets `THREADWIRE_ACTIVE=1` in the provider child environment. The executable can be overridden per provider with `THREADWIRE_CODEX_BIN`, `THREADWIRE_CLAUDE_BIN`, `THREADWIRE_KIMI_BIN`, or `THREADWIRE_OPENCODE_BIN` for staged cutover and rollback. It does not impose a worker timeout. `SIGINT` and `SIGTERM` are forwarded to the worker as explicit cancellation. Terminal completion is explicit and provider-owned rather than derived only from process exit: native Kimi confirms a completed run with a final assistant response (assistant content without further tool calls) plus its session handoff record in the stream. Once both records are confirmed, Threadwire preserves the session continuation handle, terminates the attempt-owned provider process group (`SIGTERM`), and emits exactly one `delegated_result`. Success settles only once the entire owned group is gone: members that survive `SIGTERM` — including attempt-owned grandchildren — keep the grace-period escalation running and receive `SIGKILL` against the exact group before settlement, so no descendant is leaked even when the direct provider exits first. A provider that exits nonzero before its protocol confirms completion keeps its failure status, and long-running tool activity remains unbounded: there is no generic idle timeout, only caller cancellation or an explicit deadline. Parent `SIGINT`/`SIGTERM` is owned cancellation, not just forwarding: the signal reaches the exact attempt-owned group, surviving members are escalated to `SIGKILL` after the grace period, and settlement keeps the forwarded signal's exit status (143/130 on Linux) only after the owned tree is gone. Relay/consumer failure cleans up the same owned tree — even when the failure lands after the direct child closed and only descendants survive — and the worker runner also accepts an explicit `AbortSignal`: an abort terminates the exact attempt-owned group with the same grace/escalation and settles with the abort failure, never success. After any `SIGKILL`, settlement waits for bounded cleanup confirmation — the exact group is re-probed, or on platforms without process groups the direct child's close is required — and cleanup that cannot be confirmed within the bounded window settles as failure, never success; this confirmation bound is not an idle or tool-activity timeout.
86
84
 
87
85
  For Codex in ordinary project images, the fixed structural path described above
88
86
  is only the preferred fallback after a valid override. When that adapter is not
@@ -93,6 +91,23 @@ executable, Threadwire uses the first inspected safe `codex` candidate from
93
91
 
94
92
  `/opt/data/bin/codex`, `/opt/data/bin/claude`, and `/opt/data/bin/opencode-local-fleet` are Threadwire front-door relay shims. Install or refresh both them and their paired libexec adapters with `npm run install:provider-shims`; the installer generates all six scripts from the repo templates and never modifies runtime artifacts on its own. A worker invocation is routed through the `/opt/data/bin/threadwire` launcher so its activity streams to the caller's explicit Telegram destination; every non-worker mode runs the real provider CLI unchanged through the paired libexec adapter.
95
93
 
94
+ A trusted host controller can generate its launcher with an absolute,
95
+ controller-only Telegram bot-token file:
96
+
97
+ ```sh
98
+ node scripts/install-local-launcher.js /opt/data/bin/threadwire \
99
+ --telegram-bot-token-file /run/secrets/threadwire_telegram_bot_token
100
+ ```
101
+
102
+ After token-free argument validation, the launcher reads at most 8 KiB from a
103
+ regular, non-symlink file that is not group/other-writable, exports
104
+ `THREADWIRE_TELEGRAM_BOT_TOKEN` only to the Threadwire controller, and clears
105
+ the file pointer. Threadwire's existing provider boundary removes the token and
106
+ pointer before spawning any provider. The token file and value are never passed
107
+ through Docker argv/environment, mounted or copied into the task container or
108
+ source, added to prompts, or written to logs. Omitting the option preserves the
109
+ existing `TELEGRAM_BOT_TOKEN` dotenv lookup.
110
+
96
111
  The relay contract: the caller supplies `THREADWIRE_TARGET` (`telegram:<chat-id>` or `telegram:<chat-id>:<thread-id>`) and, optionally, `THREADWIRE_PROCESS_NUMBER`. The prompt arrives on a piped stdin; all native provider flags pass through after `--`. A positional prompt is never parsed. A missing `THREADWIRE_TARGET` fails closed (exit code 2) and a malformed one is rejected by the launcher's token-free preflight — the shim never invents a default destination and never silently downgrades a requested relay to a direct run. A shim relays only for a worker mode (`codex exec …`, `opencode-local-fleet run …`, or any non-interactive `claude …`); it drops the worker subcommand before relaying and Threadwire re-adds it around the prompt. Version and help queries, non-worker subcommands, interactive TTY sessions, and any invocation already inside a relayed run (`THREADWIRE_ACTIVE=1`, so nested provider workers stay local/native) run the real CLI directly through the libexec adapter.
97
112
 
98
113
  Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, Kimi, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
@@ -52,15 +52,14 @@ pin before relying on it.
52
52
  - **Codex** spawns `codex app-server` and runs a bounded two-request JSON-RPC
53
53
  exchange over stdio: `initialize`, then `account/rateLimits/read`. The
54
54
  executable resolves exactly like the Codex worker provider: a valid
55
- `THREADWIRE_CODEX_BIN`, the executable structural
56
- `/opt/data/libexec/threadwire/codex` adapter, then the first executable
57
- `codex` found by left-to-right Unix lookup in the supplied `PATH`. Each PATH
55
+ `THREADWIRE_CODEX_BIN`, then the first executable `codex` found by
56
+ left-to-right Unix lookup in the supplied `PATH`; if no safe candidate is
57
+ found, it runs the normal `codex` command. Each PATH
58
58
  candidate is canonically resolved and inspected before use; relative and
59
59
  empty entries resolve from the probe's working directory. Known Threadwire
60
60
  front doors, notably `/opt/data/bin/codex` and aliases to it, are skipped
61
- while lookup continues; if no safe candidate exists, the probe reports
62
- unavailable without spawning an unchecked bare command. A stale explicit
63
- override warns with the selected fallback.
61
+ while lookup continues. A stale explicit override warns with the selected
62
+ fallback.
64
63
  Only the quota projection of the result is parsed: `rateLimits.primary` maps
65
64
  to `short` and `rateLimits.secondary` to `long`; a `null` secondary yields a
66
65
  short-only snapshot, and a secondary shorter than the primary fails closed.
@@ -101,6 +101,17 @@ Codex state persists at `/home/threadwire/.codex`. The Codex adapter reads only
101
101
  `exec`, removes its pointer, and removes every ingress value and pointer from
102
102
  the provider environment. Threadwire repeats ingress filtering before spawn.
103
103
 
104
+ For a trusted host controller, `scripts/install-local-launcher.js` accepts
105
+ `--telegram-bot-token-file <absolute-file>`. The generated launcher validates
106
+ Threadwire arguments before credential access, then reads at most 8 KiB from a
107
+ regular, non-symlink, non-group/other-writable file. It exports the resulting
108
+ `THREADWIRE_TELEGRAM_BOT_TOKEN` only to the controller and clears the file
109
+ pointer before execution. Threadwire and the libexec adapters scrub Telegram
110
+ credentials and pointers before provider spawn, so neither the value nor file
111
+ pointer enters Docker provider argv/environment, a task container, source,
112
+ prompts, or logs. Without this installer option, the launcher retains its
113
+ existing `TELEGRAM_BOT_TOKEN` dotenv behavior.
114
+
104
115
  Docker secrets are ordinary files in the same container mount namespace. A
105
116
  provider running as the same UID can open another mounted secret file and can
106
117
  inspect same-UID process state permitted by the kernel, even though ingress
@@ -188,7 +199,7 @@ Kimi source is not a host worktree. The task controller creates one labelled sou
188
199
 
189
200
  The checked-in standard `runtime` image installs Codex only: it does not install
190
201
  Kimi or provide Kimi credentials. Native Kimi therefore requires a caller image
191
- or environment that provides `/usr/local/bin/kimi` (or sets
202
+ or environment that provides `kimi` on `PATH` (or sets
192
203
  `THREADWIRE_KIMI_BIN`); a missing executable fails normally. Never mount
193
204
  `threadwire-kimi-oauth`, its access/refresh tokens, or the auth home into the
194
205
  Threadwire runtime, supervisor, worker, workspace, or session-state volume.
@@ -157,8 +157,8 @@ logs; worker self-report is not the sole assertion.
157
157
 
158
158
  ## Optional isolated Kimi Code subscription boundary
159
159
 
160
- Normal `threadwire run --provider kimi` is native execution: it directly runs
161
- the caller-provided Kimi executable at `/usr/local/bin/kimi`, or the path in
160
+ Normal `threadwire run --provider kimi` is native execution: it resolves the
161
+ caller-provided `kimi` executable from `PATH`, or uses the path in
162
162
  `THREADWIRE_KIMI_BIN`. It does not select this isolated supervisor, broker, or
163
163
  model allowlist. The checked-in standard `runtime` image installs Codex only,
164
164
  so native Kimi needs a caller-supplied executable and fails normally when it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadwire",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Stream Codex, Claude, Kimi Code, and OpenCode worker progress to an explicit Telegram destination",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -7,13 +7,13 @@ import {discardStagedFiles, replaceFile, stageFile, syncDirectory} from "./atomi
7
7
 
8
8
  const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
9
9
 
10
- /** @param {string} targetPath @param {Parameters<typeof stageFile>[2]} [operations] */
11
- export async function installLocalLauncher(targetPath, operations = {}) {
10
+ /** @param {string} targetPath @param {Parameters<typeof stageFile>[2]} [operations] @param {{telegramBotTokenFile?: string | undefined}} [launcherOptions] */
11
+ export async function installLocalLauncher(targetPath, operations = {}, launcherOptions = {}) {
12
12
  const entrypointPath = resolve(repositoryRoot, "bin/threadwire.js")
13
13
  await mkdir(dirname(targetPath), {recursive: true})
14
14
  const stagedFiles = []
15
15
  try {
16
- const stagedFile = await stageFile(targetPath, launcherScript(entrypointPath), operations)
16
+ const stagedFile = await stageFile(targetPath, launcherScript(entrypointPath, undefined, launcherOptions), operations)
17
17
  stagedFiles.push(stagedFile)
18
18
  await replaceFile(stagedFile)
19
19
  await syncDirectory(dirname(targetPath))
@@ -23,11 +23,27 @@ export async function installLocalLauncher(targetPath, operations = {}) {
23
23
  }
24
24
 
25
25
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
26
- await installLocalLauncher(process.argv[2] ?? "/opt/data/bin/threadwire")
26
+ const arguments_ = process.argv.slice(2)
27
+ const firstArgument = arguments_[0]
28
+ let targetPath = "/opt/data/bin/threadwire"
29
+ if (firstArgument !== undefined && !firstArgument.startsWith("--")) {
30
+ targetPath = firstArgument
31
+ arguments_.shift()
32
+ }
33
+ let telegramBotTokenFile
34
+ while (arguments_.length > 0) {
35
+ const option = arguments_.shift()
36
+ if (option !== "--telegram-bot-token-file" || arguments_.length === 0) throw new Error(`Unknown or incomplete option: ${option}`)
37
+ telegramBotTokenFile = arguments_.shift()
38
+ }
39
+ if (telegramBotTokenFile !== undefined && !telegramBotTokenFile.startsWith("/")) throw new Error("Telegram bot-token file must be an absolute path")
40
+ await installLocalLauncher(targetPath, {}, {telegramBotTokenFile})
27
41
  }
28
42
 
29
- /** @param {string} entrypoint @param {string} [dotenvPath] */
30
- export function launcherScript(entrypoint, dotenvPath = "/opt/data/.env") {
43
+ /** @param {string} entrypoint @param {string} [dotenvPath] @param {{telegramBotTokenFile?: string | undefined}} [launcherOptions] */
44
+ export function launcherScript(entrypoint, dotenvPath = "/opt/data/.env", launcherOptions = {}) {
45
+ const telegramBotTokenFile = launcherOptions.telegramBotTokenFile
46
+ if (telegramBotTokenFile !== undefined && !telegramBotTokenFile.startsWith("/")) throw new Error("Telegram bot-token file must be an absolute path")
31
47
  return `#!/usr/bin/env bash
32
48
  # Generated by Threadwire's install:local-launcher script. Do not source the
33
49
  # shared dotenv: it contains unrelated credentials.
@@ -35,8 +51,9 @@ set -euo pipefail
35
51
 
36
52
  readonly THREADWIRE_ENTRYPOINT=${shellLiteral(entrypoint)}
37
53
  readonly THREADWIRE_DOTENV_PATH=${shellLiteral(dotenvPath)}
54
+ threadwire_telegram_bot_token_file=${shellLiteral(telegramBotTokenFile ?? "")}
38
55
 
39
- unset TELEGRAM_BOT_TOKEN THREADWIRE_TELEGRAM_BOT_TOKEN
56
+ unset TELEGRAM_BOT_TOKEN THREADWIRE_TELEGRAM_BOT_TOKEN THREADWIRE_TELEGRAM_BOT_TOKEN_FILE
40
57
 
41
58
  # Scope validation-only strictly to this launcher's own preflight below: clear any
42
59
  # inherited value so it can never leak into the real invocation and let it exit
@@ -71,27 +88,58 @@ while [[ "$#" -gt 0 ]]; do
71
88
  done
72
89
 
73
90
  if [[ "$threadwire_target" == file:* ]]; then
91
+ unset threadwire_telegram_bot_token_file
74
92
  exec node "$THREADWIRE_ENTRYPOINT" "\${threadwire_arguments[@]}"
75
93
  fi
76
94
 
77
- if [[ ! -r "$THREADWIRE_DOTENV_PATH" ]]; then
95
+ telegram_bot_token=""
96
+ if [[ -n "$threadwire_telegram_bot_token_file" ]]; then
97
+ threadwire_token_read_status=0
98
+ telegram_bot_token=$(node - "$threadwire_telegram_bot_token_file" <<'NODE'
99
+ const fs = require("node:fs")
100
+ const maximumBytes = 8192
101
+ let descriptor
102
+ try {
103
+ descriptor = fs.openSync(process.argv[2], fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW)
104
+ const metadata = fs.fstatSync(descriptor)
105
+ if (!metadata.isFile() || metadata.size === 0 || metadata.size > maximumBytes || (metadata.mode & 0o077) !== 0) throw new Error("invalid")
106
+ const buffer = Buffer.alloc(maximumBytes + 1)
107
+ const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0)
108
+ if (bytesRead > maximumBytes) throw new Error("invalid")
109
+ const value = buffer.subarray(0, bytesRead).toString("utf8").replace(/[\\r\\n]+$/, "")
110
+ if (value.length === 0 || /[\\r\\n\\0]/u.test(value)) throw new Error("invalid")
111
+ process.stdout.write(value)
112
+ } catch {
113
+ process.stderr.write("threadwire: Telegram bot-token file is invalid.\\n")
114
+ process.exitCode = 2
115
+ } finally {
116
+ if (descriptor !== undefined) fs.closeSync(descriptor)
117
+ }
118
+ NODE
119
+ ) || threadwire_token_read_status=$?
120
+ unset threadwire_telegram_bot_token_file THREADWIRE_TELEGRAM_BOT_TOKEN_FILE
121
+ if [[ "$threadwire_token_read_status" -ne 0 ]]; then
122
+ exit "$threadwire_token_read_status"
123
+ fi
124
+ elif [[ ! -r "$THREADWIRE_DOTENV_PATH" ]]; then
125
+ unset threadwire_telegram_bot_token_file
78
126
  printf 'threadwire: required dotenv is not readable: %s\\n' "$THREADWIRE_DOTENV_PATH" >&2
79
127
  exit 2
80
- fi
81
-
82
- telegram_bot_token=""
83
- while IFS= read -r line || [[ -n "$line" ]]; do
84
- case "$line" in
85
- TELEGRAM_BOT_TOKEN=*) telegram_bot_token="\${line#TELEGRAM_BOT_TOKEN=}" ;;
86
- "export TELEGRAM_BOT_TOKEN="*) telegram_bot_token="\${line#export TELEGRAM_BOT_TOKEN=}" ;;
128
+ else
129
+ unset threadwire_telegram_bot_token_file
130
+ while IFS= read -r line || [[ -n "$line" ]]; do
131
+ case "$line" in
132
+ TELEGRAM_BOT_TOKEN=*) telegram_bot_token="\${line#TELEGRAM_BOT_TOKEN=}" ;;
133
+ "export TELEGRAM_BOT_TOKEN="*) telegram_bot_token="\${line#export TELEGRAM_BOT_TOKEN=}" ;;
134
+ esac
135
+ done < "$THREADWIRE_DOTENV_PATH"
136
+
137
+ telegram_bot_token="\${telegram_bot_token%$'\\r'}"
138
+ case "$telegram_bot_token" in
139
+ \\"*\\") telegram_bot_token="\${telegram_bot_token:1:\${#telegram_bot_token}-2}" ;;
140
+ \\'*\\') telegram_bot_token="\${telegram_bot_token:1:\${#telegram_bot_token}-2}" ;;
87
141
  esac
88
- done < "$THREADWIRE_DOTENV_PATH"
89
-
90
- telegram_bot_token="\${telegram_bot_token%$'\\r'}"
91
- case "$telegram_bot_token" in
92
- \\"*\\") telegram_bot_token="\${telegram_bot_token:1:\${#telegram_bot_token}-2}" ;;
93
- \\'*\\') telegram_bot_token="\${telegram_bot_token:1:\${#telegram_bot_token}-2}" ;;
94
- esac
142
+ fi
95
143
 
96
144
  if [[ -z "$telegram_bot_token" ]]; then
97
145
  printf 'threadwire: TELEGRAM_BOT_TOKEN is required in %s\\n' "$THREADWIRE_DOTENV_PATH" >&2
@@ -3,7 +3,6 @@
3
3
  import {accessSync, constants, realpathSync, statSync} from "node:fs"
4
4
  import {resolve} from "node:path"
5
5
 
6
- const CODEX_STRUCTURAL_EXECUTABLE = "/opt/data/libexec/threadwire/codex"
7
6
  const CODEX_FRONT_DOOR_EXECUTABLES = new Set(["/opt/data/bin/codex"])
8
7
 
9
8
  /** @typedef {(path: string) => boolean} ExecutableProbe */
@@ -37,9 +36,9 @@ export function providerExecutable(overrideVariable, defaultPath, environment =
37
36
 
38
37
  /**
39
38
  * Resolve Codex without consulting a bare command through spawn. A valid
40
- * override wins, followed by the structural adapter and then executable PATH
41
- * candidates in Unix lookup order. Known Threadwire front doors are skipped so
42
- * a normal direct-CLI lookup cannot recurse into Threadwire.
39
+ * override wins, followed by executable PATH candidates in Unix lookup order.
40
+ * Known Threadwire front doors are skipped so a normal direct-CLI lookup cannot
41
+ * recurse into Threadwire.
43
42
  * @param {NodeJS.ProcessEnv} [environment]
44
43
  * @param {ExecutableResolutionOptions} [options]
45
44
  * @returns {string}
@@ -52,7 +51,7 @@ export function codexExecutable(environment = process.env, options = {}) {
52
51
  const override = environment.THREADWIRE_CODEX_BIN
53
52
  if (override !== undefined && override.length > 0 && isExecutable(override)) return override
54
53
 
55
- const fallback = discoveredCodexExecutable(environment, cwd, isExecutable, canonicalize) ?? CODEX_STRUCTURAL_EXECUTABLE
54
+ const fallback = discoveredCodexExecutable(environment, cwd, isExecutable, canonicalize) ?? "codex"
56
55
  if (override !== undefined && override.length > 0) {
57
56
  warning(`threadwire: THREADWIRE_CODEX_BIN=${override} is not executable, falling back to ${fallback}`)
58
57
  }
@@ -67,7 +66,6 @@ export function codexExecutable(environment = process.env, options = {}) {
67
66
  * @returns {string | undefined}
68
67
  */
69
68
  function discoveredCodexExecutable(environment, cwd, isExecutable, canonicalize) {
70
- if (isExecutable(CODEX_STRUCTURAL_EXECUTABLE)) return CODEX_STRUCTURAL_EXECUTABLE
71
69
  if (environment.PATH === undefined) return undefined
72
70
  for (const directory of environment.PATH.split(":")) {
73
71
  const candidate = resolve(cwd, directory, "codex")
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- const EXECUTABLE = "/usr/local/bin/kimi"
3
+ const EXECUTABLE = "kimi"
4
4
  const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,63}$/u
5
5
  const SESSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u
6
6
  const TOOL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u