hacklab 0.10.5 → 0.11.1

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.
Files changed (51) hide show
  1. package/README.md +40 -20
  2. package/dist/commands/daemon.d.ts +6 -2
  3. package/dist/commands/daemon.d.ts.map +1 -1
  4. package/dist/commands/daemon.js +12 -8
  5. package/dist/commands/daemon.js.map +1 -1
  6. package/dist/commands/jobs.d.ts +18 -0
  7. package/dist/commands/jobs.d.ts.map +1 -0
  8. package/dist/commands/jobs.js +138 -0
  9. package/dist/commands/jobs.js.map +1 -0
  10. package/dist/commands/join.d.ts.map +1 -1
  11. package/dist/commands/join.js +2 -0
  12. package/dist/commands/join.js.map +1 -1
  13. package/dist/commands/org-jobs.d.ts +50 -0
  14. package/dist/commands/org-jobs.d.ts.map +1 -0
  15. package/dist/commands/org-jobs.js +383 -0
  16. package/dist/commands/org-jobs.js.map +1 -0
  17. package/dist/commands/org.d.ts +14 -0
  18. package/dist/commands/org.d.ts.map +1 -1
  19. package/dist/commands/org.js +98 -20
  20. package/dist/commands/org.js.map +1 -1
  21. package/dist/commands/sync.d.ts +1 -0
  22. package/dist/commands/sync.d.ts.map +1 -1
  23. package/dist/commands/sync.js +131 -6
  24. package/dist/commands/sync.js.map +1 -1
  25. package/dist/daily-sync.d.ts +20 -3
  26. package/dist/daily-sync.d.ts.map +1 -1
  27. package/dist/daily-sync.js +154 -44
  28. package/dist/daily-sync.js.map +1 -1
  29. package/dist/job-fields.d.ts +47 -0
  30. package/dist/job-fields.d.ts.map +1 -0
  31. package/dist/job-fields.js +148 -0
  32. package/dist/job-fields.js.map +1 -0
  33. package/dist/registry.d.ts.map +1 -1
  34. package/dist/registry.js +12 -2
  35. package/dist/registry.js.map +1 -1
  36. package/dist/scanners/incremental.d.ts +144 -0
  37. package/dist/scanners/incremental.d.ts.map +1 -0
  38. package/dist/scanners/incremental.js +520 -0
  39. package/dist/scanners/incremental.js.map +1 -0
  40. package/dist/scanners/index.d.ts +58 -0
  41. package/dist/scanners/index.d.ts.map +1 -1
  42. package/dist/scanners/index.js +236 -128
  43. package/dist/scanners/index.js.map +1 -1
  44. package/dist/scanners/util.d.ts +1 -1
  45. package/dist/scanners/util.d.ts.map +1 -1
  46. package/dist/scanners/util.js.map +1 -1
  47. package/dist/sync.d.ts +12 -0
  48. package/dist/sync.d.ts.map +1 -1
  49. package/dist/sync.js +30 -6
  50. package/dist/sync.js.map +1 -1
  51. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  import * as clack from '@clack/prompts';
2
- import { appendSyncLog, clearSyncPaused, markSyncPaused, } from '../daily-sync.js';
2
+ import { appendSyncLog, clearSyncPaused, markSyncPaused, readSyncPaused, trimSyncLog, } from '../daily-sync.js';
3
3
  import { captureEvent } from '../posthog.js';
4
4
  import { loadPromptConsent, parsePromptStatsFlag, resolvePromptConsent, savePromptConsent, } from '../prompt-consent.js';
5
- import { scanAllTools } from '../scanners/index.js';
5
+ import { cumulativeTotals, loadScanState, markUploaded, rebuildScanState, runTick, sameTotals, saveScanState, tickPayload, } from '../scanners/incremental.js';
6
+ import { collectToolScans, mergeToolScans } from '../scanners/index.js';
6
7
  import { loadSessionState } from '../session.js';
7
- import { checkSession, ensureFreshSession, formatTokens, LOGIN_EXPIRED_MESSAGE, refreshSession, runSync, scanConsentedPromptStats, syncGithubRepos, uploadTokenScan, } from '../sync.js';
8
+ import { checkSession, ensureFreshSession, formatTokens, LOGIN_EXPIRED_MESSAGE, refreshSession, runSync, SyncUploadError, scanConsentedPromptStats, syncGithubRepos, uploadTokenScan, } from '../sync.js';
8
9
  import { bold, dim, error, info, success } from '../ui.js';
9
10
  import { daemon } from './daemon.js';
10
11
  const SESSION_EXPIRED_REASON = 'your hacklab session expired — run `hacklab login`';
@@ -12,6 +13,7 @@ const SESSION_EXPIRED_REASON = 'your hacklab session expired — run `hacklab lo
12
13
  * `hacklab sync` — dispatch on flags:
13
14
  * (none) interactive sync (scan, upload, show stats)
14
15
  * --install-daily deprecated alias for `hacklab daemon`
16
+ * --tick the every-minute incremental run (usually a no-op)
15
17
  * --quiet the unattended daily run (logs to a file, no output)
16
18
  */
