@oss-autopilot/core 0.45.0 → 0.46.1

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.
@@ -4,7 +4,7 @@
4
4
  * and detecting whether a server is already running.
5
5
  */
6
6
  import { spawn } from 'child_process';
7
- import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo, removeDashboardServerInfo, } from './dashboard-server.js';
7
+ import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo, removeDashboardServerInfo, } from './dashboard-process.js';
8
8
  import { resolveAssetsDir } from './dashboard.js';
9
9
  import { getCLIVersion } from '../core/index.js';
10
10
  const DEFAULT_PORT = 3000;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Dashboard server process management.
3
+ * PID file operations, health probes, and running server detection.
4
+ */
5
+ export interface DashboardServerInfo {
6
+ pid: number;
7
+ port: number;
8
+ startedAt: string;
9
+ version?: string;
10
+ }
11
+ export declare function getDashboardPidPath(): string;
12
+ export declare function writeDashboardServerInfo(info: DashboardServerInfo): void;
13
+ export declare function readDashboardServerInfo(): DashboardServerInfo | null;
14
+ export declare function removeDashboardServerInfo(): void;
15
+ export declare function isDashboardServerRunning(port: number): Promise<boolean>;
16
+ export declare function findRunningDashboardServer(): Promise<{
17
+ port: number;
18
+ url: string;
19
+ } | null>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Dashboard server process management.
3
+ * PID file operations, health probes, and running server detection.
4
+ */
5
+ import * as http from 'http';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { getDataDir } from '../core/index.js';
9
+ import { warn } from '../core/logger.js';
10
+ const MODULE = 'dashboard-server';
11
+ // ── PID File Management ────────────────────────────────────────────────────────
12
+ export function getDashboardPidPath() {
13
+ return path.join(getDataDir(), 'dashboard-server.pid');
14
+ }
15
+ export function writeDashboardServerInfo(info) {
16
+ fs.writeFileSync(getDashboardPidPath(), JSON.stringify(info), { mode: 0o600 });
17
+ }
18
+ export function readDashboardServerInfo() {
19
+ try {
20
+ const content = fs.readFileSync(getDashboardPidPath(), 'utf-8');
21
+ const parsed = JSON.parse(content);
22
+ if (typeof parsed !== 'object' ||
23
+ parsed === null ||
24
+ typeof parsed.pid !== 'number' ||
25
+ !Number.isInteger(parsed.pid) ||
26
+ parsed.pid <= 0 ||
27
+ typeof parsed.port !== 'number' ||
28
+ typeof parsed.startedAt !== 'string') {
29
+ warn(MODULE, 'PID file has invalid structure, ignoring');
30
+ return null;
31
+ }
32
+ return parsed;
33
+ }
34
+ catch (err) {
35
+ const code = err.code;
36
+ if (code !== 'ENOENT') {
37
+ warn(MODULE, `Failed to read PID file: ${err.message}`);
38
+ }
39
+ return null;
40
+ }
41
+ }
42
+ export function removeDashboardServerInfo() {
43
+ try {
44
+ fs.unlinkSync(getDashboardPidPath());
45
+ }
46
+ catch (err) {
47
+ const code = err.code;
48
+ if (code !== 'ENOENT') {
49
+ warn(MODULE, `Failed to remove PID file: ${err.message}`);
50
+ }
51
+ }
52
+ }
53
+ // ── Health Probe ───────────────────────────────────────────────────────────────
54
+ export function isDashboardServerRunning(port) {
55
+ return new Promise((resolve) => {
56
+ const req = http.get(`http://127.0.0.1:${port}/api/data`, { timeout: 2000 }, (res) => {
57
+ // Consume response data to free up memory
58
+ res.resume();
59
+ resolve(res.statusCode === 200);
60
+ });
61
+ req.on('error', () => resolve(false));
62
+ req.on('timeout', () => {
63
+ req.destroy();
64
+ resolve(false);
65
+ });
66
+ });
67
+ }
68
+ export async function findRunningDashboardServer() {
69
+ const info = readDashboardServerInfo();
70
+ if (!info)
71
+ return null;
72
+ // Check if process is alive (signal 0 = existence check only)
73
+ try {
74
+ process.kill(info.pid, 0);
75
+ }
76
+ catch (err) {
77
+ const code = err.code;
78
+ if (code !== 'ESRCH' && code !== 'EPERM') {
79
+ warn(MODULE, `Unexpected error checking PID ${info.pid}: ${err.message}`);
80
+ }
81
+ // ESRCH = no process at that PID; EPERM = PID recycled to another user's process
82
+ // Either way, our dashboard server is no longer running — clean up stale PID file
83
+ removeDashboardServerInfo();
84
+ return null;
85
+ }
86
+ // Process exists — verify it's actually our server via HTTP probe
87
+ if (await isDashboardServerRunning(info.port)) {
88
+ return { port: info.port, url: `http://localhost:${info.port}` };
89
+ }
90
+ // Process exists but not responding on expected port — stale
91
+ removeDashboardServerInfo();
92
+ return null;
93
+ }
@@ -5,25 +5,11 @@
5
5
  *
