@commandgarden/cli 1.4.1 → 2.0.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 (81) hide show
  1. package/dist/main.js +6 -12
  2. package/node_modules/@commandgarden/app/dist/client/assets/index-CThI0UCl.css +1 -0
  3. package/node_modules/@commandgarden/app/dist/client/assets/index-WANip_um.js +252 -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/index.js +2 -0
  20. package/node_modules/@commandgarden/app/dist/server/routes/journal.d.ts +9 -0
  21. package/node_modules/@commandgarden/app/dist/server/routes/journal.js +56 -0
  22. package/node_modules/@commandgarden/chrome/dist/content-script.js +84 -2
  23. package/node_modules/@commandgarden/chrome/dist/content-script.js.map +2 -2
  24. package/node_modules/@commandgarden/chrome/dist/service-worker.js +73 -8
  25. package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +3 -3
  26. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +143 -0
  27. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +70 -0
  28. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +58 -0
  29. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +47 -0
  30. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -0
  31. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -0
  32. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +186 -0
  33. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +41 -0
  34. package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -0
  35. package/node_modules/@commandgarden/daemon/connectors/tokenmaster-client-trustedby.yaml +62 -0
  36. package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -0
  37. package/node_modules/@commandgarden/daemon/connectors/wiz-blog-security.eval.js +29 -0
  38. package/node_modules/@commandgarden/daemon/connectors/wiz-blog-security.yaml +44 -0
  39. package/node_modules/@commandgarden/daemon/dist/daemon-runner.d.ts +12 -0
  40. package/node_modules/@commandgarden/daemon/dist/daemon-runner.d.ts.map +1 -0
  41. package/node_modules/@commandgarden/daemon/dist/daemon-runner.js +65 -0
  42. package/node_modules/@commandgarden/daemon/dist/daemon-runner.js.map +1 -0
  43. package/node_modules/@commandgarden/daemon/dist/server.d.ts.map +1 -1
  44. package/node_modules/@commandgarden/daemon/dist/server.js +87 -13
  45. package/node_modules/@commandgarden/daemon/dist/server.js.map +1 -1
  46. package/node_modules/@commandgarden/daemon/dist/transforms.d.ts +3 -0
  47. package/node_modules/@commandgarden/daemon/dist/transforms.d.ts.map +1 -0
  48. package/node_modules/@commandgarden/daemon/dist/transforms.js +36 -0
  49. package/node_modules/@commandgarden/daemon/dist/transforms.js.map +1 -0
  50. package/node_modules/@commandgarden/daemon/package.json +4 -2
  51. package/node_modules/@commandgarden/shared/dist/connector.d.ts +175 -0
  52. package/node_modules/@commandgarden/shared/dist/connector.d.ts.map +1 -1
  53. package/node_modules/@commandgarden/shared/dist/connector.js +2 -0
  54. package/node_modules/@commandgarden/shared/dist/connector.js.map +1 -1
  55. package/node_modules/@commandgarden/shared/dist/expression/evaluator.d.ts +2 -0
  56. package/node_modules/@commandgarden/shared/dist/expression/evaluator.d.ts.map +1 -1
  57. package/node_modules/@commandgarden/shared/dist/expression/evaluator.js.map +1 -1
  58. package/node_modules/@commandgarden/shared/dist/expression/filters.d.ts.map +1 -1
  59. package/node_modules/@commandgarden/shared/dist/expression/filters.js +5 -0
  60. package/node_modules/@commandgarden/shared/dist/expression/filters.js.map +1 -1
  61. package/node_modules/@commandgarden/shared/dist/fan-out.d.ts +28 -0
  62. package/node_modules/@commandgarden/shared/dist/fan-out.d.ts.map +1 -0
  63. package/node_modules/@commandgarden/shared/dist/fan-out.js +55 -0
  64. package/node_modules/@commandgarden/shared/dist/fan-out.js.map +1 -0
  65. package/node_modules/@commandgarden/shared/dist/index.d.ts +2 -1
  66. package/node_modules/@commandgarden/shared/dist/index.d.ts.map +1 -1
  67. package/node_modules/@commandgarden/shared/dist/index.js +3 -1
  68. package/node_modules/@commandgarden/shared/dist/index.js.map +1 -1
  69. package/node_modules/@commandgarden/shared/dist/loader.d.ts.map +1 -1
  70. package/node_modules/@commandgarden/shared/dist/loader.js +17 -0
  71. package/node_modules/@commandgarden/shared/dist/loader.js.map +1 -1
  72. package/node_modules/@commandgarden/shared/dist/pipeline.d.ts +223 -1
  73. package/node_modules/@commandgarden/shared/dist/pipeline.d.ts.map +1 -1
  74. package/node_modules/@commandgarden/shared/dist/pipeline.js +58 -2
  75. package/node_modules/@commandgarden/shared/dist/pipeline.js.map +1 -1
  76. package/node_modules/@commandgarden/shared/dist/protocol.d.ts +1 -0
  77. package/node_modules/@commandgarden/shared/dist/protocol.d.ts.map +1 -1
  78. package/node_modules/@commandgarden/shared/dist/protocol.js.map +1 -1
  79. package/package.json +1 -1
  80. package/node_modules/@commandgarden/app/dist/client/assets/index-B-_w54e9.css +0 -1
  81. package/node_modules/@commandgarden/app/dist/client/assets/index-Bb1BOK1E.js +0 -252
