kalvium-worklog 1.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/README.md +124 -0
- package/dist/capture-token.d.ts +5 -0
- package/dist/capture-token.js +128 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +209 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +75 -0
- package/dist/daily-submit.d.ts +2 -0
- package/dist/daily-submit.js +9 -0
- package/dist/discover-position.d.ts +5 -0
- package/dist/discover-position.js +121 -0
- package/dist/generate-webapp.d.ts +4 -0
- package/dist/generate-webapp.js +61 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/postinstall.d.ts +10 -0
- package/dist/postinstall.js +73 -0
- package/dist/refresh.d.ts +7 -0
- package/dist/refresh.js +42 -0
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +200 -0
- package/dist/submit.d.ts +26 -0
- package/dist/submit.js +196 -0
- package/package.json +51 -0
- package/templates/worklog_standalone.html +423 -0
- package/templates/worklog_template.html +387 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# kalvium-worklog
|
|
2
|
+
|
|
3
|
+
Auto-submit your daily [Kalvium](https://kalvium.community) worklog without opening the website.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g kalvium-worklog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Full setup (login + discover position + schedule + phone webapp)
|
|
15
|
+
kalvium-worklog install
|
|
16
|
+
|
|
17
|
+
# Or step by step:
|
|
18
|
+
kalvium-worklog login # log in via browser (Google + 2FA)
|
|
19
|
+
kalvium-worklog discover # auto-discover your position ID
|
|
20
|
+
kalvium-worklog schedule 14:00 # daily auto-submit at 2 PM
|
|
21
|
+
kalvium-worklog webapp # generate phone web app
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
| Command | Description |
|
|
27
|
+
|---------|-------------|
|
|
28
|
+
| `kalvium-worklog install` | Full setup: login + discover + schedule + webapp |
|
|
29
|
+
| `kalvium-worklog login` | Log in to Kalvium (opens browser, waits for Google + 2FA) |
|
|
30
|
+
| `kalvium-worklog discover` | Auto-discover your internship position ID |
|
|
31
|
+
| `kalvium-worklog submit [text]` | Submit worklog (default: "working") |
|
|
32
|
+
| `kalvium-worklog submit "built a feature"` | Submit with custom text |
|
|
33
|
+
| `kalvium-worklog submit --status remote` | Submit with custom status |
|
|
34
|
+
| `kalvium-worklog submit --dry-run` | Test without submitting |
|
|
35
|
+
| `kalvium-worklog status` | Check recent worklog entries |
|
|
36
|
+
| `kalvium-worklog schedule 14:00` | Set up daily auto-submit at 2 PM |
|
|
37
|
+
| `kalvium-worklog schedule --remove` | Remove daily auto-submit |
|
|
38
|
+
| `kalvium-worklog webapp [path]` | Generate phone web app HTML |
|
|
39
|
+
| `kalvium-worklog daily` | Run daily submit (used by scheduler) |
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Cross-platform**: macOS (launchd), Windows (Task Scheduler), Linux (cron)
|
|
44
|
+
- **Auto re-login**: If the token expires, automatically opens browser, waits for login, and retries
|
|
45
|
+
- **Token auto-refresh**: Refresh token rotates on each use, lasts 6 days
|
|
46
|
+
- **Phone web app**: Submit from your iPhone/Android (no computer needed after setup)
|
|
47
|
+
- **Auto-discovers position ID**: Works for any user, no hardcoded IDs
|
|
48
|
+
- **Weekend skipping**: Auto-submit skips Saturdays and Sundays
|
|
49
|
+
|
|
50
|
+
## Work Statuses
|
|
51
|
+
|
|
52
|
+
| Key | Label |
|
|
53
|
+
|-----|-------|
|
|
54
|
+
| `on_site` | Working on-site (Company location) |
|
|
55
|
+
| `remote` | Working Remotely (Not in the Kalvium environment) |
|
|
56
|
+
| `classroom` | Working out of the Kalvium environment (Classroom) |
|
|
57
|
+
| `holiday` | Today was a company Holiday |
|
|
58
|
+
| `leave` | Took an approved leave from work |
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
kalvium-worklog submit "worked from home" --status remote
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Phone Web App
|
|
65
|
+
|
|
66
|
+
After running `kalvium-worklog webapp`:
|
|
67
|
+
|
|
68
|
+
1. AirDrop `~/KalviumWorklog.html` to your phone
|
|
69
|
+
2. Open in **Safari** (iPhone) or **Chrome** (Android)
|
|
70
|
+
3. **Add to Home Screen**
|
|
71
|
+
4. Tap the icon to submit worklogs from your phone
|
|
72
|
+
|
|
73
|
+
The phone app works independently — it refreshes its own token and auto-retries if the token expires.
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
One-time setup:
|
|
79
|
+
Browser login → capture refresh token → discover position ID
|
|
80
|
+
|
|
81
|
+
Daily auto-submit (scheduler):
|
|
82
|
+
Refresh token → PUT worklog API → done
|
|
83
|
+
If token expired → auto open browser → re-login → retry
|
|
84
|
+
|
|
85
|
+
Manual submit:
|
|
86
|
+
kalvium-worklog submit "my update" → refresh token → PUT worklog
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Token Lifecycle
|
|
90
|
+
|
|
91
|
+
- **Access token**: 3 days
|
|
92
|
+
- **Refresh token**: 6 days, rotates on each use
|
|
93
|
+
- As long as you submit at least once every 6 days, the token never expires
|
|
94
|
+
- If expired: `kalvium-worklog login` (or it auto-triggers)
|
|
95
|
+
|
|
96
|
+
## Files
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
~/.kalvium/
|
|
100
|
+
├── refresh_token.json # Current token (auto-rotates)
|
|
101
|
+
├── config.json # Position ID + API URL
|
|
102
|
+
├── daily_submit.js # Scheduler entry point
|
|
103
|
+
└── worklog.log # Log file
|
|
104
|
+
|
|
105
|
+
~/.kalvium_profile/ # Browser profile (saved login)
|
|
106
|
+
~/KalviumWorklog.html # Phone web app
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Uninstall
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
kalvium-worklog schedule --remove
|
|
113
|
+
npm uninstall -g kalvium-worklog
|
|
114
|
+
rm -rf ~/.kalvium ~/.kalvium_profile ~/KalviumWorklog.html
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Requirements
|
|
118
|
+
|
|
119
|
+
- Node.js 18+
|
|
120
|
+
- npm
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { PROFILE_DIR, TOKEN_FILE, ensureInstallDir, saveToken, } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Try headless mode first (fast — works if session is still valid).
|
|
5
|
+
* If no session, opens a headed browser and waits for manual login.
|
|
6
|
+
*/
|
|
7
|
+
export async function captureToken() {
|
|
8
|
+
ensureInstallDir();
|
|
9
|
+
// Step 1: Try headless (quick — works if session is still valid)
|
|
10
|
+
console.log("Trying headless mode (existing session)...");
|
|
11
|
+
let data = await tryCapture(true);
|
|
12
|
+
if (data) {
|
|
13
|
+
console.log(" Session still valid — token captured!");
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// Step 2: Need manual login
|
|
17
|
+
console.log(" No valid session. Opening browser for manual login...");
|
|
18
|
+
data = await tryCapture(false);
|
|
19
|
+
}
|
|
20
|
+
if (data) {
|
|
21
|
+
saveToken(data);
|
|
22
|
+
console.log(`\nToken saved to ${TOKEN_FILE}`);
|
|
23
|
+
console.log(` Access token expires in: ${data.expires_in ?? "?"} seconds`);
|
|
24
|
+
console.log(` Refresh token expires in: ${data.refresh_expires_in ?? "?"} seconds`);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
console.log("\nERROR: Could not capture token. Please log in and try again.");
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
async function tryCapture(headless) {
|
|
31
|
+
const tokenData = {};
|
|
32
|
+
const context = await chromium.launchPersistentContext(PROFILE_DIR, {
|
|
33
|
+
headless: false, // Playwright doesn't support --headless=new directly
|
|
34
|
+
args: [
|
|
35
|
+
"--disable-blink-features=AutomationControlled",
|
|
36
|
+
...(headless ? ["--headless=new"] : []),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
40
|
+
// Intercept token exchange requests
|
|
41
|
+
context.route("**/openid-connect/token", (route) => {
|
|
42
|
+
if (route.request().method() === "POST") {
|
|
43
|
+
route.fetch().then((response) => {
|
|
44
|
+
response
|
|
45
|
+
.text()
|
|
46
|
+
.then((text) => {
|
|
47
|
+
try {
|
|
48
|
+
const data = JSON.parse(text);
|
|
49
|
+
if (data.refresh_token) {
|
|
50
|
+
if (!headless)
|
|
51
|
+
console.log(" Refresh token captured!");
|
|
52
|
+
tokenData.data = data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.catch(() => { });
|
|
60
|
+
route.fulfill({ response }).catch(() => { });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
route.continue().catch(() => { });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (headless) {
|
|
68
|
+
console.log("Loading kalvium.community/internships (headless)...");
|
|
69
|
+
try {
|
|
70
|
+
await page.goto("https://kalvium.community/internships", {
|
|
71
|
+
waitUntil: "domcontentloaded",
|
|
72
|
+
timeout: 30000,
|
|
73
|
+
});
|
|
74
|
+
await page
|
|
75
|
+
.waitForLoadState("networkidle", { timeout: 20000 })
|
|
76
|
+
.catch(() => { });
|
|
77
|
+
await page.waitForTimeout(8000);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log("\n ========================================");
|
|
85
|
+
console.log(" Browser opened. Please log in now:");
|
|
86
|
+
console.log(" 1. Click 'Continue with Google'");
|
|
87
|
+
console.log(" 2. Sign in with your Kalvium account");
|
|
88
|
+
console.log(" 3. Complete 2FA if prompted");
|
|
89
|
+
console.log(" 4. Wait — the script continues automatically");
|
|
90
|
+
console.log(" ========================================\n");
|
|
91
|
+
await page.goto("https://kalvium.community/internships", {
|
|
92
|
+
waitUntil: "domcontentloaded",
|
|
93
|
+
timeout: 120000,
|
|
94
|
+
});
|
|
95
|
+
console.log(" Waiting for login to complete (up to 5 minutes)...");
|
|
96
|
+
// Poll up to 5 minutes
|
|
97
|
+
for (let i = 0; i < 300; i++) {
|
|
98
|
+
if (tokenData.data)
|
|
99
|
+
break;
|
|
100
|
+
await page.waitForTimeout(1000);
|
|
101
|
+
if (i > 0 && i % 30 === 0) {
|
|
102
|
+
const url = page.url();
|
|
103
|
+
if (url.includes("kalvium.community/internships") &&
|
|
104
|
+
!url.includes("error")) {
|
|
105
|
+
await page
|
|
106
|
+
.waitForLoadState("networkidle", { timeout: 5000 })
|
|
107
|
+
.catch(() => { });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Last resort: reload to trigger token exchange
|
|
112
|
+
if (!tokenData.data) {
|
|
113
|
+
console.log(" Trying page reload to trigger token exchange...");
|
|
114
|
+
try {
|
|
115
|
+
await page.reload({ waitUntil: "domcontentloaded", timeout: 30000 });
|
|
116
|
+
await page
|
|
117
|
+
.waitForLoadState("networkidle", { timeout: 15000 })
|
|
118
|
+
.catch(() => { });
|
|
119
|
+
await page.waitForTimeout(8000);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// ignore
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await context.close();
|
|
127
|
+
return tokenData.data ?? null;
|
|
128
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { captureToken } from "./capture-token.js";
|
|
4
|
+
import { discoverPosition } from "./discover-position.js";
|
|
5
|
+
import { submitWorklog, checkStatus, dailySubmit } from "./submit.js";
|
|
6
|
+
import { setupScheduler, removeScheduler } from "./scheduler.js";
|
|
7
|
+
import { generateWebapp } from "./generate-webapp.js";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("kalvium-worklog")
|
|
14
|
+
.description("Auto-submit your daily Kalvium worklog")
|
|
15
|
+
.version("1.0.0");
|
|
16
|
+
// ─── login ─────────────────────────────────────────────────────────────────
|
|
17
|
+
program
|
|
18
|
+
.command("login")
|
|
19
|
+
.description("Log in to Kalvium and capture refresh token (opens browser)")
|
|
20
|
+
.action(async () => {
|
|
21
|
+
const ok = await captureToken();
|
|
22
|
+
if (!ok)
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
// ─── discover ──────────────────────────────────────────────────────────────
|
|
26
|
+
program
|
|
27
|
+
.command("discover")
|
|
28
|
+
.description("Auto-discover your internship position ID")
|
|
29
|
+
.action(async () => {
|
|
30
|
+
const ok = await discoverPosition();
|
|
31
|
+
if (!ok)
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
// ─── submit ────────────────────────────────────────────────────────────────
|
|
35
|
+
program
|
|
36
|
+
.command("submit [text]")
|
|
37
|
+
.description("Submit worklog (default text: 'working')")
|
|
38
|
+
.option("--status <type>", "Work status", "on_site")
|
|
39
|
+
.option("--dry-run", "Test without submitting")
|
|
40
|
+
.action(async (text, opts) => {
|
|
41
|
+
// Skip weekends (unless dry run)
|
|
42
|
+
const day = new Date().getDay();
|
|
43
|
+
if ((day === 0 || day === 6) && !opts.dryRun) {
|
|
44
|
+
console.log("It's a weekend — skipping.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const result = await submitWorklog({
|
|
48
|
+
text: text ?? "working",
|
|
49
|
+
status: opts.status,
|
|
50
|
+
dryRun: opts.dryRun,
|
|
51
|
+
});
|
|
52
|
+
if (!result.success)
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
// ─── status ────────────────────────────────────────────────────────────────
|
|
56
|
+
program
|
|
57
|
+
.command("status")
|
|
58
|
+
.description("Check recent worklog entries")
|
|
59
|
+
.action(async () => {
|
|
60
|
+
await checkStatus();
|
|
61
|
+
});
|
|
62
|
+
// ─── daily ─────────────────────────────────────────────────────────────────
|
|
63
|
+
program
|
|
64
|
+
.command("daily")
|
|
65
|
+
.description("Run daily auto-submit (used by scheduler — skips weekends)")
|
|
66
|
+
.action(async () => {
|
|
67
|
+
const ok = await dailySubmit();
|
|
68
|
+
if (!ok)
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
// ─── schedule ──────────────────────────────────────────────────────────────
|
|
72
|
+
program
|
|
73
|
+
.command("schedule [time]")
|
|
74
|
+
.description("Set up daily auto-submit (e.g. '14:00' for 2 PM)")
|
|
75
|
+
.option("--remove", "Remove the scheduled task")
|
|
76
|
+
.action(async (time, opts) => {
|
|
77
|
+
if (opts.remove) {
|
|
78
|
+
await removeScheduler();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let hour = 14;
|
|
82
|
+
let minute = 0;
|
|
83
|
+
if (time) {
|
|
84
|
+
const match = time.match(/^(\d{1,2}):(\d{2})$/);
|
|
85
|
+
if (!match) {
|
|
86
|
+
console.log("Invalid time format. Use HH:MM (e.g. 14:00 for 2 PM)");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
hour = parseInt(match[1], 10);
|
|
90
|
+
minute = parseInt(match[2], 10);
|
|
91
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
92
|
+
console.log("Invalid time. Hour: 0-23, Minute: 0-59");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Interactive prompt
|
|
98
|
+
const readline = await import("readline");
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input: process.stdin,
|
|
101
|
+
output: process.stdout,
|
|
102
|
+
});
|
|
103
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
104
|
+
const hourInput = await ask("Enter hour (24-hour, e.g. 14 for 2 PM) [14]: ");
|
|
105
|
+
const minuteInput = await ask("Enter minute [0]: ");
|
|
106
|
+
rl.close();
|
|
107
|
+
hour = parseInt(hourInput || "14", 10);
|
|
108
|
+
minute = parseInt(minuteInput || "0", 10);
|
|
109
|
+
if (isNaN(hour) || hour < 0 || hour > 23) {
|
|
110
|
+
console.log("Invalid hour, defaulting to 14 (2 PM)");
|
|
111
|
+
hour = 14;
|
|
112
|
+
}
|
|
113
|
+
if (isNaN(minute) || minute < 0 || minute > 59) {
|
|
114
|
+
console.log("Invalid minute, defaulting to 0");
|
|
115
|
+
minute = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const ok = await setupScheduler({ hour, minute });
|
|
119
|
+
if (!ok)
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
// ─── webapp ────────────────────────────────────────────────────────────────
|
|
123
|
+
program
|
|
124
|
+
.command("webapp [path]")
|
|
125
|
+
.description("Generate phone web app HTML (default: ~/KalviumWorklog.html)")
|
|
126
|
+
.action(async (path) => {
|
|
127
|
+
const ok = await generateWebapp(path);
|
|
128
|
+
if (!ok)
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
131
|
+
// ─── install (full setup) ──────────────────────────────────────────────────
|
|
132
|
+
program
|
|
133
|
+
.command("install")
|
|
134
|
+
.description("Full setup: login + discover + schedule + webapp")
|
|
135
|
+
.action(async () => {
|
|
136
|
+
const readline = await import("readline");
|
|
137
|
+
const rl = readline.createInterface({
|
|
138
|
+
input: process.stdin,
|
|
139
|
+
output: process.stdout,
|
|
140
|
+
});
|
|
141
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
142
|
+
console.log("\n🚀 Kalvium Worklog — Full Setup\n");
|
|
143
|
+
// Step 1: Login
|
|
144
|
+
console.log("── Step 1: Log in ──");
|
|
145
|
+
console.log("A browser will open. Log in with Google + 2FA if needed.\n");
|
|
146
|
+
const loginOk = await captureToken();
|
|
147
|
+
if (!loginOk) {
|
|
148
|
+
console.log("Login failed. Try again with `kalvium-worklog login`");
|
|
149
|
+
rl.close();
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
// Step 2: Discover position
|
|
153
|
+
console.log("\n── Step 2: Discover position ID ──");
|
|
154
|
+
const discoverOk = await discoverPosition();
|
|
155
|
+
if (!discoverOk) {
|
|
156
|
+
console.log("Discovery failed. Try again with `kalvium-worklog discover`");
|
|
157
|
+
rl.close();
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
// Step 3: Schedule
|
|
161
|
+
console.log("\n── Step 3: Set up daily auto-submit ──");
|
|
162
|
+
const timeInput = await ask("What time? (HH:MM, e.g. 14:00 for 2 PM) [14:00]: ");
|
|
163
|
+
rl.close();
|
|
164
|
+
let hour = 14;
|
|
165
|
+
let minute = 0;
|
|
166
|
+
if (timeInput.trim()) {
|
|
167
|
+
const match = timeInput.trim().match(/^(\d{1,2}):(\d{2})$/);
|
|
168
|
+
if (match) {
|
|
169
|
+
hour = parseInt(match[1], 10);
|
|
170
|
+
minute = parseInt(match[2], 10);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await setupScheduler({ hour, minute });
|
|
174
|
+
// Step 4: Webapp
|
|
175
|
+
console.log("\n── Step 4: Generate phone web app ──");
|
|
176
|
+
await generateWebapp();
|
|
177
|
+
console.log("\n✅ Setup complete!\n");
|
|
178
|
+
console.log("Commands:");
|
|
179
|
+
console.log(" kalvium-worklog submit # submit with 'working'");
|
|
180
|
+
console.log(" kalvium-worklog submit 'my update' # custom text");
|
|
181
|
+
console.log(" kalvium-worklog status # check status");
|
|
182
|
+
console.log(" kalvium-worklog schedule --remove # stop automation");
|
|
183
|
+
console.log("");
|
|
184
|
+
});
|
|
185
|
+
// ─── Default: show help ────────────────────────────────────────────────────
|
|
186
|
+
if (process.argv.length <= 2) {
|
|
187
|
+
// Check if already set up
|
|
188
|
+
const tokenExists = existsSync(join(homedir(), ".kalvium", "refresh_token.json"));
|
|
189
|
+
const configExists = existsSync(join(homedir(), ".kalvium", "config.json"));
|
|
190
|
+
if (!tokenExists || !configExists) {
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log(" Welcome to Kalvium Worklog!");
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log(" You haven't set up yet. Run:");
|
|
195
|
+
console.log("");
|
|
196
|
+
console.log(" kalvium-worklog install");
|
|
197
|
+
console.log("");
|
|
198
|
+
console.log(" This will log you in, discover your position ID,");
|
|
199
|
+
console.log(" set up daily auto-submit, and generate a phone web app.");
|
|
200
|
+
console.log("");
|
|
201
|
+
console.log(" Or run kalvium-worklog --help to see all commands.");
|
|
202
|
+
console.log("");
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
program.help();
|
|
206
|
+
}
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
program.parse();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const INSTALL_DIR: string;
|
|
2
|
+
export declare const PROFILE_DIR: string;
|
|
3
|
+
export declare const TOKEN_FILE: string;
|
|
4
|
+
export declare const CONFIG_FILE: string;
|
|
5
|
+
export declare const LOG_FILE: string;
|
|
6
|
+
export declare const KEYCLOAK_URL = "https://auth.kalvium.community/auth/realms/kalvium/protocol/openid-connect/token";
|
|
7
|
+
export declare const CLIENT_ID = "login_client";
|
|
8
|
+
export declare const API_BASE = "https://student-api.kalvium.community/api/internships/worklogs";
|
|
9
|
+
export interface TokenData {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token: string;
|
|
12
|
+
expires_in?: number;
|
|
13
|
+
refresh_expires_in?: number;
|
|
14
|
+
token_type?: string;
|
|
15
|
+
session_state?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ConfigData {
|
|
18
|
+
position_id: string;
|
|
19
|
+
worklog_api_url: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function ensureInstallDir(): void;
|
|
22
|
+
export declare function loadToken(): TokenData | null;
|
|
23
|
+
export declare function saveToken(data: TokenData): void;
|
|
24
|
+
export declare function loadConfig(): ConfigData | null;
|
|
25
|
+
export declare function saveConfig(config: ConfigData): void;
|
|
26
|
+
export declare function getWorklogApi(): string | null;
|
|
27
|
+
export declare function log(message: string): void;
|
|
28
|
+
export declare function decodeJwt(token: string): Record<string, unknown> | null;
|
|
29
|
+
export declare function getPositionIdFromToken(token: TokenData): string | null;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
export const INSTALL_DIR = join(homedir(), ".kalvium");
|
|
5
|
+
export const PROFILE_DIR = join(homedir(), ".kalvium_profile");
|
|
6
|
+
export const TOKEN_FILE = join(INSTALL_DIR, "refresh_token.json");
|
|
7
|
+
export const CONFIG_FILE = join(INSTALL_DIR, "config.json");
|
|
8
|
+
export const LOG_FILE = join(INSTALL_DIR, "worklog.log");
|
|
9
|
+
export const KEYCLOAK_URL = "https://auth.kalvium.community/auth/realms/kalvium/protocol/openid-connect/token";
|
|
10
|
+
export const CLIENT_ID = "login_client";
|
|
11
|
+
export const API_BASE = "https://student-api.kalvium.community/api/internships/worklogs";
|
|
12
|
+
export function ensureInstallDir() {
|
|
13
|
+
if (!existsSync(INSTALL_DIR)) {
|
|
14
|
+
mkdirSync(INSTALL_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function loadToken() {
|
|
18
|
+
if (!existsSync(TOKEN_FILE))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function saveToken(data) {
|
|
28
|
+
ensureInstallDir();
|
|
29
|
+
writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2));
|
|
30
|
+
}
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
if (!existsSync(CONFIG_FILE))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function saveConfig(config) {
|
|
42
|
+
ensureInstallDir();
|
|
43
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
44
|
+
}
|
|
45
|
+
export function getWorklogApi() {
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
return config?.worklog_api_url ?? null;
|
|
48
|
+
}
|
|
49
|
+
export function log(message) {
|
|
50
|
+
ensureInstallDir();
|
|
51
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
52
|
+
const line = `${timestamp} ${message}\n`;
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(LOG_FILE, line, { flag: "a" });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ignore log errors
|
|
58
|
+
}
|
|
59
|
+
console.log(message);
|
|
60
|
+
}
|
|
61
|
+
// ─── JWT decode ──────────────────────────────────────────────────────────
|
|
62
|
+
export function decodeJwt(token) {
|
|
63
|
+
try {
|
|
64
|
+
const payload = token.split(".")[1];
|
|
65
|
+
const decoded = Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
|
|
66
|
+
return JSON.parse(decoded);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function getPositionIdFromToken(token) {
|
|
73
|
+
const payload = decodeJwt(token.access_token);
|
|
74
|
+
return payload?.sub ?? null;
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Daily auto-submit entry point (used by launchd/Task Scheduler/cron).
|
|
4
|
+
* Skips weekends. Auto re-logins if token expired.
|
|
5
|
+
*/
|
|
6
|
+
import { dailySubmit } from "./submit.js";
|
|
7
|
+
dailySubmit().then((ok) => {
|
|
8
|
+
process.exit(ok ? 0 : 1);
|
|
9
|
+
});
|