convene-cli 1.2.0 → 1.4.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/api.js CHANGED
@@ -149,6 +149,29 @@ class ConveneApi {
149
149
  getProject(slug, timeoutMs) {
150
150
  return this.request('GET', `/projects/${encodeURIComponent(slug)}`, { timeoutMs });
151
151
  }
152
+ /**
153
+ * POST /projects/:slug/best-practices — report the adopted best-practices
154
+ * manifest so the dashboard can show per-project adoption (Phase 5). PURELY
155
+ * for observability: ALWAYS best-effort / fail-open. The shared `request`
156
+ * already never throws (it returns `ok:false` on a network error / timeout),
157
+ * so the call sites can fire-and-forget. Bounded by an explicit short timeout
158
+ * — onboarding / update must never slow down because the report is slow.
159
+ */
160
+ reportBestPractices(slug, manifest, timeoutMs) {
161
+ return this.request('POST', `/projects/${encodeURIComponent(slug)}/best-practices`, {
162
+ body: { manifest },
163
+ timeoutMs,
164
+ });
165
+ }
166
+ /**
167
+ * GET /catalog — the canonical best-practices catalog (PUBLIC, no tenant data).
168
+ * The CLI prefers this live read but is fully fail-soft: on any non-ok / network
169
+ * error the caller falls back to the bundled offline mirror. Bounded by a short
170
+ * timeout — never the 10s default.
171
+ */
172
+ getCatalog(timeoutMs) {
173
+ return this.request('GET', '/catalog', { timeoutMs });
174
+ }
152
175
  // ── Catch-up / session-open (WP2) ──────────────────────────────────────────
153
176
  /**
154
177
  * GET /session-open — the "open already knowing the world" digest. `advance`
package/dist/cache.js CHANGED
@@ -3,10 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeWatchHighWater = void 0;
6
+ exports.OVERRIDE_TTL_MS = exports.writeWatchHighWater = void 0;
7
7
  exports.readCache = readCache;
8
8
  exports.writeCache = writeCache;
9
9
  exports.ageSeconds = ageSeconds;
10
+ exports.readCatalogVersion = readCatalogVersion;
11
+ exports.writeCatalogVersion = writeCatalogVersion;
10
12
  exports.readSessionInstance = readSessionInstance;
11
13
  exports.mintSessionInstance = mintSessionInstance;
12
14
  exports.ensureSessionInstance = ensureSessionInstance;
@@ -20,6 +22,8 @@ exports.appendWatch = appendWatch;
20
22
  exports.readWatchSince = readWatchSince;
21
23
  exports.touchWatchHeartbeat = touchWatchHeartbeat;
22
24
  exports.watchHeartbeatAgeSec = watchHeartbeatAgeSec;
25
+ exports.writeOverrideToken = writeOverrideToken;
26
+ exports.readLiveOverrideToken = readLiveOverrideToken;
23
27
  /**
24
28
  * Tiny per-project file cache so rapid successive prompts don't each hit the
25
29
  * network (P0-LATENCY). Short TTL; on a fetch failure the stale cache still
@@ -63,6 +67,33 @@ function writeCache(slug, data) {
63
67
  function ageSeconds(entry) {
64
68
  return Math.max(0, Math.round((Date.now() - entry.fetchedAt) / 1000));
65
69
  }
70
+ function catalogVersionFile() {
71
+ return node_path_1.default.join(config_1.CACHE_DIR, 'catalog-version.json');
72
+ }
73
+ /** The cached live catalog version if present AND within `ttlSec`, else null. */
74
+ function readCatalogVersion(ttlSec) {
75
+ try {
76
+ const e = JSON.parse(node_fs_1.default.readFileSync(catalogVersionFile(), 'utf8'));
77
+ if (!e || typeof e.version !== 'string' || typeof e.fetchedAt !== 'number')
78
+ return null;
79
+ if ((Date.now() - e.fetchedAt) / 1000 >= ttlSec)
80
+ return null;
81
+ return e.version || null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** Persist the live catalog version (best-effort; never throws). */
88
+ function writeCatalogVersion(version) {
89
+ try {
90
+ node_fs_1.default.mkdirSync(config_1.CACHE_DIR, { recursive: true, mode: 0o700 });
91
+ node_fs_1.default.writeFileSync(catalogVersionFile(), JSON.stringify({ fetchedAt: Date.now(), version }), { mode: 0o600 });
92
+ }
93
+ catch {
94
+ /* best-effort */
95
+ }
96
+ }
66
97
  // ── session-instance id (WP2) ────────────────────────────────────────────────
67
98
  // An opaque per-session UUID minted at SessionStart and persisted in CACHE_DIR.
68
99
  // Sent as X-Convene-Session-Instance so the server can stamp holder_instance and
@@ -296,3 +327,54 @@ function watchHeartbeatAgeSec(slug) {
296
327
  return null;
297
328
  }
298
329
  }
330
+ // ── best-practice override token (Phase 3) ───────────────────────────────────
331
+ // `convene override <id> --reason …` writes a short-TTL, expiry-based token that
332
+ // `convene practice-guard <id>` honors → ALLOW. The token is purely LOCAL state
333
+ // (the bus status post is informational/attribution, never the gate's source of
334
+ // truth) and SESSION-SCOPED via `scoped()` so one session's override does not
335
+ // silently lift the gate for a sibling session sharing the checkout. Keyed by
336
+ // (slug,id). Expiry-based: honored repeatedly until it expires (the simplest
337
+ // correct semantics — a brief, attributed window, not a single-shot counter).
338
+ function overrideFile(slug, id) {
339
+ const key = `${scoped(slug)}__bp-override__${id}`.replace(/[^a-zA-Z0-9_-]/g, '_');
340
+ return node_path_1.default.join(config_1.CACHE_DIR, `${key}.json`);
341
+ }
342
+ /** Default override window — short on purpose (a deliberate, brief bypass). */
343
+ exports.OVERRIDE_TTL_MS = 5 * 60 * 1000;
344
+ /** Write an expiry-based override token for (slug,id). Best-effort; throws never. */
345
+ function writeOverrideToken(slug, id, reason, ttlMs = exports.OVERRIDE_TTL_MS) {
346
+ const tok = { slug, id, reason, expiresAt: Date.now() + ttlMs };
347
+ try {
348
+ node_fs_1.default.mkdirSync(config_1.CACHE_DIR, { recursive: true, mode: 0o700 });
349
+ node_fs_1.default.writeFileSync(overrideFile(slug, id), JSON.stringify(tok), { mode: 0o600 });
350
+ }
351
+ catch {
352
+ /* best-effort; the caller still reports the in-memory token */
353
+ }
354
+ return tok;
355
+ }
356
+ /**
357
+ * Return a LIVE (unexpired) override token for (slug,id), or null. An expired or
358
+ * malformed token is treated as absent AND opportunistically removed so a stale
359
+ * file can never resurrect a gate bypass.
360
+ */
361
+ function readLiveOverrideToken(slug, id) {
362
+ const file = overrideFile(slug, id);
363
+ let tok;
364
+ try {
365
+ tok = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
366
+ }
367
+ catch {
368
+ return null;
369
+ }
370
+ if (!tok || typeof tok.expiresAt !== 'number' || !Number.isFinite(tok.expiresAt) || Date.now() >= tok.expiresAt) {
371
+ try {
372
+ node_fs_1.default.unlinkSync(file);
373
+ }
374
+ catch {
375
+ /* best-effort cleanup */
376
+ }
377
+ return null;
378
+ }
379
+ return tok;
380
+ }