@popoverai/dotrequirements 0.32.0 → 0.33.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/dist/cli.js CHANGED
@@ -25,8 +25,10 @@ import { styleCheckCommand } from "./commands/style-check.js";
25
25
  import { syncCliAction } from "./commands/sync.js";
26
26
  import { testsForCommand } from "./commands/tests-for.js";
27
27
  import { validateCommand } from "./commands/validate.js";
28
+ import { commandErrorMessage } from "./utils/command-error.js";
28
29
  import { loadEnvFile } from "./utils/env.js";
29
30
  import { setPreferEnvCredentials } from "./utils/project-settings.js";
31
+ import { enableVersionMessages, formatVersionMessages, markVersionMessagesShown, versionMessages, } from "./version-messages.js";
30
32
  // Read version from package.json
31
33
  const __filename = fileURLToPath(import.meta.url);
32
34
  const __dirname = dirname(__filename);
@@ -34,6 +36,20 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"),
34
36
  const VERSION = packageJson.version;
35
37
  // Load environment variables from .env.local if it exists
36
38
  loadEnvFile();
39
+ // CLI-MSG-1: a run that reaches the cloud hears what it has to say to this
40
+ // version.
41
+ enableVersionMessages();
42
+ /**
43
+ * The cloud's messages for this version, after the command's own output. On
44
+ * stderr, so a command's piped output stays clean.
45
+ */
46
+ async function printVersionMessages() {
47
+ const messages = await versionMessages();
48
+ if (messages.length > 0) {
49
+ console.error(`${formatVersionMessages(messages)}\n`);
50
+ }
51
+ markVersionMessagesShown();
52
+ }
37
53
  /**
38
54
  * Wrap command handlers to catch errors and display them cleanly
39
55
  * without showing stack traces to users
@@ -44,14 +60,11 @@ function wrapCommand(fn) {
44
60
  await fn(...args);
45
61
  }
46
62
  catch (error) {
47
- if (error instanceof Error) {
48
- console.error(`\nError: ${error.message}\n`);
49
- }
50
- else {
51
- console.error(`\nError: ${String(error)}\n`);
52
- }
63
+ console.error(`\nError: ${commandErrorMessage(error)}\n`);
64
+ await printVersionMessages();
53
65
  process.exit(1);
54
66
  }
67
+ await printVersionMessages();
55
68
  };
56
69
  }
57
70
  const program = new Command();
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The one way into the dotrequirements cloud. Every place the CLI reaches the
3
+ * cloud asks here for the address, so the first use in a run is visible — the
4
+ * version-message check (CLI-MSG-1) starts there.
5
+ */
6
+ /** The web app, home of the hosted AI endpoints and publishing. */
7
+ export declare const DEFAULT_API_BASE_URL = "https://app.dotrequirements.io";
8
+ /** The Convex backend's address, for a ConvexHttpClient or its HTTP API. */
9
+ export declare function convexUrl(): string;
10
+ /** The web app's address; `DOTREQUIREMENTS_API_URL` overrides it. */
11
+ export declare function webApiUrl(): string;
12
+ /** Run `callback` the first time this process reaches the cloud. */
13
+ export declare function whenCloudFirstUsed(callback: () => void): void;
14
+ //# sourceMappingURL=cloud.d.ts.map
package/dist/cloud.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The one way into the dotrequirements cloud. Every place the CLI reaches the
3
+ * cloud asks here for the address, so the first use in a run is visible — the
4
+ * version-message check (CLI-MSG-1) starts there.
5
+ */
6
+ /** The Convex backend every user's CLI talks to. */
7
+ const CONVEX_URL = "https://data.dotrequirements.io";
8
+ /** The web app, home of the hosted AI endpoints and publishing. */
9
+ export const DEFAULT_API_BASE_URL = "https://app.dotrequirements.io";
10
+ let onFirstUse = null;
11
+ let used = false;
12
+ function noteUse() {
13
+ if (used)
14
+ return;
15
+ used = true;
16
+ onFirstUse?.();
17
+ }
18
+ /** The Convex backend's address, for a ConvexHttpClient or its HTTP API. */
19
+ export function convexUrl() {
20
+ noteUse();
21
+ return CONVEX_URL;
22
+ }
23
+ /** The web app's address; `DOTREQUIREMENTS_API_URL` overrides it. */
24
+ export function webApiUrl() {
25
+ noteUse();
26
+ return process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
27
+ }
28
+ /** Run `callback` the first time this process reaches the cloud. */
29
+ export function whenCloudFirstUsed(callback) {
30
+ onFirstUse = callback;
31
+ if (used)
32
+ callback();
33
+ }
34
+ //# sourceMappingURL=cloud.js.map
@@ -1,9 +1,9 @@
1
+ import { convexUrl } from "../cloud.js";
1
2
  import { getProjectContext } from "../requirements/cloud-ai.js";
2
3
  import { loadAllRequirements } from "../requirements/index.js";
3
4
  import { generateStyleGuide } from "../requirements/style-guide.js";
4
5
  import { readLocalStyleGuide } from "../requirements/style-guide-file.js";
5
6
  import { findProjectRoot, getProjectCredentials, } from "../utils/project-settings.js";
