nightshift-mcp 2.2.0 → 2.3.0

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 CHANGED
@@ -37,12 +37,18 @@ claude codex
37
37
  - **Agent spawning**: Spawn Claude, Codex, Gemini, Vibe, Goose, or Ollama as subprocesses with full lifecycle tracking
38
38
  - **Autonomous mode**: Single `orchestrate` tool runs claim-implement-complete loops until all stories pass
39
39
  - **Daemon mode**: Event-driven background orchestrator with file watchers, health checks, and auto-recovery
40
+ - **Operating modes**: Sprint (build features) → Maintain (run routines) → Grow (vision re-eval) → Sprint lifecycle
41
+ - **Vision-driven auto-generation**: Define a project vision; the system automatically generates new stories when work runs low
42
+ - **Scheduled operation**: Configure time windows (e.g., 1AM-5AM) for autonomous overnight work
43
+ - **Maintenance routines**: Recurring tasks (security audits, dependency updates, test coverage) that run on schedule
40
44
  - **Delegation**: Hand off stories or research tasks to specific agents with full context injection
41
45
  - **Failover handling**: Seamless handoffs when an agent hits rate limits or context windows
42
46
  - **Runtime project switching**: Switch active project without restarting the MCP server
43
47
 
44
48
  ### Reliability
45
49
  - **Health tracker**: Per-agent failure tracking. Auto-disables failing agents, re-enables after configurable cooldown. Persisted across daemon restarts.
50
+ - **Exponential backoff**: When all agents hit rate limits, global pause with exponential backoff (5min → 10min → 20min...) instead of burning through retries
51
+ - **Dedup protection**: Completed stories are tracked per session to prevent duplicate verification and redundant commits
46
52
  - **Immutable audit trail**: Append-only SQLite table records every spawn, completion, failure, and stuck-agent kill
47
53
  - **Run records**: Structured execution history per agent run — duration, exit status, files changed, scope leak detection
48
54
  - **Budget tracking**: Per-agent cost tracking with daily/monthly limits, warning thresholds, enable/disable toggle
@@ -70,7 +76,7 @@ claude codex
70
76
  ### Platform
71
77
  - **Cross-platform**: Windows, Linux, macOS
72
78
  - **6 agent types**: Claude, Codex, Gemini, Vibe, Goose, Ollama
73
- - **52 MCP tools** across 10 categories
79
+ - **62 MCP tools** across 10 categories
74
80
  - **NightShift CLI**: `nightshift` command for agent coordination without MCP
75
81
  - **Zero external services**: Everything runs locally with SQLite + file storage
76
82
 
@@ -144,6 +150,41 @@ orchestrate()
144
150
  nightshift-daemon --verbose --max-agents 4