@@ -0,0 +1,143 @@
1
+ // Runs in page context via js_evaluate step.
2
+ // Fetches ADO Git commits (via Pushes API, all branches) and completed PRs
3
+ // using the browser's ADO session cookies — no PAT or MSAL token required.
4
+ //
5
+ // Strategy: navigate to dev.azure.com to trigger SSO, then call the ADO REST
6
+ // API with session cookies (credentials:'include'). The Pushes API captures
7
+ // work across ALL branches (unlike the Commits API which only searches the
8
+ // default branch).
9
+ //
10
+ // Template variables interpolated before execution:
11
+ // ${{ args.org }}
12
+ // ${{ args.project }}
13
+ // ${{ args.repo }}
14
+ // ${{ args.fromDate }}
15
+ // ${{ args.toDate }}
16
+ // ${{ args.author | default("") }}
17
+
18
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
19
+
20
+ // ── Read args ─────────────────────────────────────────────────────────
21
+ const org = '${{ args.org }}'.trim();
22
+ const project = '${{ args.project }}'.trim();
23
+ const repo = '${{ args.repo }}'.trim();
24
+ const fromDate = '${{ args.fromDate }}'.trim();
25
+ const toDate = '${{ args.toDate }}'.trim();
26
+ const authorFilter = '${{ args.author | default("") }}'.trim().toLowerCase();
27
+
28
+ if (!org || !project || !repo || !fromDate || !toDate) {
29
+ throw new Error('Missing required args: org, project, repo, fromDate, toDate');
30
+ }
31
+
32
+ // ── Wait for ADO to finish SSO/redirect (up to 30s) ──────────────────
33
+ // ADO uses httpOnly session cookies — no MSAL tokens in sessionStorage.
34
+ // We just need the browser to be logged in; session cookies are sent
35
+ // automatically with same-origin fetch (credentials: 'include').
36
+ const __deadline = Date.now() + 30000;
37
+ while (Date.now() < __deadline) {
38
+ if (location.hostname === 'dev.azure.com') break;
39
+ await sleep(1000);
40
+ }
41
+ if (location.hostname !== 'dev.azure.com') {
42
+ throw new Error('Not signed in to Azure DevOps — log in at dev.azure.com and retry');
43
+ }
44
+
45
+ // ADO REST API: dev.azure.com/{org}/{project}/_apis/...
46
+ const apiBase = `https://dev.azure.com/${org}/${project}/_apis`;
47
+ // Use session cookies for auth (same-origin, no Bearer token needed)
48
+ const authHeaders = { Accept: 'application/json' };
49
+
50
+ // ── Step 1: List pushes in the date range ─────────────────────────────
51
+ const listUrl =
52
+ `${apiBase}/git/repositories/${repo}/pushes` +
53
+ `?searchCriteria.fromDate=${fromDate}T00:00:00Z` +
54
+ `&searchCriteria.toDate=${toDate}T23:59:59Z` +
55
+ `&api-version=7.1`;
56
+
57
+ const listResp = await fetch(listUrl, { headers: authHeaders, credentials: 'include' });
58
+ if (!listResp.ok) throw new Error(`Pushes list HTTP ${listResp.status}: ${await listResp.text()}`);
59
+ const listBody = await listResp.json();
60
+ let pushes = listBody.value || [];
61
+
62
+ // Filter by author if provided (case-insensitive substring match on
63
+ // pushedBy.uniqueName or pushedBy.displayName)
64
+ if (authorFilter) {
65
+ pushes = pushes.filter(p => {
66
+ const un = (p.pushedBy?.uniqueName || '').toLowerCase();
67
+ const dn = (p.pushedBy?.displayName || '').toLowerCase();
68
+ return un.includes(authorFilter) || dn.includes(authorFilter);
69
+ });
70
+ }
71
+
72
+ // ── Step 2: Fetch each push's commits ─────────────────────────────────
73
+ const seen = new Set();
74
+ const commits = [];
75
+
76
+ for (const push of pushes) {
77
+ const detailUrl = `${apiBase}/git/repositories/${repo}/pushes/${push.pushId}?api-version=7.1`;
78
+ try {
79
+ const detailResp = await fetch(detailUrl, { headers: authHeaders, credentials: 'include' });
80
+ if (!detailResp.ok) continue;
81
+ const detail = await detailResp.json();
82
+ for (const c of (detail.commits || [])) {
83
+ // Deduplicate: same commit can appear in multiple pushes (e.g. after merge)
84
+ if (seen.has(c.commitId)) continue;
85
+ seen.add(c.commitId);
86
+ commits.push(c);
87
+ }
88
+ } catch (_) { /* skip failed push detail fetches */ }
89
+ }
90
+
91
+ // ── Step 3: Fetch completed PRs in the date range ─────────────────────
92
+ const prUrl =
93
+ `${apiBase}/git/repositories/${repo}/pullrequests` +
94
+ `?searchCriteria.status=completed` +
95
+ `&api-version=7.1`;
96
+
97
+ let prRows = [];
98
+ try {
99
+ const prResp = await fetch(prUrl, { headers: authHeaders, credentials: 'include' });
100
+ if (prResp.ok) {
101
+ const prBody = await prResp.json();
102
+ const prs = (prBody.value || []).filter(pr => {
103
+ if (!pr.closedDate) return false;
104
+ return pr.closedDate >= `${fromDate}T00:00:00Z` && pr.closedDate <= `${toDate}T23:59:59Z`;
105
+ });
106
+ // Optionally filter PRs by author
107
+ const filtered = authorFilter
108
+ ? prs.filter(pr => {
109
+ const un = (pr.createdBy?.uniqueName || '').toLowerCase();
110
+ return un.includes(authorFilter);
111
+ })
112
+ : prs;
113
+ prRows = filtered.map(pr => ({
114
+ commitId: '',
115
+ authorName: pr.createdBy?.displayName || '',
116
+ authorEmail: pr.createdBy?.uniqueName || '',
117
+ date: (pr.closedDate || '').slice(0, 10),
118
+ message: `PR #${pr.pullRequestId}: ${pr.title || '(no title)'}`,
119
+ filesAdded: 0,
120
+ filesEdited: 0,
121
+ filesDeleted: 0,
122
+ isPR: 'true',
123
+ prId: pr.pullRequestId,
124
+ }));
125
+ }
126
+ } catch (_) { /* PRs are supplemental — don't fail on error */ }
127
+
128
+ // ── Build output rows ─────────────────────────────────────────────────
129
+ const commitRows = commits.map(c => ({
130
+ commitId: c.commitId || '',
131
+ authorName: c.author?.name || '',
132
+ authorEmail: c.author?.email || '',
133
+ date: (c.author?.date || '').slice(0, 10),
134
+ message: (c.comment || '').split('\n')[0].slice(0, 200),
135
+ filesAdded: c.changeCounts?.Add || 0,
136
+ filesEdited: c.changeCounts?.Edit || 0,
137
+ filesDeleted: c.changeCounts?.Delete || 0,
138
+ isPR: 'false',
139
+ prId: 0,
140
+ }));
141
+
142
+ const rows = [...commitRows, ...prRows];
143
+ return rows;
@@ -0,0 +1,70 @@
1
+ site: ado
2
+ name: git-commits
3
+ version: "1.0"
4
+ description: "Azure DevOps Git commits and PRs across all branches for a date range"
5
+ access: read
6
+
7
+ domains:
8
+ - "dev.azure.com"
9
+ capabilities:
10
+ - navigate
11
+ - js_evaluate
12
+
13
+ args:
14
+ - name: org
15
+ type: string
16
+ required: true
17
+ help: "ADO organization name (e.g. 'daimler-mic')"
18
+ - name: project
19
+ type: string
20
+ required: true
21
+ help: "ADO project name (e.g. 'mic-dns')"
22
+ - name: repo
23
+ type: string
24
+ required: true
25
+ help: "Repository name (e.g. 'mic-dns-api')"
26
+ - name: fromDate
27
+ type: string
28
+ required: true
29
+ help: "Start date in YYYY-MM-DD format"
30
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
31
+ - name: toDate
32
+ type: string
33
+ required: true
34
+ help: "End date in YYYY-MM-DD format"
35
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
36
+ - name: author
37
+ type: string
38
+ required: false
39
+ help: "Filter by pusher name or email (case-insensitive substring match)"
40
+
41
+ columns:
42
+ - name: commitId
43
+ type: string
44
+ - name: authorName
45
+ type: string
46
+ - name: authorEmail
47
+ type: string
48
+ - name: date
49
+ type: string
50
+ - name: message
51
+ type: string
52
+ - name: filesAdded
53
+ type: number
54
+ - name: filesEdited
55
+ type: number
56
+ - name: filesDeleted
57
+ type: number
58
+ - name: isPR
59
+ type: string
60
+ - name: prId
61
+ type: number
62
+
63
+ pipeline:
64
+ - step: navigate
65
+ url: "https://dev.azure.com/${{ args.org }}"
66
+ - step: wait
67
+ selector: "body"
68
+ timeout: 30000
69
+ - step: js_evaluate
70
+ file: ado-git-commits.eval.js
@@ -0,0 +1,58 @@
1
+ site: gcs
2
+ name: kb-content
3
+ version: "2.0"
4
+ description: "Retrieve a single GCS Knowledge Base page as Markdown"
5
+ access: read
6
+
7
+ domains:
8
+ - "pages.i.mercedes-benz.com"
9
+ capabilities:
10
+ - navigate
11
+ - dom_read
12
+
13
+ args:
14
+ - name: path
15
+ type: string
16
+ required: true
17
+ help: "Page path from the kb-pages index, e.g. /gcs/KB/docs/general-security/edr/"
18
+ pattern: "^/gcs/KB/[a-zA-Z0-9_./-]+$"
19
+
20
+ columns:
21
+ - name: title
22
+ type: string
23
+ - name: path
24
+ type: string
25
+ - name: author
26
+ type: string
27
+ - name: lastUpdated
28
+ type: string
29
+ - name: content
30
+ type: string
31
+
32
+ pipeline:
33
+ - step: navigate
34
+ url: "https://pages.i.mercedes-benz.com${{ args.path }}"
35
+ - step: wait
36
+ selector: "article"
37
+ timeout: 30000
38
+ - step: extract
39
+ selector: "article"
40
+ fields:
41
+ title: "h1"
42
+ authorPill: "span.inline-flex"
43
+ - step: extract_html
44
+ selector: "article"
45
+ as: rawHtml
46
+ - step: transform
47
+ type: html_to_markdown
48
+ input: rawHtml
49
+ as: content
50
+ - step: transform
51
+ type: split_metadata
52
+ input: authorPill
53
+ as: meta
54
+ options:
55
+ delimiter: "|"
56
+ fields:
57
+ - author
58
+ - lastUpdated
@@ -0,0 +1,47 @@
1
+ site: gcs
2
+ name: kb-pages
3
+ version: "2.0"
4
+ description: "Page index of the GCS Knowledge Base — titles, URLs, sections, and hierarchy"
5
+ access: read
6
+
7
+ domains:
8
+ - "pages.i.mercedes-benz.com"
9
+ capabilities:
10
+ - navigate
11
+ - dom_read
12
+ - dom_write
13
+
14
+ columns:
15
+ - name: title
16
+ type: string
17
+ - name: url
18
+ type: string
19
+ - name: section
20
+ type: string
21
+ - name: path
22
+ type: string
23
+ - name: depth
24
+ type: number
25
+
26
+ pipeline:
27
+ - step: navigate
28
+ url: "https://pages.i.mercedes-benz.com/gcs/KB/docs/main/"
29
+ - step: wait
30
+ selector: "aside"
31
+ timeout: 30000
32
+ - step: click_all
33
+ selector: "aside button[aria-expanded='false']"
34
+ pause: 200
35
+ maxRounds: 10
36
+ settle: 300
37
+ - step: extract_tree
38
+ root: "aside > *:nth-child(2)"
39
+ group:
40
+ match: "div:has(> button), section:has(> button)"
41
+ title: ":scope > button"
42
+ children: ":scope > div"
43
+ leaf:
44
+ match: "a[href^='/gcs/KB/']"
45
+ fields:
46
+ title: textContent
47
+ url: href
@@ -0,0 +1,115 @@
1
+ // Runs in page context via js_evaluate step.
2
+ // Fetches Jira Cloud tickets assigned to the user using the browser's
3
+ // authenticated session (Atlassian cookies — no API token required).
4
+ //
5
+ // Uses Jira REST API v3 with JQL queries to find:
6
+ // - Resolved tickets within the date range
7
+ // - Currently in-progress tickets
8
+ // - Blockers: in-progress items not updated in 3+ days
9
+ //
10
+ // Template variables interpolated before execution:
11
+ // ${{ args.fromDate }}
12
+ // ${{ args.toDate }}
13
+ // ${{ args.assignee | default("") }}
14
+
15
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
16
+
17
+ const JIRA_BASE = 'https://mercedes-benz.atlassian.net';
18
+ const API = `${JIRA_BASE}/rest/api/3`;
19
+
20
+ const fromDate = '${{ args.fromDate }}'.trim();
21
+ const toDate = '${{ args.toDate }}'.trim();
22
+ const assigneeArg = '${{ args.assignee | default("") }}'.trim();
23
+
24
+ if (!fromDate || !toDate) {
25
+ throw new Error('Missing required args: fromDate, toDate');
26
+ }
27
+
28
+ // Use currentUser() if no assignee provided — Jira JQL function that
29
+ // resolves to the logged-in user without knowing their username.
30
+ const assignee = assigneeArg || 'currentUser()';
31
+ // JQL uses currentUser() as a function (no quotes), but a literal
32
+ // username needs quotes. Detect which format to use.
33
+ const assigneeJql = assignee === 'currentUser()' ? 'currentUser()' : `"${assignee}"`;
34
+
35
+ // ── Wait for Jira to finish SSO/redirect (up to 30s) ──────────────────
36
+ const __deadline = Date.now() + 30000;
37
+ while (Date.now() < __deadline) {
38
+ if (location.hostname === 'mercedes-benz.atlassian.net') break;
39
+ await sleep(1000);
40
+ }
41
+ if (location.hostname !== 'mercedes-benz.atlassian.net') {
42
+ throw new Error('Not signed in to Jira — log in at mercedes-benz.atlassian.net and retry');
43
+ }
44
+
45
+ // ── Helper: run a JQL search and return issues ────────────────────────
46
+ // Uses the browser's session cookies for auth (same-origin fetch).
47
+ async function jqlSearch(jql, maxResults = 100) {
48
+ // Jira Cloud deprecated /rest/api/3/search (HTTP 410) — must use /rest/api/3/search/jql
49
+ const url = `${API}/search/jql?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=summary,status,issuetype,resolutiondate,updated`;
50
+ const resp = await fetch(url, {
51
+ headers: { Accept: 'application/json' },
52
+ credentials: 'include', // send Atlassian session cookies
53
+ });
54
+ if (!resp.ok) {
55
+ const text = await resp.text().catch(() => '');
56
+ throw new Error(`Jira search failed (HTTP ${resp.status}): ${text.slice(0, 200)}`);
57
+ }
58
+ const body = await resp.json();
59
+ return body.issues || [];
60
+ }
61
+
62
+ // ── Run two JQL queries ───────────────────────────────────────────────
63
+
64
+ // Resolved: tickets resolved/closed within the date range
65
+ const resolvedJql = `assignee = ${assigneeJql} AND status changed TO ("Done", "Closed", "Resolved") DURING ("${fromDate}", "${toDate}") ORDER BY resolutiondate DESC`;
66
+
67
+ // In-progress: currently active tickets assigned to the user
68
+ const inProgressJql = `assignee = ${assigneeJql} AND status IN ("In Progress", "In Review", "To Do", "Open") ORDER BY updated ASC`;
69
+
70
+ const [resolvedIssues, inProgressIssues] = await Promise.all([
71
+ jqlSearch(resolvedJql),
72
+ jqlSearch(inProgressJql),
73
+ ]);
74
+
75
+ // ── Build output rows ─────────────────────────────────────────────────
76
+ const now = Date.now();
77
+ const rows = [];
78
+
79
+ for (const issue of resolvedIssues) {
80
+ const f = issue.fields || {};
81
+ rows.push({
82
+ key: issue.key,
83
+ summary: f.summary || '(no title)',
84
+ status: f.status?.name || '',
85
+ type: f.issuetype?.name || '',
86
+ resolvedDate: (f.resolutiondate || '').slice(0, 10),
87
+ updatedDate: (f.updated || '').slice(0, 10),
88
+ staleDays: 0,
89
+ category: 'resolved',
90
+ });
91
+ }
92
+
93
+ for (const issue of inProgressIssues) {
94
+ const f = issue.fields || {};
95
+ const updated = f.updated;
96
+ // Calculate staleDays: days since the ticket was last updated
97
+ const staleDays = updated
98
+ ? Math.floor((now - new Date(updated).getTime()) / 86400000)
99
+ : 0;
100
+ // Tickets not updated in 3+ days are flagged as blockers
101
+ const category = staleDays >= 3 ? 'blocker' : 'inProgress';
102
+
103
+ rows.push({
104
+ key: issue.key,
105
+ summary: f.summary || '(no title)',
106
+ status: f.status?.name || '',
107
+ type: f.issuetype?.name || '',
108
+ resolvedDate: '',
109
+ updatedDate: (updated || '').slice(0, 10),
110
+ staleDays,
111
+ category,
112
+ });
113
+ }
114
+
115
+ return rows;
@@ -0,0 +1,54 @@
1
+ site: jira
2
+ name: my-tickets
3
+ version: "2.0"
4
+ description: "Jira Cloud tickets assigned to the user — resolved, in-progress, and blockers"
5
+ access: read
6
+
7
+ domains:
8
+ - "mercedes-benz.atlassian.net"
9
+ capabilities:
10
+ - navigate
11
+ - js_evaluate
12
+
13
+ args:
14
+ - name: fromDate
15
+ type: string
16
+ required: true
17
+ help: "Start date in YYYY-MM-DD format"
18
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
19
+ - name: toDate
20
+ type: string
21
+ required: true
22
+ help: "End date in YYYY-MM-DD format"
23
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
24
+ - name: assignee
25
+ type: string
26
+ required: false
27
+ help: "Override assignee (Jira username or 'currentUser()'). Default: currentUser()"
28
+
29
+ columns:
30
+ - name: key
31
+ type: string
32
+ - name: summary
33
+ type: string
34
+ - name: status
35
+ type: string
36
+ - name: type
37
+ type: string
38
+ - name: resolvedDate
39
+ type: string
40
+ - name: updatedDate
41
+ type: string
42
+ - name: staleDays
43
+ type: number
44
+ - name: category
45
+ type: string
46
+
47
+ pipeline:
48
+ - step: navigate
49
+ url: "https://mercedes-benz.atlassian.net/"
50
+ - step: wait
51
+ selector: "body"
52
+ timeout: 30000
53
+ - step: js_evaluate
54
+ file: jira-my-tickets.eval.js
@@ -0,0 +1,186 @@
1
+ // Runs in page context via js_evaluate step.
2
+ // Captures the user's calendar events by intercepting OWA's own data fetches.
3
+ //
4
+ // Strategy: patch fetch() and XMLHttpRequest to capture responses containing
5
+ // calendar event data. OWA loads calendar events automatically when the
6
+ // calendar page opens — we just wait and read what it fetched.
7
+ //
8
+ // This avoids all auth issues (no tokens, no CANARY, no cross-origin) because
9
+ // we're reading data OWA already fetched for itself.
10
+ //
11
+ // Template variables interpolated before execution:
12
+ // ${{ args.date }}
13
+
14
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
15
+ function pad2(n) { return String(n).padStart(2, '0'); }
16
+
17
+ const DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
18
+ const date = '${{ args.date }}'.trim();
19
+ if (!DATE_RE.test(date)) throw new Error(`Invalid date "${date}" — use YYYY-MM-DD`);
20
+
21
+ // ── Wait for OWA to finish SSO/redirect (up to 30s) ──────────────────
22
+ const __deadline = Date.now() + 30000;
23
+ while (Date.now() < __deadline) {
24
+ if (location.hostname.includes('outlook') && !(/login\.|\/oauth2\/|signin|sso/i.test(location.href))) break;
25
+ await sleep(1000);
26
+ }
27
+ if (/login\.|\/oauth2\/|signin|sso/i.test(location.href)) {
28
+ throw new Error('Not signed in to Outlook — log in and retry');
29
+ }
30
+
31
+ // ── Install interceptors to capture calendar event data ───────────────
32
+ // OWA uses both fetch() and XMLHttpRequest for API calls. We patch both
33
+ // to capture any response containing calendar events (Items, calendarView,
34
+ // getSchedule, FindItem).
35
+ if (!window.__calEvents) {
36
+ window.__calEvents = [];
37
+
38
+ // Patch fetch
39
+ const origFetch = window.fetch;
40
+ window.fetch = function () {
41
+ const args = arguments;
42
+ return origFetch.apply(this, args).then(r => {
43
+ try {
44
+ r.clone().text().then(t => {
45
+ // Capture responses that look like calendar event data
46
+ if (t.includes('"Subject"') && (t.includes('"Start"') || t.includes('"StartTime"'))) {
47
+ try {
48
+ const parsed = JSON.parse(t);
49
+ extractEvents(parsed);
50
+ } catch (_) {}
51
+ }
52
+ // Also capture getSchedule GraphQL responses (fallback)
53
+ if (t.includes('getSchedule') && t.includes('scheduleItems')) {
54
+ try {
55
+ const parsed = JSON.parse(t);
56
+ extractScheduleItems(parsed);
57
+ } catch (_) {}
58
+ }
59
+ }).catch(() => {});
60
+ } catch (_) {}
61
+ return r;
62
+ });
63
+ };
64
+
65
+ // Patch XMLHttpRequest (OWA uses this for some API calls)
66
+ const origXHROpen = XMLHttpRequest.prototype.open;
67
+ const origXHRSend = XMLHttpRequest.prototype.send;
68
+ XMLHttpRequest.prototype.open = function () {
69
+ this.__url = arguments[1] || '';
70
+ return origXHROpen.apply(this, arguments);
71
+ };
72
+ XMLHttpRequest.prototype.send = function () {
73
+ this.addEventListener('load', function () {
74
+ try {
75
+ const t = this.responseText || '';
76
+ if (t.includes('"Subject"') && (t.includes('"Start"') || t.includes('"StartTime"'))) {
77
+ try { extractEvents(JSON.parse(t)); } catch (_) {}
78
+ }
79
+ } catch (_) {}
80
+ });
81
+ return origXHRSend.apply(this, arguments);
82
+ };
83
+ }
84
+
85
+ /** Extract calendar events from OWA API response shapes. */
86
+ function extractEvents(data) {
87
+ // OWA returns events in various shapes; try common patterns
88
+ const items = data.value || data.Items || data.items || [];
89
+ const arr = Array.isArray(data) ? data : (Array.isArray(items) ? items : []);
90
+ for (const item of arr) {
91
+ if (item && (item.Subject || item.subject) && (item.Start || item.start)) {
92
+ window.__calEvents.push(item);
93
+ }
94
+ }
95
+ }
96
+
97
+ /** Extract events from getSchedule GraphQL responses (fallback path). */
98
+ function extractScheduleItems(data) {
99
+ const nodes = Array.isArray(data) ? data : [data];
100
+ for (const node of nodes) {
101
+ const schedules = node?.data?.getSchedule?.schedules;
102
+ if (!Array.isArray(schedules)) continue;
103
+ for (const s of schedules) {
104
+ for (const it of (s.scheduleItems || [])) {
105
+ if (it && it.startTime && it.endTime) {
106
+ // Convert getSchedule format to calendarView-like format
107
+ window.__calEvents.push({
108
+ subject: it.subject || '(meeting)',
109
+ start: it.startTime,
110
+ end: it.endTime,
111
+ showAs: it.status || 'Busy',
112
+ responseStatus: { response: 'Accepted' },
113
+ isAllDayEvent: false,
114
+ isCancelled: false,
115
+ _fromSchedule: true,
116
+ });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // ── Wait for OWA to load calendar data (up to 20s) ───────────────────
124
+ for (let i = 0; i < 20; i++) {
125
+ await sleep(1000);
126
+ if (window.__calEvents.length > 0) break;
127
+ }
128
+
129
+ // Deduplicate events by start+end time
130
+ const seen = new Set();
131
+ const uniqueEvents = [];
132
+ for (const ev of window.__calEvents) {
133
+ const startStr = ev.Start?.DateTime || ev.start?.dateTime || ev.start?.DateTime || '';
134
+ const endStr = ev.End?.DateTime || ev.end?.dateTime || ev.end?.DateTime || '';
135
+ const key = startStr + '|' + endStr;
136
+ if (seen.has(key)) continue;
137
+ seen.add(key);
138
+ uniqueEvents.push(ev);
139
+ }
140
+
141
+ // Clear captured events for next run
142
+ window.__calEvents = [];
143
+
144
+ // ── Filter to the target date and map to output rows ──────────────────
145
+ const rows = [];
146
+ for (const ev of uniqueEvents) {
147
+ // Normalize field names (OWA mixes PascalCase and camelCase)
148
+ const subject = ev.Subject || ev.subject || '(meeting)';
149
+ const startRaw = ev.Start?.DateTime || ev.start?.dateTime || ev.start?.DateTime || '';
150
+ const endRaw = ev.End?.DateTime || ev.end?.dateTime || ev.end?.DateTime || '';
151
+ const showAs = (ev.ShowAs || ev.showAs || 'busy');
152
+ const isCancelled = ev.IsCancelled || ev.isCancelled || false;
153
+ const isAllDay = ev.IsAllDayEvent || ev.isAllDayEvent || false;
154
+ const responseType = ev.ResponseStatus?.Response || ev.responseStatus?.response || '';
155
+
156
+ if (isCancelled || isAllDay) continue;
157
+
158
+ // Filter by response status if available (accepted/organizer only)
159
+ // If responseStatus is missing (e.g. from getSchedule), include the event
160
+ if (responseType && responseType !== 'Organizer' && responseType !== 'Accepted') continue;
161
+
162
+ // Only show busy/tentative items (skip free blocks from getSchedule)
163
+ const showAsLower = String(showAs).toLowerCase();
164
+ if (showAsLower === 'free') continue;
165
+
166
+ const startDt = new Date(startRaw.endsWith('Z') ? startRaw : startRaw + 'Z');
167
+ const endDt = new Date(endRaw.endsWith('Z') ? endRaw : endRaw + 'Z');
168
+ const evDate = `${startDt.getFullYear()}-${pad2(startDt.getMonth() + 1)}-${pad2(startDt.getDate())}`;
169
+
170
+ // Filter to the target date
171
+ if (evDate !== date) continue;
172
+
173
+ const durationMin = Math.round((endDt.getTime() - startDt.getTime()) / 60000);
174
+ if (durationMin < 5 || durationMin > 480) continue;
175
+
176
+ rows.push({
177
+ date: evDate,
178
+ subject: subject,
179
+ start: `${pad2(startDt.getHours())}:${pad2(startDt.getMinutes())}`,
180
+ end: `${pad2(endDt.getHours())}:${pad2(endDt.getMinutes())}`,
181
+ durationMin,
182
+ state: showAsLower,
183
+ });
184
+ }
185
+
186
+ return rows;