jowork 0.2.5 → 0.3.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.
Files changed (37) hide show
  1. package/dist/{chunk-ROIINI33.js → chunk-4PIT2GZ4.js} +13 -1
  2. package/dist/{chunk-XLYRHKG6.js → chunk-54SD5GBF.js} +1 -1
  3. package/dist/chunk-63AMINQC.js +156 -0
  4. package/dist/{chunk-XAEGXSEO.js → chunk-74AHY7X6.js} +4 -0
  5. package/dist/{chunk-7U3SXINY.js → chunk-ATAUWJYD.js} +320 -50
  6. package/dist/chunk-DQW74UCN.js +671 -0
  7. package/dist/chunk-EYP6WMFF.js +153 -0
  8. package/dist/{chunk-JSTXMDXI.js → chunk-FCFZCZHR.js} +1 -1
  9. package/dist/chunk-FX6Z3QHV.js +34 -0
  10. package/dist/chunk-HENAABEL.js +419 -0
  11. package/dist/chunk-OXWWOKC7.js +201 -0
  12. package/dist/{chunk-HUHDL7WV.js → chunk-QGHJ45PL.js} +276 -199
  13. package/dist/chunk-RO3KK5RC.js +132 -0
  14. package/dist/{chunk-JE6TOU7W.js → chunk-TFMF3EXE.js} +2 -7
  15. package/dist/{chunk-TN327MDF.js → chunk-VX662YLA.js} +3 -3
  16. package/dist/cli.js +308 -135
  17. package/dist/{config-AI6UIJJN.js → config-FH2XLN7A.js} +2 -2
  18. package/dist/content-reader-VPGTR2SF.js +10 -0
  19. package/dist/context-ZNI3WOB7.js +10 -0
  20. package/dist/{credential-store-ZRZCSRPC.js → credential-store-OS5ZY4OW.js} +2 -2
  21. package/dist/{feishu-A6YVFKEN.js → feishu-XW5T6ER2.js} +8 -3
  22. package/dist/{git-manager-N35XSG4Y.js → git-manager-RVWV2GSV.js} +2 -1
  23. package/dist/github-PQKAYTLO.js +11 -0
  24. package/dist/{paths-JXOMBYIT.js → paths-FFRET6F7.js} +7 -3
  25. package/dist/{server-5GVWN2NB.js → server-WEADPUST.js} +59 -66
  26. package/dist/{setup-SYBQIL2O.js → setup-S2S2CHB2.js} +76 -30
  27. package/dist/sync-SRLFR5NA.js +21 -0
  28. package/dist/transport.js +6 -4
  29. package/package.json +1 -1
  30. package/src/dashboard/public/app.js +34 -8
  31. package/src/dashboard/public/style.css +14 -0
  32. package/dist/chunk-L5ZR7TSK.js +0 -82
  33. package/dist/chunk-LS2AJM5A.js +0 -163
  34. package/dist/chunk-QMOFQX7X.js +0 -612
  35. package/dist/chunk-YJWTKFWX.js +0 -451
  36. package/dist/github-SHWUFNYB.js +0 -10
  37. package/dist/sync-KDSPGY4A.js +0 -18
@@ -124,19 +124,39 @@ const App = {
124
124
  }, 4000);
125
125
  }
126
126
 
127
- // WebSocket
127
+ // WebSocket with heartbeat + stable reconnect
128
128
  let ws = null;
129
129
  let reconnectTimer = null;
130
+ let reconnectDelay = 1000;
131
+ let disconnectGraceTimer = null;
130
132
 
