@commandgarden/cli 2.1.0 → 2.3.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/main.js +7 -2
- package/node_modules/@commandgarden/app/dist/client/assets/{index-JB_Cdl_F.css → index-DlAU-HAE.css} +1 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-VwLyBHHl.js +397 -0
- package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
- package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +1 -0
- package/node_modules/@commandgarden/app/dist/server/routes/index.js +2 -0
- package/node_modules/@commandgarden/app/dist/server/routes/trusted-peers-cache.d.ts +3 -0
- package/node_modules/@commandgarden/app/dist/server/routes/trusted-peers-cache.js +17 -0
- package/node_modules/@commandgarden/app/dist/server/store.d.ts +5 -0
- package/node_modules/@commandgarden/app/dist/server/store.js +19 -0
- package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.eval.js +94 -0
- package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.yaml +34 -0
- package/package.json +2 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-GpK1-Tyq.js +0 -255
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
12
12
|
<title>commandGarden</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-VwLyBHHl.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DlAU-HAE.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -2,6 +2,7 @@ import { HIGH_RISK_CAPABILITIES } from '@commandgarden/shared';
|
|
|
2
2
|
const APP_ROUTES = {
|
|
3
3
|
'timetracking/report': '/apps/timetracking',
|
|
4
4
|
'teams/room-availability': '/apps/rooms',
|
|
5
|
+
'saba/pending-training': '/apps/saba',
|
|
5
6
|
'tokenmaster/clients-list': '/apps/trusted-peer-expiry',
|
|
6
7
|
'tokenmaster/client-trustedby': '/apps/trusted-peer-expiry',
|
|
7
8
|
};
|
|
@@ -8,6 +8,7 @@ import { goalsRoutes } from './goals.js';
|
|
|
8
8
|
import { timetrackingCacheRoutes } from './timetracking-cache.js';
|
|
9
9
|
import { journalRoutes } from './journal.js';
|
|
10
10
|
import { securityNewsCacheRoutes } from './security-news-cache.js';
|
|
11
|
+
import { trustedPeersCacheRoutes } from './trusted-peers-cache.js';
|
|
11
12
|
import { skillsRoutes } from './skills.js';
|
|
12
13
|
export function registerRoutes(app, daemon, store) {
|
|
13
14
|
statusRoutes(app, daemon);
|
|
@@ -20,5 +21,6 @@ export function registerRoutes(app, daemon, store) {
|
|
|
20
21
|
timetrackingCacheRoutes(app, store);
|
|
21
22
|
journalRoutes(app, daemon, store);
|
|
22
23
|
securityNewsCacheRoutes(app, store);
|
|
24
|
+
trustedPeersCacheRoutes(app, store);
|
|
23
25
|
skillsRoutes(app);
|
|
24
26
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function trustedPeersCacheRoutes(app, store) {
|
|
2
|
+
app.get('/api/trusted-peers/cache', async () => {
|
|
3
|
+
const cached = store.getCachedTrustedPeers();
|
|
4
|
+
if (!cached)
|
|
5
|
+
return { ok: true, data: null, fetchedAt: null };
|
|
6
|
+
return { ok: true, data: cached.data, fetchedAt: cached.fetchedAt };
|
|
7
|
+
});
|
|
8
|
+
app.post('/api/trusted-peers/cache', async (req, reply) => {
|
|
9
|
+
const { data } = req.body;
|
|
10
|
+
if (!Array.isArray(data)) {
|
|
11
|
+
reply.code(400);
|
|
12
|
+
return { ok: false, error: 'Missing data array' };
|
|
13
|
+
}
|
|
14
|
+
store.cacheTrustedPeers(data);
|
|
15
|
+
return { ok: true };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -43,6 +43,11 @@ export declare class AppStore {
|
|
|
43
43
|
data: Record<string, unknown>[];
|
|
44
44
|
fetchedAt: string;
|
|
45
45
|
} | null;
|
|
46
|
+
cacheTrustedPeers(data: Record<string, unknown>[]): void;
|
|
47
|
+
getCachedTrustedPeers(): {
|
|
48
|
+
data: Record<string, unknown>[];
|
|
49
|
+
fetchedAt: string;
|
|
50
|
+
} | null;
|
|
46
51
|
cacheSecurityNews(data: Record<string, unknown>[]): void;
|
|
47
52
|
getCachedSecurityNews(): {
|
|
48
53
|
data: Record<string, unknown>[];
|
|
@@ -101,6 +101,11 @@ export class AppStore {
|
|
|
101
101
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
102
|
data TEXT NOT NULL,
|
|
103
103
|
fetched_at TEXT NOT NULL
|
|
104
|
+
)`);
|
|
105
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
|
|
106
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
|
+
data TEXT NOT NULL,
|
|
108
|
+
fetched_at TEXT NOT NULL
|
|
104
109
|
)`);
|
|
105
110
|
}
|
|
106
111
|
getPreference(key) {
|
|
@@ -200,6 +205,20 @@ export class AppStore {
|
|
|
200
205
|
fetchedAt: rows[0].fetched_at,
|
|
201
206
|
};
|
|
202
207
|
}
|
|
208
|
+
cacheTrustedPeers(data) {
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
this.db.run('INSERT OR REPLACE INTO trusted_peers_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
|
|
211
|
+
this.persist();
|
|
212
|
+
}
|
|
213
|
+
getCachedTrustedPeers() {
|
|
214
|
+
const rows = this.query('SELECT data, fetched_at FROM trusted_peers_cache WHERE id = 1');
|
|
215
|
+
if (rows.length === 0)
|
|
216
|
+
return null;
|
|
217
|
+
return {
|
|
218
|
+
data: JSON.parse(rows[0].data),
|
|
219
|
+
fetchedAt: rows[0].fetched_at,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
203
222
|
cacheSecurityNews(data) {
|
|
204
223
|
const now = new Date().toISOString();
|
|
205
224
|
this.db.run('INSERT OR REPLACE INTO security_news_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Runs in Saba Cloud page context via js_evaluate step.
|
|
2
|
+
// Fetches pending mandatory learning items from Saba Cloud (v64+) using two
|
|
3
|
+
// confirmed API endpoints:
|
|
4
|
+
//
|
|
5
|
+
// 1. GET /Saba/api/ui/torque/uicontext/currentuser
|
|
6
|
+
// → userInfo.userId (e.g. "emplo000000001653633")
|
|
7
|
+
//
|
|
8
|
+
// 2. GET /Saba/api/common/todocontroller/detail/{userId}?context=learning&expand=savetabpref
|
|
9
|
+
// → searchResults[1] (Saba list format: ["list", [...items]])
|
|
10
|
+
|
|
11
|
+
const ORIGIN = 'https://daimler.sabacloud.com';
|
|
12
|
+
|
|
13
|
+
// ── Wait for valid Saba session (SSO / redirects, up to 60s) ─────────────
|
|
14
|
+
|
|
15
|
+
const __deadline = Date.now() + 60000;
|
|
16
|
+
while (Date.now() < __deadline) {
|
|
17
|
+
if (location.hostname === 'daimler.sabacloud.com' && document.readyState === 'complete') break;
|
|
18
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
19
|
+
}
|
|
20
|
+
if (location.hostname !== 'daimler.sabacloud.com') {
|
|
21
|
+
throw new Error('AUTH_REQUIRED: Not signed in to Saba — log in at daimler.sabacloud.com and retry');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Step 1: Resolve current user ID ──────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const meResp = await fetch(`${ORIGIN}/Saba/api/ui/torque/uicontext/currentuser`, {
|
|
27
|
+
headers: { Accept: 'application/json' },
|
|
28
|
+
credentials: 'include',
|
|
29
|
+
});
|
|
30
|
+
if (meResp.status === 401 || meResp.status === 403) {
|
|
31
|
+
throw new Error('AUTH_REQUIRED: Saba session expired — log in and retry');
|
|
32
|
+
}
|
|
33
|
+
if (!meResp.ok) {
|
|
34
|
+
throw new Error(`currentuser API returned HTTP ${meResp.status}`);
|
|
35
|
+
}
|
|
36
|
+
const meData = await meResp.json();
|
|
37
|
+
const userId = meData?.userInfo?.userId;
|
|
38
|
+
if (!userId) {
|
|
39
|
+
throw new Error('Could not read userInfo.userId from currentuser response');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Step 2: Fetch pending learning to-do items ────────────────────────────
|
|
43
|
+
|
|
44
|
+
const todoResp = await fetch(
|
|
45
|
+
`${ORIGIN}/Saba/api/common/todocontroller/detail/${encodeURIComponent(userId)}?context=learning&expand=savetabpref`,
|
|
46
|
+
{ headers: { Accept: 'application/json' }, credentials: 'include' }
|
|
47
|
+
);
|
|
48
|
+
if (!todoResp.ok) {
|
|
49
|
+
throw new Error(`todocontroller API returned HTTP ${todoResp.status} for userId=${userId}`);
|
|
50
|
+
}
|
|
51
|
+
const todoData = await todoResp.json();
|
|
52
|
+
|
|
53
|
+
// searchResults is serialised as ["list", [item, item, ...]]
|
|
54
|
+
const rawList = todoData?.searchResults;
|
|
55
|
+
const items = Array.isArray(rawList)
|
|
56
|
+
? (rawList[0] === 'list' ? rawList[1] : rawList)
|
|
57
|
+
: [];
|
|
58
|
+
|
|
59
|
+
if (!Array.isArray(items)) {
|
|
60
|
+
throw new Error('Unexpected todocontroller response shape — searchResults is not a list');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Step 3: Process and return rows ──────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const DONE_STATUSES = new Set([
|
|
66
|
+
'completed', 'passed', 'withdrawn', 'cancelled', 'waived', 'successful',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const today = new Date();
|
|
70
|
+
today.setHours(0, 0, 0, 0);
|
|
71
|
+
|
|
72
|
+
return items
|
|
73
|
+
.filter(item => {
|
|
74
|
+
if (!item.mandatory) return false;
|
|
75
|
+
const status = (item.itemDisplayStatus || '').toLowerCase();
|
|
76
|
+
return !DONE_STATUSES.has(status);
|
|
77
|
+
})
|
|
78
|
+
.map(item => {
|
|
79
|
+
const dueDateMs = item.dueDate?.date ?? null;
|
|
80
|
+
const dueDateObj = dueDateMs ? new Date(dueDateMs) : null;
|
|
81
|
+
const daysUntilDue = item.dueDateTs?.localDaysDifference
|
|
82
|
+
?? (dueDateObj !== null
|
|
83
|
+
? Math.ceil((dueDateObj.getTime() - today.getTime()) / 86400000)
|
|
84
|
+
: null);
|
|
85
|
+
return {
|
|
86
|
+
title: item.itemName || '(unknown)',
|
|
87
|
+
type: item.itemType || 'Training',
|
|
88
|
+
status: item.itemDisplayStatus || 'Pending',
|
|
89
|
+
dueDate: dueDateObj ? dueDateObj.toISOString().slice(0, 10) : '',
|
|
90
|
+
daysUntilDue: daysUntilDue ?? 9999,
|
|
91
|
+
isOverdue: item.overdue ?? (daysUntilDue !== null && daysUntilDue < 0),
|
|
92
|
+
};
|
|
93
|
+
})
|
|
94
|
+
.sort((a, b) => a.daysUntilDue - b.daysUntilDue);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
site: saba
|
|
2
|
+
name: pending-training
|
|
3
|
+
version: "1.0"
|
|
4
|
+
description: "Pending mandatory learning plan items from Saba Cloud (daimler.sabacloud.com)"
|
|
5
|
+
access: read
|
|
6
|
+
|
|
7
|
+
domains:
|
|
8
|
+
- "daimler.sabacloud.com"
|
|
9
|
+
capabilities:
|
|
10
|
+
- navigate
|
|
11
|
+
- js_evaluate
|
|
12
|
+
|
|
13
|
+
columns:
|
|
14
|
+
- name: title
|
|
15
|
+
type: string
|
|
16
|
+
- name: type
|
|
17
|
+
type: string
|
|
18
|
+
- name: status
|
|
19
|
+
type: string
|
|
20
|
+
- name: dueDate
|
|
21
|
+
type: string
|
|
22
|
+
- name: daysUntilDue
|
|
23
|
+
type: number
|
|
24
|
+
- name: isOverdue
|
|
25
|
+
type: boolean
|
|
26
|
+
|
|
27
|
+
pipeline:
|
|
28
|
+
- step: navigate
|
|
29
|
+
url: "https://daimler.sabacloud.com/"
|
|
30
|
+
- step: wait
|
|
31
|
+
selector: "body"
|
|
32
|
+
timeout: 60000
|
|
33
|
+
- step: js_evaluate
|
|
34
|
+
file: saba-pending-training.eval.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commandgarden/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Enterprise browser automation CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@fastify/static": "^8.0.0",
|
|
43
43
|
"@fastify/websocket": "^11.0.0",
|
|
44
44
|
"sql.js": "^1.11.0",
|
|
45
|
+
"turndown": "^7.2.4",
|
|
45
46
|
"cli-table3": "^0.6.0",
|
|
46
47
|
"commander": "^12.0.0",
|
|
47
48
|
"fastify": "^5.0.0",
|