6
- const CONVEX_URL = "https://data.dotrequirements.io";
7
7
  export async function createRequirementDocumentCommand(filePath) {
8
8
  const workspaceRoot = process.cwd();
9
9
  // CLI-CREATE-DOC-1.1 / .2: discover existing patterns from the workspace.
@@ -24,7 +24,7 @@ export async function createRequirementDocumentCommand(filePath) {
24
24
  if (projectRoot) {
25
25
  try {
26
26
  const { projectId, projectSecret } = getProjectCredentials(workspaceRoot);
27
- const contextData = await getProjectContext(projectId, projectSecret, CONVEX_URL);
27
+ const contextData = await getProjectContext(projectId, projectSecret, convexUrl());
28
28
  customStyleGuidance = contextData?.requirementsStyleContext ?? null;
29
29
  }
30
30
  catch {
@@ -1,12 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { ConvexHttpClient } from "convex/browser";
4
- import { getConvexUrl } from "../config.js";
4
+ import { convexUrl } from "../cloud.js";
5
5
  import { api } from "../convex.js";
6
6
  import { generateStyleGuideBody } from "../requirements/style-guide.js";
7
7
  import { generateExampleRequirements } from "../templates/example-requirements.js";
8
8
  import { generateRequirementsReadme } from "../templates/requirements-readme.js";
9
9
  import { brand } from "../utils/brand.js";
10
+ import { commandErrorMessage } from "../utils/command-error.js";
10
11
  import { ensureGitignore } from "../utils/gitignore.js";
11
12
  import { executeOAuthFlow } from "../utils/oauth-flow.js";
12
13
  import { detectProjectName } from "../utils/project-name.js";
@@ -69,7 +70,7 @@ export async function initCommand(options = {}) {
69
70
  }
70
71
  }
71
72
  catch (error) {
72
- console.error(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`);
73
+ console.error(`\n✗ Error: ${commandErrorMessage(error)}`);
73
74
  // INIT-11: Network error guidance
74
75
  if (error instanceof Error && error.message.includes("fetch")) {
75
76
  console.error("\nCheck your internet connection and try again.");
@@ -130,7 +131,7 @@ async function authenticatedFlow(cwd, options) {
130
131
  return false;
131
132
  }
132
133
  // Set up authenticated client using the session token
133
- const client = new ConvexHttpClient(getConvexUrl());
134
+ const client = new ConvexHttpClient(convexUrl());
134
135
  client.setAuth(oauthResult.accessToken);
135
136
  // INIT-18: Select team
136
137
  const selectedTeam = await selectTeam(client);
@@ -216,9 +217,9 @@ async function selectTeam(client) {
216
217
  */
217
218
  async function chooseProjectAction(client, teamId) {
218
219
  // INIT-7.3: Check if team has existing projects the user can access
219
- const projectsWithSecrets = (await client.query(api.projectSecrets.queries.listOwnSecretsForTeam,
220
+ const projectsWithSecrets = (await client.query(api.projectSecrets.queries.listOwnForTeam,
220
221
  // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id<"teams"> type; the runtime value is a plain string the convex server validates.
221
- { target: { type: "team", id: teamId } }));
222
+ { teamId: teamId }));
222
223
  if (projectsWithSecrets.length === 0) {
223
224
  // No existing projects, only offer create
224
225
  const confirmed = await promptConfirm("This team has no projects. Create a new one?", true);
@@ -315,7 +316,7 @@ async function connectExistingProjectFlow(cwd, client, team) {
315
316
  */
316
317
  async function inviteFlow(cwd, token) {
317
318
  // MEMBERSHIP-18.0: Validate token before auth (no auth needed for this query)
318
- const client = new ConvexHttpClient(getConvexUrl());
319
+ const client = new ConvexHttpClient(convexUrl());
319
320
  console.log("Validating invite token...");
320
321
  const inviteInfo = await client.query(api.teamInvites.queries.getInviteInfo, {
321
322
  token,
@@ -1,15 +1,17 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { ConvexHttpClient } from "convex/browser";
4
- import { getConvexUrl } from "../config.js";
4
+ import { convexUrl } from "../cloud.js";
5
5
  import { api } from "../convex.js";
6
6
  import { WEB_APP_URL } from "../push/index.js";
7
7
  import { brand } from "../utils/brand.js";
8
+ import { commandErrorMessage } from "../utils/command-error.js";
8
9
  import { ensureGitignore } from "../utils/gitignore.js";
9
10
  import { executeOAuthFlow } from "../utils/oauth-flow.js";
10
11
  import { getOrCreateProjectSecret, promptExpiryDays, selectTeam, } from "../utils/project-selector.js";
11
12
  import { findProjectRoot, readProjectSettings, writeProjectSettings, } from "../utils/project-settings.js";
12
13
  import { promptChoice, promptConfirm } from "../utils/prompts.js";
14
+ import { markVersionMessagesShown, versionMessages, } from "../version-messages.js";
13
15
  import { hasCapacity, isNonInteractive, resolveProject, resolveProjectName, resolveTeam, } from "./link-resolution.js";
14
16
  import { syncCommand } from "./sync.js";
15
17
  /**
@@ -47,10 +49,13 @@ async function runNonInteractiveLink(options) {
47
49
  if (options.json) {
48
50
  console.log = (...args) => console.error(...args);
49
51
  }
52
+ // CLI-MSG-1.1: the JSON result carries the cloud's messages, so it is
53
+ // printed once the command is done and they are in.
54
+ let jsonOutcome = null;
50
55
  const finish = (outcome) => {
51
56
  console.log = originalLog;
52
57
  if (options.json) {
53
- originalLog(JSON.stringify(outcome));
58
+ jsonOutcome = outcome;
54
59
  }
55
60
  else if (outcome.status === "decision_needed") {
56
61
  console.log(`\n${outcome.message}`);
@@ -101,7 +106,7 @@ async function runNonInteractiveLink(options) {
101
106
  }
102
107
  // LINK-3 / LINK-10.7: browser auth is the only human step
103
108
  const oauthResult = await executeOAuthFlow();
104
- const client = new ConvexHttpClient(getConvexUrl());
109
+ const client = new ConvexHttpClient(convexUrl());
105
110
  client.setAuth(oauthResult.accessToken);
106
111
  await client.mutation(api.teams.mutations.ensureDefaultTeam, {});
107
112
  const teams = (await client.query(api.teams.queries.listForUserWithUsage));
@@ -201,12 +206,9 @@ async function runNonInteractiveLink(options) {
201
206
  if (projectResolution.rename) {
202
207
  const newName = resolveProjectName(options, readPackageName(cwd), path.basename(cwd));
203
208
  try {
204
- await client.mutation(api.projects.mutations.update, {
205
- target: {
206
- type: "project",
207
- // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
208
- id: projectResolution.project.projectId,
209
- },
209
+ await client.mutation(api.projects.mutations.updateProject, {
210
+ // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
211
+ projectId: projectResolution.project.projectId,
210
212
  name: newName,
211
213
  });
212
214
  projectName = newName;
@@ -234,13 +236,17 @@ async function runNonInteractiveLink(options) {
234
236
  });
235
237
  }
236
238
  catch (error) {
237
- finish({
238
- status: "error",
239
- message: error instanceof Error ? error.message : String(error),
240
- });
239
+ finish({ status: "error", message: commandErrorMessage(error) });
241
240
  }
242
241
  finally {
243
242
  console.log = originalLog;
243
+ // Here, not after the block: every outcome above returns from inside
244
+ // `try`. Assigned inside `finish`, which TypeScript can't follow.
245
+ const outcome = jsonOutcome;
246
+ if (outcome) {
247
+ originalLog(JSON.stringify({ ...outcome, messages: await versionMessages() }));
248
+ markVersionMessagesShown();
249
+ }
244
250
  }
245
251
  }
246
252
  /**
@@ -253,18 +259,18 @@ async function runNonInteractiveLink(options) {
253
259
  async function mintShareArtifacts(client, teamId, projectId) {
254
260
  const artifacts = {};
255
261
  try {
256
- const invite = (await client.mutation(api.teamInvites.mutations.createInvite,
262
+ const invite = (await client.mutation(api.teamInvites.mutations.createInviteLink,
257
263
  // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
258
- { target: { type: "team", id: teamId } }));
264
+ { teamId: teamId }));
259
265
  artifacts.inviteUrl = `${WEB_APP_URL}/invite/${invite.token}`;
260
266
  }
261
267
  catch {
262
268
  // LINK-13.3: admin-gated — omit the invite URL for non-admin members
263
269
  }
264
270
  try {
265
- const share = (await client.mutation(api.projectSecrets.mutations.ensureShareToken,
271
+ const share = (await client.mutation(api.projectSecrets.mutations.ensureProjectShareToken,
266
272
  // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
267
- { target: { type: "project", id: projectId } }));
273
+ { projectId: projectId }));
268
274
  artifacts.shareSyncCommand = `npx -y @popoverai/dotrequirements@latest sync --share ${share.token}`;
269
275
  }
270
276
  catch {
@@ -273,9 +279,9 @@ async function mintShareArtifacts(client, teamId, projectId) {
273
279
  return artifacts;
274
280
  }
275
281
  async function listProjects(client, teamId) {
276
- const projects = (await client.query(api.projectSecrets.queries.listOwnSecretsForTeam,
282
+ const projects = (await client.query(api.projectSecrets.queries.listOwnForTeam,
277
283
  // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
278
- { target: { type: "team", id: teamId } }));
284
+ { teamId: teamId }));
279
285
  return projects.map((p) => ({
280
286
  projectId: p.projectId,
281
287
  projectName: p.projectName,
@@ -349,7 +355,7 @@ async function runInteractiveLink(options) {
349
355
  return;
350
356
  }
351
357
  // Set up authenticated client using the session token
352
- const client = new ConvexHttpClient(getConvexUrl());
358
+ const client = new ConvexHttpClient(convexUrl());
353
359
  client.setAuth(oauthResult.accessToken);
354
360
  // LINK-4: Select team
355
361
  const selectedTeam = await selectTeam(client);
@@ -417,7 +423,7 @@ async function runInteractiveLink(options) {
417
423
  console.log("\nDocumentation: https://docs.dotrequirements.io/getting-started");
418
424
  }
419
425
  catch (error) {
420
- console.error(`\n✗ Error: ${error instanceof Error ? error.message : String(error)}`);
426
+ console.error(`\n✗ Error: ${commandErrorMessage(error)}`);
421
427
  // LINK-8: Network error guidance
422
428
  if (error instanceof Error && error.message.includes("fetch")) {
423
429
  console.error("\nCheck your internet connection and try again.");
@@ -1,9 +1,10 @@
1
+ import { convexUrl } from "../cloud.js";
1
2
  import { findRequirementsDir, readCoverageCache } from "../harness/cache.js";
2
3
  import { formatUnrecordedNotice } from "../harness/reportingStatus.js";
3
4
  import { getProjectCoverage, getRequirementCoverage, } from "../requirements/cloud-coverage.js";
4
5
  import { buildLocalReport, } from "../requirements/coverage.js";
5
6
  import { findProjectRoot, getProjectCredentials, } from "../utils/project-settings.js";
6
- const CONVEX_URL = "https://data.dotrequirements.io";
7
+ import { markVersionMessagesShown, versionMessages, } from "../version-messages.js";
7
8
  export async function reportCommand(options) {
8
9
  const source = options.source ?? "local";
9
10
  const format = options.format ?? "console";
@@ -52,24 +53,28 @@ export async function reportCommand(options) {
52
53
  if (options.requirement) {
53
54
  // REPORT-CLOUD-1.7: --branch/--since apply to the requirement-scoped
54
55
  // output too, not just the project-wide report
55
- const record = await getRequirementCoverage(options.requirement, projectId, projectSecret, CONVEX_URL, {
56
+ const record = await getRequirementCoverage(options.requirement, projectId, projectSecret, convexUrl(), {
56
57
  branch: options.branch,
57
58
  sinceTimestamp: options.since,
58
59
  });
59
60
  console.log(printCloudRequirement(record, format, {
60
61
  branch: options.branch,
61
62
  since: options.since,
62
- }));
63
+ }, format === "json" ? await versionMessages() : []));
64
+ if (format === "json")
65
+ markVersionMessagesShown();
63
66
  return;
64
67
  }
65
- const record = await getProjectCoverage(projectId, projectSecret, CONVEX_URL, {
68
+ const record = await getProjectCoverage(projectId, projectSecret, convexUrl(), {
66
69
  branch: options.branch,
67
70
  sinceTimestamp: options.since,
68
71
  });
69
72
  console.log(printCloudProject(record, format, {
70
73
  branch: options.branch,
71
74
  since: options.since,
72
- }));
75
+ }, format === "json" ? await versionMessages() : []));
76
+ if (format === "json")
77
+ markVersionMessagesShown();
73
78
  }
74
79
  function printLocalReport(report, format) {
75
80
  switch (format) {
@@ -122,9 +127,10 @@ function formatLocalMarkdown(report) {
122
127
  }
123
128
  return out;
124
129
  }
125
- function printCloudRequirement(record, format, filters) {
130
+ function printCloudRequirement(record, format, filters, messages = []) {
126
131
  if (format === "json") {
127
- return JSON.stringify({ source: "cloud", requirement: record }, null, 2);
132
+ // CLI-MSG-1.1: the cloud's messages for this version travel in the JSON.
133
+ return JSON.stringify({ source: "cloud", requirement: record, messages }, null, 2);
128
134
  }
129
135
  if (!record.lastTestedAt) {
130
136
  // REPORT-CLOUD-1.7: with filters in play, an empty record means nothing
@@ -168,9 +174,16 @@ function printCloudRequirement(record, format, filters) {
168
174
  }
169
175
  return lines.join("\n");
170
176
  }
171
- function printCloudProject(record, format, filters) {
177
+ function printCloudProject(record, format, filters, messages = []) {
172
178
  if (format === "json") {
173
- return JSON.stringify({ source: "cloud", filters, summary: cloudSummary(record), ...record }, null, 2);
179
+ // CLI-MSG-1.1: the cloud's messages for this version travel in the JSON.
180
+ return JSON.stringify({
181
+ source: "cloud",
182
+ filters,
183
+ summary: cloudSummary(record),
184
+ ...record,
185
+ messages,
186
+ }, null, 2);
174
187
  }
175
188
  const summary = cloudSummary(record);
176
189
  const filterNote = formatCloudFilters(filters);
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { DEFAULT_API_BASE_URL, fetchReviewTestFeedback, } from "../requirements/cloud-ai.js";
3
+ import { webApiUrl } from "../cloud.js";
4
+ import { fetchReviewTestFeedback } from "../requirements/cloud-ai.js";
4
5
  import { findRequirementsInFile } from "../requirements/grep.js";
5
6
  import { formatRequirementTree, getRequirementTree, loadAllRequirements, } from "../requirements/index.js";
6
7
  import { getProjectCredentials } from "../utils/project-settings.js";
@@ -145,7 +146,7 @@ async function runCloudReview(params) {
145
146
  `(${error instanceof Error ? error.message : String(error)})`);
146
147
  }
147
148
  // CLI-REVIEW-3.0 / .1 / .3: dispatch to the hosted endpoint
148
- const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
149
+ const apiBaseUrl = webApiUrl();
149
150
  let feedback;
150
151
  let quotaWarning;
151
152
  try {
@@ -1,11 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { DEFAULT_API_BASE_URL, fetchStyleCheckFeedback, getProjectContext, } from "../requirements/cloud-ai.js";
3
+ import { convexUrl, webApiUrl } from "../cloud.js";
4
+ import { fetchStyleCheckFeedback, getProjectContext, } from "../requirements/cloud-ai.js";
4
5
  import { filterRequirementsByKeys, loadAllRequirements, } from "../requirements/index.js";
5
6
  import { generateStyleGuideBody } from "../requirements/style-guide.js";
6
7
  import { readLocalStyleGuide } from "../requirements/style-guide-file.js";
7
8
  import { findProjectRoot, getProjectCredentials, } from "../utils/project-settings.js";
8
- const CONVEX_URL = "https://data.dotrequirements.io";
9
9
  /**
10
10
  * Judgment instructions for test-file style review in local mode. Mirrors the
11
11
  * hosted test-style rubric (PROMPT-STYLE-TESTS-*): requirement() usage,
@@ -123,7 +123,7 @@ async function emitStyleMaterials(params) {
123
123
  if (projectRoot) {
124
124
  try {
125
125
  const { projectId, projectSecret } = getProjectCredentials(workspaceRoot);
126
- const contextData = await getProjectContext(projectId, projectSecret, CONVEX_URL);
126
+ const contextData = await getProjectContext(projectId, projectSecret, convexUrl());
127
127
  customStyleGuidance = contextData?.requirementsStyleContext ?? null;
128
128
  }
129
129
  catch {
@@ -170,7 +170,7 @@ async function runCloudStyleCheck(params) {
170
170
  `(${error instanceof Error ? error.message : String(error)})`);
171
171
  }
172
172
  // CLI-STYLE-3.0 / .1 / .3: dispatch to the hosted endpoint
173
- const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
173
+ const apiBaseUrl = webApiUrl();
174
174
  let feedback;
175
175
  let quotaWarning;
176
176
  try {
package/dist/convex.d.ts CHANGED
@@ -17,15 +17,13 @@
17
17
  export declare const api: {
18
18
  documents: {
19
19
  queries: {
20
- exportForCli: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
21
- exportProjectForCli: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
22
- checkDocumentsExist: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
20
+ exportDocument: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
21
+ exportProject: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
22
+ exportProjectForShare: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
23
23
  };
24
24
  mutations: {
25
- create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
26
- update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
27
- deleteForCli: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
28
- setAttachmentsForSync: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
25
+ deleteForSync: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
26
+ syncAttachments: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
29
27
  };
30
28
  saveWithRequirements: {
31
29
  saveWithRequirements: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
@@ -39,7 +37,7 @@ export declare const api: {
39
37
  projects: {
40
38
  mutations: {
41
39
  create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
42
- update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
40
+ updateProject: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
43
41
  };
44
42
  };
45
43
  teams: {
@@ -55,19 +53,19 @@ export declare const api: {
55
53
  getInviteInfo: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
56
54
  };
57
55
  mutations: {
58
- createInvite: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
56
+ createInviteLink: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
59
57
  acceptInvite: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
60
58
  };
61
59
  };
62
60
  projectSecrets: {
63
61
  queries: {
64
62
  getOwnSecret: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
65
- listOwnSecretsForTeam: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
63
+ listOwnForTeam: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
66
64
  };
67
65
  mutations: {
68
- ensureOwnSecret: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
66
+ ensureOwn: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
69
67
  revokeOwnSecret: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
70
- ensureShareToken: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
68
+ ensureProjectShareToken: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
71
69
  };
72
70
  };
73
71
  lib: {
package/dist/convex.js CHANGED
@@ -21,17 +21,16 @@ const action = (name) => makeFunctionReference(name);
21
21
  export const api = {
22
22
  documents: {
23
23
  queries: {
24
- exportForCli: query("documents/queries:exportForCli"),
25
- exportProjectForCli: query("documents/queries:exportProjectForCli"),
26
- checkDocumentsExist: query("documents/queries:checkDocumentsExist"),
24
+ exportDocument: query("documents/queries:exportDocument"),
25
+ exportProject: query("documents/queries:exportProject"),
26
+ // SHARE-TOKEN-3: the same export for a read-only share token
27
+ exportProjectForShare: query("documents/queries:exportProjectForShare"),
27
28
  },
28
29
  mutations: {
29
- create: mutation("documents/mutations:create"),
30
- update: mutation("documents/mutations:update"),
31
30
  // SYNC-MODE-3: delete a cloud document from the CLI (repo-wins)
32
- deleteForCli: mutation("documents/mutations:deleteForCli"),
31
+ deleteForSync: mutation("documents/mutations:deleteForSync"),
33
32
  // ATTACH-10.2: attachment-only sync — a metadata patch, never a publish
34
- setAttachmentsForSync: mutation("documents/mutations:setAttachmentsForSync"),
33
+ syncAttachments: mutation("documents/mutations:syncAttachments"),
35
34
  },
36
35
  // SYNC-ARCH-1: Save document with requirements derivation
37
36
  saveWithRequirements: {
@@ -46,7 +45,7 @@ export const api = {
46
45
  projects: {
47
46
  mutations: {
48
47
  create: mutation("projects/mutations:create"),
49
- update: mutation("projects/mutations:update"),
48
+ updateProject: mutation("projects/mutations:updateProject"),
50
49
  },
51
50
  },
52
51
  teams: {
@@ -62,7 +61,7 @@ export const api = {
62
61
  getInviteInfo: query("teamInvites/queries:getInviteInfo"),
63
62
  },
64
63
  mutations: {
65
- createInvite: mutation("teamInvites/mutations:createInvite"),
64
+ createInviteLink: mutation("teamInvites/mutations:createInviteLink"),
66
65
  // Joins the team and reconciles paid-team seats via a scheduled sync
67
66
  // (POLAR-SEATS-3). Replaced the deleted polar:acceptTeamInvite action.
68
67
  acceptInvite: mutation("teamInvites/mutations:acceptInvite"),
@@ -71,12 +70,12 @@ export const api = {
71
70
  projectSecrets: {
72
71
  queries: {
73
72
  getOwnSecret: query("projectSecrets/queries:getOwnSecret"),
74
- listOwnSecretsForTeam: query("projectSecrets/queries:listOwnSecretsForTeam"),
73
+ listOwnForTeam: query("projectSecrets/queries:listOwnForTeam"),
75
74
  },
76
75
  mutations: {
77
- ensureOwnSecret: mutation("projectSecrets/mutations:ensureOwnSecret"),
76
+ ensureOwn: mutation("projectSecrets/mutations:ensureOwn"),
78
77
  revokeOwnSecret: mutation("projectSecrets/mutations:revokeOwnSecret"),
79
- ensureShareToken: mutation("projectSecrets/mutations:ensureShareToken"),
78
+ ensureProjectShareToken: mutation("projectSecrets/mutations:ensureProjectShareToken"),
80
79
  },
81
80
  },
82
81
  lib: {
@@ -11,8 +11,10 @@
11
11
  */
12
12
  import { execSync } from "node:child_process";
13
13
  import { randomUUID } from "node:crypto";
14
+ import { convexUrl } from "../cloud.js";
14
15
  import { canonicalRequirementPath } from "../schema/index.js";
15
16
  import { getProjectInfo } from "../utils/project-settings.js";
17
+ import { enableVersionMessages, formatVersionMessages, markVersionMessagesShown, versionMessages, } from "../version-messages.js";
16
18
  import { cleanupTestRunId, clearCoverageReportFailure, deleteTrackingFile, findProjectRoot, findRequirementsDir, getTestRunId, needsReporting, readCoverageCache, readLookupCache, readTrackingEntries, recordCoverageReplayed, recordCoverageReported, recordCoverageReportFailure, updateCoverageCache, } from "./cache.js";
17
19
  import { formatFailureNotice, formatUnrecordedNotice, refusalReason, } from "./reportingStatus.js";
18
20
  import { toProjectRelativePath } from "./tracking.js";
@@ -130,10 +132,6 @@ function printLocalReport(testedKeys, lookup, options) {
130
132
  console.log(report);
131
133
  }
132
134
  }
133
- /**
134
- * Production Convex deployment URL
135
- */
136
- const CONVEX_URL = "https://data.dotrequirements.io";
137
135
  /**
138
136
  * Record a reporting attempt that did not reach the cloud, so later runs and
139
137
  * `dotreq report` keep saying so (COVERAGE-REPORT-3), and hand the reason back
@@ -190,18 +188,18 @@ const REPORT_TIMEOUT_MS = 15_000;
190
188
  */
191
189
  async function postCoverage(credentials, report) {
192
190
  // Note: projectId from env is always a slug (e.g., "reduced-cephalopod-288"),
193
- // not a Convex ID. Use projectSlug field for authentication and slug for target.
194
- const response = await fetch(`${CONVEX_URL}/api/mutation`, {
191
+ // not a Convex ID. It names the project both to authenticate and as the report's project.
192
+ const response = await fetch(`${convexUrl()}/api/mutation`, {
195
193
  method: "POST",
196
194
  headers: { "Content-Type": "application/json" },
197
195
  body: JSON.stringify({
198
- path: "testCoverage/mutations:recordCoverage",
196
+ path: "testCoverage/mutations:record",
199
197
  args: {
200
198
  projectAuth: {
201
199
  projectSlug: credentials.projectId,
202
200
  projectSecret: credentials.projectSecret,
203
201
  },
204
- target: { type: "project", slug: credentials.projectId },
202
+ project: credentials.projectId,
205
203
  branch: report.branch,
206
204
  context: report.context,
207
205
  runId: report.runId,
@@ -528,6 +526,9 @@ export async function finalize(options = {}) {
528
526
  count: 0,
529
527
  };
530
528
  if (shouldReportToCloud) {
529
+ // CLI-MSG-1.4: a run that reports to the cloud hears what it has to say
530
+ // to this version.
531
+ enableVersionMessages();
531
532
  cloudResult = await reportToConvex(projectRoot, testRunId, aggregated, showCloudStatus, context);
532
533
  }
533
534
  // Read once, after reporting has had its say, for both the report header and
@@ -551,6 +552,13 @@ export async function finalize(options = {}) {
551
552
  (recordedThisRun ? null : coverageCache?.lastFailure)),
552
553
  });
553
554
  }
555
+ // CLI-MSG-1.4: the summary closes with the cloud's messages for this
556
+ // version — ahead of the notice below, which keeps the last word.
557
+ const messages = await versionMessages();
558
+ if (messages.length > 0) {
559
+ console.error(`${formatVersionMessages(messages)}\n`);
560
+ }
561
+ markVersionMessagesShown();
554
562
  // COVERAGE-REPORT-2.1, -3.0: the run's last word is whether coverage
555
563
  // recorded. Written to stderr so it cannot be silenced by a quiet run or
556
564
  // swallowed by a caller parsing stdout — a failure nobody sees is the whole
@@ -5,6 +5,7 @@
5
5
  * COVERAGE-REPORT-2 / -3 (the run says coverage did not record, and keeps
6
6
  * saying it until it does).
7
7
  */
8
+ import { userFacingMessage } from "../utils/command-error.js";
8
9
  /**
9
10
  * Shown when a refusal carries nothing a developer could act on
10
11
  * (COVERAGE-REPORT-1.2). The raw `errorMessage` is deliberately not used in its
@@ -19,17 +20,7 @@ export const REFUSAL_WITHOUT_REASON = "the cloud refused the report without givi
19
20
  * ConvexError's data is either a plain string or an object carrying `message`.
20
21
  */
21
22
  export function refusalReason(response) {
22
- const data = response.errorData;
23
- if (typeof data === "string" && data.trim()) {
24
- return data;
25
- }
26
- if (data && typeof data === "object" && "message" in data) {
27
- const { message } = data;
28
- if (typeof message === "string" && message.trim()) {
29
- return message;
30
- }
31
- }
32
- return REFUSAL_WITHOUT_REASON;
23
+ return userFacingMessage(response.errorData) ?? REFUSAL_WITHOUT_REASON;
33
24
  }
34
25
  const MONTHS = [
35
26
  "January",
@@ -7,12 +7,6 @@
7
7
  * throws on failure). Surface-specific concerns (error formatting, exit
8
8
  * codes, MCP tool responses) live in the callers.
9
9
  */
10
- /**
11
- * Default base URL for the hosted AI endpoints (`/api/style-check`,
12
- * `/api/review-test`). Overridable via the `apiBaseUrl` argument or
13
- * the `DOTREQUIREMENTS_API_URL` environment variable in callers.
14
- */
15
- export declare const DEFAULT_API_BASE_URL = "https://app.dotrequirements.io";
16
10
  export interface StyleCheckParams {
17
11
  apiBaseUrl: string;
18
12
  projectId: string;
@@ -7,12 +7,6 @@
7
7
  * throws on failure). Surface-specific concerns (error formatting, exit
8
8
  * codes, MCP tool responses) live in the callers.
9
9
  */
10
- /**
11
- * Default base URL for the hosted AI endpoints (`/api/style-check`,
12
- * `/api/review-test`). Overridable via the `apiBaseUrl` argument or
13
- * the `DOTREQUIREMENTS_API_URL` environment variable in callers.
14
- */
15
- export const DEFAULT_API_BASE_URL = "https://app.dotrequirements.io";
16
10
  /**
17
11
  * POST to `/api/style-check`. Returns the feedback (and any quota warning)
18
12
  * on success; throws an Error with the underlying failure reason on a
@@ -73,16 +67,13 @@ export async function getProjectContext(projectId, projectSecret, convexUrl) {
73
67
  method: "POST",
74
68
  headers: { "Content-Type": "application/json" },
75
69
  body: JSON.stringify({
76
- path: "projects/queries:get",
70
+ path: "projects/queries:getProject",
77
71
  args: {
78
72
  projectAuth: {
79
73
  projectSlug: projectId,
80
74
  projectSecret,
81
75
  },
82
- target: {
83
- type: "project",
84
- slug: projectId,
85
- },
76
+ project: projectId,
86
77
  },
87
78
  format: "json",
88
79
  }),
@@ -19,10 +19,10 @@ export async function getRequirementCoverage(requirementKey, projectId, projectS
19
19
  method: "POST",
20
20
  headers: { "Content-Type": "application/json" },
21
21
  body: JSON.stringify({
22
- path: "testCoverage/queries:getRequirementCoverage",
22
+ path: "testCoverage/queries:requirementCoverage",
23
23
  args: {
24
24
  projectAuth: { projectSlug: projectId, projectSecret },
25
- target: { type: "project", slug: projectId },
25
+ project: projectId,
26
26
  requirementKey,
27
27
  },
28
28
  format: "json",
@@ -68,10 +68,10 @@ export async function getProjectCoverage(projectId, projectSecret, convexUrl, op
68
68
  method: "POST",
69
69
  headers: { "Content-Type": "application/json" },
70
70
  body: JSON.stringify({
71
- path: "testCoverage/queries:getProjectCoverage",
71
+ path: "testCoverage/queries:projectCoverage",
72
72
  args: {
73
73
  projectAuth: { projectSlug: projectId, projectSecret },
74
- target: { type: "project", slug: projectId },
74
+ project: projectId,
75
75
  branch: options?.branch,
76
76
  sinceTimestamp: options?.sinceTimestamp,
77
77
  },
@@ -10,10 +10,11 @@
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
12
  import { ConvexHttpClient } from "convex/browser";
13
- import { getConvexUrl } from "../config.js";
13
+ import { convexUrl } from "../cloud.js";
14
14
  import { api } from "../convex.js";
15
15
  import { parseFilesForPushIndividually, WEB_APP_URL } from "../push/core.js";
16
16
  import { composeMarkdownWithTitle, splitLeadingH1, writeRequirementsFile, } from "../schema/index.js";
17
+ import { commandErrorMessage } from "../utils/command-error.js";
17
18
  import { cloudAttachmentRemovals, resolveAttachments } from "./attachments.js";
18
19
  import { resolveLocalPath } from "./local-files.js";
19
20
  import { publishToCloud } from "./publish.js";
@@ -46,7 +47,7 @@ function emptyOutcome() {
46
47
  */
47
48
  export async function executePlan(plan, cloud, auth, workspaceRoot, mode) {
48
49
  const outcome = emptyOutcome();
49
- const client = new ConvexHttpClient(getConvexUrl());
50
+ const client = new ConvexHttpClient(convexUrl());
50
51
  const cloudById = new Map(cloud.documents.map((d) => [d.documentId, d]));
51
52
  const requirementsDir = path.join(workspaceRoot, ".requirements");
52
53
  const usedPaths = new Set();
@@ -83,14 +84,14 @@ export async function executePlan(plan, cloud, auth, workspaceRoot, mode) {
83
84
  await syncResidualAttachments(client, auth, doc, action, outcome, mode);
84
85
  }
85
86
  catch (err) {
86
- outcome.errors.push({ name, error: errorDisplayMessage(err) });
87
+ outcome.errors.push({ name, error: commandErrorMessage(err) });
87
88
  }
88
89
  }
89
90
  return outcome;
90
91
  }
91
92
  /**
92
93
  * Attachment-only operations for whatever the body action didn't cover.
93
- * The push goes through setAttachmentsForSync — a metadata patch, never the
94
+ * The push goes through syncAttachments — a metadata patch, never the
94
95
  * publish path, so naming a link cannot displace anyone's working document.
95
96
  */
96
97
  async function syncResidualAttachments(client, auth, doc, action, outcome, mode) {
@@ -110,12 +111,11 @@ async function syncResidualAttachments(client, auth, doc, action, outcome, mode)
110
111
  if (!auth.projectSlug) {
111
112
  throw new Error("Cannot sync attachments without project credentials.");
112
113
  }
113
- await client.mutation(api.documents.mutations.setAttachmentsForSync, {
114
+ await client.mutation(api.documents.mutations.syncAttachments, {
114
115
  projectAuth: {
115
116
  projectSlug: auth.projectSlug,
116
117
  projectSecret: auth.projectSecret,
117
118
  },
118
- target: { type: "project", slug: auth.projectSlug },
119
119
  documentId: doc.documentId,
120
120
  attachments: res.cloudFinal,
121
121
  // Compare-and-set: the resolution was computed from the snapshot's
@@ -161,17 +161,6 @@ async function syncResidualAttachments(client, auth, doc, action, outcome, mode)
161
161
  outcome.attachmentWrites++;
162
162
  }
163
163
  }
164
- /**
165
- * Servers throw ConvexError({kind, message}) for expected failures; production
166
- * redacts the Error message to "Server Error" but preserves error.data.
167
- * Surface the human-readable message rather than the blob.
168
- */
169
- function errorDisplayMessage(err) {
170
- const data = err.data;
171
- if (data && typeof data.message === "string")
172
- return data.message;
173
- return err instanceof Error ? err.message : String(err);
174
- }
175
164
  async function cloudWrite(auth, doc, cloudById, outcome, mode) {
176
165
  if (!auth.projectSlug || !doc.filePath) {
177
166
  throw new Error("Cannot write to the cloud without project credentials.");
@@ -361,12 +350,11 @@ async function cloudDelete(client, auth, doc, outcome) {
361
350
  if (!auth.projectSlug || !doc.documentId) {
362
351
  throw new Error("Cannot delete from the cloud without project credentials.");
363
352
  }
364
- await client.mutation(api.documents.mutations.deleteForCli, {
353
+ await client.mutation(api.documents.mutations.deleteForSync, {
365
354
  projectAuth: {
366
355
  projectSlug: auth.projectSlug,
367
356
  projectSecret: auth.projectSecret,
368
357
  },
369
- target: { type: "project", slug: auth.projectSlug },
370
358
  documentId: doc.documentId,
371
359
  });
372
360
  outcome.cloudDeleted++;
@@ -11,7 +11,7 @@
11
11
  * repo unable to push. Recoverable rather than lossy — see
12
12
  * docs/working/publishing-and-unpublished-changes.md.
13
13
  */
14
- import { DEFAULT_API_BASE_URL } from "../requirements/cloud-ai.js";
14
+ import { webApiUrl } from "../cloud.js";
15
15
  /**
16
16
  * POST to `/api/publish`. Returns the saved document; throws with the server's
17
17
  * own message on failure, which is what Jaime reads in the sync output.
@@ -21,7 +21,7 @@ import { DEFAULT_API_BASE_URL } from "../requirements/cloud-ai.js";
21
21
  * apply next time.
22
22
  */
23
23
  export async function publishToCloud(params) {
24
- const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
24
+ const apiBaseUrl = webApiUrl();
25
25
  const response = await fetch(`${apiBaseUrl}/api/publish`, {
26
26
  method: "POST",
27
27
  headers: { "Content-Type": "application/json" },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Snapshot acquisition for the comparator — kept separate from comparison so
3
3
  * the comparator stays a pure function (compare.ts). The local half reads the
4
- * workspace; the cloud half is one exportProjectForCli query.
4
+ * workspace; the cloud half is one export query.
5
5
  */
6
6
  import type { CloudSnapshot, LocalDocument, LocalSnapshot } from "./types.js";
7
7
  /**
@@ -22,9 +22,9 @@ export interface CloudAuth {
22
22
  projectSecret: string;
23
23
  }
24
24
  /**
25
- * Build the cloud snapshot with one exportProjectForCli query. A share token is
26
- * passed as the projectSecret with no slug (the read-only wrapper resolves the
27
- * project from the token) — the read-only path never writes back.
25
+ * Build the cloud snapshot with one export query: the project's own, with a
26
+ * project secret, or the share endpoint, with a bare share token (the token
27
+ * names the project) — the read-only path never writes back.
28
28
  */
29
29
  export declare function acquireCloudSnapshot(auth: CloudAuth): Promise<CloudSnapshot>;
30
30
  //# sourceMappingURL=snapshot.d.ts.map
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Snapshot acquisition for the comparator — kept separate from comparison so
3
3
  * the comparator stays a pure function (compare.ts). The local half reads the
4
- * workspace; the cloud half is one exportProjectForCli query.
4
+ * workspace; the cloud half is one export query.
5
5
  */
6
6
  import * as fs from "node:fs";
7
7
  import { ConvexHttpClient } from "convex/browser";
8
8
  import YAML from "yaml";
9
- import { getConvexUrl } from "../config.js";
9
+ import { convexUrl } from "../cloud.js";
10
10
  import { api } from "../convex.js";
11
11
  import { findRequirementsFiles } from "../requirements/index.js";
12
12
  import { splitLeadingH1, validateMetadata } from "../schema/index.js";
@@ -120,18 +120,23 @@ export async function acquireLocalSnapshot(workspaceRoot, filePaths) {
120
120
  return { documents: paths.map(readLocalDocument) };
121
121
  }
122
122
  /**
123
- * Build the cloud snapshot with one exportProjectForCli query. A share token is
124
- * passed as the projectSecret with no slug (the read-only wrapper resolves the
125
- * project from the token) — the read-only path never writes back.
123
+ * Build the cloud snapshot with one export query: the project's own, with a
124
+ * project secret, or the share endpoint, with a bare share token (the token
125
+ * names the project) — the read-only path never writes back.
126
126
  */
127
127
  export async function acquireCloudSnapshot(auth) {
128
- const client = new ConvexHttpClient(getConvexUrl());
129
- const projectAuth = auth.projectSlug
130
- ? { projectSlug: auth.projectSlug, projectSecret: auth.projectSecret }
131
- : { projectSecret: auth.projectSecret };
132
- const result = (await client.query(api.documents.queries.exportProjectForCli, {
133
- projectAuth,
134
- }));
128
+ const client = new ConvexHttpClient(convexUrl());
129
+ const result = (await (auth.projectSlug
130
+ ? client.query(api.documents.queries.exportProject, {
131
+ projectAuth: {
132
+ projectSlug: auth.projectSlug,
133
+ projectSecret: auth.projectSecret,
134
+ },
135
+ project: auth.projectSlug,
136
+ })
137
+ : client.query(api.documents.queries.exportProjectForShare, {
138
+ shareToken: auth.projectSecret,
139
+ })));
135
140
  return {
136
141
  projectName: result.projectName,
137
142
  projectSlug: result.projectSlug,
@@ -34,7 +34,7 @@ export interface LocalDocument {
34
34
  parseError?: string;
35
35
  }
36
36
  /**
37
- * One published document as it exists in the cloud, from exportProjectForCli.
37
+ * One published document as it exists in the cloud, from the project export.
38
38
  */
39
39
  export interface CloudDocument {
40
40
  documentId: string;
@@ -26,8 +26,8 @@ HOWTO-1: How to use dotrequirements
26
26
  2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('HOWTO-1.nesting').
27
27
  2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('HOWTO-1.several-words')
28
28
  3. → While you _can_ write your requirements longhand in this format, there are a number of better ways.
29
- 3.0. MCP → The dotrequirements MCP equips an AI assistant to develop requirements with you
30
- 3.1. Your current tools → Dotrequirements has a composer for Jira, Confluence, and Notion
29
+ 3.0. Coding assistants → Run \`dotreq ai-setup\` and your coding assistant develops requirements with you
30
+ 3.1. Chat → Connect Claude or ChatGPT to dot•requirements and write specs in the conversation
31
31
  3.2. Our composer → You can use our requirements composer at app.dotrequirements.io
32
32
  4. Have fun → Especially in the time you get back from having a more stable, transparent, and AI-friendly codebase
33
33
  \`\`\`
@@ -29,8 +29,8 @@ HOWTO-1: How to use dotrequirements
29
29
  2.0. Nesting → You can keep nesting. Label references don't need to be nested: requirement('HOWTO-1.nesting').
30
30
  2.1. Several words → Reference multi-word labels using kebab-case. Label references are never case sensitive: requirement('HOWTO-1.several-words')
31
31
  3. → While you _can_ write your requirements longhand in this format, there are a number of better ways.
32
- 3.0. MCP → The dotrequirements MCP equips an AI assistant to develop requirements with you
33
- 3.1. Your current tools → Dotrequirements has a composer for Jira, Confluence, and Notion
32
+ 3.0. Coding assistants → Run \`dotreq ai-setup\` and your coding assistant develops requirements with you
33
+ 3.1. Chat → Connect Claude or ChatGPT to dot•requirements and write specs in the conversation
34
34
  3.2. Our composer → You can use our requirements composer at app.dotrequirements.io
35
35
  4. Have fun → Especially in the time you get back from having a more stable, transparent, and AI-friendly codebase
36
36
  \`\`\`
@@ -0,0 +1,19 @@
1
+ /**
2
+ * What a failed command tells Sam (ERR-6).
3
+ *
4
+ * Production Convex replaces the message of every server error with
5
+ * "[Request ID: …] Server Error" and keeps only a ConvexError's `data`, which
6
+ * is where the backend puts the sentence written for users. So a refusal is
7
+ * read from its data; a server failure that carries none becomes a fixed
8
+ * sentence with its request ID; anything else is an error raised on Sam's
9
+ * machine, whose own message is already meant to be read.
10
+ */
11
+ /**
12
+ * The user-facing message a ConvexError's data carries, if any: the data is
13
+ * either a plain string or an object with a `message`. Mirrors
14
+ * `packages/web/lib/convex-error.ts`.
15
+ */
16
+ export declare function userFacingMessage(data: unknown): string | undefined;
17
+ /** The text to print for an error that ended a command. */
18
+ export declare function commandErrorMessage(error: unknown): string;
19
+ //# sourceMappingURL=command-error.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * What a failed command tells Sam (ERR-6).
3
+ *
4
+ * Production Convex replaces the message of every server error with
5
+ * "[Request ID: …] Server Error" and keeps only a ConvexError's `data`, which
6
+ * is where the backend puts the sentence written for users. So a refusal is
7
+ * read from its data; a server failure that carries none becomes a fixed
8
+ * sentence with its request ID; anything else is an error raised on Sam's
9
+ * machine, whose own message is already meant to be read.
10
+ */
11
+ /**
12
+ * The user-facing message a ConvexError's data carries, if any: the data is
13
+ * either a plain string or an object with a `message`. Mirrors
14
+ * `packages/web/lib/convex-error.ts`.
15
+ */
16
+ export function userFacingMessage(data) {
17
+ if (typeof data === "string" && data.trim())
18
+ return data;
19
+ if (data && typeof data === "object" && "message" in data) {
20
+ const { message } = data;
21
+ if (typeof message === "string" && message.trim())
22
+ return message;
23
+ }
24
+ return undefined;
25
+ }
26
+ const SERVER_ERROR = /\bServer Error\b/;
27
+ const REQUEST_ID = /Request ID: ([\w-]+)/;
28
+ /** The text to print for an error that ended a command. */
29
+ export function commandErrorMessage(error) {
30
+ if (error && typeof error === "object" && "data" in error) {
31
+ const message = userFacingMessage(error.data);
32
+ if (message)
33
+ return message;
34
+ }
35
+ const raw = error instanceof Error ? error.message : String(error);
36
+ if (SERVER_ERROR.test(raw)) {
37
+ const requestId = REQUEST_ID.exec(raw)?.[1];
38
+ return requestId
39
+ ? `Something went wrong in dotrequirements' cloud (request ID ${requestId}). Try again, and if it keeps happening, email support@dotrequirements.io with the request ID.`
40
+ : "Something went wrong in dotrequirements' cloud. Try again, and if it keeps happening, email support@dotrequirements.io.";
41
+ }
42
+ return raw;
43
+ }
44
+ //# sourceMappingURL=command-error.js.map
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
- import { getConvexUrl } from "../config.js";
9
+ import { convexUrl } from "../cloud.js";
10
10
  import { api } from "../convex.js";
11
11
  import { openBrowserWithFallback } from "./browser-launch.js";
12
12
  import { startCallbackServer } from "./oauth-callback-server.js";
@@ -58,7 +58,7 @@ export async function executeOAuthFlow() {
58
58
  }
59
59
  console.log("Authentication successful, exchanging code for tokens...");
60
60
  // Exchange authorization code for tokens via Convex action (server-side WorkOS call)
61
- const client = new ConvexHttpClient(getConvexUrl());
61
+ const client = new ConvexHttpClient(convexUrl());
62
62
  const result = await client.action(api.lib.authActions.exchangeAuthorizationCode, {
63
63
  code,
64
64
  codeVerifier,
@@ -43,9 +43,9 @@ export async function selectTeam(client) {
43
43
  */
44
44
  export async function selectProject(client, teamId) {
45
45
  console.log("\nFetching projects...\n");
46
- const projectsWithSecrets = (await client.query(api.projectSecrets.queries.listOwnSecretsForTeam,
46
+ const projectsWithSecrets = (await client.query(api.projectSecrets.queries.listOwnForTeam,
47
47
  // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id<"teams"> type; the runtime value is a plain string the convex server validates.
48
- { target: { type: "team", id: teamId } }));
48
+ { teamId: teamId }));
49
49
  if (projectsWithSecrets.length === 0) {
50
50
  console.log("No projects found in this team.");
51
51
  console.log('Use "dotrequirements init" to create a new project.\n');
@@ -79,9 +79,9 @@ export async function selectProject(client, teamId) {
79
79
  * Free tier always gets 30 days regardless of this parameter.
80
80
  */
81
81
  export async function getOrCreateProjectSecret(client, projectId, expiryDays) {
82
- const result = (await client.mutation(api.projectSecrets.mutations.ensureOwnSecret, {
82
+ const result = (await client.mutation(api.projectSecrets.mutations.ensureOwn, {
83
83
  // biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id<"projects"> type; the runtime value is a plain string the convex server validates.
84
- target: { type: "project", id: projectId },
84
+ projectId: projectId,
85
85
  expiryDays,
86
86
  }));
87
87
  return result;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * What the cloud has to say to this CLI version (CLI-MSG-1, CLI-MSG-3).
3
+ *
4
+ * Once enabled, the first cloud use in a run asks the cloud for this
5
+ * version's messages, alongside the command's own work. At the end the
6
+ * command takes them: text commands print them after their output, JSON
7
+ * commands carry them in their JSON. Asking never changes the command: a
8
+ * failed, unreadable or slow answer means no messages.
9
+ */
10
+ /** How long a finished command waits for the cloud's answer (CLI-MSG-3.2). */
11
+ export declare const MESSAGE_WAIT_MS = 1000;
12
+ /** Ask the cloud for this version's messages on the run's first cloud use. */
13
+ export declare function enableVersionMessages(): void;
14
+ /**
15
+ * The messages for this version, waiting at most `waitMs` for the cloud's
16
+ * answer; empty when the run never reached the cloud or they were already
17
+ * shown. Asking twice waits once.
18
+ */
19
+ export declare function versionMessages(waitMs?: number): Promise<string[]>;
20
+ /**
21
+ * Record that this run's messages reached the user, once they are actually
22
+ * printed, so `wrapCommand` doesn't print them again after a JSON command
23
+ * carried them — and does print them if that command failed first.
24
+ */
25
+ export declare function markVersionMessagesShown(): void;
26
+ /** The messages as text, for a command's human-readable output. */
27
+ export declare function formatVersionMessages(messages: string[]): string;
28
+ //# sourceMappingURL=version-messages.d.ts.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * What the cloud has to say to this CLI version (CLI-MSG-1, CLI-MSG-3).
3
+ *
4
+ * Once enabled, the first cloud use in a run asks the cloud for this
5
+ * version's messages, alongside the command's own work. At the end the
6
+ * command takes them: text commands print them after their output, JSON
7
+ * commands carry them in their JSON. Asking never changes the command: a
8
+ * failed, unreadable or slow answer means no messages.
9
+ */
10
+ import { convexUrl, whenCloudFirstUsed } from "./cloud.js";
11
+ import { getOwnPackage } from "./utils/own-package.js";
12
+ /** How long a finished command waits for the cloud's answer (CLI-MSG-3.2). */
13
+ export const MESSAGE_WAIT_MS = 1_000;
14
+ let pending = null;
15
+ let abort = null;
16
+ let shown = false;
17
+ async function askCloud(signal) {
18
+ try {
19
+ const response = await fetch(`${convexUrl()}/api/query`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify({
23
+ path: "cliMessages/queries:forVersion",
24
+ args: { version: getOwnPackage().version },
25
+ format: "json",
26
+ }),
27
+ signal,
28
+ });
29
+ if (!response.ok)
30
+ return [];
31
+ const body = (await response.json());
32
+ const messages = body.status === "success" ? body.value?.messages : null;
33
+ return Array.isArray(messages)
34
+ ? messages.filter((m) => typeof m === "string" && !!m)
35
+ : [];
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ /** Ask the cloud for this version's messages on the run's first cloud use. */
42
+ export function enableVersionMessages() {
43
+ whenCloudFirstUsed(() => {
44
+ if (pending)
45
+ return;
46
+ const controller = new AbortController();
47
+ abort = controller;
48
+ // Asked just after the command's own first request goes out, so the
49
+ // command's work is never queued behind the question.
50
+ pending = new Promise((resolve) => {
51
+ queueMicrotask(() => resolve(askCloud(controller.signal)));
52
+ });
53
+ });
54
+ }
55
+ /**
56
+ * The messages for this version, waiting at most `waitMs` for the cloud's
57
+ * answer; empty when the run never reached the cloud or they were already
58
+ * shown. Asking twice waits once.
59
+ */
60
+ export async function versionMessages(waitMs = MESSAGE_WAIT_MS) {
61
+ if (shown || !pending)
62
+ return [];
63
+ let timer;
64
+ const late = new Promise((resolve) => {
65
+ timer = setTimeout(() => resolve([]), waitMs);
66
+ });
67
+ const messages = await Promise.race([pending, late]);
68
+ clearTimeout(timer);
69
+ // Whatever came back is the answer: a slow one must neither keep the
70
+ // process alive after the command nor be waited for a second time.
71
+ abort?.abort();
72
+ pending = Promise.resolve(messages);
73
+ return messages;
74
+ }
75
+ /**
76
+ * Record that this run's messages reached the user, once they are actually
77
+ * printed, so `wrapCommand` doesn't print them again after a JSON command
78
+ * carried them — and does print them if that command failed first.
79
+ */
80
+ export function markVersionMessagesShown() {
81
+ shown = true;
82
+ }
83
+ /** The messages as text, for a command's human-readable output. */
84
+ export function formatVersionMessages(messages) {
85
+ return messages.map((message) => `\n${message}`).join("\n");
86
+ }
87
+ //# sourceMappingURL=version-messages.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Requirements as testable, human-readable data — CLI and test harness for spec-driven development with AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/dist/config.d.ts DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Get the Convex deployment URL
3
- */
4
- export declare function getConvexUrl(): string;
5
- //# sourceMappingURL=config.d.ts.map
package/dist/config.js DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * Production Convex deployment URL
3
- * All users connect to the same backend
4
- */
5
- const CONVEX_URL = "https://data.dotrequirements.io";
6
- /**
7
- * Get the Convex deployment URL
8
- */
9
- export function getConvexUrl() {
10
- return CONVEX_URL;
11
- }
12
- //# sourceMappingURL=config.js.map