131
133
  function connectWs() {
134
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
135
+ wsStatus.value = 'connecting';
132
136
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
133
137
  ws = new WebSocket(`${protocol}//${location.host}/api/ws`);
134
- ws.onopen = () => { wsStatus.value = 'open'; };
138
+
139
+ ws.onopen = () => {
140
+ if (disconnectGraceTimer) { clearTimeout(disconnectGraceTimer); disconnectGraceTimer = null; }
141
+ wsStatus.value = 'open';
142
+ reconnectDelay = 1000; // Reset backoff on success
143
+ };
144
+
135
145
  ws.onclose = () => {
136
- wsStatus.value = 'closed';
137
- reconnectTimer = setTimeout(connectWs, 3000);
146
+ // Grace period: don't show "Reconnecting" for brief disconnects
147
+ if (!disconnectGraceTimer) {
148
+ disconnectGraceTimer = setTimeout(() => {
149
+ if (wsStatus.value !== 'open') wsStatus.value = 'closed';
150
+ disconnectGraceTimer = null;
151
+ }, 2000);
152
+ }
153
+ // Exponential backoff: 1s → 2s → 4s → 8s (max)
154
+ reconnectTimer = setTimeout(connectWs, reconnectDelay);
155
+ reconnectDelay = Math.min(reconnectDelay * 2, 8000);
138
156
  };
157
+
139
158
  ws.onerror = () => { ws.close(); };
159
+
140
160
  ws.onmessage = (event) => {
141
161
  try {
142
162
  const msg = JSON.parse(event.data);
@@ -148,7 +168,6 @@ const App = {
148
168
  uploadProgress.value = Math.round((indexed / total) * 100);
149
169
  }
150
170
  if (done) {
151
- // Keep at 100% briefly, then hide
152
171
  uploadProgress.value = 100;
153
172
  setTimeout(() => {
154
173
  uploadActive.value = false;
@@ -360,6 +379,12 @@ const App = {
360
379
  }));
361
380
  });
362
381
 
382
+ /** Returns true if lastSync is more than 1 hour ago. */
383
+ function isStale(timestamp) {
384
+ if (!timestamp) return false;
385
+ return (Date.now() - timestamp) > 60 * 60 * 1000;
386
+ }
387
+
363
388
  function goalProgress(goal) {
364
389
  if (!goal.signals || goal.signals.length === 0) return 0;
365
390
  let met = 0;
@@ -408,7 +433,7 @@ const App = {
408
433
  uploadProgress, uploadActive, toasts,
409
434
  addContext, removeContext, triggerSync, focusSession, toggleTheme,
410
435
  onDragOver, onDragLeave, onDrop,
411
- goalProgress, goalMeasureCounts,
436
+ goalProgress, goalMeasureCounts, isStale,
412
437
  formatTimeAgo, formatDuration,
413
438
  };
414
439
  },
@@ -433,8 +458,9 @@ const App = {
433
458
  <span class="source-name">{{ item.name }}</span>
434
459
  <span class="source-count">{{ item.count }}</span>
435
460
  </div>
436
- <div v-if="item.lastSync" class="source-time">
461
+ <div v-if="item.lastSync" class="source-time" :class="{ 'stale-warning': isStale(item.lastSync) }">
437
462
  {{ formatTimeAgo(item.lastSync) }}
463
+ <span v-if="isStale(item.lastSync)" class="stale-icon" title="Data may be outdated. Click Sync Now to refresh.">&#9888;</span>
438
464
  </div>
439
465
  </div>
440
466
 
@@ -464,7 +490,7 @@ const App = {
464
490
  <div class="sidebar-footer">
465
491
  <div class="ws-status" style="margin-bottom: 8px;">
466
492
  <span class="ws-dot" :class="wsStatus"></span>
467
- <span>{{ wsStatus === 'open' ? 'Connected' : 'Reconnecting...' }}</span>
493
+ <span>{{ wsStatus === 'open' ? 'Connected' : wsStatus === 'connecting' ? 'Connecting...' : 'Reconnecting...' }}</span>
468
494
  </div>
469
495
  <button class="theme-toggle" @click="toggleTheme">
470
496
  {{ theme === 'dark' ? 'Light mode' : 'Dark mode' }}
@@ -151,6 +151,19 @@ body {
151
151
  padding-left: 24px;
152
152
  }
153
153
 
154
+ .source-time.stale-warning {
155
+ color: #d97706;
156
+ }
157
+
158
+ .stale-icon {
159
+ margin-left: 4px;
160
+ font-size: 12px;
161
+ }
162
+
163
+ [data-theme="dark"] .source-time.stale-warning {
164
+ color: #fbbf24;
165
+ }
166
+
154
167
  /* Sidebar buttons */
155
168
  .sidebar-btn {
156
169
  display: flex;
@@ -613,6 +626,7 @@ body {
613
626
  }
614
627
 
615
628
  .ws-dot.open { background: var(--success); }
629
+ .ws-dot.connecting { background: var(--warning); animation: pulse 1.5s ease-in-out infinite; }
616
630
  .ws-dot.closed { background: var(--error); }
617
631
 
618
632
  /* Responsive */
@@ -1,82 +0,0 @@
1
- import {
2
- logInfo
3
- } from "./chunk-MYDK7MWB.js";
4
-
5
- // src/sync/git-manager.ts
6
- import simpleGit from "simple-git";
7
- import { existsSync, writeFileSync } from "fs";
8
- import { join } from "path";
9
- var GitManager = class {
10
- git;
11
- repoDir;
12
- constructor(repoDir) {
13
- this.repoDir = repoDir;
14
- this.git = simpleGit(repoDir);
15
- }
16
- /** Initialize git repo if not already initialized */
17
- async init() {
18
- const gitDir = join(this.repoDir, ".git");
19
- if (existsSync(gitDir)) return;
20
- await this.git.init();
21
- const gitignore = [
22
- "# JoWork \u2014 auto-generated",
23
- "*.db",
24
- "*.db-wal",
25
- "*.db-shm",
26
- ".DS_Store",
27
- "Thumbs.db",
28
- "*.key",
29
- "*.pem",
30
- "*.env",
31
- "credentials/",
32
- ""
33
- ].join("\n");
34
- writeFileSync(join(this.repoDir, ".gitignore"), gitignore);
35
- await this.git.add("-A");
36
- await this.git.commit("init: jowork data repo");
37
- logInfo("git", "Initialized data repo");
38
- }
39
- /** Commit all changes after a sync cycle */
40
- async commitSync(summary) {
41
- await this.git.add("-A");
42
- const status = await this.git.status();
43
- if (status.staged.length === 0 && status.created.length === 0 && status.modified.length === 0 && status.deleted.length === 0) {
44
- return null;
45
- }
46
- const lines = [`sync: ${summary.timestamp}`, ""];
47
- for (const s of summary.sources) {
48
- if (s.newObjects > 0) {
49
- lines.push(`${s.source}: +${s.newObjects} ${s.label ?? "objects"}`);
50
- }
51
- }
52
- if (lines.length === 2) lines.push("(no new data)");
53
- const result = await this.git.commit(lines.join("\n"));
54
- const sha = result.commit;
55
- logInfo("git", `Committed sync: ${sha}`, {
56
- files: status.staged.length + status.created.length
57
- });
58
- return sha;
59
- }
60
- /** Get recent sync log entries */
61
- async getLog(limit = 20) {
62
- const log = await this.git.log({ maxCount: limit });
63
- return log.all.map((entry) => ({
64
- hash: entry.hash.slice(0, 7),
65
- date: entry.date,
66
- message: entry.message.split("\n")[0]
67
- }));
68
- }
69
- /** Get current status (changed files) */
70
- async getStatus() {
71
- const status = await this.git.status();
72
- return {
73
- modified: status.modified,
74
- created: status.created,
75
- deleted: status.deleted
76
- };
77
- }
78
- };
79
-
80
- export {
81
- GitManager
82
- };
@@ -1,163 +0,0 @@
1
- import {
2
- contentHash,
3
- formatIssue,
4
- formatPullRequest
5
- } from "./chunk-QMOFQX7X.js";
6
- import {
7
- createId
8
- } from "./chunk-JE6TOU7W.js";
9
- import {
10
- logError,
11
- logInfo
12
- } from "./chunk-MYDK7MWB.js";
13
-
14
- // src/sync/github.ts
15
- var defaultLogger = {
16
- info: (msg, ctx) => logInfo("sync", msg, ctx),
17
- warn: (msg, ctx) => logError("sync", msg, ctx),
18
- error: (msg, ctx) => logError("sync", msg, ctx)
19
- };
20
- var GITHUB_API = "https://api.github.com";
21
- var MAX_REPOS = 30;
22
- var RATE_LIMIT_DELAY_MS = 200;
23
- async function syncGitHub(sqlite, data, logger = defaultLogger, fileWriter) {
24
- const token = data.token;
25
- if (!token) throw new Error("Missing GitHub token");
26
- const headers = {
27
- Authorization: `Bearer ${token}`,
28
- Accept: "application/vnd.github.v3+json",
29
- "User-Agent": "jowork/0.1.0"
30
- };
31
- let repos = 0;
32
- let issues = 0;
33
- let prs = 0;
34
- let newObjects = 0;
35
- const reposRes = await fetch(
36
- `${GITHUB_API}/user/repos?per_page=30&sort=pushed&affiliation=owner,collaborator`,
37
- { headers }
38
- );
39
- if (!reposRes.ok) {
40
- if (reposRes.status === 401) throw new Error("GitHub token expired or invalid");
41
- if (reposRes.status === 403) throw new Error("GitHub rate limit exceeded");
42
- throw new Error(`GitHub API error: ${reposRes.status}`);
43
- }
44
- const repoList = await reposRes.json();
45
- repos = repoList.length;
46
- logger.info(`Found ${repos} repos`);
47
- const checkExists = sqlite.prepare("SELECT id FROM objects WHERE uri = ?");
48
- const insertObj = sqlite.prepare(`
49
- INSERT INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
50
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
51
- `);
52
- const insertBody = sqlite.prepare(`
53
- INSERT OR REPLACE INTO object_bodies (object_id, content, content_type, fetched_at)
54
- VALUES (?, ?, ?, ?)
55
- `);
56
- const insertFts = sqlite.prepare(`
57
- INSERT INTO objects_fts(rowid, title, summary, tags, source, source_type, body_excerpt)
58
- VALUES (?, ?, ?, ?, ?, ?, ?)
59
- `);
60
- const getRowid = sqlite.prepare("SELECT rowid FROM objects WHERE id = ?");
61
- for (const repo of repoList.slice(0, MAX_REPOS)) {
62
- try {
63
- const issuesRes = await fetch(
64
- `${GITHUB_API}/repos/${repo.full_name}/issues?state=all&per_page=30&sort=updated`,
65
- { headers }
66
- );
67
- if (!issuesRes.ok) {
68
- logger.warn(`Failed to fetch issues for ${repo.full_name}: ${issuesRes.status}`);
69
- continue;
70
- }
71
- const issueList = await issuesRes.json();
72
- const batchInsert = sqlite.transaction((items) => {
73
- for (const item of items) {
74
- const isPR = !!item.pull_request;
75
- const sourceType = isPR ? "pull_request" : "issue";
76
- const uri = `github://${repo.full_name}/${sourceType}/${item.number}`;
77
- if (checkExists.get(uri)) continue;
78
- const now = Date.now();
79
- const id = createId("obj");
80
- const title = `${repo.full_name}#${item.number}: ${item.title}`;
81
- const summary = item.body ? item.body.length > 200 ? item.body.slice(0, 200) + "..." : item.body : item.title;
82
- const tags = JSON.stringify([
83
- "github",
84
- sourceType,
85
- item.state,
86
- ...item.labels.map((l) => l.name)
87
- ]);
88
- const body = formatIssueBody(item, repo.full_name);
89
- const hash = contentHash(title + (item.body ?? ""));
90
- insertObj.run(
91
- id,
92
- "github",
93
- sourceType,
94
- uri,
95
- title,
96
- summary,
97
- tags,
98
- hash,
99
- now,
100
- new Date(item.created_at).getTime()
101
- );
102
- insertBody.run(id, body, "text/plain", now);
103
- try {
104
- const rowid = getRowid.get(id);
105
- if (rowid) {
106
- const excerpt = body.length > 500 ? body.slice(0, 500) : body;
107
- insertFts.run(rowid.rowid, title, summary ?? "", tags, "github", sourceType, excerpt);
108
- }
109
- } catch {
110
- }
111
- if (fileWriter) {
112
- try {
113
- const labelNames = item.labels.map((l) => l.name);
114
- const formatter = isPR ? formatPullRequest : formatIssue;
115
- const fileContent = formatter({
116
- source: "github",
117
- repo: repo.full_name,
118
- number: item.number,
119
- title: item.title,
120
- state: item.state,
121
- author: item.user?.login ?? "unknown",
122
- labels: labelNames,
123
- created: item.created_at,
124
- uri,
125
- body: item.body ?? ""
126
- });
127
- const filePath = fileWriter.writeObject("github", sourceType, {
128
- id,
129
- repo: repo.full_name,
130
- number: item.number,
131
- title: item.title
132
- }, fileContent);
133
- sqlite.prepare("UPDATE objects SET file_path = ? WHERE id = ?").run(filePath, id);
134
- } catch {
135
- }
136
- }
137
- newObjects++;
138
- if (isPR) prs++;
139
- else issues++;
140
- }
141
- });
142
- batchInsert(issueList);
143
- } catch (err) {
144
- logger.warn(`Error syncing ${repo.full_name}: ${err}`);
145
- }
146
- await new Promise((r) => setTimeout(r, RATE_LIMIT_DELAY_MS));
147
- }
148
- logger.info("GitHub sync complete", { repos, issues, prs, newObjects });
149
- return { repos, issues, prs, newObjects };
150
- }
151
- function formatIssueBody(item, repo) {
152
- return [
153
- `${repo}#${item.number}: ${item.title}`,
154
- `State: ${item.state} | Author: ${item.user?.login ?? "unknown"} | Created: ${item.created_at}`,
155
- `Labels: ${item.labels.map((l) => l.name).join(", ") || "none"}`,
156
- "",
157
- item.body ?? "(no description)"
158
- ].join("\n");
159
- }
160
-
161
- export {
162
- syncGitHub
163
- };