@lifeaitools/clauth 1.8.1 → 1.9.2

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/README.md CHANGED
@@ -27,19 +27,37 @@ At the end it prints a **bootstrap token** — save it for the next step.
27
27
 
28
28
  ---
29
29
 
30
- ## After Install — Register Your Machine
31
-
32
- ```bash
33
- clauth setup
34
- ```
30
+ ## After Install — Register Your Machine
31
+
32
+ ```bash
33
+ clauth setup
34
+ ```
35
35
 
36
36
  Prompts for: machine label, password, bootstrap token (from `clauth install`).
37
37
 
38
- Then verify:
39
- ```bash
40
- clauth test # → PASS
41
- clauth status # → 12 services, all NO KEY
42
- ```
38
+ Then verify:
39
+ ```bash
40
+ clauth test # → PASS
41
+ clauth status # → 12 services, all NO KEY
42
+ ```
43
+
44
+ ### Add A New Computer
45
+
46
+ On an old computer where clauth is already registered:
47
+
48
+ ```bash
49
+ clauth enroll --label "Dave-New-Laptop"
50
+ ```
51
+
52
+ This creates a one-time enrollment code tied to the same `install_id` and writes
53
+ a one-time PowerShell setup script. Move that script to the new computer and run
54
+ it. The script installs clauth, enrolls the computer, installs startup, then
55
+ deletes itself. Setup defaults the machine label to the computer name and only
56
+ asks you to set the new computer's local clauth password.
57
+
58
+ The enrollment code does not copy repo credentials into the script. It lets the
59
+ new hardware-bound `machine_hash` join the shared Supabase Vault once. After the
60
+ code is redeemed, it cannot be used again.
43
61
 
44
62
  ---
45
63
 
@@ -70,9 +88,10 @@ clauth get github
70
88
  ## Command Reference
71
89
 
