discoclaw 0.1.4 → 0.1.8
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/.context/discord.md +2 -0
- package/.context/pa.md +1 -1
- package/.context/runtime.md +1 -1
- package/.env.example +10 -4
- package/.env.example.full +61 -8
- package/README.md +14 -5
- package/dist/cli/index.js +45 -0
- package/dist/cli/init-wizard.js +47 -10
- package/dist/cli/init-wizard.test.js +169 -4
- package/dist/config.js +31 -3
- package/dist/config.test.js +111 -4
- package/dist/cron/executor.js +18 -0
- package/dist/cron/executor.test.js +118 -0
- package/dist/cron/run-stats.js +33 -2
- package/dist/cron/run-stats.test.js +91 -2
- package/dist/discord/action-categories.js +41 -0
- package/dist/discord/actions-config.js +6 -0
- package/dist/discord/actions-config.test.js +57 -0
- package/dist/discord/actions-imagegen.js +240 -0
- package/dist/discord/actions-imagegen.test.js +643 -0
- package/dist/discord/actions.js +15 -0
- package/dist/discord/actions.test.js +36 -0
- package/dist/discord/allowlist.js +17 -0
- package/dist/discord/allowlist.test.js +21 -1
- package/dist/discord/deferred-runner.js +38 -7
- package/dist/discord/deferred-runner.test.js +194 -0
- package/dist/discord/forge-commands.js +70 -7
- package/dist/discord/forge-commands.test.js +263 -6
- package/dist/discord/memory-timing.integration.test.js +2 -0
- package/dist/discord/message-batching.js +38 -0
- package/dist/discord/message-batching.test.js +176 -0
- package/dist/discord/message-coordinator.js +159 -41
- package/dist/discord/message-coordinator.onboarding.test.js +2 -0
- package/dist/discord/message-coordinator.plan-run.test.js +2 -0
- package/dist/discord/message-coordinator.reaction-cleanup.test.js +205 -0
- package/dist/discord/models-command.js +2 -0
- package/dist/discord/models-command.test.js +5 -0
- package/dist/discord/output-common.js +90 -4
- package/dist/discord/output-common.test.js +76 -1
- package/dist/discord/output-utils.js +3 -0
- package/dist/discord/plan-manager.js +6 -2
- package/dist/discord/plan-manager.test.js +5 -0
- package/dist/discord/prompt-common.js +1 -1
- package/dist/discord/prompt-common.test.js +2 -0
- package/dist/discord/reaction-handler.js +4 -5
- package/dist/discord/reaction-handler.test.js +40 -1
- package/dist/discord/startup-profile.test.js +2 -0
- package/dist/discord/summarizer.test.js +37 -0
- package/dist/discord/update-command.js +60 -0
- package/dist/discord/update-command.test.js +68 -0
- package/dist/discord/user-errors.js +10 -0
- package/dist/discord/user-errors.test.js +9 -1
- package/dist/discord-followup.test.js +155 -1
- package/dist/discord.fail-closed.test.js +6 -0
- package/dist/discord.health-command.integration.test.js +2 -0
- package/dist/discord.prompt-context.test.js +162 -0
- package/dist/discord.status-wiring.test.js +2 -0
- package/dist/health/startup-healing.js +19 -0
- package/dist/health/startup-healing.test.js +35 -1
- package/dist/index.js +33 -5
- package/dist/index.post-connect.js +2 -0
- package/dist/npm-managed.js +82 -0
- package/dist/npm-managed.test.js +123 -0
- package/dist/observability/metrics.js +2 -0
- package/dist/runtime/cli-adapter.js +2 -2
- package/dist/runtime/model-tiers.js +41 -1
- package/dist/runtime/model-tiers.test.js +38 -2
- package/dist/runtime/strategies/claude-strategy.js +9 -0
- package/dist/tasks/store.js +61 -0
- package/dist/tasks/store.test.js +124 -0
- package/dist/tasks/task-action-executor.test.js +20 -0
- package/dist/tasks/task-action-read-ops.js +7 -1
- package/package.json +1 -1
package/.context/discord.md
CHANGED
|
@@ -85,11 +85,13 @@ Each action category has its own flag (only active when the master switch is `1`
|
|
|
85
85
|
| `DISCOCLAW_DISCORD_ACTIONS_PLAN` | `1` | planList, planShow, planApprove, planClose, planCreate, planRun |
|
|
86
86
|
| `DISCOCLAW_DISCORD_ACTIONS_MEMORY` | `1` | memoryRemember, memoryForget, memoryShow |
|
|
87
87
|
| `DISCOCLAW_DISCORD_ACTIONS_DEFER` | `1` | defer |
|
|
88
|
+
| `DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN` | `0` | generateImage |
|
|
88
89
|
| _(config — always on)_ | — | modelSet, modelShow |
|
|
89
90
|
|
|
90
91
|
Notes:
|
|
91
92
|
- `reactionPrompt` is gated by the MESSAGING flag — it is registered via `REACTION_PROMPT_ACTION_TYPES` only when `flags.messaging` is true (`src/discord/actions.ts:113`).
|
|
92
93
|
- Config actions (`modelSet`, `modelShow`) have no separate env flag. They are always enabled when the master switch is on, hardcoded in `src/index.ts`.
|
|
94
|
+
- `generateImage` supports two providers: **OpenAI** (models: `dall-e-3`, `gpt-image-1`) and **Gemini** (models: `imagen-4.0-generate-001`, `imagen-4.0-fast-generate-001`, `imagen-4.0-ultra-generate-001`). Provider is auto-detected from the model prefix (`dall-e-*`/`gpt-image-*` → openai, `imagen-*` → gemini) or set explicitly via the `provider` field. OpenAI provider uses `OPENAI_API_KEY` (required) and optional `OPENAI_BASE_URL`. Gemini provider uses `IMAGEGEN_GEMINI_API_KEY`. At least one key must be set when `DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=1`. Default model is auto-detected: if only `IMAGEGEN_GEMINI_API_KEY` is set, defaults to `imagen-4.0-generate-001`; otherwise defaults to `dall-e-3`. Override with `IMAGEGEN_DEFAULT_MODEL`.
|
|
93
95
|
|
|
94
96
|
Auto-follow-up: When query actions (channelList, channelInfo, threadListArchived, forumTagList, readMessages, fetchMessage, listPins, memberInfo, roleInfo, searchMessages, eventList, taskList, taskShow, cronList, cronShow, planList, planShow, memoryShow, modelShow, forgeStatus) succeed, DiscoClaw automatically re-invokes Claude with the results. This allows Claude to reason about query results without requiring the user to send a follow-up message. Controlled by `DISCOCLAW_ACTION_FOLLOWUP_DEPTH` (default `3`, `0` disables). Mutation-only responses do not trigger follow-ups. Trivially short follow-up responses (<50 chars with no actions) are suppressed.
|
|
95
97
|
|
package/.context/pa.md
CHANGED
|
@@ -14,7 +14,7 @@ For architecture details, see `.context/architecture.md`.
|
|
|
14
14
|
| `SOUL.md` | Core personality and values | Every prompt |
|
|
15
15
|
| `IDENTITY.md` | Name and vibe | Every prompt |
|
|
16
16
|
| `USER.md` | Who you're helping | Every prompt |
|
|
17
|
-
| `AGENTS.md` | Your personal rules and conventions |
|
|
17
|
+
| `AGENTS.md` | Your personal rules and conventions | Every prompt |
|
|
18
18
|
| `TOOLS.md` | Available tools and integrations | Every prompt |
|
|
19
19
|
| `HEARTBEAT.md` | Periodic self-check template | By cron |
|
|
20
20
|
| `MEMORY.md` | Curated long-term memory | DM prompts |
|
package/.context/runtime.md
CHANGED
|
@@ -194,7 +194,7 @@ Both require `CLAUDE_OUTPUT_FORMAT=stream-json` for structured events.
|
|
|
194
194
|
|
|
195
195
|
**Budget semantics:** For multi-turn sessions, budget accumulates across turns and cannot be reset mid-session. Recommend $5-10 for multi-turn.
|
|
196
196
|
|
|
197
|
-
**Append system prompt:** When set, workspace PA files (SOUL.md, IDENTITY.md, USER.md, TOOLS.md) are skipped from the context file list (their content is already in the system prompt). PA context modules (`.context/pa.md`, `.context/pa-safety.md`) and channel-specific context are unaffected. **Note:** Do not set this on first run before `workspace/BOOTSTRAP.md` has been consumed — the skip logic also bypasses BOOTSTRAP.md loading.
|
|
197
|
+
**Append system prompt:** When set, workspace PA files (SOUL.md, IDENTITY.md, USER.md, AGENTS.md, TOOLS.md) are skipped from the context file list (their content is already in the system prompt). PA context modules (`.context/pa.md`, `.context/pa-safety.md`) and channel-specific context are unaffected. **Note:** Do not set this on first run before `workspace/BOOTSTRAP.md` has been consumed — the skip logic also bypasses BOOTSTRAP.md loading.
|
|
198
198
|
|
|
199
199
|
- **Session scanner** (`src/runtime/session-scanner.ts`): watches `~/.claude/projects/<escaped-cwd>/<session-id>.jsonl`, skips pre-existing content, degrades gracefully if the file never appears.
|
|
200
200
|
- **Tool-aware queue** (`src/discord/tool-aware-queue.ts`): state machine that suppresses narration text before tools, shows human-readable activity labels (from `src/runtime/tool-labels.ts`), and streams the final answer after all tool use completes.
|
package/.env.example
CHANGED
|
@@ -20,11 +20,17 @@ DISCORD_TOKEN=
|
|
|
20
20
|
# Empty = nobody can use it (fail-closed).
|
|
21
21
|
DISCORD_ALLOW_USER_IDS=
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
|
|
23
|
+
# ----------------------------------------------------------
|
|
24
|
+
# AUTO-DETECTED — written by the bot on first connect; only override if needed
|
|
25
|
+
# ----------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
# Tasks forum channel ID. Auto-created by the bot on first guild connect and
|
|
28
|
+
# persisted to system-scaffold.json. Only set this to override the auto-created channel.
|
|
29
|
+
#DISCOCLAW_TASKS_FORUM=
|
|
25
30
|
|
|
26
|
-
# Automations forum channel ID
|
|
27
|
-
|
|
31
|
+
# Automations (cron) forum channel ID. Auto-created by the bot on first guild
|
|
32
|
+
# connect and persisted to system-scaffold.json. Only set this to override the auto-created channel.
|
|
33
|
+
#DISCOCLAW_CRON_FORUM=
|
|
28
34
|
|
|
29
35
|
# ----------------------------------------------------------
|
|
30
36
|
# CORE — most users will want to review these
|
package/.env.example.full
CHANGED
|
@@ -20,11 +20,17 @@ DISCORD_TOKEN=
|
|
|
20
20
|
# Empty = nobody can use it (fail-closed).
|
|
21
21
|
DISCORD_ALLOW_USER_IDS=
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
|
|
23
|
+
# ----------------------------------------------------------
|
|
24
|
+
# AUTO-DETECTED — written by the bot on first connect; only override if needed
|
|
25
|
+
# ----------------------------------------------------------
|
|
25
26
|
|
|
26
|
-
#
|
|
27
|
-
|
|
27
|
+
# Tasks forum channel ID. Auto-created by the bot on first guild connect and
|
|
28
|
+
# persisted to system-scaffold.json. Only set this to override the auto-created channel.
|
|
29
|
+
#DISCOCLAW_TASKS_FORUM=
|
|
30
|
+
|
|
31
|
+
# Automations (cron) forum channel ID. Auto-created by the bot on first guild
|
|
32
|
+
# connect and persisted to system-scaffold.json. Only set this to override the auto-created channel.
|
|
33
|
+
#DISCOCLAW_CRON_FORUM=
|
|
28
34
|
|
|
29
35
|
# ----------------------------------------------------------
|
|
30
36
|
# CORE — most users will want to review these
|
|
@@ -52,6 +58,28 @@ DISCOCLAW_CRON_FORUM=
|
|
|
52
58
|
# Individual overrides (DISCOCLAW_SUMMARY_MODEL, etc.) still win when set.
|
|
53
59
|
#DISCOCLAW_FAST_MODEL=fast
|
|
54
60
|
|
|
61
|
+
# --- Tier model overrides ---
|
|
62
|
+
# Override the concrete model resolved for any runtime × tier combination.
|
|
63
|
+
# Format: DISCOCLAW_TIER_<RUNTIME>_<FAST|CAPABLE>=<model>
|
|
64
|
+
# Unset = use the built-in default shown in the comments.
|
|
65
|
+
# Concrete model names (e.g. sonnet, gpt-4o-mini) are passed through unchanged.
|
|
66
|
+
#
|
|
67
|
+
# Claude Code adapter (default: fast=haiku, capable=opus):
|
|
68
|
+
#DISCOCLAW_TIER_CLAUDE_CODE_FAST=haiku
|
|
69
|
+
#DISCOCLAW_TIER_CLAUDE_CODE_CAPABLE=opus
|
|
70
|
+
#
|
|
71
|
+
# Gemini CLI adapter (default: fast=gemini-2.5-flash, capable=gemini-2.5-pro):
|
|
72
|
+
#DISCOCLAW_TIER_GEMINI_FAST=gemini-2.5-flash
|
|
73
|
+
#DISCOCLAW_TIER_GEMINI_CAPABLE=gemini-2.5-pro
|
|
74
|
+
#
|
|
75
|
+
# OpenAI-compatible adapter (default: adapter-default for both tiers):
|
|
76
|
+
#DISCOCLAW_TIER_OPENAI_FAST=gpt-4o-mini
|
|
77
|
+
#DISCOCLAW_TIER_OPENAI_CAPABLE=gpt-4o
|
|
78
|
+
#
|
|
79
|
+
# Codex CLI adapter (default: adapter-default for both tiers):
|
|
80
|
+
#DISCOCLAW_TIER_CODEX_FAST=
|
|
81
|
+
#DISCOCLAW_TIER_CODEX_CAPABLE=
|
|
82
|
+
|
|
55
83
|
# Output format for the Claude CLI. stream-json gives smoother streaming.
|
|
56
84
|
#CLAUDE_OUTPUT_FORMAT=stream-json
|
|
57
85
|
|
|
@@ -76,7 +104,7 @@ DISCOCLAW_CRON_FORUM=
|
|
|
76
104
|
# ----------------------------------------------------------
|
|
77
105
|
# Cron — forum-based scheduled tasks (enabled by default)
|
|
78
106
|
# ----------------------------------------------------------
|
|
79
|
-
# Forum channel ID is
|
|
107
|
+
# Forum channel ID is auto-created on first connect (see AUTO-DETECTED above).
|
|
80
108
|
# Model tier for cron execution: fast | capable (concrete names accepted as passthrough).
|
|
81
109
|
#DISCOCLAW_CRON_MODEL=fast
|
|
82
110
|
# Enable cron Discord actions (CRUD via Discord action blocks).
|
|
@@ -97,7 +125,7 @@ DISCOCLAW_CRON_FORUM=
|
|
|
97
125
|
# ----------------------------------------------------------
|
|
98
126
|
# Tasks — task tracking (enabled by default)
|
|
99
127
|
# ----------------------------------------------------------
|
|
100
|
-
# Forum channel ID is
|
|
128
|
+
# Forum channel ID is auto-created on first connect (see AUTO-DETECTED above).
|
|
101
129
|
# Guild ID — used for task sync bootstrap and system channel bootstrap.
|
|
102
130
|
# Also used for System channel bootstrap when the bot is in multiple servers.
|
|
103
131
|
DISCORD_GUILD_ID=
|
|
@@ -308,12 +336,19 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
|
|
|
308
336
|
#DISCOCLAW_SESSION_SCANNING=1
|
|
309
337
|
# Parse tool-use events during streaming for better progress reporting and stall suppression.
|
|
310
338
|
#DISCOCLAW_TOOL_AWARE_STREAMING=1
|
|
311
|
-
# Stream stall detection: kill one-shot process if no stdout/stderr for this long (ms). 0 = disabled.
|
|
339
|
+
# Stream stall detection: kill one-shot process if no stdout/stderr for this long (ms). 0 = disabled. (default: 600000)
|
|
312
340
|
#DISCOCLAW_STREAM_STALL_TIMEOUT_MS=120000
|
|
313
341
|
# Progress stall timeout: alert after this many ms with no progress event (ms). 0 = disabled.
|
|
314
342
|
#DISCOCLAW_PROGRESS_STALL_TIMEOUT_MS=300000
|
|
315
|
-
# Stream stall warning: show user-visible warning in Discord after this many ms of no events. 0 = disabled.
|
|
343
|
+
# Stream stall warning: show user-visible warning in Discord after this many ms of no events. 0 = disabled. (default: 300000)
|
|
316
344
|
#DISCOCLAW_STREAM_STALL_WARNING_MS=60000
|
|
345
|
+
# Post a brief "Done (Xm Ys)" completion notice as a new message after long-running runs finish.
|
|
346
|
+
# Discord's unread indicator fires on new messages but not edits, so users who left the channel
|
|
347
|
+
# while the bot was working will see an unread badge. Set to 0 to disable.
|
|
348
|
+
#DISCOCLAW_COMPLETION_NOTIFY=1
|
|
349
|
+
# Minimum elapsed time (ms) before a completion notice is sent. Runs shorter than this are
|
|
350
|
+
# considered "fast" and don't need a notification. Default: 30000 (30 seconds).
|
|
351
|
+
#DISCOCLAW_COMPLETION_NOTIFY_THRESHOLD_MS=30000
|
|
317
352
|
|
|
318
353
|
# ----------------------------------------------------------
|
|
319
354
|
# Plan & Forge — AI-assisted planning and implementation
|
|
@@ -405,3 +440,21 @@ DISCOCLAW_DISCORD_ACTIONS_DEFER=1
|
|
|
405
440
|
# Chromium path for agent-browser. Only needed if the bundled browser
|
|
406
441
|
# (from `agent-browser install`) doesn't work.
|
|
407
442
|
#AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
|
443
|
+
|
|
444
|
+
# ----------------------------------------------------------
|
|
445
|
+
# Image generation
|
|
446
|
+
# ----------------------------------------------------------
|
|
447
|
+
# Master switch — enables the imagegen Discord action category (default: off).
|
|
448
|
+
# When enabled, the AI can generate images via action blocks using OpenAI or Gemini Imagen.
|
|
449
|
+
DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=0
|
|
450
|
+
# API key for Gemini Imagen models (imagen-4.0-generate-001 and similar).
|
|
451
|
+
# Leave unset to use OpenAI only.
|
|
452
|
+
#IMAGEGEN_GEMINI_API_KEY=
|
|
453
|
+
# Override the default image generation model. If unset, auto-detected:
|
|
454
|
+
# only IMAGEGEN_GEMINI_API_KEY set → imagen-4.0-generate-001; otherwise → dall-e-3.
|
|
455
|
+
# OpenAI models: dall-e-3, gpt-image-1
|
|
456
|
+
# Gemini models: imagen-4.0-generate-001, imagen-4.0-fast-generate-001, imagen-4.0-ultra-generate-001
|
|
457
|
+
#IMAGEGEN_DEFAULT_MODEL=
|
|
458
|
+
# Note: OpenAI image generation reuses OPENAI_API_KEY (documented above in the
|
|
459
|
+
# OpenAI-compatible HTTP adapter section). When DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=1,
|
|
460
|
+
# at least one of OPENAI_API_KEY or IMAGEGEN_GEMINI_API_KEY must be set.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# DiscoClaw
|
|
6
6
|
|
|
7
|
-
A personal AI orchestrator that turns Discord into a persistent workspace — built on three pillars: **Memory**, **Tasks**, and **
|
|
7
|
+
A personal AI orchestrator that turns Discord into a persistent workspace — built on three pillars: **Memory**, **Tasks**, and **Automations**.
|
|
8
8
|
|
|
9
9
|
DiscoClaw is an orchestrator: it coordinates between a user interface (Discord), one or more AI runtimes (Claude Code, OpenAI, Codex), and local system resources — managing conversation state, task routing, scheduling, and tool access. The intelligence is rented; the coordination is owned.
|
|
10
10
|
|
|
@@ -39,11 +39,11 @@ A lightweight in-process task store that syncs bidirectionally with Discord foru
|
|
|
39
39
|
- **Create from either side** — Ask your assistant in chat or use task commands
|
|
40
40
|
- **Bidirectional sync** — Status, priority, and tags stay in sync between the task store and Discord threads
|
|
41
41
|
- **Status emoji and auto-tagging** — Thread names show live status at a glance
|
|
42
|
-
- **Discord actions** — Your assistant manages tasks through conversation: create channels, send messages, search history, run polls, and more
|
|
42
|
+
- **Discord actions** — Your assistant manages tasks through conversation: create channels, send messages, search history, run polls, and more (see [docs/discord-actions.md](docs/discord-actions.md))
|
|
43
43
|
|
|
44
44
|
**Why Discord fits:** forum threads = task cards, archive = done, thread names show live status.
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## Automations — the bot acts on its own
|
|
47
47
|
|
|
48
48
|
Recurring tasks defined as forum threads in plain language — no crontab, no separate scheduler UI.
|
|
49
49
|
|
|
@@ -56,7 +56,7 @@ Recurring tasks defined as forum threads in plain language — no crontab, no se
|
|
|
56
56
|
|
|
57
57
|
## How it works
|
|
58
58
|
|
|
59
|
-
DiscoClaw orchestrates the flow between Discord and AI runtimes (Claude Code by default, with OpenAI, Codex, and OpenRouter adapters available via `PRIMARY_RUNTIME`). It doesn't contain intelligence itself — it decides *when* to call the AI, *what context* to give it, and *what to do* with the output. When you send a message, the orchestrator:
|
|
59
|
+
DiscoClaw orchestrates the flow between Discord and AI runtimes (Claude Code by default, with Gemini, OpenAI, Codex, and OpenRouter adapters available via `PRIMARY_RUNTIME`). It doesn't contain intelligence itself — it decides *when* to call the AI, *what context* to give it, and *what to do* with the output. When you send a message, the orchestrator:
|
|
60
60
|
|
|
61
61
|
1. Checks the user allowlist (fail-closed — empty list means respond to nobody)
|
|
62
62
|
2. Assembles context: per-channel rules, conversation history, rolling summary, and durable memory
|
|
@@ -93,10 +93,11 @@ Author one recipe file for an integration, share it, then let another user's Dis
|
|
|
93
93
|
- **Node.js >=20** — check with `node --version`
|
|
94
94
|
- One primary runtime:
|
|
95
95
|
- **Claude CLI** on your `PATH` — check with `claude --version` (see [Claude CLI docs](https://docs.anthropic.com/en/docs/claude-code) to install), or
|
|
96
|
+
- **Gemini CLI** on your `PATH` — check with `gemini --version`, or
|
|
96
97
|
- **Codex CLI** on your `PATH` — check with `codex --version`, or
|
|
97
98
|
- **OpenAI-compatible API key** via `OPENAI_API_KEY`, or
|
|
98
99
|
- **OpenRouter API key** via `OPENROUTER_API_KEY` (access to many providers)
|
|
99
|
-
- Runtime-specific access for your chosen provider (Anthropic plan/API credits for Claude, OpenAI access for Codex/OpenAI models)
|
|
100
|
+
- Runtime-specific access for your chosen provider (Anthropic plan/API credits for Claude, Google account for Gemini, OpenAI access for Codex/OpenAI models)
|
|
100
101
|
|
|
101
102
|
**Contributors (from source):**
|
|
102
103
|
- Everything above, plus **pnpm** — enable via Corepack (`corepack enable`) or install separately
|
|
@@ -158,6 +159,14 @@ pnpm dev
|
|
|
158
159
|
|
|
159
160
|
**Global install:**
|
|
160
161
|
|
|
162
|
+
If DiscoClaw is running, update from Discord:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
!update apply
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or from the command line:
|
|
169
|
+
|
|
161
170
|
```bash
|
|
162
171
|
npm update -g discoclaw
|
|
163
172
|
discoclaw install-daemon # re-register the service after updating
|
package/dist/cli/index.js
CHANGED
|
@@ -16,6 +16,49 @@ switch (command) {
|
|
|
16
16
|
case 'install-daemon':
|
|
17
17
|
await runDaemonInstaller();
|
|
18
18
|
break;
|
|
19
|
+
case 'update': {
|
|
20
|
+
const subcommand = process.argv[3];
|
|
21
|
+
const { isNpmManaged, getLocalVersion, getLatestNpmVersion, npmGlobalUpgrade } = await import('../npm-managed.js');
|
|
22
|
+
const npmMode = await isNpmManaged();
|
|
23
|
+
if (subcommand === 'apply') {
|
|
24
|
+
if (!npmMode) {
|
|
25
|
+
console.error('This instance is not npm-managed. Use the git-based workflow to update.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log('Installing latest version from npm...');
|
|
29
|
+
const result = await npmGlobalUpgrade();
|
|
30
|
+
if (result.exitCode !== 0) {
|
|
31
|
+
const detail = (result.stderr || result.stdout).trim().slice(0, 500);
|
|
32
|
+
console.error(`npm install -g discoclaw failed:\n${detail}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
console.log('Update complete. Restart discoclaw to run the new version.');
|
|
36
|
+
}
|
|
37
|
+
else if (subcommand === undefined) {
|
|
38
|
+
if (!npmMode) {
|
|
39
|
+
console.log('This instance is not npm-managed; update checking is not supported in this mode.');
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
const installed = getLocalVersion();
|
|
43
|
+
const latest = await getLatestNpmVersion();
|
|
44
|
+
if (latest === null) {
|
|
45
|
+
console.error('Failed to fetch latest version from npm registry.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
if (installed === latest) {
|
|
49
|
+
console.log(`Already on latest version (${installed}).`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(`Update available: ${installed} → ${latest}. Run \`discoclaw update apply\` to upgrade.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error(`Unknown update subcommand: ${subcommand}\n`);
|
|
57
|
+
printHelp(version);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
19
62
|
case '--version':
|
|
20
63
|
case '-v':
|
|
21
64
|
console.log(version);
|
|
@@ -38,6 +81,8 @@ function printHelp(ver) {
|
|
|
38
81
|
` install-daemon [--service-name <name>] Register discoclaw as a persistent background service\n` +
|
|
39
82
|
` Use --service-name to run multiple instances side-by-side.\n` +
|
|
40
83
|
` Defaults to "discoclaw".\n` +
|
|
84
|
+
` update Check for available updates (npm-managed installs only)\n` +
|
|
85
|
+
` update apply Install the latest version from npm and print restart reminder\n` +
|
|
41
86
|
`\nOptions:\n` +
|
|
42
87
|
` -v, --version Print version\n` +
|
|
43
88
|
` -h, --help Print this help\n`);
|
package/dist/cli/init-wizard.js
CHANGED
|
@@ -35,8 +35,9 @@ export function buildEnvContent(vals, now = new Date()) {
|
|
|
35
35
|
lines.push('# REQUIRED');
|
|
36
36
|
lines.push(`DISCORD_TOKEN=${vals.DISCORD_TOKEN ?? ''}`);
|
|
37
37
|
lines.push(`DISCORD_ALLOW_USER_IDS=${vals.DISCORD_ALLOW_USER_IDS ?? ''}`);
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (vals.DISCOCLAW_DATA_DIR) {
|
|
39
|
+
lines.push(`DISCOCLAW_DATA_DIR=${vals.DISCOCLAW_DATA_DIR}`);
|
|
40
|
+
}
|
|
40
41
|
lines.push('');
|
|
41
42
|
if (vals.PRIMARY_RUNTIME) {
|
|
42
43
|
const providerSpecificKeys = [
|
|
@@ -75,6 +76,16 @@ export function buildEnvContent(vals, now = new Date()) {
|
|
|
75
76
|
}
|
|
76
77
|
lines.push('');
|
|
77
78
|
}
|
|
79
|
+
const autoDetectedKeys = ['DISCOCLAW_TASKS_FORUM', 'DISCOCLAW_CRON_FORUM'];
|
|
80
|
+
const hasAutoDetected = autoDetectedKeys.some((k) => vals[k]);
|
|
81
|
+
if (hasAutoDetected) {
|
|
82
|
+
lines.push('# AUTO-DETECTED');
|
|
83
|
+
for (const k of autoDetectedKeys) {
|
|
84
|
+
if (vals[k])
|
|
85
|
+
lines.push(`${k}=${vals[k]}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
78
89
|
lines.push('# For all options, see .env.example.full');
|
|
79
90
|
lines.push('');
|
|
80
91
|
return lines.join('\n');
|
|
@@ -94,8 +105,7 @@ export async function runInitWizard() {
|
|
|
94
105
|
console.error('discoclaw init requires an interactive terminal.\n');
|
|
95
106
|
process.exit(1);
|
|
96
107
|
}
|
|
97
|
-
|
|
98
|
-
const envPath = path.join(cwd, '.env');
|
|
108
|
+
let cwd = process.cwd();
|
|
99
109
|
let rl = null;
|
|
100
110
|
let canceled = false;
|
|
101
111
|
let completed = false;
|
|
@@ -152,8 +162,15 @@ export async function runInitWizard() {
|
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
164
|
// ── Welcome ──────────────────────────────────────────────────────────────
|
|
155
|
-
console.log(`\nDiscoclaw Init\n
|
|
156
|
-
|
|
165
|
+
console.log(`\nDiscoclaw Init\n==============`);
|
|
166
|
+
const installDirInput = await ask(`Install directory [${cwd}]: `);
|
|
167
|
+
if (installDirInput.trim()) {
|
|
168
|
+
cwd = path.resolve(installDirInput.trim());
|
|
169
|
+
if (!fs.existsSync(cwd)) {
|
|
170
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
console.log(`This wizard creates a .env file and workspace/ directory in:\n ${cwd}\n`);
|
|
157
174
|
// ── Discord bot guidance ──────────────────────────────────────────────────
|
|
158
175
|
console.log(`Discord Bot Setup\n-----------------\n` +
|
|
159
176
|
`If you haven't created a Discord bot yet, follow these steps:\n\n` +
|
|
@@ -163,11 +180,21 @@ export async function runInitWizard() {
|
|
|
163
180
|
` 4. Enable "Message Content Intent" under Privileged Gateway Intents.\n` +
|
|
164
181
|
` 5. Click "Reset Token", copy it — you'll enter it below.\n` +
|
|
165
182
|
` 6. Invite your bot: Bot tab → OAuth2 → URL Generator\n` +
|
|
166
|
-
` Scopes: bot Permissions:
|
|
183
|
+
` Scopes: bot Permissions: View Channels, Send Messages,\n` +
|
|
184
|
+
` Read Message History, Manage Channels, Manage Threads,\n` +
|
|
185
|
+
` Send Messages in Threads\n` +
|
|
167
186
|
` Open the generated URL and select your server.\n\n` +
|
|
168
187
|
`Already have a bot? Just press Enter.\n`);
|
|
169
188
|
await ask('Press Enter to continue... ');
|
|
189
|
+
// ── Data directory ────────────────────────────────────────────────────────
|
|
190
|
+
const defaultDataDir = path.join(cwd, 'data');
|
|
191
|
+
const dataDirInput = await ask(`Data directory [${defaultDataDir}]: `);
|
|
192
|
+
const dataDir = dataDirInput.trim() || defaultDataDir;
|
|
193
|
+
// ── Collected values ──────────────────────────────────────────────────────
|
|
194
|
+
const values = {};
|
|
195
|
+
values.DISCOCLAW_DATA_DIR = dataDir;
|
|
170
196
|
// ── Check existing .env ───────────────────────────────────────────────────
|
|
197
|
+
const envPath = path.join(cwd, '.env');
|
|
171
198
|
if (fs.existsSync(envPath)) {
|
|
172
199
|
const existing = fs.readFileSync(envPath, 'utf8');
|
|
173
200
|
const tokenMatch = existing.match(/^DISCORD_TOKEN=(.*)$/m);
|
|
@@ -200,13 +227,23 @@ export async function runInitWizard() {
|
|
|
200
227
|
const backupPath = path.join(cwd, bkName);
|
|
201
228
|
fs.copyFileSync(envPath, backupPath);
|
|
202
229
|
console.log(` Backed up to ${bkName}\n`);
|
|
230
|
+
// Carry forward auto-detected forum channel IDs so explicit overrides survive re-runs
|
|
231
|
+
const tasksMatch = existing.match(/^DISCOCLAW_TASKS_FORUM=(.*)$/m);
|
|
232
|
+
const cronMatch = existing.match(/^DISCOCLAW_CRON_FORUM=(.*)$/m);
|
|
233
|
+
if (tasksMatch?.[1]?.trim())
|
|
234
|
+
values.DISCOCLAW_TASKS_FORUM = tasksMatch[1].trim();
|
|
235
|
+
if (cronMatch?.[1]?.trim())
|
|
236
|
+
values.DISCOCLAW_CRON_FORUM = cronMatch[1].trim();
|
|
203
237
|
}
|
|
204
238
|
// ── Required values ───────────────────────────────────────────────────────
|
|
205
|
-
const values = {};
|
|
206
239
|
values.DISCORD_TOKEN = await askValidated('Discord bot token: ', (val) => {
|
|
207
240
|
const r = validateDiscordToken(val);
|
|
208
241
|
return r.valid ? null : (r.reason ?? 'Invalid token format');
|
|
209
242
|
});
|
|
243
|
+
console.log(`\nDiscord User ID\n` +
|
|
244
|
+
` A Discord user ID is an 18-19 digit number uniquely identifying your account.\n` +
|
|
245
|
+
` To find yours: Settings → Advanced → enable Developer Mode,\n` +
|
|
246
|
+
` then right-click your username anywhere and choose "Copy User ID".\n`);
|
|
210
247
|
values.DISCORD_ALLOW_USER_IDS = await askValidated('Allowed user IDs (comma-separated): ', (val) => {
|
|
211
248
|
if (!val.trim())
|
|
212
249
|
return 'At least one user ID is required';
|
|
@@ -217,8 +254,7 @@ export async function runInitWizard() {
|
|
|
217
254
|
return 'At least one valid snowflake ID is required';
|
|
218
255
|
return null;
|
|
219
256
|
});
|
|
220
|
-
|
|
221
|
-
values.DISCOCLAW_CRON_FORUM = await askValidated('Automations forum channel ID (required): ', (val) => (validateSnowflake(val) ? null : 'Must be a 17-20 digit number'));
|
|
257
|
+
// (DISCOCLAW_TASKS_FORUM and DISCOCLAW_CRON_FORUM are auto-created on first connect)
|
|
222
258
|
// ── Runtime detection ─────────────────────────────────────────────────────
|
|
223
259
|
const detected = [];
|
|
224
260
|
if (which('claude'))
|
|
@@ -349,6 +385,7 @@ export async function runInitWizard() {
|
|
|
349
385
|
}
|
|
350
386
|
console.log('Configuration complete!\n');
|
|
351
387
|
console.log('Next steps:');
|
|
388
|
+
console.log(' Note: The bot will auto-create its forum channels on first connect.');
|
|
352
389
|
if (values.PRIMARY_RUNTIME === 'claude') {
|
|
353
390
|
console.log(` ${daemonHint}`);
|
|
354
391
|
}
|
|
@@ -55,6 +55,16 @@ describe('init wizard helpers', () => {
|
|
|
55
55
|
expect(content).toContain('DISCORD_GUILD_ID=1000000000000000004');
|
|
56
56
|
expect(content).toContain('# OPTIONAL');
|
|
57
57
|
expect(content).toContain('DISCOCLAW_DISCORD_ACTIONS=1');
|
|
58
|
+
// Forum IDs are auto-detected, not in REQUIRED
|
|
59
|
+
expect(content).toContain('# AUTO-DETECTED');
|
|
60
|
+
expect(content).toContain('DISCOCLAW_TASKS_FORUM=1000000000000000002');
|
|
61
|
+
expect(content).toContain('DISCOCLAW_CRON_FORUM=1000000000000000003');
|
|
62
|
+
// Forum IDs must not appear before the AUTO-DETECTED header
|
|
63
|
+
const requiredIdx = content.indexOf('# REQUIRED');
|
|
64
|
+
const autoDetectedIdx = content.indexOf('# AUTO-DETECTED');
|
|
65
|
+
const tasksIdx = content.indexOf('DISCOCLAW_TASKS_FORUM=');
|
|
66
|
+
expect(tasksIdx).toBeGreaterThan(autoDetectedIdx);
|
|
67
|
+
expect(autoDetectedIdx).toBeGreaterThan(requiredIdx);
|
|
58
68
|
});
|
|
59
69
|
it('selects provider defaults in expected precedence order', () => {
|
|
60
70
|
expect(selectDefaultProvider(['codex'])).toBe('4');
|
|
@@ -82,6 +92,32 @@ describe('init wizard helpers', () => {
|
|
|
82
92
|
expect(content).toContain('OPENROUTER_API_KEY=sk-or-test-key');
|
|
83
93
|
expect(content).toContain('OPENROUTER_BASE_URL=https://openrouter.ai/api/v1');
|
|
84
94
|
expect(content).toContain('OPENROUTER_MODEL=anthropic/claude-sonnet-4');
|
|
95
|
+
expect(content).toContain('# AUTO-DETECTED');
|
|
96
|
+
expect(content).toContain('DISCOCLAW_TASKS_FORUM=1000000000000000002');
|
|
97
|
+
expect(content).toContain('DISCOCLAW_CRON_FORUM=1000000000000000003');
|
|
98
|
+
});
|
|
99
|
+
it('writes DISCOCLAW_DATA_DIR in required section when provided', () => {
|
|
100
|
+
const content = buildEnvContent({
|
|
101
|
+
DISCORD_TOKEN: 'a.b.c',
|
|
102
|
+
DISCORD_ALLOW_USER_IDS: '1000000000000000001',
|
|
103
|
+
DISCOCLAW_DATA_DIR: '/home/user/discoclaw-data',
|
|
104
|
+
}, new Date('2026-02-23T00:00:00.000Z'));
|
|
105
|
+
expect(content).toContain('DISCOCLAW_DATA_DIR=/home/user/discoclaw-data');
|
|
106
|
+
// Must appear inside the REQUIRED section (before any PROVIDER or AUTO-DETECTED section)
|
|
107
|
+
const requiredIdx = content.indexOf('# REQUIRED');
|
|
108
|
+
const dataDirIdx = content.indexOf('DISCOCLAW_DATA_DIR=');
|
|
109
|
+
expect(dataDirIdx).toBeGreaterThan(requiredIdx);
|
|
110
|
+
// No AUTO-DETECTED section when no forum IDs are present
|
|
111
|
+
expect(content).not.toContain('# AUTO-DETECTED');
|
|
112
|
+
});
|
|
113
|
+
it('omits auto-detected section when no forum IDs are present', () => {
|
|
114
|
+
const content = buildEnvContent({
|
|
115
|
+
DISCORD_TOKEN: 'a.b.c',
|
|
116
|
+
DISCORD_ALLOW_USER_IDS: '1000000000000000001',
|
|
117
|
+
}, new Date('2026-02-23T00:00:00.000Z'));
|
|
118
|
+
expect(content).not.toContain('# AUTO-DETECTED');
|
|
119
|
+
expect(content).not.toContain('DISCOCLAW_TASKS_FORUM');
|
|
120
|
+
expect(content).not.toContain('DISCOCLAW_CRON_FORUM');
|
|
85
121
|
});
|
|
86
122
|
});
|
|
87
123
|
describe('runInitWizard', () => {
|
|
@@ -117,12 +153,12 @@ describe('runInitWizard', () => {
|
|
|
117
153
|
const previousCwd = process.cwd();
|
|
118
154
|
const oldEnv = 'DISCORD_TOKEN=old.token.value\nDISCORD_ALLOW_USER_IDS=111111111111111111\n';
|
|
119
155
|
const answers = [
|
|
156
|
+
'', // install directory (default)
|
|
120
157
|
'', // Press Enter to continue
|
|
158
|
+
'', // data directory (default cwd/data)
|
|
121
159
|
'y', // Overwrite existing .env
|
|
122
160
|
'a.b.c', // DISCORD_TOKEN
|
|
123
161
|
'1000000000000000001', // DISCORD_ALLOW_USER_IDS
|
|
124
|
-
'1000000000000000002', // DISCOCLAW_TASKS_FORUM
|
|
125
|
-
'1000000000000000003', // DISCOCLAW_CRON_FORUM
|
|
126
162
|
'', // provider selection -> default (Claude)
|
|
127
163
|
'', // enable skip permissions
|
|
128
164
|
'', // enable stream-json
|
|
@@ -151,18 +187,19 @@ describe('runInitWizard', () => {
|
|
|
151
187
|
expect(newEnv).toContain('PRIMARY_RUNTIME=claude');
|
|
152
188
|
expect(newEnv).toContain('CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=1');
|
|
153
189
|
expect(newEnv).toContain('CLAUDE_OUTPUT_FORMAT=stream-json');
|
|
190
|
+
expect(newEnv).toContain(`DISCOCLAW_DATA_DIR=${path.join(tmpDir, 'data')}`);
|
|
154
191
|
expect(ensureWorkspaceBootstrapFiles).toHaveBeenCalledWith(path.join(tmpDir, 'workspace'));
|
|
155
192
|
});
|
|
156
193
|
it('writes openrouter config when provider 5 is selected', async () => {
|
|
157
194
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
|
|
158
195
|
const previousCwd = process.cwd();
|
|
159
196
|
const answers = [
|
|
197
|
+
'', // install directory (default)
|
|
160
198
|
'', // Press Enter to continue
|
|
199
|
+
'', // data directory (default cwd/data)
|
|
161
200
|
// no existing .env
|
|
162
201
|
'a.b.c', // DISCORD_TOKEN
|
|
163
202
|
'1000000000000000001', // DISCORD_ALLOW_USER_IDS
|
|
164
|
-
'1000000000000000002', // DISCOCLAW_TASKS_FORUM
|
|
165
|
-
'1000000000000000003', // DISCOCLAW_CRON_FORUM
|
|
166
203
|
'5', // provider selection -> OpenRouter
|
|
167
204
|
'sk-or-test-key', // OPENROUTER_API_KEY
|
|
168
205
|
'', // OPENROUTER_BASE_URL (optional, skip)
|
|
@@ -187,5 +224,133 @@ describe('runInitWizard', () => {
|
|
|
187
224
|
expect(newEnv).toContain('PRIMARY_RUNTIME=openrouter');
|
|
188
225
|
expect(newEnv).toContain('OPENROUTER_API_KEY=sk-or-test-key');
|
|
189
226
|
expect(newEnv).toContain('OPENROUTER_MODEL=anthropic/claude-sonnet-4');
|
|
227
|
+
expect(newEnv).toContain(`DISCOCLAW_DATA_DIR=${path.join(tmpDir, 'data')}`);
|
|
228
|
+
});
|
|
229
|
+
it('always writes DISCOCLAW_DATA_DIR when a custom path is given', async () => {
|
|
230
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
|
|
231
|
+
const previousCwd = process.cwd();
|
|
232
|
+
const customDataDir = path.join(tmpDir, 'my-data');
|
|
233
|
+
const answers = [
|
|
234
|
+
'', // install directory (default)
|
|
235
|
+
'', // Press Enter to continue
|
|
236
|
+
customDataDir, // data directory (custom path)
|
|
237
|
+
'a.b.c', // DISCORD_TOKEN
|
|
238
|
+
'1000000000000000001', // DISCORD_ALLOW_USER_IDS
|
|
239
|
+
'', // provider selection -> default (Claude)
|
|
240
|
+
'', // enable skip permissions
|
|
241
|
+
'', // enable stream-json
|
|
242
|
+
'n', // configure recommended settings
|
|
243
|
+
'n', // configure optional features
|
|
244
|
+
];
|
|
245
|
+
process.chdir(tmpDir);
|
|
246
|
+
vi.mocked(createInterface).mockReturnValue(makeReadline(answers));
|
|
247
|
+
vi.mocked(execFileSync).mockImplementation(() => {
|
|
248
|
+
throw new Error('binary not found');
|
|
249
|
+
});
|
|
250
|
+
vi.mocked(ensureWorkspaceBootstrapFiles).mockResolvedValue([]);
|
|
251
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
252
|
+
try {
|
|
253
|
+
await runInitWizard();
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
process.chdir(previousCwd);
|
|
257
|
+
}
|
|
258
|
+
const newEnv = fs.readFileSync(path.join(tmpDir, '.env'), 'utf8');
|
|
259
|
+
expect(newEnv).toContain(`DISCOCLAW_DATA_DIR=${customDataDir}`);
|
|
260
|
+
});
|
|
261
|
+
it('carries forward DISCOCLAW_TASKS_FORUM and DISCOCLAW_CRON_FORUM from existing .env on overwrite', async () => {
|
|
262
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
|
|
263
|
+
const previousCwd = process.cwd();
|
|
264
|
+
const oldEnv = [
|
|
265
|
+
'DISCORD_TOKEN=old.token.value',
|
|
266
|
+
'DISCORD_ALLOW_USER_IDS=111111111111111111',
|
|
267
|
+
'DISCOCLAW_TASKS_FORUM=9000000000000000001',
|
|
268
|
+
'DISCOCLAW_CRON_FORUM=9000000000000000002',
|
|
269
|
+
].join('\n') + '\n';
|
|
270
|
+
const answers = [
|
|
271
|
+
'', // install directory (default)
|
|
272
|
+
'', // Press Enter to continue
|
|
273
|
+
'', // data directory (default cwd/data)
|
|
274
|
+
'y', // Overwrite existing .env
|
|
275
|
+
'a.b.c', // DISCORD_TOKEN
|
|
276
|
+
'1000000000000000001', // DISCORD_ALLOW_USER_IDS
|
|
277
|
+
'', // provider selection -> default (Claude)
|
|
278
|
+
'', // enable skip permissions
|
|
279
|
+
'', // enable stream-json
|
|
280
|
+
'n', // configure recommended settings
|
|
281
|
+
'n', // configure optional features
|
|
282
|
+
];
|
|
283
|
+
fs.writeFileSync(path.join(tmpDir, '.env'), oldEnv, 'utf8');
|
|
284
|
+
process.chdir(tmpDir);
|
|
285
|
+
vi.mocked(createInterface).mockReturnValue(makeReadline(answers));
|
|
286
|
+
vi.mocked(execFileSync).mockImplementation(() => {
|
|
287
|
+
throw new Error('binary not found');
|
|
288
|
+
});
|
|
289
|
+
vi.mocked(ensureWorkspaceBootstrapFiles).mockResolvedValue([]);
|
|
290
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
291
|
+
try {
|
|
292
|
+
await runInitWizard();
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
process.chdir(previousCwd);
|
|
296
|
+
}
|
|
297
|
+
const newEnv = fs.readFileSync(path.join(tmpDir, '.env'), 'utf8');
|
|
298
|
+
expect(newEnv).toContain('# AUTO-DETECTED');
|
|
299
|
+
expect(newEnv).toContain('DISCOCLAW_TASKS_FORUM=9000000000000000001');
|
|
300
|
+
expect(newEnv).toContain('DISCOCLAW_CRON_FORUM=9000000000000000002');
|
|
301
|
+
// Must appear under AUTO-DETECTED, not REQUIRED
|
|
302
|
+
const autoDetectedIdx = newEnv.indexOf('# AUTO-DETECTED');
|
|
303
|
+
const tasksIdx = newEnv.indexOf('DISCOCLAW_TASKS_FORUM=');
|
|
304
|
+
expect(tasksIdx).toBeGreaterThan(autoDetectedIdx);
|
|
305
|
+
});
|
|
306
|
+
it('uses a custom install directory when a path is provided', async () => {
|
|
307
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
|
|
308
|
+
const answers = [
|
|
309
|
+
tmpDir, // install directory (custom path)
|
|
310
|
+
'', // Press Enter to continue
|
|
311
|
+
'', // data directory (default)
|
|
312
|
+
'a.b.c', // DISCORD_TOKEN
|
|
313
|
+
'1000000000000000001', // DISCORD_ALLOW_USER_IDS
|
|
314
|
+
'', // provider selection -> default (Claude)
|
|
315
|
+
'', // enable skip permissions
|
|
316
|
+
'', // enable stream-json
|
|
317
|
+
'n', // configure recommended settings
|
|
318
|
+
'n', // configure optional features
|
|
319
|
+
];
|
|
320
|
+
vi.mocked(createInterface).mockReturnValue(makeReadline(answers));
|
|
321
|
+
vi.mocked(execFileSync).mockImplementation(() => {
|
|
322
|
+
throw new Error('binary not found');
|
|
323
|
+
});
|
|
324
|
+
vi.mocked(ensureWorkspaceBootstrapFiles).mockResolvedValue([]);
|
|
325
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
326
|
+
await runInitWizard();
|
|
327
|
+
expect(fs.existsSync(path.join(tmpDir, '.env'))).toBe(true);
|
|
328
|
+
expect(ensureWorkspaceBootstrapFiles).toHaveBeenCalledWith(path.join(tmpDir, 'workspace'));
|
|
329
|
+
});
|
|
330
|
+
it('creates the install directory if it does not exist', async () => {
|
|
331
|
+
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'discoclaw-init-test-'));
|
|
332
|
+
const newDir = path.join(baseDir, 'new-subdir');
|
|
333
|
+
const answers = [
|
|
334
|
+
newDir, // install directory (non-existent path)
|
|
335
|
+
'', // Press Enter to continue
|
|
336
|
+
'', // data directory (default)
|
|
337
|
+
'a.b.c', // DISCORD_TOKEN
|
|
338
|
+
'1000000000000000001', // DISCORD_ALLOW_USER_IDS
|
|
339
|
+
'', // provider selection -> default (Claude)
|
|
340
|
+
'', // enable skip permissions
|
|
341
|
+
'', // enable stream-json
|
|
342
|
+
'n', // configure recommended settings
|
|
343
|
+
'n', // configure optional features
|
|
344
|
+
];
|
|
345
|
+
vi.mocked(createInterface).mockReturnValue(makeReadline(answers));
|
|
346
|
+
vi.mocked(execFileSync).mockImplementation(() => {
|
|
347
|
+
throw new Error('binary not found');
|
|
348
|
+
});
|
|
349
|
+
vi.mocked(ensureWorkspaceBootstrapFiles).mockResolvedValue([]);
|
|
350
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
351
|
+
await runInitWizard();
|
|
352
|
+
expect(fs.existsSync(newDir)).toBe(true);
|
|
353
|
+
expect(fs.existsSync(path.join(newDir, '.env'))).toBe(true);
|
|
354
|
+
expect(ensureWorkspaceBootstrapFiles).toHaveBeenCalledWith(path.join(newDir, 'workspace'));
|
|
190
355
|
});
|
|
191
356
|
});
|