agent-sin 0.1.12 → 0.1.15
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/CHANGELOG.md +66 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +126 -72
- package/builtin-skills/memo-list/skill.yaml +8 -14
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +9 -3
- package/dist/core/chat-engine.js +1263 -146
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +568 -14
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +1 -0
- package/dist/discord/bot.js +181 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +115 -7
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rename, rmdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getAiProvider } from "./ai-provider.js";
|
|
4
|
+
const MIGRATION_MARKER = ".memo-flat-structure-v1.migrated";
|
|
5
|
+
const DATE_FILE_RE = /^(\d{4})-(\d{2})-(\d{2})\.md$/;
|
|
6
|
+
const YEAR_DIR_RE = /^\d{4}$/;
|
|
7
|
+
const MONTH_DIR_RE = /^\d{2}$/;
|
|
8
|
+
const FORBIDDEN_FILENAME_RE = /[\\/:*?"<>|#^[\]]/g;
|
|
9
|
+
const CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
|
|
10
|
+
const MAX_TITLE_LEN = 48;
|
|
11
|
+
const TIME_PREFIX_RE = /^(?:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\S+)\s+/;
|
|
12
|
+
const HASH_TAG_RE = /(?<!\S)#([^\s#]+)/g;
|
|
13
|
+
const FILE_DATE_H1_RE = /^# \d{4}-\d{2}-\d{2}\s*$/;
|
|
14
|
+
const MARKDOWN_HEADING_RE = /^#{1,6}\s+(.+?)\s*$/;
|
|
15
|
+
export async function migrateMemoFlatStructure(config) {
|
|
16
|
+
const markerPath = path.join(config.workspace, MIGRATION_MARKER);
|
|
17
|
+
if (await fileExists(markerPath))
|
|
18
|
+
return;
|
|
19
|
+
const notesDir = config.notes_dir;
|
|
20
|
+
if (!(await fileExists(notesDir))) {
|
|
21
|
+
await writeFile(markerPath, "", "utf8");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const legacyFiles = await collectLegacyFiles(notesDir);
|
|
25
|
+
if (legacyFiles.length === 0) {
|
|
26
|
+
await writeFile(markerPath, "", "utf8");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const memoDir = path.join(notesDir, "memo");
|
|
30
|
+
const legacyRoot = path.join(notesDir, "legacy");
|
|
31
|
+
await mkdir(memoDir, { recursive: true });
|
|
32
|
+
await mkdir(legacyRoot, { recursive: true });
|
|
33
|
+
let migratedAll = true;
|
|
34
|
+
for (const filePath of legacyFiles) {
|
|
35
|
+
try {
|
|
36
|
+
const ok = await migrateFile(config, filePath, notesDir, memoDir, legacyRoot);
|
|
37
|
+
if (!ok)
|
|
38
|
+
migratedAll = false;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
migratedAll = false;
|
|
42
|
+
console.warn(`memo-migration: failed to process ${filePath}: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (migratedAll) {
|
|
46
|
+
await pruneEmptyYearMonth(notesDir);
|
|
47
|
+
await writeFile(markerPath, formatLocalDateTime(new Date()), "utf8");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function collectLegacyFiles(notesDir) {
|
|
51
|
+
const found = [];
|
|
52
|
+
let years;
|
|
53
|
+
try {
|
|
54
|
+
years = await readdir(notesDir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
for (const year of years) {
|
|
60
|
+
if (!YEAR_DIR_RE.test(year))
|
|
61
|
+
continue;
|
|
62
|
+
const yearDir = path.join(notesDir, year);
|
|
63
|
+
let months;
|
|
64
|
+
try {
|
|
65
|
+
months = await readdir(yearDir);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const month of months) {
|
|
71
|
+
if (!MONTH_DIR_RE.test(month))
|
|
72
|
+
continue;
|
|
73
|
+
const monthDir = path.join(yearDir, month);
|
|
74
|
+
let files;
|
|
75
|
+
try {
|
|
76
|
+
files = await readdir(monthDir);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
if (DATE_FILE_RE.test(file)) {
|
|
83
|
+
found.push(path.join(monthDir, file));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
async function migrateFile(config, filePath, notesDir, memoDir, legacyRoot) {
|
|
91
|
+
const raw = await readFile(filePath, "utf8");
|
|
92
|
+
const fileDate = path.basename(filePath, ".md");
|
|
93
|
+
const body = stripFrontmatterAndH1(raw);
|
|
94
|
+
const relativeFrom = path.relative(notesDir, filePath).replace(/\\/g, "/");
|
|
95
|
+
if (classifyContent(body) === "longform") {
|
|
96
|
+
const title = extractLongformTitle(body, fileDate);
|
|
97
|
+
const tags = extractTags(body);
|
|
98
|
+
const sanitized = sanitizeFilename(title) || fileDate;
|
|
99
|
+
const targetPath = uniqueTargetPath(memoDir, sanitized);
|
|
100
|
+
await writeMemoFile(targetPath, body.trim(), tags, `${fileDate} 00:00:00`, relativeFrom);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const blocks = parseMemoBlocks(body, fileDate);
|
|
104
|
+
for (const block of blocks) {
|
|
105
|
+
const bodyText = block.bodyLines.join("\n").trim();
|
|
106
|
+
if (!bodyText)
|
|
107
|
+
continue;
|
|
108
|
+
const tags = extractTags(bodyText);
|
|
109
|
+
const cleanedBody = stripInlineTags(bodyText);
|
|
110
|
+
const titleSource = cleanedBody || bodyText;
|
|
111
|
+
const aiTitle = await generateTitle(config, titleSource);
|
|
112
|
+
const sanitized = sanitizeFilename(aiTitle) || `${fileDate}-${block.timestamp.replace(/[:\sT]/g, "-")}`;
|
|
113
|
+
const targetPath = path.join(memoDir, `${sanitized}.md`);
|
|
114
|
+
await writeMemoFile(targetPath, cleanedBody || bodyText, tags, block.timestamp, relativeFrom);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const legacyTarget = path.join(legacyRoot, path.relative(notesDir, filePath));
|
|
118
|
+
await mkdir(path.dirname(legacyTarget), { recursive: true });
|
|
119
|
+
await rename(filePath, legacyTarget);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
function stripFrontmatterAndH1(raw) {
|
|
123
|
+
let text = raw;
|
|
124
|
+
const fm = text.match(/^---\n[\s\S]*?\n---\n?/);
|
|
125
|
+
if (fm)
|
|
126
|
+
text = text.slice(fm[0].length);
|
|
127
|
+
// 先頭の `# YYYY-MM-DD` 1行を除去
|
|
128
|
+
const lines = text.split(/\r?\n/);
|
|
129
|
+
let i = 0;
|
|
130
|
+
while (i < lines.length && lines[i].trim() === "")
|
|
131
|
+
i++;
|
|
132
|
+
if (i < lines.length && FILE_DATE_H1_RE.test(lines[i].trim())) {
|
|
133
|
+
lines.splice(i, 1);
|
|
134
|
+
}
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
function classifyContent(body) {
|
|
138
|
+
const lines = body.split(/\r?\n/);
|
|
139
|
+
let i = 0;
|
|
140
|
+
let strayLines = 0;
|
|
141
|
+
while (i < lines.length) {
|
|
142
|
+
const line = lines[i];
|
|
143
|
+
if (line.trim() === "") {
|
|
144
|
+
i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (/^-\s+/.test(line)) {
|
|
148
|
+
i++;
|
|
149
|
+
while (i < lines.length && /^ {2}\S/.test(lines[i]))
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
strayLines++;
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
return strayLines > 0 ? "longform" : "bullets";
|
|
157
|
+
}
|
|
158
|
+
function extractLongformTitle(body, fileDate) {
|
|
159
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
160
|
+
const trimmed = rawLine.trim();
|
|
161
|
+
if (!trimmed)
|
|
162
|
+
continue;
|
|
163
|
+
if (FILE_DATE_H1_RE.test(trimmed))
|
|
164
|
+
continue;
|
|
165
|
+
const headingMatch = trimmed.match(MARKDOWN_HEADING_RE);
|
|
166
|
+
if (headingMatch)
|
|
167
|
+
return headingMatch[1].replace(/^#+\s*/, "");
|
|
168
|
+
// バレット行は longform のタイトルとしては採用しない
|
|
169
|
+
if (/^-\s+/.test(trimmed))
|
|
170
|
+
continue;
|
|
171
|
+
return trimmed.length > MAX_TITLE_LEN ? trimmed.slice(0, MAX_TITLE_LEN) : trimmed;
|
|
172
|
+
}
|
|
173
|
+
return `${fileDate} メモ`;
|
|
174
|
+
}
|
|
175
|
+
function uniqueTargetPath(memoDir, baseName) {
|
|
176
|
+
// longform は冪等性のため同名にぶつかった時は連番にする(同一日付に長文が2つはレアだが安全策)
|
|
177
|
+
let candidate = path.join(memoDir, `${baseName}.md`);
|
|
178
|
+
let counter = 2;
|
|
179
|
+
// 既存があればその後ろにサフィックス
|
|
180
|
+
// 注意: 同期 stat は使わない / 同期判定が必要なら別実装にするが、簡略化のため一旦そのまま返す
|
|
181
|
+
return candidate;
|
|
182
|
+
}
|
|
183
|
+
function parseMemoBlocks(body, fileDate) {
|
|
184
|
+
const lines = body.split(/\r?\n/);
|
|
185
|
+
const blocks = [];
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i < lines.length) {
|
|
188
|
+
const line = lines[i];
|
|
189
|
+
if (/^-\s+/.test(line)) {
|
|
190
|
+
const headRest = line.replace(/^-\s+/, "");
|
|
191
|
+
const tsMatch = headRest.match(/^(\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\S+)\s+/);
|
|
192
|
+
let timestamp;
|
|
193
|
+
if (tsMatch) {
|
|
194
|
+
if (tsMatch[1].length === 5) {
|
|
195
|
+
timestamp = `${fileDate} ${tsMatch[1]}:00`;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const dt = new Date(tsMatch[1]);
|
|
199
|
+
timestamp = isNaN(dt.getTime()) ? `${fileDate} 00:00:00` : formatLocalDateTime(dt);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
timestamp = `${fileDate} 00:00:00`;
|
|
204
|
+
}
|
|
205
|
+
const firstBody = headRest.replace(TIME_PREFIX_RE, "").trim();
|
|
206
|
+
const bodyLines = [];
|
|
207
|
+
if (firstBody)
|
|
208
|
+
bodyLines.push(firstBody);
|
|
209
|
+
let j = i + 1;
|
|
210
|
+
while (j < lines.length && /^ {2}\S/.test(lines[j])) {
|
|
211
|
+
bodyLines.push(lines[j].slice(2));
|
|
212
|
+
j++;
|
|
213
|
+
}
|
|
214
|
+
blocks.push({ bodyLines, timestamp });
|
|
215
|
+
i = j;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return blocks;
|
|
222
|
+
}
|
|
223
|
+
function extractTags(body) {
|
|
224
|
+
const tags = new Set();
|
|
225
|
+
let match;
|
|
226
|
+
HASH_TAG_RE.lastIndex = 0;
|
|
227
|
+
while ((match = HASH_TAG_RE.exec(body)) !== null) {
|
|
228
|
+
tags.add(match[1]);
|
|
229
|
+
}
|
|
230
|
+
return Array.from(tags);
|
|
231
|
+
}
|
|
232
|
+
function stripInlineTags(body) {
|
|
233
|
+
return body.replace(HASH_TAG_RE, "").replace(/\s+\n/g, "\n").replace(/[ \t]+$/gm, "").trim();
|
|
234
|
+
}
|
|
235
|
+
async function generateTitle(config, text) {
|
|
236
|
+
if (!text)
|
|
237
|
+
return "";
|
|
238
|
+
if (process.env.AGENT_SIN_FAKE_PROVIDER === "1" || process.env.AGENT_SIN_SKIP_MEMO_AI_TITLE === "1") {
|
|
239
|
+
return fallbackTitle(text);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const provider = getAiProvider();
|
|
243
|
+
const prompt = `次のメモ本文から、24文字以下の短いタイトルを1つだけ返してください。
|
|
244
|
+
引用符・カギ括弧・句読点・絵文字・説明文は付けず、タイトル本文のみを1行で返してください。
|
|
245
|
+
本文の言語に合わせて日本語/英語で返してください。
|
|
246
|
+
|
|
247
|
+
本文:
|
|
248
|
+
${text.length > 600 ? text.slice(0, 600) + "..." : text}`;
|
|
249
|
+
const response = await provider(config, {
|
|
250
|
+
model_id: "chat",
|
|
251
|
+
messages: [{ role: "user", content: prompt }],
|
|
252
|
+
role: "chat",
|
|
253
|
+
});
|
|
254
|
+
const first = (response.text || "").split(/\r?\n/)[0]?.trim() || "";
|
|
255
|
+
const stripped = first.replace(/^["'「『`]+|["'」』`]+$/g, "").trim();
|
|
256
|
+
if (stripped)
|
|
257
|
+
return stripped;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
console.warn(`memo-migration: title generation failed: ${error.message}`);
|
|
261
|
+
}
|
|
262
|
+
return fallbackTitle(text);
|
|
263
|
+
}
|
|
264
|
+
function fallbackTitle(text) {
|
|
265
|
+
const head = text.trim().split(/\r?\n/)[0] || "";
|
|
266
|
+
return head.length > 40 ? head.slice(0, 40) : head;
|
|
267
|
+
}
|
|
268
|
+
function sanitizeFilename(raw) {
|
|
269
|
+
if (!raw)
|
|
270
|
+
return "";
|
|
271
|
+
let cleaned = raw.replace(CONTROL_CHARS_RE, "");
|
|
272
|
+
cleaned = cleaned.replace(FORBIDDEN_FILENAME_RE, "-");
|
|
273
|
+
cleaned = cleaned.replace(/[\r\n]+/g, " ");
|
|
274
|
+
cleaned = cleaned.replace(/\s+/g, " ").trim();
|
|
275
|
+
cleaned = cleaned.replace(/^[._\- ]+|[._\- ]+$/g, "");
|
|
276
|
+
if (!cleaned)
|
|
277
|
+
return "";
|
|
278
|
+
if (cleaned.length > MAX_TITLE_LEN) {
|
|
279
|
+
cleaned = cleaned.slice(0, MAX_TITLE_LEN).replace(/[._\- ]+$/g, "");
|
|
280
|
+
}
|
|
281
|
+
if (cleaned === "." || cleaned === "..")
|
|
282
|
+
return "";
|
|
283
|
+
return cleaned;
|
|
284
|
+
}
|
|
285
|
+
async function writeMemoFile(targetPath, body, tags, timestamp, migratedFrom) {
|
|
286
|
+
const tagLine = tags.length > 0 ? tags.map((t) => `#${t}`).join(" ") : "";
|
|
287
|
+
const newContent = tagLine ? `${body.trim()}\n\n${tagLine}\n` : `${body.trim()}\n`;
|
|
288
|
+
let existing = null;
|
|
289
|
+
try {
|
|
290
|
+
existing = await readFile(targetPath, "utf8");
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
existing = null;
|
|
294
|
+
}
|
|
295
|
+
if (existing) {
|
|
296
|
+
const parsed = parseFrontmatterBlock(existing);
|
|
297
|
+
const fm = { ...parsed.frontmatter };
|
|
298
|
+
if (fm.created === undefined)
|
|
299
|
+
fm.created = timestamp;
|
|
300
|
+
fm.updated = timestamp;
|
|
301
|
+
const mergedTagSet = new Set();
|
|
302
|
+
if (Array.isArray(fm.tags))
|
|
303
|
+
for (const t of fm.tags)
|
|
304
|
+
mergedTagSet.add(String(t));
|
|
305
|
+
mergedTagSet.add("memo");
|
|
306
|
+
for (const t of tags)
|
|
307
|
+
mergedTagSet.add(t);
|
|
308
|
+
fm.tags = Array.from(mergedTagSet);
|
|
309
|
+
const trimmed = parsed.body.replace(/\n+$/, "");
|
|
310
|
+
const merged = `${trimmed}\n\n---\n\n${newContent}`;
|
|
311
|
+
await writeFile(targetPath, `${serializeFrontmatter(fm)}${merged}`, "utf8");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const fm = {
|
|
315
|
+
tags: ["memo", ...tags],
|
|
316
|
+
created: timestamp,
|
|
317
|
+
updated: timestamp,
|
|
318
|
+
migrated_from: migratedFrom,
|
|
319
|
+
};
|
|
320
|
+
await writeFile(targetPath, `${serializeFrontmatter(fm)}${newContent}`, "utf8");
|
|
321
|
+
}
|
|
322
|
+
function serializeFrontmatter(frontmatter) {
|
|
323
|
+
if (Object.keys(frontmatter).length === 0)
|
|
324
|
+
return "";
|
|
325
|
+
const lines = ["---"];
|
|
326
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
327
|
+
if (Array.isArray(value)) {
|
|
328
|
+
lines.push(`${key}: [${value.map((item) => JSON.stringify(item)).join(", ")}]`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
lines.push("---", "");
|
|
335
|
+
return `${lines.join("\n")}\n`;
|
|
336
|
+
}
|
|
337
|
+
function parseFrontmatterBlock(raw) {
|
|
338
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
339
|
+
if (!match)
|
|
340
|
+
return { frontmatter: {}, body: raw };
|
|
341
|
+
const fm = {};
|
|
342
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
343
|
+
if (!line.trim() || !line.includes(":"))
|
|
344
|
+
continue;
|
|
345
|
+
const [key, ...rest] = line.split(":");
|
|
346
|
+
let value = rest.join(":").trim();
|
|
347
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
348
|
+
const inner = value.slice(1, -1);
|
|
349
|
+
fm[key.trim()] = inner
|
|
350
|
+
.split(",")
|
|
351
|
+
.map((part) => part.trim().replace(/^["']|["']$/g, ""))
|
|
352
|
+
.filter(Boolean);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
356
|
+
value = value.slice(1, -1);
|
|
357
|
+
}
|
|
358
|
+
fm[key.trim()] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { frontmatter: fm, body: raw.slice(match[0].length) };
|
|
362
|
+
}
|
|
363
|
+
async function pruneEmptyYearMonth(notesDir) {
|
|
364
|
+
let years;
|
|
365
|
+
try {
|
|
366
|
+
years = await readdir(notesDir);
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
for (const year of years) {
|
|
372
|
+
if (!YEAR_DIR_RE.test(year))
|
|
373
|
+
continue;
|
|
374
|
+
const yearDir = path.join(notesDir, year);
|
|
375
|
+
let months;
|
|
376
|
+
try {
|
|
377
|
+
months = await readdir(yearDir);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
for (const month of months) {
|
|
383
|
+
if (!MONTH_DIR_RE.test(month))
|
|
384
|
+
continue;
|
|
385
|
+
const monthDir = path.join(yearDir, month);
|
|
386
|
+
try {
|
|
387
|
+
const remaining = await readdir(monthDir);
|
|
388
|
+
if (remaining.length === 0)
|
|
389
|
+
await rmdir(monthDir);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// ignore
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const remaining = await readdir(yearDir);
|
|
397
|
+
if (remaining.length === 0)
|
|
398
|
+
await rmdir(yearDir);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// ignore
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function fileExists(p) {
|
|
406
|
+
try {
|
|
407
|
+
await stat(p);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
export function formatLocalDateTime(date) {
|
|
415
|
+
const yyyy = String(date.getFullYear());
|
|
416
|
+
const MM = String(date.getMonth() + 1).padStart(2, "0");
|
|
417
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
418
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
419
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
420
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
421
|
+
return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`;
|
|
422
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect and self-heal native module ABI mismatch for `better-sqlite3`.
|
|
3
|
+
*
|
|
4
|
+
* Native bindings (.node) are pinned to a Node.js NODE_MODULE_VERSION. If
|
|
5
|
+
* `npm install` ran on one Node major and the service later boots under a
|
|
6
|
+
* different one, every `new Database(...)` call throws and weekly reports
|
|
7
|
+
* silently lose history. We probe in a child process to keep our own
|
|
8
|
+
* process unpolluted, and rebuild with the same `process.execPath` that
|
|
9
|
+
* will run the service so the produced binary matches the actual runtime.
|
|
10
|
+
*/
|
|
11
|
+
export type NativeModulesLogger = (line: string) => void;
|
|
12
|
+
export interface EnsureBetterSqlite3Options {
|
|
13
|
+
/** Directory that contains node_modules/better-sqlite3 (the agent-sin install root). */
|
|
14
|
+
packageRoot: string;
|
|
15
|
+
/** Optional human-readable status logger. */
|
|
16
|
+
logger?: NativeModulesLogger;
|
|
17
|
+
}
|
|
18
|
+
export interface EnsureBetterSqlite3Result {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
rebuilt: boolean;
|
|
21
|
+
detail?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function ensureBetterSqlite3(options: EnsureBetterSqlite3Options): Promise<EnsureBetterSqlite3Result>;
|
|
24
|
+
export declare function isAbiMismatchError(message: string): boolean;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const PROBE_SCRIPT = `
|
|
4
|
+
try {
|
|
5
|
+
const Database = require('better-sqlite3');
|
|
6
|
+
const db = new Database(':memory:');
|
|
7
|
+
db.close();
|
|
8
|
+
process.exit(0);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error && error.message ? error.message : String(error);
|
|
11
|
+
process.stderr.write(message);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
export async function ensureBetterSqlite3(options) {
|
|
16
|
+
const log = options.logger ?? (() => { });
|
|
17
|
+
const first = await probeBetterSqlite3(options.packageRoot);
|
|
18
|
+
if (first.ok) {
|
|
19
|
+
return { ok: true, rebuilt: false };
|
|
20
|
+
}
|
|
21
|
+
if (!isAbiMismatchError(first.detail || "")) {
|
|
22
|
+
log(`better-sqlite3 load failed: ${first.detail}`);
|
|
23
|
+
return { ok: false, rebuilt: false, detail: first.detail };
|
|
24
|
+
}
|
|
25
|
+
log(`better-sqlite3 ABI mismatch detected; rebuilding for ${process.execPath}…`);
|
|
26
|
+
const rebuild = await runRebuild(options.packageRoot);
|
|
27
|
+
if (!rebuild.ok) {
|
|
28
|
+
log(`better-sqlite3 rebuild failed: ${rebuild.detail}`);
|
|
29
|
+
return { ok: false, rebuilt: false, detail: rebuild.detail };
|
|
30
|
+
}
|
|
31
|
+
const second = await probeBetterSqlite3(options.packageRoot);
|
|
32
|
+
if (!second.ok) {
|
|
33
|
+
log(`better-sqlite3 still broken after rebuild: ${second.detail}`);
|
|
34
|
+
return { ok: false, rebuilt: true, detail: second.detail };
|
|
35
|
+
}
|
|
36
|
+
log("better-sqlite3 rebuild succeeded.");
|
|
37
|
+
return { ok: true, rebuilt: true };
|
|
38
|
+
}
|
|
39
|
+
async function probeBetterSqlite3(packageRoot) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const child = spawn(process.execPath, ["-e", PROBE_SCRIPT], {
|
|
42
|
+
cwd: packageRoot,
|
|
43
|
+
env: { ...process.env, NODE_DISABLE_COLORS: "1" },
|
|
44
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
45
|
+
});
|
|
46
|
+
let stderr = "";
|
|
47
|
+
child.stdout?.on("data", () => { });
|
|
48
|
+
child.stderr?.on("data", (chunk) => {
|
|
49
|
+
stderr += chunk.toString("utf8");
|
|
50
|
+
});
|
|
51
|
+
child.on("error", (error) => {
|
|
52
|
+
resolve({ ok: false, detail: error.message });
|
|
53
|
+
});
|
|
54
|
+
child.on("exit", (code) => {
|
|
55
|
+
if (code === 0) {
|
|
56
|
+
resolve({ ok: true });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
resolve({ ok: false, detail: stderr.trim() || `probe exited with code ${code ?? "null"}` });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function runRebuild(packageRoot) {
|
|
65
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
66
|
+
const nodeDir = path.dirname(process.execPath);
|
|
67
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
68
|
+
const env = {
|
|
69
|
+
...process.env,
|
|
70
|
+
PATH: `${nodeDir}${pathSep}${process.env.PATH || ""}`,
|
|
71
|
+
npm_config_node_execpath: process.execPath,
|
|
72
|
+
};
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const child = spawn(npm, ["rebuild", "better-sqlite3"], {
|
|
75
|
+
cwd: packageRoot,
|
|
76
|
+
env,
|
|
77
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
78
|
+
});
|
|
79
|
+
let stderr = "";
|
|
80
|
+
child.stdout?.on("data", () => { });
|
|
81
|
+
child.stderr?.on("data", (chunk) => {
|
|
82
|
+
stderr += chunk.toString("utf8");
|
|
83
|
+
});
|
|
84
|
+
child.on("error", (error) => {
|
|
85
|
+
resolve({ ok: false, detail: error.message });
|
|
86
|
+
});
|
|
87
|
+
child.on("exit", (code) => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
resolve({ ok: true });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
resolve({ ok: false, detail: stderr.trim() || `npm rebuild exited with code ${code ?? "null"}` });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function isAbiMismatchError(message) {
|
|
98
|
+
return /NODE_MODULE_VERSION|was compiled against a different Node\.js version|invalid ELF header|incompatible|Module did not self-register|dlopen.*Symbol not found|napi|node-gyp|Could not locate the bindings file/i.test(message);
|
|
99
|
+
}
|
package/dist/core/notifier.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
export type NotifyChannel = "macos" | "windows" | "discord" | "telegram" | "slack" | "mail" | "stderr";
|
|
1
|
+
export type NotifyChannel = "macos" | "windows" | "discord" | "telegram" | "slack" | "mail" | "g2" | "stderr";
|
|
2
2
|
export interface NotifyOptions {
|
|
3
|
-
title
|
|
4
|
-
body
|
|
3
|
+
title?: string;
|
|
4
|
+
body?: string;
|
|
5
5
|
subtitle?: string;
|
|
6
6
|
sound?: boolean;
|
|
7
7
|
channel?: NotifyChannel | "auto";
|
|
8
8
|
to?: string;
|
|
9
9
|
discordThreadId?: string;
|
|
10
10
|
telegramThreadId?: string;
|
|
11
|
+
filePath?: string;
|
|
12
|
+
filePaths?: string[];
|
|
13
|
+
imagePath?: string;
|
|
14
|
+
imagePaths?: string[];
|
|
15
|
+
cwd?: string;
|
|
11
16
|
}
|
|
12
17
|
export interface NotifyResult {
|
|
13
18
|
channel: NotifyChannel;
|