72
90
  ```
73
- clauth install Provision Supabase + install Claude skill
74
- clauth setup Register this machine with the vault
75
- clauth status All services + state
91
+ clauth install Provision Supabase + install Claude skill
92
+ clauth setup Register this machine with the vault
93
+ clauth enroll Create one-time code to add another computer
94
+ clauth status All services + state
76
95
  clauth search <query> Find services by name, project, description, or redacted address
77
96
  clauth test Verify connection
78
97
 
package/cli/api.js CHANGED
@@ -102,20 +102,38 @@ export async function test(password, machineHash, token, timestamp) {
102
102
  return authPost("test", password, machineHash, token, timestamp);
103
103
  }
104
104
 
105
- export async function changePassword(password, machineHash, token, timestamp, newSeedHash) {
106
- return authPost("change-password", password, machineHash, token, timestamp, { new_hmac_seed_hash: newSeedHash });
107
- }
108
-
109
- export async function registerMachine(machineHash, seedHash, label, adminToken) {
110
- return post("register-machine", {
111
- machine_hash: machineHash,
112
- hmac_seed_hash: seedHash,
113
- label,
114
- admin_token: adminToken
115
- });
116
- }
117
-
118
- export default {
119
- retrieve, write, enable, addService, updateService, removeService, revoke,
120
- status, test, registerMachine, getBaseUrl, getAnonKey
121
- };
105
+ export async function changePassword(password, machineHash, token, timestamp, newSeedHash) {
106
+ return authPost("change-password", password, machineHash, token, timestamp, { new_hmac_seed_hash: newSeedHash });
107
+ }
108
+
109
+ export async function createEnrollment(password, machineHash, token, timestamp, label, ttlMinutes, installId) {
110
+ const extra = {};
111
+ if (label) extra.label = label;
112
+ if (ttlMinutes) extra.ttl_minutes = ttlMinutes;
113
+ if (installId) extra.install_id = installId;
114
+ return authPost("create-enrollment", password, machineHash, token, timestamp, extra);
115
+ }
116
+
117
+ export async function registerMachine(machineHash, seedHash, label, adminToken, extras = {}) {
118
+ return post("register-machine", {
119
+ machine_hash: machineHash,
120
+ hmac_seed_hash: seedHash,
121
+ label,
122
+ admin_token: adminToken,
123
+ ...extras
124
+ });
125
+ }
126
+
127
+ export async function redeemEnrollment(machineHash, seedHash, label, enrollmentCode) {
128
+ return post("redeem-enrollment", {
129
+ machine_hash: machineHash,
130
+ hmac_seed_hash: seedHash,
131
+ label,
132
+ enrollment_code: enrollmentCode
133
+ });
134
+ }
135
+
136
+ export default {
137
+ retrieve, write, enable, addService, updateService, removeService, revoke,
138
+ status, test, createEnrollment, registerMachine, redeemEnrollment, getBaseUrl, getAnonKey
139
+ };
@@ -45,6 +45,14 @@ function getRequestPath() {
45
45
  return path.join(getClauthAppDir(), "codevelop-request.json");
46
46
  }
47
47
 
48
+ function getAdhocStatePath() {
49
+ return path.join(getClauthAppDir(), "codevelop-channel-active.json");
50
+ }
51
+
52
+ function getAdhocChannelsPath() {
53
+ return path.join(getClauthAppDir(), "codevelop-channels.json");
54
+ }
55
+
48
56
  function cmdValue(value) {
49
57
  return String(value ?? "").replace(/%/g, "%%").replace(/"/g, "'");
50
58
  }
@@ -430,6 +438,271 @@ function normalizeOptionalPeer(peer) {
430
438
  return value;
431
439
  }
432
440
 
441
+ function normalizeAdhocId(value, label) {
442
+ const text = String(value || "").trim().toLowerCase();
443
+ if (!text) throw new Error(`${label} is required`);
444
+ const normalized = text.replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
445
+ if (!normalized) throw new Error(`${label} must contain letters, numbers, dots, underscores, or hyphens`);
446
+ if (normalized.length > 64) throw new Error(`${label} is too long; keep it under 64 characters`);
447
+ return normalized;
448
+ }
449
+
450
+ function normalizeOptionalAdhocId(value) {
451
+ const text = String(value || "").trim();
452
+ return text ? normalizeAdhocId(text, "peer") : null;
453
+ }
454
+
455
+ function getCodevelopBaseUrl(opts = {}) {
456
+ const port = Number(opts.port || DEFAULT_PORT);
457
+ if (RESERVED_PORTS.has(port)) throw new Error(`Refusing to use reserved clauth port ${port}`);
458
+ return opts.baseUrl || process.env.CODEVELOP_BASE_URL || `http://127.0.0.1:${port}`;
459
+ }
460
+
461
+ async function ensureCodevelopReachable(opts = {}) {
462
+ const baseUrl = getCodevelopBaseUrl(opts);
463
+ if (await testPing(baseUrl)) return { baseUrl, startedClauthPid: null };
464
+ if (!asBool(opts.startIsolatedClauth)) {
465
+ throw new Error(`clauth is not reachable at ${baseUrl}. Re-run with --start-isolated-clauth.`);
466
+ }
467
+ const port = Number(opts.port || DEFAULT_PORT);
468
+ const startedClauthPid = startIsolatedClauth(port);
469
+ if (!(await waitForPing(baseUrl))) throw new Error(`isolated clauth did not become ready at ${baseUrl}`);
470
+ return { baseUrl, startedClauthPid };
471
+ }
472
+
473
+ function writeAdhocState(state) {
474
+ const appDir = getClauthAppDir();
475
+ fs.mkdirSync(appDir, { recursive: true });
476
+ fs.writeFileSync(getAdhocStatePath(), `${JSON.stringify({
477
+ ...state,
478
+ updated_at: new Date().toISOString(),
479
+ }, null, 2)}\n`, "utf8");
480
+ }
481
+
482
+ function readAdhocState() {
483
+ const statePath = getAdhocStatePath();
484
+ if (!fs.existsSync(statePath)) return null;
485
+ return readJsonFile(statePath);
486
+ }
487
+
488
+ function readAdhocChannels() {
489
+ const channelsPath = getAdhocChannelsPath();
490
+ if (!fs.existsSync(channelsPath)) return {};
491
+ const parsed = readJsonFile(channelsPath);
492
+ return parsed && typeof parsed === "object" ? parsed : {};
493
+ }
494
+
495
+ function writeAdhocChannels(channels) {
496
+ const appDir = getClauthAppDir();
497
+ fs.mkdirSync(appDir, { recursive: true });
498
+ fs.writeFileSync(getAdhocChannelsPath(), `${JSON.stringify(channels, null, 2)}\n`, "utf8");
499
+ }
500
+
501
+ async function getCodevelopStatus(baseUrl, sessionId) {
502
+ return requestJson("GET", `${baseUrl}/codevelop/${encodeURIComponent(sessionId)}/status`);
503
+ }
504
+
505
+ async function findOrCreateAdhocChannel(opts = {}) {
506
+ const channel = normalizeAdhocId(opts.channel || opts.name || process.env.CODEVELOP_CHANNEL || "1", "channel");
507
+ const sessionName = `channel-${channel}`;
508
+ const { baseUrl, startedClauthPid } = await ensureCodevelopReachable(opts);
509
+ const channels = readAdhocChannels();
510
+ const existing = channels[channel];
511
+ if (existing?.session_id && existing?.base_url === baseUrl) {
512
+ try {
513
+ const session = await getCodevelopStatus(baseUrl, existing.session_id);
514
+ if (session.status !== "stopped") return { baseUrl, channel, session, created: false, startedClauthPid };
515
+ } catch {}
516
+ }
517
+
518
+ const session = await requestJson("POST", `${baseUrl}/codevelop/start`, {
519
+ name: sessionName,
520
+ repo: opts.repo || DEFAULT_REPO,
521
+ metadata: {
522
+ mode: "adhoc-channel",
523
+ channel,
524
+ created_by: opts.peer || process.env.CODEVELOP_PEER || null,
525
+ },
526
+ });
527
+ channels[channel] = {
528
+ channel,
529
+ session_id: session.session_id,
530
+ base_url: baseUrl,
531
+ name: sessionName,
532
+ created_at: new Date().toISOString(),
533
+ updated_at: new Date().toISOString(),
534
+ };
535
+ writeAdhocChannels(channels);
536
+ return { baseUrl, channel, session, created: true, startedClauthPid };
537
+ }
538
+
539
+ async function loadAdhocContext(opts = {}) {
540
+ if (opts.session && opts.peer) {
541
+ return {
542
+ baseUrl: getCodevelopBaseUrl(opts),
543
+ channel: normalizeAdhocId(opts.channel || opts.name || "custom", "channel"),
544
+ sessionId: opts.session,
545
+ peer: normalizeAdhocId(opts.peer, "peer"),
546
+ };
547
+ }
548
+
549
+ const state = readAdhocState();
550
+ const channel = opts.channel || state?.channel || process.env.CODEVELOP_CHANNEL;
551
+ const peer = opts.peer || opts.from || state?.peer || process.env.CODEVELOP_PEER;
552
+ if (!channel || !peer) {
553
+ throw new Error("No active ad hoc channel. Run: clauth codevelop join --channel 1 --peer codex-1");
554
+ }
555
+
556
+ if (opts.channel || opts.peer || !state?.session_id) {
557
+ const joined = await joinAdhocChannel({ ...opts, channel, peer, silent: true });
558
+ return {
559
+ baseUrl: joined.baseUrl,
560
+ channel: joined.channel,
561
+ sessionId: joined.session_id,
562
+ peer: joined.peer,
563
+ };
564
+ }
565
+
566
+ return {
567
+ baseUrl: opts.baseUrl || state.base_url || getCodevelopBaseUrl(opts),
568
+ channel: normalizeAdhocId(channel, "channel"),
569
+ sessionId: state.session_id,
570
+ peer: normalizeAdhocId(peer, "peer"),
571
+ };
572
+ }
573
+
574
+ function formatAdhocMessage(message) {
575
+ const body = message.task || message.message || message.summary || "";
576
+ return `[${message.turn_id}] ${message.from} -> ${message.to} ${message.type || "message"}: ${body}`;
577
+ }
578
+
579
+ async function joinAdhocChannel(opts = {}) {
580
+ const peer = normalizeAdhocId(opts.peer || opts.from || process.env.CODEVELOP_PEER, "peer");
581
+ const { baseUrl, channel, session, created, startedClauthPid } = await findOrCreateAdhocChannel(opts);
582
+ const sessionId = session.session_id;
583
+ const joined = await requestJson("POST", `${baseUrl}/codevelop/join`, {
584
+ session_id: sessionId,
585
+ peer_id: peer,
586
+ role: opts.role || "participant",
587
+ metadata: {
588
+ mode: "adhoc-channel",
589
+ channel,
590
+ cwd: process.cwd(),
591
+ },
592
+ });
593
+
594
+ writeAdhocState({
595
+ channel,
596
+ peer,
597
+ session_id: sessionId,
598
+ base_url: baseUrl,
599
+ state_path: getAdhocStatePath(),
600
+ });
601
+
602
+ if (!opts.silent) {
603
+ console.log(`CHANNEL=${channel}`);
604
+ console.log(`PEER=${peer}`);
605
+ console.log(`CODEVELOP_SESSION=${sessionId}`);
606
+ console.log(`BASE_URL=${baseUrl}`);
607
+ console.log(`CREATED=${created ? "yes" : "no"}`);
608
+ if (startedClauthPid) console.log(`CLAUTH_PID=${startedClauthPid}`);
609
+ console.log(`STATE=${getAdhocStatePath()}`);
610
+ console.log("");
611
+ console.log(`Send: clauth codevelop say --message "hello"`);
612
+ console.log(`Read: clauth codevelop read`);
613
+ console.log(`Watch: clauth codevelop watch`);
614
+ }
615
+
616
+ return { ...joined, baseUrl, channel, peer, session_id: sessionId };
617
+ }
618
+
619
+ async function listAdhocPeers(opts = {}) {
620
+ const ctx = await loadAdhocContext(opts);
621
+ const status = await requestJson("GET", `${ctx.baseUrl}/codevelop/${encodeURIComponent(ctx.sessionId)}/status`);
622
+ console.log(`channel ${ctx.channel} (${ctx.sessionId})`);
623
+ for (const peer of Object.values(status.peers || {})) {
624
+ console.log(`${peer.peer_id}\trole=${peer.role || "participant"}\tqueued=${peer.queued}\tstreams=${peer.streams}\tlast_seen=${peer.last_seen_at || ""}`);
625
+ }
626
+ }
627
+
628
+ async function sayAdhoc(opts = {}) {
629
+ const ctx = await loadAdhocContext(opts);
630
+ const message = opts.message || opts.task || opts.args?.join(" ") || "";
631
+ if (!String(message).trim()) throw new Error("--message is required for codevelop say");
632
+
633
+ const status = await requestJson("GET", `${ctx.baseUrl}/codevelop/${encodeURIComponent(ctx.sessionId)}/status`);
634
+ const explicitTo = normalizeOptionalAdhocId(opts.to);
635
+ const targets = explicitTo
636
+ ? [explicitTo]
637
+ : Object.keys(status.peers || {}).filter(peer => peer !== ctx.peer && peer !== "system");
638
+ if (!targets.length) {
639
+ throw new Error(`No other peers are in channel ${ctx.channel}. Have the other pane run: clauth codevelop join --channel ${ctx.channel} --peer claude-1`);
640
+ }
641
+
642
+ const sent = [];
643
+ for (const target of targets) {
644
+ const result = await requestJson("POST", `${ctx.baseUrl}/codevelop/send`, {
645
+ session_id: ctx.sessionId,
646
+ from: ctx.peer,
647
+ to: target,
648
+ type: "chat",
649
+ task: message,
650
+ message,
651
+ context: {
652
+ mode: "adhoc-channel",
653
+ channel: ctx.channel,
654
+ },
655
+ });
656
+ sent.push(result);
657
+ }
658
+
659
+ console.log(JSON.stringify({
660
+ channel: ctx.channel,
661
+ session_id: ctx.sessionId,
662
+ from: ctx.peer,
663
+ to: targets,
664
+ sent,
665
+ }, null, 2));
666
+ }
667
+
668
+ async function readAdhoc(opts = {}) {
669
+ const ctx = await loadAdhocContext(opts);
670
+ const inbox = await requestJson("POST", `${ctx.baseUrl}/codevelop/poll`, {
671
+ session_id: ctx.sessionId,
672
+ peer_id: ctx.peer,
673
+ });
674
+ if (asBool(opts.json)) {
675
+ console.log(JSON.stringify(inbox, null, 2));
676
+ return;
677
+ }
678
+ if (!inbox.count) {
679
+ console.log(`channel ${ctx.channel}: no messages for ${ctx.peer}`);
680
+ return;
681
+ }
682
+ for (const message of inbox.messages || []) console.log(formatAdhocMessage(message));
683
+ }
684
+
685
+ async function watchAdhoc(opts = {}) {
686
+ const ctx = await loadAdhocContext(opts);
687
+ const streamUrl = `${ctx.baseUrl}/codevelop/${encodeURIComponent(ctx.sessionId)}/${encodeURIComponent(ctx.peer)}/stream`;
688
+ const once = asBool(opts.once);
689
+ console.log(`Watching channel ${ctx.channel} as ${ctx.peer}`);
690
+ console.log(`stream ${streamUrl}`);
691
+
692
+ connectSse(streamUrl, {
693
+ onEvent: ({ event, data }) => {
694
+ if (event !== "message" && event !== "stop") return;
695
+ console.log(formatAdhocMessage(data || {}));
696
+ if (once) process.exit(0);
697
+ },
698
+ onError: err => {
699
+ console.error(`watch error: ${err.message}`);
700
+ process.exitCode = 1;
701
+ },
702
+ });
703
+ await new Promise(() => {});
704
+ }
705
+
433
706
  function inferFromPeer({ from, to, peer } = {}) {
434
707
  const explicit = normalizeOptionalPeer(from) || normalizeOptionalPeer(peer) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
435
708
  if (explicit) return explicit;
@@ -884,6 +1157,11 @@ export async function runCodevelop(opts = {}) {
884
1157
  return;
885
1158
  }
886
1159
  if (action === "start") return startCodevelop(opts);
1160
+ if (action === "join") return joinAdhocChannel(opts);
1161
+ if (action === "say") return sayAdhoc(opts);
1162
+ if (action === "read") return readAdhoc(opts);
1163
+ if (action === "watch") return watchAdhoc(opts);
1164
+ if (action === "who") return listAdhocPeers(opts);
887
1165
  if (action === "launch-peer") return launchPeer(opts);
888
1166
  if (action === "check-partner") return checkPartner(opts);
889
1167
  if (action === "sync") return syncPartner(opts);
@@ -896,6 +1174,11 @@ export async function runCodevelop(opts = {}) {
896
1174
  console.log(chalk.cyan("\nclauth codevelop\n"));
897
1175
  console.log(" clauth codevelop install-terminal [--repo C:\\Dev\\regen-root]");
898
1176
  console.log(" clauth codevelop start --port 53137 --start-isolated-clauth [--repo C:\\Dev\\regen-root]");
1177
+ console.log(" clauth codevelop join --channel 1 --peer codex-1 [--start-isolated-clauth]");
1178
+ console.log(" clauth codevelop say --message \"claude-1 look at this doc and report back\" [--to claude-1]");
1179
+ console.log(" clauth codevelop read");
1180
+ console.log(" clauth codevelop watch [--once]");
1181
+ console.log(" clauth codevelop who");
899
1182
  console.log(" clauth codevelop ask --to claude|codex --task \"...\" --wait");
900
1183
  console.log(" clauth codevelop reply --from claude|codex --turn turn-0001 --summary \"...\" --evidence \"...\"");
901
1184
  console.log(" clauth codevelop inbox --peer claude|codex");
@@ -127,10 +127,11 @@ export async function runInstall(opts = {}) {
127
127
 
128
128
  // Run migrations
129
129
  const s4 = ora('Running database migrations...').start();
130
- const migrations = [
131
- { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
132
- { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
133
- ];
130
+ const migrations = [
131
+ { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
132
+ { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
133
+ { name: '003_machine_enrollments', file: 'supabase/migrations/003_machine_enrollments.sql' },
134
+ ];
134
135
  for (const m of migrations) {
135
136
  const sql = readFileSync(join(ROOT, m.file), 'utf8');
136
137
  try {
@@ -251,10 +252,11 @@ export async function runInstall(opts = {}) {
251
252
 
252
253
  // ── Step 4: Run migrations ────────────────
253
254
  const s4 = ora('Running database migrations...').start();
254
- const migrations = [
255
- { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
256
- { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
257
- ];
255
+ const migrations = [
256
+ { name: '001_clauth_schema', file: 'supabase/migrations/001_clauth_schema.sql' },
257
+ { name: '002_vault_helpers', file: 'supabase/migrations/002_vault_helpers.sql' },
258
+ { name: '003_machine_enrollments', file: 'supabase/migrations/003_machine_enrollments.sql' },
259
+ ];
258
260
  for (const m of migrations) {
259
261
  const sql = readFileSync(join(ROOT, m.file), 'utf8');
260
262
  try {