17
19
  export async function sync(args = []) {
@@ -28,6 +30,8 @@ export async function sync(args = []) {
28
30
  info(dim('`sync --install-daily` is now `hacklab daemon` — running that.'));
29
31
  return daemon();
30
32
  }
33
+ if (args.includes('--tick'))
34
+ return tickSync();
31
35
  if (args.includes('--quiet'))
32
36
  return quietSync();
33
37
  return interactiveSync();
@@ -37,6 +41,11 @@ export async function sync(args = []) {
37
41
  * Silent by design — everything goes to ~/.hacklab/sync.log. Proactively
38
42
  * refreshes a near-expiry session; if the session has truly lapsed it drops a
39
43
  * paused marker the next interactive run surfaces, then exits cleanly.
44
+ *
45
+ * It's also the repair pass for the minutely tick: a full stateless scan, so
46
+ * whatever the tick's tail-follow missed or double-counted is corrected here and
47
+ * the tick's state is re-based on the result. Incremental drift therefore can't
48
+ * outlive a day.
40
49
  */
41
50
  async function quietSync() {
42
51
  const state = await loadSessionState();
@@ -53,15 +62,18 @@ async function quietSync() {
53
62
  return;
54
63
  }
55
64
  const session = await ensureFreshSession(state.session);
56
- const scan = await scanAllTools();
65
+ const results = await collectToolScans();
66
+ const scan = mergeToolScans(results);
57
67
  // Whatever the user already consented to. The unattended run never asks, so
58
68
  // a machine that has never answered uploads token counts only.
59
69
  const promptStats = await scanConsentedPromptStats((await loadPromptConsent()) ?? 'none');
60
70
  // Upload tokens AND mirror pinned repos together, so the after-refresh retry
61
71
  // below does the exact same work as the happy path (no silently-skipped repo
62
- // sync on a day the session had to be refreshed mid-run).
72
+ // sync on a day the session had to be refreshed mid-run). The state rebuild
73
+ // rides along: everything just went out, so nothing is left dirty.
63
74
  const push = async (s) => {
64
75
  await uploadTokenScan(s, scan, { promptStats });
76
+ await rebuildScanState(results);
65
77
  await syncGithubRepos(s);
66
78
  };
67
79
  try {
@@ -92,6 +104,116 @@ async function quietSync() {
92
104
  await appendSyncLog('paused: session expired');
93
105
  }
94
106
  }
107
+ /** How long to sit out after a 429 that came without a Retry-After. */
108
+ const DEFAULT_BACKOFF_MS = 15 * 60 * 1000;
109
+ const MAX_BACKOFF_MS = 6 * 60 * 60 * 1000;
110
+ function backoffMs(retryAfter) {
111
+ const seconds = Number(retryAfter);
112
+ if (!Number.isFinite(seconds) || seconds <= 0)
113
+ return DEFAULT_BACKOFF_MS;
114
+ return Math.min(seconds * 1000, MAX_BACKOFF_MS);
115
+ }
116
+ /**
117
+ * Log a tick failure once. A minutely job that keeps hitting the same wall (no
118
+ * network, server down) would otherwise write 1440 identical lines a day, so the
119
+ * first one goes in and the rest are suppressed until something changes.
120
+ */
121
+ async function logTickError(state, line) {
122
+ if (state.lastError === line)
123
+ return;
124
+ state.lastError = line;
125
+ await appendSyncLog(line);
126
+ }
127
+ /** Pause the background sync, logging it only the first time. */
128
+ async function pauseTick(reason) {
129
+ const alreadyPaused = await readSyncPaused();
130
+ await markSyncPaused(reason);
131
+ if (!alreadyPaused)
132
+ await appendSyncLog(`tick paused: ${reason}`);
133
+ }
134
+ /**
135
+ * `hacklab sync --tick` — the every-minute run. Where `--quiet` re-scans every
136
+ * log from scratch (~6s and a few hundred MB for a heavy user), this reads only
137
+ * what the AI tools appended since the last tick, and on the overwhelmingly
138
+ * common minute where nothing was appended it exits after a stat walk, without
139
+ * touching the network. Silent unless it uploaded, failed, or got paused: the
140
+ * log has to survive 1440 runs a day.
141
+ */
142
+ async function tickSync() {
143
+ const saved = await loadScanState();
144
+ // The server asked us to back off — respect it before doing any work at all.
145
+ if (saved?.nextAllowedAt && Date.now() < saved.nextAllowedAt)
146
+ return;
147
+ await trimSyncLog();
148
+ const sessionState = await loadSessionState();
149
+ if (!sessionState.session) {
150
+ // 'missing' is a logged-out machine: nothing to say, every minute.
151
+ if (sessionState.status === 'expired' ||
152
+ sessionState.status === 'invalid') {
153
+ await pauseTick(sessionState.status === 'invalid'
154
+ ? 'your hacklab session file is unreadable — run `hacklab login`'
155
+ : SESSION_EXPIRED_REASON);
156
+ }
157
+ return;
158
+ }
159
+ const session = await ensureFreshSession(sessionState.session);
160
+ const { state, changed } = await runTick(saved);
161
+ const totals = cumulativeTotals(state);
162
+ if (state.dirty.length === 0 &&
163
+ sameTotals(totals.toolTotals, state.uploaded.toolTotals) &&
164
+ sameTotals(totals.modelTotals, state.uploaded.modelTotals)) {
165
+ // The common case: nothing new to say. Persist only if the walk actually
166
+ // saw something move (a touched file, a re-read harness).
167
+ if (changed)
168
+ await saveScanState(state);
169
+ return;
170
+ }
171
+ const scan = tickPayload(state);
172
+ const dates = state.dirty.length;
173
+ // No prompt stats and no GitHub mirror: those are the daily job's work, and a
174
+ // minutely run has no business re-reading transcripts. Not interactive either
175
+ // — a background tick isn't user activity.
176
+ try {
177
+ const result = await uploadTokenScan(session, scan);
178
+ markUploaded(state);
179
+ await saveScanState(state);
180
+ await clearSyncPaused();
181
+ await appendSyncLog(`tick: +${formatTokens(Number(result.tokensDelta ?? 0))} tokens (${dates} date${dates === 1 ? '' : 's'})`);
182
+ }
183
+ catch (e) {
184
+ const message = e instanceof Error ? e.message : String(e);
185
+ const status = e instanceof SyncUploadError ? e.status : 0;
186
+ if (status === 429) {
187
+ const wait = backoffMs(e instanceof SyncUploadError ? e.retryAfter : null);
188
+ state.nextAllowedAt = Date.now() + wait;
189
+ await logTickError(state, `tick: rate limited, pausing ${Math.round(wait / 1000)}s`);
190
+ await saveScanState(state);
191
+ return;
192
+ }
193
+ if (message === LOGIN_EXPIRED_MESSAGE) {
194
+ const refreshed = await refreshSession(session);
195
+ if (refreshed) {
196
+ try {
197
+ const result = await uploadTokenScan(refreshed, scan);
198
+ markUploaded(state);
199
+ await saveScanState(state);
200
+ await clearSyncPaused();
201
+ await appendSyncLog(`tick: +${formatTokens(Number(result.tokensDelta ?? 0))} tokens (${dates} dates, after refresh)`);
202
+ return;
203
+ }
204
+ catch {
205
+ // fall through to pause
206
+ }
207
+ }
208
+ await saveScanState(state);
209
+ await pauseTick(SESSION_EXPIRED_REASON);
210
+ return;
211
+ }
212
+ // Dirty dates stay dirty, so the next tick retries them.
213
+ await logTickError(state, `tick error: ${message}`);
214
+ await saveScanState(state);
215
+ }
216
+ }
95
217
  async function interactiveSync() {
96
218
  const sessionState = await loadSessionState();
97
219
  if (!sessionState.session) {
@@ -131,7 +253,7 @@ async function interactiveSync() {
131
253
  error(e instanceof Error ? e.message : 'sync failed');
132
254
  process.exit(1);
133
255
  }
134
- const { claudeTotal, codexTotal, cursorTotal, openclawTotal, hermesTotal, opencodeTotal, cursorScanStatus, result: r, } = result;
256
+ const { claudeTotal, codexTotal, cursorTotal, openclawTotal, hermesTotal, opencodeTotal, grokTotal, cursorScanStatus, result: r, } = result;
135
257
  if (claudeTotal > 0)
136
258
  info(` Claude Code ${formatTokens(claudeTotal)} tokens`);
137
259
  if (codexTotal > 0)
@@ -144,6 +266,8 @@ async function interactiveSync() {
144
266
  info(` Hermes ${formatTokens(hermesTotal)} tokens`);
145
267
  if (opencodeTotal > 0)
146
268
  info(` OpenCode ${formatTokens(opencodeTotal)} tokens`);
269
+ if (grokTotal > 0)
270
+ info(` Grok Build ${formatTokens(grokTotal)} tokens`);
147
271
  // A key Cursor rejected must never be silent: the Cursor line above would be
148
272
  // the local estimate while the user believes they're getting exact counts.
149
273
  if (cursorScanStatus.source === 'api-failed') {
@@ -190,6 +314,7 @@ async function interactiveSync() {
190
314
  tokens_openclaw: openclawTotal,
191
315
  tokens_hermes: hermesTotal,
192
316
  tokens_opencode: opencodeTotal,
317
+ tokens_grok: grokTotal,
193
318
  level: r.level,
194
319
  title: r.title,
195
320
  });
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAA;AAEvC,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAgB,MAAM,eAAe,CAAA;AAC9D,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,qBAAqB,EACrB,cAAc,EACd,OAAO,EACP,wBAAwB,EACxB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,sBAAsB,GAC1B,oDAAoD,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAiB,EAAE;IAC5C,uEAAuE;IACvE,6EAA6E;IAC7E,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC3D,IAAI,QAAQ;QAAE,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAA;IAC/C,IAAI,GAAG,IAAI,CAAA;IAEX,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACrC,4EAA4E;QAC5E,6EAA6E;QAC7E,uDAAuD;QACvD,IAAI,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC,CAAA;QAC3E,OAAO,MAAM,EAAE,CAAA;IACjB,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,EAAE,CAAA;IAChD,OAAO,eAAe,EAAE,CAAA;AAC1B,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,SAAS;IACtB,MAAM,KAAK,GAAG,MAAM,gBAAgB,EAAE,CAAA;IACtC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,aAAa,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;QAC5C,4EAA4E;QAC5E,2EAA2E;QAC3E,kCAAkC;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC7D,MAAM,cAAc,CAClB,KAAK,CAAC,MAAM,KAAK,SAAS;gBACxB,CAAC,CAAC,+DAA+D;gBACjE,CAAC,CAAC,sBAAsB,CAC3B,CAAA;QACH,CAAC;QACD,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACvD,MAAM,IAAI,GAAG,MAAM,YAAY,EAAE,CAAA;IACjC,4EAA4E;IAC5E,+DAA+D;IAC/D,MAAM,WAAW,GAAG,MAAM,wBAAwB,CAChD,CAAC,MAAM,iBAAiB,EAAE,CAAC,IAAI,MAAM,CACtC,CAAA;IAED,6EAA6E;IAC7E,6EAA6E;IAC7E,0DAA0D;IAC1D,MAAM,IAAI,GAAG,KAAK,EAAE,CAAU,EAAE,EAAE;QAChC,MAAM,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,CAAC,CAAA;QAC/C,MAAM,eAAe,CAAC,CAAC,CAAC,CAAA;IAC1B,CAAC,CAAA;IAED,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,CAAC,CAAA;QACnB,MAAM,eAAe,EAAE,CAAA;QACvB,MAAM,aAAa,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QACtD,IAAI,GAAG,KAAK,qBAAqB,EAAE,CAAC;YAClC,MAAM,aAAa,CAAC,UAAU,GAAG,EAAE,CAAC,CAAA;YACpC,OAAM;QACR,CAAC;QACD,2DAA2D;QAC3D,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAA;QAC/C,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,SAAS,CAAC,CAAA;gBACrB,MAAM,eAAe,EAAE,CAAA;gBACvB,MAAM,aAAa,CAAC,oBAAoB,CAAC,CAAA;gBACzC,OAAM;YACR,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;QACD,MAAM,cAAc,CAAC,sBAAsB,CAAC,CAAA;QAC5C,MAAM,aAAa,CAAC,yBAAyB,CAAC,CAAA;IAChD,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe;IAC5B,MAAM,YAAY,GAAG,MAAM,gBAAgB,EAAE,CAAA;IAC7C,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;QAC1B,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;QAC5E,IAAI,CAAC,OAAO,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,+EAA+E;IAC/E,0BAA0B;IAC1B,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IAC7D,IAAI,YAAY,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QAC3C,KAAK,CAAC,eAAe,CAAC,CAAA;QACtB,IAAI,CAAC,OAAO,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IACD,IAAI,YAAY,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACrC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QAC3B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,+EAA+E;IAC/E,2DAA2D;IAC3D,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IAE9D,4EAA4E;IAC5E,mDAAmD;IACnD,MAAM,aAAa,GAAG,MAAM,oBAAoB,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;IAE7E,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACf,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAA;IACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC,CAAA;IACrD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAEf,IAAI,MAA2C,CAAA;IAC/C,IAAI,CAAC;QACH,8EAA8E;QAC9E,0EAA0E;QAC1E,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAA;IACvE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,KAAK,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,EACJ,WAAW,EACX,UAAU,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,MAAM,EAAE,CAAC,GACV,GAAG,MAAM,CAAA;IAEV,IAAI,WAAW,GAAG,CAAC;QACjB,IAAI,CAAC,kBAAkB,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC,CAAA;IAC5D,IAAI,UAAU,GAAG,CAAC;QAAE,IAAI,CAAC,kBAAkB,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;IAC7E,IAAI,WAAW,GAAG,CAAC;QACjB,IAAI,CAAC,kBAAkB,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC,CAAA;IAC5D,IAAI,aAAa,GAAG,CAAC;QACnB,IAAI,CAAC,kBAAkB,YAAY,CAAC,aAAa,CAAC,SAAS,CAAC,CAAA;IAC9D,IAAI,WAAW,GAAG,CAAC;QACjB,IAAI,CAAC,kBAAkB,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC,CAAA;IAC5D,IAAI,aAAa,GAAG,CAAC;QACnB,IAAI,CAAC,kBAAkB,YAAY,CAAC,aAAa,CAAC,SAAS,CAAC,CAAA;IAE9D,6EAA6E;IAC7E,2EAA2E;IAC3E,IAAI,gBAAgB,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QAC7C,KAAK,CAAC,aAAa,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAA;QAC7C,IAAI,CACF,GAAG,CAAC,oEAAoE,CAAC,CAC1E,CAAA;QACD,IAAI,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC,CAAA;IAClE,CAAC;SAAM,IAAI,gBAAgB,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;QACrD,IAAI,CAAC,GAAG,CAAC,4BAA4B,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAClE,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACf,OAAO,CACL,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ,CAC1F,CAAA;IACD,IAAI,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,kBAAkB,CAAC,CAAA;IACnE,CAAC;IAED,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,WAAW,CAAA;QAC3C,IAAI,CACF,kBAAkB,YAAY,CAAC,YAAY,CAAC,WAC1C,aAAa,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,EACvD,EAAE,CACH,CAAA;QACD,2EAA2E;QAC3E,uEAAuE;QACvE,6CAA6C;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,WAEjB,CAAA;QACb,MAAM,QAAQ,GAAG,OAAO,EAAE,gBAAgB,IAAI,CAAC,CAAA;QAC/C,MAAM,OAAO,GAAG,OAAO,EAAE,eAAe,IAAI,CAAC,CAAA;QAC7C,IAAI,QAAQ,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YAClC,IAAI,CACF,GAAG,CACD,kBAAkB,QAAQ,sEAAsE,CACjG,CACF,CAAA;QACH,CAAC;aAAM,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,aAAa,OAAO,OAAO,QAAQ,0BAA0B,CAAC,CAAC,CAAA;QAC1E,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAA;IAC5C,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,WAAW,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;QACzE,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,WAAW,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;QAC7D,IAAI,CAAC,aAAa,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC;IAED,MAAM,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,oBAAoB,EAAE;QACvD,aAAa,EAAE,WAAW;QAC1B,YAAY,EAAE,UAAU;QACxB,aAAa,EAAE,WAAW;QAC1B,eAAe,EAAE,aAAa;QAC9B,aAAa,EAAE,WAAW;QAC1B,eAAe,EAAE,aAAa;QAC9B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK;KACf,CAAC,CAAA;IAEF,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;AAC7B,CAAC"}
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,11 @@ export type InstallResult = {
14
14
  /** The daily background job writes here; launchd/systemd also point stdout+stderr
15
15
  * at it. Lives next to the session file so it honors HACKLAB_SESSION_PATH. */
16
16
  export declare function syncLogPath(): string;
17
+ export declare const SYNC_LOG_MAX_BYTES = 1000000;
18
+ export declare const SYNC_LOG_KEEP_LINES = 200;
19
+ /** Cut an oversized sync.log down to its last few hundred lines. Best-effort,
20
+ * and a no-op while the log is small — which is the normal case. */
21
+ export declare function trimSyncLog(maxBytes?: number, keepLines?: number): Promise<void>;
17
22
  /** Append a timestamped line to the background-sync log (best-effort). */
18
23
  export declare function appendSyncLog(line: string): Promise<void>;
19
24
  /** Record that the background sync can't continue (e.g. the session expired), so
@@ -29,27 +34,39 @@ export declare function launchdPlist(cmd: SyncCommand, time: {
29
34
  hour: number;
30
35
  minute: number;
31
36
  }, logPath: string): string;
37
+ /** The minutely tick. StartInterval (not a calendar interval) so launchd just
38
+ * re-runs it every 60s; a tick with nothing to report exits without a request. */
39
+ export declare function launchdTickPlist(cmd: SyncCommand, logPath: string): string;
32
40
  export declare function systemdService(cmd: SyncCommand): string;
33
41
  export declare function systemdTimer(): string;
42
+ export declare function systemdTickService(cmd: SyncCommand): string;
43
+ export declare function systemdTickTimer(): string;
34
44
  /** Where the Windows scheduled task's wrapper batch file lives. Next to the
35
45
  * session file so it honors HACKLAB_SESSION_PATH, mirroring syncLogPath(). */
36
46
  export declare function syncWrapperPath(): string;
47
+ /** The tick's own wrapper — same pattern, its own file so the two tasks can be
48
+ * created and deleted independently. */
49
+ export declare function tickWrapperPath(): string;
37
50
  /** The batch wrapper Task Scheduler runs. schtasks' `/TR` chokes on the nested
38
51
  * quotes a full `"node" "script" …` command needs, so we point the task at this
39
52
  * one-line .cmd instead and do the quoting here, where plain-batch rules apply.
40
53
  * Output is redirected to the same sync.log the job also appends to, so a crash
41
54
  * before the CLI's own logging still leaves a trace. */
42
55
  export declare function schtasksWrapper(cmd: SyncCommand, logPath: string): string;
56
+ /** Same wrapper for the minutely tick, logging to the same sync.log. */
57
+ export declare function schtasksTickWrapper(cmd: SyncCommand, logPath: string): string;
43
58
  /** Args for `schtasks /Create`. The `/TR` value is a single pre-quoted path;
44
59
  * spawn must run with windowsVerbatimArguments so Node doesn't re-escape it. */
45
60
  export declare function schtasksCreateArgs(wrapperPath: string, time: {
46
61
  hour: number;
47
62
  minute: number;
48
63
  }): string[];
64
+ /** Args for `schtasks /Create` for the tick: every minute, forever. */
65
+ export declare function schtasksTickCreateArgs(wrapperPath: string): string[];
49
66
  export declare function manualInstructions(cmd: SyncCommand): string;
50
- /** Install (or refresh) the daily background sync for the current OS. */
67
+ /** Install (or refresh) both background jobs (tick + daily) for the current OS. */
51
68
  export declare function installDailySync(): Promise<InstallResult>;
52
- /** Remove the daily background sync. Best-effort and never throws, so it's safe
53
- * to call from `logout` whether or not it was ever installed. */
69
+ /** Remove both background jobs. Best-effort and never throws, so it's safe to
70
+ * call from `logout` whether or not they were ever installed. */
54
71
  export declare function uninstallDailySync(): Promise<void>;
55
72
  //# sourceMappingURL=daily-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"daily-sync.d.ts","sourceRoot":"","sources":["../src/daily-sync.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1D,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,QAAQ,GAAG,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAgB5E;8EAC8E;AAC9E,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAMD,0EAA0E;AAC1E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D;AAED;wDACwD;AACxD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;AAED,qEAAqE;AACrE,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO7D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrD;AAED;2CAC2C;AAC3C,wBAAgB,kBAAkB,IAAI,WAAW,CAShD;AAQD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACtC,OAAO,EAAE,MAAM,GACd,MAAM,CA8BR;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CASvD;AAED,wBAAgB,YAAY,IAAI,MAAM,CAcrC;AAED;8EAC8E;AAC9E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;wDAIwD;AACxD,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;gFACgF;AAChF,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC,MAAM,EAAE,CAaV;AAID,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAO3D;AA4FD,yEAAyE;AACzE,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,CAAC,CAwB/D;AAED;iEACiE;AACjE,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA8BxD"}
1
+ {"version":3,"file":"daily-sync.d.ts","sourceRoot":"","sources":["../src/daily-sync.ts"],"names":[],"mappings":"AA+BA,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1D,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,QAAQ,GAAG,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAgB5E;8EAC8E;AAC9E,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAQD,eAAO,MAAM,kBAAkB,UAAY,CAAA;AAC3C,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAEtC;oEACoE;AACpE,wBAAsB,WAAW,CAC/B,QAAQ,SAAqB,EAC7B,SAAS,SAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,0EAA0E;AAC1E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D;AAED;wDACwD;AACxD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;AAED,qEAAqE;AACrE,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO7D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrD;AAED;2CAC2C;AAC3C,wBAAgB,kBAAkB,IAAI,WAAW,CAShD;AAyCD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACtC,OAAO,EAAE,MAAM,GACd,MAAM,CAcR;AAED;kFACkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAS1E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CASvD;AAED,wBAAgB,YAAY,IAAI,MAAM,CAcrC;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAQ3D;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAgBzC;AAED;8EAC8E;AAC9E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;wCACwC;AACxC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;wDAIwD;AACxD,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;gFACgF;AAChF,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC,MAAM,EAAE,CAaV;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAapE;AAID,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAS3D;AAiID,mFAAmF;AACnF,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,CAAC,CAwB/D;AAED;iEACiE;AACjE,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0BxD"}
@@ -1,17 +1,24 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { realpathSync } from 'node:fs';
3
- import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { appendFile, mkdir, readFile, rm, stat, writeFile, } from 'node:fs/promises';
4
4
  import { homedir, platform } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { getSessionPath } from './session.js';
7
- // Sets up (and tears down) an OS-native daily run of `hacklab sync --quiet`:
8
- // launchd on macOS, a systemd user timer on Linux, and printed instructions
9
- // when neither fits. The scheduled command is resolved to an absolute
10
- // `node <entry>` pair so a scheduler running with a bare PATH (cron/launchd/
11
- // systemd often have almost none) can launch it without a `hacklab` shim.
7
+ // Sets up (and tears down) the two OS-native background jobs: a `hacklab sync
8
+ // --tick` every minute (the incremental one — reads only what the AI tools
9
+ // appended since the last run) and the full `hacklab sync --quiet` once a day
10
+ // (the repair pass, which also re-bases the tick's state). launchd on macOS, a
11
+ // systemd user timer on Linux, Task Scheduler on Windows, and printed
12
+ // instructions when none of those fit. The scheduled command is resolved to an
13
+ // absolute `node <entry>` pair so a scheduler running with a bare PATH
14
+ // (cron/launchd/systemd often have almost none) can launch it without a
15
+ // `hacklab` shim.
12
16
  const LAUNCHD_LABEL = 'so.hacklab.sync';
17
+ const LAUNCHD_TICK_LABEL = 'so.hacklab.tick';
13
18
  const SYSTEMD_UNIT = 'hacklab-sync';
19
+ const SYSTEMD_TICK_UNIT = 'hacklab-tick';
14
20
  const SCHTASKS_TASK = 'hacklab-sync';
21
+ const SCHTASKS_TICK_TASK = 'hacklab-tick';
15
22
  /** Run a command without ever rejecting: resolves the exit code (127 when the
16
23
  * binary can't even be spawned, e.g. `systemctl` absent). */
17
24
  function run(cmd, args, opts = {}) {
@@ -29,6 +36,25 @@ export function syncLogPath() {
29
36
  function pausedMarkerPath() {
30
37
  return join(dirname(getSessionPath()), 'sync-paused');
31
38
  }
39
+ // The tick runs 1440 times a day, so the log has to be bounded even if every
40
+ // one of those runs has something to say.
41
+ export const SYNC_LOG_MAX_BYTES = 1_000_000;
42
+ export const SYNC_LOG_KEEP_LINES = 200;
43
+ /** Cut an oversized sync.log down to its last few hundred lines. Best-effort,
44
+ * and a no-op while the log is small — which is the normal case. */
45
+ export async function trimSyncLog(maxBytes = SYNC_LOG_MAX_BYTES, keepLines = SYNC_LOG_KEEP_LINES) {
46
+ try {
47
+ const path = syncLogPath();
48
+ if ((await stat(path)).size <= maxBytes)
49
+ return;
50
+ const lines = (await readFile(path, 'utf8')).split('\n');
51
+ const kept = lines.slice(-keepLines).join('\n');
52
+ await writeFile(path, kept.endsWith('\n') ? kept : `${kept}\n`, 'utf8');
53
+ }
54
+ catch {
55
+ // no log yet, or we can't rewrite it — either way, nothing to do
56
+ }
57
+ }
32
58
  /** Append a timestamped line to the background-sync log (best-effort). */
33
59
  export async function appendSyncLog(line) {
34
60
  try {
@@ -86,8 +112,9 @@ function xmlEscape(s) {
86
112
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
87
113
  }
88
114
  const pad2 = (n) => String(n).padStart(2, '0');
89
- export function launchdPlist(cmd, time, logPath) {
90
- const args = [cmd.node, cmd.script, 'sync', '--quiet']
115
+ /** The plist skeleton both jobs share — same command shape, different schedule. */
116
+ function launchdDoc(label, cmd, mode, schedule, logPath) {
117
+ const args = [cmd.node, cmd.script, 'sync', mode]
91
118
  .map((a) => ` <string>${xmlEscape(a)}</string>`)
92
119
  .join('\n');
93
120
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -95,18 +122,12 @@ export function launchdPlist(cmd, time, logPath) {
95
122
  <plist version="1.0">
96
123
  <dict>
97
124
  <key>Label</key>
98
- <string>${LAUNCHD_LABEL}</string>
125
+ <string>${label}</string>
99
126
  <key>ProgramArguments</key>
100
127
  <array>
101
128
  ${args}
102
129
  </array>
103
- <key>StartCalendarInterval</key>
104
- <dict>
105
- <key>Hour</key>
106
- <integer>${time.hour}</integer>
107
- <key>Minute</key>
108
- <integer>${time.minute}</integer>
109
- </dict>
130
+ ${schedule}
110
131
  <key>StandardOutPath</key>
111
132
  <string>${xmlEscape(logPath)}</string>
112
133
  <key>StandardErrorPath</key>
@@ -117,6 +138,21 @@ ${args}
117
138
  </plist>
118
139
  `;
119
140
  }
141
+ export function launchdPlist(cmd, time, logPath) {
142
+ return launchdDoc(LAUNCHD_LABEL, cmd, '--quiet', ` <key>StartCalendarInterval</key>
143
+ <dict>
144
+ <key>Hour</key>
145
+ <integer>${time.hour}</integer>
146
+ <key>Minute</key>
147
+ <integer>${time.minute}</integer>
148
+ </dict>`, logPath);
149
+ }
150
+ /** The minutely tick. StartInterval (not a calendar interval) so launchd just
151
+ * re-runs it every 60s; a tick with nothing to report exits without a request. */
152
+ export function launchdTickPlist(cmd, logPath) {
153
+ return launchdDoc(LAUNCHD_TICK_LABEL, cmd, '--tick', ` <key>StartInterval</key>
154
+ <integer>60</integer>`, logPath);
155
+ }
120
156
  export function systemdService(cmd) {
121
157
  // Quote node + script so paths with spaces survive systemd's parser.
122
158
  return `[Unit]
@@ -138,6 +174,32 @@ OnCalendar=daily
138
174
  Persistent=true
139
175
  RandomizedDelaySec=3600
140
176
 
177
+ [Install]
178
+ WantedBy=timers.target
179
+ `;
180
+ }
181
+ export function systemdTickService(cmd) {
182
+ return `[Unit]
183
+ Description=hacklab token tick
184
+
185
+ [Service]
186
+ Type=oneshot
187
+ ExecStart="${cmd.node}" "${cmd.script}" sync --tick
188
+ `;
189
+ }
190
+ export function systemdTickTimer() {
191
+ // OnUnitActiveSec re-arms a minute after each run finishes (so a slow tick
192
+ // can't stack up), OnBootSec gives the desktop a moment to settle first, and
193
+ // AccuracySec lets systemd batch the wakeup with others — a minutely timer is
194
+ // otherwise a needless drain on a laptop.
195
+ return `[Unit]
196
+ Description=hacklab token tick
197
+
198
+ [Timer]
199
+ OnBootSec=2min
200
+ OnUnitActiveSec=1min
201
+ AccuracySec=30s
202
+
141
203
  [Install]
142
204
  WantedBy=timers.target
143
205
  `;
@@ -147,6 +209,11 @@ WantedBy=timers.target
147
209
  export function syncWrapperPath() {
148
210
  return join(dirname(getSessionPath()), 'hacklab-sync.cmd');
149
211
  }
212
+ /** The tick's own wrapper — same pattern, its own file so the two tasks can be
213
+ * created and deleted independently. */
214
+ export function tickWrapperPath() {
215
+ return join(dirname(getSessionPath()), 'hacklab-tick.cmd');
216
+ }
150
217
  /** The batch wrapper Task Scheduler runs. schtasks' `/TR` chokes on the nested
151
218
  * quotes a full `"node" "script" …` command needs, so we point the task at this
152
219
  * one-line .cmd instead and do the quoting here, where plain-batch rules apply.
@@ -155,6 +222,10 @@ export function syncWrapperPath() {
155
222
  export function schtasksWrapper(cmd, logPath) {
156
223
  return `@echo off\r\n"${cmd.node}" "${cmd.script}" sync --quiet >> "${logPath}" 2>&1\r\n`;
157
224
  }
225
+ /** Same wrapper for the minutely tick, logging to the same sync.log. */
226
+ export function schtasksTickWrapper(cmd, logPath) {
227
+ return `@echo off\r\n"${cmd.node}" "${cmd.script}" sync --tick >> "${logPath}" 2>&1\r\n`;
228
+ }
158
229
  /** Args for `schtasks /Create`. The `/TR` value is a single pre-quoted path;
159
230
  * spawn must run with windowsVerbatimArguments so Node doesn't re-escape it. */
160
231
  export function schtasksCreateArgs(wrapperPath, time) {
@@ -171,35 +242,61 @@ export function schtasksCreateArgs(wrapperPath, time) {
171
242
  '/F',
172
243
  ];
173
244
  }
245
+ /** Args for `schtasks /Create` for the tick: every minute, forever. */
246
+ export function schtasksTickCreateArgs(wrapperPath) {
247
+ return [
248
+ '/Create',
249
+ '/SC',
250
+ 'MINUTE',
251
+ '/MO',
252
+ '1',
253
+ '/TN',
254
+ SCHTASKS_TICK_TASK,
255
+ '/TR',
256
+ `"${wrapperPath}"`,
257
+ '/F',
258
+ ];
259
+ }
174
260
  // Generic fallback text, reused for both "Linux without a systemd user manager"
175
261
  // and "OS we don't auto-schedule on" — so it must not name a specific mechanism.
176
262
  export function manualInstructions(cmd) {
177
263
  return [
178
- "Couldn't set up an automatic daily sync on this system.",
179
- 'To run it yourself, schedule this command to run once a day with cron, a',
180
- "systemd timer, or your init system's scheduler:",
264
+ "Couldn't set up the automatic background sync on this system.",
265
+ 'To run it yourself, schedule these two commands with cron, a systemd timer,',
266
+ "or your init system's scheduler — the tick every minute, the full sync once",
267
+ 'a day:',
268
+ ` "${cmd.node}" "${cmd.script}" sync --tick`,
181
269
  ` "${cmd.node}" "${cmd.script}" sync --quiet`,
182
270
  ].join('\n');
183
271
  }
272
+ function launchAgentPath(label) {
273
+ return join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
274
+ }
275
+ /** Write a plist and (re)load it. Unload first so a re-run replaces the agent
276
+ * rather than stacking a second copy. */
277
+ async function loadAgent(path, contents) {
278
+ await mkdir(dirname(path), { recursive: true });
279
+ await writeFile(path, contents, 'utf8');
280
+ await run('launchctl', ['unload', path]);
281
+ return run('launchctl', ['load', '-w', path]);
282
+ }
184
283
  async function installLaunchd(cmd, logPath) {
185
- const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
284
+ const plistPath = launchAgentPath(LAUNCHD_LABEL);
186
285
  // Randomize the time so installs don't all hit the API at the same instant.
187
286
  const time = {
188
287
  hour: Math.floor(Math.random() * 24),
189
288
  minute: Math.floor(Math.random() * 60),
190
289
  };
191
- await mkdir(dirname(plistPath), { recursive: true });
192
- await writeFile(plistPath, launchdPlist(cmd, time, logPath), 'utf8');
193
- // Reload idempotently. A plist in ~/Library/LaunchAgents also loads at next
194
- // login, so even if `load` fails now the agent activates then — report that
195
- // honestly rather than claiming it's already running.
196
- await run('launchctl', ['unload', plistPath]);
197
- const loaded = await run('launchctl', ['load', '-w', plistPath]);
290
+ const loaded = await loadAgent(plistPath, launchdPlist(cmd, time, logPath));
291
+ await loadAgent(launchAgentPath(LAUNCHD_TICK_LABEL), launchdTickPlist(cmd, logPath));
292
+ // A plist in ~/Library/LaunchAgents also loads at next login, so even if
293
+ // `load` fails now the agent activates then — report that honestly rather
294
+ // than claiming it's already running.
198
295
  return {
199
296
  ok: true,
200
297
  mechanism: 'launchd',
201
298
  detail: loaded === 0
202
- ? `daily around ${pad2(time.hour)}:${pad2(time.minute)} (${plistPath})`
299
+ ? `tick every minute, full sync daily around ${pad2(time.hour)}:${pad2(time.minute)} (${plistPath})`
203
300
  : `installed (${plistPath}) — activates at next login`,
204
301
  };
205
302
  }
@@ -219,18 +316,27 @@ async function installSystemd(cmd) {
219
316
  await mkdir(dir, { recursive: true });
220
317
  await writeFile(join(dir, `${SYSTEMD_UNIT}.service`), systemdService(cmd), 'utf8');
221
318
  await writeFile(join(dir, `${SYSTEMD_UNIT}.timer`), systemdTimer(), 'utf8');
319
+ await writeFile(join(dir, `${SYSTEMD_TICK_UNIT}.service`), systemdTickService(cmd), 'utf8');
320
+ await writeFile(join(dir, `${SYSTEMD_TICK_UNIT}.timer`), systemdTickTimer(), 'utf8');
222
321
  await run('systemctl', ['--user', 'daemon-reload']);
223
322
  await run('systemctl', ['--user', 'enable', '--now', `${SYSTEMD_UNIT}.timer`]);
323
+ await run('systemctl', [
324
+ '--user',
325
+ 'enable',
326
+ '--now',
327
+ `${SYSTEMD_TICK_UNIT}.timer`,
328
+ ]);
224
329
  return {
225
330
  ok: true,
226
331
  mechanism: 'systemd',
227
- detail: `systemd user timer (${SYSTEMD_UNIT}.timer); logs in journalctl --user -u ${SYSTEMD_UNIT}. To keep it running while logged out: loginctl enable-linger`,
332
+ detail: `systemd user timers (${SYSTEMD_TICK_UNIT}.timer every minute, ${SYSTEMD_UNIT}.timer daily); logs in journalctl --user -u ${SYSTEMD_UNIT}. To keep them running while logged out: loginctl enable-linger`,
228
333
  };
229
334
  }
230
335
  async function installSchtasks(cmd) {
231
336
  const wrapperPath = syncWrapperPath();
232
337
  await mkdir(dirname(wrapperPath), { recursive: true });
233
338
  await writeFile(wrapperPath, schtasksWrapper(cmd, syncLogPath()), 'utf8');
339
+ await writeFile(tickWrapperPath(), schtasksTickWrapper(cmd, syncLogPath()), 'utf8');
234
340
  // Randomize the time so installs don't all hit the API at the same instant.
235
341
  const time = {
236
342
  hour: Math.floor(Math.random() * 24),
@@ -249,13 +355,16 @@ async function installSchtasks(cmd) {
249
355
  instructions: manualInstructions(cmd),
250
356
  };
251
357
  }
358
+ // The daily job is the one that must exist; a refused minutely task (some
359
+ // policies cap task frequency) costs freshness, not the streak.
360
+ const tick = await run('schtasks', schtasksTickCreateArgs(tickWrapperPath()), { windowsVerbatimArguments: true });
252
361
  return {
253
362
  ok: true,
254
363
  mechanism: 'schtasks',
255
- detail: `daily around ${pad2(time.hour)}:${pad2(time.minute)} (Task Scheduler task "${SCHTASKS_TASK}")`,
364
+ detail: `${tick === 0 ? 'tick every minute, ' : ''}full sync daily around ${pad2(time.hour)}:${pad2(time.minute)} (Task Scheduler task "${SCHTASKS_TASK}")`,
256
365
  };
257
366
  }
258
- /** Install (or refresh) the daily background sync for the current OS. */
367
+ /** Install (or refresh) both background jobs (tick + daily) for the current OS. */
259
368
  export async function installDailySync() {
260
369
  const cmd = resolveSyncCommand();
261
370
  try {
@@ -283,31 +392,32 @@ export async function installDailySync() {
283
392
  };
284
393
  }
285
394
  }
286
- /** Remove the daily background sync. Best-effort and never throws, so it's safe
287
- * to call from `logout` whether or not it was ever installed. */
395
+ /** Remove both background jobs. Best-effort and never throws, so it's safe to
396
+ * call from `logout` whether or not they were ever installed. */
288
397
  export async function uninstallDailySync() {
289
398
  const os = platform();
290
399
  try {
291
400
  if (os === 'darwin') {
292
- const plistPath = join(homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
293
- await run('launchctl', ['unload', plistPath]);
294
- await rm(plistPath, { force: true });
401
+ for (const label of [LAUNCHD_LABEL, LAUNCHD_TICK_LABEL]) {
402
+ const plistPath = launchAgentPath(label);
403
+ await run('launchctl', ['unload', plistPath]);
404
+ await rm(plistPath, { force: true });
405
+ }
295
406
  }
296
407
  else if (os === 'linux') {
297
- await run('systemctl', [
298
- '--user',
299
- 'disable',
300
- '--now',
301
- `${SYSTEMD_UNIT}.timer`,
302
- ]);
303
408
  const dir = join(homedir(), '.config', 'systemd', 'user');
304
- await rm(join(dir, `${SYSTEMD_UNIT}.timer`), { force: true });
305
- await rm(join(dir, `${SYSTEMD_UNIT}.service`), { force: true });
409
+ for (const unit of [SYSTEMD_UNIT, SYSTEMD_TICK_UNIT]) {
410
+ await run('systemctl', ['--user', 'disable', '--now', `${unit}.timer`]);
411
+ await rm(join(dir, `${unit}.timer`), { force: true });
412
+ await rm(join(dir, `${unit}.service`), { force: true });
413
+ }
306
414
  await run('systemctl', ['--user', 'daemon-reload']);
307
415
  }
308
416
  else if (os === 'win32') {
309
417
  await run('schtasks', ['/Delete', '/TN', SCHTASKS_TASK, '/F']);
418
+ await run('schtasks', ['/Delete', '/TN', SCHTASKS_TICK_TASK, '/F']);
310
419
  await rm(syncWrapperPath(), { force: true });
420
+ await rm(tickWrapperPath(), { force: true });
311
421
  }
312
422
  }
313
423
  catch {