6
6
  * Uses Node's built-in http module — no Express/Fastify.
7
7
  */
8
+ export { getDashboardPidPath, writeDashboardServerInfo, readDashboardServerInfo, removeDashboardServerInfo, isDashboardServerRunning, findRunningDashboardServer, type DashboardServerInfo, } from './dashboard-process.js';
8
9
  export interface DashboardServerOptions {
9
10
  port: number;
10
11
  assetsDir: string;
11
12
  token: string | null;
12
13
  open: boolean;
13
14
  }
14
- export interface DashboardServerInfo {
15
- pid: number;
16
- port: number;
17
- startedAt: string;
18
- version?: string;
19
- }
20
- export declare function getDashboardPidPath(): string;
21
- export declare function writeDashboardServerInfo(info: DashboardServerInfo): void;
22
- export declare function readDashboardServerInfo(): DashboardServerInfo | null;
23
- export declare function removeDashboardServerInfo(): void;
24
- export declare function isDashboardServerRunning(port: number): Promise<boolean>;
25
- export declare function findRunningDashboardServer(): Promise<{
26
- port: number;
27
- url: string;
28
- } | null>;
29
15
  export declare function startDashboardServer(options: DashboardServerOptions): Promise<void>;
@@ -8,12 +8,16 @@
8
8
  import * as http from 'http';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { getStateManager, getGitHubToken, getDataDir, getCLIVersion } from '../core/index.js';
11
+ import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
12
12
  import { errorMessage, ValidationError } from '../core/errors.js';
13
13
  import { warn } from '../core/logger.js';
14
14
  import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
15
15
  import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
16
16
  import { openInBrowser } from './startup.js';
17
+ import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
18
+ import { RateLimiter } from './rate-limiter.js';
19
+ // Re-export process management functions for backward compatibility
20
+ export { getDashboardPidPath, writeDashboardServerInfo, readDashboardServerInfo, removeDashboardServerInfo, isDashboardServerRunning, findRunningDashboardServer, } from './dashboard-process.js';
17
21
  // ── Constants ────────────────────────────────────────────────────────────────
18
22
  const VALID_ACTIONS = new Set(['shelve', 'unshelve', 'override_status']);
19
23
  const MODULE = 'dashboard-server';
@@ -62,89 +66,6 @@ function applyStatusOverrides(prs, state) {
62
66
  }
63
67
  return result;
64
68
  }
