@marlin-notes/scheduler-cli 0.0.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.
- package/LICENSE +39 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/index.ts +54 -0
- package/src/oidc.ts +36 -0
- package/src/summarizer.ts +138 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Licensor: Wuyukang
|
|
4
|
+
Licensed Work: The Marlin software and associated documentation files.
|
|
5
|
+
Change Date: 2029-12-14
|
|
6
|
+
Change License: Apache License, Version 2.0
|
|
7
|
+
|
|
8
|
+
Additional Use Grant:
|
|
9
|
+
You may make use of the Licensed Work, provided that you do not provide a Commercial Product to third parties.
|
|
10
|
+
|
|
11
|
+
"Commercial Product" is defined as a product or service that competes with the Licensed Work, specifically a software-as-a-service, platform-as-a-service, or infrastructure-as-a-service offering that allows third parties to use the Licensed Work (or a modified version of it) for note-taking, knowledge management, or data synchronization.
|
|
12
|
+
|
|
13
|
+
Use for internal business purposes (e.g., employees using the software within their company for personal or team note-taking) is explicitly permitted.
|
|
14
|
+
|
|
15
|
+
-------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make use of the Licensed Work, provided that you comply with the Additional Use Grant.
|
|
18
|
+
|
|
19
|
+
For the purposes of this License, "Change Date" and "Change License" are as defined above.
|
|
20
|
+
|
|
21
|
+
The Licensor will convert the License of the Licensed Work to the Change License on the Change Date.
|
|
22
|
+
|
|
23
|
+
1. Grant of License.
|
|
24
|
+
The Licensor hereby grants you a world-wide, royalty-free, non-exclusive license to use the Licensed Work, subject to the Additional Use Grant and the terms of this License.
|
|
25
|
+
|
|
26
|
+
2. Term.
|
|
27
|
+
This License is effective as of the date of your first use of the Licensed Work and continues until the Change Date.
|
|
28
|
+
|
|
29
|
+
3. Limitations.
|
|
30
|
+
You may not use the Licensed Work for any purpose that is not expressly permitted by the Additional Use Grant.
|
|
31
|
+
|
|
32
|
+
4. Termination.
|
|
33
|
+
Your rights under this License will terminate automatically if you fail to comply with any of its terms.
|
|
34
|
+
|
|
35
|
+
5. Disclaimer of Warranty.
|
|
36
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
|
|
37
|
+
|
|
38
|
+
6. Governing Law.
|
|
39
|
+
This License shall be governed by and construed in accordance with the laws of the jurisdiction in which the Licensor resides, excluding its conflict of law provisions.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/summarizer.ts
|
|
30
|
+
var import_fs = __toESM(require("fs"));
|
|
31
|
+
var import_path = __toESM(require("path"));
|
|
32
|
+
var import_gray_matter = __toESM(require("gray-matter"));
|
|
33
|
+
var import_date_fns = require("date-fns");
|
|
34
|
+
|
|
35
|
+
// src/oidc.ts
|
|
36
|
+
async function getGithubOidcToken(audience) {
|
|
37
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
38
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
39
|
+
if (!requestUrl || !requestToken) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(requestUrl);
|
|
44
|
+
if (audience) {
|
|
45
|
+
url.searchParams.append("audience", audience);
|
|
46
|
+
}
|
|
47
|
+
const response = await fetch(url.toString(), {
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${requestToken}`,
|
|
50
|
+
Accept: "application/json; api-version=2.0",
|
|
51
|
+
"Content-Type": "application/json"
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
console.warn(`[OIDC] Failed to fetch token: ${response.status} ${text}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
return data.value;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("[OIDC] Error fetching token:", error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/summarizer.ts
|
|
68
|
+
async function summarize({
|
|
69
|
+
taskName,
|
|
70
|
+
frequency,
|
|
71
|
+
tags,
|
|
72
|
+
apiUrl,
|
|
73
|
+
userId
|
|
74
|
+
}) {
|
|
75
|
+
const now = /* @__PURE__ */ new Date();
|
|
76
|
+
let startDate;
|
|
77
|
+
if (frequency === "weekly") {
|
|
78
|
+
startDate = (0, import_date_fns.subDays)(now, 7);
|
|
79
|
+
} else if (frequency === "monthly") {
|
|
80
|
+
startDate = (0, import_date_fns.subMonths)(now, 1);
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(`Unknown frequency: ${frequency}`);
|
|
83
|
+
}
|
|
84
|
+
console.log(`[Scheduler] Running '${taskName}' (${frequency})`);
|
|
85
|
+
console.log(`[Scheduler] Filtering from: ${startDate.toISOString()}`);
|
|
86
|
+
console.log(`[Scheduler] Tags: ${tags.join(", ") || "(none)"}`);
|
|
87
|
+
const notesDir = process.cwd();
|
|
88
|
+
const files = import_fs.default.readdirSync(notesDir).filter((f) => f.endsWith(".md"));
|
|
89
|
+
const matchedNotes = [];
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const filePath = import_path.default.join(notesDir, file);
|
|
92
|
+
const content = import_fs.default.readFileSync(filePath, "utf-8");
|
|
93
|
+
const { data, content: body } = (0, import_gray_matter.default)(content);
|
|
94
|
+
let noteDate = null;
|
|
95
|
+
if (data.date) {
|
|
96
|
+
if (typeof data.date === "number") {
|
|
97
|
+
noteDate = new Date(data.date);
|
|
98
|
+
} else if (data.date instanceof Date) {
|
|
99
|
+
noteDate = data.date;
|
|
100
|
+
} else {
|
|
101
|
+
noteDate = new Date(data.date);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
const filenameTs = parseInt(file.replace(".md", ""));
|
|
105
|
+
if (!isNaN(filenameTs) && filenameTs > 1e12) {
|
|
106
|
+
noteDate = new Date(filenameTs);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!noteDate || isNaN(noteDate.getTime())) continue;
|
|
110
|
+
if ((0, import_date_fns.isBefore)(noteDate, startDate) || (0, import_date_fns.isAfter)(noteDate, now)) continue;
|
|
111
|
+
const noteTags = Array.isArray(data.tags) ? data.tags : [];
|
|
112
|
+
const normalizedNoteTags = noteTags.map(String);
|
|
113
|
+
const hasAllTags = tags.every((t) => normalizedNoteTags.includes(t));
|
|
114
|
+
if (hasAllTags || tags.length === 0) {
|
|
115
|
+
matchedNotes.push({
|
|
116
|
+
title: data.title || file.replace(".md", ""),
|
|
117
|
+
content: body,
|
|
118
|
+
date: noteDate.toISOString(),
|
|
119
|
+
tags: normalizedNoteTags
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
console.log(`[Scheduler] Found ${matchedNotes.length} matching notes.`);
|
|
124
|
+
if (matchedNotes.length === 0) {
|
|
125
|
+
console.log("No notes to summarize.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(`[Scheduler] Sending to API: ${apiUrl}`);
|
|
129
|
+
const oidcToken = await getGithubOidcToken("marlin-api");
|
|
130
|
+
const headers = {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
"X-Marlin-User-Id": userId
|
|
133
|
+
};
|
|
134
|
+
if (oidcToken) {
|
|
135
|
+
console.log("[Scheduler] Authenticating with GitHub OIDC");
|
|
136
|
+
headers["Authorization"] = `Bearer ${oidcToken}`;
|
|
137
|
+
}
|
|
138
|
+
const response = await fetch(apiUrl, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers,
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
notes: matchedNotes,
|
|
143
|
+
taskName,
|
|
144
|
+
period: frequency
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const errText = await response.text();
|
|
149
|
+
throw new Error(`API Error ${response.status}: ${errText}`);
|
|
150
|
+
}
|
|
151
|
+
const result = await response.json();
|
|
152
|
+
const summaryFilename = `summary-${frequency}-${now.toISOString().split("T")[0]}.md`;
|
|
153
|
+
const summaryContent = `---
|
|
154
|
+
date: ${now.getTime()}
|
|
155
|
+
tags: ["#summary/${frequency}", "#auto"]
|
|
156
|
+
title: ${frequency} Summary - ${taskName}
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
${result.summary}
|
|
160
|
+
`;
|
|
161
|
+
import_fs.default.writeFileSync(import_path.default.join(notesDir, summaryFilename), summaryContent);
|
|
162
|
+
console.log(`[Scheduler] Summary saved to ${summaryFilename}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/index.ts
|
|
166
|
+
var program = new import_commander.Command();
|
|
167
|
+
program.name("marlin-scheduler").description("CLI for Marlin scheduled tasks").version("0.0.1");
|
|
168
|
+
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").action(async (options) => {
|
|
169
|
+
try {
|
|
170
|
+
const taskName = options.taskName || process.env.INPUT_TASK_NAME || "Manual Task";
|
|
171
|
+
const frequency = options.frequency || process.env.INPUT_FREQUENCY || "weekly";
|
|
172
|
+
const tagsJson = options.tags || process.env.INPUT_TAGS || "[]";
|
|
173
|
+
const apiUrl = options.apiUrl || process.env.INPUT_API_URL || "https://marlin.app/api/ai/summarize";
|
|
174
|
+
const userId = options.userId || process.env.INPUT_USER_ID;
|
|
175
|
+
if (!userId) {
|
|
176
|
+
console.error("Error: User ID is required via --user-id or INPUT_USER_ID");
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
let tags = [];
|
|
180
|
+
try {
|
|
181
|
+
tags = JSON.parse(tagsJson);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.warn("Failed to parse tags JSON, assuming empty array:", e);
|
|
184
|
+
}
|
|
185
|
+
await summarize({
|
|
186
|
+
taskName,
|
|
187
|
+
frequency,
|
|
188
|
+
tags,
|
|
189
|
+
apiUrl,
|
|
190
|
+
userId
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error("Failed:", error);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
program.parse();
|
|
198
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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://marlin.app/api/ai/summarize\";\n const userId = options.userId || process.env.INPUT_USER_ID;\n\n if (!userId) {\n console.error(\"Error: User ID is required via --user-id or INPUT_USER_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 }\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":["fs","path","matter"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marlin-notes/scheduler-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI tool for Marlin scheduled tasks",
|
|
5
|
+
"bin": {
|
|
6
|
+
"marlin-scheduler": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"marlin",
|
|
10
|
+
"scheduler",
|
|
11
|
+
"cli"
|
|
12
|
+
],
|
|
13
|
+
"author": "Marlin Team",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^13.0.0",
|
|
17
|
+
"date-fns": "^4.1.0",
|
|
18
|
+
"gray-matter": "^4.0.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"tsup": "^8.0.0",
|
|
23
|
+
"typescript": "^5.0.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { summarize } from "./summarizer";
|
|
3
|
+
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name("marlin-scheduler")
|
|
8
|
+
.description("CLI for Marlin scheduled tasks")
|
|
9
|
+
.version("0.0.1");
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command("summarize")
|
|
13
|
+
.description("Run summarization task")
|
|
14
|
+
.option("--task-name <name>", "Name of the task")
|
|
15
|
+
.option("--frequency <freq>", "Frequency (weekly/monthly)")
|
|
16
|
+
.option("--tags <json>", "JSON string of tags")
|
|
17
|
+
.option("--api-url <url>", "Marlin API URL")
|
|
18
|
+
.option("--user-id <id>", "GitHub User ID")
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
try {
|
|
21
|
+
// Prioritize flags, then Env Vars (standard Action inputs often use INPUT_ prefix)
|
|
22
|
+
const taskName = options.taskName || process.env.INPUT_TASK_NAME || "Manual Task";
|
|
23
|
+
const frequency = options.frequency || process.env.INPUT_FREQUENCY || "weekly";
|
|
24
|
+
const tagsJson = options.tags || process.env.INPUT_TAGS || "[]";
|
|
25
|
+
// Default to production API if not specified
|
|
26
|
+
const apiUrl = options.apiUrl || process.env.INPUT_API_URL || "https://marlin.app/api/ai/summarize";
|
|
27
|
+
const userId = options.userId || process.env.INPUT_USER_ID;
|
|
28
|
+
|
|
29
|
+
if (!userId) {
|
|
30
|
+
console.error("Error: User ID is required via --user-id or INPUT_USER_ID");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let tags: string[] = [];
|
|
35
|
+
try {
|
|
36
|
+
tags = JSON.parse(tagsJson);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.warn("Failed to parse tags JSON, assuming empty array:", e);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await summarize({
|
|
42
|
+
taskName,
|
|
43
|
+
frequency: frequency as "weekly" | "monthly",
|
|
44
|
+
tags,
|
|
45
|
+
apiUrl,
|
|
46
|
+
userId,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("Failed:", error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
program.parse();
|
package/src/oidc.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export async function getGithubOidcToken(audience?: string): Promise<string | null> {
|
|
2
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
3
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
4
|
+
|
|
5
|
+
if (!requestUrl || !requestToken) {
|
|
6
|
+
// Not running in GitHub Actions or permissions not set
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(requestUrl);
|
|
12
|
+
if (audience) {
|
|
13
|
+
url.searchParams.append("audience", audience);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const response = await fetch(url.toString(), {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${requestToken}`,
|
|
19
|
+
Accept: "application/json; api-version=2.0",
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
console.warn(`[OIDC] Failed to fetch token: ${response.status} ${text}`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = (await response.json()) as { value: string };
|
|
31
|
+
return data.value;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn("[OIDC] Error fetching token:", error);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { subDays, subMonths, isBefore, isAfter } from "date-fns";
|
|
5
|
+
import { getGithubOidcToken } from "./oidc";
|
|
6
|
+
|
|
7
|
+
interface SummarizeOptions {
|
|
8
|
+
taskName: string;
|
|
9
|
+
frequency: "weekly" | "monthly";
|
|
10
|
+
tags: string[];
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function summarize({
|
|
16
|
+
taskName,
|
|
17
|
+
frequency,
|
|
18
|
+
tags,
|
|
19
|
+
apiUrl,
|
|
20
|
+
userId,
|
|
21
|
+
}: SummarizeOptions) {
|
|
22
|
+
// ... (existing date logic unchanged) ...
|
|
23
|
+
const now = new Date();
|
|
24
|
+
let startDate: Date;
|
|
25
|
+
|
|
26
|
+
if (frequency === "weekly") {
|
|
27
|
+
startDate = subDays(now, 7);
|
|
28
|
+
} else if (frequency === "monthly") {
|
|
29
|
+
startDate = subMonths(now, 1);
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(`Unknown frequency: ${frequency}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`[Scheduler] Running '${taskName}' (${frequency})`);
|
|
35
|
+
console.log(`[Scheduler] Filtering from: ${startDate.toISOString()}`);
|
|
36
|
+
console.log(`[Scheduler] Tags: ${tags.join(", ") || "(none)"}`);
|
|
37
|
+
|
|
38
|
+
const notesDir = process.cwd(); // Run in current dir
|
|
39
|
+
const files = fs.readdirSync(notesDir).filter((f) => f.endsWith(".md"));
|
|
40
|
+
const matchedNotes = [];
|
|
41
|
+
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const filePath = path.join(notesDir, file);
|
|
44
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
45
|
+
const { data, content: body } = matter(content);
|
|
46
|
+
|
|
47
|
+
// 1. Date Check
|
|
48
|
+
let noteDate: Date | null = null;
|
|
49
|
+
|
|
50
|
+
if (data.date) {
|
|
51
|
+
if (typeof data.date === "number") {
|
|
52
|
+
noteDate = new Date(data.date);
|
|
53
|
+
} else if (data.date instanceof Date) {
|
|
54
|
+
noteDate = data.date;
|
|
55
|
+
} else {
|
|
56
|
+
noteDate = new Date(data.date);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const filenameTs = parseInt(file.replace(".md", ""));
|
|
60
|
+
if (!isNaN(filenameTs) && filenameTs > 1000000000000) {
|
|
61
|
+
noteDate = new Date(filenameTs);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!noteDate || isNaN(noteDate.getTime())) continue;
|
|
66
|
+
|
|
67
|
+
if (isBefore(noteDate, startDate) || isAfter(noteDate, now)) continue;
|
|
68
|
+
|
|
69
|
+
// 2. Tag Check
|
|
70
|
+
const noteTags: string[] = Array.isArray(data.tags) ? data.tags : [];
|
|
71
|
+
const normalizedNoteTags = noteTags.map(String);
|
|
72
|
+
|
|
73
|
+
const hasAllTags = tags.every((t) => normalizedNoteTags.includes(t));
|
|
74
|
+
|
|
75
|
+
if (hasAllTags || tags.length === 0) {
|
|
76
|
+
matchedNotes.push({
|
|
77
|
+
title: data.title || file.replace(".md", ""),
|
|
78
|
+
content: body,
|
|
79
|
+
date: noteDate.toISOString(),
|
|
80
|
+
tags: normalizedNoteTags,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`[Scheduler] Found ${matchedNotes.length} matching notes.`);
|
|
86
|
+
|
|
87
|
+
if (matchedNotes.length === 0) {
|
|
88
|
+
console.log("No notes to summarize.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. API Call
|
|
93
|
+
console.log(`[Scheduler] Sending to API: ${apiUrl}`);
|
|
94
|
+
|
|
95
|
+
// Try to get OIDC token
|
|
96
|
+
const oidcToken = await getGithubOidcToken("marlin-api");
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
"X-Marlin-User-Id": userId,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (oidcToken) {
|
|
103
|
+
console.log("[Scheduler] Authenticating with GitHub OIDC");
|
|
104
|
+
headers["Authorization"] = `Bearer ${oidcToken}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const response = await fetch(apiUrl, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers,
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
notes: matchedNotes,
|
|
112
|
+
taskName,
|
|
113
|
+
period: frequency,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const errText = await response.text();
|
|
119
|
+
throw new Error(`API Error ${response.status}: ${errText}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = (await response.json()) as { summary: string };
|
|
123
|
+
|
|
124
|
+
// 4. Save Summary
|
|
125
|
+
const summaryFilename = `summary-${frequency}-${now.toISOString().split("T")[0]}.md`;
|
|
126
|
+
|
|
127
|
+
const summaryContent = `---
|
|
128
|
+
date: ${now.getTime()}
|
|
129
|
+
tags: ["#summary/${frequency}", "#auto"]
|
|
130
|
+
title: ${frequency} Summary - ${taskName}
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
${result.summary}
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
fs.writeFileSync(path.join(notesDir, summaryFilename), summaryContent);
|
|
137
|
+
console.log(`[Scheduler] Summary saved to ${summaryFilename}`);
|
|
138
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./dist"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"]
|
|
13
|
+
}
|
package/tsup.config.ts
ADDED