leedab 0.1.9 → 0.2.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/dist/team.js CHANGED
@@ -1,75 +1,69 @@
1
- import { loadLicense } from "./license.js";
2
- const API_URL = "https://api.leedab.com/api/v1";
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 headers = await getAuthHeader();
32
- const slug = await getOrgSlug(headers);
33
- const res = await fetch(`${API_URL}/organizations/${slug}/members/`, { headers });
34
- if (!res.ok) {
35
- throw new Error("Failed to fetch team members.");
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
- const data = await res.json();
38
- return data.members;
39
- }
40
- export async function addMember(member) {
41
- const headers = await getAuthHeader();
42
- const slug = await getOrgSlug(headers);
43
- const res = await fetch(`${API_URL}/organizations/${slug}/invites/`, {
44
- method: "POST",
45
- headers,
46
- body: JSON.stringify({
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 await res.json();
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(memberId) {
58
- const headers = await getAuthHeader();
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
- const headers = await getAuthHeader();
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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canonical list of named workflows the agent can run.
3
+ *
4
+ * Workflows are the unit of permission, not tools or skills. IDs match the
5
+ * anchors in src/templates/verticals/supply-chain/WORKFLOWS.md so the prompt
6
+ * preamble and the template stay in lockstep.
7
+ */
8
+ export interface WorkflowDef {
9
+ id: string;
10
+ title: string;
11
+ description: string;
12
+ /**
13
+ * UI hint only. Marks workflows that move money or produce billable
14
+ * documents so the team page can flag them visually. All enforcement is
15
+ * the admin-configured allowedWorkflows list, read through the prompt
16
+ * preamble. There is no server-side hard gate.
17
+ */
18
+ isFinancial: boolean;
19
+ }
20
+ export declare const WORKFLOWS: WorkflowDef[];
21
+ export declare function isKnownWorkflow(id: string): boolean;
22
+ export declare function getWorkflow(id: string): WorkflowDef | undefined;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Canonical list of named workflows the agent can run.
3
+ *
4
+ * Workflows are the unit of permission, not tools or skills. IDs match the
5
+ * anchors in src/templates/verticals/supply-chain/WORKFLOWS.md so the prompt
6
+ * preamble and the template stay in lockstep.
7
+ */
8
+ export const WORKFLOWS = [
9
+ {
10
+ id: "payroll_calculator",
11
+ title: "Payroll Calculator",
12
+ description: "Calculate monthly payroll from attendance and deduction data.",
13
+ isFinancial: true,
14
+ },
15
+ {
16
+ id: "credit_note_generator",
17
+ title: "Credit Note Generator",
18
+ description: "Generate credit notes (PDF/DOCX) from deduction data.",
19
+ isFinancial: true,
20
+ },
21
+ {
22
+ id: "deduction_reconciler",
23
+ title: "Deduction Reconciler",
24
+ description: "Reconcile credit notes against actual payout deductions.",
25
+ isFinancial: false,
26
+ },
27
+ {
28
+ id: "attendance_importer",
29
+ title: "Attendance Importer",
30
+ description: "Normalize client attendance sheets into a standard report.",
31
+ isFinancial: false,
32
+ },
33
+ {
34
+ id: "driver_clearance",
35
+ title: "Driver Clearance",
36
+ description: "Track driver offboarding, equipment return, and pending deductions.",
37
+ isFinancial: false,
38
+ },
39
+ ];
40
+ const WORKFLOW_IDS = new Set(WORKFLOWS.map((w) => w.id));
41
+ export function isKnownWorkflow(id) {
42
+ return WORKFLOW_IDS.has(id);
43
+ }
44
+ export function getWorkflow(id) {
45
+ return WORKFLOWS.find((w) => w.id === id);
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leedab",
3
- "version": "0.1.9",
3
+ "version": "0.2.2",
4
4
  "description": "LeedAB — Your enterprise AI agent. Local-first, private by default.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "LeedAB <hello@leedab.com>",
@@ -1,351 +0,0 @@
1
- // Fetch status, vault, and allowlist on load
2
- document.addEventListener("DOMContentLoaded", () => {
3
- refreshStatus();
4
- loadVault();
5
- loadAllowlist();
6
- });
7
-
8
- async function refreshStatus() {
9
- try {
10
- const res = await fetch("/api/status");
11
- const status = await res.json();
12
-
13
- for (const [key, info] of Object.entries(status)) {
14
- const card = document.getElementById(`card-${key}`);
15
- const statusEl = document.getElementById(`status-${key}`);
16
- const btn = card?.querySelector(".btn");
17
-
18
- if (info.connected) {
19
- card?.classList.add("connected");
20
- if (statusEl) {
21
- statusEl.innerHTML = '<span class="dot"></span> Connected';
22
- }
23
- if (btn) {
24
- btn.textContent = "Connected";
25
- btn.className = "btn btn-connected";
26
- btn.disabled = true;
27
- btn.onclick = null;
28
- }
29
- }
30
- }
31
- } catch (err) {
32
- console.error("Failed to fetch status:", err);
33
- }
34
- }
35
-
36
- // --- WhatsApp ---
37
-
38
- function connectWhatsApp() {
39
- const panel = document.getElementById("whatsapp-panel");
40
- panel.classList.remove("hidden");
41
- const qr = document.getElementById("whatsapp-qr");
42
- qr.innerHTML = `<p>Generating QR code...</p>`;
43
-
44
- fetch("/api/whatsapp/connect", { method: "POST" })
45
- .then((r) => r.json())
46
- .then((data) => {
47
- if (data.error) {
48
- qr.innerHTML = `<p style="color:#ef4444">${escapeHtml(data.error)}</p>`;
49
- return;
50
- }
51
- if (data.qr) {
52
- const lines = data.qr.split("\n").filter(
53
- (l) => l.includes("\u2588") || l.includes("\u2584") || l.includes("\u2580")
54
- );
55
- const qrText = lines.join("\n");
56
- qr.innerHTML = `<pre style="
57
- font-family: monospace;
58
- font-size: 4px;
59
- line-height: 4px;
60
- letter-spacing: 0px;
61
- background: white;
62
- color: black;
63
- padding: 16px;
64
- display: inline-block;
65
- border-radius: 8px;
66
- transform: scale(2);
67
- transform-origin: center;
68
- margin: 40px 0;
69
- ">${escapeHtml(qrText)}</pre>`;
70
- pollWhatsAppStatus();
71
- } else {
72
- qr.innerHTML = `<p>Waiting for QR code... try again in a moment.</p>`;
73
- }
74
- })
75
- .catch(() => {
76
- qr.innerHTML = `<p style="color:#ef4444">Connection failed. Try again.</p>`;
77
- });
78
- }
79
-
80
- function pollWhatsAppStatus() {
81
- const interval = setInterval(async () => {
82
- const res = await fetch("/api/status");
83
- const status = await res.json();
84
- if (status.whatsapp.connected) {
85
- clearInterval(interval);
86
- hideWhatsAppPanel();
87
- showToast("WhatsApp connected!", "success");
88
- refreshStatus();
89
- }
90
- }, 3000);
91
-
92
- setTimeout(() => clearInterval(interval), 120000);
93
- }
94
-
95
- function hideWhatsAppPanel() {
96
- document.getElementById("whatsapp-panel").classList.add("hidden");
97
- }
98
-
99
- // --- Telegram ---
100
-
101
- function showTelegramForm() {
102
- document.getElementById("telegram-form").classList.remove("hidden");
103
- }
104
-
105
- function hideTelegramForm() {
106
- document.getElementById("telegram-form").classList.add("hidden");
107
- }
108
-
109
- async function connectTelegram() {
110
- const token = document.getElementById("telegram-token").value.trim();
111
- if (!token) {
112
- showToast("Bot token is required", "error");
113
- return;
114
- }
115
-
116
- try {
117
- const res = await fetch("/api/telegram/connect", {
118
- method: "POST",
119
- headers: { "Content-Type": "application/json" },
120
- body: JSON.stringify({ token }),
121
- });
122
- const data = await res.json();
123
- if (data.error) {
124
- showToast(data.error, "error");
125
- } else {
126
- hideTelegramForm();
127
- showToast("Telegram connected!", "success");
128
- refreshStatus();
129
- }
130
- } catch {
131
- showToast("Connection failed", "error");
132
- }
133
- }
134
-
135
- // --- Teams ---
136
-
137
- function showTeamsForm() {
138
- document.getElementById("teams-form").classList.remove("hidden");
139
- }
140
-
141
- function hideTeamsForm() {
142
- document.getElementById("teams-form").classList.add("hidden");
143
- }
144
-
145
- async function connectTeams() {
146
- const appId = document.getElementById("teams-app-id").value.trim();
147
- const tenantId = document.getElementById("teams-tenant-id").value.trim();
148
-
149
- if (!appId || !tenantId) {
150
- showToast("All fields are required", "error");
151
- return;
152
- }
153
-
154
- try {
155
- const res = await fetch("/api/teams/connect", {
156
- method: "POST",
157
- headers: { "Content-Type": "application/json" },
158
- body: JSON.stringify({ appId, tenantId }),
159
- });
160
- const data = await res.json();
161
- if (data.error) {
162
- showToast(data.error, "error");
163
- } else {
164
- hideTeamsForm();
165
- showToast("Teams credentials saved!", "success");
166
- refreshStatus();
167
- }
168
- } catch {
169
- showToast("Connection failed", "error");
170
- }
171
- }
172
-
173
- // --- Vault ---
174
-
175
- async function loadVault() {
176
- const container = document.getElementById("vault-list");
177
- if (!container) return;
178
-
179
- try {
180
- const res = await fetch("/api/vault");
181
- const entries = await res.json();
182
-
183
- if (!entries.length) {
184
- container.innerHTML = '<div class="vault-empty">No credentials stored yet.</div>';
185
- return;
186
- }
187
-
188
- container.innerHTML = `
189
- <table class="vault-table">
190
- <thead>
191
- <tr><th>Service</th><th>URL</th><th>Username</th><th></th></tr>
192
- </thead>
193
- <tbody>
194
- ${entries.map(e => `
195
- <tr>
196
- <td>${escapeHtml(e.service)}</td>
197
- <td>${e.url ? escapeHtml(e.url) : '<span style="color:var(--text-faint)">—</span>'}</td>
198
- <td>${e.username ? escapeHtml(e.username) : '<span style="color:var(--text-faint)">—</span>'}</td>
199
- <td><button class="btn btn-danger" onclick="removeVaultEntry('${escapeHtml(e.service)}')">Remove</button></td>
200
- </tr>
201
- `).join("")}
202
- </tbody>
203
- </table>`;
204
- } catch {
205
- container.innerHTML = '<div class="vault-empty">Could not load vault.</div>';
206
- }
207
- }
208
-
209
- async function addVaultEntry() {
210
- const service = document.getElementById("vault-service").value.trim();
211
- const url = document.getElementById("vault-url").value.trim();
212
- const username = document.getElementById("vault-username").value.trim();
213
- const password = document.getElementById("vault-password").value;
214
- const notes = document.getElementById("vault-notes").value.trim();
215
-
216
- if (!service) {
217
- showToast("Service name is required", "error");
218
- return;
219
- }
220
-
221
- try {
222
- await fetch("/api/vault", {
223
- method: "POST",
224
- headers: { "Content-Type": "application/json" },
225
- body: JSON.stringify({ service, url, username, password, notes }),
226
- });
227
-
228
- // Clear form
229
- document.getElementById("vault-service").value = "";
230
- document.getElementById("vault-url").value = "";
231
- document.getElementById("vault-username").value = "";
232
- document.getElementById("vault-password").value = "";
233
- document.getElementById("vault-notes").value = "";
234
-
235
- showToast(`Added ${service} to vault`, "success");
236
- loadVault();
237
- } catch {
238
- showToast("Failed to add credential", "error");
239
- }
240
- }
241
-
242
- async function removeVaultEntry(service) {
243
- try {
244
- await fetch(`/api/vault?service=${encodeURIComponent(service)}`, {
245
- method: "DELETE",
246
- });
247
- showToast(`Removed ${service}`, "success");
248
- loadVault();
249
- } catch {
250
- showToast("Failed to remove credential", "error");
251
- }
252
- }
253
-
254
- // --- Allowlist ---
255
-
256
- async function loadAllowlist() {
257
- const container = document.getElementById("allowlist-entries");
258
- const channelSelect = document.getElementById("allowlist-channel");
259
- if (!container || !channelSelect) return;
260
-
261
- const channel = channelSelect.value;
262
-
263
- try {
264
- const res = await fetch(`/api/allowlist?channel=${encodeURIComponent(channel)}`);
265
- const data = await res.json();
266
-
267
- if (!data.allowFrom || data.allowFrom.length === 0) {
268
- container.innerHTML = `<div class="vault-empty">No users in ${channel} allowlist. Policy: ${data.dmPolicy}</div>`;
269
- return;
270
- }
271
-
272
- container.innerHTML = `
273
- <table class="vault-table">
274
- <thead>
275
- <tr><th>User ID</th><th>Policy</th><th></th></tr>
276
- </thead>
277
- <tbody>
278
- ${data.allowFrom.map(id => `
279
- <tr>
280
- <td>${escapeHtml(id)}</td>
281
- <td style="color:var(--text-dim)">${data.dmPolicy}</td>
282
- <td><button class="btn btn-danger" onclick="removeAllowlistEntry('${escapeHtml(channel)}', '${escapeHtml(id)}')">Remove</button></td>
283
- </tr>
284
- `).join("")}
285
- </tbody>
286
- </table>`;
287
- } catch {
288
- container.innerHTML = '<div class="vault-empty">Could not load allowlist.</div>';
289
- }
290
- }
291
-
292
- async function addAllowlistEntry() {
293
- const channel = document.getElementById("allowlist-channel").value;
294
- const userId = document.getElementById("allowlist-userid").value.trim();
295
-
296
- if (!userId) {
297
- showToast("User ID is required", "error");
298
- return;
299
- }
300
-
301
- try {
302
- const res = await fetch("/api/allowlist", {
303
- method: "POST",
304
- headers: { "Content-Type": "application/json" },
305
- body: JSON.stringify({ channel, userId }),
306
- });
307
- const data = await res.json();
308
- if (data.error) {
309
- showToast(data.error, "error");
310
- } else {
311
- document.getElementById("allowlist-userid").value = "";
312
- showToast(`Added ${userId} to ${channel}.`, "success");
313
- loadAllowlist();
314
- }
315
- } catch {
316
- showToast("Failed to add user", "error");
317
- }
318
- }
319
-
320
- async function removeAllowlistEntry(channel, userId) {
321
- try {
322
- await fetch(`/api/allowlist?channel=${encodeURIComponent(channel)}&userId=${encodeURIComponent(userId)}`, {
323
- method: "DELETE",
324
- });
325
- showToast(`Removed ${userId}.`, "success");
326
- loadAllowlist();
327
- } catch {
328
- showToast("Failed to remove user", "error");
329
- }
330
- }
331
-
332
- // --- Helpers ---
333
-
334
- function showToast(message, type = "success") {
335
- const toast = document.getElementById("toast");
336
- if (!toast) return;
337
- toast.textContent = message;
338
- toast.className = `toast ${type}`;
339
-
340
- setTimeout(() => {
341
- toast.classList.add("hidden");
342
- }, 4000);
343
- }
344
-
345
- function escapeHtml(str) {
346
- return str
347
- .replace(/&/g, "&amp;")
348
- .replace(/</g, "&lt;")
349
- .replace(/>/g, "&gt;")
350
- .replace(/"/g, "&quot;");
351
- }