65
- // ── PID File Management ──────────────────────────────────────────────────────
66
- export function getDashboardPidPath() {
67
- return path.join(getDataDir(), 'dashboard-server.pid');
68
- }
69
- export function writeDashboardServerInfo(info) {
70
- fs.writeFileSync(getDashboardPidPath(), JSON.stringify(info), { mode: 0o600 });
71
- }
72
- export function readDashboardServerInfo() {
73
- try {
74
- const content = fs.readFileSync(getDashboardPidPath(), 'utf-8');
75
- const parsed = JSON.parse(content);
76
- if (typeof parsed !== 'object' ||
77
- parsed === null ||
78
- typeof parsed.pid !== 'number' ||
79
- !Number.isInteger(parsed.pid) ||
80
- parsed.pid <= 0 ||
81
- typeof parsed.port !== 'number' ||
82
- typeof parsed.startedAt !== 'string') {
83
- warn(MODULE, 'PID file has invalid structure, ignoring');
84
- return null;
85
- }
86
- return parsed;
87
- }
88
- catch (err) {
89
- const code = err.code;
90
- if (code !== 'ENOENT') {
91
- warn(MODULE, `Failed to read PID file: ${err.message}`);
92
- }
93
- return null;
94
- }
95
- }
96
- export function removeDashboardServerInfo() {
97
- try {
98
- fs.unlinkSync(getDashboardPidPath());
99
- }
100
- catch (err) {
101
- const code = err.code;
102
- if (code !== 'ENOENT') {
103
- warn(MODULE, `Failed to remove PID file: ${err.message}`);
104
- }
105
- }
106
- }
107
- // ── Health Probe ─────────────────────────────────────────────────────────────
108
- export function isDashboardServerRunning(port) {
109
- return new Promise((resolve) => {
110
- const req = http.get(`http://127.0.0.1:${port}/api/data`, { timeout: 2000 }, (res) => {
111
- // Consume response data to free up memory
112
- res.resume();
113
- resolve(res.statusCode === 200);
114
- });
115
- req.on('error', () => resolve(false));
116
- req.on('timeout', () => {
117
- req.destroy();
118
- resolve(false);
119
- });
120
- });
121
- }
122
- export async function findRunningDashboardServer() {
123
- const info = readDashboardServerInfo();
124
- if (!info)
125
- return null;
126
- // Check if process is alive (signal 0 = existence check only)
127
- try {
128
- process.kill(info.pid, 0);
129
- }
130
- catch (err) {
131
- const code = err.code;
132
- if (code !== 'ESRCH' && code !== 'EPERM') {
133
- warn(MODULE, `Unexpected error checking PID ${info.pid}: ${err.message}`);
134
- }
135
- // ESRCH = no process at that PID; EPERM = PID recycled to another user's process
136
- // Either way, our dashboard server is no longer running — clean up stale PID file
137
- removeDashboardServerInfo();
138
- return null;
139
- }
140
- // Process exists — verify it's actually our server via HTTP probe
141
- if (await isDashboardServerRunning(info.port)) {
142
- return { port: info.port, url: `http://localhost:${info.port}` };
143
- }
144
- // Process exists but not responding on expected port — stale
145
- removeDashboardServerInfo();
146
- return null;
147
- }
148
69
  // ── Helpers ────────────────────────────────────────────────────────────────────
