@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.
Files changed (91) hide show
  1. package/dist/main.js +67 -8
  2. package/node_modules/@commandgarden/app/dist/client/assets/index-GpK1-Tyq.js +255 -0
  3. package/node_modules/@commandgarden/app/dist/client/assets/index-JB_Cdl_F.css +1 -0
  4. package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
  5. package/node_modules/@commandgarden/app/dist/server/journal/journal.config.d.ts +25 -0
  6. package/node_modules/@commandgarden/app/dist/server/journal/journal.config.js +31 -0
  7. package/node_modules/@commandgarden/app/dist/server/journal/journal.service.d.ts +37 -0
  8. package/node_modules/@commandgarden/app/dist/server/journal/journal.service.js +142 -0
  9. package/node_modules/@commandgarden/app/dist/server/journal/journal.types.d.ts +91 -0
  10. package/node_modules/@commandgarden/app/dist/server/journal/journal.types.js +4 -0
  11. package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.d.ts +21 -0
  12. package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.js +72 -0
  13. package/node_modules/@commandgarden/app/dist/server/journal/sources/jira.source.d.ts +15 -0
  14. package/node_modules/@commandgarden/app/dist/server/journal/sources/jira.source.js +42 -0
  15. package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.d.ts +21 -0
  16. package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.js +25 -0
  17. package/node_modules/@commandgarden/app/dist/server/journal/sources/timetracking.source.d.ts +17 -0
  18. package/node_modules/@commandgarden/app/dist/server/journal/sources/timetracking.source.js +83 -0
  19. package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +2 -0
  20. package/node_modules/@commandgarden/app/dist/server/routes/index.js +6 -0
  21. package/node_modules/@commandgarden/app/dist/server/routes/journal.d.ts +9 -0
  22. package/node_modules/@commandgarden/app/dist/server/routes/journal.js +56 -0
  23. package/node_modules/@commandgarden/app/dist/server/routes/security-news-cache.d.ts +3 -0
  24. package/node_modules/@commandgarden/app/dist/server/routes/security-news-cache.js +17 -0
  25. package/node_modules/@commandgarden/app/dist/server/routes/skills.d.ts +2 -0
  26. package/node_modules/@commandgarden/app/dist/server/routes/skills.js +65 -0
  27. package/node_modules/@commandgarden/app/dist/server/store.d.ts +5 -0
  28. package/node_modules/@commandgarden/app/dist/server/store.js +19 -0
  29. package/node_modules/@commandgarden/chrome/dist/content-script.js +84 -2
  30. package/node_modules/@commandgarden/chrome/dist/content-script.js.map +2 -2
  31. package/node_modules/@commandgarden/chrome/dist/popup.html +74 -53
  32. package/node_modules/@commandgarden/chrome/dist/service-worker.js +73 -8
  33. package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +3 -3
  34. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +143 -0
  35. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +70 -0
  36. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +58 -0
  37. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +47 -0
  38. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -0
  39. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -0
  40. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +186 -0
  41. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +41 -0
  42. package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -0
  43. package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.eval.js +27 -0
  44. package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.yaml +38 -0
  45. package/node_modules/@commandgarden/daemon/connectors/tokenmaster-client-trustedby.yaml +62 -0
  46. package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -0
  47. package/node_modules/@commandgarden/daemon/connectors/wiz-blog-security.eval.js +29 -0
  48. package/node_modules/@commandgarden/daemon/connectors/wiz-blog-security.yaml +44 -0
  49. package/node_modules/@commandgarden/daemon/dist/daemon-runner.d.ts +12 -0
  50. package/node_modules/@commandgarden/daemon/dist/daemon-runner.d.ts.map +1 -0
  51. package/node_modules/@commandgarden/daemon/dist/daemon-runner.js +65 -0
  52. package/node_modules/@commandgarden/daemon/dist/daemon-runner.js.map +1 -0
  53. package/node_modules/@commandgarden/daemon/dist/server.d.ts.map +1 -1
  54. package/node_modules/@commandgarden/daemon/dist/server.js +87 -13
  55. package/node_modules/@commandgarden/daemon/dist/server.js.map +1 -1
  56. package/node_modules/@commandgarden/daemon/dist/transforms.d.ts +3 -0
  57. package/node_modules/@commandgarden/daemon/dist/transforms.d.ts.map +1 -0
  58. package/node_modules/@commandgarden/daemon/dist/transforms.js +36 -0
  59. package/node_modules/@commandgarden/daemon/dist/transforms.js.map +1 -0
  60. package/node_modules/@commandgarden/daemon/package.json +4 -2
  61. package/node_modules/@commandgarden/shared/dist/connector.d.ts +175 -0
  62. package/node_modules/@commandgarden/shared/dist/connector.d.ts.map +1 -1
  63. package/node_modules/@commandgarden/shared/dist/connector.js +2 -0
  64. package/node_modules/@commandgarden/shared/dist/connector.js.map +1 -1
  65. package/node_modules/@commandgarden/shared/dist/expression/evaluator.d.ts +2 -0
  66. package/node_modules/@commandgarden/shared/dist/expression/evaluator.d.ts.map +1 -1
  67. package/node_modules/@commandgarden/shared/dist/expression/evaluator.js.map +1 -1
  68. package/node_modules/@commandgarden/shared/dist/expression/filters.d.ts.map +1 -1
  69. package/node_modules/@commandgarden/shared/dist/expression/filters.js +5 -0
  70. package/node_modules/@commandgarden/shared/dist/expression/filters.js.map +1 -1
  71. package/node_modules/@commandgarden/shared/dist/fan-out.d.ts +28 -0
  72. package/node_modules/@commandgarden/shared/dist/fan-out.d.ts.map +1 -0
  73. package/node_modules/@commandgarden/shared/dist/fan-out.js +55 -0
  74. package/node_modules/@commandgarden/shared/dist/fan-out.js.map +1 -0
  75. package/node_modules/@commandgarden/shared/dist/index.d.ts +2 -1
  76. package/node_modules/@commandgarden/shared/dist/index.d.ts.map +1 -1
  77. package/node_modules/@commandgarden/shared/dist/index.js +3 -1
  78. package/node_modules/@commandgarden/shared/dist/index.js.map +1 -1
  79. package/node_modules/@commandgarden/shared/dist/loader.d.ts.map +1 -1
  80. package/node_modules/@commandgarden/shared/dist/loader.js +17 -0
  81. package/node_modules/@commandgarden/shared/dist/loader.js.map +1 -1
  82. package/node_modules/@commandgarden/shared/dist/pipeline.d.ts +223 -1
  83. package/node_modules/@commandgarden/shared/dist/pipeline.d.ts.map +1 -1
  84. package/node_modules/@commandgarden/shared/dist/pipeline.js +58 -2
  85. package/node_modules/@commandgarden/shared/dist/pipeline.js.map +1 -1
  86. package/node_modules/@commandgarden/shared/dist/protocol.d.ts +1 -0
  87. package/node_modules/@commandgarden/shared/dist/protocol.d.ts.map +1 -1
  88. package/node_modules/@commandgarden/shared/dist/protocol.js.map +1 -1
  89. package/package.json +1 -1
  90. package/node_modules/@commandgarden/app/dist/client/assets/index-B-_w54e9.css +0 -1
  91. 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,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AppStore } from '../store.js';
3
+ export declare function securityNewsCacheRoutes(app: FastifyInstance, store: AppStore): void;
@@ -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,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function skillsRoutes(app: FastifyInstance): void;
@@ -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
- return fetchFromPage(
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
  }