@commandgarden/cli 1.4.2 → 2.1.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 +67 -8
- package/node_modules/@commandgarden/app/dist/client/assets/index-GpK1-Tyq.js +255 -0
- package/node_modules/@commandgarden/app/dist/client/assets/index-JB_Cdl_F.css +1 -0
- package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
- package/node_modules/@commandgarden/app/dist/server/journal/journal.config.d.ts +25 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.config.js +31 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.service.d.ts +37 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.service.js +142 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.types.d.ts +91 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.types.js +4 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.d.ts +21 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.js +72 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/jira.source.d.ts +15 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/jira.source.js +42 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.d.ts +21 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.js +25 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/timetracking.source.d.ts +17 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/timetracking.source.js +83 -0
- package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +2 -0
- package/node_modules/@commandgarden/app/dist/server/routes/index.js +6 -0
- package/node_modules/@commandgarden/app/dist/server/routes/journal.d.ts +9 -0
- package/node_modules/@commandgarden/app/dist/server/routes/journal.js +56 -0
- package/node_modules/@commandgarden/app/dist/server/routes/security-news-cache.d.ts +3 -0
- package/node_modules/@commandgarden/app/dist/server/routes/security-news-cache.js +17 -0
- package/node_modules/@commandgarden/app/dist/server/routes/skills.d.ts +2 -0
- package/node_modules/@commandgarden/app/dist/server/routes/skills.js +65 -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/chrome/dist/content-script.js +84 -2
- package/node_modules/@commandgarden/chrome/dist/content-script.js.map +2 -2
- package/node_modules/@commandgarden/chrome/dist/popup.html +74 -53
- package/node_modules/@commandgarden/chrome/dist/service-worker.js +73 -8
- package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +3 -3
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +143 -0
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +70 -0
- package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +58 -0
- package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +47 -0
- package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -0
- package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -0
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +186 -0
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +41 -0
- package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -0
- package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.eval.js +27 -0
- package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.yaml +38 -0
- package/node_modules/@commandgarden/daemon/connectors/tokenmaster-client-trustedby.yaml +62 -0
- package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -0
- package/node_modules/@commandgarden/daemon/connectors/wiz-blog-security.eval.js +29 -0
- package/node_modules/@commandgarden/daemon/connectors/wiz-blog-security.yaml +44 -0
- package/node_modules/@commandgarden/daemon/dist/daemon-runner.d.ts +12 -0
- package/node_modules/@commandgarden/daemon/dist/daemon-runner.d.ts.map +1 -0
- package/node_modules/@commandgarden/daemon/dist/daemon-runner.js +65 -0
- package/node_modules/@commandgarden/daemon/dist/daemon-runner.js.map +1 -0
- package/node_modules/@commandgarden/daemon/dist/server.d.ts.map +1 -1
- package/node_modules/@commandgarden/daemon/dist/server.js +87 -13
- package/node_modules/@commandgarden/daemon/dist/server.js.map +1 -1
- package/node_modules/@commandgarden/daemon/dist/transforms.d.ts +3 -0
- package/node_modules/@commandgarden/daemon/dist/transforms.d.ts.map +1 -0
- package/node_modules/@commandgarden/daemon/dist/transforms.js +36 -0
- package/node_modules/@commandgarden/daemon/dist/transforms.js.map +1 -0
- package/node_modules/@commandgarden/daemon/package.json +4 -2
- package/node_modules/@commandgarden/shared/dist/connector.d.ts +175 -0
- package/node_modules/@commandgarden/shared/dist/connector.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/connector.js +2 -0
- package/node_modules/@commandgarden/shared/dist/connector.js.map +1 -1
- package/node_modules/@commandgarden/shared/dist/expression/evaluator.d.ts +2 -0
- package/node_modules/@commandgarden/shared/dist/expression/evaluator.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/expression/evaluator.js.map +1 -1
- package/node_modules/@commandgarden/shared/dist/expression/filters.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/expression/filters.js +5 -0
- package/node_modules/@commandgarden/shared/dist/expression/filters.js.map +1 -1
- package/node_modules/@commandgarden/shared/dist/fan-out.d.ts +28 -0
- package/node_modules/@commandgarden/shared/dist/fan-out.d.ts.map +1 -0
- package/node_modules/@commandgarden/shared/dist/fan-out.js +55 -0
- package/node_modules/@commandgarden/shared/dist/fan-out.js.map +1 -0
- package/node_modules/@commandgarden/shared/dist/index.d.ts +2 -1
- package/node_modules/@commandgarden/shared/dist/index.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/index.js +3 -1
- package/node_modules/@commandgarden/shared/dist/index.js.map +1 -1
- package/node_modules/@commandgarden/shared/dist/loader.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/loader.js +17 -0
- package/node_modules/@commandgarden/shared/dist/loader.js.map +1 -1
- package/node_modules/@commandgarden/shared/dist/pipeline.d.ts +223 -1
- package/node_modules/@commandgarden/shared/dist/pipeline.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/pipeline.js +58 -2
- package/node_modules/@commandgarden/shared/dist/pipeline.js.map +1 -1
- package/node_modules/@commandgarden/shared/dist/protocol.d.ts +1 -0
- package/node_modules/@commandgarden/shared/dist/protocol.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/protocol.js.map +1 -1
- package/package.json +1 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-B-_w54e9.css +0 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-Bb1BOK1E.js +0 -252
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git data source — fetches commits and PRs via the ado/git-commits
|
|
3
|
+
* commandGarden connector through the daemon API.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3 rewrite: replaced direct REST API calls + PAT auth with a single
|
|
6
|
+
* daemon.post('/api/run') call per repo. The connector uses the browser's
|
|
7
|
+
* MSAL session token — no PAT required.
|
|
8
|
+
*/
|
|
9
|
+
export class GitSource {
|
|
10
|
+
daemon;
|
|
11
|
+
org;
|
|
12
|
+
repos;
|
|
13
|
+
author;
|
|
14
|
+
constructor(daemon, config) {
|
|
15
|
+
this.daemon = daemon;
|
|
16
|
+
this.org = config.azureDevOps.org;
|
|
17
|
+
this.repos = config.azureDevOps.repos;
|
|
18
|
+
this.author = config.author;
|
|
19
|
+
}
|
|
20
|
+
async fetch(weekStart, weekEnd) {
|
|
21
|
+
try {
|
|
22
|
+
// Call the ado/git-commits connector once per configured repo via daemon
|
|
23
|
+
const results = await Promise.all(this.repos.map((r) => this.fetchRepo(r.project, r.repo, weekStart, weekEnd)));
|
|
24
|
+
// Aggregate daily commits across all repos
|
|
25
|
+
const dailyMap = new Map();
|
|
26
|
+
const repoActivity = [];
|
|
27
|
+
let totalPRsMerged = 0;
|
|
28
|
+
for (let i = 0; i < this.repos.length; i++) {
|
|
29
|
+
const { project, repo } = this.repos[i];
|
|
30
|
+
const rows = results[i];
|
|
31
|
+
const commitRows = rows.filter((r) => r.isPR !== 'true');
|
|
32
|
+
const prRows = rows.filter((r) => r.isPR === 'true');
|
|
33
|
+
repoActivity.push({ project, repo, commits: commitRows.length });
|
|
34
|
+
totalPRsMerged += prRows.length;
|
|
35
|
+
for (const r of commitRows) {
|
|
36
|
+
const date = r.date;
|
|
37
|
+
const entry = dailyMap.get(date) ?? { date, commits: 0, filesChanged: 0 };
|
|
38
|
+
entry.commits += 1;
|
|
39
|
+
entry.filesChanged += (r.filesAdded ?? 0) + (r.filesEdited ?? 0) + (r.filesDeleted ?? 0);
|
|
40
|
+
dailyMap.set(date, entry);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const daily = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
44
|
+
const totalCommits = daily.reduce((sum, d) => sum + d.commits, 0);
|
|
45
|
+
return {
|
|
46
|
+
totalCommits,
|
|
47
|
+
totalPRsMerged,
|
|
48
|
+
totalPRsReviewed: 0,
|
|
49
|
+
daily,
|
|
50
|
+
repos: repoActivity.filter((r) => r.commits > 0),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
console.error('[git] fetch error:', e);
|
|
55
|
+
// Graceful degradation: return empty data instead of failing
|
|
56
|
+
return { totalCommits: 0, totalPRsMerged: 0, totalPRsReviewed: 0, daily: [], repos: [] };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Fetch a single repo's commits+PRs via the daemon connector. */
|
|
60
|
+
async fetchRepo(project, repo, fromDate, toDate) {
|
|
61
|
+
try {
|
|
62
|
+
const result = await this.daemon.post('/api/run', {
|
|
63
|
+
connector: 'ado/git-commits',
|
|
64
|
+
args: { org: this.org, project, repo, fromDate, toDate, author: this.author },
|
|
65
|
+
});
|
|
66
|
+
return result.ok && result.data ? result.data : [];
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira Cloud data source — fetches tickets via the jira/my-tickets
|
|
3
|
+
* commandGarden connector through the daemon API.
|
|
4
|
+
*
|
|
5
|
+
* Uses Jira Cloud REST API (mercedes-benz.atlassian.net) with the browser's
|
|
6
|
+
* Atlassian session — no API token required. Connector returns rows with a
|
|
7
|
+
* `category` field (resolved/inProgress/blocker) and Jira issue keys (e.g. PROJ-123).
|
|
8
|
+
*/
|
|
9
|
+
import type { DaemonClient } from '@commandgarden/shared';
|
|
10
|
+
import type { JiraData } from '../journal.types.js';
|
|
11
|
+
export declare class JiraSource {
|
|
12
|
+
private readonly daemon;
|
|
13
|
+
constructor(daemon: DaemonClient);
|
|
14
|
+
fetch(weekStart: string, weekEnd: string): Promise<JiraData | null>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira Cloud data source — fetches tickets via the jira/my-tickets
|
|
3
|
+
* commandGarden connector through the daemon API.
|
|
4
|
+
*
|
|
5
|
+
* Uses Jira Cloud REST API (mercedes-benz.atlassian.net) with the browser's
|
|
6
|
+
* Atlassian session — no API token required. Connector returns rows with a
|
|
7
|
+
* `category` field (resolved/inProgress/blocker) and Jira issue keys (e.g. PROJ-123).
|
|
8
|
+
*/
|
|
9
|
+
export class JiraSource {
|
|
10
|
+
daemon;
|
|
11
|
+
constructor(daemon) {
|
|
12
|
+
this.daemon = daemon;
|
|
13
|
+
}
|
|
14
|
+
async fetch(weekStart, weekEnd) {
|
|
15
|
+
try {
|
|
16
|
+
// assignee left empty → connector uses Jira's currentUser() function
|
|
17
|
+
const result = await this.daemon.post('/api/run', {
|
|
18
|
+
connector: 'jira/my-tickets',
|
|
19
|
+
args: { fromDate: weekStart, toDate: weekEnd },
|
|
20
|
+
});
|
|
21
|
+
if (!result.ok || !result.data || result.data.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
// Map connector rows by category to the JiraData sub-arrays.
|
|
24
|
+
// Connector returns Jira issue keys directly (e.g. "PROJ-123").
|
|
25
|
+
const rows = result.data;
|
|
26
|
+
return {
|
|
27
|
+
resolved: rows
|
|
28
|
+
.filter((r) => r.category === 'resolved')
|
|
29
|
+
.map((r) => ({ key: r.key, summary: r.summary, resolvedDate: r.resolvedDate })),
|
|
30
|
+
inProgress: rows
|
|
31
|
+
.filter((r) => r.category === 'inProgress')
|
|
32
|
+
.map((r) => ({ key: r.key, summary: r.summary })),
|
|
33
|
+
blockers: rows
|
|
34
|
+
.filter((r) => r.category === 'blocker')
|
|
35
|
+
.map((r) => ({ key: r.key, summary: r.summary, staleDays: r.staleDays })),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meetings data source — DISABLED for v1.
|
|
3
|
+
*
|
|
4
|
+
* The Outlook connector is blocked by the corporate MCAS proxy
|
|
5
|
+
* (outlook.cloud.microsoft.mcas.ms) which prevents all standard auth patterns:
|
|
6
|
+
* - OWA REST API: X-OWA-CANARY cookie is HttpOnly (JS can't read it)
|
|
7
|
+
* - Graph API: no Graph-scoped MSAL token available in MCAS session storage
|
|
8
|
+
* - Response interception: eval script installs after OWA's initial data fetch
|
|
9
|
+
*
|
|
10
|
+
* Returns null immediately — the journal renders without meeting data
|
|
11
|
+
* (graceful degradation). Git + Jira provide a strong 2-source story.
|
|
12
|
+
*
|
|
13
|
+
* TODO: Revisit post-demo with MCAS-specific auth investigation.
|
|
14
|
+
*/
|
|
15
|
+
import type { DaemonClient } from '@commandgarden/shared';
|
|
16
|
+
import type { MeetingsData } from '../journal.types.js';
|
|
17
|
+
export declare class MeetingsSource {
|
|
18
|
+
private readonly _daemon;
|
|
19
|
+
constructor(_daemon: DaemonClient);
|
|
20
|
+
fetch(_weekStart: string, _weekEnd: string): Promise<MeetingsData | null>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meetings data source — DISABLED for v1.
|
|
3
|
+
*
|
|
4
|
+
* The Outlook connector is blocked by the corporate MCAS proxy
|
|
5
|
+
* (outlook.cloud.microsoft.mcas.ms) which prevents all standard auth patterns:
|
|
6
|
+
* - OWA REST API: X-OWA-CANARY cookie is HttpOnly (JS can't read it)
|
|
7
|
+
* - Graph API: no Graph-scoped MSAL token available in MCAS session storage
|
|
8
|
+
* - Response interception: eval script installs after OWA's initial data fetch
|
|
9
|
+
*
|
|
10
|
+
* Returns null immediately — the journal renders without meeting data
|
|
11
|
+
* (graceful degradation). Git + Jira provide a strong 2-source story.
|
|
12
|
+
*
|
|
13
|
+
* TODO: Revisit post-demo with MCAS-specific auth investigation.
|
|
14
|
+
*/
|
|
15
|
+
export class MeetingsSource {
|
|
16
|
+
_daemon;
|
|
17
|
+
constructor(_daemon) {
|
|
18
|
+
this._daemon = _daemon;
|
|
19
|
+
}
|
|
20
|
+
async fetch(_weekStart, _weekEnd) {
|
|
21
|
+
// Disabled for v1 — MCAS proxy blocks all standard auth patterns.
|
|
22
|
+
// Returns null so the journal renders without meeting data.
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeTracking data source — fetches hours via commandGarden daemon.
|
|
3
|
+
* Ported from dashboard/api/src/journal/sources/timetracking.source.ts.
|
|
4
|
+
*
|
|
5
|
+
* Replaces OpencliService (CLI spawn) with DaemonClient (HTTP to daemon).
|
|
6
|
+
* The daemon runs the timetracking/report connector via browser automation.
|
|
7
|
+
* Returns null gracefully when the connector isn't available (Phase 2 dependency).
|
|
8
|
+
*/
|
|
9
|
+
import type { DaemonClient } from '@commandgarden/shared';
|
|
10
|
+
import type { TimetrackingData } from '../journal.types.js';
|
|
11
|
+
import type { JournalConfig } from '../journal.config.js';
|
|
12
|
+
export declare class TimetrackingSource {
|
|
13
|
+
private readonly daemon;
|
|
14
|
+
private readonly config;
|
|
15
|
+
constructor(daemon: DaemonClient, config: JournalConfig);
|
|
16
|
+
fetch(weekStart: string, weekEnd: string): Promise<TimetrackingData | null>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeTracking data source — fetches hours via commandGarden daemon.
|
|
3
|
+
* Ported from dashboard/api/src/journal/sources/timetracking.source.ts.
|
|
4
|
+
*
|
|
5
|
+
* Replaces OpencliService (CLI spawn) with DaemonClient (HTTP to daemon).
|
|
6
|
+
* The daemon runs the timetracking/report connector via browser automation.
|
|
7
|
+
* Returns null gracefully when the connector isn't available (Phase 2 dependency).
|
|
8
|
+
*/
|
|
9
|
+
export class TimetrackingSource {
|
|
10
|
+
daemon;
|
|
11
|
+
config;
|
|
12
|
+
constructor(daemon, config) {
|
|
13
|
+
this.daemon = daemon;
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
async fetch(weekStart, weekEnd) {
|
|
17
|
+
try {
|
|
18
|
+
// Determine which month(s) the week spans
|
|
19
|
+
const startMonth = weekStart.slice(0, 7);
|
|
20
|
+
const endMonth = weekEnd.slice(0, 7);
|
|
21
|
+
const months = startMonth === endMonth ? [startMonth] : [startMonth, endMonth];
|
|
22
|
+
// Fetch each month's report via daemon
|
|
23
|
+
const allRows = [];
|
|
24
|
+
for (const month of months) {
|
|
25
|
+
const result = await this.daemon.post('/api/run', {
|
|
26
|
+
connector: 'timetracking/report',
|
|
27
|
+
args: { month },
|
|
28
|
+
});
|
|
29
|
+
if (result.ok && result.data) {
|
|
30
|
+
allRows.push(...result.data);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Filter to rows within the week range
|
|
34
|
+
const weekRows = allRows.filter((r) => {
|
|
35
|
+
if (!r.date)
|
|
36
|
+
return false;
|
|
37
|
+
return r.date >= weekStart && r.date <= weekEnd;
|
|
38
|
+
});
|
|
39
|
+
// Group by date
|
|
40
|
+
const dailyMap = new Map();
|
|
41
|
+
for (const row of weekRows) {
|
|
42
|
+
const date = row.date;
|
|
43
|
+
const entry = dailyMap.get(date) ?? { date, hours: 0, projects: [] };
|
|
44
|
+
const hrs = typeof row.hours === 'number' ? row.hours : 0;
|
|
45
|
+
entry.hours += hrs;
|
|
46
|
+
if (row.projectId) {
|
|
47
|
+
const proj = entry.projects.find((p) => p.projectId === row.projectId);
|
|
48
|
+
if (proj)
|
|
49
|
+
proj.hours += hrs;
|
|
50
|
+
else
|
|
51
|
+
entry.projects.push({ projectId: row.projectId, hours: hrs });
|
|
52
|
+
}
|
|
53
|
+
dailyMap.set(date, entry);
|
|
54
|
+
}
|
|
55
|
+
const daily = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
56
|
+
const totalHours = Math.round(daily.reduce((sum, d) => sum + d.hours, 0) * 100) / 100;
|
|
57
|
+
// Find gaps: weekdays in range with 0 hours
|
|
58
|
+
const gaps = [];
|
|
59
|
+
const cursor = new Date(weekStart);
|
|
60
|
+
const end = new Date(weekEnd);
|
|
61
|
+
while (cursor <= end) {
|
|
62
|
+
const day = cursor.getDay();
|
|
63
|
+
if (day >= 1 && day <= 5) {
|
|
64
|
+
const iso = cursor.toISOString().slice(0, 10);
|
|
65
|
+
const entry = dailyMap.get(iso);
|
|
66
|
+
if (!entry || entry.hours === 0)
|
|
67
|
+
gaps.push(iso);
|
|
68
|
+
}
|
|
69
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
totalHours,
|
|
73
|
+
targetHours: this.config.targetHours,
|
|
74
|
+
daily,
|
|
75
|
+
gaps,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Graceful degradation: connector not available yet (Phase 2 dependency)
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -2,6 +2,8 @@ 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
|
+
'tokenmaster/clients-list': '/apps/trusted-peer-expiry',
|
|
6
|
+
'tokenmaster/client-trustedby': '/apps/trusted-peer-expiry',
|
|
5
7
|
};
|
|
6
8
|
export function connectorRoutes(app, daemon) {
|
|
7
9
|
app.get('/api/connectors', async () => {
|
|
@@ -6,6 +6,9 @@ import { configRoutes } from './config.js';
|
|
|
6
6
|
import { preferencesRoutes } from './preferences.js';
|
|
7
7
|
import { goalsRoutes } from './goals.js';
|
|
8
8
|
import { timetrackingCacheRoutes } from './timetracking-cache.js';
|
|
9
|
+
import { journalRoutes } from './journal.js';
|
|
10
|
+
import { securityNewsCacheRoutes } from './security-news-cache.js';
|
|
11
|
+
import { skillsRoutes } from './skills.js';
|
|
9
12
|
export function registerRoutes(app, daemon, store) {
|
|
10
13
|
statusRoutes(app, daemon);
|
|
11
14
|
connectorRoutes(app, daemon);
|
|
@@ -15,4 +18,7 @@ export function registerRoutes(app, daemon, store) {
|
|
|
15
18
|
preferencesRoutes(app, store);
|
|
16
19
|
goalsRoutes(app, store);
|
|
17
20
|
timetrackingCacheRoutes(app, store);
|
|
21
|
+
journalRoutes(app, daemon, store);
|
|
22
|
+
securityNewsCacheRoutes(app, store);
|
|
23
|
+
skillsRoutes(app);
|
|
18
24
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal Fastify route — POST /api/journal/generate.
|
|
3
|
+
* Reads journal config from user preferences (SQLite), instantiates
|
|
4
|
+
* sources with that config, and generates the weekly report.
|
|
5
|
+
*/
|
|
6
|
+
import type { FastifyInstance } from 'fastify';
|
|
7
|
+
import type { DaemonClient } from '@commandgarden/shared';
|
|
8
|
+
import type { AppStore } from '../store.js';
|
|
9
|
+
export declare function journalRoutes(app: FastifyInstance, daemon: DaemonClient, store: AppStore): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal Fastify route — POST /api/journal/generate.
|
|
3
|
+
* Reads journal config from user preferences (SQLite), instantiates
|
|
4
|
+
* sources with that config, and generates the weekly report.
|
|
5
|
+
*/
|
|
6
|
+
import { getJournalConfig } from '../journal/journal.config.js';
|
|
7
|
+
import { GitSource } from '../journal/sources/git.source.js';
|
|
8
|
+
import { JiraSource } from '../journal/sources/jira.source.js';
|
|
9
|
+
import { TimetrackingSource } from '../journal/sources/timetracking.source.js';
|
|
10
|
+
import { MeetingsSource } from '../journal/sources/meetings.source.js';
|
|
11
|
+
import { JournalService } from '../journal/journal.service.js';
|
|
12
|
+
export function journalRoutes(app, daemon, store) {
|
|
13
|
+
app.post('/api/journal/generate', async (req, reply) => {
|
|
14
|
+
const body = req.body;
|
|
15
|
+
// Validate weekStart is present and matches YYYY-MM-DD format
|
|
16
|
+
if (!body?.weekStart || !/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/.test(body.weekStart)) {
|
|
17
|
+
return reply.code(400).send({
|
|
18
|
+
ok: false,
|
|
19
|
+
error: 'weekStart is required and must be YYYY-MM-DD format',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Build config from user preferences (configured via GUI Config page)
|
|
23
|
+
const config = getJournalConfig(store);
|
|
24
|
+
// Check that required settings are configured
|
|
25
|
+
if (!config.azureDevOps.org || config.azureDevOps.repos.length === 0) {
|
|
26
|
+
return reply.code(400).send({
|
|
27
|
+
ok: false,
|
|
28
|
+
error: 'Journal not configured. Go to Configuration → Journal Settings and set your ADO org and repos.',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const start = Date.now();
|
|
33
|
+
// Sources receive config from preferences — no env vars needed
|
|
34
|
+
const git = new GitSource(daemon, config);
|
|
35
|
+
const jira = new JiraSource(daemon);
|
|
36
|
+
const timetracking = new TimetrackingSource(daemon, config);
|
|
37
|
+
const meetings = new MeetingsSource(daemon);
|
|
38
|
+
const service = new JournalService(git, timetracking, meetings, jira);
|
|
39
|
+
const result = await service.generate({ weekStart: body.weekStart });
|
|
40
|
+
// Audit log (non-blocking — failure should not break the response)
|
|
41
|
+
const durationMs = Date.now() - start;
|
|
42
|
+
daemon.post('/api/audit/log', {
|
|
43
|
+
command: 'journal generate',
|
|
44
|
+
args: { weekStart: body.weekStart },
|
|
45
|
+
status: result.status,
|
|
46
|
+
durationMs,
|
|
47
|
+
}).catch(() => { });
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
52
|
+
console.error('[journal] generate error:', message);
|
|
53
|
+
return reply.code(500).send({ ok: false, error: message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function securityNewsCacheRoutes(app, store) {
|
|
2
|
+
app.get('/api/security-news/cache', async () => {
|
|
3
|
+
const cached = store.getCachedSecurityNews();
|
|
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/security-news/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.cacheSecurityNews(data);
|
|
15
|
+
return { ok: true };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
function findSkillsDir() {
|
|
6
|
+
const candidates = [
|
|
7
|
+
// From app/{dist,src}/server/routes/ → commandGarden/skills/
|
|
8
|
+
join(__dirname, '..', '..', '..', '..', 'skills'),
|
|
9
|
+
// From app/{dist,src}/server/routes/ → app/skills/ (bundled via prepare-bundle)
|
|
10
|
+
join(__dirname, '..', '..', '..', 'skills'),
|
|
11
|
+
];
|
|
12
|
+
for (const p of candidates) {
|
|
13
|
+
if (existsSync(p) && statSync(p).isDirectory())
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function parseFrontmatter(content) {
|
|
19
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
20
|
+
if (!match)
|
|
21
|
+
return { description: '', body: content };
|
|
22
|
+
const frontmatter = match[1];
|
|
23
|
+
const body = match[2];
|
|
24
|
+
const descMatch = frontmatter.match(/description:\s*"(.+?)"/);
|
|
25
|
+
return { description: descMatch?.[1] ?? '', body };
|
|
26
|
+
}
|
|
27
|
+
function loadSkills(dir) {
|
|
28
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
const skills = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.isDirectory())
|
|
32
|
+
continue;
|
|
33
|
+
const skillDir = join(dir, entry.name);
|
|
34
|
+
const skillMd = join(skillDir, 'SKILL.md');
|
|
35
|
+
if (!existsSync(skillMd))
|
|
36
|
+
continue;
|
|
37
|
+
const skillContent = readFileSync(skillMd, 'utf-8');
|
|
38
|
+
const { description } = parseFrontmatter(skillContent);
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const f of readdirSync(skillDir)) {
|
|
41
|
+
const fp = join(skillDir, f);
|
|
42
|
+
if (statSync(fp).isFile() && f.endsWith('.md')) {
|
|
43
|
+
files.push({ name: f, content: readFileSync(fp, 'utf-8') });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
skills.push({ name: entry.name, description, files });
|
|
47
|
+
}
|
|
48
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
}
|
|
50
|
+
export function skillsRoutes(app) {
|
|
51
|
+
app.get('/api/skills', async (_req, reply) => {
|
|
52
|
+
const dir = findSkillsDir();
|
|
53
|
+
if (!dir) {
|
|
54
|
+
reply.code(404).send({ ok: false, error: 'Skills directory not found' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const skills = loadSkills(dir);
|
|
59
|
+
return { ok: true, skills };
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
reply.code(500).send({ ok: false, error: err instanceof Error ? err.message : 'Failed to load skills' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -43,5 +43,10 @@ export declare class AppStore {
|
|
|
43
43
|
data: Record<string, unknown>[];
|
|
44
44
|
fetchedAt: string;
|
|
45
45
|
} | null;
|
|
46
|
+
cacheSecurityNews(data: Record<string, unknown>[]): void;
|
|
47
|
+
getCachedSecurityNews(): {
|
|
48
|
+
data: Record<string, unknown>[];
|
|
49
|
+
fetchedAt: string;
|
|
50
|
+
} | null;
|
|
46
51
|
close(): void;
|
|
47
52
|
}
|
|
@@ -96,6 +96,11 @@ export class AppStore {
|
|
|
96
96
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
97
97
|
data TEXT NOT NULL,
|
|
98
98
|
fetched_at TEXT NOT NULL
|
|
99
|
+
)`);
|
|
100
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS security_news_cache (
|
|
101
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
|
+
data TEXT NOT NULL,
|
|
103
|
+
fetched_at TEXT NOT NULL
|
|
99
104
|
)`);
|
|
100
105
|
}
|
|
101
106
|
getPreference(key) {
|
|
@@ -195,6 +200,20 @@ export class AppStore {
|
|
|
195
200
|
fetchedAt: rows[0].fetched_at,
|
|
196
201
|
};
|
|
197
202
|
}
|
|
203
|
+
cacheSecurityNews(data) {
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
this.db.run('INSERT OR REPLACE INTO security_news_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
|
|
206
|
+
this.persist();
|
|
207
|
+
}
|
|
208
|
+
getCachedSecurityNews() {
|
|
209
|
+
const rows = this.query('SELECT data, fetched_at FROM security_news_cache WHERE id = 1');
|
|
210
|
+
if (rows.length === 0)
|
|
211
|
+
return null;
|
|
212
|
+
return {
|
|
213
|
+
data: JSON.parse(rows[0].data),
|
|
214
|
+
fetchedAt: rows[0].fetched_at,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
198
217
|
close() {
|
|
199
218
|
this.db.close();
|
|
200
219
|
}
|
|
@@ -43,6 +43,61 @@
|
|
|
43
43
|
return record;
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
function extractHtml(selector) {
|
|
47
|
+
const el = document.querySelector(selector);
|
|
48
|
+
if (!el) throw new Error(`Element "${selector}" not found`);
|
|
49
|
+
return el.innerHTML;
|
|
50
|
+
}
|
|
51
|
+
function extractTree(rootSelector, group, leaf, pathSeparator) {
|
|
52
|
+
const root = document.querySelector(rootSelector);
|
|
53
|
+
if (!root) throw new Error(`Root "${rootSelector}" not found`);
|
|
54
|
+
function extractField(el, spec) {
|
|
55
|
+
if (spec === "textContent") return el.textContent?.trim().replace(/\s+/g, " ") ?? "";
|
|
56
|
+
if (spec === "href") return el.getAttribute("href") ?? "";
|
|
57
|
+
if (spec.startsWith("attr:")) return el.getAttribute(spec.slice(5)) ?? "";
|
|
58
|
+
return el.textContent?.trim() ?? "";
|
|
59
|
+
}
|
|
60
|
+
function walk(container, ancestors, depth) {
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const child of Array.from(container.children)) {
|
|
63
|
+
if (child.matches(leaf.match)) {
|
|
64
|
+
const row = {};
|
|
65
|
+
for (const [name, spec] of Object.entries(leaf.fields)) {
|
|
66
|
+
row[name] = extractField(child, spec);
|
|
67
|
+
}
|
|
68
|
+
const title = row.title || child.textContent?.trim() || "";
|
|
69
|
+
const section = ancestors.length > 0 ? ancestors[0] : title;
|
|
70
|
+
const pathParts = [...ancestors, title];
|
|
71
|
+
row.section = section;
|
|
72
|
+
row.path = pathParts.join(pathSeparator);
|
|
73
|
+
row.depth = depth;
|
|
74
|
+
results.push(row);
|
|
75
|
+
} else if (child.matches(group.match)) {
|
|
76
|
+
const titleEl = child.querySelector(group.title);
|
|
77
|
+
const groupTitle = titleEl?.textContent?.trim().replace(/\s+/g, " ") ?? "";
|
|
78
|
+
const childContainer = child.querySelector(group.children);
|
|
79
|
+
if (childContainer) {
|
|
80
|
+
results.push(...walk(childContainer, [...ancestors, groupTitle], depth + 1));
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
results.push(...walk(child, ancestors, depth));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
return walk(root, [], 0);
|
|
89
|
+
}
|
|
90
|
+
async function clickAll(selector, pause, maxRounds, settle) {
|
|
91
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
92
|
+
const elements = document.querySelectorAll(selector);
|
|
93
|
+
if (elements.length === 0) break;
|
|
94
|
+
for (const el of Array.from(elements)) {
|
|
95
|
+
el.click();
|
|
96
|
+
if (pause > 0) await new Promise((r) => setTimeout(r, pause));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (settle > 0) await new Promise((r) => setTimeout(r, settle));
|
|
100
|
+
}
|
|
46
101
|
async function clickElement(selector) {
|
|
47
102
|
const el = document.querySelector(selector);
|
|
48
103
|
if (!el) throw new Error(`Element "${selector}" not found`);
|
|
@@ -86,19 +141,46 @@
|
|
|
86
141
|
return void 0;
|
|
87
142
|
case "extract":
|
|
88
143
|
return extractData(p.selector, p.fields);
|
|
144
|
+
case "extract_html":
|
|
145
|
+
return extractHtml(p.selector);
|
|
146
|
+
case "extract_tree":
|
|
147
|
+
return extractTree(
|
|
148
|
+
p.root,
|
|
149
|
+
p.group,
|
|
150
|
+
p.leaf,
|
|
151
|
+
p.pathSeparator ?? " / "
|
|
152
|
+
);
|
|
89
153
|
case "click":
|
|
90
154
|
await clickElement(p.selector);
|
|
91
155
|
return void 0;
|
|
156
|
+
case "click_all":
|
|
157
|
+
await clickAll(
|
|
158
|
+
p.selector,
|
|
159
|
+
p.pause ?? 200,
|
|
160
|
+
p.maxRounds ?? 10,
|
|
161
|
+
p.settle ?? 300
|
|
162
|
+
);
|
|
163
|
+
return void 0;
|
|
92
164
|
case "type":
|
|
93
165
|
await typeIntoElement(p.selector, p.value);
|
|
94
166
|
return void 0;
|
|
95
|
-
case "fetch":
|
|
96
|
-
|
|
167
|
+
case "fetch": {
|
|
168
|
+
const result = await fetchFromPage(
|
|
97
169
|
p.url,
|
|
98
170
|
p.method,
|
|
99
171
|
p.headers,
|
|
100
172
|
p.body
|
|
101
173
|
);
|
|
174
|
+
if (p.dataPath && result != null && typeof result === "object" && !Array.isArray(result)) {
|
|
175
|
+
let nested = result;
|
|
176
|
+
for (const key of p.dataPath.split(".")) {
|
|
177
|
+
if (nested == null || typeof nested !== "object") return [];
|
|
178
|
+
nested = nested[key];
|
|
179
|
+
}
|
|
180
|
+
return Array.isArray(nested) ? nested : [];
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
102
184
|
default:
|
|
103
185
|
throw new Error(`Unknown action: ${req.action}`);
|
|
104
186
|
}
|