149
70
  /**
150
71
  * Build the JSON payload that the SPA expects from GET /api/data.
@@ -262,6 +183,10 @@ export async function startDashboardServer(options) {
262
183
  catch (error) {
263
184
  throw new Error(`Failed to build dashboard data: ${errorMessage(error)}. State data may be corrupted — try running: daily --json`, { cause: error });
264
185
  }
186
+ // ── Rate limiters ───────────────────────────────────────────────────────
187
+ const dataLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60_000 }); // 30/min
188
+ const actionLimiter = new RateLimiter({ maxRequests: 10, windowMs: 60_000 }); // 10/min
189
+ const refreshLimiter = new RateLimiter({ maxRequests: 2, windowMs: 60_000 }); // 2/min
265
190
  // ── Request handler ──────────────────────────────────────────────────────
266
191
  const server = http.createServer(async (req, res) => {
267
192
  const method = req.method || 'GET';
@@ -269,6 +194,12 @@ export async function startDashboardServer(options) {
269
194
  try {
270
195
  // ── API routes ─────────────────────────────────────────────────────
271
196
  if (url === '/api/data' && method === 'GET') {
197
+ const check = dataLimiter.check();
198
+ if (!check.allowed) {
199
+ res.setHeader('Retry-After', String(check.retryAfterSeconds));
200
+ sendError(res, 429, 'Too many requests');
201
+ return;
202
+ }
272
203
  sendJson(res, 200, cachedJsonData);
273
204
  return;
274
205
  }
@@ -277,6 +208,12 @@ export async function startDashboardServer(options) {
277
208
  sendError(res, 403, 'Invalid origin');
278
209
  return;
279
210
  }
211
+ const check = actionLimiter.check();
212
+ if (!check.allowed) {
213
+ res.setHeader('Retry-After', String(check.retryAfterSeconds));
214
+ sendError(res, 429, 'Too many requests');
215
+ return;
216
+ }
280
217
  await handleAction(req, res);
281
218
  return;
282
219
  }
@@ -285,6 +222,12 @@ export async function startDashboardServer(options) {
285
222
  sendError(res, 403, 'Invalid origin');
286
223
  return;
287
224
  }
225
+ const check = refreshLimiter.check();
226
+ if (!check.allowed) {
227
+ res.setHeader('Retry-After', String(check.retryAfterSeconds));
228
+ sendError(res, 429, 'Too many requests');
229
+ return;
230
+ }
288
231
  await handleRefresh(req, res);
289
232
  return;
290
233
  }
@@ -1,16 +1,6 @@
1
1
  /**
2
2
  * Dashboard command — serves the interactive Preact SPA dashboard.
3
- * Also provides writeDashboardFromState() for generating a static HTML fallback
4
- * when the SPA cannot be launched (e.g., assets not built).
5
3
  */
6
- /**
7
- * Generate dashboard HTML from state (no GitHub fetch).
8
- * Call after executeDailyCheck() which saves fresh data to state.
9
- * Returns the path to the generated dashboard HTML file.
10
- *
11
- * Used as a safety net when the interactive SPA dashboard cannot be launched.
12
- */
13
- export declare function writeDashboardFromState(): string;
14
4
  interface ServeOptions {
15
5
  port: number;
16
6
  open: boolean;
@@ -1,35 +1,9 @@
1
1
  /**
2
2
  * Dashboard command — serves the interactive Preact SPA dashboard.
3
- * Also provides writeDashboardFromState() for generating a static HTML fallback
4
- * when the SPA cannot be launched (e.g., assets not built).
5
3
  */
6
4
  import * as fs from 'fs';
7
5
  import * as path from 'path';
8
- import { getStateManager, getDashboardPath, getGitHubToken } from '../core/index.js';
9
- import { getMonthlyData } from './dashboard-data.js';
10
- import { buildDashboardStats, generateDashboardHtml } from './dashboard-templates.js';
11
- // ── Static HTML fallback ────────────────────────────────────────────────────
12
- /**
13
- * Generate dashboard HTML from state (no GitHub fetch).
14
- * Call after executeDailyCheck() which saves fresh data to state.
15
- * Returns the path to the generated dashboard HTML file.
16
- *
17
- * Used as a safety net when the interactive SPA dashboard cannot be launched.
18
- */
19
- export function writeDashboardFromState() {
20
- const stateManager = getStateManager();
21
- const state = stateManager.getState();
22
- const digest = state.lastDigest;
23
- if (!digest) {
24
- throw new Error('No digest data available. Run daily check first.');
25
- }
26
- const { monthlyMerged, monthlyClosed, monthlyOpened } = getMonthlyData(state);
27
- const stats = buildDashboardStats(digest, state);
28
- const html = generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state);
29
- const dashboardPath = getDashboardPath();
30
- fs.writeFileSync(dashboardPath, html, { mode: 0o644 });
31
- return dashboardPath;
32
- }
6
+ import { getGitHubToken } from '../core/index.js';
33
7
  /**
34
8
  * Resolve the SPA assets directory from packages/dashboard/dist/.
35
9
  * Tries multiple strategies to locate it across dev (tsx) and bundled (cjs) modes.
@@ -1,23 +1,67 @@
1
1
  /**
2
2
  * Barrel export for all command functions and their output types.
3
3
  * Used by @oss-autopilot/mcp to import command functions directly.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { runDaily, runSearch, runStatus } from '@oss-autopilot/core/commands';
8
+ *
9
+ * const digest = await runDaily();
10
+ * const issues = await runSearch({ maxResults: 10 });
11
+ * const stats = await runStatus({});
12
+ * ```
4
13
  */
