leedab 0.1.8 → 0.2.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/agent/resolveMember.d.ts +21 -0
- package/dist/agent/resolveMember.js +44 -0
- package/dist/dashboard/routes.d.ts +12 -0
- package/dist/dashboard/routes.js +235 -15
- package/dist/dashboard/server.js +6 -1
- package/dist/dashboard/static/admin.html +688 -0
- package/dist/dashboard/static/index.html +4 -26
- package/dist/dashboard/static/sessions.html +20 -24
- package/dist/license.d.ts +3 -0
- package/dist/license.js +12 -3
- package/dist/team/permissions.d.ts +65 -0
- package/dist/team/permissions.js +138 -0
- package/dist/team/syncAllowlists.d.ts +7 -0
- package/dist/team/syncAllowlists.js +62 -0
- package/dist/team.d.ts +10 -7
- package/dist/team.js +62 -68
- package/dist/templates/verticals/supply-chain/WORKFLOWS.md +5 -0
- package/dist/workflows/registry.d.ts +22 -0
- package/dist/workflows/registry.js +46 -0
- package/package.json +1 -1
- package/dist/dashboard/static/app.js +0 -351
- package/dist/dashboard/static/console.html +0 -252
- package/dist/dashboard/static/settings.html +0 -274
- package/dist/dashboard/static/team.html +0 -215
|
@@ -80,24 +80,6 @@
|
|
|
80
80
|
letter-spacing: -0.01em;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
.header-badge {
|
|
84
|
-
display: flex;
|
|
85
|
-
align-items: center;
|
|
86
|
-
gap: 5px;
|
|
87
|
-
padding: 3px 8px;
|
|
88
|
-
background: rgba(34, 197, 94, 0.1);
|
|
89
|
-
border-radius: 20px;
|
|
90
|
-
font-size: 11px;
|
|
91
|
-
color: var(--green);
|
|
92
|
-
font-weight: 500;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.header-badge-dot {
|
|
96
|
-
width: 6px;
|
|
97
|
-
height: 6px;
|
|
98
|
-
border-radius: 50%;
|
|
99
|
-
background: var(--green);
|
|
100
|
-
}
|
|
101
83
|
|
|
102
84
|
.header-nav {
|
|
103
85
|
display: flex;
|
|
@@ -533,18 +515,14 @@
|
|
|
533
515
|
<body>
|
|
534
516
|
<div class="header">
|
|
535
517
|
<div class="header-left">
|
|
536
|
-
<
|
|
537
|
-
<div class="header-badge">
|
|
538
|
-
<div class="header-badge-dot"></div>
|
|
539
|
-
On
|
|
540
|
-
</div>
|
|
518
|
+
<a href="/" class="header-title" style="text-decoration:none;color:inherit">LeedAB</a>
|
|
541
519
|
</div>
|
|
542
520
|
<div class="header-nav">
|
|
543
|
-
<a href="/
|
|
521
|
+
<a href="/admin">
|
|
544
522
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
545
|
-
<
|
|
523
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
546
524
|
</svg>
|
|
547
|
-
|
|
525
|
+
Admin
|
|
548
526
|
</a>
|
|
549
527
|
<a href="/sessions.html">
|
|
550
528
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -20,17 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
<div class="page-header">
|
|
22
22
|
<div class="page-header-left">
|
|
23
|
-
<
|
|
23
|
+
<a href="/" class="page-header-title" style="text-decoration:none;color:inherit">LeedAB</a>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="page-nav">
|
|
26
|
-
<a href="/">
|
|
27
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4 20-7Z"/><path d="M22 2 11 13"/></svg>
|
|
28
|
-
Chat
|
|
29
|
-
</a>
|
|
30
|
-
<a href="/console.html">
|
|
31
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
32
|
-
Console
|
|
33
|
-
</a>
|
|
34
26
|
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">
|
|
35
27
|
<svg id="theme-icon-sun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
36
28
|
<svg id="theme-icon-moon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
@@ -83,6 +75,16 @@
|
|
|
83
75
|
color: var(--text-dim);
|
|
84
76
|
}
|
|
85
77
|
|
|
78
|
+
.session-summary {
|
|
79
|
+
color: var(--text-secondary) !important;
|
|
80
|
+
font-size: 13px !important;
|
|
81
|
+
margin-bottom: 2px;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
max-width: 500px;
|
|
86
|
+
}
|
|
87
|
+
|
|
86
88
|
.session-meta {
|
|
87
89
|
font-size: 12px;
|
|
88
90
|
color: var(--text-faint);
|
|
@@ -140,28 +142,22 @@
|
|
|
140
142
|
const date = s.updatedAt ? new Date(s.updatedAt).toLocaleDateString("en-US", {
|
|
141
143
|
month: "short", day: "numeric", hour: "numeric", minute: "2-digit"
|
|
142
144
|
}) : "";
|
|
143
|
-
// Derive channel
|
|
145
|
+
// Derive channel from key like "agent:main:telegram:direct:123"
|
|
144
146
|
const parts = (s.key || "").split(":");
|
|
145
147
|
let channel = "console";
|
|
146
|
-
|
|
147
|
-
if (parts[2] === "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
label = parts.slice(2).join(":");
|
|
153
|
-
} else if (parts[2] === "subagent") {
|
|
154
|
-
channel = "subagent";
|
|
155
|
-
label = parts.slice(2).join(":");
|
|
156
|
-
} else if (parts.length >= 3) {
|
|
157
|
-
label = parts.slice(2).join(":");
|
|
158
|
-
}
|
|
148
|
+
if (parts[2] === "telegram") channel = "telegram";
|
|
149
|
+
else if (parts[2] === "cron") channel = "cron";
|
|
150
|
+
else if (parts[2] === "subagent") channel = "subagent";
|
|
151
|
+
|
|
152
|
+
const title = s.senderName || channel;
|
|
153
|
+
const summary = s.summary ? escapeHtml(s.summary) : "";
|
|
159
154
|
const tokens = s.totalTokens != null ? `${(s.totalTokens / 1000).toFixed(1)}k tokens` : "";
|
|
160
155
|
const sid = s.sessionId || s.key || "";
|
|
161
156
|
return `
|
|
162
157
|
<a class="session-item" href="/?session=${encodeURIComponent(sid)}">
|
|
163
158
|
<div class="session-info">
|
|
164
|
-
<h3>${escapeHtml(
|
|
159
|
+
<h3>${escapeHtml(title)}</h3>
|
|
160
|
+
${summary ? `<p class="session-summary">${summary}</p>` : ""}
|
|
165
161
|
<p>${escapeHtml(s.model || "")} ${tokens ? "· " + tokens : ""}</p>
|
|
166
162
|
</div>
|
|
167
163
|
<div class="session-meta">
|
package/dist/license.d.ts
CHANGED
package/dist/license.js
CHANGED
|
@@ -2,8 +2,10 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { STATE_DIR } from "./paths.js";
|
|
4
4
|
const LICENSE_PATH = resolve(STATE_DIR, "license.json");
|
|
5
|
-
const
|
|
5
|
+
const API_URL = process.env.LEEDAB_API_URL || "https://api.leedab.com/api/v1";
|
|
6
|
+
const VERIFY_URL = `${API_URL}/licensing/verify`;
|
|
6
7
|
const REVALIDATE_DAYS = 7;
|
|
8
|
+
const REVALIDATE_DAYS_TRIAL = 1;
|
|
7
9
|
/**
|
|
8
10
|
* Validate a license key against the API.
|
|
9
11
|
*/
|
|
@@ -14,6 +16,7 @@ export async function validateLicenseKey(key) {
|
|
|
14
16
|
Authorization: `Bearer ${key}`,
|
|
15
17
|
"Content-Type": "application/json",
|
|
16
18
|
},
|
|
19
|
+
signal: AbortSignal.timeout(8000),
|
|
17
20
|
});
|
|
18
21
|
if (!res.ok) {
|
|
19
22
|
return {
|
|
@@ -35,6 +38,9 @@ export async function validateLicenseKey(key) {
|
|
|
35
38
|
seatsUsed: data.seats_used ?? 0,
|
|
36
39
|
maxSeats: data.max_seats ?? 1,
|
|
37
40
|
validatedAt: new Date().toISOString(),
|
|
41
|
+
email: data.email ?? undefined,
|
|
42
|
+
name: data.name ?? undefined,
|
|
43
|
+
orgName: data.org_name ?? undefined,
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
@@ -63,7 +69,8 @@ function isStale(license) {
|
|
|
63
69
|
const validated = new Date(license.validatedAt).getTime();
|
|
64
70
|
const now = Date.now();
|
|
65
71
|
const daysSince = (now - validated) / (1000 * 60 * 60 * 24);
|
|
66
|
-
|
|
72
|
+
const maxDays = license.status === "trialing" ? REVALIDATE_DAYS_TRIAL : REVALIDATE_DAYS;
|
|
73
|
+
return daysSince > maxDays;
|
|
67
74
|
}
|
|
68
75
|
/**
|
|
69
76
|
* Ensure we have a valid license. Returns the license if valid.
|
|
@@ -74,8 +81,10 @@ export async function ensureLicense() {
|
|
|
74
81
|
const cached = await loadLicense();
|
|
75
82
|
if (!cached)
|
|
76
83
|
return null;
|
|
84
|
+
// Re-validate if fresh but missing fields added after initial cache (one-time migration).
|
|
85
|
+
const needsMigration = cached.valid && !cached.email;
|
|
77
86
|
// If cached and fresh, trust it
|
|
78
|
-
if (cached.valid && !isStale(cached)) {
|
|
87
|
+
if (cached.valid && !isStale(cached) && !needsMigration) {
|
|
79
88
|
return cached;
|
|
80
89
|
}
|
|
81
90
|
// Revalidate
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local team store. This is the single source of truth for who can reach the
|
|
3
|
+
* agent and what they can run. No cloud membership is required.
|
|
4
|
+
*
|
|
5
|
+
* Shape persisted at ~/.leedab/team-permissions.json:
|
|
6
|
+
* {
|
|
7
|
+
* "version": 2,
|
|
8
|
+
* "members": {
|
|
9
|
+
* "<uuid>": {
|
|
10
|
+
* "name": "Jane",
|
|
11
|
+
* "email": "jane@example.com", // optional
|
|
12
|
+
* "role": "member",
|
|
13
|
+
* "joined_at": "2026-04-13T...",
|
|
14
|
+
* "handles": { "telegram": "6549960466" },
|
|
15
|
+
* "allowedWorkflows": [],
|
|
16
|
+
* "allowedChannels": ["telegram"]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export declare const OVERLAY_PATH: string;
|
|
22
|
+
export type ChannelName = "whatsapp" | "telegram" | "teams" | "dashboard";
|
|
23
|
+
export type MemberRole = "owner" | "admin" | "member";
|
|
24
|
+
export interface MemberHandles {
|
|
25
|
+
whatsapp?: string;
|
|
26
|
+
telegram?: string;
|
|
27
|
+
teams?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MemberPermissions {
|
|
30
|
+
name: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
role: MemberRole;
|
|
33
|
+
joined_at: string;
|
|
34
|
+
handles: MemberHandles;
|
|
35
|
+
allowedWorkflows: string[];
|
|
36
|
+
allowedChannels: ChannelName[];
|
|
37
|
+
}
|
|
38
|
+
export interface Overlay {
|
|
39
|
+
version: 2;
|
|
40
|
+
members: Record<string, MemberPermissions>;
|
|
41
|
+
}
|
|
42
|
+
export declare const EMPTY_PERMISSIONS: Omit<MemberPermissions, "name" | "role" | "joined_at">;
|
|
43
|
+
export declare function readOverlay(): Promise<Overlay>;
|
|
44
|
+
/** Atomic write: write a sibling temp file then rename. */
|
|
45
|
+
export declare function writeOverlay(overlay: Overlay): Promise<void>;
|
|
46
|
+
export declare function getMemberPermissions(memberId: string): Promise<MemberPermissions | null>;
|
|
47
|
+
/** Update only the permission fields (handles/workflows/channels) for an existing member. */
|
|
48
|
+
export declare function setMemberPermissions(memberId: string, perms: Partial<Pick<MemberPermissions, "handles" | "allowedWorkflows" | "allowedChannels">>): Promise<MemberPermissions | null>;
|
|
49
|
+
/** Add a new member and return their full record including generated id. */
|
|
50
|
+
export declare function addLocalMember(data: {
|
|
51
|
+
name: string;
|
|
52
|
+
email?: string;
|
|
53
|
+
role: MemberRole;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
id: string;
|
|
56
|
+
} & MemberPermissions>;
|
|
57
|
+
/** Remove a member. Returns true if the member existed. */
|
|
58
|
+
export declare function removeLocalMember(id: string): Promise<boolean>;
|
|
59
|
+
/** Update a member's role. */
|
|
60
|
+
export declare function setMemberRole(id: string, role: MemberRole): Promise<boolean>;
|
|
61
|
+
/** Update a member's identity fields (name, email). */
|
|
62
|
+
export declare function updateMemberIdentity(id: string, fields: {
|
|
63
|
+
name?: string;
|
|
64
|
+
email?: string;
|
|
65
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
4
|
+
import { STATE_DIR } from "../paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Local team store. This is the single source of truth for who can reach the
|
|
7
|
+
* agent and what they can run. No cloud membership is required.
|
|
8
|
+
*
|
|
9
|
+
* Shape persisted at ~/.leedab/team-permissions.json:
|
|
10
|
+
* {
|
|
11
|
+
* "version": 2,
|
|
12
|
+
* "members": {
|
|
13
|
+
* "<uuid>": {
|
|
14
|
+
* "name": "Jane",
|
|
15
|
+
* "email": "jane@example.com", // optional
|
|
16
|
+
* "role": "member",
|
|
17
|
+
* "joined_at": "2026-04-13T...",
|
|
18
|
+
* "handles": { "telegram": "6549960466" },
|
|
19
|
+
* "allowedWorkflows": [],
|
|
20
|
+
* "allowedChannels": ["telegram"]
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export const OVERLAY_PATH = resolve(STATE_DIR, "team-permissions.json");
|
|
26
|
+
export const EMPTY_PERMISSIONS = {
|
|
27
|
+
handles: {},
|
|
28
|
+
allowedWorkflows: [],
|
|
29
|
+
allowedChannels: [],
|
|
30
|
+
};
|
|
31
|
+
function emptyOverlay() {
|
|
32
|
+
return { version: 2, members: {} };
|
|
33
|
+
}
|
|
34
|
+
export async function readOverlay() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readFile(OVERLAY_PATH, "utf-8");
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (!parsed || typeof parsed !== "object" || !parsed.members) {
|
|
39
|
+
return emptyOverlay();
|
|
40
|
+
}
|
|
41
|
+
// Migrate v1 entries (no name/role/joined_at) gracefully
|
|
42
|
+
const members = {};
|
|
43
|
+
for (const [id, entry] of Object.entries(parsed.members)) {
|
|
44
|
+
members[id] = {
|
|
45
|
+
name: entry.name ?? entry.username ?? id.slice(0, 8),
|
|
46
|
+
email: entry.email,
|
|
47
|
+
role: entry.role ?? "member",
|
|
48
|
+
joined_at: entry.joined_at ?? new Date().toISOString(),
|
|
49
|
+
handles: entry.handles ?? {},
|
|
50
|
+
allowedWorkflows: entry.allowedWorkflows ?? [],
|
|
51
|
+
allowedChannels: entry.allowedChannels ?? [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { version: 2, members };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return emptyOverlay();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Atomic write: write a sibling temp file then rename. */
|
|
61
|
+
export async function writeOverlay(overlay) {
|
|
62
|
+
await mkdir(dirname(OVERLAY_PATH), { recursive: true });
|
|
63
|
+
const tmp = `${OVERLAY_PATH}.${process.pid}.tmp`;
|
|
64
|
+
await writeFile(tmp, JSON.stringify(overlay, null, 2) + "\n", "utf-8");
|
|
65
|
+
await rename(tmp, OVERLAY_PATH);
|
|
66
|
+
}
|
|
67
|
+
export async function getMemberPermissions(memberId) {
|
|
68
|
+
const overlay = await readOverlay();
|
|
69
|
+
return overlay.members[memberId] ?? null;
|
|
70
|
+
}
|
|
71
|
+
/** Update only the permission fields (handles/workflows/channels) for an existing member. */
|
|
72
|
+
export async function setMemberPermissions(memberId, perms) {
|
|
73
|
+
const overlay = await readOverlay();
|
|
74
|
+
const existing = overlay.members[memberId];
|
|
75
|
+
if (!existing)
|
|
76
|
+
return null;
|
|
77
|
+
const updated = {
|
|
78
|
+
...existing,
|
|
79
|
+
handles: perms.handles !== undefined
|
|
80
|
+
? { ...existing.handles, ...perms.handles }
|
|
81
|
+
: existing.handles,
|
|
82
|
+
allowedWorkflows: perms.allowedWorkflows !== undefined
|
|
83
|
+
? [...perms.allowedWorkflows]
|
|
84
|
+
: existing.allowedWorkflows,
|
|
85
|
+
allowedChannels: perms.allowedChannels !== undefined
|
|
86
|
+
? [...perms.allowedChannels]
|
|
87
|
+
: existing.allowedChannels,
|
|
88
|
+
};
|
|
89
|
+
overlay.members[memberId] = updated;
|
|
90
|
+
await writeOverlay(overlay);
|
|
91
|
+
return updated;
|
|
92
|
+
}
|
|
93
|
+
/** Add a new member and return their full record including generated id. */
|
|
94
|
+
export async function addLocalMember(data) {
|
|
95
|
+
const overlay = await readOverlay();
|
|
96
|
+
const id = randomUUID();
|
|
97
|
+
const member = {
|
|
98
|
+
name: data.name.trim(),
|
|
99
|
+
email: data.email?.trim().toLowerCase() || undefined,
|
|
100
|
+
role: data.role,
|
|
101
|
+
joined_at: new Date().toISOString(),
|
|
102
|
+
handles: {},
|
|
103
|
+
allowedWorkflows: [],
|
|
104
|
+
allowedChannels: [],
|
|
105
|
+
};
|
|
106
|
+
overlay.members[id] = member;
|
|
107
|
+
await writeOverlay(overlay);
|
|
108
|
+
return { id, ...member };
|
|
109
|
+
}
|
|
110
|
+
/** Remove a member. Returns true if the member existed. */
|
|
111
|
+
export async function removeLocalMember(id) {
|
|
112
|
+
const overlay = await readOverlay();
|
|
113
|
+
if (!overlay.members[id])
|
|
114
|
+
return false;
|
|
115
|
+
delete overlay.members[id];
|
|
116
|
+
await writeOverlay(overlay);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/** Update a member's role. */
|
|
120
|
+
export async function setMemberRole(id, role) {
|
|
121
|
+
const overlay = await readOverlay();
|
|
122
|
+
if (!overlay.members[id])
|
|
123
|
+
return false;
|
|
124
|
+
overlay.members[id].role = role;
|
|
125
|
+
await writeOverlay(overlay);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
/** Update a member's identity fields (name, email). */
|
|
129
|
+
export async function updateMemberIdentity(id, fields) {
|
|
130
|
+
const overlay = await readOverlay();
|
|
131
|
+
if (!overlay.members[id])
|
|
132
|
+
return;
|
|
133
|
+
if (fields.name)
|
|
134
|
+
overlay.members[id].name = fields.name;
|
|
135
|
+
if (fields.email !== undefined)
|
|
136
|
+
overlay.members[id].email = fields.email;
|
|
137
|
+
await writeOverlay(overlay);
|
|
138
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { STATE_DIR } from "../paths.js";
|
|
6
|
+
import { resolveOpenClawBin, openclawEnv } from "../openclaw.js";
|
|
7
|
+
import { readOverlay } from "./permissions.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const MESSAGING_CHANNELS = ["whatsapp", "telegram", "teams"];
|
|
10
|
+
/**
|
|
11
|
+
* Rebuild per-channel allowlists in openclaw.json from the team overlay.
|
|
12
|
+
* The overlay is the source of truth; this writer is idempotent.
|
|
13
|
+
*/
|
|
14
|
+
export async function syncAllowlists(options = {}) {
|
|
15
|
+
const configPath = resolve(STATE_DIR, "openclaw.json");
|
|
16
|
+
let config;
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(configPath, "utf-8");
|
|
19
|
+
config = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// No config yet — nothing to sync into.
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!config.channels)
|
|
26
|
+
config.channels = {};
|
|
27
|
+
const overlay = await readOverlay();
|
|
28
|
+
for (const channel of MESSAGING_CHANNELS) {
|
|
29
|
+
const handles = [];
|
|
30
|
+
for (const perms of Object.values(overlay.members)) {
|
|
31
|
+
if (!perms.allowedChannels?.includes(channel))
|
|
32
|
+
continue;
|
|
33
|
+
const handle = perms.handles[channel];
|
|
34
|
+
if (handle && handle.trim())
|
|
35
|
+
handles.push(handle.trim());
|
|
36
|
+
}
|
|
37
|
+
if (!config.channels[channel]) {
|
|
38
|
+
// Channel not registered locally; skip — we don't want to invent it.
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const deduped = Array.from(new Set(handles));
|
|
42
|
+
config.channels[channel].allowFrom = deduped;
|
|
43
|
+
// allowlist with an empty list is invalid in openclaw ≥2026.3.31;
|
|
44
|
+
// an empty set is semantically equivalent to blocking anyway.
|
|
45
|
+
config.channels[channel].dmPolicy = deduped.length > 0 ? "allowlist" : "disabled";
|
|
46
|
+
}
|
|
47
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
48
|
+
if (options.restart !== false) {
|
|
49
|
+
await restartGatewayQuietly();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function restartGatewayQuietly() {
|
|
53
|
+
try {
|
|
54
|
+
const bin = resolveOpenClawBin();
|
|
55
|
+
const env = openclawEnv(STATE_DIR);
|
|
56
|
+
await execFileAsync(bin, ["gateway", "health"], { env, timeout: 3000 });
|
|
57
|
+
await execFileAsync(bin, ["gateway", "restart"], { env, timeout: 10000 });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Gateway not running — nothing to restart.
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/team.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ChannelName, type MemberHandles, type MemberRole } from "./team/permissions.js";
|
|
2
|
+
export type Role = MemberRole;
|
|
2
3
|
export interface TeamMember {
|
|
3
4
|
id: string;
|
|
4
|
-
|
|
5
|
-
email
|
|
6
|
-
username: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email?: string;
|
|
7
7
|
role: Role;
|
|
8
8
|
joined_at: string;
|
|
9
|
+
handles: MemberHandles;
|
|
10
|
+
allowedWorkflows: string[];
|
|
11
|
+
allowedChannels: ChannelName[];
|
|
9
12
|
}
|
|
10
13
|
export declare function loadTeam(): Promise<TeamMember[]>;
|
|
11
|
-
export declare function addMember(
|
|
14
|
+
export declare function addMember(data: {
|
|
12
15
|
name: string;
|
|
13
|
-
email
|
|
16
|
+
email?: string;
|
|
14
17
|
role: Role;
|
|
15
18
|
}): Promise<TeamMember>;
|
|
16
|
-
export declare function removeMember(
|
|
19
|
+
export declare function removeMember(id: string): Promise<boolean>;
|
|
17
20
|
export declare function updateRole(memberId: string, role: Role): Promise<boolean>;
|
package/dist/team.js
CHANGED
|
@@ -1,75 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Get the license key for API auth.
|
|
5
|
-
*/
|
|
6
|
-
async function getAuthHeader() {
|
|
7
|
-
const license = await loadLicense();
|
|
8
|
-
if (!license?.key) {
|
|
9
|
-
throw new Error("No license key found. Run `leedab onboard` first.");
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
Authorization: `Bearer ${license.key}`,
|
|
13
|
-
"Content-Type": "application/json",
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Get the user's organization slug.
|
|
18
|
-
*/
|
|
19
|
-
async function getOrgSlug(headers) {
|
|
20
|
-
const res = await fetch(`${API_URL}/organizations`, { headers });
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
throw new Error("Failed to fetch organizations.");
|
|
23
|
-
}
|
|
24
|
-
const data = await res.json();
|
|
25
|
-
if (!data.length) {
|
|
26
|
-
throw new Error("No organization found. Create one in the dashboard first.");
|
|
27
|
-
}
|
|
28
|
-
return data[0].slug;
|
|
29
|
-
}
|
|
1
|
+
import { userInfo } from "node:os";
|
|
2
|
+
import { readOverlay, addLocalMember, removeLocalMember, setMemberRole, updateMemberIdentity, } from "./team/permissions.js";
|
|
3
|
+
import { ensureLicense } from "./license.js";
|
|
30
4
|
export async function loadTeam() {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
5
|
+
const overlay = await readOverlay();
|
|
6
|
+
const entries = Object.entries(overlay.members);
|
|
7
|
+
// ensureLicense re-validates if email/name fields are missing (one-time migration).
|
|
8
|
+
const license = await ensureLicense().catch(() => null);
|
|
9
|
+
const licenseEmail = license?.email;
|
|
10
|
+
const licenseName = license?.name || licenseEmail?.split("@")[0];
|
|
11
|
+
// Seed the install owner on first use so they always appear in the list.
|
|
12
|
+
if (entries.length === 0) {
|
|
13
|
+
const owner = await addLocalMember({
|
|
14
|
+
name: licenseName || userInfo().username,
|
|
15
|
+
email: licenseEmail,
|
|
16
|
+
role: "owner",
|
|
17
|
+
});
|
|
18
|
+
return [{
|
|
19
|
+
id: owner.id,
|
|
20
|
+
name: owner.name,
|
|
21
|
+
email: owner.email,
|
|
22
|
+
role: owner.role,
|
|
23
|
+
joined_at: owner.joined_at,
|
|
24
|
+
handles: owner.handles,
|
|
25
|
+
allowedWorkflows: owner.allowedWorkflows,
|
|
26
|
+
allowedChannels: owner.allowedChannels,
|
|
27
|
+
}];
|
|
36
28
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
email: member.email,
|
|
48
|
-
role: member.role,
|
|
49
|
-
}),
|
|
50
|
-
});
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
const err = await res.json().catch(() => ({}));
|
|
53
|
-
throw new Error(err.error || "Failed to add team member.");
|
|
29
|
+
// Auto-update owner entry if it was seeded with the OS username before
|
|
30
|
+
// license name/email were available.
|
|
31
|
+
if (licenseName && licenseName !== userInfo().username) {
|
|
32
|
+
const ownerEntry = entries.find(([, m]) => m.role === "owner" && m.name === userInfo().username);
|
|
33
|
+
if (ownerEntry) {
|
|
34
|
+
await updateMemberIdentity(ownerEntry[0], { name: licenseName, email: licenseEmail });
|
|
35
|
+
ownerEntry[1].name = licenseName;
|
|
36
|
+
if (licenseEmail)
|
|
37
|
+
ownerEntry[1].email = licenseEmail;
|
|
38
|
+
}
|
|
54
39
|
}
|
|
55
|
-
return
|
|
40
|
+
return entries.map(([id, m]) => ({
|
|
41
|
+
id,
|
|
42
|
+
name: m.name,
|
|
43
|
+
email: m.email,
|
|
44
|
+
role: m.role,
|
|
45
|
+
joined_at: m.joined_at,
|
|
46
|
+
handles: { ...m.handles },
|
|
47
|
+
allowedWorkflows: [...m.allowedWorkflows],
|
|
48
|
+
allowedChannels: [...m.allowedChannels],
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
export async function addMember(data) {
|
|
52
|
+
const result = await addLocalMember(data);
|
|
53
|
+
return {
|
|
54
|
+
id: result.id,
|
|
55
|
+
name: result.name,
|
|
56
|
+
email: result.email,
|
|
57
|
+
role: result.role,
|
|
58
|
+
joined_at: result.joined_at,
|
|
59
|
+
handles: result.handles,
|
|
60
|
+
allowedWorkflows: result.allowedWorkflows,
|
|
61
|
+
allowedChannels: result.allowedChannels,
|
|
62
|
+
};
|
|
56
63
|
}
|
|
57
|
-
export async function removeMember(
|
|
58
|
-
|
|
59
|
-
const slug = await getOrgSlug(headers);
|
|
60
|
-
const res = await fetch(`${API_URL}/organizations/${slug}/members/${memberId}/`, {
|
|
61
|
-
method: "DELETE",
|
|
62
|
-
headers,
|
|
63
|
-
});
|
|
64
|
-
return res.ok;
|
|
64
|
+
export async function removeMember(id) {
|
|
65
|
+
return removeLocalMember(id);
|
|
65
66
|
}
|
|
66
67
|
export async function updateRole(memberId, role) {
|
|
67
|
-
|
|
68
|
-
const slug = await getOrgSlug(headers);
|
|
69
|
-
const res = await fetch(`${API_URL}/organizations/${slug}/members/${memberId}/`, {
|
|
70
|
-
method: "PUT",
|
|
71
|
-
headers,
|
|
72
|
-
body: JSON.stringify({ role }),
|
|
73
|
-
});
|
|
74
|
-
return res.ok;
|
|
68
|
+
return setMemberRole(memberId, role);
|
|
75
69
|
}
|
|
@@ -22,6 +22,7 @@ When you write a Python script to generate a document from a template, save thes
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
<a id="payroll_calculator"></a>
|
|
25
26
|
## 1. Payroll Calculator
|
|
26
27
|
|
|
27
28
|
**Trigger:** User says "run payroll", "calculate payroll", "process payroll for [period]", or uploads attendance/salary files.
|
|
@@ -45,6 +46,7 @@ When you write a Python script to generate a document from a template, save thes
|
|
|
45
46
|
|
|
46
47
|
---
|
|
47
48
|
|
|
49
|
+
<a id="credit_note_generator"></a>
|
|
48
50
|
## 2. Credit Note Generator
|
|
49
51
|
|
|
50
52
|
**Trigger:** User says "generate credit notes", "create credit notes for [period]", or uploads deduction data.
|
|
@@ -72,6 +74,7 @@ When you write a Python script to generate a document from a template, save thes
|
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
77
|
+
<a id="deduction_reconciler"></a>
|
|
75
78
|
## 3. Deduction Reconciler
|
|
76
79
|
|
|
77
80
|
**Trigger:** User says "reconcile deductions", "check deductions", or uploads credit notes + payout sheets.
|
|
@@ -96,6 +99,7 @@ When you write a Python script to generate a document from a template, save thes
|
|
|
96
99
|
|
|
97
100
|
---
|
|
98
101
|
|
|
102
|
+
<a id="attendance_importer"></a>
|
|
99
103
|
## 4. Attendance Importer
|
|
100
104
|
|
|
101
105
|
**Trigger:** User says "import attendance", "process attendance for [period]", or uploads client attendance sheets.
|
|
@@ -121,6 +125,7 @@ When you write a Python script to generate a document from a template, save thes
|
|
|
121
125
|
|
|
122
126
|
---
|
|
123
127
|
|
|
128
|
+
<a id="driver_clearance"></a>
|
|
124
129
|
## 5. Driver Clearance
|
|
125
130
|
|
|
126
131
|
**Trigger:** User says "process clearance", "driver offboarding", or uploads clearance data.
|