braith 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +59 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # braith
2
2
 
3
- Run `npx braith init` in your application repository, then follow `.braith/SETUP.md` with your coding agent. The CLI validates and publishes reviewed intent manifests; it does not upload your repository.
3
+ Run `npx braith@0.3.0 connect` in your application repository, then follow `.braith/SETUP.md` with your coding agent. The CLI validates and publishes reviewed intent manifests; it does not upload your repository.
4
4
 
5
- Commands: `init`, `enroll`, `validate`, `push`, `release`, `test`.
5
+ Commands: `connect`, `init`, `enroll`, `validate`, `push`, `release`, `test`.
6
6
 
7
- Version 0.2.0 adds optional discovery context alongside configured outcomes. The coding tool installs supported capture across the app and creates a static catalogue, starting in shadow mode. Discovery does not require a declared outcome for every flow. Capture/source changes enter the local review map; source files are not uploaded. The `test` command remains an isolated configured-outcome fixture, not a discovery accuracy test. Exercise actual hooks separately with normal, failed and recovered interactions.
7
+ Version 0.3.0 includes optional discovery context alongside configured outcomes. The coding tool installs supported capture across the app and creates a static catalogue, with findings visible in review mode. Discovery does not require a declared outcome for every flow. Capture/source changes enter the local review map; source files are not uploaded. The `test` command remains an isolated configured-outcome fixture, not a discovery accuracy test. Exercise actual hooks separately with normal, failed and recovered interactions.
8
8
 
9
- Use a short-lived enrollment prompt from the Braith dashboard to get scoped setup credentials. Keep `.braith/credentials.json` private. A pushed manifest is not active until you bind it to a release or explicitly select `release default`.
9
+ The setup prompt is public and identical for everyone. The CLI displays an expiring authorization link and code; approve the matching code in your signed-in Braith account. Only the waiting CLI receives scoped setup credentials. Run connect again to renew expired setup access. Keep `.braith/credentials.json` private. A pushed manifest is not active until you bind it to a release or explicitly select `release default`.
package/dist/cli.js CHANGED
@@ -19868,7 +19868,7 @@ var catalogue = (limit) => external_exports.record(IdentifierSchema, external_ex
19868
19868
  );