5
- export { runDaily, runDailyForDisplay, executeDailyCheck } from './daily.js';
14
+ /** Fetch all open PRs, compute digest, and return structured daily output. Requires GITHUB_TOKEN. */
15
+ export { runDaily } from './daily.js';
16
+ /** Like runDaily but returns the full (non-deduplicated) result for text-mode display. */
17
+ export { runDailyForDisplay } from './daily.js';
18
+ /** Lower-level daily check that accepts a token directly. Used by startup orchestration. */
19
+ export { executeDailyCheck } from './daily.js';
20
+ /** Combined startup: auth check, setup check, daily fetch, dashboard launch, version detection. */
21
+ export { runStartup } from './startup.js';
22
+ /** Return contribution statistics (merge rate, PR counts, repo breakdown) from local state. */
6
23
  export { runStatus } from './status.js';
24
+ /** Search GitHub for contributable issues using multi-strategy discovery. */
7
25
  export { runSearch } from './search.js';
26
+ /** Vet a single GitHub issue for claimability (open, unassigned, no linked PRs, repo health). */
8
27
  export { runVet } from './vet.js';
9
- export { runTrack, runUntrack } from './track.js';
28
+ /** Add a PR to tracking state. */
29
+ export { runTrack } from './track.js';
30
+ /** Remove a PR from tracking state. */
31
+ export { runUntrack } from './track.js';
32
+ /** Mark PR comments as read. */
10
33
  export { runRead } from './read.js';
11
- export { runComments, runPost, runClaim } from './comments.js';
34
+ /** Temporarily hide a PR from the daily digest. */
35
+ export { runShelve } from './shelve.js';
36
+ /** Restore a shelved PR to the daily digest. */
37
+ export { runUnshelve } from './shelve.js';
38
+ /** Dismiss issue reply notifications (auto-resurfaces on new activity). */
39
+ export { runDismiss } from './dismiss.js';
40
+ /** Restore a dismissed issue to notifications. */
41
+ export { runUndismiss } from './dismiss.js';
42
+ /** Temporarily suppress CI failure notifications for a PR. */
43
+ export { runSnooze } from './snooze.js';
44
+ /** Restore CI failure notifications for a snoozed PR. */
45
+ export { runUnsnooze } from './snooze.js';
46
+ /** Fetch comments for tracked issues/PRs. */
47
+ export { runComments } from './comments.js';
48
+ /** Post a comment to a GitHub issue or PR. */
49
+ export { runPost } from './comments.js';
50
+ /** Post a claim comment on a GitHub issue. */
51
+ export { runClaim } from './comments.js';
52
+ /** Read or write user configuration (githubUsername, languages, labels, etc). */
12
53
  export { runConfig } from './config.js';
54
+ /** Initialize with a GitHub username and import open PRs. */
13
55
  export { runInit } from './init.js';
14
- export { runSetup, runCheckSetup } from './setup.js';
15
- export { runShelve, runUnshelve } from './shelve.js';
16
- export { runDismiss, runUndismiss } from './dismiss.js';
17
- export { runSnooze, runUnsnooze } from './snooze.js';
18
- export { runStartup } from './startup.js';
56
+ /** Interactive first-run setup wizard. */
57
+ export { runSetup } from './setup.js';
58
+ /** Check whether setup has been completed. */
59
+ export { runCheckSetup } from './setup.js';
60
+ /** Parse a curated markdown issue list file into structured issue items. */
19
61
  export { runParseList } from './parse-list.js';
62
+ /** Check if new files are properly referenced/integrated. */
20
63
  export { runCheckIntegration } from './check-integration.js';
64
+ /** Scan for locally cloned repos. */
21
65
  export { runLocalRepos } from './local-repos.js';
