@popoverai/dotrequirements 0.14.0 → 0.15.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
@@ -114,8 +114,19 @@ Sync requirements from dot•requirements cloud to local `.requirements/` files.
114
114
  dotreq pull
115
115
  dotreq pull --project <project-id>
116
116
  dotreq pull --document <document-id>
117
+ dotreq pull --share <token>
117
118
  ```
118
119
 
120
+ **First-time setup with a share token:**
121
+
122
+ If a team member shares a pull command with you, you can pull requirements without creating an account:
123
+
124
+ ```bash
125
+ npx @popoverai/dotrequirements pull --share drt_abc123...
126
+ ```
127
+
128
+ This gives you read-only access to view requirements. To push changes or report coverage, run `dotreq link` afterward.
129
+
119
130
  ### `dotreq push`
120
131
 
121
132
  Push local requirements to dot•requirements cloud.
@@ -421,7 +432,7 @@ Follow the prompts to configure for your AI assistant (Claude Code, Cursor, etc.
421
432
  **Authoring:**
422
433
  - `create_requirement_document` — Get a Markdown template with format examples
423
434
  - `validate_requirements` — Validate file schema (works offline)
424
- - `style_check` — AI-powered style feedback on requirements
435
+ - `style_check` — AI-powered style feedback on requirements (supports optional `requirementKeys` filter)
425
436
  - `review_test` — Comprehensive test review (style + semantic correctness)
426
437
 
427
438
  **Cloud:**
@@ -432,6 +443,39 @@ Follow the prompts to configure for your AI assistant (Claude Code, Cursor, etc.
432
443
  **Diagnostic:**
433
444
  - `debug_mcp_environment` — Debug MCP server configuration
434
445
 
446
+ ### CI/CD Mode
447
+
448
+ For CI/CD environments (GitHub Actions, etc.), use the `--auth-from-env` flag to read credentials from environment variables instead of `project-settings.json`:
449
+
450
+ ```bash
451
+ node packages/cli/dist/mcp/index.js --auth-from-env
452
+ ```
453
+
454
+ Required environment variables:
455
+ - `DOTREQ_PROJECT_ID` — Your project slug
456
+ - `DOTREQ_PROJECT_SECRET` — Your project secret
457
+
458
+ **Important:** Without `--auth-from-env`, these environment variables are ignored. This explicit opt-in prevents credential conflicts between local and CI environments.
459
+
460
+ Example MCP config for GitHub Actions:
461
+
462
+ ```json
463
+ {
464
+ "mcpServers": {
465
+ "dotrequirements": {
466
+ "command": "node",
467
+ "args": ["packages/cli/dist/mcp/index.js", "--auth-from-env"],
468
+ "env": {
469
+ "DOTREQ_PROJECT_ID": "${DOTREQ_PROJECT_ID}",
470
+ "DOTREQ_PROJECT_SECRET": "${DOTREQ_PROJECT_SECRET}"
471
+ }
472
+ }
473
+ }
474
+ }
475
+ ```
476
+
477
+ See the [CI/CD Integration docs](https://dotrequirements.io/tools/ai/ci-cd) for complete examples.
478
+
435
479
  ---
436
480
 
437
481
  ## Package Exports
package/dist/cli.js CHANGED
@@ -57,6 +57,7 @@ program
57
57
  .description('Sync requirements from cloud to local .requirements/ files')
58
58
  .option('-p, --project <id>', 'Project ID to sync')
59
59
  .option('-d, --document <id>', 'Specific document ID to sync')
60
+ .option('-s, --share <token>', 'Read-only share token for quick onboarding (no setup required)')
60
61
  .action(wrapCommand(pullCommand));
61
62
  program
62
63
  .command('push [file]')
@@ -1,6 +1,7 @@
1
1
  interface PullOptions {
2
2
  project?: string;
3
3
  document?: string;
4
+ share?: string;
4
5
  }
5
6
  export declare function pullCommand(options: PullOptions): Promise<void>;
6
7
  export {};
@@ -9,43 +9,75 @@ import { findRequirementsFiles } from '../mcp/requirements.js';
9
9
  import { getProjectCredentials } from '../utils/project-settings.js';
10
10
  export async function pullCommand(options) {
11
11
  try {
12
- console.log(`Syncing requirements from ${brand} cloud...`);
13
12
  // Connect to Convex
14
13
  const convexUrl = getConvexUrl();
15
14
  const client = new ConvexHttpClient(convexUrl);
16
- // Get project credentials from .requirements/project-settings.json
17
- // This throws helpful errors if project not found or not connected to cloud
18
- const credentials = getProjectCredentials();
19
- // Allow --project flag to override, but still need secret from settings
20
- const projectId = options.project ?? credentials.projectId;
21
- const projectSecret = credentials.projectSecret;
22
- // Fetch documents
23
15
  let documents;
24
- if (options.document) {
25
- // Fetch specific document
26
- const doc = await client.query(api.documents.queries.exportForCli, {
27
- projectAuth: {
28
- projectSlug: projectId,
29
- projectSecret,
30
- },
31
- target: { type: 'document', id: options.document },
32
- documentId: options.document,
33
- });
34
- documents = doc ? [doc] : [];
35
- }
36
- else if (projectId) {
37
- // Fetch all documents for project
38
- // SYNC-ARCH-3.1: Use projectSlug (not projectId) since env stores the slug
39
- documents = await client.query(api.documents.queries.exportProjectForCli, {
40
- projectAuth: {
41
- projectSlug: projectId,
42
- projectSecret,
43
- },
44
- target: { type: 'project', slug: projectId },
45
- });
16
+ let projectName;
17
+ let isShareTokenFlow = false;
18
+ // SHARE-TOKEN-CLI-1: Handle share token flow (no project config needed)
19
+ if (options.share) {
20
+ isShareTokenFlow = true;
21
+ console.log(`Pulling requirements with share token...`);
22
+ try {
23
+ // SHARE-TOKEN-CLI-1.1: Pass only projectSecret, no projectSlug
24
+ // The authzQueryReadOnly wrapper will resolve project from the token
25
+ const result = await client.query(api.documents.queries.exportProjectForCli, {
26
+ projectAuth: {
27
+ projectSecret: options.share,
28
+ },
29
+ });
30
+ documents = result.documents;
31
+ projectName = result.projectName;
32
+ // SHARE-TOKEN-CLI-1.2: Display project name in output
33
+ console.log(`\nProject: "${projectName}"`);
34
+ }
35
+ catch (error) {
36
+ // SHARE-TOKEN-CLI-2: Handle invalid/revoked tokens gracefully
37
+ const errorMessage = error instanceof Error ? error.message : String(error);
38
+ if (errorMessage.includes('Invalid share token')) {
39
+ throw new Error('Invalid share token. The token may be incorrect or has been revoked.\n' +
40
+ 'Please request a new share token from your team.');
41
+ }
42
+ throw error;
43
+ }
46
44
  }
47
45
  else {
48
- throw new Error('Either --project or --document must be specified');
46
+ console.log(`Syncing requirements from ${brand} cloud...`);
47
+ // Get project credentials from .requirements/project-settings.json
48
+ // This throws helpful errors if project not found or not connected to cloud
49
+ const credentials = getProjectCredentials();
50
+ // Allow --project flag to override, but still need secret from settings
51
+ const projectId = options.project ?? credentials.projectId;
52
+ const projectSecret = credentials.projectSecret;
53
+ // Fetch documents
54
+ if (options.document) {
55
+ // Fetch specific document
56
+ const doc = await client.query(api.documents.queries.exportForCli, {
57
+ projectAuth: {
58
+ projectSlug: projectId,
59
+ projectSecret,
60
+ },
61
+ target: { type: 'document', id: options.document },
62
+ documentId: options.document,
63
+ });
64
+ documents = doc ? [doc] : [];
65
+ }
66
+ else if (projectId) {
67
+ // Fetch all documents for project
68
+ // SYNC-ARCH-3.1: Use projectSlug (not projectId) since env stores the slug
69
+ const result = await client.query(api.documents.queries.exportProjectForCli, {
70
+ projectAuth: {
71
+ projectSlug: projectId,
72
+ projectSecret,
73
+ },
74
+ });
75
+ documents = result.documents;
76
+ projectName = result.projectName;
77
+ }
78
+ else {
79
+ throw new Error('Either --project or --document must be specified');
80
+ }
49
81
  }
50
82
  if (documents.length === 0) {
51
83
  console.log('No documents found.');
@@ -82,6 +114,13 @@ export async function pullCommand(options) {
82
114
  console.log(`✓ Synced: ${fileName} (version ${doc.version})`);
83
115
  }
84
116
  console.log(`\nSuccessfully synced ${documents.length} document(s) to .requirements/`);
117
+ // SHARE-TOKEN-CLI-3: Show next steps for share token users
118
+ if (isShareTokenFlow) {
119
+ console.log('\n─────────────────────────────────────────────────────────');
120
+ console.log('Next steps to get full access (push, coverage):');
121
+ console.log(' Run: dotrequirements link');
122
+ console.log('─────────────────────────────────────────────────────────');
123
+ }
85
124
  }
86
125
  catch (error) {
87
126
  // Re-throw to let the CLI error handler display it cleanly
package/dist/config.js CHANGED
@@ -7,10 +7,6 @@ const CONVEX_URL = 'https://data.dotrequirements.io';
7
7
  * Get the Convex deployment URL
8
8
  */
9
9
  export function getConvexUrl() {
10
- // Show deprecation warning if user has CONVEX_URL in their env
11
- if (process.env.CONVEX_URL) {
12
- console.warn('⚠️ CONVEX_URL is no longer needed and will be ignored. You can safely remove it from your .env.local');
13
- }
14
10
  return CONVEX_URL;
15
11
  }
16
12
  //# sourceMappingURL=config.js.map
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Production Convex deployment URL
3
+ */
4
+ export declare const CONVEX_URL = "https://data.dotrequirements.io";
1
5
  /**
2
6
  * Convex configuration for cloud operations
3
7
  */
@@ -2,7 +2,7 @@ import { getProjectCredentials } from '../utils/project-settings.js';
2
2
  /**
3
3
  * Production Convex deployment URL
4
4
  */
5
- const CONVEX_URL = 'https://data.dotrequirements.io';
5
+ export const CONVEX_URL = 'https://data.dotrequirements.io';
6
6
  /**
7
7
  * Load Convex configuration from project settings.
8
8
  * Returns null if credentials are not configured.
package/dist/mcp/index.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { loadAllRequirements, searchRequirements, getRequirementById, getRequirementTree, formatRequirementTree, } from './requirements.js';
5
+ import { loadAllRequirements, searchRequirements, getRequirementById, getRequirementTree, formatRequirementTree, filterRequirementsByKeys, } from './requirements.js';
6
6
  import { findAllTestReferences, getReferencedRequirementIds, } from './grep.js';
7
7
  import { findTestCodeForRequirement, findFilesWithRequirement, } from './testCodeExtractor.js';
8
8
  import { glob } from 'glob';
9
- import { loadConvexConfig, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, } from './convexClient.js';
9
+ import { CONVEX_URL, getRequirementCoverage as queryRequirementCoverage, getProjectCoverage as queryProjectCoverage, } from './convexClient.js';
10
10
  import { discoverProjects, resolveProject, } from '../utils/project-discovery.js';
11
+ import { getCredentialsFromEnv } from '../utils/project-settings.js';
11
12
  import { readFileSync } from 'fs';
12
13
  import { fileURLToPath } from 'url';
13
14
  import { dirname, join } from 'path';
@@ -32,6 +33,9 @@ function getProjectPathsFromEnv() {
32
33
  // Get workspace root from environment or default to cwd
33
34
  const WORKSPACE_ROOT = process.env.REQUIREMENTS_DIR || process.cwd();
34
35
  const PROJECT_PATHS = getProjectPathsFromEnv();
36
+ // Parse --auth-from-env flag for CI/CD environments
37
+ // When set, credentials are read from DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET env vars
38
+ const USE_ENV_AUTH = process.argv.includes('--auth-from-env');
35
39
  const STYLE_CHECK_GUIDANCE = `