145
151
  ```
146
152
 
153
+ ### Scheduled overnight operation
154
+ ```
155
+ set_schedule({ start: "01:00", stop: "05:00", timezone: "America/Chicago", days: ["mon","tue","wed","thu","fri"] })
156
+ ```
157
+
158
+ ### Vision-driven (auto-generate stories)
159
+ ```json
160
+ // nightshift.config.json
161
+ {
162
+ "vision": "Your project's end-state goal goes here. Be specific about what 'done' looks like.",
163
+ "mode": "auto",
164
+ "sprint": { "autoGenerateWhenBelow": 5 },
165
+ "maintain": {
166
+ "routines": [
167
+ { "name": "security-audit", "schedule": "weekly", "agent": "gemini", "type": "audit",
168
+ "prompt": "Audit the codebase for security vulnerabilities and file bugs for findings." },
169
+ { "name": "test-coverage", "schedule": "daily", "agent": "claude", "type": "quality",
170
+ "prompt": "Find untested code paths. Write tests for the 3 most critical gaps." }
171
+ ]
172
+ },
173
+ "grow": {
174
+ "strategy": "Identify growth opportunities based on the project vision",
175
+ "cadence": "weekly",
176
+ "maxStoriesPerCycle": 5,
177
+ "agent": "gemini"
178
+ }
179
+ }
180
+ ```
181
+
182
+ When `mode` is `"auto"`, the daemon transitions through the full lifecycle:
183
+
184
+ 1. **Sprint**: Execute stories from `prd.json` until all complete. When stories run low and a vision is set, auto-generates more.
185
+ 2. **Maintain**: Run recurring routines (security, dependencies, tests). If routines file new bugs or stories, transitions back to sprint.
186
+ 3. **Grow**: Re-evaluates the vision against the codebase and generates strategic stories for the next sprint.
187
+
147
188
  ## PRD Setup
148
189
 
149
190
  Create `prd.json` in your project root:
@@ -209,7 +250,7 @@ When all stories pass and all bugs are fixed, NightShift posts `READY_TO_TEST` t
209
250
 
210
251
  ## Tools Reference
211
252
 
212
- NightShift provides 52 tools across 10 categories. Key tools listed below — use `nightshift_setup(showExamples: true)` or `nightshift_help` for full documentation.
253
+ NightShift provides 62 tools across 10 categories. Key tools listed below — use `nightshift_setup(showExamples: true)` or `nightshift_help` for full documentation.
213
254
 
214
255
  ### Communication
215
256
  | Tool | Description |
@@ -228,6 +269,8 @@ NightShift provides 52 tools across 10 categories. Key tools listed below — us
228
269
  | `get_next_story` | Get highest priority incomplete story |
229
270
  | `claim_story` | Claim a story and notify via chat |
230
271
  | `complete_story` | Mark done, log progress, create savepoint |
272
+ | `generate_stories` | Generate new stories from project vision |
273
+ | `approve_stories` | Approve auto-generated stories for execution |
231
274
  | `read_bugs` / `claim_bug` / `mark_bug_fixed` | Bug lifecycle |
232
275
 
233
276
  ### Agent Spawning
@@ -250,6 +293,18 @@ NightShift provides 52 tools across 10 categories. Key tools listed below — us
250
293
  | `get_pipeline` | View configured pipelines and tag routing |
251
294
  | `set_pipeline` | Create/update agent pipelines at runtime |
252
295
 
296
+ ### Schedule & Modes
297
+ | Tool | Description |
298
+ |------|-------------|
299
+ | `set_schedule` | Set daily start/stop time window for the daemon |
300
+ | `get_schedule` | View current schedule |
301
+ | `remove_schedule` | Remove schedule |
302
+ | `set_mode` | Set operating mode (sprint/maintain/grow/auto) |
303
+ | `get_mode` | View mode and project lifecycle state |
304
+ | `list_routines` | View maintenance routines and their status |
305
+ | `add_routine` | Add a maintenance routine (built-in or custom) |
306
+ | `reset_routine` | Re-enable a circuit-broken routine |
307
+
253
308
  ### Workflow
254
309
  | Tool | Description |
255
310
  |------|-------------|
@@ -294,10 +349,14 @@ nightshift-daemon [options]
294
349
  **What it does:**
295
350
  - Watches `prd.json` and `chat.txt` for changes
296
351
  - Auto-spawns agents for unassigned stories
297
- - Health tracker disables failing agents, re-enables after cooldown
352
+ - Deduplicates won't re-verify stories already completed this session
353
+ - Health tracker disables failing agents, re-enables after 5-minute cooldown
354
+ - Exponential backoff when all agents are rate-limited (5min → 10min → 20min, max ~60min)
298
355
  - Detects and kills stuck agents
299
356
  - Claims failover requests
300
357
  - Quarantines stories that fail repeatedly
358
+ - Runs maintenance routines in maintain mode
359
+ - Auto-generates stories from vision when work runs low
301
360
  - Logs everything to the SQLite audit trail
302
361
 
303
362
  ## Local Models
@@ -346,6 +405,9 @@ NightShift uses a hybrid storage approach:
346
405
  | **Health tracker** | `.robot-chat/nightshift.db` | SQLite |
347
406
  | **Run log** | `.robot-chat/runs.jsonl` | JSONL (append-only) |
348
407
  | **Pipeline config** | `.robot-chat/pipeline-override.json` | JSON (runtime) |
408
+ | **Routine state** | `.robot-chat/routine-state.json` | JSON |
409
+ | **Routine log** | `.robot-chat/routine-runs.jsonl` | JSONL (append-only) |
410
+ | **Project config** | `nightshift.config.json` | JSON (vision, mode, schedule, routines) |
349
411
 
350
412
  Add `.robot-chat/` to your `.gitignore` — coordination state is ephemeral.
351
413
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * NightShift Configuration
3
+ *
4
+ * Unified config loading for nightshift.config.json.
5
+ * All modules read from here instead of parsing the config file independently.
6
+ */
7
+ import type { AgentType } from "./agent-spawner.js";
8
+ export type OperatingMode = "sprint" | "maintain" | "grow" | "auto";
9
+ export type RoutineSchedule = "hourly" | "daily" | "weekly" | "monthly";
10
+ export type RoutineType = "audit" | "maintenance" | "quality" | "optimization" | "monitoring" | "custom";
11
+ export interface RoutineConfig {
12
+ /** Unique name for this routine */
13
+ name: string;
14
+ /** How often to run */
15
+ schedule: RoutineSchedule;
16
+ /** Agent to execute the routine */
17
+ agent: AgentType;
18
+ /** Prompt describing what the routine should do */
19
+ prompt: string;
20
+ /** Category of routine */
21
+ type: RoutineType;
22
+ /** Whether this routine is enabled (default: true) */
23
+ enabled?: boolean;
24
+ /** Timeout in seconds (default: 600) */
25
+ timeout?: number;
26
+ }
27
+ export interface ScheduleConfig {
28
+ /** Time to start the daemon (HH:MM, 24h format) */
29
+ start: string;
30
+ /** Time to stop the daemon (HH:MM, 24h format) */
31
+ stop: string;
32
+ /** Timezone (IANA format, e.g. "America/Chicago"). Default: system timezone */
33
+ timezone?: string;
34
+ /** Days of the week to run. Default: every day */
35
+ days?: ("mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun")[];
36
+ }
37
+ export interface SprintConfig {
38
+ /** Generate new stories when incomplete count drops below this (default: 5) */
39
+ autoGenerateWhenBelow: number;
40
+ /** Agent to use for story generation (default: "gemini") */
41
+ taskGenerationAgent: AgentType;
42
+ /** Max stories to generate per cycle (default: 10) */
43
+ maxAutoGenerated: number;
44
+ /** Require human approval before generated stories are activated (default: false) */
45
+ requireApproval: boolean;
46
+ }
47
+ export interface MaintainConfig {
48
+ /** Recurring maintenance routines */
49
+ routines: RoutineConfig[];
50
+ }
51
+ export interface GrowConfig {
52
+ /** Strategic growth evaluation prompt */
53
+ strategy: string;
54
+ /** How often to run growth evaluation */
55
+ cadence: RoutineSchedule;
56
+ /** Max stories to generate per growth cycle (default: 5) */
57
+ maxStoriesPerCycle: number;
58
+ /** Agent for growth evaluation (default: "gemini") */
59
+ agent: AgentType;
60
+ }
61
+ export interface NightShiftConfig {
62
+ /** Project vision / end-state goal */
63
+ vision?: string;
64
+ /** Operating mode */
65
+ mode: OperatingMode;
66
+ /** Sprint mode settings */
67
+ sprint: SprintConfig;
68
+ /** Maintain mode settings */
69
+ maintain: MaintainConfig;
70
+ /** Grow mode settings */
71
+ grow?: GrowConfig;
72
+ /** Daemon schedule (time window) */
73
+ schedule?: ScheduleConfig;
74
+ }
75
+ /**
76
+ * Load the full NightShift config from nightshift.config.json.
77
+ * Returns defaults merged with file config.
78
+ */
79
+ export declare function loadConfig(projectPath: string): NightShiftConfig;
80
+ /**
81
+ * Save config to nightshift.config.json.
82
+ * Preserves any extra fields the user has (pipeline config, etc).
83
+ */
84
+ export declare function saveConfig(projectPath: string, updates: Partial<NightShiftConfig>): void;
85
+ /**
86
+ * Get the list of default routines that users can opt into.
87
+ */
88
+ export declare function getDefaultRoutines(): RoutineConfig[];
89
+ /**
90
+ * Parse a time string like "01:00" into { hour, minute }.
91
+ */
92
+ export declare function parseTime(time: string): {
93
+ hour: number;
94
+ minute: number;
95
+ } | null;
96
+ /**
97
+ * Convert day names to cron day-of-week numbers (0=Sun, 1=Mon, ..., 6=Sat).
98
+ */
99
+ export declare function daysToCron(days?: ScheduleConfig["days"]): string;
100
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * NightShift Configuration
3
+ *
4
+ * Unified config loading for nightshift.config.json.
5
+ * All modules read from here instead of parsing the config file independently.
6
+ */
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ // ============================================
10
+ // Defaults
11
+ // ============================================
12
+ const DEFAULT_ROUTINES = [
13
+ {
14
+ name: "security-audit",
15
+ schedule: "weekly",
16
+ agent: "gemini",
17
+ prompt: `Audit the codebase for security vulnerabilities:
18
+ 1. Check for OWASP top 10 issues (injection, XSS, auth flaws, etc.)
19
+ 2. Review dependency versions for known CVEs (check package.json / requirements.txt / go.mod)
20
+ 3. Look for hardcoded secrets, credentials, or API keys
21
+ 4. Check file permissions and access controls
22
+
23
+ For each finding, use the nightshift CLI to file a bug:
24
+ nightshift bug-claim BUG-XXX (or create a new one if needed)
25
+
26
+ Output a summary of findings and any bugs filed.`,
27
+ type: "audit",
28
+ timeout: 600,
29
+ },
30
+ {
31
+ name: "dependency-updates",
32
+ schedule: "weekly",
33
+ agent: "codex",
34
+ prompt: `Check for outdated dependencies and update safe ones:
35
+ 1. Run the appropriate update check command (npm outdated, pip list --outdated, etc.)
36
+ 2. Update patch and minor versions that are backward-compatible
37
+ 3. Run tests after updates to verify nothing breaks
38
+ 4. For major version bumps, do NOT update — instead file a story via nightshift CLI
39
+
40
+ Commit any successful updates. Report what was updated and what needs manual attention.`,
41
+ type: "maintenance",
42
+ timeout: 600,
43
+ },
44
+ {
45
+ name: "test-coverage",
46
+ schedule: "daily",
47
+ agent: "claude",
48
+ prompt: `Identify gaps in test coverage and write tests:
49
+ 1. Look for untested files, functions, and code paths
50
+ 2. Focus on critical business logic and error handling paths
51
+ 3. Write 3-5 new tests for the most important gaps
52
+ 4. Run the test suite to verify all tests pass
53
+
54
+ Use nightshift CLI to report progress. Commit passing tests.`,
55
+ type: "quality",
56
+ timeout: 600,
57
+ },
58
+ ];
59
+ function getDefaultConfig() {
60
+ return {
61
+ mode: "auto",
62
+ sprint: {
63
+ autoGenerateWhenBelow: 5,
64
+ taskGenerationAgent: "gemini",
65
+ maxAutoGenerated: 10,
66
+ requireApproval: false,
67
+ },
68
+ maintain: {
69
+ routines: [], // Empty by default — user opts in
70
+ },
71
+ };
72
+ }
73
+ // ============================================
74
+ // Config Loading
75
+ // ============================================
76
+ const CONFIG_FILENAME = "nightshift.config.json";
77
+ /**
78
+ * Load the full NightShift config from nightshift.config.json.
79
+ * Returns defaults merged with file config.
80
+ */
81
+ export function loadConfig(projectPath) {
82
+ const config = getDefaultConfig();
83
+ const configFile = path.join(projectPath, CONFIG_FILENAME);
84
+ if (!fs.existsSync(configFile))
85
+ return config;
86
+ try {
87
+ const raw = JSON.parse(fs.readFileSync(configFile, "utf-8"));
88
+ // Vision
89
+ if (typeof raw.vision === "string") {
90
+ config.vision = raw.vision;
91
+ }
92
+ // Mode
93
+ if (raw.mode && ["sprint", "maintain", "grow", "auto"].includes(raw.mode)) {
94
+ config.mode = raw.mode;
95
+ }
96
+ // Sprint config
97
+ if (raw.sprint && typeof raw.sprint === "object") {
98
+ Object.assign(config.sprint, raw.sprint);
99
+ }
100
+ // Legacy flat fields (from vision.ts v1)
101
+ if (raw.autoGenerateWhenBelow !== undefined) {
102
+ config.sprint.autoGenerateWhenBelow = raw.autoGenerateWhenBelow;
103
+ }
104
+ if (raw.taskGenerationAgent !== undefined) {
105
+ config.sprint.taskGenerationAgent = raw.taskGenerationAgent;
106
+ }
107
+ if (raw.maxAutoGenerated !== undefined) {
108
+ config.sprint.maxAutoGenerated = raw.maxAutoGenerated;
109
+ }
110
+ if (raw.requireApproval !== undefined) {
111
+ config.sprint.requireApproval = raw.requireApproval;
112
+ }
113
+ // Maintain config
114
+ if (raw.maintain && typeof raw.maintain === "object") {
115
+ if (Array.isArray(raw.maintain.routines)) {
116
+ config.maintain.routines = raw.maintain.routines;
117
+ }
118
+ }
119
+ // Grow config
120
+ if (raw.grow && typeof raw.grow === "object") {
121
+ config.grow = {
122
+ strategy: raw.grow.strategy || "Identify growth opportunities based on the project vision.",
123
+ cadence: raw.grow.cadence || "weekly",
124
+ maxStoriesPerCycle: raw.grow.maxStoriesPerCycle ?? 5,
125
+ agent: raw.grow.agent || "gemini",
126
+ };
127
+ }
128
+ // Schedule
129
+ if (raw.schedule && typeof raw.schedule === "object") {
130
+ if (raw.schedule.start && raw.schedule.stop) {
131
+ config.schedule = {
132
+ start: raw.schedule.start,
133
+ stop: raw.schedule.stop,
134
+ timezone: raw.schedule.timezone,
135
+ days: raw.schedule.days,
136
+ };
137
+ }
138
+ }
139
+ }
140
+ catch {
141
+ // Bad config file — use defaults
142
+ }
143
+ return config;
144
+ }
145
+ /**
146
+ * Save config to nightshift.config.json.
147
+ * Preserves any extra fields the user has (pipeline config, etc).
148
+ */
149
+ export function saveConfig(projectPath, updates) {
150
+ const configFile = path.join(projectPath, CONFIG_FILENAME);
151
+ let existing = {};
152
+ if (fs.existsSync(configFile)) {
153
+ try {
154
+ existing = JSON.parse(fs.readFileSync(configFile, "utf-8"));
155
+ }
156
+ catch {
157
+ // Bad file — overwrite
158
+ }
159
+ }
160
+ // Deep-merge updates into existing (preserves sibling fields within objects)
161
+ if (updates.vision !== undefined)
162
+ existing.vision = updates.vision;
163
+ if (updates.mode !== undefined)
164
+ existing.mode = updates.mode;
165
+ if (updates.sprint !== undefined) {
166
+ existing.sprint = { ...(existing.sprint ?? {}), ...updates.sprint };
167
+ }
168
+ if (updates.maintain !== undefined) {
169
+ existing.maintain = { ...(existing.maintain ?? {}), ...updates.maintain };
170
+ }
171
+ if (updates.grow !== undefined) {
172
+ existing.grow = { ...(existing.grow ?? {}), ...updates.grow };
173
+ }
174
+ if (updates.schedule !== undefined)
175
+ existing.schedule = updates.schedule;
176
+ fs.writeFileSync(configFile, JSON.stringify(existing, null, 2), "utf-8");
177
+ }
178
+ /**
179
+ * Get the list of default routines that users can opt into.
180
+ */
181
+ export function getDefaultRoutines() {
182
+ return JSON.parse(JSON.stringify(DEFAULT_ROUTINES));
183
+ }
184
+ /**
185
+ * Parse a time string like "01:00" into { hour, minute }.
186
+ */
187
+ export function parseTime(time) {
188
+ const match = time.match(/^(\d{1,2}):(\d{2})$/);
189
+ if (!match)
190
+ return null;
191
+ const hour = parseInt(match[1], 10);
192
+ const minute = parseInt(match[2], 10);
193
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
194
+ return null;
195
+ return { hour, minute };
196
+ }
197
+ /**
198
+ * Convert day names to cron day-of-week numbers (0=Sun, 1=Mon, ..., 6=Sat).
199
+ */
200
+ export function daysToCron(days) {
201
+ if (!days || days.length === 0 || days.length === 7)
202
+ return "*";
203
+ const dayMap = {
204
+ sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6,
205
+ };
206
+ return days.map(d => dayMap[d]).sort().join(",");
207
+ }
208
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@
4
4
  * Utilities for checking daemon status and starting the daemon
5
5
  * as a detached background process.
6
6
  */
7
+ import { type ScheduleConfig } from "./config.js";
7
8
  export interface DaemonStatus {
8
9
  running: boolean;
9
10
  pid?: number;
@@ -74,4 +75,27 @@ export declare function removeWatchdog(projectPath: string): {
74
75
  success: boolean;
75
76
  error?: string;
76
77
  };
78
+ /**
79
+ * Install start/stop cron entries for time-window daemon operation.
80
+ *
81
+ * Example: start at 01:00, stop at 05:00 on weekdays
82
+ * - Creates a cron entry that starts the daemon at the start time
83
+ * - Creates a cron entry that stops the daemon at the stop time
84
+ */
85
+ export declare function installSchedule(projectPath: string, schedule: ScheduleConfig): {
86
+ success: boolean;
87
+ error?: string;
88
+ details?: string;
89
+ };
90
+ /**
91
+ * Remove scheduled start/stop cron entries for a project.
92
+ */
93
+ export declare function removeSchedule(projectPath: string): {
94
+ success: boolean;
95
+ error?: string;
96
+ };
97
+ /**
98
+ * Get the current schedule for a project.
99
+ */
100
+ export declare function getSchedule(projectPath: string): ScheduleConfig | null;
77
101
  //# sourceMappingURL=daemon-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-manager.d.ts","sourceRoot":"","sources":["../src/daemon-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CAgDjE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAY7D;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+LzF;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA6CpF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAM,GACpD,OAAO,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAgCD;AAqDD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAsDzF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAoCxF"}
1
+ {"version":3,"file":"daemon-manager.d.ts","sourceRoot":"","sources":["../src/daemon-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAiD,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjG,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CAgDjE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAY7D;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+LzF;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA6CpF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAM,GACpD,OAAO,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAgCD;AAqDD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAsDzF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAoCxF;AAeD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,cAAc,GACvB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA2FxD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAwCxF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAGtE"}
@@ -9,6 +9,7 @@ import * as path from "path";
9
9
  import { spawnSync, execSync } from "child_process";
10
10
  import { fileURLToPath } from "url";
11
11
  import { isProcessRunning, isWindows, killProcess } from "./platform.js";
12
+ import { loadConfig, saveConfig, parseTime, daysToCron } from "./config.js";
12
13
  /**
13
14
  * Get the daemon lock file path for a project
14
15
  */
@@ -478,4 +479,145 @@ export function removeWatchdog(projectPath) {
478
479
  };
479
480
  }
480
481
  }
482
+ // ============================================
483
+ // Daemon Schedule (Time Window)
484
+ // ============================================
485
+ const SCHEDULE_START_TAG = "nightshift-start";
486
+ const SCHEDULE_STOP_TAG = "nightshift-stop";
487
+ function getScheduleTag(projectPath, type) {
488
+ const hash = projectPath.replace(/[^a-zA-Z0-9]/g, "").slice(-20);
489
+ const base = type === "start" ? SCHEDULE_START_TAG : SCHEDULE_STOP_TAG;
490
+ return `${base}-${hash}`;
491
+ }
492
+ /**
493
+ * Install start/stop cron entries for time-window daemon operation.
494
+ *
495
+ * Example: start at 01:00, stop at 05:00 on weekdays
496
+ * - Creates a cron entry that starts the daemon at the start time
497
+ * - Creates a cron entry that stops the daemon at the stop time
498
+ */
499
+ export function installSchedule(projectPath, schedule) {
500
+ const startTime = parseTime(schedule.start);
501
+ const stopTime = parseTime(schedule.stop);
502
+ if (!startTime || !stopTime) {
503
+ return { success: false, error: `Invalid time format. Use HH:MM (24h), got start="${schedule.start}" stop="${schedule.stop}"` };
504
+ }
505
+ const startTag = getScheduleTag(projectPath, "start");
506
+ const stopTag = getScheduleTag(projectPath, "stop");
507
+ const daemonJs = path.join(path.dirname(fileURLToPath(import.meta.url)), "daemon.js").replace(/\\/g, "/");
508
+ const daysCron = daysToCron(schedule.days);
509
+ try {
510
+ if (isWindows) {
511
+ // Windows: schtasks for start
512
+ const startCmd = `powershell -NoProfile -Command "Start-Process node -ArgumentList '${daemonJs}','--verbose' -WindowStyle Hidden -WorkingDirectory '${projectPath.replace(/\\/g, "/")}'"`;
513
+ const stopCmd = `powershell -NoProfile -Command "` +
514
+ `$lock='${path.join(projectPath, ".robot-chat", "daemon.lock").replace(/\\/g, "/")}'; ` +
515
+ `if (Test-Path $lock) { ` +
516
+ `$pid = Get-Content $lock; ` +
517
+ `Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue; ` +
518
+ `Remove-Item $lock -Force -ErrorAction SilentlyContinue ` +
519
+ `}"`;
520
+ // Remove existing tasks
521
+ for (const tag of [startTag, stopTag]) {
522
+ try {
523
+ execSync(`schtasks /Delete /TN "${tag}" /F`, { stdio: "ignore", windowsHide: true });
524
+ }
525
+ catch { /* ok */ }
526
+ }
527
+ // Create start task
528
+ execSync(`schtasks /Create /TN "${startTag}" /SC DAILY /ST ${schedule.start} /TR "${startCmd}" /F`, { stdio: "ignore", windowsHide: true });
529
+ // Create stop task
530
+ execSync(`schtasks /Create /TN "${stopTag}" /SC DAILY /ST ${schedule.stop} /TR "${stopCmd}" /F`, { stdio: "ignore", windowsHide: true });
531
+ }
532
+ else {
533
+ // Unix: crontab entries
534
+ let existingCrontab = "";
535
+ try {
536
+ existingCrontab = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
537
+ }
538
+ catch { /* no crontab */ }
539
+ // Remove existing schedule entries for this project
540
+ const lines = existingCrontab.split("\n").filter(line => !line.includes(startTag) && !line.includes(stopTag));
541
+ // Build TZ prefix if timezone specified
542
+ const tzPrefix = schedule.timezone ? `TZ=${schedule.timezone} ` : "";
543
+ // Add start cron: at start time, launch daemon
544
+ const startCron = `${tzPrefix}${startTime.minute} ${startTime.hour} * * ${daysCron} ROBOT_CHAT_PROJECT_PATH="${projectPath}" node "${daemonJs}" --verbose >> "${path.join(projectPath, ".robot-chat", "daemon-schedule.log").replace(/\\/g, "/")}" 2>&1 & # ${startTag}`;
545
+ // Add stop cron: at stop time, kill daemon
546
+ const lockFile = path.join(projectPath, ".robot-chat", "daemon.lock").replace(/\\/g, "/");
547
+ const stopCron = `${tzPrefix}${stopTime.minute} ${stopTime.hour} * * ${daysCron} if [ -f "${lockFile}" ]; then kill $(cat "${lockFile}") 2>/dev/null; rm -f "${lockFile}"; fi # ${stopTag}`;
548
+ lines.push(startCron);
549
+ lines.push(stopCron);
550
+ const newCrontab = lines.filter(l => l.trim()).join("\n") + "\n";
551
+ execSync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`, {
552
+ encoding: "utf-8",
553
+ stdio: ["pipe", "ignore", "ignore"],
554
+ });
555
+ }
556
+ // Save schedule to config
557
+ saveConfig(projectPath, { schedule });
558
+ return {
559
+ success: true,
560
+ details: `Daemon will start at ${schedule.start} and stop at ${schedule.stop}` +
561
+ (schedule.days ? ` on ${schedule.days.join(", ")}` : " daily") +
562
+ (schedule.timezone ? ` (${schedule.timezone})` : ""),
563
+ };
564
+ }
565
+ catch (err) {
566
+ return {
567
+ success: false,
568
+ error: `Failed to install schedule: ${err instanceof Error ? err.message : String(err)}`,
569
+ };
570
+ }
571
+ }
572
+ /**
573
+ * Remove scheduled start/stop cron entries for a project.
574
+ */
575
+ export function removeSchedule(projectPath) {
576
+ const startTag = getScheduleTag(projectPath, "start");
577
+ const stopTag = getScheduleTag(projectPath, "stop");
578
+ try {
579
+ if (isWindows) {
580
+ for (const tag of [startTag, stopTag]) {
581
+ try {
582
+ execSync(`schtasks /Delete /TN "${tag}" /F`, { stdio: "ignore", windowsHide: true });
583
+ }
584
+ catch { /* ok */ }
585
+ }
586
+ }
587
+ else {
588
+ let existingCrontab = "";
589
+ try {
590
+ existingCrontab = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
591
+ }
592
+ catch {
593
+ return { success: true };
594
+ }
595
+ const lines = existingCrontab.split("\n").filter(line => !line.includes(startTag) && !line.includes(stopTag));
596
+ const newCrontab = lines.filter(l => l.trim()).join("\n") + "\n";
597
+ execSync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`, {
598
+ encoding: "utf-8",
599
+ stdio: ["pipe", "ignore", "ignore"],
600
+ });
601
+ }
602
+ // Remove schedule from config
603
+ const config = loadConfig(projectPath);
604
+ if (config.schedule) {
605
+ saveConfig(projectPath, { schedule: undefined });
606
+ }
607
+ return { success: true };
608
+ }
609
+ catch (err) {
610
+ return {
611
+ success: false,
612
+ error: `Failed to remove schedule: ${err instanceof Error ? err.message : String(err)}`,
613
+ };
614
+ }
615
+ }
616
+ /**
617
+ * Get the current schedule for a project.
618
+ */
619
+ export function getSchedule(projectPath) {
620
+ const config = loadConfig(projectPath);
621
+ return config.schedule ?? null;
622
+ }
481
623
  //# sourceMappingURL=daemon-manager.js.map