22
66
  export type { DailyOutput, SearchOutput, StartupOutput, StatusOutput, TrackOutput } from '../formatters/json.js';
23
67
  export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput } from '../formatters/json.js';
@@ -1,22 +1,70 @@
1
1
  /**
2
2
  * Barrel export for all command functions and their output types.
3
3
  * Used by @oss-autopilot/mcp to import command functions directly.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { runDaily, runSearch, runStatus } from '@oss-autopilot/core/commands';
8
+ *
9
+ * const digest = await runDaily();
10
+ * const issues = await runSearch({ maxResults: 10 });
11
+ * const stats = await runStatus({});
12
+ * ```
4
13
  */
5
- // Command functions
6
- export { runDaily, runDailyForDisplay, executeDailyCheck } from './daily.js';
14
+ // ── Primary Commands ────────────────────────────────────────────────────────
15
+ /** Fetch all open PRs, compute digest, and return structured daily output. Requires GITHUB_TOKEN. */
16
+ export { runDaily } from './daily.js';
17
+ /** Like runDaily but returns the full (non-deduplicated) result for text-mode display. */
18
+ export { runDailyForDisplay } from './daily.js';
19
+ /** Lower-level daily check that accepts a token directly. Used by startup orchestration. */
20
+ export { executeDailyCheck } from './daily.js';
21
+ /** Combined startup: auth check, setup check, daily fetch, dashboard launch, version detection. */
22
+ export { runStartup } from './startup.js';
23
+ /** Return contribution statistics (merge rate, PR counts, repo breakdown) from local state. */
7
24
  export { runStatus } from './status.js';
25
+ /** Search GitHub for contributable issues using multi-strategy discovery. */
8
26
  export { runSearch } from './search.js';
27
+ /** Vet a single GitHub issue for claimability (open, unassigned, no linked PRs, repo health). */
9
28
  export { runVet } from './vet.js';
10
- export { runTrack, runUntrack } from './track.js';
29
+ // ── PR Management ───────────────────────────────────────────────────────────
30
+ /** Add a PR to tracking state. */
31
+ export { runTrack } from './track.js';
32
+ /** Remove a PR from tracking state. */
33
+ export { runUntrack } from './track.js';
34
+ /** Mark PR comments as read. */
11
35
  export { runRead } from './read.js';
12
- export { runComments, runPost, runClaim } from './comments.js';
36
+ /** Temporarily hide a PR from the daily digest. */
37
+ export { runShelve } from './shelve.js';
38
+ /** Restore a shelved PR to the daily digest. */
39
+ export { runUnshelve } from './shelve.js';
40
+ /** Dismiss issue reply notifications (auto-resurfaces on new activity). */
41
+ export { runDismiss } from './dismiss.js';
42
+ /** Restore a dismissed issue to notifications. */
43
+ export { runUndismiss } from './dismiss.js';
44
+ /** Temporarily suppress CI failure notifications for a PR. */
45
+ export { runSnooze } from './snooze.js';
46
+ /** Restore CI failure notifications for a snoozed PR. */
47
+ export { runUnsnooze } from './snooze.js';
48
+ // ── Issue & Comment Management ──────────────────────────────────────────────
49
+ /** Fetch comments for tracked issues/PRs. */
50
+ export { runComments } from './comments.js';
51
+ /** Post a comment to a GitHub issue or PR. */
52
+ export { runPost } from './comments.js';
53
+ /** Post a claim comment on a GitHub issue. */
54
+ export { runClaim } from './comments.js';
55
+ // ── Configuration & Setup ───────────────────────────────────────────────────
56
+ /** Read or write user configuration (githubUsername, languages, labels, etc). */
13
57
  export { runConfig } from './config.js';
58
+ /** Initialize with a GitHub username and import open PRs. */
14
59
  export { runInit } from './init.js';