36
40
 
37
41
  ---
@@ -57,6 +61,20 @@ async function getRequirements(projectId) {
57
61
  }
58
62
  async function getProjectFromDiscovery(projectId) {
59
63
  const { isConfiguredProject } = await import('../utils/project-discovery.js');
64
+ // If --auth-from-env flag is set, use credentials from environment variables
65
+ // This is the explicit opt-in for CI/CD environments
66
+ if (USE_ENV_AUTH) {
67
+ const envCredentials = getCredentialsFromEnv();
68
+ if (!envCredentials) {
69
+ throw new Error('--auth-from-env flag requires DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET environment variables to be set');
70
+ }
71
+ // Return a synthetic project using env credentials and workspace root
72
+ return {
73
+ path: WORKSPACE_ROOT,
74
+ projectId: envCredentials.projectId,
75
+ projectSecret: envCredentials.projectSecret,
76
+ };
77
+ }
60
78
  // If we have PROJ_* env vars, use those instead of filesystem discovery
61
79
  if (PROJECT_PATHS.size > 0) {
62
80
  const projects = [];
@@ -300,7 +318,7 @@ const tools = [
300
318
  },
301
319
  {
302
320
  name: 'style_check',
303
- description: 'Check requirements files or test files for style issues and best practices. Uses AI to provide actionable feedback on writing style, clarity, and conventions. Supports requirements files (*.requirements.md) and test files (*.test.*, *.spec.*). Requires project to be linked to cloud (run `dotrequirements link`).',
321
+ description: 'Check requirements files or test files for style issues and best practices. Uses AI to provide actionable feedback on writing style, clarity, and conventions. Supports requirements files (*.requirements.md) and test files (*.test.*, *.spec.*). For requirements files, you can optionally specify requirement keys to check only those requirements instead of the entire file. Requires project to be linked to cloud (run `dotrequirements link`).',
304
322
  inputSchema: {
305
323
  type: 'object',
306
324
  properties: {
@@ -308,6 +326,11 @@ const tools = [
308
326
  type: 'string',
309
327
  description: 'Path to the file to check (e.g., ".requirements/auth.requirements.md" or "src/auth.test.ts")',
310
328
  },
329
+ requirementKeys: {
330
+ type: 'array',
331
+ items: { type: 'string' },
332
+ description: 'Optional: Array of requirement keys to check (e.g., ["AUTH-1", "AUTH-2"]). Only valid for requirements files (*.requirements.md). When provided, only these requirements are checked instead of the entire file.',
333
+ },
311
334
  model: {
312
335
  type: 'string',
313
336
  description: 'Optional: AI model to use for style checking (default: "anthropic/claude-haiku-4.5"). Supported models: "anthropic/claude-haiku-4.5", "google/gemini-3-flash"',
@@ -899,19 +922,7 @@ ${projectList || '(none found)'}`,
899
922
  case 'get_requirement_coverage': {
900
923
  const { requirementKey, projectId } = args;
901
924
  const project = await getProjectFromDiscovery(projectId);
902
- const config = loadConvexConfig(project.path);
903
- if (!config) {
904
- return {
905
- content: [
906
- {
907
- type: 'text',
908
- text: 'Coverage queries require project credentials. Run `dotrequirements link` to connect to cloud.',
909
- },
910
- ],
911
- isError: true,
912
- };
913
- }
914
- const coverage = await queryRequirementCoverage(requirementKey, config.projectId, config.projectSecret, config.convexUrl);
925
+ const coverage = await queryRequirementCoverage(requirementKey, project.projectId, project.projectSecret, CONVEX_URL);
915
926
  if (!coverage.lastTestedAt) {
916
927
  return {
917
928
  content: [
@@ -946,19 +957,7 @@ ${projectList || '(none found)'}`,
946
957
  case 'get_project_coverage_summary': {
947
958
  const { branch, sinceTimestamp, projectId } = args;
948
959
  const project = await getProjectFromDiscovery(projectId);
949
- const config = loadConvexConfig(project.path);
950
- if (!config) {
951
- return {
952
- content: [
953
- {
954
- type: 'text',
955
- text: 'Coverage queries require project credentials. Run `dotrequirements link` to connect to cloud.',
956
- },
957
- ],
958
- isError: true,
959
- };
960
- }
961
- const coverage = await queryProjectCoverage(config.projectId, config.projectSecret, config.convexUrl, { branch, sinceTimestamp });
960
+ const coverage = await queryProjectCoverage(project.projectId, project.projectSecret, CONVEX_URL, { branch, sinceTimestamp });
962
961
  const total = coverage.tested.length + coverage.untested.length;
963
962
  const percentage = total > 0 ? ((coverage.tested.length / total) * 100).toFixed(1) : '0.0';
964
963
  let filterInfo = '';
@@ -1286,23 +1285,11 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1286
1285
  const path = await import('path');
1287
1286
  const { parseRequirementsFromFile, getAllRequirements } = await import('../schema/index.js');
1288
1287
  const { ConvexHttpClient } = await import('convex/browser');
1289
- // Get project
1288
+ // Get project (credentials come from project object, works with both file and env auth)
1290
1289
  const project = await getProjectFromDiscovery(projectIdParam);
1291
- const config = loadConvexConfig(project.path);
1292
- if (!config) {
1293
- return {
1294
- content: [
1295
- {
1296
- type: 'text',
1297
- text: 'Push requires project credentials. Run `dotrequirements link` to connect to cloud.',
1298
- },
1299
- ],
1300
- isError: true,
1301
- };
1302
- }
1303
- const convexUrl = config.convexUrl;
1304
- const projectId = config.projectId;
1305
- const projectSecret = config.projectSecret;
1290
+ const convexUrl = CONVEX_URL;
1291
+ const projectId = project.projectId;
1292
+ const projectSecret = project.projectSecret;
1306
1293
  // Determine files to push
1307
1294
  const requirementsDir = path.resolve(project.path, '.requirements');
1308
1295
  let filesToPush;
@@ -1411,7 +1398,7 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1411
1398
  }
1412
1399
  }
1413
1400
  case 'style_check': {
1414
- const { filePath, model } = args;
1401
+ const { filePath, model, requirementKeys } = args;
1415
1402
  const fs = await import('fs');
1416
1403
  const path = await import('path');
1417
1404
  const fullPath = path.resolve(WORKSPACE_ROOT, filePath);
@@ -1443,22 +1430,73 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1443
1430
  };
1444
1431
  }
1445
1432
  const fileType = isRequirementsFile ? 'requirements' : 'test';
1433
+ // If requirementKeys provided, filter the file to only those requirements
1434
+ let fileContentsToCheck = fileContents;
1435
+ let scopeNote = '';
1436
+ if (requirementKeys && requirementKeys.length > 0) {
1437
+ if (!isRequirementsFile) {
1438
+ return {
1439
+ content: [
1440
+ {
1441
+ type: 'text',
1442
+ text: `requirementKeys can only be used with requirements files (*.requirements.md), not test files.`,
1443
+ },
1444
+ ],
1445
+ isError: true,
1446
+ };
1447
+ }
1448
+ try {
1449
+ const { filteredContent, foundKeys, missingKeys } = filterRequirementsByKeys(fileContents, requirementKeys);
1450
+ if (foundKeys.length === 0) {
1451
+ return {
1452
+ content: [
1453
+ {
1454
+ type: 'text',
1455
+ text: `None of the specified requirement keys were found in ${filePath}: ${requirementKeys.join(', ')}`,
1456
+ },
1457
+ ],
1458
+ isError: true,
1459
+ };
1460
+ }
1461
+ fileContentsToCheck = filteredContent;
1462
+ if (missingKeys.length > 0) {
1463
+ scopeNote = `\n\n> **Note:** Some specified keys were not found in the file: ${missingKeys.join(', ')}`;
1464
+ }
1465
+ }
1466
+ catch (error) {
1467
+ return {
1468
+ content: [
1469
+ {
1470
+ type: 'text',
1471
+ text: `Failed to filter requirements: ${error instanceof Error ? error.message : String(error)}`,
1472
+ },
1473
+ ],
1474
+ isError: true,
1475
+ };
1476
+ }
1477
+ }
1446
1478
  // Get Convex config for credentials - walk up from file's directory to find project
1447
1479
  const fileDir = path.dirname(fullPath);
1448
1480
  let project;
1449
1481
  try {
1450
- // Try to find a project starting from the file's directory
1451
- const result = await discoverProjects(fileDir);
1452
- if (result.type === 'none') {
1453
- // If no project found from file dir, try from WORKSPACE_ROOT
1482
+ // If using env auth, go straight to getProjectFromDiscovery (handles env credentials)
1483
+ if (USE_ENV_AUTH) {
1454
1484
  project = await getProjectFromDiscovery();
1455
1485
  }
1456
- else if (result.type === 'single') {
1457
- project = result.project;
1458
- }
1459
1486
  else {
1460
- // Multiple projects - can't auto-detect which one to use
1461
- throw new Error('Multiple projects found - cannot auto-detect for this file');
1487
+ // Try to find a project starting from the file's directory
1488
+ const result = await discoverProjects(fileDir);
1489
+ if (result.type === 'none') {
1490
+ // If no project found from file dir, try from WORKSPACE_ROOT
1491
+ project = await getProjectFromDiscovery();
1492
+ }
1493
+ else if (result.type === 'single') {
1494
+ project = result.project;
1495
+ }
1496
+ else {
1497
+ // Multiple projects - can't auto-detect which one to use
1498
+ throw new Error('Multiple projects found - cannot auto-detect for this file');
1499
+ }
1462
1500
  }
1463
1501
  }
1464
1502
  catch (error) {
@@ -1472,18 +1510,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1472
1510
  isError: true,
1473
1511
  };
1474
1512
  }
1475
- const config = loadConvexConfig(project.path);
1476
- if (!config) {
1477
- return {
1478
- content: [
1479
- {
1480
- type: 'text',
1481
- text: 'Style check requires project credentials. Run `dotrequirements link` to connect to cloud.',
1482
- },
1483
- ],
1484
- isError: true,
1485
- };
1486
- }
1513
+ // Use credentials from project (works with both file-based and env-based auth)
1514
+ const { projectId: projId, projectSecret: projSecret } = project;
1487
1515
  // Call Vercel API endpoint for style checking
1488
1516
  // Default to production, allow override via env var for local dev
1489
1517
  const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
@@ -1494,9 +1522,9 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1494
1522
  'Content-Type': 'application/json',
1495
1523
  },
1496
1524
  body: JSON.stringify({
1497
- projectId: config.projectId,
1498
- projectSecret: config.projectSecret,
1499
- fileContents,
1525
+ projectId: projId,
1526
+ projectSecret: projSecret,
1527
+ fileContents: fileContentsToCheck,
1500
1528
  fileType,
1501
1529
  model,
1502
1530
  }),
@@ -1514,11 +1542,14 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1514
1542
  };
1515
1543
  }
1516
1544
  const data = await response.json();
1545
+ const scopeLabel = requirementKeys && requirementKeys.length > 0
1546
+ ? ` (${requirementKeys.join(', ')})`
1547
+ : '';
1517
1548
  return {
1518
1549
  content: [
1519
1550
  {
1520
1551
  type: 'text',
1521
- text: `# Style Check Results for \`${filePath}\`\n\n${data.feedback}${STYLE_CHECK_GUIDANCE}`,
1552
+ text: `# Style Check Results for \`${filePath}\`${scopeLabel}\n\n${data.feedback}${scopeNote}${STYLE_CHECK_GUIDANCE}`,
1522
1553
  },
1523
1554
  ],
1524
1555
  };
@@ -1614,18 +1645,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1614
1645
  });
