agent-sin 0.1.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/assets/logo.png +0 -0
- package/builtin-skills/_shared/_models_lib.py +227 -0
- package/builtin-skills/_shared/_profile_lib.py +98 -0
- package/builtin-skills/_shared/_schedules_lib.py +313 -0
- package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
- package/builtin-skills/_shared/i18n.py +26 -0
- package/builtin-skills/memo-delete/main.py +155 -0
- package/builtin-skills/memo-delete/skill.yaml +57 -0
- package/builtin-skills/memo-index/main.py +178 -0
- package/builtin-skills/memo-index/skill.yaml +53 -0
- package/builtin-skills/memo-save/README.md +5 -0
- package/builtin-skills/memo-save/main.py +74 -0
- package/builtin-skills/memo-save/skill.yaml +52 -0
- package/builtin-skills/memo-search/README.md +10 -0
- package/builtin-skills/memo-search/main.py +97 -0
- package/builtin-skills/memo-search/skill.yaml +51 -0
- package/builtin-skills/memo-vector-search/main.py +121 -0
- package/builtin-skills/memo-vector-search/skill.yaml +53 -0
- package/builtin-skills/model-add/main.py +180 -0
- package/builtin-skills/model-add/skill.yaml +112 -0
- package/builtin-skills/model-list/main.py +93 -0
- package/builtin-skills/model-list/skill.yaml +48 -0
- package/builtin-skills/model-set/main.py +123 -0
- package/builtin-skills/model-set/skill.yaml +69 -0
- package/builtin-skills/profile-delete/_profile_lib.py +98 -0
- package/builtin-skills/profile-delete/main.py +98 -0
- package/builtin-skills/profile-delete/skill.yaml +64 -0
- package/builtin-skills/profile-edit/_profile_lib.py +98 -0
- package/builtin-skills/profile-edit/main.py +97 -0
- package/builtin-skills/profile-edit/skill.yaml +72 -0
- package/builtin-skills/profile-save/main.py +52 -0
- package/builtin-skills/profile-save/skill.yaml +69 -0
- package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-add/main.py +137 -0
- package/builtin-skills/schedule-add/skill.yaml +94 -0
- package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-list/main.py +86 -0
- package/builtin-skills/schedule-list/skill.yaml +45 -0
- package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-remove/main.py +69 -0
- package/builtin-skills/schedule-remove/skill.yaml +49 -0
- package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-toggle/main.py +78 -0
- package/builtin-skills/schedule-toggle/skill.yaml +61 -0
- package/builtin-skills/skills-disable/main.py +63 -0
- package/builtin-skills/skills-disable/skill.yaml +52 -0
- package/builtin-skills/skills-enable/main.py +62 -0
- package/builtin-skills/skills-enable/skill.yaml +51 -0
- package/builtin-skills/todo-add/main.py +68 -0
- package/builtin-skills/todo-add/skill.yaml +53 -0
- package/builtin-skills/todo-delete/main.py +65 -0
- package/builtin-skills/todo-delete/skill.yaml +47 -0
- package/builtin-skills/todo-done/main.py +75 -0
- package/builtin-skills/todo-done/skill.yaml +47 -0
- package/builtin-skills/todo-list/main.py +91 -0
- package/builtin-skills/todo-list/skill.yaml +48 -0
- package/builtin-skills/todo-tick/main.py +125 -0
- package/builtin-skills/todo-tick/skill.yaml +48 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +142 -0
- package/dist/builder/build-commands.d.ts +19 -0
- package/dist/builder/build-commands.js +133 -0
- package/dist/builder/build-flow.d.ts +72 -0
- package/dist/builder/build-flow.js +416 -0
- package/dist/builder/builder-session.d.ts +117 -0
- package/dist/builder/builder-session.js +1129 -0
- package/dist/builder/conversation-router.d.ts +22 -0
- package/dist/builder/conversation-router.js +69 -0
- package/dist/builder/intent-runtime-store.d.ts +7 -0
- package/dist/builder/intent-runtime-store.js +60 -0
- package/dist/builder/progress-format.d.ts +7 -0
- package/dist/builder/progress-format.js +46 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2835 -0
- package/dist/cli/spinner.d.ts +30 -0
- package/dist/cli/spinner.js +164 -0
- package/dist/core/ai-provider.d.ts +75 -0
- package/dist/core/ai-provider.js +678 -0
- package/dist/core/builtin-skills.d.ts +27 -0
- package/dist/core/builtin-skills.js +120 -0
- package/dist/core/chat-engine.d.ts +70 -0
- package/dist/core/chat-engine.js +812 -0
- package/dist/core/config.d.ts +127 -0
- package/dist/core/config.js +1379 -0
- package/dist/core/daily-memory-promotion.d.ts +21 -0
- package/dist/core/daily-memory-promotion.js +422 -0
- package/dist/core/i18n.d.ts +23 -0
- package/dist/core/i18n.js +167 -0
- package/dist/core/info-lines.d.ts +5 -0
- package/dist/core/info-lines.js +39 -0
- package/dist/core/input-schema.d.ts +2 -0
- package/dist/core/input-schema.js +156 -0
- package/dist/core/intent-router.d.ts +27 -0
- package/dist/core/intent-router.js +160 -0
- package/dist/core/logger.d.ts +60 -0
- package/dist/core/logger.js +240 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/memory.js +72 -0
- package/dist/core/message-utils.d.ts +13 -0
- package/dist/core/message-utils.js +104 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +424 -0
- package/dist/core/output-writer.d.ts +13 -0
- package/dist/core/output-writer.js +100 -0
- package/dist/core/plan-decision.d.ts +16 -0
- package/dist/core/plan-decision.js +88 -0
- package/dist/core/profile-memory.d.ts +17 -0
- package/dist/core/profile-memory.js +142 -0
- package/dist/core/runtime.d.ts +50 -0
- package/dist/core/runtime.js +187 -0
- package/dist/core/scheduler.d.ts +28 -0
- package/dist/core/scheduler.js +155 -0
- package/dist/core/secrets.d.ts +31 -0
- package/dist/core/secrets.js +214 -0
- package/dist/core/service.d.ts +35 -0
- package/dist/core/service.js +479 -0
- package/dist/core/skill-planner.d.ts +24 -0
- package/dist/core/skill-planner.js +100 -0
- package/dist/core/skill-registry.d.ts +98 -0
- package/dist/core/skill-registry.js +319 -0
- package/dist/core/skill-scaffold.d.ts +33 -0
- package/dist/core/skill-scaffold.js +256 -0
- package/dist/core/skill-settings.d.ts +11 -0
- package/dist/core/skill-settings.js +63 -0
- package/dist/core/transfer.d.ts +31 -0
- package/dist/core/transfer.js +270 -0
- package/dist/core/update-notifier.d.ts +2 -0
- package/dist/core/update-notifier.js +140 -0
- package/dist/discord/bot.d.ts +96 -0
- package/dist/discord/bot.js +2424 -0
- package/dist/runtimes/codex-app-server.d.ts +53 -0
- package/dist/runtimes/codex-app-server.js +305 -0
- package/dist/runtimes/python-runner.d.ts +7 -0
- package/dist/runtimes/python-runner.js +302 -0
- package/dist/runtimes/typescript-runner.d.ts +5 -0
- package/dist/runtimes/typescript-runner.js +172 -0
- package/dist/skills-sdk/types.d.ts +38 -0
- package/dist/skills-sdk/types.js +1 -0
- package/dist/telegram/bot.d.ts +94 -0
- package/dist/telegram/bot.js +1219 -0
- package/install.ps1 +132 -0
- package/install.sh +130 -0
- package/package.json +60 -0
- package/templates/skill-python/main.py +74 -0
- package/templates/skill-python/skill.yaml +48 -0
- package/templates/skill-typescript/main.ts +87 -0
- package/templates/skill-typescript/skill.yaml +42 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const VERSION_KEY = "__version";
|
|
4
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
5
|
+
export function skillMemoryAccess(manifest) {
|
|
6
|
+
return {
|
|
7
|
+
namespace: manifest.memory?.namespace || manifest.id,
|
|
8
|
+
canRead: manifest.memory?.read === true,
|
|
9
|
+
canWrite: manifest.memory?.write === true,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function loadSkillMemory(config, manifest) {
|
|
13
|
+
const access = skillMemoryAccess(manifest);
|
|
14
|
+
if (!access.canRead) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
const stored = await loadExistingMemory(memoryFile(config, access.namespace));
|
|
18
|
+
return stripVersion(stored);
|
|
19
|
+
}
|
|
20
|
+
export async function saveSkillMemoryUpdates(config, manifest, updates) {
|
|
21
|
+
const access = skillMemoryAccess(manifest);
|
|
22
|
+
if (!access.canWrite || Object.keys(updates).length === 0) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const file = memoryFile(config, access.namespace);
|
|
26
|
+
const current = await loadExistingMemory(file);
|
|
27
|
+
const next = {
|
|
28
|
+
[VERSION_KEY]: CURRENT_SCHEMA_VERSION,
|
|
29
|
+
...stripVersion(current),
|
|
30
|
+
...updates,
|
|
31
|
+
};
|
|
32
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
33
|
+
await writeFile(file, JSON.stringify(next, null, 2), "utf8");
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
function memoryFile(config, namespace) {
|
|
37
|
+
return path.join(config.memory_dir, "skill-memory", `${safeName(namespace)}.json`);
|
|
38
|
+
}
|
|
39
|
+
async function loadExistingMemory(file) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
42
|
+
if (!isPlainObject(parsed)) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
return migrateSkillMemory(parsed);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function migrateSkillMemory(raw) {
|
|
52
|
+
const version = typeof raw[VERSION_KEY] === "number" ? raw[VERSION_KEY] : 1;
|
|
53
|
+
// Future migrations branch on `version` here. For now we only support v1.
|
|
54
|
+
if (version > CURRENT_SCHEMA_VERSION) {
|
|
55
|
+
// Newer-than-known data: pass through untouched so we don't lose fields.
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
return { [VERSION_KEY]: CURRENT_SCHEMA_VERSION, ...stripVersion(raw) };
|
|
59
|
+
}
|
|
60
|
+
function stripVersion(value) {
|
|
61
|
+
if (!(VERSION_KEY in value)) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const { [VERSION_KEY]: _omit, ...rest } = value;
|
|
65
|
+
return rest;
|
|
66
|
+
}
|
|
67
|
+
function safeName(value) {
|
|
68
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
69
|
+
}
|
|
70
|
+
function isPlainObject(value) {
|
|
71
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function cleanAttachmentText(text: string, maxChars: number): string;
|
|
2
|
+
export declare function indentAttachmentContent(text: string): string;
|
|
3
|
+
export declare function formatBytes(value: number): string;
|
|
4
|
+
export declare function isTextLikeFile(contentType: string | undefined, filename: string | undefined): boolean;
|
|
5
|
+
export declare function isImageLikeFile(contentType: string | undefined, filename: string | undefined): boolean;
|
|
6
|
+
export declare function guessImageMimeType(filename: string | undefined): string;
|
|
7
|
+
export declare function formatAttachmentLabel(input: {
|
|
8
|
+
name: string | undefined;
|
|
9
|
+
fallback: string;
|
|
10
|
+
contentType?: string;
|
|
11
|
+
size?: number;
|
|
12
|
+
}): string;
|
|
13
|
+
export declare function chunkText(text: string, max: number): string[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { l } from "./i18n.js";
|
|
2
|
+
const TEXT_EXT_PATTERN = /\.(txt|md|markdown|json|jsonl|csv|tsv|yaml|yml|xml|html|css|js|jsx|ts|tsx|mjs|cjs|py|rb|php|java|kt|go|rs|swift|c|cc|cpp|h|hpp|sh|bash|zsh|fish|sql|toml|ini|env|log)$/i;
|
|
3
|
+
const IMAGE_EXT_PATTERN = /\.(png|jpe?g|gif|webp|bmp|heic|heif)$/i;
|
|
4
|
+
const TEXT_CONTENT_TYPES = new Set([
|
|
5
|
+
"application/json",
|
|
6
|
+
"application/ld+json",
|
|
7
|
+
"application/xml",
|
|
8
|
+
"application/yaml",
|
|
9
|
+
"application/x-yaml",
|
|
10
|
+
"application/javascript",
|
|
11
|
+
"application/typescript",
|
|
12
|
+
"application/x-sh",
|
|
13
|
+
"application/sql",
|
|
14
|
+
"application/csv",
|
|
15
|
+
"application/vnd.ms-excel",
|
|
16
|
+
]);
|
|
17
|
+
export function cleanAttachmentText(text, maxChars) {
|
|
18
|
+
const cleaned = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\u0000/g, "").trim();
|
|
19
|
+
if (cleaned.length <= maxChars) {
|
|
20
|
+
return cleaned;
|
|
21
|
+
}
|
|
22
|
+
return `${cleaned.slice(0, maxChars).trimEnd()}\n${l("... (omitted because it is long)", "...(長いため省略)")}`;
|
|
23
|
+
}
|
|
24
|
+
export function indentAttachmentContent(text) {
|
|
25
|
+
return text
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((line) => ` ${line}`)
|
|
28
|
+
.join("\n");
|
|
29
|
+
}
|
|
30
|
+
export function formatBytes(value) {
|
|
31
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
if (value < 1024) {
|
|
35
|
+
return `${value} B`;
|
|
36
|
+
}
|
|
37
|
+
const kb = value / 1024;
|
|
38
|
+
if (kb < 1024) {
|
|
39
|
+
return `${kb.toFixed(kb >= 10 ? 0 : 1)} KB`;
|
|
40
|
+
}
|
|
41
|
+
const mb = kb / 1024;
|
|
42
|
+
return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
|
|
43
|
+
}
|
|
44
|
+
export function isTextLikeFile(contentType, filename) {
|
|
45
|
+
const normalizedType = (contentType || "").toLowerCase().split(";")[0].trim();
|
|
46
|
+
if (normalizedType.startsWith("text/")) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (TEXT_CONTENT_TYPES.has(normalizedType)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return TEXT_EXT_PATTERN.test((filename || "").toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
export function isImageLikeFile(contentType, filename) {
|
|
55
|
+
const normalizedType = (contentType || "").toLowerCase().split(";")[0].trim();
|
|
56
|
+
if (normalizedType.startsWith("image/")) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return IMAGE_EXT_PATTERN.test((filename || "").toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
export function guessImageMimeType(filename) {
|
|
62
|
+
const normalized = (filename || "").toLowerCase();
|
|
63
|
+
if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg"))
|
|
64
|
+
return "image/jpeg";
|
|
65
|
+
if (normalized.endsWith(".gif"))
|
|
66
|
+
return "image/gif";
|
|
67
|
+
if (normalized.endsWith(".webp"))
|
|
68
|
+
return "image/webp";
|
|
69
|
+
if (normalized.endsWith(".bmp"))
|
|
70
|
+
return "image/bmp";
|
|
71
|
+
if (normalized.endsWith(".heic"))
|
|
72
|
+
return "image/heic";
|
|
73
|
+
if (normalized.endsWith(".heif"))
|
|
74
|
+
return "image/heif";
|
|
75
|
+
return "image/png";
|
|
76
|
+
}
|
|
77
|
+
export function formatAttachmentLabel(input) {
|
|
78
|
+
const name = cleanAttachmentText(input.name || input.fallback, 200);
|
|
79
|
+
const meta = [
|
|
80
|
+
input.contentType ? cleanAttachmentText(input.contentType, 120) : "",
|
|
81
|
+
typeof input.size === "number" ? formatBytes(input.size) : "",
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
return meta.length > 0 ? `${name} (${meta.join(", ")})` : name;
|
|
84
|
+
}
|
|
85
|
+
export function chunkText(text, max) {
|
|
86
|
+
if (text.length <= max) {
|
|
87
|
+
return [text];
|
|
88
|
+
}
|
|
89
|
+
const chunks = [];
|
|
90
|
+
let remaining = text;
|
|
91
|
+
while (remaining.length > 0) {
|
|
92
|
+
if (remaining.length <= max) {
|
|
93
|
+
chunks.push(remaining);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
let cut = remaining.lastIndexOf("\n", max);
|
|
97
|
+
if (cut < Math.floor(max / 2)) {
|
|
98
|
+
cut = max;
|
|
99
|
+
}
|
|
100
|
+
chunks.push(remaining.slice(0, cut).trimEnd());
|
|
101
|
+
remaining = remaining.slice(cut).replace(/^\s+/, "");
|
|
102
|
+
}
|
|
103
|
+
return chunks;
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type NotifyChannel = "macos" | "windows" | "discord" | "telegram" | "slack" | "mail" | "stderr";
|
|
2
|
+
export interface NotifyOptions {
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
sound?: boolean;
|
|
7
|
+
channel?: NotifyChannel | "auto";
|
|
8
|
+
to?: string;
|
|
9
|
+
discordThreadId?: string;
|
|
10
|
+
telegramThreadId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface NotifyResult {
|
|
13
|
+
channel: NotifyChannel;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
detail?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function notify(options: NotifyOptions): Promise<NotifyResult>;
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { l } from "./i18n.js";
|
|
3
|
+
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
4
|
+
const DISCORD_MESSAGE_LIMIT = 1900;
|
|
5
|
+
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
6
|
+
const TELEGRAM_MESSAGE_LIMIT = 3900;
|
|
7
|
+
export async function notify(options) {
|
|
8
|
+
const title = sanitize(options.title);
|
|
9
|
+
const body = sanitize(options.body);
|
|
10
|
+
const subtitle = options.subtitle ? sanitize(options.subtitle) : undefined;
|
|
11
|
+
if (!title && !body) {
|
|
12
|
+
throw new Error(l("notify: title or body is required", "notify: title または body が必要です"));
|
|
13
|
+
}
|
|
14
|
+
const requested = options.channel || "auto";
|
|
15
|
+
const channel = requested === "auto" ? resolveAutoChannel() : requested;
|
|
16
|
+
if (process.env.AGENT_SIN_NOTIFY_BACKEND === "stderr") {
|
|
17
|
+
return notifyStderr(title, body, subtitle);
|
|
18
|
+
}
|
|
19
|
+
switch (channel) {
|
|
20
|
+
case "macos":
|
|
21
|
+
return notifyMacOs(title, body, subtitle, Boolean(options.sound));
|
|
22
|
+
case "windows":
|
|
23
|
+
return notifyWindows(title, body, subtitle);
|
|
24
|
+
case "discord":
|
|
25
|
+
return notifyDiscord(title, body, subtitle, options.discordThreadId);
|
|
26
|
+
case "telegram":
|
|
27
|
+
return notifyTelegram(title, body, subtitle, options.telegramThreadId);
|
|
28
|
+
case "slack":
|
|
29
|
+
return notifySlack(title, body, subtitle);
|
|
30
|
+
case "mail":
|
|
31
|
+
return notifyMail(title, body, subtitle, options.to);
|
|
32
|
+
case "stderr":
|
|
33
|
+
return notifyStderr(title, body, subtitle);
|
|
34
|
+
default:
|
|
35
|
+
return notifyStderr(title, body, subtitle);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function resolveAutoChannel() {
|
|
39
|
+
if (process.env.AGENT_SIN_NOTIFY_BACKEND === "stderr") {
|
|
40
|
+
return "stderr";
|
|
41
|
+
}
|
|
42
|
+
if (process.env.AGENT_SIN_DISCORD_WEBHOOK_URL || hasDiscordBotNotifyConfig()) {
|
|
43
|
+
return "discord";
|
|
44
|
+
}
|
|
45
|
+
if (hasTelegramNotifyConfig()) {
|
|
46
|
+
return "telegram";
|
|
47
|
+
}
|
|
48
|
+
if (process.env.AGENT_SIN_SLACK_WEBHOOK_URL) {
|
|
49
|
+
return "slack";
|
|
50
|
+
}
|
|
51
|
+
if (process.env.AGENT_SIN_SMTP_HOST && process.env.AGENT_SIN_MAIL_TO) {
|
|
52
|
+
return "mail";
|
|
53
|
+
}
|
|
54
|
+
if (process.platform === "darwin") {
|
|
55
|
+
return "macos";
|
|
56
|
+
}
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
return "windows";
|
|
59
|
+
}
|
|
60
|
+
return "stderr";
|
|
61
|
+
}
|
|
62
|
+
function sanitize(value) {
|
|
63
|
+
return value.replace(/\r/g, "").slice(0, 4000);
|
|
64
|
+
}
|
|
65
|
+
function escapeAppleScript(value) {
|
|
66
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
67
|
+
}
|
|
68
|
+
function notifyMacOs(title, body, subtitle, withSound) {
|
|
69
|
+
const parts = [
|
|
70
|
+
`display notification "${escapeAppleScript(body)}"`,
|
|
71
|
+
`with title "${escapeAppleScript(title)}"`,
|
|
72
|
+
];
|
|
73
|
+
if (subtitle) {
|
|
74
|
+
parts.push(`subtitle "${escapeAppleScript(subtitle)}"`);
|
|
75
|
+
}
|
|
76
|
+
if (withSound) {
|
|
77
|
+
parts.push('sound name "Glass"');
|
|
78
|
+
}
|
|
79
|
+
return runChild("osascript", ["-e", parts.join(" ")], "macos");
|
|
80
|
+
}
|
|
81
|
+
function escapePowerShell(value) {
|
|
82
|
+
// Single-quoted PowerShell strings: doubled single quote escapes a literal '.
|
|
83
|
+
return value.replace(/'/g, "''");
|
|
84
|
+
}
|
|
85
|
+
async function notifyWindows(title, body, subtitle) {
|
|
86
|
+
const head = subtitle ? `${title} - ${subtitle}` : title;
|
|
87
|
+
// Windows 10/11 toast via Windows Runtime APIs in PowerShell.
|
|
88
|
+
// Falls back to stderr if PowerShell or WinRT is unavailable.
|
|
89
|
+
const script = [
|
|
90
|
+
"$ErrorActionPreference = 'Stop';",
|
|
91
|
+
"[void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime];",
|
|
92
|
+
"[void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType=WindowsRuntime];",
|
|
93
|
+
`$title = '${escapePowerShell(head)}';`,
|
|
94
|
+
`$body = '${escapePowerShell(body)}';`,
|
|
95
|
+
"$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02);",
|
|
96
|
+
"$nodes = $template.GetElementsByTagName('text');",
|
|
97
|
+
"$nodes.Item(0).AppendChild($template.CreateTextNode($title)) | Out-Null;",
|
|
98
|
+
"$nodes.Item(1).AppendChild($template.CreateTextNode($body)) | Out-Null;",
|
|
99
|
+
"$toast = [Windows.UI.Notifications.ToastNotification]::new($template);",
|
|
100
|
+
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Agent-Sin').Show($toast);",
|
|
101
|
+
].join(" ");
|
|
102
|
+
const result = await runChild("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], "windows");
|
|
103
|
+
if (result.ok)
|
|
104
|
+
return result;
|
|
105
|
+
// Fallback: stderr so the message is not lost on systems without WinRT toast support.
|
|
106
|
+
return notifyStderr(title, body, subtitle);
|
|
107
|
+
}
|
|
108
|
+
async function notifyDiscord(title, body, subtitle, threadIdOverride) {
|
|
109
|
+
const url = process.env.AGENT_SIN_DISCORD_WEBHOOK_URL;
|
|
110
|
+
const content = formatPlainTextMessage(title, body, subtitle);
|
|
111
|
+
const chunks = splitMessage(content, DISCORD_MESSAGE_LIMIT);
|
|
112
|
+
const rawThreadId = discordNotifyThreadId(threadIdOverride);
|
|
113
|
+
const threadId = normalizeDiscordSnowflake(rawThreadId);
|
|
114
|
+
if (rawThreadId && !threadId) {
|
|
115
|
+
return { channel: "discord", ok: false, detail: `invalid Discord thread id: ${rawThreadId}` };
|
|
116
|
+
}
|
|
117
|
+
if (url) {
|
|
118
|
+
const webhookUrl = discordWebhookUrl(url, threadId);
|
|
119
|
+
return postDiscordChunks(chunks, (chunk) => postWebhook(webhookUrl, { content: chunk }, "discord"));
|
|
120
|
+
}
|
|
121
|
+
const token = process.env.AGENT_SIN_DISCORD_BOT_TOKEN;
|
|
122
|
+
const targetId = threadId || discordNotifyChannelId();
|
|
123
|
+
if (token && targetId) {
|
|
124
|
+
return postDiscordChunks(chunks, (chunk) => postDiscordBotMessage(token, targetId, chunk));
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
channel: "discord",
|
|
128
|
+
ok: false,
|
|
129
|
+
detail: "Discord notification config is not set",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function notifySlack(title, body, subtitle) {
|
|
133
|
+
const url = process.env.AGENT_SIN_SLACK_WEBHOOK_URL;
|
|
134
|
+
if (!url) {
|
|
135
|
+
return { channel: "slack", ok: false, detail: "AGENT_SIN_SLACK_WEBHOOK_URL is not set" };
|
|
136
|
+
}
|
|
137
|
+
const text = formatPlainTextMessage(title, body, subtitle);
|
|
138
|
+
return postWebhook(url, { text }, "slack");
|
|
139
|
+
}
|
|
140
|
+
async function notifyTelegram(title, body, subtitle, threadIdOverride) {
|
|
141
|
+
const token = process.env.AGENT_SIN_TELEGRAM_BOT_TOKEN;
|
|
142
|
+
const chatId = telegramNotifyChatId();
|
|
143
|
+
if (!token) {
|
|
144
|
+
return { channel: "telegram", ok: false, detail: "AGENT_SIN_TELEGRAM_BOT_TOKEN is not set" };
|
|
145
|
+
}
|
|
146
|
+
if (!chatId) {
|
|
147
|
+
return { channel: "telegram", ok: false, detail: "Telegram chat id is not set" };
|
|
148
|
+
}
|
|
149
|
+
const rawThreadId = telegramNotifyThreadId(threadIdOverride);
|
|
150
|
+
const threadId = normalizeTelegramInteger(rawThreadId);
|
|
151
|
+
if (rawThreadId && !threadId) {
|
|
152
|
+
return { channel: "telegram", ok: false, detail: `invalid Telegram thread id: ${rawThreadId}` };
|
|
153
|
+
}
|
|
154
|
+
const content = formatPlainTextMessage(title, body, subtitle);
|
|
155
|
+
const chunks = splitMessage(content, TELEGRAM_MESSAGE_LIMIT);
|
|
156
|
+
return postTelegramChunks(chunks, (chunk) => postTelegramBotMessage(token, chatId, chunk, threadId));
|
|
157
|
+
}
|
|
158
|
+
async function notifyMail(title, body, subtitle, toOverride) {
|
|
159
|
+
const host = process.env.AGENT_SIN_SMTP_HOST;
|
|
160
|
+
const portRaw = process.env.AGENT_SIN_SMTP_PORT || "587";
|
|
161
|
+
const user = process.env.AGENT_SIN_SMTP_USER;
|
|
162
|
+
const pass = process.env.AGENT_SIN_SMTP_PASS;
|
|
163
|
+
const from = process.env.AGENT_SIN_MAIL_FROM || user;
|
|
164
|
+
const to = toOverride || process.env.AGENT_SIN_MAIL_TO;
|
|
165
|
+
const secure = (process.env.AGENT_SIN_SMTP_SECURE || "").toLowerCase() === "true";
|
|
166
|
+
if (!host) {
|
|
167
|
+
return { channel: "mail", ok: false, detail: "AGENT_SIN_SMTP_HOST is not set" };
|
|
168
|
+
}
|
|
169
|
+
if (!from) {
|
|
170
|
+
return { channel: "mail", ok: false, detail: "AGENT_SIN_MAIL_FROM (or AGENT_SIN_SMTP_USER) is not set" };
|
|
171
|
+
}
|
|
172
|
+
if (!to) {
|
|
173
|
+
return { channel: "mail", ok: false, detail: "mail recipient is not set (--to or AGENT_SIN_MAIL_TO)" };
|
|
174
|
+
}
|
|
175
|
+
const port = Number.parseInt(portRaw, 10);
|
|
176
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
177
|
+
return { channel: "mail", ok: false, detail: `invalid AGENT_SIN_SMTP_PORT: ${portRaw}` };
|
|
178
|
+
}
|
|
179
|
+
let createTransport;
|
|
180
|
+
try {
|
|
181
|
+
({ createTransport } = await import("nodemailer"));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
channel: "mail",
|
|
186
|
+
ok: false,
|
|
187
|
+
detail: `nodemailer not available: ${error instanceof Error ? error.message : String(error)}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const transporter = createTransport({
|
|
191
|
+
host,
|
|
192
|
+
port,
|
|
193
|
+
secure,
|
|
194
|
+
auth: user && pass ? { user, pass } : undefined,
|
|
195
|
+
});
|
|
196
|
+
const subject = subtitle ? `${title} - ${subtitle}` : title;
|
|
197
|
+
try {
|
|
198
|
+
const info = await transporter.sendMail({
|
|
199
|
+
from,
|
|
200
|
+
to,
|
|
201
|
+
subject,
|
|
202
|
+
text: body,
|
|
203
|
+
});
|
|
204
|
+
return { channel: "mail", ok: true, detail: info.messageId };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
channel: "mail",
|
|
209
|
+
ok: false,
|
|
210
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function notifyStderr(title, body, subtitle) {
|
|
215
|
+
const head = subtitle ? `${title} - ${subtitle}` : title;
|
|
216
|
+
process.stderr.write(`[notify] ${head}: ${body}\n`);
|
|
217
|
+
return { channel: "stderr", ok: true };
|
|
218
|
+
}
|
|
219
|
+
function formatPlainTextMessage(title, body, subtitle) {
|
|
220
|
+
const head = subtitle ? `*${title}* — ${subtitle}` : `*${title}*`;
|
|
221
|
+
return body ? `${head}\n${body}` : head;
|
|
222
|
+
}
|
|
223
|
+
function hasDiscordBotNotifyConfig() {
|
|
224
|
+
return Boolean(process.env.AGENT_SIN_DISCORD_BOT_TOKEN && (discordNotifyThreadId() || discordNotifyChannelId()));
|
|
225
|
+
}
|
|
226
|
+
function hasTelegramNotifyConfig() {
|
|
227
|
+
return Boolean(process.env.AGENT_SIN_TELEGRAM_BOT_TOKEN && telegramNotifyChatId());
|
|
228
|
+
}
|
|
229
|
+
function discordNotifyChannelId() {
|
|
230
|
+
return (firstListValue(process.env.AGENT_SIN_DISCORD_NOTIFY_CHANNEL_ID) ||
|
|
231
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_CHANNEL_ID) ||
|
|
232
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_LISTEN_CHANNEL_IDS));
|
|
233
|
+
}
|
|
234
|
+
function discordNotifyThreadId(override) {
|
|
235
|
+
return (firstListValue(override) ||
|
|
236
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_NOTIFY_THREAD_ID) ||
|
|
237
|
+
firstListValue(process.env.AGENT_SIN_DISCORD_THREAD_ID));
|
|
238
|
+
}
|
|
239
|
+
function telegramNotifyChatId() {
|
|
240
|
+
return (firstListValue(process.env.AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID) ||
|
|
241
|
+
firstListValue(process.env.AGENT_SIN_TELEGRAM_CHAT_ID) ||
|
|
242
|
+
firstListValue(process.env.AGENT_SIN_TELEGRAM_LISTEN_CHAT_IDS));
|
|
243
|
+
}
|
|
244
|
+
function telegramNotifyThreadId(override) {
|
|
245
|
+
return (firstListValue(override) ||
|
|
246
|
+
firstListValue(process.env.AGENT_SIN_TELEGRAM_NOTIFY_THREAD_ID) ||
|
|
247
|
+
firstListValue(process.env.AGENT_SIN_TELEGRAM_THREAD_ID));
|
|
248
|
+
}
|
|
249
|
+
function firstListValue(value) {
|
|
250
|
+
return (value || "")
|
|
251
|
+
.split(/[,\s]+/)
|
|
252
|
+
.map((item) => item.trim())
|
|
253
|
+
.find(Boolean) || "";
|
|
254
|
+
}
|
|
255
|
+
function normalizeDiscordSnowflake(value) {
|
|
256
|
+
const trimmed = value.trim();
|
|
257
|
+
return /^\d+$/.test(trimmed) ? trimmed : "";
|
|
258
|
+
}
|
|
259
|
+
function normalizeTelegramInteger(value) {
|
|
260
|
+
const trimmed = value.trim();
|
|
261
|
+
return /^-?\d+$/.test(trimmed) ? trimmed : "";
|
|
262
|
+
}
|
|
263
|
+
function discordWebhookUrl(url, threadId) {
|
|
264
|
+
if (!threadId) {
|
|
265
|
+
return url;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const parsed = new URL(url);
|
|
269
|
+
parsed.searchParams.set("thread_id", threadId);
|
|
270
|
+
return parsed.toString();
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
274
|
+
return `${url}${separator}thread_id=${encodeURIComponent(threadId)}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function splitMessage(text, maxLength) {
|
|
278
|
+
const trimmed = text.trim();
|
|
279
|
+
if (!trimmed)
|
|
280
|
+
return [""];
|
|
281
|
+
if (trimmed.length <= maxLength)
|
|
282
|
+
return [trimmed];
|
|
283
|
+
const chunks = [];
|
|
284
|
+
let current = "";
|
|
285
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
286
|
+
if (current && current.length + line.length + 1 > maxLength) {
|
|
287
|
+
chunks.push(current);
|
|
288
|
+
current = "";
|
|
289
|
+
}
|
|
290
|
+
if (line.length > maxLength) {
|
|
291
|
+
for (let index = 0; index < line.length; index += maxLength) {
|
|
292
|
+
const piece = line.slice(index, index + maxLength);
|
|
293
|
+
if (current) {
|
|
294
|
+
chunks.push(current);
|
|
295
|
+
current = "";
|
|
296
|
+
}
|
|
297
|
+
chunks.push(piece);
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
current = current ? `${current}\n${line}` : line;
|
|
302
|
+
}
|
|
303
|
+
if (current)
|
|
304
|
+
chunks.push(current);
|
|
305
|
+
return chunks;
|
|
306
|
+
}
|
|
307
|
+
async function postDiscordChunks(chunks, send) {
|
|
308
|
+
let sent = 0;
|
|
309
|
+
for (const chunk of chunks) {
|
|
310
|
+
const result = await send(chunk);
|
|
311
|
+
if (!result.ok) {
|
|
312
|
+
return sent > 0
|
|
313
|
+
? { channel: "discord", ok: false, detail: `sent ${sent}/${chunks.length}: ${result.detail || "failed"}` }
|
|
314
|
+
: result;
|
|
315
|
+
}
|
|
316
|
+
sent += 1;
|
|
317
|
+
}
|
|
318
|
+
return { channel: "discord", ok: true, detail: chunks.length > 1 ? `${chunks.length} messages` : undefined };
|
|
319
|
+
}
|
|
320
|
+
async function postDiscordBotMessage(token, channelId, content) {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(`${DISCORD_API_BASE}/channels/${channelId}/messages`, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: {
|
|
325
|
+
authorization: `Bot ${token}`,
|
|
326
|
+
"content-type": "application/json",
|
|
327
|
+
},
|
|
328
|
+
body: JSON.stringify({ content }),
|
|
329
|
+
});
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
const detail = `HTTP ${response.status} ${response.statusText}`.trim();
|
|
332
|
+
return { channel: "discord", ok: false, detail };
|
|
333
|
+
}
|
|
334
|
+
return { channel: "discord", ok: true };
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return {
|
|
338
|
+
channel: "discord",
|
|
339
|
+
ok: false,
|
|
340
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function postTelegramChunks(chunks, send) {
|
|
345
|
+
let sent = 0;
|
|
346
|
+
for (const chunk of chunks) {
|
|
347
|
+
const result = await send(chunk);
|
|
348
|
+
if (!result.ok) {
|
|
349
|
+
return sent > 0
|
|
350
|
+
? { channel: "telegram", ok: false, detail: `sent ${sent}/${chunks.length}: ${result.detail || "failed"}` }
|
|
351
|
+
: result;
|
|
352
|
+
}
|
|
353
|
+
sent += 1;
|
|
354
|
+
}
|
|
355
|
+
return { channel: "telegram", ok: true, detail: chunks.length > 1 ? `${chunks.length} messages` : undefined };
|
|
356
|
+
}
|
|
357
|
+
async function postTelegramBotMessage(token, chatId, text, threadId) {
|
|
358
|
+
try {
|
|
359
|
+
const payload = {
|
|
360
|
+
chat_id: chatId,
|
|
361
|
+
text,
|
|
362
|
+
disable_web_page_preview: true,
|
|
363
|
+
};
|
|
364
|
+
if (threadId) {
|
|
365
|
+
payload.message_thread_id = Number.parseInt(threadId, 10);
|
|
366
|
+
}
|
|
367
|
+
const response = await fetch(`${TELEGRAM_API_BASE}/bot${token}/sendMessage`, {
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: { "content-type": "application/json" },
|
|
370
|
+
body: JSON.stringify(payload),
|
|
371
|
+
});
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
const detail = `HTTP ${response.status} ${response.statusText}`.trim();
|
|
374
|
+
return { channel: "telegram", ok: false, detail };
|
|
375
|
+
}
|
|
376
|
+
return { channel: "telegram", ok: true };
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
return {
|
|
380
|
+
channel: "telegram",
|
|
381
|
+
ok: false,
|
|
382
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function postWebhook(url, payload, channel) {
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch(url, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
headers: { "content-type": "application/json" },
|
|
391
|
+
body: JSON.stringify(payload),
|
|
392
|
+
});
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
const detail = `HTTP ${response.status} ${response.statusText}`.trim();
|
|
395
|
+
return { channel, ok: false, detail };
|
|
396
|
+
}
|
|
397
|
+
return { channel, ok: true };
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
return {
|
|
401
|
+
channel,
|
|
402
|
+
ok: false,
|
|
403
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function runChild(bin, args, channel) {
|
|
408
|
+
return new Promise((resolve) => {
|
|
409
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
410
|
+
const stderr = [];
|
|
411
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
412
|
+
child.on("error", (error) => {
|
|
413
|
+
resolve({ channel, ok: false, detail: error.message });
|
|
414
|
+
});
|
|
415
|
+
child.on("close", (code) => {
|
|
416
|
+
if (code === 0) {
|
|
417
|
+
resolve({ channel, ok: true });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const detail = Buffer.concat(stderr).toString("utf8").trim() || `exit ${code}`;
|
|
421
|
+
resolve({ channel, ok: false, detail });
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AppConfig } from "./config.js";
|
|
2
|
+
import type { SkillManifest, SkillOutputDefinition } from "./skill-registry.js";
|
|
3
|
+
import type { SkillResult } from "../skills-sdk/types.js";
|
|
4
|
+
export interface SavedOutput {
|
|
5
|
+
id: string;
|
|
6
|
+
type: string;
|
|
7
|
+
path: string;
|
|
8
|
+
append: boolean;
|
|
9
|
+
show_saved: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function writeSkillOutputs(config: AppConfig, manifest: SkillManifest, result: SkillResult, date?: Date): Promise<SavedOutput[]>;
|
|
12
|
+
export declare function resolveOutputFile(config: AppConfig, output: SkillOutputDefinition, date: Date): string;
|
|
13
|
+
export declare function renderTemplate(template: string, date: Date): string;
|