15
- export { runSetup, runCheckSetup } from './setup.js';
16
- export { runShelve, runUnshelve } from './shelve.js';
17
- export { runDismiss, runUndismiss } from './dismiss.js';
18
- export { runSnooze, runUnsnooze } from './snooze.js';
19
- export { runStartup } from './startup.js';
60
+ /** Interactive first-run setup wizard. */
61
+ export { runSetup } from './setup.js';
62
+ /** Check whether setup has been completed. */
63
+ export { runCheckSetup } from './setup.js';
64
+ // ── Utilities ───────────────────────────────────────────────────────────────
65
+ /** Parse a curated markdown issue list file into structured issue items. */
20
66
  export { runParseList } from './parse-list.js';
67
+ /** Check if new files are properly referenced/integrated. */
21
68
  export { runCheckIntegration } from './check-integration.js';
69
+ /** Scan for locally cloned repos. */
22
70
  export { runLocalRepos } from './local-repos.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Simple in-memory sliding-window rate limiter for the dashboard server.
3
+ *
4
+ * Each endpoint gets its own limiter with a configurable max number of
5
+ * requests per time window. Returns 429 with Retry-After when exceeded.
6
+ */
7
+ export interface RateLimiterConfig {
8
+ /** Maximum requests allowed within the window. */
9
+ maxRequests: number;
10
+ /** Window duration in milliseconds. */
11
+ windowMs: number;
12
+ }
13
+ export declare class RateLimiter {
14
+ private readonly maxRequests;
15
+ private readonly windowMs;
16
+ /** Timestamps of accepted requests within the current window. */
17
+ private timestamps;
18
+ constructor(config: RateLimiterConfig);
19
+ /**
20
+ * Check if a request is allowed.
21
+ *
22
+ * @returns `{ allowed: true }` if under the limit, or
23
+ * `{ allowed: false, retryAfterSeconds }` if rate-limited.
24
+ */
25
+ check(): {
26
+ allowed: true;
27
+ } | {
28
+ allowed: false;
29
+ retryAfterSeconds: number;
30
+ };
31
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Simple in-memory sliding-window rate limiter for the dashboard server.
3
+ *
4
+ * Each endpoint gets its own limiter with a configurable max number of
5
+ * requests per time window. Returns 429 with Retry-After when exceeded.
6
+ */
7
+ export class RateLimiter {
8
+ maxRequests;
9
+ windowMs;
10
+ /** Timestamps of accepted requests within the current window. */
11
+ timestamps = [];
12
+ constructor(config) {
13
+ this.maxRequests = config.maxRequests;
14
+ this.windowMs = config.windowMs;
15
+ }
16
+ /**
17
+ * Check if a request is allowed.
18
+ *
19
+ * @returns `{ allowed: true }` if under the limit, or
20
+ * `{ allowed: false, retryAfterSeconds }` if rate-limited.
21
+ */
22
+ check() {
23
+ const now = Date.now();
24
+ const windowStart = now - this.windowMs;
25
+ // Prune timestamps outside the current window
26
+ this.timestamps = this.timestamps.filter((t) => t > windowStart);
27
+ if (this.timestamps.length >= this.maxRequests) {
28
+ // Earliest timestamp in window — client must wait until it expires
29
+ const oldestInWindow = this.timestamps[0];
30
+ const retryAfterMs = oldestInWindow + this.windowMs - now;
31
+ return { allowed: false, retryAfterSeconds: Math.ceil(retryAfterMs / 1000) };
32
+ }
33
+ this.timestamps.push(now);
34
+ return { allowed: true };
35
+ }
36
+ }
@@ -32,7 +32,7 @@ export declare function openInBrowser(url: string): void;
32
32
  * Returns StartupOutput with one of three shapes:
33
33
  * 1. Setup incomplete: { version, setupComplete: false }
34
34
  * 2. Auth failure: { version, setupComplete: true, authError: "..." }
35
- * 3. Success: { version, setupComplete: true, daily, dashboardUrl?, dashboardPath?, issueList? }
35
+ * 3. Success: { version, setupComplete: true, daily, dashboardUrl?, issueList? }
36
36
  *
37
37
  * Errors from the daily check propagate to the caller.
38
38
  */