1615
1646
  }
1616
1647
  // Get Convex config
1617
- const config = loadConvexConfig(project.path);
1618
- if (!config) {
1619
- return {
1620
- content: [
1621
- {
1622
- type: 'text',
1623
- text: 'Test review requires project credentials. Run `dotrequirements link` to connect to cloud.',
1624
- },
1625
- ],
1626
- isError: true,
1627
- };
1628
- }
1648
+ // Use credentials from project (works with both file-based and env-based auth)
1649
+ const { projectId: projId, projectSecret: projSecret } = project;
1629
1650
  // Call Vercel API endpoint for test review
1630
1651
  // Default to production, allow override via env var for local dev
1631
1652
  const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL || 'https://app.dotrequirements.io';
@@ -1636,8 +1657,8 @@ describe(requirement('AUTH-LOGIN-1'), () => {
1636
1657
  'Content-Type': 'application/json',
1637
1658
  },
1638
1659
  body: JSON.stringify({
1639
- projectId: config.projectId,
1640
- projectSecret: config.projectSecret,
1660
+ projectId: projId,
1661
+ projectSecret: projSecret,
1641
1662
  testFileContents,
1642
1663
  requirements,
1643
1664
  }),
@@ -40,6 +40,16 @@ export declare function getRequirementTree(requirements: FlattenedRequirement[],
40
40
  * Format a requirement for display
41
41
  */
42
42
  export declare function formatRequirement(req: FlattenedRequirement): string;
43
+ /**
44
+ * Filter a requirements file's content to only include specified requirement keys.
45
+ * Parses the file content, filters top-level requirements by key, and rebuilds as
46
+ * markdown requirement blocks (without frontmatter).
47
+ */
48
+ export declare function filterRequirementsByKeys(fileContents: string, keys: string[]): {
49
+ filteredContent: string;
50
+ foundKeys: string[];
51
+ missingKeys: string[];
52
+ };
43
53
  /**
44
54
  * Format a requirement tree for display
45
55
  */
@@ -1,6 +1,6 @@
1
1
  import * as path from 'path';
2
2
  import { glob } from 'glob';
3
- import { parseRequirementsFromFile, getAllRequirements, } from '../schema/index.js';
3
+ import { parseRequirementsFromFile, parseRequirementsFile, buildRequirementMarkdown, getAllRequirements, } from '../schema/index.js';
4
4
  /**
5
5
  * Find all *.requirements.md files in the workspace.
6
6
  * Supports both .requirements/ directories and colocated files.
@@ -114,6 +114,20 @@ export function formatRequirement(req) {
114
114
  const label = req.label ? ` (${req.label})` : '';
115
115
  return `${indent}${req.id}${label}: ${req.content}`;
116
116
  }
117
+ /**
118
+ * Filter a requirements file's content to only include specified requirement keys.
119
+ * Parses the file content, filters top-level requirements by key, and rebuilds as
120
+ * markdown requirement blocks (without frontmatter).
121
+ */
122
+ export function filterRequirementsByKeys(fileContents, keys) {
123
+ const { requirements } = parseRequirementsFile(fileContents);
124
+ const upperKeys = keys.map(k => k.toUpperCase());
125
+ const filtered = requirements.filter(r => upperKeys.includes(r.id.toUpperCase()));
126
+ const foundKeys = filtered.map(r => r.id);
127
+ const missingKeys = upperKeys.filter(k => !foundKeys.map(f => f.toUpperCase()).includes(k));
128
+ const filteredContent = filtered.map(buildRequirementMarkdown).join('\n');
129
+ return { filteredContent, foundKeys, missingKeys };
130
+ }
117
131
  /**
118
132
  * Format a requirement tree for display
119
133
  */
@@ -20,6 +20,15 @@ export interface ProjectInfo {
20
20
  /** Credentials if cloud-connected */
21
21
  credentials?: ProjectSettings;
22
22
  }
23
+ /**
24
+ * Get project credentials from environment variables.
25
+ * Used in CI/CD environments where credentials are passed via env vars
26
+ * rather than stored in project-settings.json.
27
+ *
28
+ * Requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET to be set.
29
+ * Returns undefined if either is missing.
30
+ */
31
+ export declare function getCredentialsFromEnv(): ProjectSettings | undefined;
23
32
  /**
24
33
  * Find the project root by walking up from startDir looking for .requirements/ folder
25
34
  * Returns the directory containing .requirements/, or undefined if not found
@@ -2,6 +2,22 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  const REQUIREMENTS_DIR = '.requirements';
4
4
  const SETTINGS_FILE = 'project-settings.json';
5
+ /**
6
+ * Get project credentials from environment variables.
7
+ * Used in CI/CD environments where credentials are passed via env vars
8
+ * rather than stored in project-settings.json.
9
+ *
10
+ * Requires both DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET to be set.
11
+ * Returns undefined if either is missing.
12
+ */
13
+ export function getCredentialsFromEnv() {
14
+ const projectId = process.env.DOTREQ_PROJECT_ID;
15
+ const projectSecret = process.env.DOTREQ_PROJECT_SECRET;
16
+ if (projectId && projectSecret) {
17
+ return { projectId, projectSecret };
18
+ }
19
+ return undefined;
20
+ }
5
21
  /**
6
22
  * Find the project root by walking up from startDir looking for .requirements/ folder
7
23
  * Returns the directory containing .requirements/, or undefined if not found
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Requirements tracking CLI, test harness, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {