fix(web): correct LinkedIn URL on about page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
90
apps/cli/src/auth/callback-listener.ts
Normal file
90
apps/cli/src/auth/callback-listener.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Localhost HTTP callback listener for CLI-to-browser sync flow.
|
||||
*
|
||||
* Endpoints:
|
||||
* GET /ping → reachability check (web page preflight)
|
||||
* GET /callback → receives sync token via ?token= query param
|
||||
* OPTIONS * → CORS preflight for claudemesh.com
|
||||
*/
|
||||
|
||||
import { createServer, type Server } from "node:http";
|
||||
|
||||
export interface CallbackListener {
|
||||
/** Port the server is listening on. */
|
||||
port: number;
|
||||
/** Resolves when the /callback endpoint receives a token. */
|
||||
token: Promise<string>;
|
||||
/** Shut down the server. */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a localhost HTTP server on a random OS-assigned port.
|
||||
* Returns the port and a promise that resolves with the sync token.
|
||||
*/
|
||||
export function startCallbackListener(): Promise<CallbackListener> {
|
||||
return new Promise((resolveStart) => {
|
||||
let resolveToken: (token: string) => void;
|
||||
const tokenPromise = new Promise<string>((r) => {
|
||||
resolveToken = r;
|
||||
});
|
||||
|
||||
const server: Server = createServer((req, res) => {
|
||||
const url = new URL(req.url!, "http://localhost");
|
||||
|
||||
// CORS preflight
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204, {
|
||||
"Access-Control-Allow-Origin": "https://claudemesh.com",
|
||||
"Access-Control-Allow-Methods": "GET",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reachability check — web page calls this before redirecting
|
||||
if (url.pathname === "/ping") {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/plain",
|
||||
"Access-Control-Allow-Origin": "https://claudemesh.com",
|
||||
});
|
||||
res.end("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync token callback
|
||||
if (url.pathname === "/callback") {
|
||||
const token = url.searchParams.get("token");
|
||||
if (token) {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
"Access-Control-Allow-Origin": "https://claudemesh.com",
|
||||
});
|
||||
res.end(
|
||||
"<html><body><h2>Done! You can close this tab.</h2><p>Launching claudemesh...</p></body></html>",
|
||||
);
|
||||
resolveToken(token);
|
||||
// Close server after a short delay to ensure response is sent
|
||||
setTimeout(() => server.close(), 500);
|
||||
} else {
|
||||
res.writeHead(400, { "Content-Type": "text/plain" });
|
||||
res.end("Missing token");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address() as { port: number };
|
||||
resolveStart({
|
||||
port: addr.port,
|
||||
token: tokenPromise,
|
||||
close: () => server.close(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
4
apps/cli/src/auth/index.ts
Normal file
4
apps/cli/src/auth/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { startCallbackListener, type CallbackListener } from "./callback-listener";
|
||||
export { openBrowser } from "./open-browser";
|
||||
export { generatePairingCode } from "./pairing-code";
|
||||
export { syncWithBroker, type SyncResult } from "./sync-with-broker";
|
||||
33
apps/cli/src/auth/open-browser.ts
Normal file
33
apps/cli/src/auth/open-browser.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Cross-platform browser opener.
|
||||
* Respects BROWSER env var. Falls back to platform-specific launcher.
|
||||
*/
|
||||
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Open a URL in the user's default browser.
|
||||
* Returns true if the command succeeded, false otherwise.
|
||||
* Non-fatal — callers should show the URL as fallback.
|
||||
*/
|
||||
export function openBrowser(url: string): Promise<boolean> {
|
||||
// Validate URL
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const quoted = JSON.stringify(url);
|
||||
const browserCmd = process.env.BROWSER;
|
||||
|
||||
const cmd = browserCmd
|
||||
? `${browserCmd} ${quoted}`
|
||||
: process.platform === "darwin"
|
||||
? `open ${quoted}`
|
||||
: process.platform === "win32"
|
||||
? `rundll32 url.dll,FileProtocolHandler ${quoted}`
|
||||
: `xdg-open ${quoted}`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(cmd, (err) => resolve(!err));
|
||||
});
|
||||
}
|
||||
17
apps/cli/src/auth/pairing-code.ts
Normal file
17
apps/cli/src/auth/pairing-code.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generate a short pairing code for CLI-to-browser visual confirmation.
|
||||
* Excludes ambiguous characters (0/O, 1/l/I) for readability.
|
||||
*/
|
||||
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
const CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
||||
|
||||
/**
|
||||
* Generate a 4-character alphanumeric pairing code.
|
||||
* Example output: "A3Kx", "Hn7v", "pQ4m"
|
||||
*/
|
||||
export function generatePairingCode(): string {
|
||||
const bytes = randomBytes(4);
|
||||
return Array.from(bytes, (b) => CHARS[b % CHARS.length]).join("");
|
||||
}
|
||||
83
apps/cli/src/auth/sync-with-broker.ts
Normal file
83
apps/cli/src/auth/sync-with-broker.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Call the broker's POST /cli-sync endpoint to sync dashboard meshes.
|
||||
*
|
||||
* Takes a sync JWT (from the browser callback) and a freshly generated
|
||||
* ed25519 keypair. The broker creates member rows and returns mesh details.
|
||||
*/
|
||||
|
||||
export interface SyncResult {
|
||||
account_id: string;
|
||||
meshes: Array<{
|
||||
mesh_id: string;
|
||||
slug: string;
|
||||
broker_url: string;
|
||||
member_id: string;
|
||||
role: "admin" | "member";
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync meshes from dashboard via broker.
|
||||
*
|
||||
* @param syncToken - JWT from the browser sync flow
|
||||
* @param peerPubkey - ed25519 public key hex (64 chars)
|
||||
* @param displayName - display name for the new member
|
||||
* @param brokerBaseUrl - HTTPS base URL of the broker (derived from WSS URL)
|
||||
*/
|
||||
export async function syncWithBroker(
|
||||
syncToken: string,
|
||||
peerPubkey: string,
|
||||
displayName: string,
|
||||
brokerBaseUrl?: string,
|
||||
): Promise<SyncResult> {
|
||||
// Default broker URL — derive HTTPS from WSS
|
||||
const base = brokerBaseUrl ?? deriveHttpUrl(
|
||||
process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
|
||||
);
|
||||
|
||||
const res = await fetch(`${base}/cli-sync`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sync_token: syncToken,
|
||||
peer_pubkey: peerPubkey,
|
||||
display_name: displayName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
let msg: string;
|
||||
try {
|
||||
msg = JSON.parse(body).error ?? body;
|
||||
} catch {
|
||||
msg = body;
|
||||
}
|
||||
throw new Error(`Broker sync failed (${res.status}): ${msg}`);
|
||||
}
|
||||
|
||||
const body = (await res.json()) as { ok: boolean; account_id?: string; meshes?: SyncResult["meshes"]; error?: string };
|
||||
|
||||
if (!body.ok) {
|
||||
throw new Error(`Broker sync failed: ${body.error ?? "unknown error"}`);
|
||||
}
|
||||
|
||||
return {
|
||||
account_id: body.account_id!,
|
||||
meshes: body.meshes!,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a WSS broker URL to an HTTPS base URL.
|
||||
* wss://ic.claudemesh.com/ws → https://ic.claudemesh.com
|
||||
* ws://localhost:3001/ws → http://localhost:3001
|
||||
*/
|
||||
function deriveHttpUrl(wssUrl: string): string {
|
||||
const url = new URL(wssUrl);
|
||||
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
||||
// Remove /ws path suffix
|
||||
url.pathname = url.pathname.replace(/\/ws\/?$/, "");
|
||||
// Remove trailing slash
|
||||
return url.toString().replace(/\/$/, "");
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { loadConfig, getConfigPath } from "../state/config";
|
||||
import type { Config, JoinedMesh, GroupEntry } from "../state/config";
|
||||
import { startCallbackListener, openBrowser, generatePairingCode } from "../auth";
|
||||
import { BrokerClient } from "../ws/client";
|
||||
|
||||
// Flags as parsed by citty (index.ts is the source of truth for definitions).
|
||||
@@ -216,10 +217,85 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
||||
|
||||
// 2. Load config, pick mesh.
|
||||
const config = loadConfig();
|
||||
let justSynced = false;
|
||||
|
||||
if (config.meshes.length === 0 && !args.joinLink) {
|
||||
const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||
const bold = (s: string): string => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s);
|
||||
|
||||
const code = generatePairingCode();
|
||||
const listener = await startCallbackListener();
|
||||
const url = `https://claudemesh.com/cli-auth?port=${listener.port}&code=${code}&action=sync`;
|
||||
|
||||
console.log(`\n ${bold("Welcome to claudemesh!")} No meshes found.`);
|
||||
console.log(` Opening browser to sign in...\n`);
|
||||
|
||||
const opened = await openBrowser(url);
|
||||
if (!opened) {
|
||||
console.log(` Couldn't open browser automatically.`);
|
||||
}
|
||||
console.log(` ${dim(`Visit: ${url}`)}`);
|
||||
console.log(` ${dim(`Or join with invite: claudemesh launch --join <url>`)}\n`);
|
||||
|
||||
// Race: localhost callback vs manual paste vs timeout
|
||||
const manualPromise = new Promise<string>((resolve) => {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
rl.question(" Paste sync token (or wait for browser): ", (answer) => {
|
||||
rl.close();
|
||||
if (answer.trim()) resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise<null>((resolve) => {
|
||||
setTimeout(() => resolve(null), 15 * 60_000);
|
||||
});
|
||||
|
||||
const syncToken = await Promise.race([
|
||||
listener.token,
|
||||
manualPromise,
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
listener.close();
|
||||
|
||||
if (!syncToken) {
|
||||
console.error("\n Timed out waiting for sign-in.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate keypair and sync with broker
|
||||
const { generateKeypair } = await import("../crypto/keypair");
|
||||
const keypair = await generateKeypair();
|
||||
const displayNameForSync = args.name ?? `${hostname()}-${process.pid}`;
|
||||
|
||||
const { syncWithBroker } = await import("../auth/sync-with-broker");
|
||||
const result = await syncWithBroker(syncToken, keypair.publicKey, displayNameForSync);
|
||||
|
||||
// Write all meshes to config
|
||||
const { saveConfig } = await import("../state/config");
|
||||
for (const m of result.meshes) {
|
||||
config.meshes.push({
|
||||
meshId: m.mesh_id,
|
||||
memberId: m.member_id,
|
||||
slug: m.slug,
|
||||
name: m.slug,
|
||||
pubkey: keypair.publicKey,
|
||||
secretKey: keypair.secretKey,
|
||||
brokerUrl: m.broker_url,
|
||||
joinedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
config.accountId = result.account_id;
|
||||
saveConfig(config);
|
||||
justSynced = true;
|
||||
|
||||
console.log(`\n ${green("✓")} Synced ${result.meshes.length} mesh(es): ${result.meshes.map(m => m.slug).join(", ")}\n`);
|
||||
}
|
||||
|
||||
if (config.meshes.length === 0) {
|
||||
console.error(
|
||||
"No meshes joined. Run `claudemesh join <url>` or use --join <url>.",
|
||||
);
|
||||
console.error("No meshes joined. Run `claudemesh join <url>` or use --join <url>.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -248,7 +324,7 @@ export async function runLaunch(flags: LaunchFlags, rawArgs: string[]): Promise<
|
||||
|
||||
let messageMode: "push" | "inbox" | "off" = args.messageMode ?? "push";
|
||||
|
||||
if (!args.quiet) {
|
||||
if (!args.quiet && !justSynced) {
|
||||
if (role === null) {
|
||||
const answer = await askLine(" Role (optional): ");
|
||||
if (answer) role = answer;
|
||||
|
||||
114
apps/cli/src/commands/profile.ts
Normal file
114
apps/cli/src/commands/profile.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* `claudemesh profile` — view or edit your member profile.
|
||||
*
|
||||
* Profile fields (roleTag, groups, messageMode, displayName) are persistent
|
||||
* on the server. Changes are pushed to active sessions in real-time.
|
||||
*/
|
||||
|
||||
import { loadConfig } from "../state/config";
|
||||
import { BrokerClient } from "../ws/client";
|
||||
|
||||
export interface ProfileFlags {
|
||||
mesh?: string;
|
||||
"role-tag"?: string;
|
||||
groups?: string;
|
||||
"message-mode"?: string;
|
||||
name?: string;
|
||||
member?: string; // admin only: edit another member
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export async function runProfile(flags: ProfileFlags): Promise<void> {
|
||||
const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s);
|
||||
|
||||
const config = loadConfig();
|
||||
if (config.meshes.length === 0) {
|
||||
console.error("No meshes joined. Run `claudemesh join <url>` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Pick mesh
|
||||
const mesh = flags.mesh
|
||||
? config.meshes.find(m => m.slug === flags.mesh)
|
||||
: config.meshes[0]!;
|
||||
|
||||
if (!mesh) {
|
||||
console.error(`Mesh "${flags.mesh}" not found. Joined: ${config.meshes.map(m => m.slug).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Derive broker HTTP URL from WSS URL
|
||||
const brokerUrl = mesh.brokerUrl.replace("wss://", "https://").replace("ws://", "http://").replace(/\/ws\/?$/, "");
|
||||
|
||||
const hasEdits = flags["role-tag"] !== undefined || flags.groups !== undefined || flags["message-mode"] !== undefined || flags.name !== undefined;
|
||||
|
||||
if (hasEdits) {
|
||||
// PATCH member profile
|
||||
const targetMemberId = flags.member ?? mesh.memberId; // TODO: resolve --member by name
|
||||
const body: Record<string, unknown> = {};
|
||||
if (flags.name !== undefined) body.displayName = flags.name;
|
||||
if (flags["role-tag"] !== undefined) body.roleTag = flags["role-tag"];
|
||||
if (flags.groups !== undefined) {
|
||||
body.groups = flags.groups.split(",").map(s => {
|
||||
const [name, role] = s.trim().split(":");
|
||||
return role ? { name: name!, role } : { name: name! };
|
||||
});
|
||||
}
|
||||
if (flags["message-mode"] !== undefined) body.messageMode = flags["message-mode"];
|
||||
|
||||
const res = await fetch(`${brokerUrl}/mesh/${mesh.meshId}/member/${targetMemberId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Member-Id": mesh.memberId,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const result = await res.json() as Record<string, unknown>;
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else if (result.ok) {
|
||||
console.log(green("✓ Profile updated"));
|
||||
const member = result.member as Record<string, unknown>;
|
||||
printProfile(member, dim);
|
||||
} else {
|
||||
console.error(`Error: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// GET members list, show current user's profile
|
||||
const res = await fetch(`${brokerUrl}/mesh/${mesh.meshId}/members`);
|
||||
const result = await res.json() as { ok: boolean; members?: Array<Record<string, unknown>>; error?: string };
|
||||
|
||||
if (!result.ok) {
|
||||
console.error(`Error: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const me = result.members?.find(m => m.id === mesh.memberId);
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(me ?? {}, null, 2));
|
||||
} else if (me) {
|
||||
printProfile(me, dim);
|
||||
} else {
|
||||
console.log("Member not found in mesh.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printProfile(member: Record<string, unknown>, dim: (s: string) => string): void {
|
||||
const groups = member.groups as Array<{ name: string; role?: string }> | undefined;
|
||||
const groupStr = groups?.length
|
||||
? groups.map(g => g.role ? `${g.name} (${g.role})` : g.name).join(", ")
|
||||
: dim("(none)");
|
||||
|
||||
console.log(` Name: ${member.displayName ?? dim("(not set)")}`);
|
||||
console.log(` Role: ${member.roleTag ?? dim("(not set)")}`);
|
||||
console.log(` Groups: ${groupStr}`);
|
||||
console.log(` Messages: ${member.messageMode ?? "push"}`);
|
||||
console.log(` Access: ${member.permission ?? "member"}`);
|
||||
console.log(` Mesh: ${dim(String(member.id ?? ""))}`);
|
||||
}
|
||||
88
apps/cli/src/commands/sync.ts
Normal file
88
apps/cli/src/commands/sync.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* `claudemesh sync` — re-sync meshes from dashboard account.
|
||||
*
|
||||
* Opens browser for OAuth, receives sync token, calls broker /cli-sync,
|
||||
* merges new meshes into local config.
|
||||
*/
|
||||
|
||||
import { createInterface } from "node:readline";
|
||||
import { hostname } from "node:os";
|
||||
import { loadConfig, saveConfig } from "../state/config";
|
||||
import { startCallbackListener, openBrowser, generatePairingCode, syncWithBroker } from "../auth";
|
||||
import { generateKeypair } from "../crypto/keypair";
|
||||
|
||||
export async function runSync(args: { force?: boolean }): Promise<void> {
|
||||
const useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||
const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||
const green = (s: string): string => (useColor ? `\x1b[32m${s}\x1b[39m` : s);
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const code = generatePairingCode();
|
||||
const listener = await startCallbackListener();
|
||||
const url = `https://claudemesh.com/cli-auth?port=${listener.port}&code=${code}&action=sync`;
|
||||
|
||||
console.log(`Opening browser to sync meshes...`);
|
||||
console.log(dim(`Visit: ${url}`));
|
||||
await openBrowser(url);
|
||||
|
||||
// Race: localhost callback vs manual paste vs timeout
|
||||
const manualPromise = new Promise<string>((resolve) => {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
rl.question("Paste sync token (or wait for browser): ", (answer) => {
|
||||
rl.close();
|
||||
if (answer.trim()) resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise<null>((resolve) => {
|
||||
setTimeout(() => resolve(null), 15 * 60_000);
|
||||
});
|
||||
|
||||
const syncToken = await Promise.race([
|
||||
listener.token,
|
||||
manualPromise,
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
listener.close();
|
||||
|
||||
if (!syncToken) {
|
||||
console.error("Timed out waiting for sign-in.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Use existing keypair from first mesh, or generate new
|
||||
const keypair = config.meshes.length > 0
|
||||
? { publicKey: config.meshes[0]!.pubkey, secretKey: config.meshes[0]!.secretKey }
|
||||
: await generateKeypair();
|
||||
|
||||
const displayName = config.displayName ?? `${hostname()}-${process.pid}`;
|
||||
|
||||
const result = await syncWithBroker(syncToken, keypair.publicKey, displayName);
|
||||
|
||||
// Merge: add new meshes, skip duplicates
|
||||
let added = 0;
|
||||
for (const m of result.meshes) {
|
||||
if (config.meshes.some(existing => existing.meshId === m.mesh_id)) continue;
|
||||
config.meshes.push({
|
||||
meshId: m.mesh_id,
|
||||
memberId: m.member_id,
|
||||
slug: m.slug,
|
||||
name: m.slug,
|
||||
pubkey: keypair.publicKey,
|
||||
secretKey: keypair.secretKey,
|
||||
brokerUrl: m.broker_url,
|
||||
joinedAt: new Date().toISOString(),
|
||||
});
|
||||
added++;
|
||||
}
|
||||
config.accountId = result.account_id;
|
||||
saveConfig(config);
|
||||
|
||||
if (added > 0) {
|
||||
console.log(green(`✓ Added ${added} new mesh(es)`));
|
||||
} else {
|
||||
console.log(`Already up to date (${config.meshes.length} meshes)`);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export interface Config {
|
||||
role?: string; // per-session role tag (display + hello)
|
||||
groups?: GroupEntry[];
|
||||
messageMode?: "push" | "inbox" | "off";
|
||||
accountId?: string; // linked dashboard user ID (from CLI sync flow)
|
||||
}
|
||||
|
||||
const CONFIG_DIR = env.CLAUDEMESH_CONFIG_DIR ?? join(homedir(), ".claudemesh");
|
||||
@@ -55,7 +56,7 @@ export function loadConfig(): Config {
|
||||
if (!parsed || !Array.isArray(parsed.meshes)) {
|
||||
return { version: 1, meshes: [] };
|
||||
}
|
||||
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, role: parsed.role, groups: parsed.groups, messageMode: parsed.messageMode };
|
||||
return { version: 1, meshes: parsed.meshes, displayName: parsed.displayName, role: parsed.role, groups: parsed.groups, messageMode: parsed.messageMode, accountId: parsed.accountId };
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to load ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`,
|
||||
|
||||
Reference in New Issue
Block a user