kalvium-worklog 1.0.2 → 1.1.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Catch-up entry point — runs on login/wake.
4
+ * Checks if today's worklog was submitted, and if not, submits it.
5
+ * Used by the login launchd job (RunAtLoad).
6
+ */
7
+ import { catchUpSubmit } from "./submit.js";
8
+ catchUpSubmit().then((ok) => {
9
+ process.exit(ok ? 0 : 1);
10
+ });
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { captureToken } from "./capture-token.js";
4
4
  import { discoverPosition } from "./discover-position.js";
5
- import { submitWorklog, checkStatus, dailySubmit } from "./submit.js";
5
+ import { submitWorklog, checkStatus, dailySubmit, catchUpSubmit } from "./submit.js";
6
6
  import { setupScheduler, removeScheduler } from "./scheduler.js";
7
7
  import { generateWebapp } from "./generate-webapp.js";
8
8
  import { loadToken, saveConfig, getPositionIdFromToken } from "./config.js";
@@ -41,7 +41,7 @@ program
41
41
  // ─── submit ────────────────────────────────────────────────────────────────
42
42
  program
43
43
  .command("submit [text]")
44
- .description("Submit worklog (default text: 'working')")
44
+ .description("Submit worklog (default text: 'Working on assigned task')")
45
45
  .option("--status <type>", "Work status", "on_site")
46
46
  .option("--dry-run", "Test without submitting")
