convene-cli 1.11.0 → 1.12.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 +10 -0
- package/dist/brand.js +2 -2
- package/dist/cache.js +29 -85
- package/dist/commands/enroll.js +72 -0
- package/dist/commands/join.js +20 -2
- package/dist/commands/watch.js +30 -48
- package/dist/index.js +10 -0
- package/package.json +2 -2
package/dist/api.js
CHANGED
|
@@ -42,6 +42,8 @@ class ConveneApi {
|
|
|
42
42
|
headers['x-convene-session-instance'] = this.instance;
|
|
43
43
|
if (opts.idempotencyKey)
|
|
44
44
|
headers['idempotency-key'] = opts.idempotencyKey;
|
|
45
|
+
if (opts.headers)
|
|
46
|
+
Object.assign(headers, opts.headers);
|
|
45
47
|
const res = await fetch(`${this.baseUrl}${brand_1.BRAND.apiBase}${apiPath}`, {
|
|
46
48
|
method,
|
|
47
49
|
headers,
|
|
@@ -154,6 +156,14 @@ class ConveneApi {
|
|
|
154
156
|
join(slug, body, timeoutMs) {
|
|
155
157
|
return this.request('POST', `/projects/${encodeURIComponent(slug)}/join`, { body, timeoutMs });
|
|
156
158
|
}
|
|
159
|
+
/** Device enrollment — start (no bearer). Emails the member a confirm link if one exists. */
|
|
160
|
+
enrollStart(body, timeoutMs) {
|
|
161
|
+
return this.request('POST', '/enroll/device/start', { body, timeoutMs });
|
|
162
|
+
}
|
|
163
|
+
/** Device enrollment — poll for the minted key, presenting the raw device secret (no bearer). */
|
|
164
|
+
enrollPoll(enrollmentId, rawSecret, timeoutMs) {
|
|
165
|
+
return this.request('GET', `/enroll/device/poll?id=${encodeURIComponent(String(enrollmentId))}`, { headers: { 'x-device-secret': rawSecret }, timeoutMs });
|
|
166
|
+
}
|
|
157
167
|
/** Self-recovery: re-grant your own membership via the committed join token (no bearer
|
|
158
168
|
* auth required). Restores OWNER only from a server-recorded prior-owner row. */
|
|
159
169
|
reclaim(slug, body, timeoutMs) {
|
package/dist/brand.js
CHANGED
|
@@ -13,8 +13,8 @@ exports.BRAND = {
|
|
|
13
13
|
product: 'Convene',
|
|
14
14
|
slug: 'convene',
|
|
15
15
|
bin: 'convene',
|
|
16
|
-
domain: '
|
|
17
|
-
baseUrl: 'https://
|
|
16
|
+
domain: 'convene.live',
|
|
17
|
+
baseUrl: 'https://convene.live',
|
|
18
18
|
/** Public product / front-door base URL for human-facing links (dashboard, /start, docs). */
|
|
19
19
|
siteDomain: 'convene.live',
|
|
20
20
|
siteUrl: 'https://convene.live',
|
package/dist/cache.js
CHANGED
|
@@ -3,7 +3,7 @@ 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.OVERRIDE_TTL_MS = exports.
|
|
6
|
+
exports.OVERRIDE_TTL_MS = exports.LIVE_SESSION_RECENT_SEC = exports.LIVE_SESSION_WINDOW_SEC = void 0;
|
|
7
7
|
exports.readCache = readCache;
|
|
8
8
|
exports.writeCache = writeCache;
|
|
9
9
|
exports.ageSeconds = ageSeconds;
|
|
@@ -25,11 +25,8 @@ exports.announceAlreadyPosted = announceAlreadyPosted;
|
|
|
25
25
|
exports.markAnnounced = markAnnounced;
|
|
26
26
|
exports.readLastBroadcastSha = readLastBroadcastSha;
|
|
27
27
|
exports.writeLastBroadcastSha = writeLastBroadcastSha;
|
|
28
|
-
exports.
|
|
29
|
-
exports.
|
|
30
|
-
exports.appendWatchEntry = appendWatchEntry;
|
|
31
|
-
exports.appendWatch = appendWatch;
|
|
32
|
-
exports.readWatchSince = readWatchSince;
|
|
28
|
+
exports.readWatchCursor = readWatchCursor;
|
|
29
|
+
exports.persistWatchCursor = persistWatchCursor;
|
|
33
30
|
exports.touchWatchHeartbeat = touchWatchHeartbeat;
|
|
34
31
|
exports.watchHeartbeatAgeSec = watchHeartbeatAgeSec;
|
|
35
32
|
exports.writeWatchPid = writeWatchPid;
|
|
@@ -390,19 +387,32 @@ function writeLastBroadcastSha(slug, sha) {
|
|
|
390
387
|
/* best-effort */
|
|
391
388
|
}
|
|
392
389
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
390
|
+
// ── watch poll resume cursor (WP12) ──────────────────────────────────────────
|
|
391
|
+
// The `convene watch` daemon (cli/src/commands/watch.ts) long-polls the bus,
|
|
392
|
+
// stamps a liveness heartbeat each iteration (the health line reads its age), and
|
|
393
|
+
// optionally desktop-notifies on a directed halt. It persists a MONOTONIC poll
|
|
394
|
+
// cursor here so a relaunch (every SessionStart) resumes incrementally instead of
|
|
395
|
+
// re-scanning the backlog from zero.
|
|
396
|
+
//
|
|
397
|
+
// The daemon does NOT render anything into agent-facing context: directed halts
|
|
398
|
+
// reach the agent via the server-truth PULL surfaces (fetch / session-open /
|
|
399
|
+
// lane-state), which query the server — the single authority for halt state. (The
|
|
400
|
+
// daemon formerly APPENDED matched halts to a per-slug `.watch.jsonl` for a reader
|
|
401
|
+
// to drain, but that reader was never wired into any surface, and a local cache
|
|
402
|
+
// could drift from server truth and mislead. The jsonl was removed in favor of the
|
|
403
|
+
// pull path; only this resume cursor + the heartbeat remain.)
|
|
404
|
+
function watchCursorFile(slug) {
|
|
405
|
+
// Legacy `.watch.hw` (high-water) extension retained so existing caches aren't
|
|
406
|
+
// orphaned on upgrade — the file is now the daemon's own poll resume cursor.
|
|
397
407
|
return slugFile(slug, 'watch.hw');
|
|
398
408
|
}
|
|
399
409
|
function watchHeartbeatFile(slug) {
|
|
400
410
|
return slugFile(slug, 'watch.hb');
|
|
401
411
|
}
|
|
402
|
-
/** Read the persisted
|
|
403
|
-
function
|
|
412
|
+
/** Read the daemon's persisted poll resume cursor (0 if none). */
|
|
413
|
+
function readWatchCursor(slug) {
|
|
404
414
|
try {
|
|
405
|
-
const n = parseInt(node_fs_1.default.readFileSync(
|
|
415
|
+
const n = parseInt(node_fs_1.default.readFileSync(watchCursorFile(slug), 'utf8').trim(), 10);
|
|
406
416
|
return Number.isFinite(n) ? n : 0;
|
|
407
417
|
}
|
|
408
418
|
catch {
|
|
@@ -410,87 +420,21 @@ function readWatchHighWater(slug) {
|
|
|
410
420
|
}
|
|
411
421
|
}
|
|
412
422
|
/**
|
|
413
|
-
* Persist the
|
|
414
|
-
*
|
|
415
|
-
* already
|
|
423
|
+
* Persist the daemon's poll resume cursor — MONOTONIC. A lower seq is ignored
|
|
424
|
+
* (GREATEST semantics) so a relaunch never rewinds the cursor and re-scans
|
|
425
|
+
* material it already polled past. Best-effort; never throws.
|
|
416
426
|
*/
|
|
417
|
-
function
|
|
427
|
+
function persistWatchCursor(slug, seq) {
|
|
418
428
|
try {
|
|
419
429
|
node_fs_1.default.mkdirSync(config_1.CACHE_DIR, { recursive: true, mode: 0o700 });
|
|
420
|
-
const cur =
|
|
430
|
+
const cur = readWatchCursor(slug);
|
|
421
431
|
if (seq > cur)
|
|
422
|
-
node_fs_1.default.writeFileSync(
|
|
432
|
+
node_fs_1.default.writeFileSync(watchCursorFile(slug), String(seq) + '\n', { mode: 0o600 });
|
|
423
433
|
}
|
|
424
434
|
catch {
|
|
425
435
|
/* best-effort */
|
|
426
436
|
}
|
|
427
437
|
}
|
|
428
|
-
/** Back-compat alias for the WP2 stub name; identical monotonic behavior. */
|
|
429
|
-
exports.writeWatchHighWater = persistHighWater;
|
|
430
|
-
/**
|
|
431
|
-
* APPEND a watch entry to the jsonl (append-only — never rewrites the file).
|
|
432
|
-
* The append is atomic at the OS level for a single small write, so a concurrent
|
|
433
|
-
* reader sees whole lines. Best-effort; never throws (the watch daemon is
|
|
434
|
-
* fail-open). Entries WITHOUT a finite numeric seq are dropped — an un-keyed
|
|
435
|
-
* entry can't participate in the high-water drain and would be re-rendered
|
|
436
|
-
* forever.
|
|
437
|
-
*/
|
|
438
|
-
function appendWatchEntry(slug, entry) {
|
|
439
|
-
if (!entry || typeof entry.seq !== 'number' || !Number.isFinite(entry.seq))
|
|
440
|
-
return;
|
|
441
|
-
try {
|
|
442
|
-
node_fs_1.default.mkdirSync(config_1.CACHE_DIR, { recursive: true, mode: 0o700 });
|
|
443
|
-
node_fs_1.default.appendFileSync(watchFile(slug), JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
/* best-effort */
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
/** Back-compat alias for the WP2 stub name. */
|
|
450
|
-
function appendWatch(slug, entry) {
|
|
451
|
-
appendWatchEntry(slug, entry);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Read all watch entries with seq > highWater, ascending by seq, de-duplicated
|
|
455
|
-
* on seq (the daemon may re-append the same entry after a resume — the reader
|
|
456
|
-
* tolerates duplicates by keeping the first per seq). Does NOT advance the
|
|
457
|
-
* high-water and does NOT truncate — the caller decides what was actually
|
|
458
|
-
* rendered and calls persistHighWater(slug, lastRenderedSeq). A malformed line
|
|
459
|
-
* is skipped, never fatal.
|
|
460
|
-
*/
|
|
461
|
-
function readWatchSince(slug, highWater) {
|
|
462
|
-
let raw;
|
|
463
|
-
try {
|
|
464
|
-
raw = node_fs_1.default.readFileSync(watchFile(slug), 'utf8');
|
|
465
|
-
}
|
|
466
|
-
catch {
|
|
467
|
-
return [];
|
|
468
|
-
}
|
|
469
|
-
const seen = new Set();
|
|
470
|
-
const out = [];
|
|
471
|
-
for (const line of raw.split('\n')) {
|
|
472
|
-
const s = line.trim();
|
|
473
|
-
if (!s)
|
|
474
|
-
continue;
|
|
475
|
-
let e;
|
|
476
|
-
try {
|
|
477
|
-
e = JSON.parse(s);
|
|
478
|
-
}
|
|
479
|
-
catch {
|
|
480
|
-
continue; // a torn/partial line — skip, the next drain re-reads it whole
|
|
481
|
-
}
|
|
482
|
-
if (typeof e.seq !== 'number' || !Number.isFinite(e.seq))
|
|
483
|
-
continue;
|
|
484
|
-
if (e.seq <= highWater)
|
|
485
|
-
continue;
|
|
486
|
-
if (seen.has(e.seq))
|
|
487
|
-
continue;
|
|
488
|
-
seen.add(e.seq);
|
|
489
|
-
out.push(e);
|
|
490
|
-
}
|
|
491
|
-
out.sort((a, b) => a.seq - b.seq);
|
|
492
|
-
return out;
|
|
493
|
-
}
|
|
494
438
|
// ── watch heartbeat (WP12) ───────────────────────────────────────────────────
|
|
495
439
|
// The daemon stamps a heartbeat each loop iteration; the health line / doctor
|
|
496
440
|
// read its age. A stale/absent heartbeat ⇒ watch is down ⇒ surface DEGRADED.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.enrollDevice = enrollDevice;
|
|
7
|
+
/**
|
|
8
|
+
* `convene enroll-device` — connect THIS machine to your EXISTING identity without
|
|
9
|
+
* copying a key. Proves email ownership (an emailed confirm link) + device ownership
|
|
10
|
+
* (a CLI-held secret), then the server mints a fresh per-device key that ONLY this
|
|
11
|
+
* CLI can claim. Also auto-invoked by `convene setup`/`join` when the bus reports an
|
|
12
|
+
* identity already exists for your email (EMAIL_TAKEN).
|
|
13
|
+
*/
|
|
14
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
16
|
+
const api_1 = require("../api");
|
|
17
|
+
const config_1 = require("../config");
|
|
18
|
+
const git_1 = require("../git");
|
|
19
|
+
const ctx_1 = require("../ctx");
|
|
20
|
+
const log = (m) => process.stdout.write(m + '\n');
|
|
21
|
+
const sha256 = (s) => node_crypto_1.default.createHash('sha256').update(s, 'utf8').digest('hex');
|
|
22
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
/**
|
|
24
|
+
* Run the device-enrollment flow. Returns true once a fresh key is saved to the
|
|
25
|
+
* local config; on timeout / expiry it prints guidance and exits the process.
|
|
26
|
+
*/
|
|
27
|
+
async function enrollDevice(opts) {
|
|
28
|
+
const top = (0, git_1.gitToplevel)();
|
|
29
|
+
const proj = (0, config_1.loadProjectConfig)(top);
|
|
30
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
|
|
31
|
+
const baseUrl = (opts.baseUrl || cfg.baseUrl).replace(/\/$/, '');
|
|
32
|
+
const email = opts.email || (0, git_1.gitUserEmail)(top ?? undefined) || undefined;
|
|
33
|
+
if (!email)
|
|
34
|
+
(0, ctx_1.die)('no email to enroll — pass `--email <you@example.com>` (or set git user.email)');
|
|
35
|
+
// CLI-held secret: send only its hash to start; present the raw secret at poll so
|
|
36
|
+
// ONLY this process can claim the minted key (a browser that clicks the link cannot).
|
|
37
|
+
const rawSecret = node_crypto_1.default.randomBytes(32).toString('base64url');
|
|
38
|
+
const secretHash = sha256(rawSecret);
|
|
39
|
+
const label = (opts.label || node_os_1.default.hostname() || 'cli').slice(0, 80);
|
|
40
|
+
const api = new api_1.ConveneApi(baseUrl, null);
|
|
41
|
+
const start = await api.enrollStart({ email: email, secret_hash: secretHash, device_label: label }, 10_000);
|
|
42
|
+
if (!start.ok || !start.json) {
|
|
43
|
+
(0, ctx_1.die)(`could not start device enrollment (${start.status}): ${start.error ?? 'unreachable'}`);
|
|
44
|
+
}
|
|
45
|
+
const { enrollment_id, poll_interval_ms, expires_in_s } = start.json;
|
|
46
|
+
const ttlS = expires_in_s ?? 900;
|
|
47
|
+
const mins = Math.max(1, Math.round(ttlS / 60));
|
|
48
|
+
log('');
|
|
49
|
+
log(`📧 We emailed a confirmation link to ${email}.`);
|
|
50
|
+
log(` Open it and click "Authorize device" to connect this machine (expires in ${mins} min).`);
|
|
51
|
+
log(` Waiting…`);
|
|
52
|
+
const intervalMs = Math.max(1000, poll_interval_ms ?? 2000);
|
|
53
|
+
const deadline = Date.now() + ttlS * 1000;
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
await sleep(intervalMs);
|
|
56
|
+
const poll = await api.enrollPoll(enrollment_id, rawSecret, 8000);
|
|
57
|
+
const st = poll.json?.status;
|
|
58
|
+
if (st === 'ok' && poll.json?.api_key) {
|
|
59
|
+
const member = poll.json.member?.handle ?? '';
|
|
60
|
+
(0, config_1.saveFileConfig)({ apiKey: poll.json.api_key, baseUrl, member });
|
|
61
|
+
log('');
|
|
62
|
+
log(`✓ Authorized as ${member}. This machine is connected (config saved 0600).`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (st === 'expired') {
|
|
66
|
+
(0, ctx_1.die)('the confirmation link expired or was already used — run `convene setup` again to retry.');
|
|
67
|
+
}
|
|
68
|
+
// 'pending' / 'not_found' → the human hasn't confirmed yet; keep waiting.
|
|
69
|
+
}
|
|
70
|
+
(0, ctx_1.die)('timed out waiting for email confirmation — run `convene setup` again to retry.');
|
|
71
|
+
return false; // unreachable: die() exits.
|
|
72
|
+
}
|
package/dist/commands/join.js
CHANGED
|
@@ -15,6 +15,7 @@ const config_1 = require("../config");
|
|
|
15
15
|
const git_1 = require("../git");
|
|
16
16
|
const hook_1 = require("../hook");
|
|
17
17
|
const githook_1 = require("../githook");
|
|
18
|
+
const enroll_1 = require("./enroll");
|
|
18
19
|
const ctx_1 = require("../ctx");
|
|
19
20
|
const log = (m) => process.stdout.write(m + '\n');
|
|
20
21
|
async function join(opts) {
|
|
@@ -34,8 +35,25 @@ async function join(opts) {
|
|
|
34
35
|
// unauthenticated mode otherwise (creates a new identity + key).
|
|
35
36
|
const api = new api_1.ConveneApi(baseUrl, cfg.apiKey ?? null);
|
|
36
37
|
const res = await api.join(slug, { token, handle, email, display_name: handle, tool: 'cli' }, 10_000);
|
|
37
|
-
if (res.status === 409)
|
|
38
|
-
|
|
38
|
+
if (res.status === 409) {
|
|
39
|
+
const code = res.json?.code;
|
|
40
|
+
const keyHint = `paste your key (Settings → API key at ${brand_1.BRAND.siteUrl}/settings, or ~/.convene/config.json on your other machine)`;
|
|
41
|
+
if (code === 'EMAIL_TAKEN') {
|
|
42
|
+
// Same human, new machine: connect THIS machine to the existing identity with NO key
|
|
43
|
+
// copying — prove email ownership via an emailed link, then re-run join authenticated
|
|
44
|
+
// so the (now-known) member is added to this project (idempotent if already a member).
|
|
45
|
+
log(`An identity already exists for ${email ?? 'your email'} — connecting this machine to it (no key copying needed).`);
|
|
46
|
+
const ok = await (0, enroll_1.enrollDevice)({ email, baseUrl });
|
|
47
|
+
if (ok)
|
|
48
|
+
return join(opts);
|
|
49
|
+
return; // enrollDevice prints guidance + exits on failure
|
|
50
|
+
}
|
|
51
|
+
(0, ctx_1.die)(`The handle "${handle}" is already taken on this bus.\n` +
|
|
52
|
+
`If that's you (same person, another machine), connect to your existing identity:\n` +
|
|
53
|
+
` convene login --api-key - # ${keyHint}\n` +
|
|
54
|
+
`If you want a SEPARATE identity here, pick a different handle via your email:\n` +
|
|
55
|
+
` convene setup --email <a-different-email> # the handle is derived from the email's local part`);
|
|
56
|
+
}
|
|
39
57
|
if (res.status === 401 && cfg.apiKey)
|
|
40
58
|
(0, ctx_1.die)('join token is invalid/expired/revoked, or your saved key is bad — ask an owner');
|
|
41
59
|
if (res.status === 401)
|
package/dist/commands/watch.js
CHANGED
|
@@ -17,21 +17,24 @@ exports.watch = watch;
|
|
|
17
17
|
* - A bounded run (no live config / not on the bus) exits 0 silently so a
|
|
18
18
|
* SessionStart launch on a non-bus repo is a no-op.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* RENDERING IS NOT THIS DAEMON'S JOB. Directed halts reach the agent's context
|
|
21
|
+
* via the server-truth PULL surfaces (fetch / session-open / lane-state), which
|
|
22
|
+
* query the server — the single authority for halt state. This daemon only:
|
|
23
|
+
* - stamps a liveness heartbeat each iteration (the health line reads its age,
|
|
24
|
+
* surfacing DEGRADED if the watcher is down);
|
|
25
|
+
* - optionally fires a best-effort desktop notification per matched halt
|
|
26
|
+
* (--notify; off by default — never enabled by the SessionStart spawn);
|
|
27
|
+
* - persists a MONOTONIC poll cursor so a relaunch (every SessionStart) resumes
|
|
28
|
+
* incrementally instead of re-scanning the backlog.
|
|
29
|
+
* It formerly appended matched halts to a per-slug jsonl for a reader to drain,
|
|
30
|
+
* but that reader was never wired up and a local cache could drift from server
|
|
31
|
+
* truth and mislead — so the jsonl was removed in favor of the pull path.
|
|
27
32
|
*
|
|
28
|
-
* TRUST: the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* lane-state; watch only narrows the window for non-deploy turns.
|
|
33
|
+
* TRUST: the block DECISION is the guard's, from lane-state. A halt body is
|
|
34
|
+
* UNTRUSTED and is NEVER interpreted here — the notification is a fixed template
|
|
35
|
+
* that never splices the body. watch only narrows the window for non-deploy turns.
|
|
32
36
|
*
|
|
33
|
-
* --notify: best-effort desktop ping per
|
|
34
|
-
* verb's mechanism if present; otherwise silently skipped). Never blocks the loop.
|
|
37
|
+
* --notify: best-effort desktop ping per matched halt. Never blocks the loop.
|
|
35
38
|
*/
|
|
36
39
|
const node_child_process_1 = require("node:child_process");
|
|
37
40
|
const git_1 = require("../git");
|
|
@@ -80,30 +83,12 @@ function watchShouldExit(args) {
|
|
|
80
83
|
}
|
|
81
84
|
const HALT_TYPES = new Set(['halt', 'interrupt']);
|
|
82
85
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
83
|
-
/**
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
return null;
|
|
87
|
-
const type = typeof m.type === 'string' ? m.type : '';
|
|
88
|
-
if (!HALT_TYPES.has(type))
|
|
89
|
-
return null;
|
|
90
|
-
// seq is the messages.id (enrich exposes it as `seq`, falling back to numeric id).
|
|
91
|
-
const seq = typeof m.seq === 'number' ? m.seq : typeof m.id === 'number' ? m.id : Number(m.id);
|
|
92
|
-
if (!Number.isFinite(seq))
|
|
93
|
-
return null;
|
|
94
|
-
return {
|
|
95
|
-
seq,
|
|
96
|
-
type,
|
|
97
|
-
short_id: typeof m.short_id === 'string' ? m.short_id : null,
|
|
98
|
-
// from/to/body are DISPLAY/UNTRUSTED — copied verbatim, never interpreted here.
|
|
99
|
-
from: typeof m.from_handle === 'string' ? m.from_handle : typeof m.from === 'string' ? m.from : null,
|
|
100
|
-
to: typeof m.to === 'string' ? m.to : typeof m.to_member === 'string' ? m.to_member : null,
|
|
101
|
-
body: typeof m.body === 'string' ? m.body : null,
|
|
102
|
-
at: typeof m.created_at === 'string' ? m.created_at : null,
|
|
103
|
-
};
|
|
86
|
+
/** True if a server feed/poll message is a halt/interrupt control message. */
|
|
87
|
+
function isHaltMessage(m) {
|
|
88
|
+
return !!m && typeof m === 'object' && typeof m.type === 'string' && HALT_TYPES.has(m.type);
|
|
104
89
|
}
|
|
105
90
|
/** Best-effort desktop notification; never blocks or throws. */
|
|
106
|
-
function notifyBestEffort(
|
|
91
|
+
function notifyBestEffort() {
|
|
107
92
|
try {
|
|
108
93
|
if (process.platform === 'darwin') {
|
|
109
94
|
// A halt is a control signal; the body is UNTRUSTED so we do NOT splice it
|
|
@@ -132,10 +117,9 @@ async function loop(opts) {
|
|
|
132
117
|
return 0; // can't authenticate → silent no-op
|
|
133
118
|
const instance = (0, cache_1.ensureSessionInstance)(slug);
|
|
134
119
|
const api = new api_1.ConveneApi(cfg.baseUrl, cfg.apiKey, session, cfg.tool, instance);
|
|
135
|
-
// Resume
|
|
136
|
-
// re-
|
|
137
|
-
|
|
138
|
-
let cursor = (0, cache_1.readWatchHighWater)(slug);
|
|
120
|
+
// Resume from the persisted poll cursor so a relaunch (every SessionStart)
|
|
121
|
+
// resumes incrementally instead of re-scanning the backlog from zero.
|
|
122
|
+
let cursor = (0, cache_1.readWatchCursor)(slug);
|
|
139
123
|
let backoff = BACKOFF_BASE_MS;
|
|
140
124
|
let iterations = 0;
|
|
141
125
|
const limit = typeof opts.maxIterations === 'number' ? opts.maxIterations : Infinity;
|
|
@@ -195,19 +179,17 @@ async function loop(opts) {
|
|
|
195
179
|
backoff = BACKOFF_BASE_MS; // recovered
|
|
196
180
|
const msgs = Array.isArray(res.json.messages) ? res.json.messages : [];
|
|
197
181
|
for (const m of msgs) {
|
|
198
|
-
|
|
199
|
-
if (!entry)
|
|
182
|
+
if (!isHaltMessage(m))
|
|
200
183
|
continue;
|
|
201
|
-
(0, cache_1.appendWatchEntry)(slug, entry);
|
|
202
184
|
if (opts.notify)
|
|
203
|
-
notifyBestEffort(
|
|
185
|
+
notifyBestEffort();
|
|
204
186
|
}
|
|
205
|
-
// Advance the resume cursor
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
// forever. The reader's high-water only advances over rendered halts.
|
|
209
|
-
if (typeof res.json.cursor === 'number' && res.json.cursor > cursor)
|
|
187
|
+
// Advance the resume cursor past EVERY message seen (incl. non-halts) so the
|
|
188
|
+
// daemon never re-fetches them, and PERSIST it so a relaunch resumes here.
|
|
189
|
+
if (typeof res.json.cursor === 'number' && res.json.cursor > cursor) {
|
|
210
190
|
cursor = res.json.cursor;
|
|
191
|
+
(0, cache_1.persistWatchCursor)(slug, cursor);
|
|
192
|
+
}
|
|
211
193
|
iterations++;
|
|
212
194
|
}
|
|
213
195
|
}
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ const post = __importStar(require("./commands/post"));
|
|
|
48
48
|
const inbox_1 = require("./commands/inbox");
|
|
49
49
|
const feedback_1 = require("./commands/feedback");
|
|
50
50
|
const auth_1 = require("./commands/auth");
|
|
51
|
+
const enroll_1 = require("./commands/enroll");
|
|
51
52
|
const init_1 = require("./commands/init");
|
|
52
53
|
const offboard_1 = require("./commands/offboard");
|
|
53
54
|
const join_1 = require("./commands/join");
|
|
@@ -94,6 +95,15 @@ program
|
|
|
94
95
|
.option('--api-key <key>', 'API key, or "-" to read from stdin (no shell history)')
|
|
95
96
|
.option('--base-url <url>', 'Convene base URL')
|
|
96
97
|
.action((opts) => (0, auth_1.login)(opts));
|
|
98
|
+
program
|
|
99
|
+
.command('enroll-device')
|
|
100
|
+
.description('connect THIS machine to your existing identity via an emailed link (no key copying)')
|
|
101
|
+
.option('--email <email>', 'email of your existing identity (defaults to git user.email)')
|
|
102
|
+
.option('--base-url <url>', 'Convene base URL')
|
|
103
|
+
.option('--label <label>', 'a label for this device (defaults to hostname)')
|
|
104
|
+
.action(async (opts) => {
|
|
105
|
+
await (0, enroll_1.enrollDevice)({ email: opts.email, baseUrl: opts.baseUrl, label: opts.label });
|
|
106
|
+
});
|
|
97
107
|
program.command('whoami').description('show identity, base URL, session, bus status').action(() => (0, auth_1.whoami)());
|
|
98
108
|
program
|
|
99
109
|
.command('fetch')
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "convene-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Convene CLI — AI development coordination bus client + UserPromptSubmit hook. Install: npm i -g convene-cli; then `convene setup`.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://convene.live",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/alex-hawkinson/Convene.git",
|