@marlin-notes/scheduler-cli 0.0.2 → 0.0.4
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/dist/index.js +44 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +22 -6
- package/src/summarizer.ts +52 -13
package/dist/index.js
CHANGED
|
@@ -70,7 +70,8 @@ async function summarize({
|
|
|
70
70
|
frequency,
|
|
71
71
|
tags,
|
|
72
72
|
apiUrl,
|
|
73
|
-
userId
|
|
73
|
+
userId,
|
|
74
|
+
baseDir
|
|
74
75
|
}) {
|
|
75
76
|
const now = /* @__PURE__ */ new Date();
|
|
76
77
|
let startDate;
|
|
@@ -84,23 +85,34 @@ async function summarize({
|
|
|
84
85
|
console.log(`[Scheduler] Running '${taskName}' (${frequency})`);
|
|
85
86
|
console.log(`[Scheduler] Filtering from: ${startDate.toISOString()}`);
|
|
86
87
|
console.log(`[Scheduler] Tags: ${tags.join(", ") || "(none)"}`);
|
|
87
|
-
const notesDir = process.cwd();
|
|
88
|
-
|
|
88
|
+
const notesDir = baseDir ? import_path.default.resolve(process.cwd(), baseDir) : process.cwd();
|
|
89
|
+
console.log(`[Scheduler] Searching in: ${notesDir}`);
|
|
90
|
+
if (!import_fs.default.existsSync(notesDir)) {
|
|
91
|
+
throw new Error(`Directory not found: ${notesDir}`);
|
|
92
|
+
}
|
|
93
|
+
const files = import_fs.default.readdirSync(notesDir, { recursive: true }).map((f) => String(f)).filter(
|
|
94
|
+
(f) => f.endsWith(".md") && !f.includes("node_modules") && !f.includes(".git")
|
|
95
|
+
);
|
|
89
96
|
const matchedNotes = [];
|
|
90
97
|
for (const file of files) {
|
|
91
98
|
const filePath = import_path.default.join(notesDir, file);
|
|
92
99
|
const content = import_fs.default.readFileSync(filePath, "utf-8");
|
|
93
100
|
const { data, content: body } = (0, import_gray_matter.default)(content);
|
|
94
101
|
let noteDate = null;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
noteDate =
|
|
102
|
+
const dateFields = [data.date, data.updatedAt, data.createdAt];
|
|
103
|
+
for (const field of dateFields) {
|
|
104
|
+
if (!field) continue;
|
|
105
|
+
if (field instanceof Date) {
|
|
106
|
+
noteDate = field;
|
|
100
107
|
} else {
|
|
101
|
-
|
|
108
|
+
const d = new Date(field);
|
|
109
|
+
if (!isNaN(d.getTime())) {
|
|
110
|
+
noteDate = d;
|
|
111
|
+
}
|
|
102
112
|
}
|
|
103
|
-
|
|
113
|
+
if (noteDate) break;
|
|
114
|
+
}
|
|
115
|
+
if (!noteDate) {
|
|
104
116
|
const filenameTs = parseInt(file.replace(".md", ""));
|
|
105
117
|
if (!isNaN(filenameTs) && filenameTs > 1e12) {
|
|
106
118
|
noteDate = new Date(filenameTs);
|
|
@@ -109,8 +121,15 @@ async function summarize({
|
|
|
109
121
|
if (!noteDate || isNaN(noteDate.getTime())) continue;
|
|
110
122
|
if ((0, import_date_fns.isBefore)(noteDate, startDate) || (0, import_date_fns.isAfter)(noteDate, now)) continue;
|
|
111
123
|
const noteTags = Array.isArray(data.tags) ? data.tags : [];
|
|
112
|
-
const normalizedNoteTags = noteTags.map(
|
|
113
|
-
|
|
124
|
+
const normalizedNoteTags = noteTags.map(
|
|
125
|
+
(t) => String(t).replace(/^#/, "").toLowerCase()
|
|
126
|
+
);
|
|
127
|
+
const normalizedSearchTags = tags.map(
|
|
128
|
+
(t) => t.replace(/^#/, "").toLowerCase()
|
|
129
|
+
);
|
|
130
|
+
const hasAllTags = normalizedSearchTags.every(
|
|
131
|
+
(t) => normalizedNoteTags.includes(t)
|
|
132
|
+
);
|
|
114
133
|
if (hasAllTags || tags.length === 0) {
|
|
115
134
|
matchedNotes.push({
|
|
116
135
|
title: data.title || file.replace(".md", ""),
|
|
@@ -135,7 +154,9 @@ async function summarize({
|
|
|
135
154
|
console.log("[Scheduler] Authenticating with GitHub OIDC");
|
|
136
155
|
headers["Authorization"] = `Bearer ${oidcToken}`;
|
|
137
156
|
} else {
|
|
138
|
-
console.warn(
|
|
157
|
+
console.warn(
|
|
158
|
+
"[Scheduler] WARNING: No OIDC token found. Ensure 'id-token: write' permission is set in the workflow."
|
|
159
|
+
);
|
|
139
160
|
}
|
|
140
161
|
const response = await fetch(apiUrl, {
|
|
141
162
|
method: "POST",
|
|
@@ -167,15 +188,21 @@ ${result.summary}
|
|
|
167
188
|
// src/index.ts
|
|
168
189
|
var program = new import_commander.Command();
|
|
169
190
|
program.name("marlin-scheduler").description("CLI for Marlin scheduled tasks").version("0.0.1");
|
|
170
|
-
program.command("summarize").description("Run summarization task").option("--task-name <name>", "Name of the task").option("--frequency <freq>", "Frequency (weekly/monthly)").option("--tags <json>", "JSON string of tags").option("--api-url <url>", "Marlin API URL").option("--user-id <id>", "GitHub User ID").
|
|
191
|
+
program.command("summarize").description("Run summarization task").option("--task-name <name>", "Name of the task").option("--frequency <freq>", "Frequency (weekly/monthly)").option("--tags <json>", "JSON string of tags").option("--api-url <url>", "Marlin API URL").option("--user-id <id>", "GitHub User ID").option(
|
|
192
|
+
"--dir <path>",
|
|
193
|
+
"Directory to search for notes (default: current dir)"
|
|
194
|
+
).action(async (options) => {
|
|
171
195
|
try {
|
|
172
196
|
const taskName = options.taskName || process.env.INPUT_TASK_NAME || "Manual Task";
|
|
173
197
|
const frequency = options.frequency || process.env.INPUT_FREQUENCY || "weekly";
|
|
174
198
|
const tagsJson = options.tags || process.env.INPUT_TAGS || "[]";
|
|
175
199
|
const apiUrl = options.apiUrl || process.env.INPUT_API_URL || "https://marlinnotes.com/api/ai/summarize";
|
|
176
200
|
const userId = options.userId || process.env.INPUT_USER_ID || process.env.GITHUB_ACTOR_ID;
|
|
201
|
+
const baseDir = options.dir || process.env.INPUT_DIR;
|
|
177
202
|
if (!userId) {
|
|
178
|
-
console.error(
|
|
203
|
+
console.error(
|
|
204
|
+
"Error: User ID is required. Pass via --user-id, set INPUT_USER_ID, or run in GitHub Actions (GITHUB_ACTOR_ID)."
|
|
205
|
+
);
|
|
179
206
|
process.exit(1);
|
|
180
207
|
}
|
|
181
208
|
let tags = [];
|
|
@@ -189,7 +216,8 @@ program.command("summarize").description("Run summarization task").option("--tas
|
|
|
189
216
|
frequency,
|
|
190
217
|
tags,
|
|
191
218
|
apiUrl,
|
|
192
|
-
userId
|
|
219
|
+
userId,
|
|
220
|
+
baseDir
|
|
193
221
|
});
|
|
194
222
|
} catch (error) {
|
|
195
223
|
console.error("Failed:", error);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/summarizer.ts","../src/oidc.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { summarize } from \"./summarizer\";\n\nconst program = new Command();\n\nprogram\n .name(\"marlin-scheduler\")\n .description(\"CLI for Marlin scheduled tasks\")\n .version(\"0.0.1\");\n\nprogram\n .command(\"summarize\")\n .description(\"Run summarization task\")\n .option(\"--task-name <name>\", \"Name of the task\")\n .option(\"--frequency <freq>\", \"Frequency (weekly/monthly)\")\n .option(\"--tags <json>\", \"JSON string of tags\")\n .option(\"--api-url <url>\", \"Marlin API URL\")\n .option(\"--user-id <id>\", \"GitHub User ID\")\n .action(async (options) => {\n try {\n // Prioritize flags, then Env Vars (standard Action inputs often use INPUT_ prefix)\n const taskName = options.taskName || process.env.INPUT_TASK_NAME || \"Manual Task\";\n const frequency = options.frequency || process.env.INPUT_FREQUENCY || \"weekly\";\n const tagsJson = options.tags || process.env.INPUT_TAGS || \"[]\";\n // Default to production API if not specified\n const apiUrl = options.apiUrl || process.env.INPUT_API_URL || \"https://marlinnotes.com/api/ai/summarize\";\n \n // Resolve User ID: Flag -> Input -> GitHub Actor ID\n const userId = options.userId || process.env.INPUT_USER_ID || process.env.GITHUB_ACTOR_ID;\n\n if (!userId) {\n console.error(\"Error: User ID is required. Pass via --user-id, set INPUT_USER_ID, or run in GitHub Actions (GITHUB_ACTOR_ID).\");\n process.exit(1);\n }\n\n let tags: string[] = [];\n try {\n tags = JSON.parse(tagsJson);\n } catch (e) {\n console.warn(\"Failed to parse tags JSON, assuming empty array:\", e);\n }\n\n await summarize({\n taskName,\n frequency: frequency as \"weekly\" | \"monthly\",\n tags,\n apiUrl,\n userId,\n });\n } catch (error) {\n console.error(\"Failed:\", error);\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import fs from \"fs\";\nimport path from \"path\";\nimport matter from \"gray-matter\";\nimport { subDays, subMonths, isBefore, isAfter } from \"date-fns\";\nimport { getGithubOidcToken } from \"./oidc\";\n\ninterface SummarizeOptions {\n taskName: string;\n frequency: \"weekly\" | \"monthly\";\n tags: string[];\n apiUrl: string;\n userId: string;\n}\n\nexport async function summarize({\n taskName,\n frequency,\n tags,\n apiUrl,\n userId,\n}: SummarizeOptions) {\n // ... (existing date logic unchanged) ...\n const now = new Date();\n let startDate: Date;\n\n if (frequency === \"weekly\") {\n startDate = subDays(now, 7);\n } else if (frequency === \"monthly\") {\n startDate = subMonths(now, 1);\n } else {\n throw new Error(`Unknown frequency: ${frequency}`);\n }\n\n console.log(`[Scheduler] Running '${taskName}' (${frequency})`);\n console.log(`[Scheduler] Filtering from: ${startDate.toISOString()}`);\n console.log(`[Scheduler] Tags: ${tags.join(\", \") || \"(none)\"}`);\n\n const notesDir = process.cwd(); // Run in current dir\n const files = fs.readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n const matchedNotes = [];\n\n for (const file of files) {\n const filePath = path.join(notesDir, file);\n const content = fs.readFileSync(filePath, \"utf-8\");\n const { data, content: body } = matter(content);\n\n // 1. Date Check\n let noteDate: Date | null = null;\n\n if (data.date) {\n if (typeof data.date === \"number\") {\n noteDate = new Date(data.date);\n } else if (data.date instanceof Date) {\n noteDate = data.date;\n } else {\n noteDate = new Date(data.date);\n }\n } else {\n const filenameTs = parseInt(file.replace(\".md\", \"\"));\n if (!isNaN(filenameTs) && filenameTs > 1000000000000) {\n noteDate = new Date(filenameTs);\n }\n }\n\n if (!noteDate || isNaN(noteDate.getTime())) continue;\n\n if (isBefore(noteDate, startDate) || isAfter(noteDate, now)) continue;\n\n // 2. Tag Check\n const noteTags: string[] = Array.isArray(data.tags) ? data.tags : [];\n const normalizedNoteTags = noteTags.map(String);\n\n const hasAllTags = tags.every((t) => normalizedNoteTags.includes(t));\n\n if (hasAllTags || tags.length === 0) {\n matchedNotes.push({\n title: data.title || file.replace(\".md\", \"\"),\n content: body,\n date: noteDate.toISOString(),\n tags: normalizedNoteTags,\n });\n }\n }\n\n console.log(`[Scheduler] Found ${matchedNotes.length} matching notes.`);\n\n if (matchedNotes.length === 0) {\n console.log(\"No notes to summarize.\");\n return;\n }\n\n // 3. API Call\n console.log(`[Scheduler] Sending to API: ${apiUrl}`);\n \n // Try to get OIDC token\n const oidcToken = await getGithubOidcToken(\"marlin-api\");\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Marlin-User-Id\": userId,\n };\n\n if (oidcToken) {\n console.log(\"[Scheduler] Authenticating with GitHub OIDC\");\n headers[\"Authorization\"] = `Bearer ${oidcToken}`;\n } else {\n // Fallback or Error\n // We allow running without OIDC if strictly testing, but warn loudly.\n // In production, the API will likely reject it.\n console.warn(\"[Scheduler] WARNING: No OIDC token found. Ensure 'id-token: write' permission is set in the workflow.\");\n }\n\n const response = await fetch(apiUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n notes: matchedNotes,\n taskName,\n period: frequency,\n }),\n });\n\n if (!response.ok) {\n const errText = await response.text();\n throw new Error(`API Error ${response.status}: ${errText}`);\n }\n\n const result = (await response.json()) as { summary: string };\n\n // 4. Save Summary\n const summaryFilename = `summary-${frequency}-${now.toISOString().split(\"T\")[0]}.md`;\n\n const summaryContent = `---\ndate: ${now.getTime()}\ntags: [\"#summary/${frequency}\", \"#auto\"]\ntitle: ${frequency} Summary - ${taskName}\n---\n\n${result.summary}\n`;\n\n fs.writeFileSync(path.join(notesDir, summaryFilename), summaryContent);\n console.log(`[Scheduler] Summary saved to ${summaryFilename}`);\n}\n","export async function getGithubOidcToken(audience?: string): Promise<string | null> {\n const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n\n if (!requestUrl || !requestToken) {\n // Not running in GitHub Actions or permissions not set\n return null;\n }\n\n try {\n const url = new URL(requestUrl);\n if (audience) {\n url.searchParams.append(\"audience\", audience);\n }\n\n const response = await fetch(url.toString(), {\n headers: {\n Authorization: `Bearer ${requestToken}`,\n Accept: \"application/json; api-version=2.0\",\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) {\n const text = await response.text();\n console.warn(`[OIDC] Failed to fetch token: ${response.status} ${text}`);\n return null;\n }\n\n const data = (await response.json()) as { value: string };\n return data.value;\n } catch (error) {\n console.warn(\"[OIDC] Error fetching token:\", error);\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uBAAwB;;;ACAxB,gBAAe;AACf,kBAAiB;AACjB,yBAAmB;AACnB,sBAAsD;;;ACHtD,eAAsB,mBAAmB,UAA2C;AAClF,QAAM,aAAa,QAAQ,IAAI;AAC/B,QAAM,eAAe,QAAQ,IAAI;AAEjC,MAAI,CAAC,cAAc,CAAC,cAAc;AAEhC,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,UAAU;AAC9B,QAAI,UAAU;AACZ,UAAI,aAAa,OAAO,YAAY,QAAQ;AAAA,IAC9C;AAEA,UAAM,WAAW,MAAM,MAAM,IAAI,SAAS,GAAG;AAAA,MAC3C,SAAS;AAAA,QACP,eAAe,UAAU,YAAY;AAAA,QACrC,QAAQ;AAAA,QACR,gBAAgB;AAAA,MAClB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAQ,KAAK,iCAAiC,SAAS,MAAM,IAAI,IAAI,EAAE;AACvE,aAAO;AAAA,IACT;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,KAAK;AAAA,EACd,SAAS,OAAO;AACd,YAAQ,KAAK,gCAAgC,KAAK;AAClD,WAAO;AAAA,EACT;AACF;;;ADrBA,eAAsB,UAAU;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AAEnB,QAAM,MAAM,oBAAI,KAAK;AACrB,MAAI;AAEJ,MAAI,cAAc,UAAU;AAC1B,oBAAY,yBAAQ,KAAK,CAAC;AAAA,EAC5B,WAAW,cAAc,WAAW;AAClC,oBAAY,2BAAU,KAAK,CAAC;AAAA,EAC9B,OAAO;AACL,UAAM,IAAI,MAAM,sBAAsB,SAAS,EAAE;AAAA,EACnD;AAEA,UAAQ,IAAI,wBAAwB,QAAQ,MAAM,SAAS,GAAG;AAC9D,UAAQ,IAAI,+BAA+B,UAAU,YAAY,CAAC,EAAE;AACpE,UAAQ,IAAI,qBAAqB,KAAK,KAAK,IAAI,KAAK,QAAQ,EAAE;AAE9D,QAAM,WAAW,QAAQ,IAAI;AAC7B,QAAM,QAAQ,UAAAA,QAAG,YAAY,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC;AACtE,QAAM,eAAe,CAAC;AAEtB,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAW,YAAAC,QAAK,KAAK,UAAU,IAAI;AACzC,UAAM,UAAU,UAAAD,QAAG,aAAa,UAAU,OAAO;AACjD,UAAM,EAAE,MAAM,SAAS,KAAK,QAAI,mBAAAE,SAAO,OAAO;AAG9C,QAAI,WAAwB;AAE5B,QAAI,KAAK,MAAM;AACb,UAAI,OAAO,KAAK,SAAS,UAAU;AACjC,mBAAW,IAAI,KAAK,KAAK,IAAI;AAAA,MAC/B,WAAW,KAAK,gBAAgB,MAAM;AACpC,mBAAW,KAAK;AAAA,MAClB,OAAO;AACL,mBAAW,IAAI,KAAK,KAAK,IAAI;AAAA,MAC/B;AAAA,IACF,OAAO;AACL,YAAM,aAAa,SAAS,KAAK,QAAQ,OAAO,EAAE,CAAC;AACnD,UAAI,CAAC,MAAM,UAAU,KAAK,aAAa,MAAe;AACpD,mBAAW,IAAI,KAAK,UAAU;AAAA,MAChC;AAAA,IACF;AAEA,QAAI,CAAC,YAAY,MAAM,SAAS,QAAQ,CAAC,EAAG;AAE5C,YAAI,0BAAS,UAAU,SAAS,SAAK,yBAAQ,UAAU,GAAG,EAAG;AAG7D,UAAM,WAAqB,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC;AACnE,UAAM,qBAAqB,SAAS,IAAI,MAAM;AAE9C,UAAM,aAAa,KAAK,MAAM,CAAC,MAAM,mBAAmB,SAAS,CAAC,CAAC;AAEnE,QAAI,cAAc,KAAK,WAAW,GAAG;AACnC,mBAAa,KAAK;AAAA,QAChB,OAAO,KAAK,SAAS,KAAK,QAAQ,OAAO,EAAE;AAAA,QAC3C,SAAS;AAAA,QACT,MAAM,SAAS,YAAY;AAAA,QAC3B,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAEA,UAAQ,IAAI,qBAAqB,aAAa,MAAM,kBAAkB;AAEtE,MAAI,aAAa,WAAW,GAAG;AAC7B,YAAQ,IAAI,wBAAwB;AACpC;AAAA,EACF;AAGA,UAAQ,IAAI,+BAA+B,MAAM,EAAE;AAGnD,QAAM,YAAY,MAAM,mBAAmB,YAAY;AACvD,QAAM,UAAkC;AAAA,IACtC,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,EACtB;AAEA,MAAI,WAAW;AACb,YAAQ,IAAI,6CAA6C;AACzD,YAAQ,eAAe,IAAI,UAAU,SAAS;AAAA,EAChD,OAAO;AAIL,YAAQ,KAAK,uGAAuG;AAAA,EACtH;AAEA,QAAM,WAAW,MAAM,MAAM,QAAQ;AAAA,IACnC,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB,OAAO;AAAA,MACP;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AAAA,EACH,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,UAAU,MAAM,SAAS,KAAK;AACpC,UAAM,IAAI,MAAM,aAAa,SAAS,MAAM,KAAK,OAAO,EAAE;AAAA,EAC5D;AAEA,QAAM,SAAU,MAAM,SAAS,KAAK;AAGpC,QAAM,kBAAkB,WAAW,SAAS,IAAI,IAAI,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;AAE/E,QAAM,iBAAiB;AAAA,QACjB,IAAI,QAAQ,CAAC;AAAA,mBACF,SAAS;AAAA,SACnB,SAAS,cAAc,QAAQ;AAAA;AAAA;AAAA,EAGtC,OAAO,OAAO;AAAA;AAGd,YAAAF,QAAG,cAAc,YAAAC,QAAK,KAAK,UAAU,eAAe,GAAG,cAAc;AACrE,UAAQ,IAAI,gCAAgC,eAAe,EAAE;AAC/D;;;AD3IA,IAAM,UAAU,IAAI,yBAAQ;AAE5B,QACG,KAAK,kBAAkB,EACvB,YAAY,gCAAgC,EAC5C,QAAQ,OAAO;AAElB,QACG,QAAQ,WAAW,EACnB,YAAY,wBAAwB,EACpC,OAAO,sBAAsB,kBAAkB,EAC/C,OAAO,sBAAsB,4BAA4B,EACzD,OAAO,iBAAiB,qBAAqB,EAC7C,OAAO,mBAAmB,gBAAgB,EAC1C,OAAO,kBAAkB,gBAAgB,EACzC,OAAO,OAAO,YAAY;AACzB,MAAI;AAEF,UAAM,WAAW,QAAQ,YAAY,QAAQ,IAAI,mBAAmB;AACpE,UAAM,YAAY,QAAQ,aAAa,QAAQ,IAAI,mBAAmB;AACtE,UAAM,WAAW,QAAQ,QAAQ,QAAQ,IAAI,cAAc;AAE3D,UAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,iBAAiB;AAG9D,UAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,iBAAiB,QAAQ,IAAI;AAE1E,QAAI,CAAC,QAAQ;AACX,cAAQ,MAAM,gHAAgH;AAC9H,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,QAAI,OAAiB,CAAC;AACtB,QAAI;AACF,aAAO,KAAK,MAAM,QAAQ;AAAA,IAC5B,SAAS,GAAG;AACV,cAAQ,KAAK,oDAAoD,CAAC;AAAA,IACpE;AAEA,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,WAAW,KAAK;AAC9B,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,QAAQ,MAAM;","names":["fs","path","matter"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/summarizer.ts","../src/oidc.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { summarize } from \"./summarizer\";\n\nconst program = new Command();\n\nprogram\n .name(\"marlin-scheduler\")\n .description(\"CLI for Marlin scheduled tasks\")\n .version(\"0.0.1\");\n\nprogram\n .command(\"summarize\")\n .description(\"Run summarization task\")\n .option(\"--task-name <name>\", \"Name of the task\")\n .option(\"--frequency <freq>\", \"Frequency (weekly/monthly)\")\n .option(\"--tags <json>\", \"JSON string of tags\")\n .option(\"--api-url <url>\", \"Marlin API URL\")\n .option(\"--user-id <id>\", \"GitHub User ID\")\n .option(\n \"--dir <path>\",\n \"Directory to search for notes (default: current dir)\"\n )\n .action(async (options) => {\n try {\n // Prioritize flags, then Env Vars (standard Action inputs often use INPUT_ prefix)\n const taskName =\n options.taskName || process.env.INPUT_TASK_NAME || \"Manual Task\";\n const frequency =\n options.frequency || process.env.INPUT_FREQUENCY || \"weekly\";\n const tagsJson = options.tags || process.env.INPUT_TAGS || \"[]\";\n // Default to production API if not specified\n const apiUrl =\n options.apiUrl ||\n process.env.INPUT_API_URL ||\n \"https://marlinnotes.com/api/ai/summarize\";\n\n // Resolve User ID: Flag -> Input -> GitHub Actor ID\n const userId =\n options.userId ||\n process.env.INPUT_USER_ID ||\n process.env.GITHUB_ACTOR_ID;\n const baseDir = options.dir || process.env.INPUT_DIR;\n\n if (!userId) {\n console.error(\n \"Error: User ID is required. Pass via --user-id, set INPUT_USER_ID, or run in GitHub Actions (GITHUB_ACTOR_ID).\"\n );\n process.exit(1);\n }\n\n let tags: string[] = [];\n try {\n tags = JSON.parse(tagsJson);\n } catch (e) {\n console.warn(\"Failed to parse tags JSON, assuming empty array:\", e);\n }\n\n await summarize({\n taskName,\n frequency: frequency as \"weekly\" | \"monthly\",\n tags,\n apiUrl,\n userId,\n baseDir,\n });\n } catch (error) {\n console.error(\"Failed:\", error);\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import fs from \"fs\";\nimport path from \"path\";\nimport matter from \"gray-matter\";\nimport { subDays, subMonths, isBefore, isAfter } from \"date-fns\";\nimport { getGithubOidcToken } from \"./oidc\";\n\ninterface SummarizeOptions {\n taskName: string;\n frequency: \"weekly\" | \"monthly\";\n tags: string[];\n apiUrl: string;\n userId: string;\n baseDir?: string;\n}\n\nexport async function summarize({\n taskName,\n frequency,\n tags,\n apiUrl,\n userId,\n baseDir,\n}: SummarizeOptions) {\n // ... (existing date logic unchanged) ...\n const now = new Date();\n let startDate: Date;\n\n if (frequency === \"weekly\") {\n startDate = subDays(now, 7);\n } else if (frequency === \"monthly\") {\n startDate = subMonths(now, 1);\n } else {\n throw new Error(`Unknown frequency: ${frequency}`);\n }\n\n console.log(`[Scheduler] Running '${taskName}' (${frequency})`);\n console.log(`[Scheduler] Filtering from: ${startDate.toISOString()}`);\n console.log(`[Scheduler] Tags: ${tags.join(\", \") || \"(none)\"}`);\n\n const notesDir = baseDir\n ? path.resolve(process.cwd(), baseDir)\n : process.cwd();\n console.log(`[Scheduler] Searching in: ${notesDir}`);\n\n if (!fs.existsSync(notesDir)) {\n throw new Error(`Directory not found: ${notesDir}`);\n }\n\n // Use recursive search if available (Node 20+), otherwise shallow\n // We filter out node_modules and .git to avoid clutter\n const files = fs\n .readdirSync(notesDir, { recursive: true } as any)\n .map((f) => String(f)) // Ensure string\n .filter(\n (f) =>\n f.endsWith(\".md\") && !f.includes(\"node_modules\") && !f.includes(\".git\")\n );\n\n const matchedNotes = [];\n\n for (const file of files) {\n const filePath = path.join(notesDir, file);\n const content = fs.readFileSync(filePath, \"utf-8\");\n const { data, content: body } = matter(content);\n\n // 1. Date Check\n let noteDate: Date | null = null;\n\n // Check fields in priority: date -> updatedAt -> createdAt\n const dateFields = [data.date, data.updatedAt, data.createdAt];\n\n for (const field of dateFields) {\n if (!field) continue;\n\n if (field instanceof Date) {\n noteDate = field;\n } else {\n const d = new Date(field);\n if (!isNaN(d.getTime())) {\n noteDate = d;\n }\n }\n\n if (noteDate) break;\n }\n\n // Fallback to filename timestamp if no valid date found in frontmatter\n if (!noteDate) {\n const filenameTs = parseInt(file.replace(\".md\", \"\"));\n if (!isNaN(filenameTs) && filenameTs > 1000000000000) {\n noteDate = new Date(filenameTs);\n }\n }\n\n if (!noteDate || isNaN(noteDate.getTime())) continue;\n\n if (isBefore(noteDate, startDate) || isAfter(noteDate, now)) continue;\n\n // 2. Tag Check\n const noteTags: string[] = Array.isArray(data.tags) ? data.tags : [];\n const normalizedNoteTags = noteTags.map((t) =>\n String(t).replace(/^#/, \"\").toLowerCase()\n );\n const normalizedSearchTags = tags.map((t) =>\n t.replace(/^#/, \"\").toLowerCase()\n );\n\n const hasAllTags = normalizedSearchTags.every((t) =>\n normalizedNoteTags.includes(t)\n );\n\n if (hasAllTags || tags.length === 0) {\n matchedNotes.push({\n title: data.title || file.replace(\".md\", \"\"),\n content: body,\n date: noteDate.toISOString(),\n tags: normalizedNoteTags,\n });\n }\n }\n\n console.log(`[Scheduler] Found ${matchedNotes.length} matching notes.`);\n\n if (matchedNotes.length === 0) {\n console.log(\"No notes to summarize.\");\n return;\n }\n\n // 3. API Call\n console.log(`[Scheduler] Sending to API: ${apiUrl}`);\n\n // Try to get OIDC token\n const oidcToken = await getGithubOidcToken(\"marlin-api\");\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Marlin-User-Id\": userId,\n };\n\n if (oidcToken) {\n console.log(\"[Scheduler] Authenticating with GitHub OIDC\");\n headers[\"Authorization\"] = `Bearer ${oidcToken}`;\n } else {\n // Fallback or Error\n // We allow running without OIDC if strictly testing, but warn loudly.\n // In production, the API will likely reject it.\n console.warn(\n \"[Scheduler] WARNING: No OIDC token found. Ensure 'id-token: write' permission is set in the workflow.\"\n );\n }\n\n const response = await fetch(apiUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n notes: matchedNotes,\n taskName,\n period: frequency,\n }),\n });\n\n if (!response.ok) {\n const errText = await response.text();\n throw new Error(`API Error ${response.status}: ${errText}`);\n }\n\n const result = (await response.json()) as { summary: string };\n\n // 4. Save Summary\n const summaryFilename = `summary-${frequency}-${now.toISOString().split(\"T\")[0]}.md`;\n\n const summaryContent = `---\ndate: ${now.getTime()}\ntags: [\"#summary/${frequency}\", \"#auto\"]\ntitle: ${frequency} Summary - ${taskName}\n---\n\n${result.summary}\n`;\n\n fs.writeFileSync(path.join(notesDir, summaryFilename), summaryContent);\n console.log(`[Scheduler] Summary saved to ${summaryFilename}`);\n}\n","export async function getGithubOidcToken(audience?: string): Promise<string | null> {\n const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n\n if (!requestUrl || !requestToken) {\n // Not running in GitHub Actions or permissions not set\n return null;\n }\n\n try {\n const url = new URL(requestUrl);\n if (audience) {\n url.searchParams.append(\"audience\", audience);\n }\n\n const response = await fetch(url.toString(), {\n headers: {\n Authorization: `Bearer ${requestToken}`,\n Accept: \"application/json; api-version=2.0\",\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) {\n const text = await response.text();\n console.warn(`[OIDC] Failed to fetch token: ${response.status} ${text}`);\n return null;\n }\n\n const data = (await response.json()) as { value: string };\n return data.value;\n } catch (error) {\n console.warn(\"[OIDC] Error fetching token:\", error);\n return null;\n }\n}\n"],"mappings":"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","names":["path","fs","matter"]}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,20 +16,35 @@ program
|
|
|
16
16
|
.option("--tags <json>", "JSON string of tags")
|
|
17
17
|
.option("--api-url <url>", "Marlin API URL")
|
|
18
18
|
.option("--user-id <id>", "GitHub User ID")
|
|
19
|
+
.option(
|
|
20
|
+
"--dir <path>",
|
|
21
|
+
"Directory to search for notes (default: current dir)"
|
|
22
|
+
)
|
|
19
23
|
.action(async (options) => {
|
|
20
24
|
try {
|
|
21
25
|
// Prioritize flags, then Env Vars (standard Action inputs often use INPUT_ prefix)
|
|
22
|
-
const taskName =
|
|
23
|
-
|
|
26
|
+
const taskName =
|
|
27
|
+
options.taskName || process.env.INPUT_TASK_NAME || "Manual Task";
|
|
28
|
+
const frequency =
|
|
29
|
+
options.frequency || process.env.INPUT_FREQUENCY || "weekly";
|
|
24
30
|
const tagsJson = options.tags || process.env.INPUT_TAGS || "[]";
|
|
25
31
|
// Default to production API if not specified
|
|
26
|
-
const apiUrl =
|
|
27
|
-
|
|
32
|
+
const apiUrl =
|
|
33
|
+
options.apiUrl ||
|
|
34
|
+
process.env.INPUT_API_URL ||
|
|
35
|
+
"https://marlinnotes.com/api/ai/summarize";
|
|
36
|
+
|
|
28
37
|
// Resolve User ID: Flag -> Input -> GitHub Actor ID
|
|
29
|
-
const userId =
|
|
38
|
+
const userId =
|
|
39
|
+
options.userId ||
|
|
40
|
+
process.env.INPUT_USER_ID ||
|
|
41
|
+
process.env.GITHUB_ACTOR_ID;
|
|
42
|
+
const baseDir = options.dir || process.env.INPUT_DIR;
|
|
30
43
|
|
|
31
44
|
if (!userId) {
|
|
32
|
-
console.error(
|
|
45
|
+
console.error(
|
|
46
|
+
"Error: User ID is required. Pass via --user-id, set INPUT_USER_ID, or run in GitHub Actions (GITHUB_ACTOR_ID)."
|
|
47
|
+
);
|
|
33
48
|
process.exit(1);
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -46,6 +61,7 @@ program
|
|
|
46
61
|
tags,
|
|
47
62
|
apiUrl,
|
|
48
63
|
userId,
|
|
64
|
+
baseDir,
|
|
49
65
|
});
|
|
50
66
|
} catch (error) {
|
|
51
67
|
console.error("Failed:", error);
|
package/src/summarizer.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface SummarizeOptions {
|
|
|
10
10
|
tags: string[];
|
|
11
11
|
apiUrl: string;
|
|
12
12
|
userId: string;
|
|
13
|
+
baseDir?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export async function summarize({
|
|
@@ -18,6 +19,7 @@ export async function summarize({
|
|
|
18
19
|
tags,
|
|
19
20
|
apiUrl,
|
|
20
21
|
userId,
|
|
22
|
+
baseDir,
|
|
21
23
|
}: SummarizeOptions) {
|
|
22
24
|
// ... (existing date logic unchanged) ...
|
|
23
25
|
const now = new Date();
|
|
@@ -35,8 +37,25 @@ export async function summarize({
|
|
|
35
37
|
console.log(`[Scheduler] Filtering from: ${startDate.toISOString()}`);
|
|
36
38
|
console.log(`[Scheduler] Tags: ${tags.join(", ") || "(none)"}`);
|
|
37
39
|
|
|
38
|
-
const notesDir =
|
|
39
|
-
|
|
40
|
+
const notesDir = baseDir
|
|
41
|
+
? path.resolve(process.cwd(), baseDir)
|
|
42
|
+
: process.cwd();
|
|
43
|
+
console.log(`[Scheduler] Searching in: ${notesDir}`);
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(notesDir)) {
|
|
46
|
+
throw new Error(`Directory not found: ${notesDir}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Use recursive search if available (Node 20+), otherwise shallow
|
|
50
|
+
// We filter out node_modules and .git to avoid clutter
|
|
51
|
+
const files = fs
|
|
52
|
+
.readdirSync(notesDir, { recursive: true } as any)
|
|
53
|
+
.map((f) => String(f)) // Ensure string
|
|
54
|
+
.filter(
|
|
55
|
+
(f) =>
|
|
56
|
+
f.endsWith(".md") && !f.includes("node_modules") && !f.includes(".git")
|
|
57
|
+
);
|
|
58
|
+
|
|
40
59
|
const matchedNotes = [];
|
|
41
60
|
|
|
42
61
|
for (const file of files) {
|
|
@@ -47,15 +66,26 @@ export async function summarize({
|
|
|
47
66
|
// 1. Date Check
|
|
48
67
|
let noteDate: Date | null = null;
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
// Check fields in priority: date -> updatedAt -> createdAt
|
|
70
|
+
const dateFields = [data.date, data.updatedAt, data.createdAt];
|
|
71
|
+
|
|
72
|
+
for (const field of dateFields) {
|
|
73
|
+
if (!field) continue;
|
|
74
|
+
|
|
75
|
+
if (field instanceof Date) {
|
|
76
|
+
noteDate = field;
|
|
55
77
|
} else {
|
|
56
|
-
|
|
78
|
+
const d = new Date(field);
|
|
79
|
+
if (!isNaN(d.getTime())) {
|
|
80
|
+
noteDate = d;
|
|
81
|
+
}
|
|
57
82
|
}
|
|
58
|
-
|
|
83
|
+
|
|
84
|
+
if (noteDate) break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback to filename timestamp if no valid date found in frontmatter
|
|
88
|
+
if (!noteDate) {
|
|
59
89
|
const filenameTs = parseInt(file.replace(".md", ""));
|
|
60
90
|
if (!isNaN(filenameTs) && filenameTs > 1000000000000) {
|
|
61
91
|
noteDate = new Date(filenameTs);
|
|
@@ -68,9 +98,16 @@ export async function summarize({
|
|
|
68
98
|
|
|
69
99
|
// 2. Tag Check
|
|
70
100
|
const noteTags: string[] = Array.isArray(data.tags) ? data.tags : [];
|
|
71
|
-
const normalizedNoteTags = noteTags.map(
|
|
101
|
+
const normalizedNoteTags = noteTags.map((t) =>
|
|
102
|
+
String(t).replace(/^#/, "").toLowerCase()
|
|
103
|
+
);
|
|
104
|
+
const normalizedSearchTags = tags.map((t) =>
|
|
105
|
+
t.replace(/^#/, "").toLowerCase()
|
|
106
|
+
);
|
|
72
107
|
|
|
73
|
-
const hasAllTags =
|
|
108
|
+
const hasAllTags = normalizedSearchTags.every((t) =>
|
|
109
|
+
normalizedNoteTags.includes(t)
|
|
110
|
+
);
|
|
74
111
|
|
|
75
112
|
if (hasAllTags || tags.length === 0) {
|
|
76
113
|
matchedNotes.push({
|
|
@@ -91,7 +128,7 @@ export async function summarize({
|
|
|
91
128
|
|
|
92
129
|
// 3. API Call
|
|
93
130
|
console.log(`[Scheduler] Sending to API: ${apiUrl}`);
|
|
94
|
-
|
|
131
|
+
|
|
95
132
|
// Try to get OIDC token
|
|
96
133
|
const oidcToken = await getGithubOidcToken("marlin-api");
|
|
97
134
|
const headers: Record<string, string> = {
|
|
@@ -106,7 +143,9 @@ export async function summarize({
|
|
|
106
143
|
// Fallback or Error
|
|
107
144
|
// We allow running without OIDC if strictly testing, but warn loudly.
|
|
108
145
|
// In production, the API will likely reject it.
|
|
109
|
-
console.warn(
|
|
146
|
+
console.warn(
|
|
147
|
+
"[Scheduler] WARNING: No OIDC token found. Ensure 'id-token: write' permission is set in the workflow."
|
|
148
|
+
);
|
|
110
149
|
}
|
|
111
150
|
|
|
112
151
|
const response = await fetch(apiUrl, {
|