@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
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
12
12
|
<title>commandGarden</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-WANip_um.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CThI0UCl.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal configuration — reads from commandGarden preferences (SQLite).
|
|
3
|
+
* Users configure their settings through the GUI Config page.
|
|
4
|
+
*
|
|
5
|
+
* Preference keys:
|
|
6
|
+
* journal.author — email/name as it appears in git commits and ADO
|
|
7
|
+
* journal.ado_org — Azure DevOps organization (e.g. "daimler-mic")
|
|
8
|
+
* journal.ado_repos — comma-separated "project/repo" pairs
|
|
9
|
+
* journal.target_hours — weekly target hours (default 40)
|
|
10
|
+
*/
|
|
11
|
+
import type { AppStore } from '../store.js';
|
|
12
|
+
/** Build journal config from the user's stored preferences. */
|
|
13
|
+
export declare function getJournalConfig(store: AppStore): {
|
|
14
|
+
author: string;
|
|
15
|
+
azureDevOps: {
|
|
16
|
+
org: string;
|
|
17
|
+
repos: {
|
|
18
|
+
project: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
targetHours: number;
|
|
23
|
+
};
|
|
24
|
+
/** Type of the config object returned by getJournalConfig. */
|
|
25
|
+
export type JournalConfig = ReturnType<typeof getJournalConfig>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal configuration — reads from commandGarden preferences (SQLite).
|
|
3
|
+
* Users configure their settings through the GUI Config page.
|
|
4
|
+
*
|
|
5
|
+
* Preference keys:
|
|
6
|
+
* journal.author — email/name as it appears in git commits and ADO
|
|
7
|
+
* journal.ado_org — Azure DevOps organization (e.g. "daimler-mic")
|
|
8
|
+
* journal.ado_repos — comma-separated "project/repo" pairs
|
|
9
|
+
* journal.target_hours — weekly target hours (default 40)
|
|
10
|
+
*/
|
|
11
|
+
/** Parse comma-separated "project/repo" pairs.
|
|
12
|
+
* Example: "mic-dns/mic-dns-api,mic-dns/mic-dns-ui" */
|
|
13
|
+
function parseRepos(raw) {
|
|
14
|
+
if (!raw)
|
|
15
|
+
return [];
|
|
16
|
+
return raw.split(',').map((entry) => {
|
|
17
|
+
const [project, repo] = entry.trim().split('/');
|
|
18
|
+
return { project, repo };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Build journal config from the user's stored preferences. */
|
|
22
|
+
export function getJournalConfig(store) {
|
|
23
|
+
return {
|
|
24
|
+
author: store.getPreference('journal.author') ?? '',
|
|
25
|
+
azureDevOps: {
|
|
26
|
+
org: store.getPreference('journal.ado_org') ?? '',
|
|
27
|
+
repos: parseRepos(store.getPreference('journal.ado_repos') ?? ''),
|
|
28
|
+
},
|
|
29
|
+
targetHours: Number(store.getPreference('journal.target_hours')) || 40,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal orchestrator — fetches all sources in parallel, cross-references,
|
|
3
|
+
* and generates basic insights.
|
|
4
|
+
* Ported from dashboard/api/src/journal/journal.service.ts.
|
|
5
|
+
*
|
|
6
|
+
* Removed NestJS DI: takes source instances via constructor params.
|
|
7
|
+
* Removed AuditService dependency: audit logging deferred to route level / Phase 3.
|
|
8
|
+
*/
|
|
9
|
+
import { GitSource } from './sources/git.source.js';
|
|
10
|
+
import { TimetrackingSource } from './sources/timetracking.source.js';
|
|
11
|
+
import { MeetingsSource } from './sources/meetings.source.js';
|
|
12
|
+
import { JiraSource } from './sources/jira.source.js';
|
|
13
|
+
import type { JournalInput, JournalResponse } from './journal.types.js';
|
|
14
|
+
export declare class JournalService {
|
|
15
|
+
private readonly git;
|
|
16
|
+
private readonly timetracking;
|
|
17
|
+
private readonly meetings;
|
|
18
|
+
private readonly jira;
|
|
19
|
+
constructor(git: GitSource, timetracking: TimetrackingSource, meetings: MeetingsSource, jira: JiraSource);
|
|
20
|
+
generate(input: JournalInput): Promise<JournalResponse>;
|
|
21
|
+
/**
|
|
22
|
+
* Cross-reference data across sources to identify patterns:
|
|
23
|
+
* - forgottenDays: days with activity but 0 hours logged
|
|
24
|
+
* - heavyMeetingDays: days with 4+ hours in meetings
|
|
25
|
+
* - zeroCodingDays: weekdays with 0 commits
|
|
26
|
+
* - meetingRatio: fraction of work hours spent in meetings
|
|
27
|
+
* - codingHours: estimated non-meeting hours
|
|
28
|
+
*/
|
|
29
|
+
private crossReference;
|
|
30
|
+
/**
|
|
31
|
+
* Generate basic insights without LLM.
|
|
32
|
+
* Phase 4 replaces this with an LLM call for richer, more contextual insights.
|
|
33
|
+
*/
|
|
34
|
+
private generateBasicInsights;
|
|
35
|
+
private addDays;
|
|
36
|
+
private weekdaysInRange;
|
|
37
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal orchestrator — fetches all sources in parallel, cross-references,
|
|
3
|
+
* and generates basic insights.
|
|
4
|
+
* Ported from dashboard/api/src/journal/journal.service.ts.
|
|
5
|
+
*
|
|
6
|
+
* Removed NestJS DI: takes source instances via constructor params.
|
|
7
|
+
* Removed AuditService dependency: audit logging deferred to route level / Phase 3.
|
|
8
|
+
*/
|
|
9
|
+
export class JournalService {
|
|
10
|
+
git;
|
|
11
|
+
timetracking;
|
|
12
|
+
meetings;
|
|
13
|
+
jira;
|
|
14
|
+
constructor(git, timetracking, meetings, jira) {
|
|
15
|
+
this.git = git;
|
|
16
|
+
this.timetracking = timetracking;
|
|
17
|
+
this.meetings = meetings;
|
|
18
|
+
this.jira = jira;
|
|
19
|
+
}
|
|
20
|
+
async generate(input) {
|
|
21
|
+
const weekStart = input.weekStart;
|
|
22
|
+
const weekEnd = this.addDays(weekStart, 6); // Mon–Sun (full 7-day week)
|
|
23
|
+
const errors = [];
|
|
24
|
+
// Fetch all sources in parallel; each source handles its own errors gracefully
|
|
25
|
+
const [ttResult, meetingsResult, jiraResult, gitResult] = await Promise.allSettled([
|
|
26
|
+
this.timetracking.fetch(weekStart, weekEnd),
|
|
27
|
+
this.meetings.fetch(weekStart, weekEnd),
|
|
28
|
+
this.jira.fetch(weekStart, weekEnd),
|
|
29
|
+
this.git.fetch(weekStart, weekEnd),
|
|
30
|
+
]);
|
|
31
|
+
const tt = ttResult.status === 'fulfilled' ? ttResult.value : null;
|
|
32
|
+
if (ttResult.status === 'rejected')
|
|
33
|
+
errors.push(`TimeTracking: ${ttResult.reason}`);
|
|
34
|
+
const mtg = meetingsResult.status === 'fulfilled' ? meetingsResult.value : null;
|
|
35
|
+
if (meetingsResult.status === 'rejected')
|
|
36
|
+
errors.push(`Meetings: ${meetingsResult.reason}`);
|
|
37
|
+
const jira = jiraResult.status === 'fulfilled' ? jiraResult.value : null;
|
|
38
|
+
if (jiraResult.status === 'rejected')
|
|
39
|
+
errors.push(`Jira: ${jiraResult.reason}`);
|
|
40
|
+
const git = gitResult.status === 'fulfilled' ? gitResult.value : null;
|
|
41
|
+
if (gitResult.status === 'rejected')
|
|
42
|
+
errors.push(`Git: ${gitResult.reason}`);
|
|
43
|
+
// Cross-reference available data
|
|
44
|
+
const crossRef = this.crossReference(tt, mtg, jira, git, weekStart, weekEnd);
|
|
45
|
+
// Determine overall status
|
|
46
|
+
const hasAnyData = tt || mtg || jira || git;
|
|
47
|
+
const status = hasAnyData ? (errors.length > 0 ? 'partial' : 'success') : 'error';
|
|
48
|
+
// Rule-based insights (LLM replacement in Phase 4)
|
|
49
|
+
const insights = this.generateBasicInsights(tt, mtg, jira, git, crossRef);
|
|
50
|
+
return {
|
|
51
|
+
status,
|
|
52
|
+
week: `${weekStart} – ${weekEnd}`,
|
|
53
|
+
timetracking: tt,
|
|
54
|
+
meetings: mtg,
|
|
55
|
+
jira,
|
|
56
|
+
git,
|
|
57
|
+
crossRef,
|
|
58
|
+
insights,
|
|
59
|
+
errors,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Cross-reference data across sources to identify patterns:
|
|
64
|
+
* - forgottenDays: days with activity but 0 hours logged
|
|
65
|
+
* - heavyMeetingDays: days with 4+ hours in meetings
|
|
66
|
+
* - zeroCodingDays: weekdays with 0 commits
|
|
67
|
+
* - meetingRatio: fraction of work hours spent in meetings
|
|
68
|
+
* - codingHours: estimated non-meeting hours
|
|
69
|
+
*/
|
|
70
|
+
crossReference(tt, mtg, _jira, git, weekStart, weekEnd) {
|
|
71
|
+
if (!tt && !mtg && !git)
|
|
72
|
+
return null;
|
|
73
|
+
// Days with activity but 0 hours logged in TimeTracking
|
|
74
|
+
const forgottenDays = [];
|
|
75
|
+
if (tt) {
|
|
76
|
+
for (const gap of tt.gaps) {
|
|
77
|
+
const hasMeetings = mtg?.daily.some((d) => d.date === gap && d.count > 0);
|
|
78
|
+
const hasCommits = git?.daily.some((d) => d.date === gap && d.commits > 0);
|
|
79
|
+
if (hasMeetings || hasCommits)
|
|
80
|
+
forgottenDays.push(gap);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Heavy meeting days (4+ hours)
|
|
84
|
+
const heavyMeetingDays = (mtg?.daily ?? [])
|
|
85
|
+
.filter((d) => d.hours >= 4)
|
|
86
|
+
.map((d) => ({ date: d.date, hours: d.hours }));
|
|
87
|
+
// Days with 0 commits
|
|
88
|
+
const allWeekdays = this.weekdaysInRange(weekStart, weekEnd);
|
|
89
|
+
const zeroCodingDays = git
|
|
90
|
+
? allWeekdays.filter((date) => !git.daily.some((d) => d.date === date && d.commits > 0))
|
|
91
|
+
: [];
|
|
92
|
+
// Meeting ratio
|
|
93
|
+
const totalWorkHours = tt?.totalHours ?? 40;
|
|
94
|
+
const meetingHours = mtg?.totalHours ?? 0;
|
|
95
|
+
const meetingRatio = totalWorkHours > 0 ? Math.round((meetingHours / totalWorkHours) * 100) / 100 : 0;
|
|
96
|
+
const codingHours = Math.max(0, totalWorkHours - meetingHours);
|
|
97
|
+
return { forgottenDays, heavyMeetingDays, zeroCodingDays, meetingRatio, codingHours };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate basic insights without LLM.
|
|
101
|
+
* Phase 4 replaces this with an LLM call for richer, more contextual insights.
|
|
102
|
+
*/
|
|
103
|
+
generateBasicInsights(tt, mtg, jira, git, crossRef) {
|
|
104
|
+
const insights = [];
|
|
105
|
+
if (crossRef?.forgottenDays.length) {
|
|
106
|
+
insights.push(`You have activity on ${crossRef.forgottenDays.join(', ')} but 0 hours logged in TimeTracking — you may have forgotten to fill it.`);
|
|
107
|
+
}
|
|
108
|
+
if (crossRef?.heavyMeetingDays.length) {
|
|
109
|
+
const days = crossRef.heavyMeetingDays.map((d) => `${d.date} (${d.hours}h)`).join(', ');
|
|
110
|
+
insights.push(`Heavy meeting days: ${days}. Consider blocking focus time.`);
|
|
111
|
+
}
|
|
112
|
+
if (tt && tt.totalHours < tt.targetHours * 0.8) {
|
|
113
|
+
const gap = tt.targetHours - tt.totalHours;
|
|
114
|
+
insights.push(`You're ${gap}h below your ${tt.targetHours}h weekly target.`);
|
|
115
|
+
}
|
|
116
|
+
if (jira?.blockers.length) {
|
|
117
|
+
const items = jira.blockers.map((b) => `${b.key} (${b.staleDays}d)`).join(', ');
|
|
118
|
+
insights.push(`Stale tickets: ${items}. Consider unblocking or reassigning.`);
|
|
119
|
+
}
|
|
120
|
+
if (crossRef && crossRef.meetingRatio > 0.5 && mtg) {
|
|
121
|
+
insights.push(`Meetings consumed ${Math.round(crossRef.meetingRatio * 100)}% of your logged hours this week.`);
|
|
122
|
+
}
|
|
123
|
+
return insights;
|
|
124
|
+
}
|
|
125
|
+
addDays(dateStr, days) {
|
|
126
|
+
const d = new Date(dateStr);
|
|
127
|
+
d.setDate(d.getDate() + days);
|
|
128
|
+
return d.toISOString().slice(0, 10);
|
|
129
|
+
}
|
|
130
|
+
weekdaysInRange(start, end) {
|
|
131
|
+
const days = [];
|
|
132
|
+
const cursor = new Date(start);
|
|
133
|
+
const endDate = new Date(end);
|
|
134
|
+
while (cursor <= endDate) {
|
|
135
|
+
if (cursor.getDay() >= 1 && cursor.getDay() <= 5) {
|
|
136
|
+
days.push(cursor.toISOString().slice(0, 10));
|
|
137
|
+
}
|
|
138
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
139
|
+
}
|
|
140
|
+
return days;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Shared types for the Dev Work Journal module.
|
|
2
|
+
* Ported from dashboard/api/src/journal/journal.types.ts — identical interfaces,
|
|
3
|
+
* no NestJS dependencies. */
|
|
4
|
+
export interface JournalInput {
|
|
5
|
+
weekStart: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TtDailyEntry {
|
|
8
|
+
date: string;
|
|
9
|
+
hours: number;
|
|
10
|
+
projects: {
|
|
11
|
+
projectId: string;
|
|
12
|
+
hours: number;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export interface TimetrackingData {
|
|
16
|
+
totalHours: number;
|
|
17
|
+
targetHours: number;
|
|
18
|
+
daily: TtDailyEntry[];
|
|
19
|
+
gaps: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface MeetingEntry {
|
|
22
|
+
date: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
start: string;
|
|
25
|
+
end: string;
|
|
26
|
+
durationMin: number;
|
|
27
|
+
}
|
|
28
|
+
export interface MeetingsDaily {
|
|
29
|
+
date: string;
|
|
30
|
+
count: number;
|
|
31
|
+
hours: number;
|
|
32
|
+
}
|
|
33
|
+
export interface MeetingsData {
|
|
34
|
+
totalCount: number;
|
|
35
|
+
totalHours: number;
|
|
36
|
+
avgDurationMin: number;
|
|
37
|
+
daily: MeetingsDaily[];
|
|
38
|
+
entries: MeetingEntry[];
|
|
39
|
+
}
|
|
40
|
+
export interface JiraTicket {
|
|
41
|
+
key: string;
|
|
42
|
+
summary: string;
|
|
43
|
+
}
|
|
44
|
+
export interface JiraData {
|
|
45
|
+
resolved: (JiraTicket & {
|
|
46
|
+
resolvedDate: string;
|
|
47
|
+
})[];
|
|
48
|
+
inProgress: JiraTicket[];
|
|
49
|
+
blockers: (JiraTicket & {
|
|
50
|
+
staleDays: number;
|
|
51
|
+
})[];
|
|
52
|
+
}
|
|
53
|
+
export interface GitDaily {
|
|
54
|
+
date: string;
|
|
55
|
+
commits: number;
|
|
56
|
+
filesChanged: number;
|
|
57
|
+
}
|
|
58
|
+
export interface GitRepoActivity {
|
|
59
|
+
project: string;
|
|
60
|
+
repo: string;
|
|
61
|
+
commits: number;
|
|
62
|
+
}
|
|
63
|
+
export interface GitData {
|
|
64
|
+
totalCommits: number;
|
|
65
|
+
totalPRsMerged: number;
|
|
66
|
+
totalPRsReviewed: number;
|
|
67
|
+
daily: GitDaily[];
|
|
68
|
+
repos: GitRepoActivity[];
|
|
69
|
+
}
|
|
70
|
+
export interface CrossRefData {
|
|
71
|
+
forgottenDays: string[];
|
|
72
|
+
heavyMeetingDays: {
|
|
73
|
+
date: string;
|
|
74
|
+
hours: number;
|
|
75
|
+
}[];
|
|
76
|
+
zeroCodingDays: string[];
|
|
77
|
+
meetingRatio: number;
|
|
78
|
+
codingHours: number;
|
|
79
|
+
}
|
|
80
|
+
export type JournalStatus = 'success' | 'partial' | 'error';
|
|
81
|
+
export interface JournalResponse {
|
|
82
|
+
status: JournalStatus;
|
|
83
|
+
week: string;
|
|
84
|
+
timetracking: TimetrackingData | null;
|
|
85
|
+
meetings: MeetingsData | null;
|
|
86
|
+
jira: JiraData | null;
|
|
87
|
+
git: GitData | null;
|
|
88
|
+
crossRef: CrossRefData | null;
|
|
89
|
+
insights: string[];
|
|
90
|
+
errors: string[];
|
|
91
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
import type { DaemonClient } from '@commandgarden/shared';
|
|
10
|
+
import type { JournalConfig } from '../journal.config.js';
|
|
11
|
+
import type { GitData } from '../journal.types.js';
|
|
12
|
+
export declare class GitSource {
|
|
13
|
+
private readonly daemon;
|
|
14
|
+
private readonly org;
|
|
15
|
+
private readonly repos;
|
|
16
|
+
private readonly author;
|
|
17
|
+
constructor(daemon: DaemonClient, config: JournalConfig);
|
|
18
|
+
fetch(weekStart: string, weekEnd: string): Promise<GitData>;
|
|
19
|
+
/** Fetch a single repo's commits+PRs via the daemon connector. */
|
|
20
|
+
private fetchRepo;
|
|
21
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -6,6 +6,7 @@ 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';
|
|
9
10
|
export function registerRoutes(app, daemon, store) {
|
|
10
11
|
statusRoutes(app, daemon);
|
|
11
12
|
connectorRoutes(app, daemon);
|
|
@@ -15,4 +16,5 @@ export function registerRoutes(app, daemon, store) {
|
|
|
15
16
|
preferencesRoutes(app, store);
|
|
16
17
|
goalsRoutes(app, store);
|
|
17
18
|
timetrackingCacheRoutes(app, store);
|
|
19
|
+
journalRoutes(app, daemon, store);
|
|
18
20
|
}
|