47
47
  .action(async (text, opts) => {
@@ -52,7 +52,7 @@ program
52
52
  return;
53
53
  }
54
54
  const result = await submitWorklog({
55
- text: text ?? "working",
55
+ text: text ?? "Working on assigned task",
56
56
  status: opts.status,
57
57
  dryRun: opts.dryRun,
58
58
  });
@@ -69,12 +69,21 @@ program
69
69
  // ─── daily ─────────────────────────────────────────────────────────────────
70
70
  program
71
71
  .command("daily")
72
- .description("Run daily auto-submit (used by scheduler — skips weekends)")
72
+ .description("Run daily auto-submit (used by scheduler — skips weekends, retries on network failure)")
73
73
  .action(async () => {
74
74
  const ok = await dailySubmit();
75
75
  if (!ok)
76
76
  process.exit(1);
77
77
  });
78
+ // ─── catchup ───────────────────────────────────────────────────────────────
79
+ program
80
+ .command("catchup")
81
+ .description("Check if today's worklog is missing and submit if needed (runs on login/wake)")
82
+ .action(async () => {
83
+ const ok = await catchUpSubmit();
84
+ if (!ok)
85
+ process.exit(1);
86
+ });
78
87
  // ─── schedule ──────────────────────────────────────────────────────────────
79
88
  program
80
89
  .command("schedule [time]")
@@ -201,7 +210,7 @@ program
201
210
  await generateWebapp();
202
211
  console.log("\n✅ Setup complete!\n");
203
212
  console.log("Commands:");
204
- console.log(" kalvium-worklog submit # submit with 'working'");
213
+ console.log(" kalvium-worklog submit # submit with default text");
205
214
  console.log(" kalvium-worklog submit 'my update' # custom text");
206
215
  console.log(" kalvium-worklog status # check status");
207
216
  console.log(" kalvium-worklog schedule --remove # stop automation");
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { captureToken } from "./capture-token.js";
2
2
  export { discoverPosition } from "./discover-position.js";
3
- export { submitWorklog, checkStatus, dailySubmit, STATUS_OPTIONS } from "./submit.js";
3
+ export { submitWorklog, checkStatus, dailySubmit, catchUpSubmit, STATUS_OPTIONS } from "./submit.js";
4
4
  export type { SubmitOptions, SubmitResult } from "./submit.js";
5
5
  export { setupScheduler, removeScheduler } from "./scheduler.js";
6
6
  export type { ScheduleOptions } from "./scheduler.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { captureToken } from "./capture-token.js";
2
2
  export { discoverPosition } from "./discover-position.js";
3
- export { submitWorklog, checkStatus, dailySubmit, STATUS_OPTIONS } from "./submit.js";
3
+ export { submitWorklog, checkStatus, dailySubmit, catchUpSubmit, STATUS_OPTIONS } from "./submit.js";
4
4
  export { setupScheduler, removeScheduler } from "./scheduler.js";
5
5
  export { generateWebapp } from "./generate-webapp.js";
6
6
  export { refreshToken } from "./refresh.js";
package/dist/scheduler.js CHANGED
@@ -37,30 +37,39 @@ function formatTime(hour, minute) {
37
37
  return `${hour - 12}:${minute.toString().padStart(2, "0")} PM`;
38
38
  }
39
39
  /**
40
- * Copy the daily-submit.js script to ~/.kalvium/ as a wrapper that
41
- * imports from the npm package's dist directory (using absolute paths).
42
- * This way it works even if the package is updated.
40
+ * Copy the daily-submit.js and catchup.js scripts to ~/.kalvium/ as wrappers
41
+ * that import from the npm package's dist directory (using absolute paths).
43
42
  */
44
43
  function copyDailyScript() {
45
44
  ensureInstallDir();
46
- const dst = join(INSTALL_DIR, "daily_submit.js");
47
- // Create a wrapper that imports from the package's dist directory
48
45
  const distDir = __dirname;
49
- const wrapper = `#!/usr/bin/env node
46
+ // Daily submit wrapper
47
+ const dailyDst = join(INSTALL_DIR, "daily_submit.js");
48
+ const dailyWrapper = `#!/usr/bin/env node
50
49
  // Auto-generated by kalvium-worklog. Do not edit.
51
50
  import { dailySubmit } from "${join(distDir, "submit.js")}";
52
51
  dailySubmit().then((ok) => process.exit(ok ? 0 : 1));
53
52
  `;
54
- writeFileSync(dst, wrapper);
55
- return dst;
53
+ writeFileSync(dailyDst, dailyWrapper);
54
+ // Catch-up wrapper (runs on login/wake)
55
+ const catchupDst = join(INSTALL_DIR, "catchup_submit.js");
56
+ const catchupWrapper = `#!/usr/bin/env node
57
+ // Auto-generated by kalvium-worklog. Do not edit.
58
+ import { catchUpSubmit } from "${join(distDir, "submit.js")}";
59
+ catchUpSubmit().then((ok) => process.exit(ok ? 0 : 1));
60
+ `;
61
+ writeFileSync(catchupDst, catchupWrapper);
62
+ return dailyDst;
56
63
  }
57
64
  function setupLaunchd(hour, minute) {
58
65
  const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
59
66
  if (!existsSync(launchAgentsDir)) {
60
67
  mkdirSync(launchAgentsDir, { recursive: true });
61
68
  }
62
- const plistPath = join(launchAgentsDir, "com.kalvium.worklog.plist");
63
69
  const dailyScript = copyDailyScript();
70
+ const catchupScript = join(INSTALL_DIR, "catchup_submit.js");
71
+ // ─── 1. Scheduled daily plist (fires at chosen time) ────────────────────
72
+ const plistPath = join(launchAgentsDir, "com.kalvium.worklog.plist");
64
73
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
65
74
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
66
75
  <plist version="1.0">
@@ -103,7 +112,43 @@ function setupLaunchd(hour, minute) {
103
112
  writeFileSync(plistPath, plist);
104
113
  execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
105
114
  execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
115
+ // ─── 2. Catch-up plist (fires on login/wake — RunAtLoad) ────────────────
116
+ // This checks if today's worklog was already submitted. If not, submits it.
117
+ // Handles the case where the scheduled time was missed (laptop asleep/offline).
118
+ const catchupPlistPath = join(launchAgentsDir, "com.kalvium.worklog.catchup.plist");
119
+ const catchupPlist = `<?xml version="1.0" encoding="UTF-8"?>
120
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
121
+ <plist version="1.0">
122
+ <dict>
123
+ <key>Label</key>
124
+ <string>com.kalvium.worklog.catchup</string>
125
+ <key>ProgramArguments</key>
126
+ <array>
127
+ <string>${process.execPath}</string>
128
+ <string>${catchupScript}</string>
129
+ </array>
130
+ <key>RunAtLoad</key>
131
+ <true/>
132
+ <key>StartInterval</key>
133
+ <integer>1800</integer>
134
+ <key>StandardOutPath</key>
135
+ <string>${LOG_FILE}</string>
136
+ <key>StandardErrorPath</key>
137
+ <string>${LOG_FILE}</string>
138
+ <key>EnvironmentVariables</key>
139
+ <dict>
140
+ <key>HOME</key>
141
+ <string>${homedir()}</string>
142
+ <key>PATH</key>
143
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin</string>
144
+ </dict>
145
+ </dict>
146
+ </plist>`;
147
+ writeFileSync(catchupPlistPath, catchupPlist);
148
+ execSync(`launchctl unload "${catchupPlistPath}"`, { stdio: "ignore" });
149
+ execSync(`launchctl load "${catchupPlistPath}"`, { stdio: "ignore" });
106
150
  console.log(`Daily auto-submit installed (${formatTime(hour, minute)} every weekday)`);
151
+ console.log(" + Catch-up check on login/wake (every 30 min)");
107
152
  return true;
108
153
  }
109
154
  function setupTaskScheduler(hour, minute) {
@@ -159,6 +204,7 @@ function setupCron(hour, minute) {
159
204
  */
160
205
  export async function removeScheduler() {
161
206
  if (IS_MAC) {
207
+ // Remove daily plist
162
208
  const plistPath = join(homedir(), "Library/LaunchAgents/com.kalvium.worklog.plist");
163
209
  try {
164
210
  execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
@@ -169,7 +215,18 @@ export async function removeScheduler() {
169
215
  if (existsSync(plistPath)) {
170
216
  execSync(`rm "${plistPath}"`);
171
217
  }
172
- console.log("Daily auto-submit removed.");
218
+ // Remove catch-up plist
219
+ const catchupPlistPath = join(homedir(), "Library/LaunchAgents/com.kalvium.worklog.catchup.plist");
220
+ try {
221
+ execSync(`launchctl unload "${catchupPlistPath}"`, { stdio: "ignore" });
222
+ }
223
+ catch {
224
+ // ignore
225
+ }
226
+ if (existsSync(catchupPlistPath)) {
227
+ execSync(`rm "${catchupPlistPath}"`);
228
+ }
229
+ console.log("Daily auto-submit removed (including catch-up).");
173
230
  return true;
174
231
  }
175
232
  else if (IS_WINDOWS) {
package/dist/submit.d.ts CHANGED
@@ -22,5 +22,11 @@ export declare function checkStatus(): Promise<void>;
22
22
  /**
23
23
  * Daily auto-submit (used by scheduler).
24
24
  * Skips weekends. Logs to file.
25
+ * Retries on network failure (up to 5 times with 60s delay).
25
26
  */
26
27
  export declare function dailySubmit(): Promise<boolean>;
28
+ /**
29
+ * Catch-up check: if it's a weekday and today's worklog hasn't been
30
+ * submitted yet, submit it. Used on login/wake to handle missed runs.
31
+ */
32
+ export declare function catchUpSubmit(): Promise<boolean>;
package/dist/submit.js CHANGED
@@ -14,7 +14,7 @@ export const STATUS_OPTIONS = {
14
14
  * for re-login and retries the submission.
15
15
  */
16
16
  export async function submitWorklog(options = {}) {
17
- const text = options.text ?? "working";
17
+ const text = options.text ?? "Working on assigned task";
18
18
  const statusKey = options.status ?? "on_site";
19
19
  const dryRun = options.dryRun ?? false;
20
20
  const worklogApi = getWorklogApi();
@@ -177,6 +177,7 @@ export async function checkStatus() {
177
177
  /**
178
178
  * Daily auto-submit (used by scheduler).
179
179
  * Skips weekends. Logs to file.
180
+ * Retries on network failure (up to 5 times with 60s delay).
180
181
  */
181
182
  export async function dailySubmit() {
182
183
  const day = new Date().getDay();
@@ -185,12 +186,95 @@ export async function dailySubmit() {
185
186
  return true;
186
187
  }
187
188
  log("[INFO] Starting daily worklog submission...");
188
- const result = await submitWorklog({ text: "working", status: "on_site" });
189
- if (result.success) {
190
- log(`[INFO] SUCCESS: ${result.message}`);
189
+ // Retry up to 5 times in case of network failure
190
+ // (covers: laptop waking up, internet reconnecting, etc.)
191
+ const MAX_RETRIES = 5;
192
+ const RETRY_DELAY_MS = 60000; // 1 minute between retries
193
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
194
+ if (attempt > 1) {
195
+ log(`[INFO] Retry ${attempt}/${MAX_RETRIES} (waiting for network...)`);
196
+ }
197
+ const result = await submitWorklog({
198
+ text: "Working on assigned task",
199
+ status: "on_site",
200
+ });
201
+ if (result.success) {
202
+ log(`[INFO] SUCCESS: ${result.message}`);
203
+ return true;
204
+ }
205
+ // Check if it's a network error (retry) vs a real error (give up)
206
+ const isNetworkError = result.message.includes("fetch") ||
207
+ result.message.includes("ECONNREFUSED") ||
208
+ result.message.includes("ENOTFOUND") ||
209
+ result.message.includes("ETIMEDOUT") ||
210
+ result.message.includes("network") ||
211
+ result.message.includes("Failed to fetch") ||
212
+ result.message.includes("refresh failed");
213
+ // "Already submitted" is a success, not an error
214
+ if (result.message.includes("Already submitted")) {
215
+ log(`[INFO] ${result.message}`);
216
+ return true;
217
+ }
218
+ if (isNetworkError && attempt < MAX_RETRIES) {
219
+ log(`[WARN] Network error: ${result.message}. Retrying in 60s...`);
220
+ await sleep(RETRY_DELAY_MS);
221
+ continue;
222
+ }
223
+ // Non-network error or max retries reached
224
+ log(`[ERROR] ${result.message}`);
225
+ return false;
226
+ }
227
+ return false;
228
+ }
229
+ function sleep(ms) {
230
+ return new Promise((resolve) => setTimeout(resolve, ms));
231
+ }
232
+ /**
233
+ * Catch-up check: if it's a weekday and today's worklog hasn't been
234
+ * submitted yet, submit it. Used on login/wake to handle missed runs.
235
+ */
236
+ export async function catchUpSubmit() {
237
+ const day = new Date().getDay();
238
+ if (day === 0 || day === 6) {
239
+ return true; // weekend, nothing to do
240
+ }
241
+ log("[INFO] Catch-up check: verifying today's worklog...");
242
+ // Check if today's worklog was already submitted
243
+ const worklogApi = getWorklogApi();
244
+ if (!worklogApi) {
245
+ log("[ERROR] No config found. Run `kalvium-worklog discover` first.");
246
+ return false;
247
+ }
248
+ // Try to refresh token and check status
249
+ const refreshResult = await refreshToken();
250
+ if (!refreshResult.success || !refreshResult.accessToken) {
251
+ log("[WARN] Could not refresh token for catch-up check. Will try submit directly.");
252
+ // Fall through to submit attempt — it has its own retry/relogin logic
191
253
  }
192
254
  else {
193
- log(`[ERROR] ${result.message}`);
255
+ try {
256
+ const res = await fetch(worklogApi, {
257
+ headers: { authorization: `Bearer ${refreshResult.accessToken}` },
258
+ });
259
+ if (res.ok) {
260
+ const worklogs = await res.json();
261
+ if (Array.isArray(worklogs)) {
262
+ const today = new Date().toISOString().slice(0, 10);
263
+ const todayEntry = worklogs.find((w) => (w.worklogDate ?? "").slice(0, 10) === today &&
264
+ w.status === "complete");
265
+ if (todayEntry) {
266
+ log("[INFO] Today's worklog already submitted. No catch-up needed.");
267
+ return true;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ catch {
273
+ // Network error — fall through to submit attempt
274
+ log("[WARN] Could not check status for catch-up. Will try submit directly.");
275
+ }
194
276
  }
195
- return result.success;
277
+ // Today's worklog is missing — submit it
278
+ log("[INFO] Today's worklog is missing. Running catch-up submit...");
279
+ return dailySubmit();
196
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kalvium-worklog",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Auto-submit your daily Kalvium worklog without opening the website",
5
5
  "keywords": [
6
6
  "kalvium",
@@ -80,7 +80,7 @@
80
80
  </div>
81
81
  </div>
82
82
 
83
- <textarea id="text" placeholder="What did you worked on today?">working</textarea>
83
+ <textarea id="text" placeholder="What did you worked on today?">Working on assigned task</textarea>
84
84
 
85
85
  <button class="btn btn-submit" id="submitBtn" onclick="submitWorklog()">Submit Worklog</button>
86
86
  <button class="btn btn-status" onclick="checkStatus()">Check Status</button>
@@ -76,7 +76,7 @@
76
76
  </div>
77
77
  </div>
78
78
 
79
- <textarea id="text" placeholder="What did you work on today?">working</textarea>
79
+ <textarea id="text" placeholder="What did you work on today?">Working on assigned task</textarea>
80
80
 
81
81
  <button class="btn btn-submit" id="submitBtn" onclick="submitWorklog()">Submit Worklog</button>
82
82
  <button class="btn btn-status" onclick="checkStatus()">Check Status</button>