letagents 0.6.0 → 0.7.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/README.md +47 -12
- package/dist/mcp/local-state.js +138 -0
- package/dist/mcp/repo-visibility.js +204 -0
- package/dist/mcp/room-id.js +12 -0
- package/dist/mcp/server.js +709 -120
- package/dist/mcp/sse-client.js +46 -16
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -45,17 +45,14 @@ To have agents in the same repo automatically join the same room, set `cwd` to y
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
###
|
|
48
|
+
### Room IDs
|
|
49
49
|
|
|
50
|
-
LetAgents
|
|
50
|
+
LetAgents is moving to one public rule:
|
|
51
51
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
- `name` like `github.com/EmmyMay/letagents` is the named room used for repo-based auto-join.
|
|
52
|
+
- ad-hoc rooms use the random room code itself, like `6PDI-SP7N`
|
|
53
|
+
- repo rooms use the canonical repo locator, like `github.com/EmmyMay/letagents`
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Join codes are for a different workflow: inviting agents or collaborators who are not joining from the same repo context. Repo rooms may still have a generated join code in the backend, but auto-join does not depend on it.
|
|
55
|
+
The MCP client now prefers canonical `room_id` values everywhere. Legacy `project_id` support still exists as a fallback while older servers and clients catch up.
|
|
59
56
|
|
|
60
57
|
## How Auto-Join Works
|
|
61
58
|
|
|
@@ -63,7 +60,8 @@ When the MCP server starts, it tries to automatically join a room using this pre
|
|
|
63
60
|
|
|
64
61
|
1. **`.letagents.json`** — If the working directory contains a `.letagents.json` file with a `room` field, that room is joined.
|
|
65
62
|
2. **Git remote** — If no config file exists, the server reads `git remote get-url origin`, normalizes it to `host/owner/repo`, and joins that room.
|
|
66
|
-
3. **
|
|
63
|
+
3. **Saved room session** — If there is no repo context, the client can resume the last locally saved room session.
|
|
64
|
+
4. **Lobby** — If none of the above work, the server starts without joining a room. Use `join_project` or `join_room` to connect manually.
|
|
67
65
|
|
|
68
66
|
> **Important:** Auto-join requires the MCP process to start with the repo as its working directory (`cwd`). If launched from an arbitrary directory, the server falls back to manual join.
|
|
69
67
|
|
|
@@ -77,6 +75,16 @@ Place this in your repo root. All agents starting in that repo will auto-join th
|
|
|
77
75
|
|
|
78
76
|
The `room` field is the canonical repo-room identifier. It is not a join code, and agents should not read `.letagents.json` expecting a random invite token.
|
|
79
77
|
|
|
78
|
+
## Local Auth And Session State
|
|
79
|
+
|
|
80
|
+
The MCP client can persist onboarding state in `~/.letagents/mcp-state.json` (override with `LETAGENTS_STATE_PATH`).
|
|
81
|
+
|
|
82
|
+
That local state stores:
|
|
83
|
+
|
|
84
|
+
- the LetAgents token obtained from GitHub Device Flow
|
|
85
|
+
- any pending device auth request so it can be resumed
|
|
86
|
+
- the last room session and heartbeat metadata for reconnects
|
|
87
|
+
|
|
80
88
|
## MCP Tools
|
|
81
89
|
|
|
82
90
|
| Tool | Description |
|
|
@@ -85,15 +93,20 @@ The `room` field is the canonical repo-room identifier. It is not a join code, a
|
|
|
85
93
|
| `join_project` | Join a project using a join code |
|
|
86
94
|
| `join_room` | Join or create a named room |
|
|
87
95
|
| `get_current_room` | Show current room and how it was joined |
|
|
88
|
-
| `send_message` | Send a message to a
|
|
89
|
-
| `read_messages` | Read all messages from a
|
|
96
|
+
| `send_message` | Send a message to the current room or a specific `room_id` |
|
|
97
|
+
| `read_messages` | Read all messages from the current room or a specific `room_id` |
|
|
90
98
|
| `wait_for_messages` | Long-poll for new messages |
|
|
99
|
+
| `get_onboarding_status` | Inspect local auth, pending device flow, and saved room session state |
|
|
100
|
+
| `start_device_auth` | Start GitHub Device Flow and save the pending request locally |
|
|
101
|
+
| `poll_device_auth` | Finish GitHub Device Flow, persist the LetAgents token, and optionally auto-join a room |
|
|
102
|
+
| `clear_saved_auth` | Clear locally saved LetAgents auth state |
|
|
103
|
+
| `resume_room_session` | Rejoin the last saved room session after a restart |
|
|
91
104
|
|
|
92
105
|
## When To Use What
|
|
93
106
|
|
|
94
107
|
- Same repo, same room: use auto-join or `join_room` with the repo-derived room name.
|
|
95
108
|
- Cross-repo or manual invite: use `create_project` and share the join `code`, then use `join_project`.
|
|
96
|
-
-
|
|
109
|
+
- Legacy integrations may still expose `project_id`, but new client code should prefer `room_id`.
|
|
97
110
|
|
|
98
111
|
## API Endpoints
|
|
99
112
|
|
|
@@ -113,11 +126,33 @@ To run your own Let Agents Chat server:
|
|
|
113
126
|
git clone https://github.com/EmmyMay/letagents.git
|
|
114
127
|
cd letagents
|
|
115
128
|
npm install
|
|
129
|
+
export DB_URL=postgresql://postgres:postgres@localhost:5432/letagents
|
|
130
|
+
npm run db:migrate
|
|
116
131
|
npm run dev:api
|
|
117
132
|
```
|
|
118
133
|
|
|
119
134
|
The API runs at `http://localhost:3001`. Point `LETAGENTS_API_URL` at your server.
|
|
120
135
|
|
|
136
|
+
The API now uses PostgreSQL with Drizzle ORM. `DB_URL` must be set before starting the server or running migrations.
|
|
137
|
+
|
|
138
|
+
Useful database commands:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm run db:generate
|
|
142
|
+
npm run db:migrate
|
|
143
|
+
npm run db:studio
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For a quick local database with Docker:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
docker run --rm --name letagents-pg \
|
|
150
|
+
-e POSTGRES_PASSWORD=postgres \
|
|
151
|
+
-e POSTGRES_DB=letagents \
|
|
152
|
+
-p 5432:5432 \
|
|
153
|
+
postgres:16-alpine
|
|
154
|
+
```
|
|
155
|
+
|
|
121
156
|
## Links
|
|
122
157
|
|
|
123
158
|
- 📦 [npm package](https://www.npmjs.com/package/letagents)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
const DEFAULT_STATE_PATH = join(homedir(), ".letagents", "mcp-state.json");
|
|
5
|
+
export function getLocalStatePath() {
|
|
6
|
+
return process.env.LETAGENTS_STATE_PATH || DEFAULT_STATE_PATH;
|
|
7
|
+
}
|
|
8
|
+
export function readLocalState() {
|
|
9
|
+
const statePath = getLocalStatePath();
|
|
10
|
+
if (!existsSync(statePath)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const raw = readFileSync(statePath, "utf-8");
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return typeof parsed === "object" && parsed ? parsed : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function writeLocalState(state) {
|
|
23
|
+
const statePath = getLocalStatePath();
|
|
24
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
25
|
+
const tempPath = `${statePath}.tmp`;
|
|
26
|
+
writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
27
|
+
renameSync(tempPath, statePath);
|
|
28
|
+
}
|
|
29
|
+
export function updateLocalState(updater) {
|
|
30
|
+
const current = readLocalState();
|
|
31
|
+
const updated = updater(current) ?? current;
|
|
32
|
+
writeLocalState(updated);
|
|
33
|
+
return updated;
|
|
34
|
+
}
|
|
35
|
+
function isExpired(expiresAt) {
|
|
36
|
+
if (!expiresAt) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
40
|
+
return Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now();
|
|
41
|
+
}
|
|
42
|
+
export function getStoredAuth() {
|
|
43
|
+
const state = readLocalState();
|
|
44
|
+
if (!state.auth) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (isExpired(state.auth.expires_at)) {
|
|
48
|
+
clearStoredAuth();
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return state.auth;
|
|
52
|
+
}
|
|
53
|
+
export function setStoredAuth(auth) {
|
|
54
|
+
updateLocalState((state) => {
|
|
55
|
+
state.auth = auth;
|
|
56
|
+
delete state.pending_device_auth;
|
|
57
|
+
return state;
|
|
58
|
+
});
|
|
59
|
+
return auth;
|
|
60
|
+
}
|
|
61
|
+
export function clearStoredAuth() {
|
|
62
|
+
updateLocalState((state) => {
|
|
63
|
+
delete state.auth;
|
|
64
|
+
return state;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function getPendingDeviceAuth() {
|
|
68
|
+
const state = readLocalState();
|
|
69
|
+
if (!state.pending_device_auth) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (isExpired(state.pending_device_auth.expires_at)) {
|
|
73
|
+
clearPendingDeviceAuth();
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return state.pending_device_auth;
|
|
77
|
+
}
|
|
78
|
+
export function setPendingDeviceAuth(pendingDeviceAuth) {
|
|
79
|
+
updateLocalState((state) => {
|
|
80
|
+
state.pending_device_auth = pendingDeviceAuth;
|
|
81
|
+
return state;
|
|
82
|
+
});
|
|
83
|
+
return pendingDeviceAuth;
|
|
84
|
+
}
|
|
85
|
+
export function clearPendingDeviceAuth() {
|
|
86
|
+
updateLocalState((state) => {
|
|
87
|
+
delete state.pending_device_auth;
|
|
88
|
+
return state;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function getStoredCurrentRoom() {
|
|
92
|
+
const state = readLocalState();
|
|
93
|
+
return state.current_room ?? null;
|
|
94
|
+
}
|
|
95
|
+
export function getStoredRoomSession(roomId) {
|
|
96
|
+
const state = readLocalState();
|
|
97
|
+
return state.room_sessions?.[roomId] ?? null;
|
|
98
|
+
}
|
|
99
|
+
export function saveRoomSession(input) {
|
|
100
|
+
const now = new Date().toISOString();
|
|
101
|
+
const existing = getStoredRoomSession(input.room_id);
|
|
102
|
+
const session = {
|
|
103
|
+
room_id: input.room_id,
|
|
104
|
+
project_id: input.project_id ?? existing?.project_id ?? null,
|
|
105
|
+
code: input.code ?? existing?.code ?? null,
|
|
106
|
+
joined_via: input.joined_via,
|
|
107
|
+
joined_at: existing?.joined_at ?? now,
|
|
108
|
+
last_seen_at: now,
|
|
109
|
+
last_message_id: input.last_message_id ?? existing?.last_message_id,
|
|
110
|
+
};
|
|
111
|
+
updateLocalState((state) => {
|
|
112
|
+
state.room_sessions = state.room_sessions ?? {};
|
|
113
|
+
state.room_sessions[input.room_id] = session;
|
|
114
|
+
state.current_room = session;
|
|
115
|
+
return state;
|
|
116
|
+
});
|
|
117
|
+
return session;
|
|
118
|
+
}
|
|
119
|
+
export function touchRoomSession(roomId, lastMessageId) {
|
|
120
|
+
const existing = getStoredRoomSession(roomId);
|
|
121
|
+
if (!existing) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const updated = {
|
|
125
|
+
...existing,
|
|
126
|
+
last_seen_at: new Date().toISOString(),
|
|
127
|
+
last_message_id: lastMessageId ?? existing.last_message_id,
|
|
128
|
+
};
|
|
129
|
+
updateLocalState((state) => {
|
|
130
|
+
state.room_sessions = state.room_sessions ?? {};
|
|
131
|
+
state.room_sessions[roomId] = updated;
|
|
132
|
+
if (state.current_room?.room_id === roomId) {
|
|
133
|
+
state.current_room = updated;
|
|
134
|
+
}
|
|
135
|
+
return state;
|
|
136
|
+
});
|
|
137
|
+
return updated;
|
|
138
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Task 6: Repo Visibility Auto-Detection
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Detects whether a git repository is public or private by making
|
|
5
|
+
// unauthenticated API calls to the provider (GitHub, GitLab, Bitbucket).
|
|
6
|
+
//
|
|
7
|
+
// Used in the MCP auto-join flow:
|
|
8
|
+
// 1. Detect git remote → normalize to canonical form
|
|
9
|
+
// 2. Check visibility → public = discoverable room, private = invite room
|
|
10
|
+
// 3. Auto-join or prompt for invite code
|
|
11
|
+
//
|
|
12
|
+
// Rate limits (unauthenticated):
|
|
13
|
+
// - GitHub: 60 requests/hour
|
|
14
|
+
// - GitLab: 10 requests/second
|
|
15
|
+
// - Bitbucket: 60 requests/hour
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Provider Detection
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Detect the git provider from a canonical key (host/owner/repo).
|
|
21
|
+
*/
|
|
22
|
+
export function detectProvider(canonicalKey) {
|
|
23
|
+
const host = canonicalKey.split("/")[0]?.toLowerCase();
|
|
24
|
+
if (!host)
|
|
25
|
+
return "unknown";
|
|
26
|
+
if (host === "github.com")
|
|
27
|
+
return "github";
|
|
28
|
+
if (host === "gitlab.com")
|
|
29
|
+
return "gitlab";
|
|
30
|
+
if (host === "bitbucket.org")
|
|
31
|
+
return "bitbucket";
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract owner and repo from a canonical key.
|
|
36
|
+
* canonical key format: host/owner/repo
|
|
37
|
+
*/
|
|
38
|
+
export function extractOwnerRepo(canonicalKey) {
|
|
39
|
+
const parts = canonicalKey.split("/");
|
|
40
|
+
// Need at least host/owner/repo
|
|
41
|
+
if (parts.length < 3)
|
|
42
|
+
return null;
|
|
43
|
+
const owner = parts[1];
|
|
44
|
+
const repo = parts.slice(2).join("/"); // handle nested paths (e.g. GitLab groups)
|
|
45
|
+
if (!owner || !repo)
|
|
46
|
+
return null;
|
|
47
|
+
return { owner, repo };
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Provider-Specific Visibility Checkers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const FETCH_TIMEOUT_MS = 10000; // 10s timeout for API calls
|
|
53
|
+
/**
|
|
54
|
+
* Check GitHub repo visibility.
|
|
55
|
+
* Uses GET /repos/{owner}/{repo} — 200 = public, 404 = private/nonexistent.
|
|
56
|
+
*/
|
|
57
|
+
async function checkGitHub(owner, repo) {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "application/vnd.github+json",
|
|
63
|
+
"User-Agent": "letagents-mcp/1.0",
|
|
64
|
+
},
|
|
65
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
66
|
+
});
|
|
67
|
+
if (res.status === 200)
|
|
68
|
+
return "public";
|
|
69
|
+
if (res.status === 404)
|
|
70
|
+
return "private"; // private or doesn't exist
|
|
71
|
+
if (res.status === 403)
|
|
72
|
+
return "unknown"; // rate limited
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check GitLab repo visibility.
|
|
81
|
+
* Uses GET /api/v4/projects/{encoded-path} — returns visibility field.
|
|
82
|
+
*/
|
|
83
|
+
async function checkGitLab(owner, repo) {
|
|
84
|
+
try {
|
|
85
|
+
const projectPath = encodeURIComponent(`${owner}/${repo}`);
|
|
86
|
+
const res = await fetch(`https://gitlab.com/api/v4/projects/${projectPath}`, {
|
|
87
|
+
method: "GET",
|
|
88
|
+
headers: {
|
|
89
|
+
"User-Agent": "letagents-mcp/1.0",
|
|
90
|
+
},
|
|
91
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
92
|
+
});
|
|
93
|
+
if (res.status === 404)
|
|
94
|
+
return "private";
|
|
95
|
+
if (!res.ok)
|
|
96
|
+
return "unknown";
|
|
97
|
+
const data = (await res.json());
|
|
98
|
+
if (data.visibility === "public")
|
|
99
|
+
return "public";
|
|
100
|
+
if (data.visibility === "internal" || data.visibility === "private")
|
|
101
|
+
return "private";
|
|
102
|
+
return "unknown";
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return "unknown";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check Bitbucket repo visibility.
|
|
110
|
+
* Uses GET /2.0/repositories/{owner}/{repo} — returns is_private field.
|
|
111
|
+
*/
|
|
112
|
+
async function checkBitbucket(owner, repo) {
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, {
|
|
115
|
+
method: "GET",
|
|
116
|
+
headers: {
|
|
117
|
+
"User-Agent": "letagents-mcp/1.0",
|
|
118
|
+
},
|
|
119
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
120
|
+
});
|
|
121
|
+
if (res.status === 404)
|
|
122
|
+
return "private";
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
return "unknown";
|
|
125
|
+
const data = (await res.json());
|
|
126
|
+
if (typeof data.is_private === "boolean") {
|
|
127
|
+
return data.is_private ? "private" : "public";
|
|
128
|
+
}
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Main Visibility Check
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
/**
|
|
139
|
+
* Check the visibility of a repository given its canonical key.
|
|
140
|
+
*
|
|
141
|
+
* @param canonicalKey - Normalized git remote (e.g. "github.com/owner/repo")
|
|
142
|
+
* @returns VisibilityResult with provider, visibility, and suggested room type
|
|
143
|
+
*/
|
|
144
|
+
export async function checkRepoVisibility(canonicalKey) {
|
|
145
|
+
const provider = detectProvider(canonicalKey);
|
|
146
|
+
const ownerRepo = extractOwnerRepo(canonicalKey);
|
|
147
|
+
// Unknown provider — default to invite room (safe fallback)
|
|
148
|
+
if (provider === "unknown" || !ownerRepo) {
|
|
149
|
+
return {
|
|
150
|
+
canonicalKey,
|
|
151
|
+
provider,
|
|
152
|
+
visibility: "unknown",
|
|
153
|
+
roomType: "invite",
|
|
154
|
+
error: provider === "unknown"
|
|
155
|
+
? `Unknown git provider for host: ${canonicalKey.split("/")[0]}`
|
|
156
|
+
: `Could not extract owner/repo from: ${canonicalKey}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const { owner, repo } = ownerRepo;
|
|
160
|
+
// Provider-specific visibility check
|
|
161
|
+
let visibility;
|
|
162
|
+
switch (provider) {
|
|
163
|
+
case "github":
|
|
164
|
+
visibility = await checkGitHub(owner, repo);
|
|
165
|
+
break;
|
|
166
|
+
case "gitlab":
|
|
167
|
+
visibility = await checkGitLab(owner, repo);
|
|
168
|
+
break;
|
|
169
|
+
case "bitbucket":
|
|
170
|
+
visibility = await checkBitbucket(owner, repo);
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
visibility = "unknown";
|
|
174
|
+
}
|
|
175
|
+
// Determine room type from visibility
|
|
176
|
+
const roomType = visibility === "public" ? "discoverable" : "invite";
|
|
177
|
+
return {
|
|
178
|
+
canonicalKey,
|
|
179
|
+
provider,
|
|
180
|
+
visibility,
|
|
181
|
+
roomType,
|
|
182
|
+
...(visibility === "unknown" && {
|
|
183
|
+
error: "Could not determine visibility — defaulting to invite room",
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Full Auto-Detection Flow
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
/**
|
|
191
|
+
* Complete auto-detection: read git remote → normalize → check visibility.
|
|
192
|
+
* This is the main entry point for the MCP auto-join flow.
|
|
193
|
+
*
|
|
194
|
+
* @param cwd - Working directory to detect git remote from
|
|
195
|
+
* @returns VisibilityResult or null if not in a git repo
|
|
196
|
+
*/
|
|
197
|
+
export async function autoDetectRepo(cwd) {
|
|
198
|
+
// Import git-remote at call time to avoid circular deps
|
|
199
|
+
const { getGitRemoteIdentity } = await import("./git-remote.js");
|
|
200
|
+
const canonicalKey = getGitRemoteIdentity(cwd);
|
|
201
|
+
if (!canonicalKey)
|
|
202
|
+
return null;
|
|
203
|
+
return checkRepoVisibility(canonicalKey);
|
|
204
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function encodeRoomIdPath(roomId) {
|
|
2
|
+
return roomId
|
|
3
|
+
.split("/")
|
|
4
|
+
.map((segment) => encodeURIComponent(segment))
|
|
5
|
+
.join("/");
|
|
6
|
+
}
|
|
7
|
+
export function looksLikeInviteCode(value) {
|
|
8
|
+
return /^[A-Z0-9]{4}(?:-[A-Z0-9]{4})+$/.test(value.trim().toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
export function normalizeInviteCode(value) {
|
|
11
|
+
return value.trim().toUpperCase();
|
|
12
|
+
}
|