@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.
- package/dist/main.js +6 -12
- package/node_modules/@commandgarden/app/dist/client/assets/index-CThI0UCl.css +1 -0
- package/node_modules/@commandgarden/app/dist/client/assets/index-WANip_um.js +252 -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/index.js +2 -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/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/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/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,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;
|