mulmoclaude 0.1.2 → 0.4.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/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/config.ts +12 -12
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +7 -6
- package/server/agent/prompt.ts +195 -29
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +10 -10
- package/server/agent/stream.ts +4 -4
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +22 -21
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +14 -12
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +19 -17
- package/server/api/routes/sessions.ts +26 -26
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +36 -22
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- package/server/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +59 -65
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +16 -16
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +19 -8
- package/server/utils/files/journal-io.ts +4 -4
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +12 -12
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +7 -7
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +16 -16
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +20 -20
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +10 -10
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +12 -12
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +8 -13
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +15 -13
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +17 -10
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +315 -141
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +67 -33
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +17 -14
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +6 -3
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +38 -34
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +49 -36
- package/src/components/SettingsModal.vue +24 -22
- package/src/components/SettingsReferenceDirsTab.vue +39 -34
- package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +7 -4
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +19 -14
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +17 -12
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +10 -7
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +20 -34
- package/src/config/roles.ts +2 -2
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +5 -5
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +29 -25
- package/src/plugins/manageSource/Preview.vue +2 -2
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +26 -36
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +40 -30
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +13 -10
- package/src/plugins/scheduler/TasksTab.vue +57 -28
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +29 -9
- package/src/plugins/todo/Preview.vue +13 -8
- package/src/plugins/todo/View.vue +38 -24
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +10 -7
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +46 -28
- package/src/router/index.ts +41 -26
- package/src/types/session.ts +4 -3
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +22 -3
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-KNLBjwuh.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -65,27 +65,27 @@ export function parseState(raw: unknown): JournalState {
|
|
|
65
65
|
// Version mismatch → throw it all out. Cheap to rebuild.
|
|
66
66
|
if (obj.version !== JOURNAL_STATE_VERSION) return defaultState();
|
|
67
67
|
|
|
68
|
-
const
|
|
68
|
+
const fallback = defaultState();
|
|
69
69
|
return {
|
|
70
70
|
version: JOURNAL_STATE_VERSION,
|
|
71
71
|
lastDailyRunAt: typeof obj.lastDailyRunAt === "string" ? obj.lastDailyRunAt : null,
|
|
72
72
|
lastOptimizationRunAt: typeof obj.lastOptimizationRunAt === "string" ? obj.lastOptimizationRunAt : null,
|
|
73
|
-
dailyIntervalHours: typeof obj.dailyIntervalHours === "number" && obj.dailyIntervalHours > 0 ? obj.dailyIntervalHours :
|
|
73
|
+
dailyIntervalHours: typeof obj.dailyIntervalHours === "number" && obj.dailyIntervalHours > 0 ? obj.dailyIntervalHours : fallback.dailyIntervalHours,
|
|
74
74
|
optimizationIntervalDays:
|
|
75
|
-
typeof obj.optimizationIntervalDays === "number" && obj.optimizationIntervalDays > 0 ? obj.optimizationIntervalDays :
|
|
75
|
+
typeof obj.optimizationIntervalDays === "number" && obj.optimizationIntervalDays > 0 ? obj.optimizationIntervalDays : fallback.optimizationIntervalDays,
|
|
76
76
|
processedSessions: parseProcessedSessions(obj.processedSessions),
|
|
77
|
-
knownTopics: Array.isArray(obj.knownTopics) ? obj.knownTopics.filter((
|
|
77
|
+
knownTopics: Array.isArray(obj.knownTopics) ? obj.knownTopics.filter((topic): topic is string => typeof topic === "string") : [],
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function parseProcessedSessions(raw: unknown): Record<string, ProcessedSessionRecord> {
|
|
82
82
|
if (!isRecord(raw)) return {};
|
|
83
83
|
const out: Record<string, ProcessedSessionRecord> = {};
|
|
84
|
-
for (const [
|
|
84
|
+
for (const [sessionId, rec] of Object.entries(raw as Record<string, unknown>)) {
|
|
85
85
|
if (!isRecord(rec)) continue;
|
|
86
86
|
const mtime = (rec as Record<string, unknown>).lastMtimeMs;
|
|
87
87
|
if (typeof mtime === "number" && mtime >= 0) {
|
|
88
|
-
out[
|
|
88
|
+
out[sessionId] = { lastMtimeMs: mtime };
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
return out;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// `WORKSPACE_DIRS` record below. The absolute path is derived
|
|
22
22
|
// automatically via `WORKSPACE_PATHS`.
|
|
23
23
|
|
|
24
|
-
import
|
|
24
|
+
import { homedir } from "os";
|
|
25
25
|
import path from "path";
|
|
26
26
|
|
|
27
27
|
// Workspace root. Hard-coded to `~/mulmoclaude` — there is no
|
|
@@ -29,7 +29,7 @@ import path from "path";
|
|
|
29
29
|
// requires a code edit or a symlink. Re-exported by
|
|
30
30
|
// `server/workspace.ts` for backwards compatibility of existing
|
|
31
31
|
// callers that `import { workspacePath } from "./workspace.js"`.
|
|
32
|
-
export const workspacePath = path.join(
|
|
32
|
+
export const workspacePath = path.join(homedir(), "mulmoclaude");
|
|
33
33
|
|
|
34
34
|
// Workspace-relative paths. Keys are the stable code-side identifiers
|
|
35
35
|
// (e.g. `markdowns` — unchanged for call-site compatibility); values
|
|
@@ -90,7 +90,7 @@ import { WORKSPACE_FILES } from "../../src/config/workspacePaths.js";
|
|
|
90
90
|
export { WORKSPACE_FILES };
|
|
91
91
|
|
|
92
92
|
// Absolute paths, built once at module load from `workspacePath`.
|
|
93
|
-
// The `workspacePath` const is itself fixed (reads `
|
|
93
|
+
// The `workspacePath` const is itself fixed (reads `homedir()`
|
|
94
94
|
// at process start — no env override, see `server/workspace.ts`),
|
|
95
95
|
// so freezing these paths is safe.
|
|
96
96
|
export const WORKSPACE_PATHS = {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createHash } from "crypto";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import
|
|
11
|
+
import { homedir } from "os";
|
|
12
12
|
import { log } from "../system/logger/index.js";
|
|
13
13
|
import { readReferenceDirsJson, writeReferenceDirsJson, isExistingDirectory } from "../utils/files/reference-dirs-io.js";
|
|
14
14
|
import { isRecord } from "../utils/types.js";
|
|
@@ -39,11 +39,11 @@ const CONTROL_CHAR_RE_G = /[\x00-\x1f]/g;
|
|
|
39
39
|
|
|
40
40
|
// ── Validation ──────────────────────────────────────────────────
|
|
41
41
|
|
|
42
|
-
function expandHome(
|
|
43
|
-
if (
|
|
44
|
-
return path.join(
|
|
42
|
+
function expandHome(inputPath: string): string {
|
|
43
|
+
if (inputPath.startsWith("~/")) {
|
|
44
|
+
return path.join(homedir(), inputPath.slice(2));
|
|
45
45
|
}
|
|
46
|
-
return
|
|
46
|
+
return inputPath;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function isSensitivePath(absPath: string): boolean {
|
|
@@ -52,15 +52,15 @@ function isSensitivePath(absPath: string): boolean {
|
|
|
52
52
|
// Reject filesystem root
|
|
53
53
|
if (normalized === path.parse(normalized).root) return true;
|
|
54
54
|
|
|
55
|
-
const home =
|
|
55
|
+
const home = homedir();
|
|
56
56
|
|
|
57
57
|
// Block $HOME itself (transitively exposes .ssh etc.)
|
|
58
58
|
if (normalized === home) return true;
|
|
59
59
|
|
|
60
60
|
// Block home-relative sensitive dirs
|
|
61
61
|
if (
|
|
62
|
-
HOME_RELATIVE_BLOCKED.some((
|
|
63
|
-
const full = path.join(home,
|
|
62
|
+
HOME_RELATIVE_BLOCKED.some((blockedPath) => {
|
|
63
|
+
const full = path.join(home, blockedPath);
|
|
64
64
|
return normalized === full || normalized.startsWith(full + path.sep);
|
|
65
65
|
})
|
|
66
66
|
) {
|
|
@@ -68,7 +68,7 @@ function isSensitivePath(absPath: string): boolean {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Block system directories
|
|
71
|
-
return SYSTEM_BLOCKED_PREFIXES.some((
|
|
71
|
+
return SYSTEM_BLOCKED_PREFIXES.some((blockedPrefix) => normalized === blockedPrefix || normalized.startsWith(blockedPrefix + path.sep));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function sanitizeLabel(raw: string): string {
|
|
@@ -76,8 +76,8 @@ function sanitizeLabel(raw: string): string {
|
|
|
76
76
|
return raw.replace(CONTROL_CHAR_RE_G, " ").trim().slice(0, MAX_LABEL_LENGTH);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function hasTraversalSegment(
|
|
80
|
-
return
|
|
79
|
+
function hasTraversalSegment(inputPath: string): boolean {
|
|
80
|
+
return inputPath.split(path.sep).some((segment) => segment === "..");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function validateEntry(raw: unknown): ReferenceDirEntry | null {
|
|
@@ -117,11 +117,11 @@ export function loadReferenceDirs(root?: string): ReferenceDirEntry[] {
|
|
|
117
117
|
const entries = parsed
|
|
118
118
|
.slice(0, MAX_ENTRIES)
|
|
119
119
|
.map(validateEntry)
|
|
120
|
-
.filter((
|
|
121
|
-
if (!
|
|
120
|
+
.filter((entry): entry is ReferenceDirEntry => {
|
|
121
|
+
if (!entry) return false;
|
|
122
122
|
// Deduplicate labels — first entry wins
|
|
123
|
-
if (seenLabels.has(
|
|
124
|
-
seenLabels.add(
|
|
123
|
+
if (seenLabels.has(entry.label)) return false;
|
|
124
|
+
seenLabels.add(entry.label);
|
|
125
125
|
return true;
|
|
126
126
|
});
|
|
127
127
|
|
|
@@ -155,8 +155,8 @@ export function validateReferenceDirs(raw: unknown): { entries: ReferenceDirEntr
|
|
|
155
155
|
if (entry) {
|
|
156
156
|
entries.push(entry);
|
|
157
157
|
} else {
|
|
158
|
-
const
|
|
159
|
-
errors.push(`entry ${i}: invalid or blocked path "${
|
|
158
|
+
const hostPath = isRecord(item) ? String((item as Record<string, unknown>).hostPath ?? "") : "";
|
|
159
|
+
errors.push(`entry ${i}: invalid or blocked path "${hostPath}"`);
|
|
160
160
|
}
|
|
161
161
|
});
|
|
162
162
|
if (errors.length > 0) {
|
|
@@ -233,9 +233,9 @@ export function buildReferenceDirsPrompt(entries: readonly ReferenceDirEntry[],
|
|
|
233
233
|
"",
|
|
234
234
|
];
|
|
235
235
|
|
|
236
|
-
for (const
|
|
237
|
-
const mountPath = useDocker ? containerPath(
|
|
238
|
-
lines.push(`- \`${mountPath}\` — ${
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
const mountPath = useDocker ? containerPath(entry) : entry.hostPath;
|
|
238
|
+
lines.push(`- \`${mountPath}\` — ${entry.label}`);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
if (!useDocker) {
|
|
@@ -14,10 +14,10 @@ function withSwitchRole(role: Role): Role {
|
|
|
14
14
|
|
|
15
15
|
export function loadCustomRoles(): Role[] {
|
|
16
16
|
return readdirUnderSync(workspacePath, WORKSPACE_DIRS.roles)
|
|
17
|
-
.filter((
|
|
18
|
-
.flatMap((
|
|
17
|
+
.filter((fileName) => fileName.endsWith(".json"))
|
|
18
|
+
.flatMap((fileName) => {
|
|
19
19
|
try {
|
|
20
|
-
const raw = readTextUnderSync(workspacePath, path.posix.join(WORKSPACE_DIRS.roles,
|
|
20
|
+
const raw = readTextUnderSync(workspacePath, path.posix.join(WORKSPACE_DIRS.roles, fileName));
|
|
21
21
|
if (!raw) return [];
|
|
22
22
|
return [withSwitchRole(RoleSchema.parse(JSON.parse(raw)))];
|
|
23
23
|
} catch {
|
|
@@ -28,10 +28,10 @@ export function loadCustomRoles(): Role[] {
|
|
|
28
28
|
|
|
29
29
|
export function loadAllRoles(): Role[] {
|
|
30
30
|
const custom = loadCustomRoles();
|
|
31
|
-
const builtIn = BUILTIN_ROLES.filter((
|
|
31
|
+
const builtIn = BUILTIN_ROLES.filter((role) => !custom.find((customRole) => customRole.id === role.id));
|
|
32
32
|
return [...builtIn, ...custom];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function getRole(
|
|
36
|
-
return loadAllRoles().find((
|
|
35
|
+
export function getRole(roleId: string): Role {
|
|
36
|
+
return loadAllRoles().find((role) => role.id === roleId) ?? BUILTIN_ROLES[0];
|
|
37
37
|
}
|
|
@@ -90,7 +90,7 @@ export async function collectSkillsFromDir(root: string, source: SkillSource): P
|
|
|
90
90
|
if (skill) results.push(skill);
|
|
91
91
|
}
|
|
92
92
|
// Stable alphabetical order for the UI.
|
|
93
|
-
results.sort((
|
|
93
|
+
results.sort((leftSkill, rightSkill) => leftSkill.name.localeCompare(rightSkill.name));
|
|
94
94
|
return results;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -118,8 +118,8 @@ export async function discoverSkills(opts: DiscoverSkillsOptions = {}): Promise<
|
|
|
118
118
|
// Project overrides user on name collision. Merge by building a
|
|
119
119
|
// map keyed by name, starting with user, overwriting with project.
|
|
120
120
|
const merged = new Map<string, Skill>();
|
|
121
|
-
for (const
|
|
122
|
-
for (const
|
|
121
|
+
for (const skill of userSkills) merged.set(skill.name, skill);
|
|
122
|
+
for (const skill of projectSkills) merged.set(skill.name, skill);
|
|
123
123
|
|
|
124
|
-
return [...merged.values()].sort((
|
|
124
|
+
return [...merged.values()].sort((leftSkill, rightSkill) => leftSkill.name.localeCompare(rightSkill.name));
|
|
125
125
|
}
|
|
@@ -56,9 +56,9 @@ function parseScheduleValue(raw: string): SkillSchedule["parsed"] {
|
|
|
56
56
|
// daily HH:MM — validate range: HH 00-23, MM 00-59
|
|
57
57
|
const dailyMatch = trimmed.match(/^daily\s+(\d{2}):(\d{2})$/);
|
|
58
58
|
if (dailyMatch) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
59
|
+
const hours = Number(dailyMatch[1]);
|
|
60
|
+
const minutes = Number(dailyMatch[2]);
|
|
61
|
+
if (hours > 23 || minutes > 59) return null;
|
|
62
62
|
return {
|
|
63
63
|
type: SCHEDULE_TYPES.daily,
|
|
64
64
|
time: `${dailyMatch[1]}:${dailyMatch[2]}`,
|
|
@@ -70,9 +70,9 @@ function parseScheduleValue(raw: string): SkillSchedule["parsed"] {
|
|
|
70
70
|
if (intervalMatch) {
|
|
71
71
|
const value = Number(intervalMatch[1]);
|
|
72
72
|
const unit = intervalMatch[2];
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
const intervalMs = value *
|
|
73
|
+
const unitMs = TIME_UNIT_MS[unit];
|
|
74
|
+
if (!unitMs) return null;
|
|
75
|
+
const intervalMs = value * unitMs;
|
|
76
76
|
if (intervalMs < MIN_INTERVAL_MS) return null;
|
|
77
77
|
return { type: SCHEDULE_TYPES.interval, intervalMs };
|
|
78
78
|
}
|
|
@@ -137,10 +137,10 @@ function readSkillScheduleInfo(skill: Skill): SkillScheduleInfo | null {
|
|
|
137
137
|
try {
|
|
138
138
|
const raw = readFileSync(skill.path, "utf-8");
|
|
139
139
|
const parsed = parseSkillFrontmatter(raw);
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
140
|
+
const schedule = parsed?.schedule?.parsed;
|
|
141
|
+
if (!schedule) return null;
|
|
142
142
|
return {
|
|
143
|
-
schedule
|
|
143
|
+
schedule,
|
|
144
144
|
roleId: parsed?.roleId ?? DEFAULT_ROLE_ID,
|
|
145
145
|
};
|
|
146
146
|
} catch {
|
|
@@ -32,8 +32,8 @@ export interface PersistedUserTask {
|
|
|
32
32
|
updatedAt: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function loadUserTasks(
|
|
36
|
-
return loadRaw<PersistedUserTask>(
|
|
35
|
+
export function loadUserTasks(workspaceRoot?: string): PersistedUserTask[] {
|
|
36
|
+
return loadRaw<PersistedUserTask>(workspaceRoot);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// ── Validation ──────────────────────────────────────────────────
|
|
@@ -42,20 +42,20 @@ function isValidDailyTime(value: string): boolean {
|
|
|
42
42
|
return /^([01]\d|2[0-3]):([0-5]\d)$/.test(value);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function isValidSchedule(
|
|
46
|
-
if (!isRecord(
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
return typeof
|
|
45
|
+
function isValidSchedule(scheduleValue: unknown): scheduleValue is LocalTaskSchedule {
|
|
46
|
+
if (!isRecord(scheduleValue)) return false;
|
|
47
|
+
const scheduleRecord = scheduleValue as Record<string, unknown>;
|
|
48
|
+
if (scheduleRecord.type === SCHEDULE_TYPES.interval) {
|
|
49
|
+
return typeof scheduleRecord.intervalMs === "number" && scheduleRecord.intervalMs > 0;
|
|
50
50
|
}
|
|
51
|
-
if (
|
|
52
|
-
return typeof
|
|
51
|
+
if (scheduleRecord.type === SCHEDULE_TYPES.daily) {
|
|
52
|
+
return typeof scheduleRecord.time === "string" && isValidDailyTime(scheduleRecord.time);
|
|
53
53
|
}
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function isValidMissedRunPolicy(
|
|
58
|
-
return
|
|
57
|
+
function isValidMissedRunPolicy(policy: unknown): policy is MissedRunPolicy {
|
|
58
|
+
return policy === MISSED_RUN_POLICIES.skip || policy === MISSED_RUN_POLICIES.runOnce || policy === MISSED_RUN_POLICIES.runAll;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export type ValidateResult = { kind: "ok"; task: PersistedUserTask } | { kind: "error"; error: string };
|
|
@@ -96,44 +96,44 @@ export function validateAndCreate(input: unknown): ValidateResult {
|
|
|
96
96
|
|
|
97
97
|
export type UpdateResult = { kind: "ok"; tasks: PersistedUserTask[] } | { kind: "error"; error: string };
|
|
98
98
|
|
|
99
|
-
export function applyUpdate(tasks: PersistedUserTask[],
|
|
99
|
+
export function applyUpdate(tasks: PersistedUserTask[], taskId: string, patch: unknown): UpdateResult {
|
|
100
100
|
if (!isRecord(patch)) {
|
|
101
101
|
return { kind: "error", error: "request body required" };
|
|
102
102
|
}
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
return { kind: "error", error: `task not found: ${
|
|
103
|
+
const index = tasks.findIndex((task) => task.id === taskId);
|
|
104
|
+
if (index === -1) {
|
|
105
|
+
return { kind: "error", error: `task not found: ${taskId}` };
|
|
106
106
|
}
|
|
107
|
-
const existing = tasks[
|
|
107
|
+
const existing = tasks[index];
|
|
108
108
|
const updated: PersistedUserTask = { ...existing };
|
|
109
109
|
// patch is validated as non-null object above; spread into Record
|
|
110
|
-
const
|
|
110
|
+
const patchRecord: Record<string, unknown> = { ...patch };
|
|
111
111
|
|
|
112
|
-
if (typeof
|
|
113
|
-
updated.name =
|
|
112
|
+
if (typeof patchRecord.name === "string" && patchRecord.name.trim().length > 0) {
|
|
113
|
+
updated.name = patchRecord.name.trim();
|
|
114
114
|
}
|
|
115
|
-
if (typeof
|
|
116
|
-
updated.description =
|
|
115
|
+
if (typeof patchRecord.description === "string") {
|
|
116
|
+
updated.description = patchRecord.description.trim();
|
|
117
117
|
}
|
|
118
|
-
if (isValidSchedule(
|
|
119
|
-
updated.schedule =
|
|
118
|
+
if (isValidSchedule(patchRecord.schedule)) {
|
|
119
|
+
updated.schedule = patchRecord.schedule;
|
|
120
120
|
}
|
|
121
|
-
if (isValidMissedRunPolicy(
|
|
122
|
-
updated.missedRunPolicy =
|
|
121
|
+
if (isValidMissedRunPolicy(patchRecord.missedRunPolicy)) {
|
|
122
|
+
updated.missedRunPolicy = patchRecord.missedRunPolicy;
|
|
123
123
|
}
|
|
124
|
-
if (typeof
|
|
125
|
-
updated.enabled =
|
|
124
|
+
if (typeof patchRecord.enabled === "boolean") {
|
|
125
|
+
updated.enabled = patchRecord.enabled;
|
|
126
126
|
}
|
|
127
|
-
if (typeof
|
|
128
|
-
updated.roleId =
|
|
127
|
+
if (typeof patchRecord.roleId === "string") {
|
|
128
|
+
updated.roleId = patchRecord.roleId;
|
|
129
129
|
}
|
|
130
|
-
if (typeof
|
|
131
|
-
updated.prompt =
|
|
130
|
+
if (typeof patchRecord.prompt === "string" && patchRecord.prompt.trim().length > 0) {
|
|
131
|
+
updated.prompt = patchRecord.prompt.trim();
|
|
132
132
|
}
|
|
133
133
|
updated.updatedAt = new Date().toISOString();
|
|
134
134
|
|
|
135
135
|
const next = [...tasks];
|
|
136
|
-
next[
|
|
136
|
+
next[index] = updated;
|
|
137
137
|
return { kind: "ok", tasks: next };
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -144,7 +144,7 @@ export function applyUpdate(tasks: PersistedUserTask[], id: string, patch: unkno
|
|
|
144
144
|
let crudMutex: Promise<void> = Promise.resolve();
|
|
145
145
|
|
|
146
146
|
export async function withUserTaskLock<T>(
|
|
147
|
-
|
|
147
|
+
lockFn: (tasks: PersistedUserTask[]) => Promise<{
|
|
148
148
|
tasks: PersistedUserTask[];
|
|
149
149
|
result: T;
|
|
150
150
|
}>,
|
|
@@ -157,7 +157,7 @@ export async function withUserTaskLock<T>(
|
|
|
157
157
|
try {
|
|
158
158
|
await prev;
|
|
159
159
|
const current = loadUserTasks();
|
|
160
|
-
const { tasks: next, result } = await
|
|
160
|
+
const { tasks: next, result } = await lockFn(current);
|
|
161
161
|
await saveUserTasks(next);
|
|
162
162
|
await refreshUserTasks();
|
|
163
163
|
return result;
|
|
@@ -54,7 +54,7 @@ export async function saveProjectSkill(input: SaveSkillInput): Promise<SaveResul
|
|
|
54
54
|
// user-scope skill with the same name (project would silently
|
|
55
55
|
// override it via the precedence rule).
|
|
56
56
|
const existing = await discoverSkills({ workspaceRoot });
|
|
57
|
-
if (existing.some((
|
|
57
|
+
if (existing.some((skill) => skill.name === name)) {
|
|
58
58
|
return { kind: "exists", name };
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -98,7 +98,7 @@ export async function updateProjectSkill(input: SaveSkillInput): Promise<UpdateR
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const existing = await discoverSkills({ workspaceRoot });
|
|
101
|
-
const skill = existing.find((
|
|
101
|
+
const skill = existing.find((candidate) => candidate.name === name);
|
|
102
102
|
if (!skill) return { kind: "not-found", name };
|
|
103
103
|
if (skill.source === "user") return { kind: "user-scope", name };
|
|
104
104
|
|
|
@@ -139,7 +139,7 @@ export async function deleteProjectSkill(input: DeleteSkillInput): Promise<Delet
|
|
|
139
139
|
// Look up the skill's effective source via discovery — if the
|
|
140
140
|
// matching name is user-scope, we refuse.
|
|
141
141
|
const all = await discoverSkills({ workspaceRoot });
|
|
142
|
-
const skill = all.find((
|
|
142
|
+
const skill = all.find((candidate) => candidate.name === name);
|
|
143
143
|
if (!skill) return { kind: "not-found", name };
|
|
144
144
|
if (skill.source === "user") return { kind: "user-scope", name };
|
|
145
145
|
|
|
@@ -16,7 +16,7 @@ import { workspacePath } from "../paths.js";
|
|
|
16
16
|
import { log } from "../../system/logger/index.js";
|
|
17
17
|
import { slugify } from "../../utils/slug.js";
|
|
18
18
|
import type { Source } from "./types.js";
|
|
19
|
-
import
|
|
19
|
+
import { mkdirSync } from "fs";
|
|
20
20
|
|
|
21
21
|
// ── Constants ───────────────────────────────────────────────────
|
|
22
22
|
|
|
@@ -33,8 +33,8 @@ const DEFAULT_MAX_ITEMS = 20;
|
|
|
33
33
|
* Example: ["transformer", "attention"] → 'ti:"transformer" OR abs:"transformer" OR ti:"attention" OR abs:"attention"'
|
|
34
34
|
*/
|
|
35
35
|
export function buildArxivQuery(keywords: readonly string[]): string {
|
|
36
|
-
const terms = keywords.flatMap((
|
|
37
|
-
const stripped =
|
|
36
|
+
const terms = keywords.flatMap((keyword) => {
|
|
37
|
+
const stripped = keyword.replace(/"/g, "");
|
|
38
38
|
return [`ti:"${stripped}"`, `abs:"${stripped}"`];
|
|
39
39
|
});
|
|
40
40
|
return terms.join(" OR ");
|
|
@@ -48,8 +48,8 @@ export function buildArxivQuery(keywords: readonly string[]): string {
|
|
|
48
48
|
*/
|
|
49
49
|
export function keywordsToSlug(keywords: readonly string[]): string {
|
|
50
50
|
const latin = keywords
|
|
51
|
-
.map((
|
|
52
|
-
.filter((
|
|
51
|
+
.map((keyword) => slugify(keyword, ""))
|
|
52
|
+
.filter((slugPart) => slugPart.length > 0)
|
|
53
53
|
.slice(0, 3)
|
|
54
54
|
.join("-");
|
|
55
55
|
// Short hash of ALL keywords ensures uniqueness even when the
|
|
@@ -88,10 +88,10 @@ export async function discoverAndRegister(root?: string): Promise<DiscoveryResul
|
|
|
88
88
|
|
|
89
89
|
// Ensure sources directory exists
|
|
90
90
|
const dir = sourcesRoot(base);
|
|
91
|
-
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
92
|
|
|
93
93
|
const existing = await listSources(base);
|
|
94
|
-
const existingSlugs = new Set(existing.map((
|
|
94
|
+
const existingSlugs = new Set(existing.map((source) => source.slug));
|
|
95
95
|
|
|
96
96
|
const registered: string[] = [];
|
|
97
97
|
const skipped: string[] = [];
|
|
@@ -158,17 +158,17 @@ export async function pruneStaleAutoSources(root?: string): Promise<string[]> {
|
|
|
158
158
|
const base = root ?? workspacePath;
|
|
159
159
|
const profile = loadInterests(base);
|
|
160
160
|
const existing = await listSources(base);
|
|
161
|
-
const autoSources = existing.filter((
|
|
161
|
+
const autoSources = existing.filter((source) => source.slug.startsWith(ARXIV_SLUG_PREFIX));
|
|
162
162
|
|
|
163
163
|
if (autoSources.length === 0) return [];
|
|
164
164
|
|
|
165
165
|
// If no profile at all, prune everything auto-registered
|
|
166
|
-
const currentKeywords = profile ? new Set(profile.keywords.map((
|
|
166
|
+
const currentKeywords = profile ? new Set(profile.keywords.map((keyword) => keyword.toLowerCase())) : new Set<string>();
|
|
167
167
|
|
|
168
168
|
const pruned: string[] = [];
|
|
169
169
|
for (const source of autoSources) {
|
|
170
170
|
const notes = (source.notes ?? "").toLowerCase();
|
|
171
|
-
const hasMatch = [...currentKeywords].some((
|
|
171
|
+
const hasMatch = [...currentKeywords].some((keyword) => notes.includes(keyword));
|
|
172
172
|
if (!hasMatch && currentKeywords.size > 0) {
|
|
173
173
|
// Keywords changed — this source is stale but don't delete,
|
|
174
174
|
// just log. User can manually remove via manageSource.
|
|
@@ -115,18 +115,18 @@ export function buildClassifyPrompt(input: ClassifyInput): string {
|
|
|
115
115
|
if (titles.length > 0) {
|
|
116
116
|
lines.push("");
|
|
117
117
|
lines.push("RECENT ITEM TITLES:");
|
|
118
|
-
for (const
|
|
119
|
-
lines.push(`- ${
|
|
118
|
+
for (const title of titles.slice(0, 5)) {
|
|
119
|
+
lines.push(`- ${title}`);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
const summaries = input.sampleSummaries ?? [];
|
|
123
123
|
if (summaries.length > 0) {
|
|
124
124
|
lines.push("");
|
|
125
125
|
lines.push("RECENT ITEM SUMMARIES:");
|
|
126
|
-
for (const
|
|
126
|
+
for (const summary of summaries.slice(0, 3)) {
|
|
127
127
|
// One-line truncation so a single long abstract doesn't
|
|
128
128
|
// dominate the prompt budget.
|
|
129
|
-
lines.push(`- ${
|
|
129
|
+
lines.push(`- ${summary.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
return lines.join("\n");
|
|
@@ -167,8 +167,8 @@ export function validateClassifyResult(obj: unknown): ClassifyResult {
|
|
|
167
167
|
if (!isRecord(obj)) {
|
|
168
168
|
throw new Error("[sources/classifier] output is not an object");
|
|
169
169
|
}
|
|
170
|
-
const
|
|
171
|
-
const categories = normalizeCategories(
|
|
170
|
+
const record = obj as Record<string, unknown>;
|
|
171
|
+
const categories = normalizeCategories(record.categories);
|
|
172
172
|
if (categories.length === 0) {
|
|
173
173
|
// The model is required to pick at least one (min_items=1 in
|
|
174
174
|
// the schema). If we end up here, something went wrong upstream
|
|
@@ -178,7 +178,7 @@ export function validateClassifyResult(obj: unknown): ClassifyResult {
|
|
|
178
178
|
// with no categories.
|
|
179
179
|
throw new Error("[sources/classifier] output has no valid categories from the taxonomy");
|
|
180
180
|
}
|
|
181
|
-
const rationale = typeof
|
|
181
|
+
const rationale = typeof record.rationale === "string" ? record.rationale.slice(0, 400) : "";
|
|
182
182
|
return { categories, rationale };
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -90,8 +90,8 @@ export function normalizeArxivFeed(feed: ParsedFeed, source: Source, cursor: Rec
|
|
|
90
90
|
function parseCursorTs(cursor: Record<string, string>): number | null {
|
|
91
91
|
const raw = cursor[ARXIV_CURSOR_KEY];
|
|
92
92
|
if (!raw) return null;
|
|
93
|
-
const
|
|
94
|
-
return Number.isFinite(
|
|
93
|
+
const parsed = Date.parse(raw);
|
|
94
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Decide whether one ParsedFeedItem produces a SourceItem given
|
|
@@ -104,8 +104,8 @@ function feedItemToSourceItem(entry: ParsedFeed["items"][number], source: Source
|
|
|
104
104
|
const normalizedUrl = normalizeUrl(entry.link);
|
|
105
105
|
if (!normalizedUrl) return null;
|
|
106
106
|
if (entry.publishedAt && lastSeenTs !== null) {
|
|
107
|
-
const
|
|
108
|
-
if (Number.isFinite(
|
|
107
|
+
const publishedMs = Date.parse(entry.publishedAt);
|
|
108
|
+
if (Number.isFinite(publishedMs) && publishedMs <= lastSeenTs) return null;
|
|
109
109
|
}
|
|
110
110
|
const publishedAt = entry.publishedAt ?? new Date().toISOString();
|
|
111
111
|
return {
|
|
@@ -128,9 +128,9 @@ export function updateArxivCursor(current: Record<string, string>, feed: ParsedF
|
|
|
128
128
|
let newest: number | null = null;
|
|
129
129
|
for (const entry of feed.items) {
|
|
130
130
|
if (!entry.publishedAt) continue;
|
|
131
|
-
const
|
|
132
|
-
if (!Number.isFinite(
|
|
133
|
-
if (newest === null ||
|
|
131
|
+
const publishedMs = Date.parse(entry.publishedAt);
|
|
132
|
+
if (!Number.isFinite(publishedMs)) continue;
|
|
133
|
+
if (newest === null || publishedMs > newest) newest = publishedMs;
|
|
134
134
|
}
|
|
135
135
|
if (newest === null) return current;
|
|
136
136
|
const currentTs = current[ARXIV_CURSOR_KEY] ? Date.parse(current[ARXIV_CURSOR_KEY]) : -Infinity;
|
|
@@ -69,7 +69,7 @@ interface ParsedIssue {
|
|
|
69
69
|
// signal.
|
|
70
70
|
export function parseGithubIssue(raw: unknown): ParsedIssue | null {
|
|
71
71
|
if (!isRecord(raw)) return null;
|
|
72
|
-
const
|
|
72
|
+
const issueId = typeof raw.id === "number" && Number.isFinite(raw.id) ? raw.id : null;
|
|
73
73
|
const issueNumber = typeof raw.number === "number" && Number.isFinite(raw.number) ? raw.number : null;
|
|
74
74
|
const title = typeof raw.title === "string" ? raw.title : null;
|
|
75
75
|
const htmlUrl = typeof raw.html_url === "string" ? raw.html_url : null;
|
|
@@ -81,7 +81,7 @@ export function parseGithubIssue(raw: unknown): ParsedIssue | null {
|
|
|
81
81
|
// object) means this is a PR. Absence means it's an issue.
|
|
82
82
|
const isPr = "pull_request" in raw && raw.pull_request !== undefined && raw.pull_request !== null;
|
|
83
83
|
return {
|
|
84
|
-
id,
|
|
84
|
+
id: issueId,
|
|
85
85
|
number: issueNumber,
|
|
86
86
|
title,
|
|
87
87
|
htmlUrl,
|
|
@@ -110,7 +110,7 @@ export function issueToSourceItem(issue: ParsedIssue, source: Source, params: Is
|
|
|
110
110
|
|
|
111
111
|
const normalizedUrl = normalizeUrl(issue.htmlUrl);
|
|
112
112
|
if (!normalizedUrl) return null;
|
|
113
|
-
const
|
|
113
|
+
const itemId = stableItemId(normalizedUrl);
|
|
114
114
|
|
|
115
115
|
// Title annotations: `[PR]` for pulls, `[closed]` for closed
|
|
116
116
|
// state so the daily summary makes state visible at a glance.
|
|
@@ -123,7 +123,7 @@ export function issueToSourceItem(issue: ParsedIssue, source: Source, params: Is
|
|
|
123
123
|
const summary = issue.body ? firstParagraph(issue.body) : null;
|
|
124
124
|
|
|
125
125
|
return {
|
|
126
|
-
id,
|
|
126
|
+
id: itemId,
|
|
127
127
|
title,
|
|
128
128
|
url: normalizedUrl,
|
|
129
129
|
publishedAt: new Date(updatedTs).toISOString(),
|
|
@@ -139,9 +139,9 @@ export function updateIssuesCursor(current: Record<string, string>, issues: read
|
|
|
139
139
|
for (const issue of issues) {
|
|
140
140
|
if (issue.isPr && !params.includePrs) continue;
|
|
141
141
|
if (!issue.updatedAt) continue;
|
|
142
|
-
const
|
|
143
|
-
if (!Number.isFinite(
|
|
144
|
-
if (newest === null ||
|
|
142
|
+
const updatedMs = Date.parse(issue.updatedAt);
|
|
143
|
+
if (!Number.isFinite(updatedMs)) continue;
|
|
144
|
+
if (newest === null || updatedMs > newest) newest = updatedMs;
|
|
145
145
|
}
|
|
146
146
|
if (newest === null) return current;
|
|
147
147
|
const currentTs = current[ISSUES_CURSOR_KEY] ? Date.parse(current[ISSUES_CURSOR_KEY]) : -Infinity;
|
|
@@ -52,7 +52,7 @@ interface ParsedRelease {
|
|
|
52
52
|
// hitting the network.
|
|
53
53
|
export function parseGithubRelease(raw: unknown): ParsedRelease | null {
|
|
54
54
|
if (!isRecord(raw)) return null;
|
|
55
|
-
const
|
|
55
|
+
const releaseId = typeof raw.id === "number" && Number.isFinite(raw.id) ? raw.id : null;
|
|
56
56
|
const name = typeof raw.name === "string" ? raw.name : null;
|
|
57
57
|
const tagName = typeof raw.tag_name === "string" ? raw.tag_name : null;
|
|
58
58
|
const htmlUrl = typeof raw.html_url === "string" ? raw.html_url : null;
|
|
@@ -60,7 +60,7 @@ export function parseGithubRelease(raw: unknown): ParsedRelease | null {
|
|
|
60
60
|
const publishedAt = typeof raw.published_at === "string" ? raw.published_at : null;
|
|
61
61
|
const draft = raw.draft === true;
|
|
62
62
|
const prerelease = raw.prerelease === true;
|
|
63
|
-
return { id, name, tagName, htmlUrl, body, publishedAt, draft, prerelease };
|
|
63
|
+
return { id: releaseId, name, tagName, htmlUrl, body, publishedAt, draft, prerelease };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Build a SourceItem from a parsed release + the parent Source.
|
|
@@ -82,7 +82,7 @@ export function releaseToSourceItem(release: ParsedRelease, source: Source, last
|
|
|
82
82
|
|
|
83
83
|
const normalizedUrl = normalizeUrl(release.htmlUrl);
|
|
84
84
|
if (!normalizedUrl) return null;
|
|
85
|
-
const
|
|
85
|
+
const itemId = stableItemId(normalizedUrl);
|
|
86
86
|
|
|
87
87
|
// Title resolution: prefer <name> (release display name), fall
|
|
88
88
|
// back to <tag_name> (e.g. "v1.2.3"). Annotate pre-releases so
|
|
@@ -92,7 +92,7 @@ export function releaseToSourceItem(release: ParsedRelease, source: Source, last
|
|
|
92
92
|
const summary = release.body ? firstParagraph(release.body) : null;
|
|
93
93
|
|
|
94
94
|
return {
|
|
95
|
-
id,
|
|
95
|
+
id: itemId,
|
|
96
96
|
title,
|
|
97
97
|
url: normalizedUrl,
|
|
98
98
|
publishedAt: new Date(publishedTs).toISOString(),
|
|
@@ -128,9 +128,9 @@ export function updateReleasesCursor(current: Record<string, string>, releases:
|
|
|
128
128
|
for (const release of releases) {
|
|
129
129
|
if (release.draft) continue;
|
|
130
130
|
if (!release.publishedAt) continue;
|
|
131
|
-
const
|
|
132
|
-
if (!Number.isFinite(
|
|
133
|
-
if (newest === null ||
|
|
131
|
+
const publishedMs = Date.parse(release.publishedAt);
|
|
132
|
+
if (!Number.isFinite(publishedMs)) continue;
|
|
133
|
+
if (newest === null || publishedMs > newest) newest = publishedMs;
|
|
134
134
|
}
|
|
135
135
|
if (newest === null) return current;
|
|
136
136
|
const currentTs = current[RELEASES_CURSOR_KEY] ? Date.parse(current[RELEASES_CURSOR_KEY]) : -Infinity;
|