@commandgarden/cli 2.10.0 → 2.11.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 -6
- package/node_modules/@commandgarden/app/dist/client/assets/{Slides-D3xcxd9t.js → Slides-BvMlkL-3.js} +5 -15
- package/node_modules/@commandgarden/app/dist/client/assets/index-BD8reQ8T.js +496 -0
- package/node_modules/@commandgarden/app/dist/client/assets/{index-DbqYLIqy.css → index-wgb9-PIF.css} +1 -1
- package/node_modules/@commandgarden/app/dist/client/index.html +19 -19
- package/node_modules/@commandgarden/app/dist/server/daemon-client.test.js +47 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.service.d.ts +1 -0
- package/node_modules/@commandgarden/app/dist/server/journal/journal.service.js +25 -6
- package/node_modules/@commandgarden/app/dist/server/journal/journal.types.d.ts +26 -0
- package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.d.ts +10 -4
- package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.js +36 -29
- package/node_modules/@commandgarden/app/dist/server/journal/sources/timetracking.source.d.ts +10 -1
- package/node_modules/@commandgarden/app/dist/server/journal/sources/timetracking.source.js +62 -8
- package/node_modules/@commandgarden/app/dist/server/routes/journal.d.ts +12 -0
- package/node_modules/@commandgarden/app/dist/server/routes/journal.js +87 -5
- package/node_modules/@commandgarden/app/dist/server/routes/routes.test.js +137 -0
- package/node_modules/@commandgarden/app/dist/server/store.d.ts +27 -0
- package/node_modules/@commandgarden/app/dist/server/store.js +130 -39
- package/node_modules/@commandgarden/app/dist/server/store.test.js +59 -0
- package/node_modules/@commandgarden/app/package.json +42 -42
- package/node_modules/@commandgarden/chrome/dist/content-script.js.map +1 -1
- package/node_modules/@commandgarden/chrome/dist/fetch-interceptor.js +44 -0
- package/node_modules/@commandgarden/chrome/dist/manifest.json +26 -26
- package/node_modules/@commandgarden/chrome/dist/popup.html +218 -218
- package/node_modules/@commandgarden/chrome/dist/popup.js.map +1 -1
- package/node_modules/@commandgarden/chrome/dist/service-worker.js +11 -3
- package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
- package/node_modules/@commandgarden/chrome/package.json +26 -26
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +100 -100
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +70 -70
- package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +61 -61
- package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +54 -54
- package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -115
- package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -54
- package/node_modules/@commandgarden/daemon/connectors/lib/msal-token.js +20 -20
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +204 -198
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +42 -42
- package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.eval.js +94 -94
- package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.yaml +34 -34
- package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -63
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +437 -437
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +53 -53
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.eval.js +547 -547
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.yaml +48 -48
- package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.eval.js +52 -52
- package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.yaml +42 -42
- package/node_modules/@commandgarden/daemon/connectors/timetracking-report.eval.js +81 -81
- package/node_modules/@commandgarden/daemon/connectors/timetracking-report.yaml +52 -52
- package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.eval.js +27 -27
- package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.yaml +38 -38
- package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -55
- package/node_modules/@commandgarden/daemon/dist/audit-store.js +8 -8
- package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts +24 -0
- package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts.map +1 -0
- package/node_modules/@commandgarden/daemon/dist/cdp-client.js +150 -0
- package/node_modules/@commandgarden/daemon/dist/cdp-client.js.map +1 -0
- package/node_modules/@commandgarden/daemon/package.json +32 -32
- package/node_modules/@commandgarden/shared/package.json +32 -32
- package/package.json +58 -58
- package/node_modules/@commandgarden/app/dist/client/assets/index-_jNYPoLF.js +0 -461
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import { registerRoutes } from './index.js';
|
|
4
|
+
import { AppStore } from '../store.js';
|
|
5
|
+
function mockDaemon() {
|
|
6
|
+
return {
|
|
7
|
+
get: vi.fn(),
|
|
8
|
+
post: vi.fn(),
|
|
9
|
+
pipeRaw: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe('routes', () => {
|
|
13
|
+
let app;
|
|
14
|
+
let daemon;
|
|
15
|
+
let store;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
app = Fastify();
|
|
18
|
+
daemon = mockDaemon();
|
|
19
|
+
store = new AppStore(':memory:');
|
|
20
|
+
registerRoutes(app, daemon, store);
|
|
21
|
+
await app.ready();
|
|
22
|
+
});
|
|
23
|
+
describe('GET /api/status', () => {
|
|
24
|
+
it('proxies daemon status', async () => {
|
|
25
|
+
daemon.get.mockResolvedValueOnce({
|
|
26
|
+
ok: true, extensionConnected: true, connectorCount: 2,
|
|
27
|
+
});
|
|
28
|
+
const resp = await app.inject({ method: 'GET', url: '/api/status' });
|
|
29
|
+
expect(resp.statusCode).toBe(200);
|
|
30
|
+
const body = JSON.parse(resp.payload);
|
|
31
|
+
expect(body.ok).toBe(true);
|
|
32
|
+
expect(body.extensionConnected).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('GET /api/connectors', () => {
|
|
36
|
+
it('proxies and enriches connector list', async () => {
|
|
37
|
+
daemon.get
|
|
38
|
+
.mockImplementation((path) => {
|
|
39
|
+
if (path === '/api/connectors') {
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
ok: true,
|
|
42
|
+
connectors: [
|
|
43
|
+
{ key: 'timetracking/report', description: 'test', access: 'read', domains: [], capabilities: [] },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (path === '/api/config') {
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
ok: true,
|
|
50
|
+
config: { security: { highRiskCapabilities: ['js_evaluate', 'cookie_write'], approvedHighRisk: [], autoApproveConnectors: [] } },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve({ ok: true });
|
|
54
|
+
});
|
|
55
|
+
const resp = await app.inject({ method: 'GET', url: '/api/connectors' });
|
|
56
|
+
expect(resp.statusCode).toBe(200);
|
|
57
|
+
const body = JSON.parse(resp.payload);
|
|
58
|
+
expect(body.connectors).toHaveLength(1);
|
|
59
|
+
expect(body.connectors[0].hasAppPage).toBe(true);
|
|
60
|
+
expect(body.connectors[0].appRoute).toBe('/apps/timetracking');
|
|
61
|
+
});
|
|
62
|
+
it('enriches connectors with security flags from config', async () => {
|
|
63
|
+
daemon.get
|
|
64
|
+
.mockImplementation((path) => {
|
|
65
|
+
if (path === '/api/connectors') {
|
|
66
|
+
return Promise.resolve({
|
|
67
|
+
ok: true,
|
|
68
|
+
connectors: [
|
|
69
|
+
{ key: 'timetracking/report', description: 'test', access: 'read', domains: [], capabilities: ['navigate', 'js_evaluate'] },
|
|
70
|
+
{ key: 'safe/connector', description: 'safe', access: 'read', domains: [], capabilities: ['navigate'] },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (path === '/api/config') {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
ok: true,
|
|
77
|
+
config: {
|
|
78
|
+
security: {
|
|
79
|
+
highRiskCapabilities: ['js_evaluate', 'cookie_write'],
|
|
80
|
+
approvedHighRisk: ['timetracking/report'],
|
|
81
|
+
autoApproveConnectors: ['timetracking/report'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve({ ok: true });
|
|
87
|
+
});
|
|
88
|
+
const resp = await app.inject({ method: 'GET', url: '/api/connectors' });
|
|
89
|
+
const body = JSON.parse(resp.payload);
|
|
90
|
+
expect(body.connectors[0].isHighRisk).toBe(true);
|
|
91
|
+
expect(body.connectors[0].isApproved).toBe(true);
|
|
92
|
+
expect(body.connectors[0].isAutoApproved).toBe(true);
|
|
93
|
+
expect(body.connectors[1].isHighRisk).toBe(false);
|
|
94
|
+
expect(body.connectors[1].isApproved).toBe(false);
|
|
95
|
+
expect(body.connectors[1].isAutoApproved).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('GET /api/preferences', () => {
|
|
99
|
+
it('returns empty preferences', async () => {
|
|
100
|
+
const resp = await app.inject({ method: 'GET', url: '/api/preferences' });
|
|
101
|
+
expect(resp.statusCode).toBe(200);
|
|
102
|
+
expect(JSON.parse(resp.payload)).toEqual({ ok: true, preferences: {} });
|
|
103
|
+
});
|
|
104
|
+
it('sets and gets a preference via PUT', async () => {
|
|
105
|
+
await app.inject({
|
|
106
|
+
method: 'PUT', url: '/api/preferences',
|
|
107
|
+
payload: { key: 'theme', value: 'dark' },
|
|
108
|
+
});
|
|
109
|
+
const resp = await app.inject({ method: 'GET', url: '/api/preferences' });
|
|
110
|
+
expect(JSON.parse(resp.payload).preferences).toEqual({ theme: 'dark' });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('POST /api/run', () => {
|
|
114
|
+
it('proxies run to daemon', async () => {
|
|
115
|
+
daemon.post.mockResolvedValueOnce({
|
|
116
|
+
ok: true, data: [{ a: 1 }], rowCount: 1,
|
|
117
|
+
});
|
|
118
|
+
const resp = await app.inject({
|
|
119
|
+
method: 'POST', url: '/api/run',
|
|
120
|
+
payload: { connector: 'test/cmd', args: {} },
|
|
121
|
+
});
|
|
122
|
+
expect(resp.statusCode).toBe(200);
|
|
123
|
+
expect(JSON.parse(resp.payload).ok).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('returns 502 with error message when daemon throws', async () => {
|
|
126
|
+
daemon.post.mockRejectedValueOnce(new Error('Extension not connected'));
|
|
127
|
+
const resp = await app.inject({
|
|
128
|
+
method: 'POST', url: '/api/run',
|
|
129
|
+
payload: { connector: 'test/cmd', args: {} },
|
|
130
|
+
});
|
|
131
|
+
expect(resp.statusCode).toBe(502);
|
|
132
|
+
const body = JSON.parse(resp.payload);
|
|
133
|
+
expect(body.ok).toBe(false);
|
|
134
|
+
expect(body.error).toBe('Extension not connected');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -58,5 +58,32 @@ export declare class AppStore {
|
|
|
58
58
|
data: Record<string, unknown>[];
|
|
59
59
|
fetchedAt: string;
|
|
60
60
|
} | null;
|
|
61
|
+
cacheJournal(weekStart: string, data: Record<string, unknown>): string;
|
|
62
|
+
getCachedJournal(weekStart: string): {
|
|
63
|
+
data: Record<string, unknown>;
|
|
64
|
+
fetchedAt: string;
|
|
65
|
+
sabaData: Record<string, unknown> | null;
|
|
66
|
+
} | null;
|
|
67
|
+
/** Persists the Saba "pending training" connector result for a week,
|
|
68
|
+
* independently of the main journal data — it's fetched client-side and
|
|
69
|
+
* can complete at a different time. No-ops if the week has no cached
|
|
70
|
+
* journal row yet (generate() always caches the main result first). */
|
|
71
|
+
cacheJournalSaba(weekStart: string, sabaData: Record<string, unknown> | null): void;
|
|
72
|
+
/** The last-selected source toggles, so the UI restores the user's picks
|
|
73
|
+
* on next load instead of always resetting to "all enabled". */
|
|
74
|
+
getJournalSourcePrefs(): {
|
|
75
|
+
timetracking: boolean;
|
|
76
|
+
meetings: boolean;
|
|
77
|
+
jira: boolean;
|
|
78
|
+
git: boolean;
|
|
79
|
+
saba: boolean;
|
|
80
|
+
} | null;
|
|
81
|
+
saveJournalSourcePrefs(sources: {
|
|
82
|
+
timetracking: boolean;
|
|
83
|
+
meetings: boolean;
|
|
84
|
+
jira: boolean;
|
|
85
|
+
git: boolean;
|
|
86
|
+
saba: boolean;
|
|
87
|
+
}): void;
|
|
61
88
|
close(): void;
|
|
62
89
|
}
|
|
@@ -49,16 +49,16 @@ export class AppStore {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
migrate() {
|
|
52
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS preferences (
|
|
53
|
-
key TEXT PRIMARY KEY,
|
|
54
|
-
value TEXT NOT NULL
|
|
52
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS preferences (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value TEXT NOT NULL
|
|
55
55
|
)`);
|
|
56
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS saved_views (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
app TEXT NOT NULL,
|
|
59
|
-
name TEXT NOT NULL,
|
|
60
|
-
config TEXT NOT NULL,
|
|
61
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
56
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS saved_views (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
app TEXT NOT NULL,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
config TEXT NOT NULL,
|
|
61
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
62
62
|
)`);
|
|
63
63
|
// Drop legacy goals table (had category column) and recreate with activity
|
|
64
64
|
try {
|
|
@@ -76,41 +76,75 @@ export class AppStore {
|
|
|
76
76
|
}
|
|
77
77
|
catch { /* ignore */ }
|
|
78
78
|
}
|
|
79
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS goals (
|
|
80
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
-
month TEXT NOT NULL,
|
|
82
|
-
project_id TEXT NOT NULL,
|
|
83
|
-
activity TEXT NOT NULL DEFAULT '',
|
|
84
|
-
target_days REAL NOT NULL,
|
|
85
|
-
target_hours REAL NOT NULL,
|
|
86
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
88
|
-
UNIQUE(month, project_id, activity)
|
|
79
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS goals (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
month TEXT NOT NULL,
|
|
82
|
+
project_id TEXT NOT NULL,
|
|
83
|
+
activity TEXT NOT NULL DEFAULT '',
|
|
84
|
+
target_days REAL NOT NULL,
|
|
85
|
+
target_hours REAL NOT NULL,
|
|
86
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
88
|
+
UNIQUE(month, project_id, activity)
|
|
89
89
|
)`);
|
|
90
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS report_cache (
|
|
91
|
-
month TEXT PRIMARY KEY,
|
|
92
|
-
data TEXT NOT NULL,
|
|
93
|
-
fetched_at TEXT NOT NULL
|
|
90
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS report_cache (
|
|
91
|
+
month TEXT PRIMARY KEY,
|
|
92
|
+
data TEXT NOT NULL,
|
|
93
|
+
fetched_at TEXT NOT NULL
|
|
94
94
|
)`);
|
|
95
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS projects_cache (
|
|
96
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
97
|
-
data TEXT NOT NULL,
|
|
98
|
-
fetched_at TEXT NOT NULL
|
|
95
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS projects_cache (
|
|
96
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
97
|
+
data TEXT NOT NULL,
|
|
98
|
+
fetched_at TEXT NOT NULL
|
|
99
99
|
)`);
|
|
100
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS security_news_cache (
|
|
101
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
|
-
data TEXT NOT NULL,
|
|
103
|
-
fetched_at TEXT NOT NULL
|
|
100
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS security_news_cache (
|
|
101
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
|
+
data TEXT NOT NULL,
|
|
103
|
+
fetched_at TEXT NOT NULL
|
|
104
104
|
)`);
|
|
105
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
|
|
106
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
|
-
data TEXT NOT NULL,
|
|
108
|
-
fetched_at TEXT NOT NULL
|
|
105
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
|
|
106
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
|
+
data TEXT NOT NULL,
|
|
108
|
+
fetched_at TEXT NOT NULL
|
|
109
109
|
)`);
|
|
110
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS room_availability_cache (
|
|
111
|
-
date TEXT PRIMARY KEY,
|
|
112
|
-
data TEXT NOT NULL,
|
|
113
|
-
fetched_at TEXT NOT NULL
|
|
110
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS room_availability_cache (
|
|
111
|
+
date TEXT PRIMARY KEY,
|
|
112
|
+
data TEXT NOT NULL,
|
|
113
|
+
fetched_at TEXT NOT NULL
|
|
114
|
+
)`);
|
|
115
|
+
// Legacy schema keyed by cache_key (weekStart + sources signature) —
|
|
116
|
+
// migrate to a plain weekStart-keyed table (one cached result per week,
|
|
117
|
+
// regardless of which sources were selected when it was generated).
|
|
118
|
+
try {
|
|
119
|
+
this.db.run(`SELECT cache_key FROM journal_cache LIMIT 0`);
|
|
120
|
+
// Old schema present — drop it, cached data will simply regenerate.
|
|
121
|
+
this.db.run(`DROP TABLE journal_cache`);
|
|
122
|
+
}
|
|
123
|
+
catch { /* already on new schema, or table doesn't exist yet */ }
|
|
124
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS journal_cache (
|
|
125
|
+
week_start TEXT PRIMARY KEY,
|
|
126
|
+
data TEXT NOT NULL,
|
|
127
|
+
fetched_at TEXT NOT NULL
|
|
128
|
+
)`);
|
|
129
|
+
// saba_data holds the last Saba "pending training" connector result for
|
|
130
|
+
// the week, saved separately from `data` since it's fetched client-side
|
|
131
|
+
// (not part of JournalService.generate()) and can complete at a
|
|
132
|
+
// different time than the main journal fetch.
|
|
133
|
+
try {
|
|
134
|
+
this.db.run(`ALTER TABLE journal_cache ADD COLUMN saba_data TEXT`);
|
|
135
|
+
}
|
|
136
|
+
catch { /* column already exists */ }
|
|
137
|
+
// Remembers the last-selected source toggles (Timetracking/Meetings/Jira/
|
|
138
|
+
// Git/Saba) so the UI can restore the user's picks on next load, instead
|
|
139
|
+
// of always resetting to "all enabled".
|
|
140
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS journal_source_prefs (
|
|
141
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
142
|
+
timetracking INTEGER NOT NULL DEFAULT 1,
|
|
143
|
+
meetings INTEGER NOT NULL DEFAULT 1,
|
|
144
|
+
jira INTEGER NOT NULL DEFAULT 1,
|
|
145
|
+
git INTEGER NOT NULL DEFAULT 1,
|
|
146
|
+
saba INTEGER NOT NULL DEFAULT 1,
|
|
147
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
114
148
|
)`);
|
|
115
149
|
}
|
|
116
150
|
getPreference(key) {
|
|
@@ -252,6 +286,63 @@ export class AppStore {
|
|
|
252
286
|
fetchedAt: rows[0].fetched_at,
|
|
253
287
|
};
|
|
254
288
|
}
|
|
289
|
+
cacheJournal(weekStart, data) {
|
|
290
|
+
const now = new Date().toISOString();
|
|
291
|
+
// ON CONFLICT (rather than INSERT OR REPLACE) so an existing saba_data
|
|
292
|
+
// value for this week isn't wiped out when the main journal result is
|
|
293
|
+
// (re)generated — the two are cached independently.
|
|
294
|
+
this.db.run(`INSERT INTO journal_cache (week_start, data, fetched_at) VALUES (?, ?, ?)
|
|
295
|
+
ON CONFLICT(week_start) DO UPDATE SET data = excluded.data, fetched_at = excluded.fetched_at`, [weekStart, JSON.stringify(data), now]);
|
|
296
|
+
this.persist();
|
|
297
|
+
return now;
|
|
298
|
+
}
|
|
299
|
+
getCachedJournal(weekStart) {
|
|
300
|
+
const rows = this.query('SELECT data, fetched_at, saba_data FROM journal_cache WHERE week_start = ?', [weekStart]);
|
|
301
|
+
if (rows.length === 0)
|
|
302
|
+
return null;
|
|
303
|
+
const sabaRaw = rows[0].saba_data;
|
|
304
|
+
return {
|
|
305
|
+
data: JSON.parse(rows[0].data),
|
|
306
|
+
fetchedAt: rows[0].fetched_at,
|
|
307
|
+
sabaData: sabaRaw ? JSON.parse(sabaRaw) : null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/** Persists the Saba "pending training" connector result for a week,
|
|
311
|
+
* independently of the main journal data — it's fetched client-side and
|
|
312
|
+
* can complete at a different time. No-ops if the week has no cached
|
|
313
|
+
* journal row yet (generate() always caches the main result first). */
|
|
314
|
+
cacheJournalSaba(weekStart, sabaData) {
|
|
315
|
+
this.db.run('UPDATE journal_cache SET saba_data = ? WHERE week_start = ?', [sabaData ? JSON.stringify(sabaData) : null, weekStart]);
|
|
316
|
+
this.persist();
|
|
317
|
+
}
|
|
318
|
+
/** The last-selected source toggles, so the UI restores the user's picks
|
|
319
|
+
* on next load instead of always resetting to "all enabled". */
|
|
320
|
+
getJournalSourcePrefs() {
|
|
321
|
+
const rows = this.query('SELECT timetracking, meetings, jira, git, saba FROM journal_source_prefs WHERE id = 1');
|
|
322
|
+
if (rows.length === 0)
|
|
323
|
+
return null;
|
|
324
|
+
const r = rows[0];
|
|
325
|
+
return {
|
|
326
|
+
timetracking: Boolean(r.timetracking),
|
|
327
|
+
meetings: Boolean(r.meetings),
|
|
328
|
+
jira: Boolean(r.jira),
|
|
329
|
+
git: Boolean(r.git),
|
|
330
|
+
saba: Boolean(r.saba),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
saveJournalSourcePrefs(sources) {
|
|
334
|
+
const now = new Date().toISOString();
|
|
335
|
+
this.db.run(`INSERT OR REPLACE INTO journal_source_prefs (id, timetracking, meetings, jira, git, saba, updated_at)
|
|
336
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)`, [
|
|
337
|
+
sources.timetracking ? 1 : 0,
|
|
338
|
+
sources.meetings ? 1 : 0,
|
|
339
|
+
sources.jira ? 1 : 0,
|
|
340
|
+
sources.git ? 1 : 0,
|
|
341
|
+
sources.saba ? 1 : 0,
|
|
342
|
+
now,
|
|
343
|
+
]);
|
|
344
|
+
this.persist();
|
|
345
|
+
}
|
|
255
346
|
close() {
|
|
256
347
|
this.db.close();
|
|
257
348
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { AppStore } from './store.js';
|
|
3
|
+
describe('AppStore', () => {
|
|
4
|
+
let store;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
store = new AppStore(':memory:');
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
store.close();
|
|
10
|
+
});
|
|
11
|
+
describe('preferences', () => {
|
|
12
|
+
it('returns undefined for missing key', () => {
|
|
13
|
+
expect(store.getPreference('missing')).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
it('sets and gets a preference', () => {
|
|
16
|
+
store.setPreference('theme', 'light');
|
|
17
|
+
expect(store.getPreference('theme')).toBe('light');
|
|
18
|
+
});
|
|
19
|
+
it('overwrites existing preference', () => {
|
|
20
|
+
store.setPreference('theme', 'light');
|
|
21
|
+
store.setPreference('theme', 'dark');
|
|
22
|
+
expect(store.getPreference('theme')).toBe('dark');
|
|
23
|
+
});
|
|
24
|
+
it('returns all preferences', () => {
|
|
25
|
+
store.setPreference('a', '1');
|
|
26
|
+
store.setPreference('b', '2');
|
|
27
|
+
expect(store.getAllPreferences()).toEqual({ a: '1', b: '2' });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('saved views', () => {
|
|
31
|
+
it('returns empty array when no views', () => {
|
|
32
|
+
expect(store.listViews('timetracking')).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
it('creates and lists a view', () => {
|
|
35
|
+
const view = store.createView('timetracking', 'June report', '{"month":"2026-06"}');
|
|
36
|
+
expect(view.id).toBeDefined();
|
|
37
|
+
expect(view.app).toBe('timetracking');
|
|
38
|
+
expect(view.name).toBe('June report');
|
|
39
|
+
expect(view.config).toBe('{"month":"2026-06"}');
|
|
40
|
+
const views = store.listViews('timetracking');
|
|
41
|
+
expect(views).toHaveLength(1);
|
|
42
|
+
expect(views[0].name).toBe('June report');
|
|
43
|
+
});
|
|
44
|
+
it('filters views by app', () => {
|
|
45
|
+
store.createView('timetracking', 'View A', '{}');
|
|
46
|
+
store.createView('rooms', 'View B', '{}');
|
|
47
|
+
expect(store.listViews('timetracking')).toHaveLength(1);
|
|
48
|
+
expect(store.listViews('rooms')).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
it('deletes a view', () => {
|
|
51
|
+
const view = store.createView('timetracking', 'To delete', '{}');
|
|
52
|
+
expect(store.deleteView(view.id)).toBe(true);
|
|
53
|
+
expect(store.listViews('timetracking')).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
it('returns false when deleting non-existent view', () => {
|
|
56
|
+
expect(store.deleteView('nonexistent')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@commandgarden/app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"files": [
|
|
7
|
-
"dist"
|
|
8
|
-
],
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "vite build && tsc -p tsconfig.server.json",
|
|
11
|
-
"dev": "concurrently \"vite\" \"tsx watch src/server/main.ts\"",
|
|
12
|
-
"start": "node dist/server/main.js",
|
|
13
|
-
"test": "vitest run",
|
|
14
|
-
"test:watch": "vitest"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@commandgarden/shared": "*",
|
|
18
|
-
"@fastify/static": "^8.0.0",
|
|
19
|
-
"fastify": "^5.0.0",
|
|
20
|
-
"lucide-react": "^1.23.0",
|
|
21
|
-
"react": "^18.3.0",
|
|
22
|
-
"react-dom": "^18.3.0",
|
|
23
|
-
"react-router-dom": "^6.23.0",
|
|
24
|
-
"sql.js": "^1.11.0",
|
|
25
|
-
"yaml": "^2.4.0"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/node": "^22.0.0",
|
|
29
|
-
"@types/react": "^18.3.0",
|
|
30
|
-
"@types/react-dom": "^18.3.0",
|
|
31
|
-
"@vitejs/plugin-react": "^4.3.0",
|
|
32
|
-
"autoprefixer": "^10.4.0",
|
|
33
|
-
"concurrently": "^9.0.0",
|
|
34
|
-
"daisyui": "^4.12.0",
|
|
35
|
-
"postcss": "^8.4.0",
|
|
36
|
-
"tailwindcss": "^3.4.0",
|
|
37
|
-
"tsx": "^4.0.0",
|
|
38
|
-
"typescript": "^5.4.0",
|
|
39
|
-
"vite": "^5.4.0",
|
|
40
|
-
"vitest": "^2.0.0"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@commandgarden/app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "vite build && tsc -p tsconfig.server.json",
|
|
11
|
+
"dev": "concurrently \"vite\" \"tsx watch src/server/main.ts\"",
|
|
12
|
+
"start": "node dist/server/main.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@commandgarden/shared": "*",
|
|
18
|
+
"@fastify/static": "^8.0.0",
|
|
19
|
+
"fastify": "^5.0.0",
|
|
20
|
+
"lucide-react": "^1.23.0",
|
|
21
|
+
"react": "^18.3.0",
|
|
22
|
+
"react-dom": "^18.3.0",
|
|
23
|
+
"react-router-dom": "^6.23.0",
|
|
24
|
+
"sql.js": "^1.11.0",
|
|
25
|
+
"yaml": "^2.4.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"@types/react": "^18.3.0",
|
|
30
|
+
"@types/react-dom": "^18.3.0",
|
|
31
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
32
|
+
"autoprefixer": "^10.4.0",
|
|
33
|
+
"concurrently": "^9.0.0",
|
|
34
|
+
"daisyui": "^4.12.0",
|
|
35
|
+
"postcss": "^8.4.0",
|
|
36
|
+
"tailwindcss": "^3.4.0",
|
|
37
|
+
"tsx": "^4.0.0",
|
|
38
|
+
"typescript": "^5.4.0",
|
|
39
|
+
"vite": "^5.4.0",
|
|
40
|
+
"vitest": "^2.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/messages.ts", "../src/content/dom-executor.ts", "../src/content/content-script.ts"],
|
|
4
|
-
"sourcesContent": ["// src/messages.ts\
|
|
4
|
+
"sourcesContent": ["// src/messages.ts\nconst randomUUID = () => crypto.randomUUID();\n\nexport type DomAction = 'wait' | 'extract' | 'extract_tree' | 'extract_html' | 'click' | 'click_all' | 'type' | 'fetch';\n\nexport interface DomRequest {\n id: string;\n action: DomAction;\n params: Record<string, unknown>;\n}\n\nexport interface DomResponse {\n requestId: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\nexport function createDomRequest(action: DomAction, params: Record<string, unknown>): DomRequest {\n return { id: randomUUID(), action, params };\n}\n\nexport function createDomResponse(\n requestId: string, ok: boolean, data?: unknown, error?: string,\n): DomResponse {\n return { requestId, ok, data, error };\n}\n\nexport function isDomRequest(value: unknown): value is DomRequest {\n if (value == null || typeof value !== 'object') return false;\n const obj = value as Record<string, unknown>;\n return typeof obj.id === 'string' && typeof obj.action === 'string' && typeof obj.params === 'object';\n}\n\nexport function isDomResponse(value: unknown): value is DomResponse {\n if (value == null || typeof value !== 'object') return false;\n const obj = value as Record<string, unknown>;\n return typeof obj.requestId === 'string' && typeof obj.ok === 'boolean';\n}\n", "// src/content/dom-executor.ts\n\nexport function waitForSelector(selector: string, timeout: number): Promise<void> {\n return new Promise((resolve, reject) => {\n if (document.querySelector(selector)) { resolve(); return; }\n const interval = 100;\n let elapsed = 0;\n const timer = setInterval(() => {\n if (document.querySelector(selector)) { clearInterval(timer); resolve(); return; }\n elapsed += interval;\n if (elapsed >= timeout) {\n clearInterval(timer);\n reject(new Error(`Selector \"${selector}\" timed out after ${timeout}ms`));\n }\n }, interval);\n });\n}\n\nexport function extractData(\n rowSelector: string,\n fields: Record<string, string>,\n): Record<string, string>[] {\n const rows = document.querySelectorAll(rowSelector);\n return Array.from(rows).map(row => {\n const record: Record<string, string> = {};\n for (const [name, selector] of Object.entries(fields)) {\n record[name] = row.querySelector(selector)?.textContent?.trim() ?? '';\n }\n return record;\n });\n}\n\nexport function extractHtml(selector: string): string {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`Element \"${selector}\" not found`);\n return el.innerHTML;\n}\n\nexport function extractTree(\n rootSelector: string,\n group: { match: string; title: string; children: string },\n leaf: { match: string; fields: Record<string, string> },\n pathSeparator: string,\n): Record<string, unknown>[] {\n const root = document.querySelector(rootSelector);\n if (!root) throw new Error(`Root \"${rootSelector}\" not found`);\n\n function extractField(el: Element, spec: string): string {\n if (spec === 'textContent') return el.textContent?.trim().replace(/\\s+/g, ' ') ?? '';\n if (spec === 'href') return el.getAttribute('href') ?? '';\n if (spec.startsWith('attr:')) return el.getAttribute(spec.slice(5)) ?? '';\n return el.textContent?.trim() ?? '';\n }\n\n function walk(container: Element, ancestors: string[], depth: number): Record<string, unknown>[] {\n const results: Record<string, unknown>[] = [];\n for (const child of Array.from(container.children)) {\n if (child.matches(leaf.match)) {\n const row: Record<string, unknown> = {};\n for (const [name, spec] of Object.entries(leaf.fields)) {\n row[name] = extractField(child, spec);\n }\n const title = (row.title as string) || child.textContent?.trim() || '';\n const section = ancestors.length > 0 ? ancestors[0] : title;\n const pathParts = [...ancestors, title];\n row.section = section;\n row.path = pathParts.join(pathSeparator);\n row.depth = depth;\n results.push(row);\n } else if (child.matches(group.match)) {\n const titleEl = child.querySelector(group.title);\n const groupTitle = titleEl?.textContent?.trim().replace(/\\s+/g, ' ') ?? '';\n const childContainer = child.querySelector(group.children);\n if (childContainer) {\n results.push(...walk(childContainer, [...ancestors, groupTitle], depth + 1));\n }\n } else {\n results.push(...walk(child, ancestors, depth));\n }\n }\n return results;\n }\n\n return walk(root, [], 0);\n}\n\nexport async function clickAll(\n selector: string, pause: number, maxRounds: number, settle: number,\n): Promise<void> {\n for (let round = 0; round < maxRounds; round++) {\n const elements = document.querySelectorAll(selector);\n if (elements.length === 0) break;\n for (const el of Array.from(elements)) {\n (el as HTMLElement).click();\n if (pause > 0) await new Promise(r => setTimeout(r, pause));\n }\n }\n if (settle > 0) await new Promise(r => setTimeout(r, settle));\n}\n\nexport async function clickElement(selector: string): Promise<void> {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`Element \"${selector}\" not found`);\n (el as HTMLElement).click();\n}\n\nexport async function typeIntoElement(selector: string, value: string): Promise<void> {\n const el = document.querySelector(selector) as HTMLInputElement | null;\n if (!el) throw new Error(`Element \"${selector}\" not found`);\n el.focus();\n el.value = value;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\nexport async function fetchFromPage(\n url: string, method = 'GET', headers?: Record<string, string>, body?: string,\n): Promise<unknown> {\n const resp = await fetch(url, {\n method, headers, body, credentials: 'include',\n });\n const contentType = resp.headers.get('content-type') ?? '';\n if (contentType.includes('application/json')) return resp.json();\n return resp.text();\n}\n", "// src/content/content-script.ts\nimport { isDomRequest, createDomResponse, type DomRequest } from '../messages.js';\nimport { waitForSelector, extractData, extractHtml, extractTree, clickElement, clickAll, typeIntoElement, fetchFromPage } from './dom-executor.js';\n\nchrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {\n if (!isDomRequest(message)) return false;\n const req = message as DomRequest;\n handleRequest(req).then(\n (data) => sendResponse(createDomResponse(req.id, true, data)),\n (err) => sendResponse(createDomResponse(req.id, false, undefined, err.message)),\n );\n return true; // Keep channel open for async response\n});\n\nasync function handleRequest(req: DomRequest): Promise<unknown> {\n const p = req.params;\n switch (req.action) {\n case 'wait':\n await waitForSelector(p.selector as string, (p.timeout as number) ?? 10000);\n return undefined;\n case 'extract':\n return extractData(p.selector as string, p.fields as Record<string, string>);\n case 'extract_html':\n return extractHtml(p.selector as string);\n case 'extract_tree':\n return extractTree(\n p.root as string,\n p.group as { match: string; title: string; children: string },\n p.leaf as { match: string; fields: Record<string, string> },\n (p.pathSeparator as string) ?? ' / ',\n );\n case 'click':\n await clickElement(p.selector as string);\n return undefined;\n case 'click_all':\n await clickAll(\n p.selector as string,\n (p.pause as number) ?? 200,\n (p.maxRounds as number) ?? 10,\n (p.settle as number) ?? 300,\n );\n return undefined;\n case 'type':\n await typeIntoElement(p.selector as string, p.value as string);\n return undefined;\n case 'fetch': {\n const result = await fetchFromPage(\n p.url as string, p.method as string,\n p.headers as Record<string, string>, p.body as string,\n );\n if (p.dataPath && result != null && typeof result === 'object' && !Array.isArray(result)) {\n let nested: unknown = result;\n for (const key of (p.dataPath as string).split('.')) {\n if (nested == null || typeof nested !== 'object') return [];\n nested = (nested as Record<string, unknown>)[key];\n }\n return Array.isArray(nested) ? nested : [];\n }\n return result;\n }\n default:\n throw new Error(`Unknown action: ${req.action}`);\n }\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// fetch-interceptor.js — injected at document_start in MAIN world.
|
|
2
|
+
// Patches window.fetch BEFORE OWA's modules capture a reference to it.
|
|
3
|
+
// Minimal — only the fetch patch, nothing else.
|
|
4
|
+
(function () {
|
|
5
|
+
if (window.__rfb) return;
|
|
6
|
+
window.__rfb = [];
|
|
7
|
+
window.__rfb_urls = [];
|
|
8
|
+
|
|
9
|
+
// Intercept Service Worker messages — OWA sends getSchedule data
|
|
10
|
+
// from the SW to the page via postMessage, bypassing window.fetch.
|
|
11
|
+
if (navigator.serviceWorker) {
|
|
12
|
+
navigator.serviceWorker.addEventListener('message', function (e) {
|
|
13
|
+
try {
|
|
14
|
+
var str = typeof e.data === 'string' ? e.data : JSON.stringify(e.data);
|
|
15
|
+
if (str.indexOf('getSchedule') > -1 && str.indexOf('availabilityView') > -1) {
|
|
16
|
+
window.__rfb.push({ req: '', body: str });
|
|
17
|
+
}
|
|
18
|
+
} catch (_e) { /* ignore */ }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var origFetch = window.fetch;
|
|
23
|
+
window.fetch = function () {
|
|
24
|
+
var a = arguments;
|
|
25
|
+
var u = (a[0] && a[0].url) || a[0] || '';
|
|
26
|
+
var rb = (a[1] && a[1].body) || '';
|
|
27
|
+
var uStr = String(u);
|
|
28
|
+
if (window.__rfb_urls.length < 100) {
|
|
29
|
+
window.__rfb_urls.push(uStr.slice(0, 120));
|
|
30
|
+
}
|
|
31
|
+
return origFetch.apply(this, a).then(function (r) {
|
|
32
|
+
try {
|
|
33
|
+
if (uStr.indexOf('graphql') > -1) {
|
|
34
|
+
r.clone().text().then(function (t) {
|
|
35
|
+
if (t.indexOf('getSchedule') > -1 && t.indexOf('availabilityView') > -1) {
|
|
36
|
+
window.__rfb.push({ req: String(rb || ''), body: t });
|
|
37
|
+
}
|
|
38
|
+
}).catch(function () {});
|
|
39
|
+
}
|
|
40
|
+
} catch (_e) { /* ignore */ }
|
|
41
|
+
return r;
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
})();
|