@inerrata-corporation/errata 2.0.0-dev.97 → 2.0.1-dev.100

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 (2) hide show
  1. package/errata.mjs +733 -96
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -21786,11 +21786,24 @@ var init_client = __esm({
21786
21786
  async actableAgents() {
21787
21787
  return this.json("GET", "/api/gate/actable-agents");
21788
21788
  }
21789
+ /** Resolve the exact installation and credential context used by this client. */
21790
+ async context() {
21791
+ return this.json("GET", "/api/gate/context");
21792
+ }
21793
+ /** Resolve context after an audited, sessionless act-as re-mint. Useful for
21794
+ * CLI diagnostics that must report the same identity a selected agent uses. */
21795
+ async contextAs(handle2, teamId) {
21796
+ const acting = await this.actAs(handle2, teamId);
21797
+ return this.json("GET", "/api/gate/context", void 0, { bearerToken: acting.token });
21798
+ }
21789
21799
  /** Mint a fresh gateway JWT stamped with `handle` (authz-checked + audited
21790
21800
  * server-side). The returned `token` bears the acting-agent identity for
21791
21801
  * subsequent cloud calls. Authenticated with the caller's base JWT. */
21792
- async actAs(handle2) {
21793
- return this.json("POST", "/api/gate/act-as", { handle: handle2 });
21802
+ async actAs(handle2, teamId) {
21803
+ return this.json("POST", "/api/gate/act-as", {
21804
+ handle: handle2,
21805
+ ...teamId !== void 0 ? { teamId } : {}
21806
+ });
21794
21807
  }
21795
21808
  /** Resolve-or-create the org's project for a repo locator (ambient link,
21796
21809
  * PJ-entity). Requires user identity on the JWT — login-gated like actAs. */
@@ -22163,7 +22176,7 @@ var init_client = __esm({
22163
22176
  ...provenanceHeaders(this.provenance)
22164
22177
  };
22165
22178
  if (!opts?.skipAuth) {
22166
- const token = await this.authToken();
22179
+ const token = opts?.bearerToken ?? await this.authToken();
22167
22180
  if (token) headers["authorization"] = `Bearer ${token}`;
22168
22181
  }
22169
22182
  const ac = new AbortController();
@@ -22377,7 +22390,9 @@ function defaultConfig() {
22377
22390
  onboardedAt: null,
22378
22391
  machineId: null,
22379
22392
  updateChannel: "dev",
22380
- activeAgent: null
22393
+ activeAgent: null,
22394
+ installationProfiles: {},
22395
+ activeInstallationProfile: null
22381
22396
  };
22382
22397
  }
22383
22398
  function loadConfig() {
@@ -22402,14 +22417,89 @@ function loadConfig() {
22402
22417
  cloudUrl: envCloudUrl ?? (migrateLegacyDefault ? DEFAULT_CLOUD_URL : parsed.cloudUrl ?? base.cloudUrl),
22403
22418
  // Deep-merge consent so an old/partial config keeps the opt-in defaults
22404
22419
  // for any channel it doesn't mention.
22405
- consent: { ...base.consent, ...parsed.consent ?? {} }
22420
+ consent: { ...base.consent, ...parsed.consent ?? {} },
22421
+ installationProfiles: { ...parsed.installationProfiles ?? {} }
22406
22422
  };
22407
22423
  if (migrateLegacyDefault && !envCloudUrl) saveConfig(resolved);
22408
22424
  return resolved;
22409
22425
  }
22410
22426
  function saveConfig(cfg) {
22411
22427
  ensureDir(globalDir());
22412
- writeFileSync5(globalConfigPath(), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
22428
+ const normalized = snapshotActiveInstallationProfile(cfg);
22429
+ writeFileSync5(globalConfigPath(), JSON.stringify(normalized, null, 2), { encoding: "utf8", mode: 384 });
22430
+ }
22431
+ function profileFromCurrent(cfg, installationId) {
22432
+ return {
22433
+ installationId,
22434
+ cloudUrl: cfg.cloudUrl,
22435
+ apiKey: cfg.apiKey,
22436
+ accessToken: cfg.accessToken,
22437
+ refreshToken: cfg.refreshToken,
22438
+ tokenEndpoint: cfg.tokenEndpoint,
22439
+ oauthClientId: cfg.oauthClientId,
22440
+ activeAgent: cfg.activeAgent
22441
+ };
22442
+ }
22443
+ function snapshotActiveInstallationProfile(cfg) {
22444
+ const name2 = cfg.activeInstallationProfile;
22445
+ if (!name2) return cfg;
22446
+ const existing = cfg.installationProfiles[name2];
22447
+ if (!existing) return { ...cfg, activeInstallationProfile: null };
22448
+ return {
22449
+ ...cfg,
22450
+ installationProfiles: {
22451
+ ...cfg.installationProfiles,
22452
+ [name2]: profileFromCurrent(cfg, existing.installationId)
22453
+ }
22454
+ };
22455
+ }
22456
+ function saveInstallationProfile(name2, installationId, cfg = loadConfig()) {
22457
+ const normalizedName = name2.trim();
22458
+ if (!normalizedName || normalizedName.length > 80) {
22459
+ throw new Error("profile name must be 1\u201380 characters");
22460
+ }
22461
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(installationId)) {
22462
+ throw new Error("installation id must be a UUID");
22463
+ }
22464
+ const next = {
22465
+ ...cfg,
22466
+ installationProfiles: {
22467
+ ...cfg.installationProfiles,
22468
+ [normalizedName]: profileFromCurrent(cfg, installationId)
22469
+ },
22470
+ activeInstallationProfile: normalizedName
22471
+ };
22472
+ saveConfig(next);
22473
+ return next;
22474
+ }
22475
+ function useInstallationProfile(name2, cfg = loadConfig()) {
22476
+ const profile = cfg.installationProfiles[name2];
22477
+ if (!profile) throw new Error(`installation profile not found: ${name2}`);
22478
+ const next = {
22479
+ ...cfg,
22480
+ cloudUrl: profile.cloudUrl,
22481
+ apiKey: profile.apiKey,
22482
+ accessToken: profile.accessToken,
22483
+ refreshToken: profile.refreshToken,
22484
+ tokenEndpoint: profile.tokenEndpoint,
22485
+ oauthClientId: profile.oauthClientId,
22486
+ activeAgent: profile.activeAgent,
22487
+ activeInstallationProfile: name2
22488
+ };
22489
+ saveConfig(next);
22490
+ return next;
22491
+ }
22492
+ function removeInstallationProfile(name2, cfg = loadConfig()) {
22493
+ if (!cfg.installationProfiles[name2]) return cfg;
22494
+ const installationProfiles = { ...cfg.installationProfiles };
22495
+ delete installationProfiles[name2];
22496
+ const next = {
22497
+ ...cfg,
22498
+ installationProfiles,
22499
+ activeInstallationProfile: cfg.activeInstallationProfile === name2 ? null : cfg.activeInstallationProfile
22500
+ };
22501
+ saveConfig(next);
22502
+ return next;
22413
22503
  }
22414
22504
  function machineId() {
22415
22505
  const fromEnv = process.env["ERRATA_MACHINE_ID"];
@@ -22444,12 +22534,30 @@ var init_config = __esm({
22444
22534
  });
22445
22535
 
22446
22536
  // src/cloud-endpoint-policy.ts
22537
+ function warnDirectV1OverrideOnce() {
22538
+ if (warnedDirectV1Override) return;
22539
+ warnedDirectV1Override = true;
22540
+ console.error(
22541
+ [
22542
+ "",
22543
+ "!!! DIRECT V1 OVERRIDE ACTIVE !!!",
22544
+ `${DIRECT_V1_OVERRIDE_ENV} + ${DIRECT_V1_ACK_ENV} are set: this daemon is talking`,
22545
+ "straight to the legacy v1 data plane, BYPASSING the Console Gateway \u2014",
22546
+ "no metering, no signed context enforcement, no write-target binding.",
22547
+ "Staff testing only. This escape hatch is removed at v1 EOL.",
22548
+ ""
22549
+ ].join("\n")
22550
+ );
22551
+ }
22552
+ function isDirectV1OverrideRequested() {
22553
+ const raw2 = process.env[DIRECT_V1_OVERRIDE_ENV]?.trim().toLowerCase();
22554
+ return raw2 === "1" || raw2 === "true" || raw2 === "yes" || raw2 === "on";
22555
+ }
22447
22556
  function normalizeCloudUrl(url2) {
22448
22557
  return url2.replace(/\/+$/, "");
22449
22558
  }
22450
22559
  function isDirectV1OverrideEnabled() {
22451
- const raw2 = process.env[DIRECT_V1_OVERRIDE_ENV]?.trim().toLowerCase();
22452
- return raw2 === "1" || raw2 === "true" || raw2 === "yes" || raw2 === "on";
22560
+ return isDirectV1OverrideRequested() && process.env[DIRECT_V1_ACK_ENV]?.trim() === DIRECT_V1_ACK_VALUE;
22453
22561
  }
22454
22562
  function isLocalCloudUrl(url2) {
22455
22563
  try {
@@ -22481,18 +22589,20 @@ function evaluateCloudEndpoint(url2, inspection) {
22481
22589
  return { allowed: true, label: "gateway-configured" };
22482
22590
  }
22483
22591
  if (isDirectV1OverrideEnabled()) {
22592
+ warnDirectV1OverrideOnce();
22484
22593
  return {
22485
22594
  allowed: true,
22486
22595
  label: "legacy-direct (override)",
22487
- reason: `${DIRECT_V1_OVERRIDE_ENV} is enabled`
22596
+ reason: `${DIRECT_V1_OVERRIDE_ENV} acknowledged via ${DIRECT_V1_ACK_ENV}`
22488
22597
  };
22489
22598
  }
22490
22599
  const service = inspection?.service ? ` (${inspection.service})` : "";
22600
+ const unacked = isDirectV1OverrideRequested() ? ` ${DIRECT_V1_OVERRIDE_ENV} is set but unacknowledged \u2014 staff testing additionally requires ${DIRECT_V1_ACK_ENV}=${DIRECT_V1_ACK_VALUE}.` : "";
22491
22601
  return {
22492
22602
  allowed: false,
22493
22603
  label: "legacy-direct blocked",
22494
22604
  reason: `non-local cloud endpoint is not the Console Gateway${service}`,
22495
- guidance: `Point ERRATA_CLOUD_URL at the Console Gateway, or set ${DIRECT_V1_OVERRIDE_ENV}=1 only for accepted legacy/direct-v1 testing.`
22605
+ guidance: `Point ERRATA_CLOUD_URL at the Console Gateway.${unacked}`
22496
22606
  };
22497
22607
  }
22498
22608
  function formatCloudEndpointBlock(url2, decision) {
@@ -22634,13 +22744,16 @@ async function fetchHealthJson(fetchFn, url2, timeoutMs) {
22634
22744
  clearTimeout(tid);
22635
22745
  }
22636
22746
  }
22637
- var DIRECT_V1_OVERRIDE_ENV, GATEWAY_SERVICE, PROBE_TIMEOUT_MS, CloudEndpointPolicyError, BLOCKED_RECHECK_MS;
22747
+ var DIRECT_V1_OVERRIDE_ENV, DIRECT_V1_ACK_ENV, DIRECT_V1_ACK_VALUE, GATEWAY_SERVICE, PROBE_TIMEOUT_MS, warnedDirectV1Override, CloudEndpointPolicyError, BLOCKED_RECHECK_MS;
22638
22748
  var init_cloud_endpoint_policy = __esm({
22639
22749
  "src/cloud-endpoint-policy.ts"() {
22640
22750
  "use strict";
22641
22751
  DIRECT_V1_OVERRIDE_ENV = "ERRATA_ALLOW_DIRECT_V1_CLOUD";
22752
+ DIRECT_V1_ACK_ENV = "ERRATA_DIRECT_V1_ACK";
22753
+ DIRECT_V1_ACK_VALUE = "I_ACCEPT_UNMETERED_DIRECT_V1";
22642
22754
  GATEWAY_SERVICE = "console-gateway";
22643
22755
  PROBE_TIMEOUT_MS = 2e3;
22756
+ warnedDirectV1Override = false;
22644
22757
  CloudEndpointPolicyError = class extends Error {
22645
22758
  decision;
22646
22759
  constructor(message, decision) {
@@ -22678,6 +22791,7 @@ function authedCloudClient(cfg, opts = {}) {
22678
22791
  const versionOpts = opts.daemonVersion ? { daemonVersion: opts.daemonVersion, daemonChannel: cfg.updateChannel } : {};
22679
22792
  if (cfg.accessToken) {
22680
22793
  const tokenEndpoint = cfg.tokenEndpoint;
22794
+ const installationId = cfg.activeInstallationProfile ? cfg.installationProfiles[cfg.activeInstallationProfile]?.installationId : void 0;
22681
22795
  return new CloudClient({
22682
22796
  baseUrl,
22683
22797
  accessToken: cfg.accessToken,
@@ -22687,7 +22801,11 @@ function authedCloudClient(cfg, opts = {}) {
22687
22801
  provenance: daemonProvenance(),
22688
22802
  ...opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {},
22689
22803
  ...tokenEndpoint && cfg.refreshToken ? {
22690
- refreshFn: (rt) => refreshAccessToken(opts.fetchFn ?? fetch, tokenEndpoint, {
22804
+ refreshFn: (rt) => tokenEndpoint.endsWith("/api/gate/session-token") && installationId ? refreshDeviceSessionToken(opts.fetchFn ?? fetch, tokenEndpoint, {
22805
+ sessionToken: rt,
22806
+ installationId,
22807
+ clientId: cfg.oauthClientId ?? oauthClientId()
22808
+ }) : refreshAccessToken(opts.fetchFn ?? fetch, tokenEndpoint, {
22691
22809
  refreshToken: rt,
22692
22810
  clientId: cfg.oauthClientId ?? oauthClientId()
22693
22811
  }),
@@ -22710,6 +22828,30 @@ function authedCloudClient(cfg, opts = {}) {
22710
22828
  ...opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
22711
22829
  });
22712
22830
  }
22831
+ async function refreshDeviceSessionToken(fetchFn, endpoint, input) {
22832
+ const response = await fetchFn(endpoint, {
22833
+ method: "POST",
22834
+ headers: {
22835
+ authorization: `Bearer ${input.sessionToken}`,
22836
+ "content-type": "application/json",
22837
+ accept: "application/json"
22838
+ },
22839
+ body: JSON.stringify({
22840
+ installationId: input.installationId,
22841
+ clientId: input.clientId,
22842
+ scopes: ["graph:read", "graph:write", "mcp:tools"]
22843
+ })
22844
+ });
22845
+ const body2 = await response.json().catch(() => null);
22846
+ if (!response.ok || typeof body2?.["access_token"] !== "string") {
22847
+ throw new Error(`device session refresh failed: HTTP ${response.status}`);
22848
+ }
22849
+ return {
22850
+ accessToken: body2["access_token"],
22851
+ refreshToken: input.sessionToken,
22852
+ ...typeof body2["expires_in"] === "number" ? { expiresInSec: body2["expires_in"] } : {}
22853
+ };
22854
+ }
22713
22855
  function hasCloudCredential(cfg) {
22714
22856
  return Boolean(cfg.accessToken || cfg.apiKey);
22715
22857
  }
@@ -36701,14 +36843,15 @@ function createMcpHandler(store, ctx = {}) {
36701
36843
  };
36702
36844
  }
36703
36845
  function buildToolContext() {
36846
+ const base = { switchInstallationProfile: useInstallationProfile };
36704
36847
  try {
36705
36848
  const cfg = loadConfig();
36706
36849
  if (cfg.consent.sync && hasCloudCredential(cfg) && cfg.cloudUrl) {
36707
- return { cloud: authedCloudClient(cfg) };
36850
+ return { ...base, cloud: authedCloudClient(cfg) };
36708
36851
  }
36709
36852
  } catch {
36710
36853
  }
36711
- return {};
36854
+ return base;
36712
36855
  }
36713
36856
  async function runMcpServer(workspaceRoot) {
36714
36857
  const paths = workspacePaths(workspaceRoot);
@@ -36817,6 +36960,41 @@ var init_mcp = __esm({
36817
36960
  AUDIT_NUDGE_COOLDOWN = 12;
36818
36961
  AUDIT_FLAG_NUDGE = "\u26A1 errata \u2014 reading/auditing? anything you notice that's off, flag it inline as `[!one line]` (`[?\u2026]` = TODO) \u2014 no tool call, we harvest it.";
36819
36962
  TOOLS = [
36963
+ {
36964
+ name: "errata.switch_context",
36965
+ description: "Switch this daemon to an already-installed Personal, organization, or Team profile. The profile's full credential set changes with the selection; this never widens the current token or carries consent between installations. Returns the selected installation descriptor so the caller can verify what changed.",
36966
+ inputSchema: {
36967
+ type: "object",
36968
+ properties: {
36969
+ profile: {
36970
+ type: "string",
36971
+ minLength: 1,
36972
+ maxLength: 80,
36973
+ description: "Exact name from `errata profile list`."
36974
+ }
36975
+ },
36976
+ required: ["profile"],
36977
+ additionalProperties: false
36978
+ },
36979
+ handler: (args2, _store, ctx) => {
36980
+ const profileName = typeof args2["profile"] === "string" ? args2["profile"].trim() : "";
36981
+ if (!profileName) throw new Error("profile is required");
36982
+ const next = (ctx.switchInstallationProfile ?? useInstallationProfile)(profileName);
36983
+ const profile = next.installationProfiles[profileName];
36984
+ if (!profile) throw new Error(`installation profile not found: ${profileName}`);
36985
+ return {
36986
+ switched: true,
36987
+ profile: profileName,
36988
+ installation_id: profile.installationId,
36989
+ cloud_url: profile.cloudUrl,
36990
+ credential_type: profile.accessToken ? "oauth" : profile.apiKey ? "api_key" : "none",
36991
+ actor: profile.activeAgent,
36992
+ authority_changed: true,
36993
+ standing_consent_carried: false,
36994
+ note: "Live graph visibility and write target are resolved server-side for each request."
36995
+ };
36996
+ }
36997
+ },
36820
36998
  {
36821
36999
  name: "errata.search",
36822
37000
  description: "Find a symbol, string, or comment across the live code graph \u2014 reaches matches by structure, not just text. Multi-word queries are tokenized and OR'd (more terms matched ranks higher, then pageRank), so word order and punctuation don't matter. Returns up to `limit` hits with id, label, description, and pageRank (higher pageRank = more central \u2014 start there). A comment that documents a symbol resolves to that SYMBOL (the comment rides along as `via` \u2014 it's the why, not the destination); test scaffolding and floating comments sort below production code. When the collective is reachable, results BLEND cross-project knowledge, each tagged `provenance` (local | collective | corroborated) with `usageCount`.",
@@ -49644,8 +49822,8 @@ function tagEdgeCorroborated(targetAnchorIds, sessionTouchedNodeIds, independent
49644
49822
  for (const a of targetAnchorIds) if (sessionTouchedNodeIds.has(a)) return true;
49645
49823
  return false;
49646
49824
  }
49647
- function typePriorEdge(sourceLabel, targetLabel, sentence = "") {
49648
- const direct = LABEL_PAIR[`${sourceLabel}>${targetLabel}`];
49825
+ function typePriorEdge(sourceLabel, targetLabel2, sentence = "") {
49826
+ const direct = LABEL_PAIR[`${sourceLabel}>${targetLabel2}`];
49649
49827
  if (direct) return direct;
49650
49828
  if (sentence && !NEG.test(sentence)) {
49651
49829
  const t = TIEBREAK.find((c) => c.re.test(sentence));
@@ -51471,7 +51649,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
51471
51649
  }
51472
51650
 
51473
51651
  // src/engine.ts
51474
- var DAEMON_VERSION = true ? "2.0.0-dev.97" : "2.0.0-alpha.0";
51652
+ var DAEMON_VERSION = true ? "2.0.1-dev.100" : "2.0.0-alpha.0";
51475
51653
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
51476
51654
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
51477
51655
  var GIT_OP_MUTE_MS = 4e3;
@@ -53060,10 +53238,13 @@ function buildContextIngest(store, profile, daemonVersion, ignorePatterns = [],
53060
53238
  // Package ids are already the purl (= the cross-stratum canonicalId).
53061
53239
  id: label === "Language" ? languageCanonicalId(String(n.attrs["name"] ?? "").trim() || n.description) : n.id,
53062
53240
  embedding: [],
53241
+ // No `version` attr on the wire: the purl already encodes the resolved
53242
+ // version, and the door's temporal guard rejects ANY `attrs.version` as
53243
+ // bi-temporal bookkeeping (ingest-temporal-guard.ts) — shipping it 422s
53244
+ // the whole batch. `resolved` still travels (range-vs-lockfile signal).
53063
53245
  attrs: label === "Package" ? {
53064
53246
  purl: n.attrs["purl"],
53065
53247
  name: n.attrs["name"],
53066
- version: n.attrs["version"],
53067
53248
  ecosystem: n.attrs["ecosystem"],
53068
53249
  resolved: n.attrs["resolved"]
53069
53250
  } : { name: n.attrs["name"] }
@@ -53178,9 +53359,10 @@ function wireContextId(n) {
53178
53359
  }
53179
53360
  function shareableContext(n, wireId) {
53180
53361
  const attrs = n.label === "Package" ? {
53362
+ // No `version` attr: the purl encodes it, and the door's temporal
53363
+ // guard 422s any `attrs.version` (mirrors buildContextIngest).
53181
53364
  purl: n.attrs["purl"],
53182
53365
  name: n.attrs["name"],
53183
- version: n.attrs["version"],
53184
53366
  ecosystem: n.attrs["ecosystem"],
53185
53367
  resolved: n.attrs["resolved"]
53186
53368
  } : n.label === "Domain" ? { name: n.description, canonicalId: n.attrs["canonicalId"] } : { name: n.attrs["name"] };
@@ -54355,14 +54537,27 @@ async function startMultiDaemon(opts = {}) {
54355
54537
  const ignore = loadClaimIgnorePatterns(globalDir());
54356
54538
  const client = cloudNow();
54357
54539
  let uploaded = 0;
54540
+ const errors = [];
54541
+ const lane = async (name2, projectName, run3) => {
54542
+ try {
54543
+ await run3();
54544
+ } catch (err2) {
54545
+ const msg = `[${projectName}] ${name2}: ${err2 instanceof Error ? err2.message : String(err2)}`;
54546
+ errors.push(msg);
54547
+ console.error(`[sync\u2192cloud] ${msg}`);
54548
+ }
54549
+ };
54358
54550
  for (const r of records) {
54359
54551
  const store = r.engine.store;
54552
+ const projectName = r.engine.profile.name ?? r.engine.profile.id;
54360
54553
  const context = buildContextIngest(store, r.engine.profile, DAEMON_VERSION, ignore, {
54361
54554
  includePackages: cfg2.consent.contributePackages
54362
54555
  });
54363
54556
  if (context) {
54364
- const res = await client.ingest(context);
54365
- uploaded += res.accepted;
54557
+ await lane("context", projectName, async () => {
54558
+ const res = await client.ingest(context);
54559
+ uploaded += res.accepted;
54560
+ });
54366
54561
  }
54367
54562
  let lexicon;
54368
54563
  try {
@@ -54385,34 +54580,36 @@ async function startMultiDaemon(opts = {}) {
54385
54580
  });
54386
54581
  if (instances) {
54387
54582
  if (project) instances.projectId = project.projectId;
54388
- const res = await client.ingest(instances);
54389
- uploaded += res.accepted;
54390
- const seq = store.currentIngestSeq();
54391
- const cloudIdByLocal = new Map(
54392
- res.result.nodes.filter((rn) => rn.nodeId).map((rn) => [rn.canonicalId, rn.nodeId])
54393
- );
54394
- for (const n of instances.nodes) {
54395
- const local = store.getNode(n.id);
54396
- if (!local || !["Problem", "Solution", "RootCause"].includes(local.label)) continue;
54397
- const cloudNodeId = cloudIdByLocal.get(n.id);
54398
- store.updateNode(n.id, {
54399
- attrs: {
54400
- ...local.attrs,
54401
- contributedAtSeq: seq,
54402
- ...cloudNodeId && cloudNodeId !== n.id ? { cloudNodeId } : {}
54403
- }
54404
- });
54405
- }
54406
- for (const [localId, dg] of Object.entries(instances.anchorDigests)) {
54407
- const local = store.getNode(localId);
54408
- if (!local) continue;
54409
- store.updateNode(localId, {
54410
- attrs: { ...local.attrs, anchorsContributedDigest: dg }
54411
- });
54412
- }
54583
+ await lane("instances", projectName, async () => {
54584
+ const res = await client.ingest(instances);
54585
+ uploaded += res.accepted;
54586
+ const seq = store.currentIngestSeq();
54587
+ const cloudIdByLocal = new Map(
54588
+ res.result.nodes.filter((rn) => rn.nodeId).map((rn) => [rn.canonicalId, rn.nodeId])
54589
+ );
54590
+ for (const n of instances.nodes) {
54591
+ const local = store.getNode(n.id);
54592
+ if (!local || !["Problem", "Solution", "RootCause"].includes(local.label)) continue;
54593
+ const cloudNodeId = cloudIdByLocal.get(n.id);
54594
+ store.updateNode(n.id, {
54595
+ attrs: {
54596
+ ...local.attrs,
54597
+ contributedAtSeq: seq,
54598
+ ...cloudNodeId && cloudNodeId !== n.id ? { cloudNodeId } : {}
54599
+ }
54600
+ });
54601
+ }
54602
+ for (const [localId, dg] of Object.entries(instances.anchorDigests)) {
54603
+ const local = store.getNode(localId);
54604
+ if (!local) continue;
54605
+ store.updateNode(localId, {
54606
+ attrs: { ...local.attrs, anchorsContributedDigest: dg }
54607
+ });
54608
+ }
54609
+ });
54413
54610
  }
54414
54611
  }
54415
- return { uploaded };
54612
+ return { uploaded, ...errors.length > 0 ? { errors } : {} };
54416
54613
  },
54417
54614
  async pullTriagePublic() {
54418
54615
  if (!loadConfig().consent.sync) return { merged: 0, skipped: "consent-off" };
@@ -54462,7 +54659,12 @@ async function startMultiDaemon(opts = {}) {
54462
54659
  }
54463
54660
  const inst = await daemon.syncInstancesPublic();
54464
54661
  const skills = await daemon.syncSkillsAll();
54465
- return { uploaded: inst.uploaded, written: skills.written, pruned: skills.pruned };
54662
+ return {
54663
+ uploaded: inst.uploaded,
54664
+ written: skills.written,
54665
+ pruned: skills.pruned,
54666
+ ...inst.errors ? { errors: inst.errors } : {}
54667
+ };
54466
54668
  } finally {
54467
54669
  boundaryFlushing = false;
54468
54670
  }
@@ -54629,6 +54831,102 @@ init_cloud_auth();
54629
54831
  import { createServer } from "node:http";
54630
54832
  var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
54631
54833
  var strip = (u) => u.replace(/\/+$/, "");
54834
+ function deviceEndpoints(tokenEndpoint) {
54835
+ const token = new URL(tokenEndpoint);
54836
+ const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
54837
+ if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
54838
+ const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
54839
+ const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
54840
+ return {
54841
+ code: at(`${prefix}/api/auth/device/code`),
54842
+ token: at(`${prefix}/api/auth/device/token`),
54843
+ resource: at(`${prefix}/api/gate/session-token`)
54844
+ };
54845
+ }
54846
+ async function loginOAuthDevice(opts) {
54847
+ const fetchFn = opts.fetchFn ?? fetch;
54848
+ const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
54849
+ const device = deviceEndpoints(endpoints.tokenEndpoint);
54850
+ const clientId = oauthClientId();
54851
+ const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
54852
+ const codeResponse = await fetchFn(device.code, {
54853
+ method: "POST",
54854
+ headers: { "content-type": "application/json", accept: "application/json" },
54855
+ body: JSON.stringify({ client_id: clientId, scope })
54856
+ });
54857
+ if (!codeResponse.ok) {
54858
+ throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
54859
+ }
54860
+ const code = await codeResponse.json();
54861
+ if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
54862
+ throw new Error("device authorization returned an incomplete response");
54863
+ }
54864
+ opts.onVerification?.({
54865
+ url: code.verification_uri_complete ?? code.verification_uri,
54866
+ userCode: code.user_code,
54867
+ expiresInSec: code.expires_in
54868
+ });
54869
+ const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
54870
+ let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
54871
+ let deviceSession = null;
54872
+ let approvedInstallation = null;
54873
+ while (Date.now() < deadline) {
54874
+ await (opts.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms))))(intervalMs);
54875
+ const poll = await fetchFn(device.token, {
54876
+ method: "POST",
54877
+ headers: { "content-type": "application/json", accept: "application/json" },
54878
+ body: JSON.stringify({
54879
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
54880
+ device_code: code.device_code,
54881
+ client_id: clientId
54882
+ })
54883
+ });
54884
+ const payload = await poll.json().catch(() => null);
54885
+ if (!poll.ok) {
54886
+ const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
54887
+ if (error48 === "authorization_pending") continue;
54888
+ if (error48 === "slow_down") {
54889
+ intervalMs += 5e3;
54890
+ continue;
54891
+ }
54892
+ throw new Error(`device authorization failed: ${error48}`);
54893
+ }
54894
+ deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
54895
+ approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
54896
+ break;
54897
+ }
54898
+ if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
54899
+ if (approvedInstallation !== opts.installationId) {
54900
+ throw new Error("approved installation does not match the terminal request");
54901
+ }
54902
+ const resourceResponse = await fetchFn(device.resource, {
54903
+ method: "POST",
54904
+ headers: {
54905
+ authorization: `Bearer ${deviceSession}`,
54906
+ "content-type": "application/json",
54907
+ accept: "application/json"
54908
+ },
54909
+ body: JSON.stringify({
54910
+ installationId: approvedInstallation,
54911
+ clientId,
54912
+ scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
54913
+ })
54914
+ });
54915
+ const resource = await resourceResponse.json().catch(() => null);
54916
+ if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
54917
+ throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
54918
+ }
54919
+ return {
54920
+ installationId: approvedInstallation,
54921
+ clientId,
54922
+ tokenEndpoint: device.resource,
54923
+ tokens: {
54924
+ accessToken: resource["access_token"],
54925
+ refreshToken: deviceSession,
54926
+ ...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
54927
+ }
54928
+ };
54929
+ }
54632
54930
  async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
54633
54931
  let consoleUrl = cloudUrl;
54634
54932
  try {
@@ -54998,6 +55296,116 @@ function shouldUseOAuthLogin(flags2, env2 = process.env) {
54998
55296
  return Boolean(flags2.oauth || !flags2.device && isDaemonOAuthDefaultEnabled(env2));
54999
55297
  }
55000
55298
 
55299
+ // src/doctor.ts
55300
+ async function diagnoseInstallation(cfg, dependencies) {
55301
+ const checks = [];
55302
+ const profileName = cfg.activeInstallationProfile;
55303
+ const profile = profileName ? cfg.installationProfiles[profileName] : void 0;
55304
+ checks.push(
55305
+ profile ? { id: "installation_profile", status: "pass", detail: `${profileName} is selected` } : {
55306
+ id: "installation_profile",
55307
+ status: "fail",
55308
+ detail: "No installation profile is selected"
55309
+ }
55310
+ );
55311
+ const credentialType = cfg.accessToken ? "oauth" : cfg.apiKey ? "api_key" : "none";
55312
+ checks.push(
55313
+ credentialType === "none" ? { id: "credential", status: "fail", detail: "No credential is installed" } : { id: "credential", status: "pass", detail: `${credentialType} credential is present` }
55314
+ );
55315
+ const authorityMatches = !profile || profile.cloudUrl === cfg.cloudUrl && profile.apiKey === cfg.apiKey && profile.accessToken === cfg.accessToken && profile.refreshToken === cfg.refreshToken;
55316
+ checks.push(
55317
+ authorityMatches ? {
55318
+ id: "profile_authority",
55319
+ status: profile ? "pass" : "warn",
55320
+ detail: profile ? "Selected profile and active credential set match" : "Cannot compare authority without a selected profile"
55321
+ } : {
55322
+ id: "profile_authority",
55323
+ status: "fail",
55324
+ detail: "Selected profile and active credential set differ; reselect the profile"
55325
+ }
55326
+ );
55327
+ if (credentialType === "oauth") {
55328
+ checks.push(
55329
+ cfg.refreshToken && cfg.tokenEndpoint ? { id: "renewal", status: "pass", detail: "OAuth renewal is configured" } : {
55330
+ id: "renewal",
55331
+ status: "fail",
55332
+ detail: "OAuth access exists without a refresh credential or token endpoint"
55333
+ }
55334
+ );
55335
+ }
55336
+ try {
55337
+ const inspection = await dependencies.inspect();
55338
+ checks.push(
55339
+ inspection.reachable ? {
55340
+ id: "endpoint",
55341
+ status: "pass",
55342
+ detail: `Cloud is reachable via ${inspection.probe}`
55343
+ } : {
55344
+ id: "endpoint",
55345
+ status: "fail",
55346
+ detail: inspection.error ?? "Cloud endpoint is unreachable"
55347
+ }
55348
+ );
55349
+ } catch (cause) {
55350
+ checks.push({
55351
+ id: "endpoint",
55352
+ status: "fail",
55353
+ detail: cause instanceof Error ? cause.message : String(cause)
55354
+ });
55355
+ }
55356
+ if (credentialType !== "none") {
55357
+ try {
55358
+ const identity = await dependencies.authenticate();
55359
+ checks.push({
55360
+ id: "authentication",
55361
+ status: "pass",
55362
+ detail: `Authenticated as ${identity.handle} (${identity.tier})`
55363
+ });
55364
+ } catch (cause) {
55365
+ checks.push({
55366
+ id: "authentication",
55367
+ status: "fail",
55368
+ detail: cause instanceof Error ? cause.message : String(cause)
55369
+ });
55370
+ }
55371
+ }
55372
+ return {
55373
+ ok: checks.every((check2) => check2.status !== "fail"),
55374
+ profile: profileName,
55375
+ installationId: profile?.installationId ?? null,
55376
+ credentialType,
55377
+ checks
55378
+ };
55379
+ }
55380
+
55381
+ // src/whoami.ts
55382
+ async function resolveWhoami(client, actingHandle) {
55383
+ return actingHandle ? client.contextAs(actingHandle) : client.context();
55384
+ }
55385
+ function targetLabel(context) {
55386
+ const target = context.default_write_target;
55387
+ if (!target) return "read-only";
55388
+ if (target.visibility === "public") return "public graph";
55389
+ if (target.visibility === "org") return `organization graph (${target.org_id})`;
55390
+ return `private Team graph (${target.team_id})`;
55391
+ }
55392
+ function formatWhoami(response, localProfile) {
55393
+ const { context, identity } = response;
55394
+ return [
55395
+ `inErrata context \u2014 ${context.profile_name ?? localProfile ?? "unbound credential"}`,
55396
+ ` workspace: ${context.workspace_kind} \xB7 ${context.org_id}`,
55397
+ ` team: ${context.team_id ?? "(none)"}`,
55398
+ ` agent: ${context.agent_id ?? "(default identity)"}`,
55399
+ ` installation: ${context.installation_id ?? "(unbound)"}`,
55400
+ ` environment: ${context.environment} \xB7 ${context.client_kind}`,
55401
+ ` reads: ${context.read_visibilities.join(" + ")}`,
55402
+ ` contributes: ${targetLabel(context)}`,
55403
+ ` policy: ${context.policy_version ?? "(legacy)"}`,
55404
+ ` credential: ${identity.credential_type} \xB7 ${identity.credential_id}`,
55405
+ ` actor: ${identity.actor_type} \xB7 ${identity.actor_id}`
55406
+ ];
55407
+ }
55408
+
55001
55409
  // src/consolidation-trigger.ts
55002
55410
  var DEFAULT_CONSOLIDATION_POLICY = {
55003
55411
  baseFloorMs: 6e4,
@@ -55055,8 +55463,13 @@ async function main() {
55055
55463
  return cmdStop();
55056
55464
  case "status":
55057
55465
  return cmdStatus();
55466
+ case "doctor":
55467
+ case "verify":
55468
+ return cmdDoctor(rest);
55058
55469
  case "usage":
55059
55470
  return cmdUsage();
55471
+ case "whoami":
55472
+ return cmdWhoami(rest);
55060
55473
  case "login":
55061
55474
  return cmdLogin();
55062
55475
  case "logout":
@@ -55067,6 +55480,8 @@ async function main() {
55067
55480
  return cmdUnlink();
55068
55481
  case "use":
55069
55482
  return cmdUse(rest);
55483
+ case "profile":
55484
+ return cmdInstallationProfile(rest);
55070
55485
  case "review":
55071
55486
  return cmdReview();
55072
55487
  case "tick":
@@ -55145,6 +55560,8 @@ Commands:
55145
55560
  status Print workspace + cloud status (incl. version + pending update)
55146
55561
  usage Show current cloud plan, units remaining, estimated cost,
55147
55562
  and per-tool usage (reads the gateway's batched Hono ledger)
55563
+ whoami [--json] Show the Console-resolved Personal/org/Team, agent,
55564
+ read visibility, and exact default contribution target.
55148
55565
  update [--channel dev|latest] [--check]
55149
55566
  Pull the newest build on this machine's channel via npm.
55150
55567
  --check only reports; --channel switches + persists channel.
@@ -55193,8 +55610,12 @@ Commands:
55193
55610
  surface (navigation, problems, claims, burst, health)
55194
55611
  login Sign in with the cloud. Default: short verification URL +
55195
55612
  typeable code (device-bridged OAuth). Flags: --browser
55196
- (loopback code flow) \xB7 --token <key> \xB7 --device (legacy v1)
55613
+ (loopback code flow) \xB7 --token <key> \xB7
55614
+ --device --installation <uuid> [--profile <name>]
55615
+ (headless, installation-bound console device authorization)
55197
55616
  logout Clear local cloud credentials
55617
+ doctor [--json] Verify profile, credential authority, renewal, endpoint,
55618
+ and authentication without printing secrets (alias: verify)
55198
55619
  link Corrective project link (ambient linking covers the happy path).
55199
55620
  Flags: --project <id> adopt an existing project (fork\u2192upstream);
55200
55621
  --remote <name> derive the locator from a non-origin remote;
@@ -55203,6 +55624,12 @@ Commands:
55203
55624
  use [<handle>] Set the sticky session active agent the daemon acts as.
55204
55625
  No arg lists the agents you can act as + the current one;
55205
55626
  --none (or --clear) reverts to your default identity.
55627
+ profile List named installation profiles and the active one.
55628
+ profile save <name> --installation <uuid>
55629
+ Bind the current credential to a named Console installation.
55630
+ profile use <name> Switch credential + agent context to an installed profile.
55631
+ profile remove <name>
55632
+ Remove a local profile (does not revoke it in the Console).
55206
55633
  sync now Flush outbox to the cloud once
55207
55634
  privacy Show what is collected/scrubbed + your consent state
55208
55635
  consent <channel> <on|off>
@@ -55215,7 +55642,8 @@ Commands:
55215
55642
 
55216
55643
  Environment:
55217
55644
  ERRATA_CLOUD_URL Cloud base URL (default ${DEFAULT_CLOUD_URL})
55218
- ERRATA_ALLOW_DIRECT_V1_CLOUD=1 Allow non-local legacy v1 cloud testing
55645
+ ERRATA_ALLOW_DIRECT_V1_CLOUD=1 Staff-only: request direct legacy v1 (bypasses the gateway;
55646
+ also requires ERRATA_DIRECT_V1_ACK=I_ACCEPT_UNMETERED_DIRECT_V1)
55219
55647
  ${OAUTH_DEFAULT_ENV}=0 Test/dev escape hatch: plain \`errata login\` uses legacy device-code
55220
55648
  `);
55221
55649
  }
@@ -55237,9 +55665,7 @@ function resolveHookPort(explicit) {
55237
55665
  async function cmdInit() {
55238
55666
  const skipHooks = rest.includes("--skip-hooks");
55239
55667
  const portIdx = rest.indexOf("--port");
55240
- const port = resolveHookPort(
55241
- portIdx >= 0 && rest[portIdx + 1] ? Number(rest[portIdx + 1]) : void 0
55242
- );
55668
+ const port = resolveHookPort(portIdx >= 0 && rest[portIdx + 1] ? Number(rest[portIdx + 1]) : void 0);
55243
55669
  const existing = loadProfile(ROOT);
55244
55670
  if (existing) {
55245
55671
  console.log(`already initialized: ${existing.id} (${existing.name})`);
@@ -55356,9 +55782,7 @@ async function cmdStart() {
55356
55782
  if (claimed) {
55357
55783
  const adopted = autodetectProfile(ROOT);
55358
55784
  saveProfile(ROOT, adopted);
55359
- console.log(
55360
- `adopted: ${locator} is claimed by your org (project ${claimed.id}) \u2014 workspace initialized`
55361
- );
55785
+ console.log(`adopted: ${locator} is claimed by your org (project ${claimed.id}) \u2014 workspace initialized`);
55362
55786
  }
55363
55787
  } catch {
55364
55788
  }
@@ -55423,7 +55847,9 @@ async function cmdStatus() {
55423
55847
  console.log(` workspace: ${profile ? `${profile.name} (${profile.id})` : "(not initialized)"}`);
55424
55848
  if (profile) {
55425
55849
  const link = profile.projectId ? ` \u2192 project ${profile.projectId.slice(0, 8)}` : " (unlinked)";
55426
- console.log(` repo: ${profile.repoLocator ? `${profile.repoLocator}${link}` : "(no git remote \u2014 unlinked)"}`);
55850
+ console.log(
55851
+ ` repo: ${profile.repoLocator ? `${profile.repoLocator}${link}` : "(no git remote \u2014 unlinked)"}`
55852
+ );
55427
55853
  console.log(` stack: ${profile.stack.join(", ") || "(none)"}`);
55428
55854
  console.log(` languages: ${profile.languages.join(", ") || "(none)"}`);
55429
55855
  }
@@ -55445,12 +55871,13 @@ async function cmdStatus() {
55445
55871
  );
55446
55872
  console.log(` cloud:`);
55447
55873
  console.log(` url: ${cfg.cloudUrl}`);
55874
+ console.log(` profile: ${cfg.activeInstallationProfile ?? "(unbound legacy credential)"}`);
55448
55875
  if (!hasCloudCredential(cfg)) {
55449
55876
  console.log(` logged in: no`);
55450
55877
  } else if (cfg.accessToken) {
55451
55878
  console.log(` logged in: yes (${cfg.email}) \u2014 attested daemon, can contribute`);
55452
55879
  } else {
55453
- console.log(` logged in: yes (${cfg.email}) \u2014 READ-ONLY key; run \`errata login\` to attest for graph-write`);
55880
+ console.log(` logged in: yes (${cfg.email}) \u2014 key capability enforced by Console installation/scopes`);
55454
55881
  }
55455
55882
  const active = getActiveAgent(cfg);
55456
55883
  console.log(
@@ -55490,6 +55917,53 @@ async function cmdStatus() {
55490
55917
  \u26A1 update available: ${upd.current} \u2192 ${upd.latest} \u2014 run: errata update`);
55491
55918
  }
55492
55919
  }
55920
+ async function cmdWhoami(args2) {
55921
+ const cfg = loadConfig();
55922
+ if (!hasCloudCredential(cfg)) {
55923
+ console.error("not logged in \u2014 run `errata login` or select an installation profile");
55924
+ process.exitCode = 1;
55925
+ return;
55926
+ }
55927
+ try {
55928
+ const active = getActiveAgent(cfg);
55929
+ const response = await resolveWhoami(authedCloudClient(cfg), active?.handle ?? null);
55930
+ if (args2.includes("--json")) {
55931
+ console.log(JSON.stringify(response, null, 2));
55932
+ return;
55933
+ }
55934
+ for (const line of formatWhoami(response, cfg.activeInstallationProfile)) console.log(line);
55935
+ } catch (err2) {
55936
+ console.error(`could not resolve context: ${err2 instanceof Error ? err2.message : err2}`);
55937
+ process.exitCode = 1;
55938
+ }
55939
+ }
55940
+ async function cmdDoctor(args2) {
55941
+ const cfg = loadConfig();
55942
+ const report = await diagnoseInstallation(cfg, {
55943
+ inspect: () => inspectCloudEndpoint(cfg.cloudUrl),
55944
+ authenticate: async () => {
55945
+ const me = await authedCloudClient(cfg).me();
55946
+ return { handle: me.handle, tier: me.tier };
55947
+ }
55948
+ });
55949
+ if (args2.includes("--json")) {
55950
+ console.log(JSON.stringify(report, null, 2));
55951
+ } else {
55952
+ console.log(`inErrata doctor \u2014 ${report.ok ? "ready" : "needs attention"}`);
55953
+ console.log(` profile: ${report.profile ?? "(none)"}`);
55954
+ console.log(` installation: ${report.installationId ?? "(none)"}`);
55955
+ for (const check2 of report.checks) {
55956
+ const marker = check2.status === "pass" ? "\u2713" : check2.status === "warn" ? "!" : "\u2717";
55957
+ console.log(` ${marker} ${check2.id}: ${check2.detail}`);
55958
+ }
55959
+ if (!report.ok) {
55960
+ console.log(
55961
+ " recovery: `errata profile list`, then `errata profile use <name>`; re-login if authentication still fails."
55962
+ );
55963
+ }
55964
+ }
55965
+ if (!report.ok) process.exitCode = 1;
55966
+ }
55493
55967
  async function cmdUpdate(args2) {
55494
55968
  const cfg = loadConfig();
55495
55969
  let channel = cfg.updateChannel;
@@ -55550,6 +56024,10 @@ function parseFlags(args2) {
55550
56024
  else if (a === "--oauth") out2.oauth = true;
55551
56025
  else if (a === "--device") out2.device = true;
55552
56026
  else if (a === "--browser") out2.browser = true;
56027
+ else if (a === "--installation") out2.installation = args2[++i2];
56028
+ else if (a.startsWith("--installation=")) out2.installation = a.slice("--installation=".length);
56029
+ else if (a === "--profile") out2.profile = args2[++i2];
56030
+ else if (a.startsWith("--profile=")) out2.profile = a.slice("--profile=".length);
55553
56031
  else if (!a.startsWith("--")) out2._.push(a);
55554
56032
  }
55555
56033
  return out2;
@@ -55580,14 +56058,20 @@ async function cmdUsage() {
55580
56058
  console.log(` held: ${status.quota.units_reserved.toLocaleString()} units reserved by in-flight calls`);
55581
56059
  }
55582
56060
  console.log(` cost: $${(status.quota.estimated_cents_current / 100).toFixed(2)} estimated`);
55583
- console.log(` rate: $${(status.pricing.cents_per_1000_units / 100).toFixed(2)} / 1,000 units${status.pricing.estimate_only ? " (estimate, not an invoice)" : ""}`);
56061
+ console.log(
56062
+ ` rate: $${(status.pricing.cents_per_1000_units / 100).toFixed(2)} / 1,000 units${status.pricing.estimate_only ? " (estimate, not an invoice)" : ""}`
56063
+ );
55584
56064
  if (status.by_tool.length > 0) {
55585
56065
  console.log(" tools:");
55586
56066
  for (const row of [...status.by_tool].sort((a, b) => b.units - a.units).slice(0, 10)) {
55587
- console.log(` ${row.tool.padEnd(24)} ${row.units.toLocaleString().padStart(8)} units ${row.event_count.toLocaleString().padStart(6)} calls`);
56067
+ console.log(
56068
+ ` ${row.tool.padEnd(24)} ${row.units.toLocaleString().padStart(8)} units ${row.event_count.toLocaleString().padStart(6)} calls`
56069
+ );
55588
56070
  }
55589
56071
  }
55590
- console.log(` as of: ${status.as_of} (ledger + live reservations; poll again after ${Math.ceil(status.poll_after_ms / 1e3)}s)`);
56072
+ console.log(
56073
+ ` as of: ${status.as_of} (ledger + live reservations; poll again after ${Math.ceil(status.poll_after_ms / 1e3)}s)`
56074
+ );
55591
56075
  } catch (err2) {
55592
56076
  console.error(`usage unavailable: ${err2 instanceof Error ? err2.message : err2}`);
55593
56077
  process.exitCode = 1;
@@ -55647,6 +56131,7 @@ async function cmdLoginOAuth(cfg, useBrowserLoopback = false) {
55647
56131
  } catch (err2) {
55648
56132
  console.error(`oauth login failed: ${err2 instanceof Error ? err2.message : err2}`);
55649
56133
  if (!useBrowserLoopback) console.error(` try the browser flow: errata login --browser`);
56134
+ console.error(` use headless device login: errata login --device --installation <uuid>`);
55650
56135
  console.error(` or paste a key: errata login --token <key>`);
55651
56136
  process.exitCode = 1;
55652
56137
  return;
@@ -55672,6 +56157,68 @@ async function cmdLoginOAuth(cfg, useBrowserLoopback = false) {
55672
56157
  process.exitCode = 1;
55673
56158
  }
55674
56159
  }
56160
+ async function cmdLoginOAuthDevice(cfg, input) {
56161
+ const profileName = input.profileName?.trim() || `device-${input.installationId.slice(0, 8)}`;
56162
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.installationId)) {
56163
+ console.error("--installation must be a UUID from Console \u2192 Connect");
56164
+ process.exitCode = 1;
56165
+ return;
56166
+ }
56167
+ const inspection = await inspectCloudEndpoint(cfg.cloudUrl);
56168
+ try {
56169
+ assertCloudEndpointAllowed(cfg.cloudUrl, inspection);
56170
+ } catch (err2) {
56171
+ console.error(err2 instanceof Error ? err2.message : cloudPolicyMessage(cfg, inspection));
56172
+ process.exitCode = 1;
56173
+ return;
56174
+ }
56175
+ try {
56176
+ const result = await loginOAuthDevice({
56177
+ cloudUrl: cfg.cloudUrl,
56178
+ installationId: input.installationId,
56179
+ timeoutMs: 15 * 6e4,
56180
+ onVerification: ({ url: url2, userCode, expiresInSec }) => {
56181
+ console.log(`approve this device in the Console:
56182
+ ${url2}`);
56183
+ console.log(`code: ${userCode} \xB7 expires in ${Math.round(expiresInSec / 60)} minutes`);
56184
+ }
56185
+ });
56186
+ const nextCfg = {
56187
+ ...cfg,
56188
+ apiKey: null,
56189
+ accessToken: result.tokens.accessToken,
56190
+ refreshToken: result.tokens.refreshToken ?? null,
56191
+ tokenEndpoint: result.tokenEndpoint,
56192
+ oauthClientId: result.clientId,
56193
+ activeInstallationProfile: profileName,
56194
+ installationProfiles: {
56195
+ ...cfg.installationProfiles,
56196
+ [profileName]: {
56197
+ installationId: result.installationId,
56198
+ cloudUrl: cfg.cloudUrl,
56199
+ apiKey: null,
56200
+ accessToken: result.tokens.accessToken,
56201
+ refreshToken: result.tokens.refreshToken ?? null,
56202
+ tokenEndpoint: result.tokenEndpoint,
56203
+ oauthClientId: result.clientId,
56204
+ activeAgent: cfg.activeAgent
56205
+ }
56206
+ }
56207
+ };
56208
+ const me = await authedCloudClient(nextCfg).me();
56209
+ nextCfg.userId = me.agentId;
56210
+ nextCfg.email = me.handle;
56211
+ saveConfig(nextCfg);
56212
+ console.log(
56213
+ `logged in as ${me.handle} (${me.tier}) \xB7 profile ${profileName} \xB7 installation ${result.installationId}`
56214
+ );
56215
+ await linkRegisteredWorkspaces(nextCfg);
56216
+ } catch (err2) {
56217
+ console.error(`device login failed: ${err2 instanceof Error ? err2.message : err2}`);
56218
+ console.error(" no credential was stored; retry from Console \u2192 Connect");
56219
+ process.exitCode = 1;
56220
+ }
56221
+ }
55675
56222
  async function linkRegisteredWorkspaces(cfg) {
55676
56223
  if (!cfg.consent.sync) return;
55677
56224
  const client = authedCloudClient(cfg);
@@ -55682,9 +56229,7 @@ async function linkRegisteredWorkspaces(cfg) {
55682
56229
  refreshRepoLocator(w.path, profile);
55683
56230
  const out2 = await ensureProjectLink(w.path, profile, client);
55684
56231
  if (out2.linked) {
55685
- console.log(
55686
- ` linked ${profile.name} \u2192 project ${out2.projectId}${out2.created ? " (created)" : ""}`
55687
- );
56232
+ console.log(` linked ${profile.name} \u2192 project ${out2.projectId}${out2.created ? " (created)" : ""}`);
55688
56233
  }
55689
56234
  } catch {
55690
56235
  }
@@ -55766,7 +56311,9 @@ async function cmdUnlink() {
55766
56311
  delete profile.projectLocator;
55767
56312
  profile.projectLinkDisabled = true;
55768
56313
  saveProfile(ROOT, profile);
55769
- console.log(had ? `unlinked from project ${had} (ambient linking disabled \u2014 re-enable with \`errata link\`)` : "already unlinked (ambient linking disabled)");
56314
+ console.log(
56315
+ had ? `unlinked from project ${had} (ambient linking disabled \u2014 re-enable with \`errata link\`)` : "already unlinked (ambient linking disabled)"
56316
+ );
55770
56317
  }
55771
56318
  async function cmdLogin() {
55772
56319
  const cfg = loadConfig();
@@ -55776,6 +56323,18 @@ async function cmdLogin() {
55776
56323
  await applyToken(cfg, flags2.token);
55777
56324
  return;
55778
56325
  }
56326
+ if (flags2.device) {
56327
+ if (!flags2.installation) {
56328
+ console.error("device login requires --installation <uuid> from Console \u2192 Connect");
56329
+ process.exitCode = 1;
56330
+ return;
56331
+ }
56332
+ await cmdLoginOAuthDevice(cfg, {
56333
+ installationId: flags2.installation,
56334
+ ...flags2.profile ? { profileName: flags2.profile } : {}
56335
+ });
56336
+ return;
56337
+ }
55779
56338
  if (shouldUseOAuthLogin(flags2)) {
55780
56339
  await cmdLoginOAuth(cfg, flags2.browser ?? false);
55781
56340
  return;
@@ -55810,7 +56369,9 @@ async function cmdLogin() {
55810
56369
  await warnIfApprovalUnreachable(dc.verificationUrl);
55811
56370
  console.log(`approve this device in your browser:
55812
56371
  ${dc.verificationUrl}`);
55813
- console.log(`(code expires in ${Math.round(dc.expiresIn / 60)} minutes \u2014 or paste a key: errata login --token <key>)`);
56372
+ console.log(
56373
+ `(code expires in ${Math.round(dc.expiresIn / 60)} minutes \u2014 or paste a key: errata login --token <key>)`
56374
+ );
55814
56375
  const deadline = Date.now() + dc.expiresIn * 1e3;
55815
56376
  const intervalMs = Math.max(2, dc.interval) * 1e3;
55816
56377
  for (; ; ) {
@@ -55879,6 +56440,48 @@ async function cmdUse(args2) {
55879
56440
  const code = await runUse(action, ctx);
55880
56441
  if (code !== 0) process.exitCode = code;
55881
56442
  }
56443
+ function cmdInstallationProfile(args2) {
56444
+ const cfg = loadConfig();
56445
+ const action = args2[0] ?? "list";
56446
+ if (action === "list") {
56447
+ const entries = Object.entries(cfg.installationProfiles).sort(([a], [b]) => a.localeCompare(b));
56448
+ if (entries.length === 0) {
56449
+ console.log("(no installation profiles \u2014 create one in the Console, then run profile save)");
56450
+ return;
56451
+ }
56452
+ for (const [name3, profile] of entries) {
56453
+ const marker = cfg.activeInstallationProfile === name3 ? "*" : " ";
56454
+ console.log(`${marker} ${name3} \xB7 ${profile.installationId} \xB7 ${profile.cloudUrl}`);
56455
+ }
56456
+ return;
56457
+ }
56458
+ const name2 = args2[1]?.trim();
56459
+ if (!name2) throw new Error(`usage: errata profile ${action} <name>`);
56460
+ if (action === "use") {
56461
+ const selected = useInstallationProfile(name2, cfg);
56462
+ const profile = selected.installationProfiles[name2];
56463
+ console.log(`profile active: ${name2} \xB7 installation ${profile.installationId}`);
56464
+ return;
56465
+ }
56466
+ if (action === "remove") {
56467
+ removeInstallationProfile(name2, cfg);
56468
+ console.log(`profile removed locally: ${name2}`);
56469
+ return;
56470
+ }
56471
+ if (action === "save") {
56472
+ const direct = args2.find((arg) => arg.startsWith("--installation="))?.slice("--installation=".length);
56473
+ const flag = args2.indexOf("--installation");
56474
+ const installationId = direct ?? (flag >= 0 ? args2[flag + 1] : void 0);
56475
+ if (!installationId) throw new Error("usage: errata profile save <name> --installation <uuid>");
56476
+ if (!hasCloudCredential(cfg)) {
56477
+ throw new Error("not logged in \u2014 install the Console credential before saving a profile");
56478
+ }
56479
+ saveInstallationProfile(name2, installationId, cfg);
56480
+ console.log(`profile saved: ${name2} \xB7 installation ${installationId}`);
56481
+ return;
56482
+ }
56483
+ throw new Error(`unknown profile action: ${action}`);
56484
+ }
55882
56485
  async function cmdReview() {
55883
56486
  const paths = workspacePaths(ROOT);
55884
56487
  if (!existsSync24(paths.reviewQueue)) {
@@ -55959,9 +56562,7 @@ async function cmdLocate(relPath) {
55959
56562
  const file2 = findFileByPath2(store, relPath);
55960
56563
  if (!file2) {
55961
56564
  console.error(`not indexed: ${relPath}`);
55962
- console.error(
55963
- " \u2192 did you run `errata reindex --clean`? did you spell the relative path right?"
55964
- );
56565
+ console.error(" \u2192 did you run `errata reindex --clean`? did you spell the relative path right?");
55965
56566
  process.exit(2);
55966
56567
  }
55967
56568
  const symbols = store.outEdges(file2.id, ["DEFINES", "CONTAINS"]).map((e) => store.getNode(e.to)).filter((n) => n != null);
@@ -56209,9 +56810,7 @@ async function cmdSearch(args2) {
56209
56810
  for (const h of dual.results) {
56210
56811
  console.log(`${h.id}`);
56211
56812
  console.log(` [${h.label}] ${h.name.slice(0, 100)}`);
56212
- console.log(
56213
- ` score ${Number(h.score).toFixed(4)}${h.provenance ? ` \xB7 ${h.provenance}` : ""}`
56214
- );
56813
+ console.log(` score ${Number(h.score).toFixed(4)}${h.provenance ? ` \xB7 ${h.provenance}` : ""}`);
56215
56814
  }
56216
56815
  return;
56217
56816
  }
@@ -56321,9 +56920,7 @@ async function cmdSimilar(args2) {
56321
56920
  }
56322
56921
  console.log(`seed: ${r.seedId}`);
56323
56922
  for (const h of r.hits) {
56324
- console.log(
56325
- ` ${h.score.toFixed(4)} [${h.label}] ${h.name.slice(0, 80)} (${h.id})`
56326
- );
56923
+ console.log(` ${h.score.toFixed(4)} [${h.label}] ${h.name.slice(0, 80)} (${h.id})`);
56327
56924
  }
56328
56925
  });
56329
56926
  }
@@ -56355,7 +56952,10 @@ unresolved \u2014 no node matched "${seed}". Try \`errata search ${seed}\` for f
56355
56952
  return;
56356
56953
  }
56357
56954
  process.stdout.write(
56358
- formatBurstMd2(result, { limit, seedLabel: seed })
56955
+ formatBurstMd2(result, {
56956
+ limit,
56957
+ seedLabel: seed
56958
+ })
56359
56959
  );
56360
56960
  });
56361
56961
  }
@@ -56433,8 +57033,18 @@ async function gatherRepo(store, ws) {
56433
57033
  const sol = firstHop(store, p.id, ["SOLVED_BY", "FIXED_BY"]);
56434
57034
  const cause = firstHop(store, p.id, ["CAUSED_BY", "MANIFESTS_AS"]);
56435
57035
  const pi = addSpine(p, "Problem");
56436
- if (cause) spineEdges.push({ a: pi, b: addSpine(cause, spineType(cause.label, "RootCause")), kind: "causal" });
56437
- if (sol) spineEdges.push({ a: pi, b: addSpine(sol, spineType(sol.label, "Solution")), kind: "causal" });
57036
+ if (cause)
57037
+ spineEdges.push({
57038
+ a: pi,
57039
+ b: addSpine(cause, spineType(cause.label, "RootCause")),
57040
+ kind: "causal"
57041
+ });
57042
+ if (sol)
57043
+ spineEdges.push({
57044
+ a: pi,
57045
+ b: addSpine(sol, spineType(sol.label, "Solution")),
57046
+ kind: "causal"
57047
+ });
56438
57048
  if (pIdx >= LEARNED_MAX) continue;
56439
57049
  const srcCount = Array.isArray(p.attrs["sources"]) ? p.attrs["sources"].length : p.attrs["sources"] ? 1 : 0;
56440
57050
  const agents = Math.max(1, srcCount + Number(p.attrs["corroborations"] ?? 0));
@@ -56499,11 +57109,21 @@ async function gatherRepo(store, ws) {
56499
57109
  if (byFile.size > 0) {
56500
57110
  const ranked = [...byFile].sort((a, b) => b[1] - a[1]).slice(0, 6);
56501
57111
  const maxC = ranked[0]?.[1] ?? 1;
56502
- hotspots = ranked.map(([file2, c]) => ({ file: file2.split(/[\\/]/).pop() ?? file2, problemCount: c, weight: c / maxC, unit: "probs" }));
57112
+ hotspots = ranked.map(([file2, c]) => ({
57113
+ file: file2.split(/[\\/]/).pop() ?? file2,
57114
+ problemCount: c,
57115
+ weight: c / maxC,
57116
+ unit: "probs"
57117
+ }));
56503
57118
  } else {
56504
57119
  const fan = await runTool2("errata.hotspots", { kind: "fan-in", limit: 6 }, store);
56505
57120
  const maxF = fan.items[0]?.fanIn ?? 1;
56506
- hotspots = fan.items.map((h) => ({ file: h.name.split(/[\\/]/).pop() ?? h.name, problemCount: h.fanIn, weight: (h.fanIn ?? 0) / maxF, unit: "fan-in" }));
57121
+ hotspots = fan.items.map((h) => ({
57122
+ file: h.name.split(/[\\/]/).pop() ?? h.name,
57123
+ problemCount: h.fanIn,
57124
+ weight: (h.fanIn ?? 0) / maxF,
57125
+ unit: "fan-in"
57126
+ }));
56507
57127
  }
56508
57128
  const revisit = await runTool2("errata.needs_revisit", {}, store);
56509
57129
  const machineOnly = SEMANTIC_LABELS2.reduce((sum, l) => sum + store.findNodesByLabel(l).length, 0);
@@ -56594,14 +57214,14 @@ async function cmdReport(args2) {
56594
57214
  for (const f of files) writeFileSync18(join26(outDir, f.name), f.html, "utf8");
56595
57215
  const indexPath = join26(outDir, "report.html");
56596
57216
  console.log(`report \u2192 ${indexPath}`);
56597
- console.log(` ${data.repos.length} repo(s) \xB7 ${files.length} file(s) \xB7 ${data.rollup.nodes.toLocaleString("en-US")} nodes`);
57217
+ console.log(
57218
+ ` ${data.repos.length} repo(s) \xB7 ${files.length} file(s) \xB7 ${data.rollup.nodes.toLocaleString("en-US")} nodes`
57219
+ );
56598
57220
  console.log(` open: file://${indexPath.replace(/\\/g, "/")}`);
56599
57221
  }
56600
57222
  async function cmdStop() {
56601
57223
  const { unlinkSync: unlinkSync4 } = await import("node:fs");
56602
- const lockPath = [globalDaemonLock(), workspacePaths(ROOT).daemonLock].find(
56603
- (p) => readDaemonLock(p) !== null
56604
- );
57224
+ const lockPath = [globalDaemonLock(), workspacePaths(ROOT).daemonLock].find((p) => readDaemonLock(p) !== null);
56605
57225
  if (!lockPath) {
56606
57226
  console.log(`no daemon lock found \u2014 nothing to stop`);
56607
57227
  return;
@@ -56676,9 +57296,7 @@ function ensureSingletonRunning() {
56676
57296
  async function cmdInstallHooks(args2) {
56677
57297
  const harness = (args2[0] && !args2[0].startsWith("-") ? args2[0] : "claude").toLowerCase();
56678
57298
  const portIdx = args2.indexOf("--port");
56679
- const port = resolveHookPort(
56680
- portIdx >= 0 && args2[portIdx + 1] ? Number(args2[portIdx + 1]) : void 0
56681
- );
57299
+ const port = resolveHookPort(portIdx >= 0 && args2[portIdx + 1] ? Number(args2[portIdx + 1]) : void 0);
56682
57300
  switch (harness) {
56683
57301
  case "claude":
56684
57302
  await installClaudeHooks(port);
@@ -56984,7 +57602,13 @@ async function cmdDash(args2) {
56984
57602
  const reindexOnStart = !args2.includes("--no-reindex");
56985
57603
  const skipEmbed = !args2.includes("--embed");
56986
57604
  const skipWatchers = args2.includes("--no-watch");
56987
- const handle2 = await startMultiDaemon({ webPort: port, reindexOnStart, skipEmbed, skipWatchers, updateCheck: true });
57605
+ const handle2 = await startMultiDaemon({
57606
+ webPort: port,
57607
+ reindexOnStart,
57608
+ skipEmbed,
57609
+ skipWatchers,
57610
+ updateCheck: true
57611
+ });
56988
57612
  console.log(`errata multi-daemon running`);
56989
57613
  console.log(` endpoint: ${handle2.url} (JSON \u2014 the human view is \`errata report\`)`);
56990
57614
  console.log(` projects: ${handle2.records.length}`);
@@ -57041,7 +57665,10 @@ async function cmdDash(args2) {
57041
57665
  scheduleQuiescenceFlush();
57042
57666
  }
57043
57667
  if (notifyEnabled && (r.problemsResolved || r.reviewsTriggered)) {
57044
- notifyTick({ problemsResolved: r.problemsResolved, reviewsTriggered: r.reviewsTriggered });
57668
+ notifyTick({
57669
+ problemsResolved: r.problemsResolved,
57670
+ reviewsTriggered: r.reviewsTriggered
57671
+ });
57045
57672
  }
57046
57673
  }
57047
57674
  }).finally(() => {
@@ -57067,7 +57694,10 @@ async function cmdDash(args2) {
57067
57694
  }
57068
57695
  }
57069
57696
  if (skillsLearned > 0) {
57070
- notifyEvent("skill-learned", skillsLearned === 1 ? "Distilled a new reusable skill from your work." : `Distilled ${skillsLearned} new skills from your work.`);
57697
+ notifyEvent(
57698
+ "skill-learned",
57699
+ skillsLearned === 1 ? "Distilled a new reusable skill from your work." : `Distilled ${skillsLearned} new skills from your work.`
57700
+ );
57071
57701
  }
57072
57702
  const p = await handle2.percolateAll();
57073
57703
  const touched = /* @__PURE__ */ new Set();
@@ -57158,7 +57788,9 @@ async function cmdDash(args2) {
57158
57788
  const fanIn = wStore.inEdges(anchor.to, [...CODE_REACH_EDGES]).length;
57159
57789
  if (fanIn < HOTSPOT_FANIN) continue;
57160
57790
  const sym = wStore.getNode(anchor.to)?.description ?? "a symbol";
57161
- notifyEvent("hotspot-problem", `"${p2.description.slice(0, 60)}" touches ${sym} (${fanIn} dependents).`, { key: p2.id });
57791
+ notifyEvent("hotspot-problem", `"${p2.description.slice(0, 60)}" touches ${sym} (${fanIn} dependents).`, {
57792
+ key: p2.id
57793
+ });
57162
57794
  }
57163
57795
  }
57164
57796
  void handle2.syncPrinciplesPublic().then((r) => {
@@ -57188,7 +57820,9 @@ async function cmdDash(args2) {
57188
57820
  try {
57189
57821
  await materializeOverview2(r.root, r.engine.store);
57190
57822
  } catch (err2) {
57191
- console.warn(`[errata] overview refresh failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
57823
+ console.warn(
57824
+ `[errata] overview refresh failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
57825
+ );
57192
57826
  }
57193
57827
  }
57194
57828
  maybeFlushDigests();
@@ -57203,7 +57837,12 @@ async function cmdDash(args2) {
57203
57837
  if (consolidating) return;
57204
57838
  if (process.uptime() < CONSOLIDATE_BOOT_GRACE_S) return;
57205
57839
  consolidationState.momentum = totalMutations() - consolidatedAtMutations;
57206
- if (!shouldConsolidate({ now: Date.now(), state: consolidationState, policy: consolidationPolicy, force })) {
57840
+ if (!shouldConsolidate({
57841
+ now: Date.now(),
57842
+ state: consolidationState,
57843
+ policy: consolidationPolicy,
57844
+ force
57845
+ })) {
57207
57846
  return;
57208
57847
  }
57209
57848
  consolidating = true;
@@ -57301,9 +57940,7 @@ async function cmdFeedback(args2) {
57301
57940
  }
57302
57941
  async function ensureProfile() {
57303
57942
  if (!loadProfile(ROOT)) {
57304
- console.error(
57305
- `no errata workspace in ${ROOT}. Run 'errata init' first.`
57306
- );
57943
+ console.error(`no errata workspace in ${ROOT}. Run 'errata init' first.`);
57307
57944
  process.exit(2);
57308
57945
  }
57309
57946
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.0-dev.97",
3
+ "version": "2.0.1-dev.100",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {