claude-slack-channel-bots 0.1.0 → 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 +1 -0
- package/package.json +1 -1
- package/skills/EXAMPLE_CLAUDE.md +15 -0
- package/skills/claude-slack-channels-config/SKILL.md +121 -0
- package/skills/setup-slack-channel-bots/SKILL.md +47 -4
- package/src/config.ts +6 -0
- package/src/postinstall.ts +32 -2
- package/src/server.ts +29 -8
- package/src/session-manager.ts +12 -2
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ A skeleton file is created by postinstall. Populate it before running `start`.
|
|
|
105
105
|
| `session_restart_delay` | number | `60` | Seconds to wait before auto-restarting a dead session. Set to `0` to disable auto-restart. Must be non-negative. |
|
|
106
106
|
| `health_check_interval` | number | `120` | Seconds between periodic liveness polls. Set to `0` to disable. Must be non-negative. |
|
|
107
107
|
| `mcp_config_path` | string | `~/.claude/slack-mcp.json` | Path to the MCP config file passed to Claude Code when launching managed sessions. |
|
|
108
|
+
| `append_system_prompt_file` | string | — | Path to a file appended to every managed session's system prompt via `--append-system-prompt-file`. Missing file silently skipped. See `skills/EXAMPLE_CLAUDE.md` for a template. |
|
|
108
109
|
|
|
109
110
|
---
|
|
110
111
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Role
|
|
2
|
+
You are an orchestration agent that bridges between the User and worker-Claude sessions you manage.
|
|
3
|
+
|
|
4
|
+
# Communication with the User
|
|
5
|
+
The User only communicate to you via Slack so always use the mcp__slack-channel-router__reply tool to send messages.
|
|
6
|
+
**Important**: Nothing you send to the TUI will be seen by the User
|
|
7
|
+
|
|
8
|
+
# Development process
|
|
9
|
+
You use the development process defined in the readme in ~/projects/apiary/README.MD.
|
|
10
|
+
You use the Apiary skills listed in that project to manage software development from ideation to completion.
|
|
11
|
+
|
|
12
|
+
# Spawning workers
|
|
13
|
+
You do not do any work yourself, you always spawn Claude sessions as workers to do the work.
|
|
14
|
+
This keeps you available to interact with the User and orchestrate the work.
|
|
15
|
+
You use `waggle` to spawn workers and monitor their status.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-slack-channels-config
|
|
3
|
+
description: Manage Slack channel bot access control — pairing, allowlist, channel opt-in, ack reactions, chunking
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: Gabe Mahoney
|
|
6
|
+
license: MIT
|
|
7
|
+
user-invocable: true
|
|
8
|
+
argument-hint: "pair <code> | policy <mode> | add <user_id> | remove <user_id> | channel <id> [opts] | ack <emoji|off> | chunking <limit> [mode] | status"
|
|
9
|
+
allowed-tools: [Read, Write, Edit, Bash]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# /claude-slack-channels-config
|
|
13
|
+
|
|
14
|
+
Manage who can reach Claude Code sessions through Slack, and configure
|
|
15
|
+
message handling (ack reactions, chunking).
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/claude-slack-channels-config pair <code> # Approve a pending pairing
|
|
21
|
+
/claude-slack-channels-config policy <pairing|allowlist|disabled> # Set DM policy
|
|
22
|
+
/claude-slack-channels-config add <slack_user_id> # Add user to allowlist
|
|
23
|
+
/claude-slack-channels-config remove <slack_user_id> # Remove from allowlist
|
|
24
|
+
/claude-slack-channels-config channel <channel_id> [--mention] [--allow <user_id,...>] # Opt in a channel
|
|
25
|
+
/claude-slack-channels-config channel remove <channel_id> # Remove channel opt-in
|
|
26
|
+
/claude-slack-channels-config ack <emoji|off> # Set or clear ack reaction
|
|
27
|
+
/claude-slack-channels-config chunking <limit> [length|newline] # Set text chunk limit and mode
|
|
28
|
+
/claude-slack-channels-config status # Show current config
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## State File
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
STATE_DIR="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}"
|
|
35
|
+
# access.json lives at $STATE_DIR/access.json
|
|
36
|
+
# routing.json lives at $STATE_DIR/routing.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Always resolve the state directory using `$SLACK_STATE_DIR` with fallback to
|
|
40
|
+
`~/.claude/channels/slack/`.
|
|
41
|
+
|
|
42
|
+
## Instructions
|
|
43
|
+
|
|
44
|
+
Parse `$ARGUMENTS` and execute the matching subcommand:
|
|
45
|
+
|
|
46
|
+
### `pair <code>`
|
|
47
|
+
1. Load `access.json`
|
|
48
|
+
2. Find the pending entry matching `<code>` (case-insensitive)
|
|
49
|
+
3. If not found: show "No pending pairing with that code."
|
|
50
|
+
4. If found:
|
|
51
|
+
- Add `entry.senderId` to `allowFrom`
|
|
52
|
+
- Remove the pending entry
|
|
53
|
+
- Save `access.json` with permissions 0o600
|
|
54
|
+
- Show: `Approved! User <senderId> can now DM this session.`
|
|
55
|
+
- Send a confirmation message to the user in Slack via the reply tool
|
|
56
|
+
|
|
57
|
+
### `policy <mode>`
|
|
58
|
+
1. Validate mode is one of: `pairing`, `allowlist`, `disabled`
|
|
59
|
+
2. Update `dmPolicy` in `access.json`
|
|
60
|
+
3. Save with 0o600
|
|
61
|
+
4. Show the new policy and what it means:
|
|
62
|
+
- `pairing`: New DMs get a code to approve (default)
|
|
63
|
+
- `allowlist`: Only pre-approved users can DM
|
|
64
|
+
- `disabled`: No DMs accepted
|
|
65
|
+
|
|
66
|
+
### `add <user_id>`
|
|
67
|
+
1. Add the Slack user ID to `allowFrom` (deduplicate)
|
|
68
|
+
2. Save with 0o600
|
|
69
|
+
3. Show confirmation
|
|
70
|
+
|
|
71
|
+
### `remove <user_id>`
|
|
72
|
+
1. Remove from `allowFrom`
|
|
73
|
+
2. Also remove from any channel-level `allowFrom` lists
|
|
74
|
+
3. Save with 0o600
|
|
75
|
+
4. Show confirmation
|
|
76
|
+
|
|
77
|
+
### `channel <channel_id> [--mention] [--allow <ids>]`
|
|
78
|
+
1. Parse options:
|
|
79
|
+
- `--mention`: require @mention to trigger (default: false)
|
|
80
|
+
- `--allow <id1,id2>`: restrict to specific users in that channel
|
|
81
|
+
2. Add/update `channels[channel_id]` in `access.json`
|
|
82
|
+
3. Save with 0o600
|
|
83
|
+
4. Show the channel policy
|
|
84
|
+
|
|
85
|
+
### `channel remove <channel_id>`
|
|
86
|
+
1. Delete `channels[channel_id]`
|
|
87
|
+
2. Save with 0o600
|
|
88
|
+
3. Show confirmation
|
|
89
|
+
|
|
90
|
+
### `ack <emoji|off>`
|
|
91
|
+
1. If argument is `off`: remove `ackReaction` from `access.json`
|
|
92
|
+
2. Otherwise: set `ackReaction` to the provided emoji name (without colons)
|
|
93
|
+
3. Save with 0o600
|
|
94
|
+
4. Show confirmation: "Ack reaction set to :<emoji>:" or "Ack reaction disabled."
|
|
95
|
+
|
|
96
|
+
### `chunking <limit> [length|newline]`
|
|
97
|
+
1. Parse `<limit>` as a positive integer — this sets `textChunkLimit`
|
|
98
|
+
2. If a second argument is provided, validate it is `length` or `newline` — this sets `chunkMode`
|
|
99
|
+
3. If no second argument, leave `chunkMode` unchanged
|
|
100
|
+
4. Save with 0o600
|
|
101
|
+
5. Show confirmation with the new values
|
|
102
|
+
|
|
103
|
+
### `status`
|
|
104
|
+
1. Load `access.json`
|
|
105
|
+
2. Load `routing.json` from the same state directory
|
|
106
|
+
3. Display:
|
|
107
|
+
- DM policy
|
|
108
|
+
- Allowlisted user IDs
|
|
109
|
+
- Opted-in channels with their policies, showing two categories:
|
|
110
|
+
- **Implicit** — channels present in `routing.json` routes (automatically opted-in)
|
|
111
|
+
- **Explicit** — channels configured in `access.json` channels (with their `requireMention` and `allowFrom` settings)
|
|
112
|
+
- Pending pairings (code + sender ID + expiry)
|
|
113
|
+
- Ack reaction setting (or "not set")
|
|
114
|
+
- Text chunk limit (or "not set, default: 4000")
|
|
115
|
+
- Chunk mode (or "not set, default: newline")
|
|
116
|
+
|
|
117
|
+
## Security
|
|
118
|
+
|
|
119
|
+
- Always use atomic writes (write to .tmp then rename) for `access.json`
|
|
120
|
+
- Always set 0o600 permissions on `access.json`
|
|
121
|
+
- If `access.json` is corrupt, move it aside and start fresh
|
|
@@ -225,7 +225,49 @@ their defaults unless asked.
|
|
|
225
225
|
|
|
226
226
|
---
|
|
227
227
|
|
|
228
|
-
### Step 5 —
|
|
228
|
+
### Step 5 — Configure custom CLAUDE.md (optional)
|
|
229
|
+
|
|
230
|
+
Worker sessions launched by the server can receive a custom system-prompt
|
|
231
|
+
append via `append_system_prompt_file` in `routing.json`. This is useful for
|
|
232
|
+
giving all workers project-specific instructions: communication rules,
|
|
233
|
+
development process, how to spawn sub-workers, and so on.
|
|
234
|
+
|
|
235
|
+
An example template is included with the package. Show the operator where it
|
|
236
|
+
lives:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
ls "$(npm root -g)/claude-slack-channel-bots/skills/EXAMPLE_CLAUDE.md" 2>/dev/null \
|
|
240
|
+
|| echo "NOT_FOUND"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
If the file is found, print its path so the operator can inspect it as a
|
|
244
|
+
starting point.
|
|
245
|
+
|
|
246
|
+
Ask the operator: **"Do you want to configure a custom CLAUDE.md file for
|
|
247
|
+
worker sessions?"**
|
|
248
|
+
|
|
249
|
+
**If yes:**
|
|
250
|
+
|
|
251
|
+
1. Prompt for the absolute path to their CLAUDE.md file.
|
|
252
|
+
2. Verify the file exists:
|
|
253
|
+
```bash
|
|
254
|
+
test -f "<provided-path>" && echo "ok" || echo "not found"
|
|
255
|
+
```
|
|
256
|
+
Expand `~` before checking. If the file does not exist, warn the operator
|
|
257
|
+
and re-prompt until a valid path is given or they choose to skip.
|
|
258
|
+
3. Read `routing.json`, add or update the top-level field:
|
|
259
|
+
```json
|
|
260
|
+
"append_system_prompt_file": "<provided-path>"
|
|
261
|
+
```
|
|
262
|
+
Write the updated file, preserving all other fields.
|
|
263
|
+
|
|
264
|
+
**If skipped:**
|
|
265
|
+
|
|
266
|
+
Do not write `append_system_prompt_file` to `routing.json`. Move on.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### Step 6 — Check access.json
|
|
229
271
|
|
|
230
272
|
```bash
|
|
231
273
|
STATE_DIR="${SLACK_STATE_DIR:-$HOME/.claude/channels/slack}"
|
|
@@ -264,7 +306,7 @@ Do not modify `access.json` during setup unless the user asks to.
|
|
|
264
306
|
|
|
265
307
|
---
|
|
266
308
|
|
|
267
|
-
### Step
|
|
309
|
+
### Step 7 — Check hooks
|
|
268
310
|
|
|
269
311
|
Check whether the relay hooks exist and are executable:
|
|
270
312
|
|
|
@@ -312,7 +354,7 @@ chmod +x ~/.claude/hooks/permission-relay.sh ~/.claude/hooks/ask-relay.sh
|
|
|
312
354
|
|
|
313
355
|
---
|
|
314
356
|
|
|
315
|
-
### Step
|
|
357
|
+
### Step 8 — Check Claude Code settings.json for hook entries
|
|
316
358
|
|
|
317
359
|
Read `~/.claude/settings.json` and check whether the `PermissionRequest` and
|
|
318
360
|
`PreToolUse` hook entries for the relay scripts are present.
|
|
@@ -365,13 +407,14 @@ If the user agrees, make the targeted edits, preserving all existing content.
|
|
|
365
407
|
|
|
366
408
|
---
|
|
367
409
|
|
|
368
|
-
### Step
|
|
410
|
+
### Step 9 — Summary
|
|
369
411
|
|
|
370
412
|
Print a final summary of what was checked and configured:
|
|
371
413
|
|
|
372
414
|
- Environment variables: set / missing
|
|
373
415
|
- Token format: valid / invalid
|
|
374
416
|
- routing.json: populated (N routes) / skeleton
|
|
417
|
+
- append_system_prompt_file: configured / skipped
|
|
375
418
|
- access.json: present / missing
|
|
376
419
|
- permission-relay.sh hook: present and executable / missing
|
|
377
420
|
- ask-relay.sh hook: present and executable / missing
|
package/src/config.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface RoutingConfigInput {
|
|
|
38
38
|
session_restart_delay?: number
|
|
39
39
|
health_check_interval?: number
|
|
40
40
|
mcp_config_path?: string
|
|
41
|
+
append_system_prompt_file?: string
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/** Validated, fully-resolved routing configuration with all defaults applied. */
|
|
@@ -50,6 +51,7 @@ export interface RoutingConfig {
|
|
|
50
51
|
session_restart_delay: number
|
|
51
52
|
health_check_interval: number
|
|
52
53
|
mcp_config_path: string
|
|
54
|
+
append_system_prompt_file?: string
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
@@ -70,6 +72,7 @@ export function applyDefaults(input: RoutingConfigInput): RoutingConfig {
|
|
|
70
72
|
session_restart_delay: input.session_restart_delay ?? 60,
|
|
71
73
|
health_check_interval: input.health_check_interval ?? 120,
|
|
72
74
|
mcp_config_path: input.mcp_config_path ?? '~/.claude/slack-mcp.json',
|
|
75
|
+
append_system_prompt_file: input.append_system_prompt_file,
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -166,6 +169,9 @@ export function resolveConfig(input: RoutingConfigInput): RoutingConfig {
|
|
|
166
169
|
? resolve(expandTilde(withDefaults.default_dm_session))
|
|
167
170
|
: undefined,
|
|
168
171
|
mcp_config_path: resolve(expandTilde(withDefaults.mcp_config_path)),
|
|
172
|
+
append_system_prompt_file: withDefaults.append_system_prompt_file !== undefined
|
|
173
|
+
? resolve(expandTilde(withDefaults.append_system_prompt_file))
|
|
174
|
+
: undefined,
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
validateConfig(config)
|
package/src/postinstall.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* SPDX-License-Identifier: MIT
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync, unlinkSync } from 'fs'
|
|
13
13
|
import { homedir } from 'os'
|
|
14
|
-
import { dirname, join } from 'path'
|
|
14
|
+
import { dirname, join, resolve } from 'path'
|
|
15
15
|
import { defaultAccess } from './lib.ts'
|
|
16
16
|
import { MCP_SERVER_NAME } from './config.ts'
|
|
17
17
|
|
|
@@ -65,6 +65,36 @@ export function runPostinstall(options: PostinstallOptions = {}): void {
|
|
|
65
65
|
console.log(`created: ${accessPath}`)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Symlink skills into ~/.claude/skills/
|
|
69
|
+
const skillsTarget = join(homedir(), '.claude', 'skills')
|
|
70
|
+
mkdirSync(skillsTarget, { recursive: true })
|
|
71
|
+
|
|
72
|
+
const skillNames = ['claude-slack-channels-config']
|
|
73
|
+
const packageSkillsDir = resolve(dirname(import.meta.filename), '..', 'skills')
|
|
74
|
+
for (const name of skillNames) {
|
|
75
|
+
const src = join(packageSkillsDir, name)
|
|
76
|
+
const dest = join(skillsTarget, name)
|
|
77
|
+
if (existsSync(src)) {
|
|
78
|
+
try {
|
|
79
|
+
// Remove stale symlink or directory if it points elsewhere
|
|
80
|
+
if (existsSync(dest)) {
|
|
81
|
+
try {
|
|
82
|
+
const current = readlinkSync(dest)
|
|
83
|
+
if (resolve(current) === resolve(src)) {
|
|
84
|
+
console.log(`skipped: ${dest} (already linked)`)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
} catch { /* not a symlink — remove it */ }
|
|
88
|
+
unlinkSync(dest)
|
|
89
|
+
}
|
|
90
|
+
symlinkSync(src, dest)
|
|
91
|
+
console.log(`linked: ${dest} -> ${src}`)
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.log(`warning: could not symlink ${name}: ${err}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
// slack-mcp.json
|
|
69
99
|
if (existsSync(mcpConfigPath)) {
|
|
70
100
|
console.log(`skipped: ${mcpConfigPath}`)
|
package/src/server.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/
|
|
|
18
18
|
import { SocketModeClient } from '@slack/socket-mode'
|
|
19
19
|
import { WebClient } from '@slack/web-api'
|
|
20
20
|
import { homedir } from 'os'
|
|
21
|
-
import { join, resolve } from 'path'
|
|
21
|
+
import { join, resolve, relative, isAbsolute } from 'path'
|
|
22
22
|
import { fileURLToPath } from 'url'
|
|
23
23
|
import {
|
|
24
24
|
readFileSync,
|
|
@@ -73,6 +73,30 @@ import {
|
|
|
73
73
|
// Re-export constants so they stay in one place (lib.ts)
|
|
74
74
|
export { MAX_PENDING, MAX_PAIRING_REPLIES, PAIRING_EXPIRY_MS } from './lib.ts'
|
|
75
75
|
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Helpers
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find the channel whose route CWD is the closest ancestor of (or equal to)
|
|
82
|
+
* the given absolute path. Returns undefined when no route matches.
|
|
83
|
+
*/
|
|
84
|
+
function findChannelByCwd(absoluteCwd: string, routes: RoutingConfig['routes']): string | undefined {
|
|
85
|
+
let bestChannel: string | undefined
|
|
86
|
+
let bestLen = -1
|
|
87
|
+
for (const [channelId, route] of Object.entries(routes)) {
|
|
88
|
+
const routeCwd = resolve(expandTilde(route.cwd))
|
|
89
|
+
const rel = relative(routeCwd, absoluteCwd)
|
|
90
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) {
|
|
91
|
+
if (routeCwd.length > bestLen) {
|
|
92
|
+
bestLen = routeCwd.length
|
|
93
|
+
bestChannel = channelId
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return bestChannel
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
// ---------------------------------------------------------------------------
|
|
77
101
|
// Constants
|
|
78
102
|
// ---------------------------------------------------------------------------
|
|
@@ -1003,12 +1027,10 @@ export async function main(): Promise<void> {
|
|
|
1003
1027
|
)
|
|
1004
1028
|
}
|
|
1005
1029
|
|
|
1006
|
-
//
|
|
1030
|
+
// Find the most specific route whose CWD is an ancestor of (or equal to) the request CWD
|
|
1007
1031
|
const normalizedCwd = resolve(expandTilde(cwd))
|
|
1008
1032
|
const matchedChannelId = routingConfig
|
|
1009
|
-
?
|
|
1010
|
-
([, route]) => resolve(expandTilde(route.cwd)) === normalizedCwd,
|
|
1011
|
-
)?.[0]
|
|
1033
|
+
? findChannelByCwd(normalizedCwd, routingConfig.routes)
|
|
1012
1034
|
: undefined
|
|
1013
1035
|
|
|
1014
1036
|
if (!matchedChannelId) {
|
|
@@ -1150,11 +1172,10 @@ export async function main(): Promise<void> {
|
|
|
1150
1172
|
)
|
|
1151
1173
|
}
|
|
1152
1174
|
|
|
1175
|
+
// Find the most specific route whose CWD is an ancestor of (or equal to) the request CWD
|
|
1153
1176
|
const normalizedCwd = resolve(expandTilde(cwd as string))
|
|
1154
1177
|
const matchedChannelId = routingConfig
|
|
1155
|
-
?
|
|
1156
|
-
([, route]) => resolve(expandTilde(route.cwd)) === normalizedCwd,
|
|
1157
|
-
)?.[0]
|
|
1178
|
+
? findChannelByCwd(normalizedCwd, routingConfig.routes)
|
|
1158
1179
|
: undefined
|
|
1159
1180
|
|
|
1160
1181
|
if (!matchedChannelId) {
|
package/src/session-manager.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* SPDX-License-Identifier: MIT
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readdirSync, readFileSync } from 'fs'
|
|
12
|
+
import { readdirSync, readFileSync, accessSync, constants } from 'fs'
|
|
13
13
|
import { join } from 'path'
|
|
14
14
|
import { homedir } from 'os'
|
|
15
15
|
import { type TmuxClient, sessionName, isClaudeRunning } from './tmux.ts'
|
|
@@ -109,7 +109,17 @@ export async function launchSession(
|
|
|
109
109
|
const resumeSessionId = options?.sessionId
|
|
110
110
|
|
|
111
111
|
const escapedConfigPath = routingConfig.mcp_config_path.replace(/'/g, "'\\''")
|
|
112
|
-
|
|
112
|
+
let baseCmd = `claude --mcp-config '${escapedConfigPath}' --dangerously-load-development-channels server:${MCP_SERVER_NAME}`
|
|
113
|
+
|
|
114
|
+
if (routingConfig.append_system_prompt_file !== undefined) {
|
|
115
|
+
try {
|
|
116
|
+
accessSync(routingConfig.append_system_prompt_file, constants.R_OK)
|
|
117
|
+
const escapedPromptPath = routingConfig.append_system_prompt_file.replace(/'/g, "'\\''")
|
|
118
|
+
baseCmd += ` --append-system-prompt-file '${escapedPromptPath}'`
|
|
119
|
+
} catch {
|
|
120
|
+
// file missing or unreadable — skip
|
|
121
|
+
}
|
|
122
|
+
}
|
|
113
123
|
|
|
114
124
|
const POLL_START_MS = 500
|
|
115
125
|
const POLL_CAP_MS = 5_000
|