19869
19869
  var DiscoveryConfigSchema = external_exports.object({
19870
19870
  enabled: external_exports.literal(true),
19871
- mode: external_exports.enum(["shadow", "review"]).default("shadow"),
19871
+ mode: external_exports.enum(["shadow", "review"]).default("review"),
19872
19872
  application: external_exports.string().trim().min(10).max(600),
19873
19873
  surfaces: catalogue(64).default({}),
19874
19874
  actions: catalogue(128).default({}),
@@ -20675,12 +20675,12 @@ var setupInstructions = `# Set up Braith
20675
20675
 
20676
20676
  Install continual customer-problem discovery and stronger checks for the application's important outcomes. Inspect the repository locally; the developer should not have to define each flow.
20677
20677
 
20678
- 1. Inspect the repository's routes, shared UI components, feedback and request/state boundaries. Create a static discovery catalogue in braith.config.json: enabled true, mode shadow, application purpose, surfaces, actions and feedback. These are descriptions of what the app does, not rules listing possible failures. Configure the SDK's discovery catalogue, route templates and stable action/feedback selectors. Cover supported app surfaces, including flows without configured outcomes. Never derive catalogue labels from customer content, URL parameters or form values.
20678
+ 1. Inspect the repository's routes, shared UI components, feedback and request/state boundaries. Create a static discovery catalogue in braith.config.json: enabled true, mode review, application purpose, surfaces, actions and feedback. These are descriptions of what the app does, not rules listing possible failures. Configure the SDK's discovery catalogue, route templates and stable action/feedback selectors. Cover supported app surfaces, including flows without configured outcomes. Never derive catalogue labels from customer content, URL parameters or form values.
20679
20679
  2. Alongside broad discovery, choose a few critical outcomes for stronger explicit checks. Describe their observable success boundary. A UI success message is not enough when the outcome depends on committed server state.
20680
20680
  3. Add finite observations at shared server/job/access/state boundaries where browser status alone cannot reveal the result. Use client.observeWindow on the server for a coherent same-subject sequence, or Braith.observe in the browser for a safe semantic summary. Use only catalogue identifiers and fixed kind/result/status/contentKind fields; never send values or request/response bodies. A local before/after comparison may report changed or unchanged without sending the underlying data. Automatic browser capture handles supported clicks, submits, navigation, same-origin fetch summaries and declared feedback selectors. It does not read page text, inspect network bodies, or prove that every feature is covered.
20681
20681
  4. Add allowlisted Braith events at those code locations. In browser code, use the named Braith export from @braith/sdk and call Braith.init({ projectKey, endpoint }) once, then Braith.identify and Braith.track. On the server, use createServerClient({ projectKey, endpoint }), pass userId and attemptId in track context, and flush before a short-lived process exits. Use server events for authoritative facts and propagate an attemptId across the journey.
20682
20682
  5. Additionally identify 3\u20135 critical user-facing outcomes, or fewer when appropriate, for stronger explicit checks. Define entry, authoritative success, progress, contradiction, recovery and cancellation signals. A UI success message alone cannot establish committed server state. Replace the example event definitions and intents with these real outcomes. Their absence must not exclude other activity from discovery; discovery-only apps may have empty arrays. Broad capture is opt-in, defaults to shadow review, and uses the existing event/analysis allowance: 20% of analysis capacity for discovery and 80% for core checks. Start at 10% ordinary-window sampling and at most 10 windows/hour per browser; observed failures are prioritised within that same cap. State coverage gaps and privacy choices honestly.
20683
- 6. Run the short-lived Braith enrollment command from the dashboard prompt. It stores keys only in .braith/credentials.json, ignored by .braith/.gitignore. For a later setup prompt, run braith enroll <token> --replace; the old local file is retained if renewal fails. Set the browser and server project keys in the corresponding application deployment environment. Never commit the server key. The temporary management key expires after 30 minutes.
20683
+ 6. Run braith connect and show the owner its authorization link and matching code. The owner signs in to Braith and approves that connection. Keys are stored only in .braith/credentials.json, ignored by .braith/.gitignore. Run connect again to renew setup access; existing credentials are preserved on failure or a different-project selection. Set browser and server keys in the corresponding deployment environment. Never commit the server key. The temporary management key expires after 30 minutes.
20684
20684
  7. Run npx braith validate, resolve errors, and review every warning or unverifiable source check.
20685
20685
  8. Present the instrumentation and configuration diff to the developer for review. Do not upload the repository or deploy automatically.
20686
20686
  9. After review, run npx braith push. If core outcomes are configured, run npx braith test (and --semantic where supported). These isolated fixtures check configured-outcome plumbing, not discovery accuracy. Exercise real discovery hooks locally with normal, failed and recovered interactions; inspect sanitised payloads without exposing secrets. Push records .braith/source-map.json, a local review map of outcome and discovery source files and hashes; it uploads only the manifest. Commit and review that map with the instrumentation. Do not present synthetic test incidents as proof of real coverage or model accuracy.
@@ -20693,7 +20693,7 @@ var exampleConfig = {
20693
20693
  projectKey: "env:BRAITH_PROJECT_KEY",
20694
20694
  discovery: {
20695
20695
  enabled: true,
20696
- mode: "shadow",
20696
+ mode: "review",
20697
20697
  application: "A subscription app where customers pay for access to reporting tools.",
20698
20698
  surfaces: { billing: "Choose a plan and pay for access", reports: "Use reports included in the selected plan" },
20699
20699
  actions: { upgrade: "Upgrade the account", open_report: "Open a report" },
@@ -20762,6 +20762,10 @@ function writeNew(path, contents) {
20762
20762
  writeFileSync(path, contents, "utf8");
20763
20763
  return true;
20764
20764
  }
20765
+ function ensureCredentialIgnore(directory) {
20766
+ const path = join(directory, ".gitignore"), existing = existsSync(path) ? readFileSync(path, "utf8") : "";
20767
+ if (!existing.split(/\r?\n/).some((line) => line.trim() === CREDENTIALS_NAME)) writeFileSync(path, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + CREDENTIALS_NAME + "\n", "utf8");
20768
+ }
20765
20769
  function initProject(root = process.cwd()) {
20766
20770
  const directory = join(root, ".braith");
20767
20771
  mkdirSync(directory, { recursive: true });
@@ -20911,8 +20915,7 @@ async function enrollProject(token, root = process.cwd(), replace = false) {
20911
20915
  if (!/^br_enroll_[A-Za-z0-9_-]{32}$/.test(token)) throw new Error("Invalid enrollment token");
20912
20916
  const directory = join(root, ".braith");
20913
20917
  mkdirSync(directory, { recursive: true });
20914
- writeNew(join(directory, ".gitignore"), `${CREDENTIALS_NAME}
20915
- `);
20918
+ ensureCredentialIgnore(directory);
20916
20919
  const path = join(directory, CREDENTIALS_NAME);
20917
20920
  if (existsSync(path) && !replace) {
20918
20921
  throw new Error("Existing enrollment found. Use braith enroll <token> --replace to renew setup access without deleting credentials by hand");
@@ -20946,6 +20949,43 @@ async function enrollProject(token, root = process.cwd(), replace = false) {
20946
20949
  }
20947
20950
  return { projectId: credentials.projectId, expiresAt: credentials.expiresAt };
20948
20951
  }
20952
+ async function connectProject(root = process.cwd()) {
20953
+ const previous = localCredentials(root), apiUrl = (process.env.BRAITH_API_URL?.trim() || "https://app.braith.dev").replace(/\/$/, "");
20954
+ const origin = new URL(apiUrl);
20955
+ if (origin.protocol !== "https:" && !["127.0.0.1", "localhost"].includes(origin.hostname)) throw new Error("Braith connection requires HTTPS");
20956
+ const call = (body) => fetch(apiUrl + "/api/v1/setup", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(15e3), redirect: "error" });
20957
+ const start = await call({ action: "start" });
20958
+ if (!start.ok) throw new Error("Braith connection is unavailable. Try again shortly.");
20959
+ const pending = await start.json();
20960
+ if (!/^br_device_[A-Za-z0-9_-]{32}$/.test(pending.deviceToken) || !Number.isFinite(pending.expiresIn) || pending.expiresIn <= 0 || pending.expiresIn > 900 || new URL(pending.verificationUrl).origin !== origin.origin || !Number.isFinite(pending.interval) || pending.interval < 5 || pending.interval > 30) throw new Error("Invalid Braith connection response");
20961
+ console.log("Approve this connection in Braith: " + pending.verificationUrl + "\nCheck that the code matches: " + pending.userCode + "\nWaiting for your approval\u2026");
20962
+ const deadline = Date.now() + pending.expiresIn * 1e3;
20963
+ while (Date.now() < deadline) {
20964
+ await new Promise((resolve2) => setTimeout(resolve2, pending.interval * 1e3));
20965
+ if (Date.now() >= deadline) break;
20966
+ const response = await call({ action: "poll", deviceToken: pending.deviceToken });
20967
+ if (response.status === 202) continue;
20968
+ if (response.status === 429) {
20969
+ await new Promise((resolve2) => setTimeout(resolve2, 3e4));
20970
+ continue;
20971
+ }
20972
+ if (!response.ok) throw new Error("Connection cancelled or expired. Run braith connect again.");
20973
+ const value = await response.json();
20974
+ if (![value.projectId, value.browserKey, value.serverKey, value.managementKey, value.expiresAt].every((item) => typeof item === "string" && item.length > 0) || !Number.isFinite(Date.parse(value.expiresAt)) || Date.parse(value.expiresAt) <= Date.now()) throw new Error("Connection returned incomplete credentials");
20975
+ if (previous && previous.projectId !== value.projectId) throw new Error("This connection is for a different app. Existing credentials were preserved.");
20976
+ initProject(root);
20977
+ ensureCredentialIgnore(join(root, ".braith"));
20978
+ const path = join(root, ".braith", CREDENTIALS_NAME), temporary = path + "." + randomUUID() + ".tmp";
20979
+ try {
20980
+ writeFileSync(temporary, JSON.stringify({ ...value, apiUrl }, null, 2) + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
20981
+ renameSync(temporary, path);
20982
+ } finally {
20983
+ if (existsSync(temporary)) rmSync(temporary);
20984
+ }
20985
+ return { projectId: value.projectId, expiresAt: value.expiresAt };
20986
+ }
20987
+ throw new Error("Connection expired. Run braith connect again.");
20988
+ }
20949
20989
  function printValidation(result) {
20950
20990
  for (const message of result.messages) console.log(`${message.level.toUpperCase()}: ${message.message}`);
20951
20991
  console.log(result.valid ? "Braith configuration is valid." : "Braith configuration is invalid.");
@@ -21057,6 +21097,17 @@ async function testProject(root = process.cwd(), mode) {
21057
21097
  }
21058
21098
  async function run(argv = process.argv.slice(2), root = process.cwd()) {
21059
21099
  const command = argv[0];
21100
+ if (command === "connect") {
21101
+ try {
21102
+ if (argv.length !== 1) throw new Error("Usage: braith connect");
21103
+ await connectProject(root);
21104
+ console.log("Connected. Follow .braith/SETUP.md to integrate this app.");
21105
+ return 0;
21106
+ } catch (error62) {
21107
+ console.error(error62 instanceof Error ? error62.message : "Connection failed");
21108
+ return 1;
21109
+ }
21110
+ }
21060
21111
  if (command === "init") {
21061
21112
  const result = initProject(root);
21062
21113
  for (const path of result.created) console.log(`Created ${path}`);
@@ -21114,13 +21165,14 @@ async function run(argv = process.argv.slice(2), root = process.cwd()) {
21114
21165
  return 1;
21115
21166
  }
21116
21167
  }
21117
- console.error("Usage: braith <init|enroll|validate|push|release|test>");
21168
+ console.error("Usage: braith <connect|init|enroll|validate|push|release|test>");
21118
21169
  return 1;
21119
21170
  }
21120
21171
  var invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
21121
21172
  if (invokedPath === import.meta.url) process.exitCode = await run();
21122
21173
  export {
21123
21174
  bindRelease,
21175
+ connectProject,
21124
21176
  enrollProject,
21125
21177
  initProject,
21126
21178
  pushProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braith",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Braith coding-agent setup and outcome manifest CLI",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": { "access": "public" },