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,155 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { l } from "./i18n.js";
|
|
5
|
+
export async function loadSchedules(workspace) {
|
|
6
|
+
const file = schedulesFile(workspace);
|
|
7
|
+
let raw;
|
|
8
|
+
try {
|
|
9
|
+
raw = await readFile(file, "utf8");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
try {
|
|
13
|
+
raw = await readFile(legacySchedulesFile(workspace), "utf8");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const parsed = YAML.parse(raw);
|
|
20
|
+
const list = parsed && Array.isArray(parsed.schedules) ? parsed.schedules : [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const entries = [];
|
|
23
|
+
for (const item of list) {
|
|
24
|
+
if (!item || typeof item !== "object") {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const record = item;
|
|
28
|
+
const id = String(record.id || "").trim();
|
|
29
|
+
if (!id) {
|
|
30
|
+
throw new Error(l("Invalid schedule: missing id", "スケジュールが不正です: id がありません"));
|
|
31
|
+
}
|
|
32
|
+
if (seen.has(id)) {
|
|
33
|
+
throw new Error(l(`Duplicate schedule id: ${id}`, `スケジュール id が重複しています: ${id}`));
|
|
34
|
+
}
|
|
35
|
+
seen.add(id);
|
|
36
|
+
const cronText = String(record.cron || "").trim();
|
|
37
|
+
if (!cronText) {
|
|
38
|
+
throw new Error(l(`Invalid schedule '${id}': missing cron`, `スケジュール '${id}' が不正です: cron がありません`));
|
|
39
|
+
}
|
|
40
|
+
const skill = String(record.skill || "").trim();
|
|
41
|
+
if (!skill) {
|
|
42
|
+
throw new Error(l(`Invalid schedule '${id}': missing skill`, `スケジュール '${id}' が不正です: skill がありません`));
|
|
43
|
+
}
|
|
44
|
+
const expression = parseCron(cronText);
|
|
45
|
+
const enabled = record.enabled === undefined ? true : Boolean(record.enabled);
|
|
46
|
+
const args = isPlainObject(record.args) ? record.args : {};
|
|
47
|
+
const approve = Boolean(record.approve);
|
|
48
|
+
const description = record.description ? String(record.description) : undefined;
|
|
49
|
+
entries.push({ id, cron: cronText, skill, enabled, args, approve, description, expression });
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
export function schedulesFile(workspace) {
|
|
54
|
+
return path.join(workspace, "schedules.yaml");
|
|
55
|
+
}
|
|
56
|
+
export function legacySchedulesFile(workspace) {
|
|
57
|
+
return path.join(workspace, "schedules", "schedules.yaml");
|
|
58
|
+
}
|
|
59
|
+
export function parseCron(raw) {
|
|
60
|
+
const fields = raw.trim().split(/\s+/);
|
|
61
|
+
if (fields.length !== 5) {
|
|
62
|
+
throw new Error(l(`Cron must have 5 fields ("min hour dom month dow"): "${raw}"`, `Cron は5フィールド ("min hour dom month dow") で指定してください: "${raw}"`));
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
raw,
|
|
66
|
+
minute: parseField(fields[0], 0, 59, "minute"),
|
|
67
|
+
hour: parseField(fields[1], 0, 23, "hour"),
|
|
68
|
+
dayOfMonth: parseField(fields[2], 1, 31, "day-of-month"),
|
|
69
|
+
month: parseField(fields[3], 1, 12, "month"),
|
|
70
|
+
dayOfWeek: parseField(fields[4], 0, 6, "day-of-week"),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parseField(field, min, max, label) {
|
|
74
|
+
const values = new Set();
|
|
75
|
+
let isWildcard = field === "*" || field === "*/1";
|
|
76
|
+
for (const part of field.split(",")) {
|
|
77
|
+
const segment = part.trim();
|
|
78
|
+
if (!segment) {
|
|
79
|
+
throw new Error(l(`Empty segment in ${label} field: "${field}"`, `${label} フィールドに空のセグメントがあります: "${field}"`));
|
|
80
|
+
}
|
|
81
|
+
let step = 1;
|
|
82
|
+
let body = segment;
|
|
83
|
+
const slashIndex = body.indexOf("/");
|
|
84
|
+
if (slashIndex >= 0) {
|
|
85
|
+
const stepRaw = body.slice(slashIndex + 1);
|
|
86
|
+
const stepValue = Number.parseInt(stepRaw, 10);
|
|
87
|
+
if (!Number.isFinite(stepValue) || stepValue <= 0) {
|
|
88
|
+
throw new Error(l(`Invalid step "${stepRaw}" in ${label} field: "${field}"`, `${label} フィールドの step が不正です: "${stepRaw}" ("${field}")`));
|
|
89
|
+
}
|
|
90
|
+
step = stepValue;
|
|
91
|
+
body = body.slice(0, slashIndex);
|
|
92
|
+
isWildcard = isWildcard && step === 1;
|
|
93
|
+
}
|
|
94
|
+
let from = min;
|
|
95
|
+
let to = max;
|
|
96
|
+
if (body !== "" && body !== "*") {
|
|
97
|
+
const dashIndex = body.indexOf("-");
|
|
98
|
+
if (dashIndex >= 0) {
|
|
99
|
+
from = Number.parseInt(body.slice(0, dashIndex), 10);
|
|
100
|
+
to = Number.parseInt(body.slice(dashIndex + 1), 10);
|
|
101
|
+
isWildcard = false;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
from = Number.parseInt(body, 10);
|
|
105
|
+
to = from;
|
|
106
|
+
isWildcard = false;
|
|
107
|
+
}
|
|
108
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) {
|
|
109
|
+
throw new Error(l(`Invalid value "${body}" in ${label} field: "${field}"`, `${label} フィールドの値が不正です: "${body}" ("${field}")`));
|
|
110
|
+
}
|
|
111
|
+
if (from < min || to > max || from > to) {
|
|
112
|
+
throw new Error(l(`${label} value out of range (${from}-${to}); allowed ${min}-${max} for "${field}"`, `${label} の値が範囲外です (${from}-${to}); 使用可能範囲 ${min}-${max} ("${field}")`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (let value = from; value <= to; value += step) {
|
|
116
|
+
values.add(value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { values, isWildcard };
|
|
120
|
+
}
|
|
121
|
+
export function matchesCron(expression, date) {
|
|
122
|
+
if (!expression.minute.values.has(date.getMinutes())) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (!expression.hour.values.has(date.getHours())) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (!expression.month.values.has(date.getMonth() + 1)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const dom = expression.dayOfMonth;
|
|
132
|
+
const dow = expression.dayOfWeek;
|
|
133
|
+
const domMatch = dom.values.has(date.getDate());
|
|
134
|
+
const dowMatch = dow.values.has(date.getDay());
|
|
135
|
+
// POSIX cron: when both dom and dow are restricted, OR them; otherwise AND.
|
|
136
|
+
if (!dom.isWildcard && !dow.isWildcard) {
|
|
137
|
+
return domMatch || dowMatch;
|
|
138
|
+
}
|
|
139
|
+
return domMatch && dowMatch;
|
|
140
|
+
}
|
|
141
|
+
export function nextRunAfter(expression, from, lookaheadMinutes = 7 * 24 * 60) {
|
|
142
|
+
const cursor = new Date(from.getTime());
|
|
143
|
+
cursor.setSeconds(0, 0);
|
|
144
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
145
|
+
for (let i = 0; i < lookaheadMinutes; i += 1) {
|
|
146
|
+
if (matchesCron(expression, cursor)) {
|
|
147
|
+
return new Date(cursor.getTime());
|
|
148
|
+
}
|
|
149
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function isPlainObject(value) {
|
|
154
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
155
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface DotenvLoadResult {
|
|
2
|
+
loaded: boolean;
|
|
3
|
+
path: string;
|
|
4
|
+
vars_set: number;
|
|
5
|
+
permission_warning?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function dotenvPath(workspace?: string): Promise<string>;
|
|
8
|
+
export declare function readDotenvKeys(workspace?: string): Promise<Set<string>>;
|
|
9
|
+
export declare function loadDotenv(workspace?: string): Promise<DotenvLoadResult>;
|
|
10
|
+
export declare function upsertDotenv(workspace: string, entries: Array<{
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}>): Promise<{
|
|
14
|
+
path: string;
|
|
15
|
+
}>;
|
|
16
|
+
export interface ApiKeySource {
|
|
17
|
+
provider: string;
|
|
18
|
+
envVar: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ApiKeyResolution {
|
|
21
|
+
provider: string;
|
|
22
|
+
keys: string[];
|
|
23
|
+
sources: ApiKeySource[];
|
|
24
|
+
}
|
|
25
|
+
export declare function getApiKeyResolution(provider: string, fallbackProviders?: string[]): ApiKeyResolution;
|
|
26
|
+
export declare function getApiKeys(provider: string, fallbackProviders?: string[]): string[];
|
|
27
|
+
export declare function maskKey(key: string): string;
|
|
28
|
+
export declare function ensureDotenvSkeleton(workspace: string): Promise<{
|
|
29
|
+
created: boolean;
|
|
30
|
+
path: string;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { defaultWorkspace } from "./config.js";
|
|
4
|
+
export async function dotenvPath(workspace = defaultWorkspace()) {
|
|
5
|
+
return path.join(workspace, ".env");
|
|
6
|
+
}
|
|
7
|
+
// .env に書かれているキー名だけを返す (process.env には影響しない)。
|
|
8
|
+
// setup の検出表示で「シェルから漏れているキー」を「.env 由来」と誤認しないために使う。
|
|
9
|
+
export async function readDotenvKeys(workspace = defaultWorkspace()) {
|
|
10
|
+
const file = path.join(workspace, ".env");
|
|
11
|
+
const keys = new Set();
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = await readFile(file, "utf8");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return keys;
|
|
18
|
+
}
|
|
19
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
20
|
+
const parsed = parseDotenvLine(line);
|
|
21
|
+
if (parsed && parsed.value.length > 0) {
|
|
22
|
+
keys.add(parsed.key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return keys;
|
|
26
|
+
}
|
|
27
|
+
export async function loadDotenv(workspace = defaultWorkspace()) {
|
|
28
|
+
const file = await dotenvPath(workspace);
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = await readFile(file, "utf8");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { loaded: false, path: file, vars_set: 0 };
|
|
35
|
+
}
|
|
36
|
+
let permissionWarning;
|
|
37
|
+
try {
|
|
38
|
+
const info = await stat(file);
|
|
39
|
+
const mode = info.mode & 0o777;
|
|
40
|
+
if ((mode & 0o077) !== 0) {
|
|
41
|
+
permissionWarning = `permissions ${mode.toString(8).padStart(3, "0")} on ${file} are too open; recommend: chmod 600 ${file}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore stat failures; the file was readable above.
|
|
46
|
+
}
|
|
47
|
+
let varsSet = 0;
|
|
48
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
49
|
+
const parsed = parseDotenvLine(line);
|
|
50
|
+
if (!parsed) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (process.env[parsed.key] === undefined) {
|
|
54
|
+
process.env[parsed.key] = parsed.value;
|
|
55
|
+
varsSet += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { loaded: true, path: file, vars_set: varsSet, permission_warning: permissionWarning };
|
|
59
|
+
}
|
|
60
|
+
function parseDotenvLine(line) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
66
|
+
if (!match) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
let value = match[2].trim();
|
|
70
|
+
if (value.length >= 2) {
|
|
71
|
+
const first = value.charAt(0);
|
|
72
|
+
const last = value.charAt(value.length - 1);
|
|
73
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
74
|
+
value = value.slice(1, -1);
|
|
75
|
+
return { key: match[1], value };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Strip trailing inline comment ` # ...` (only when preceded by whitespace).
|
|
79
|
+
const commentIndex = findInlineCommentIndex(value);
|
|
80
|
+
if (commentIndex >= 0) {
|
|
81
|
+
value = value.slice(0, commentIndex).trim();
|
|
82
|
+
}
|
|
83
|
+
return { key: match[1], value };
|
|
84
|
+
}
|
|
85
|
+
function findInlineCommentIndex(value) {
|
|
86
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
87
|
+
if (value.charAt(i) === "#" && (i === 0 || /\s/.test(value.charAt(i - 1)))) {
|
|
88
|
+
return i;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
export async function upsertDotenv(workspace, entries) {
|
|
94
|
+
const file = path.join(workspace, ".env");
|
|
95
|
+
let existing = "";
|
|
96
|
+
try {
|
|
97
|
+
existing = await readFile(file, "utf8");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
existing = "";
|
|
101
|
+
}
|
|
102
|
+
const lines = existing.split(/\r?\n/);
|
|
103
|
+
const updated = new Set();
|
|
104
|
+
for (const { key, value } of entries) {
|
|
105
|
+
const formatted = `${key}=${formatDotenvValue(value)}`;
|
|
106
|
+
let replaced = false;
|
|
107
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
108
|
+
const trimmed = lines[i].trim();
|
|
109
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
110
|
+
continue;
|
|
111
|
+
const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
112
|
+
if (match && match[1] === key) {
|
|
113
|
+
lines[i] = formatted;
|
|
114
|
+
replaced = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!replaced) {
|
|
119
|
+
lines.push(formatted);
|
|
120
|
+
}
|
|
121
|
+
updated.add(key);
|
|
122
|
+
process.env[key] = value;
|
|
123
|
+
}
|
|
124
|
+
let next = lines.join("\n");
|
|
125
|
+
if (!next.endsWith("\n"))
|
|
126
|
+
next = `${next}\n`;
|
|
127
|
+
await writeFile(file, next, { mode: 0o600 });
|
|
128
|
+
return { path: file };
|
|
129
|
+
}
|
|
130
|
+
function formatDotenvValue(value) {
|
|
131
|
+
if (/[\s"'#]/.test(value) || value === "") {
|
|
132
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
133
|
+
return `"${escaped}"`;
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
const NUMBERED_KEY_LIMIT = 32;
|
|
138
|
+
export function getApiKeyResolution(provider, fallbackProviders = []) {
|
|
139
|
+
const keys = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const sources = [];
|
|
142
|
+
for (const candidate of [provider, ...fallbackProviders]) {
|
|
143
|
+
const upper = candidate.toUpperCase();
|
|
144
|
+
pushKey(keys, seen, sources, candidate, `AGENT_SIN_LIVE_${upper}_KEY`, process.env[`AGENT_SIN_LIVE_${upper}_KEY`]);
|
|
145
|
+
const list = process.env[`${upper}_API_KEYS`];
|
|
146
|
+
if (list) {
|
|
147
|
+
for (const item of list.split(/[,;]/)) {
|
|
148
|
+
pushKey(keys, seen, sources, candidate, `${upper}_API_KEYS`, item);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
pushKey(keys, seen, sources, candidate, `${upper}_API_KEY`, process.env[`${upper}_API_KEY`]);
|
|
152
|
+
for (let i = 1; i <= NUMBERED_KEY_LIMIT; i += 1) {
|
|
153
|
+
pushKey(keys, seen, sources, candidate, `${upper}_API_KEY_${i}`, process.env[`${upper}_API_KEY_${i}`]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { provider, keys, sources };
|
|
157
|
+
}
|
|
158
|
+
export function getApiKeys(provider, fallbackProviders = []) {
|
|
159
|
+
return getApiKeyResolution(provider, fallbackProviders).keys;
|
|
160
|
+
}
|
|
161
|
+
function pushKey(keys, seen, sources, provider, envVar, value) {
|
|
162
|
+
if (!value) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const trimmed = value.trim();
|
|
166
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
seen.add(trimmed);
|
|
170
|
+
keys.push(trimmed);
|
|
171
|
+
sources.push({ provider, envVar });
|
|
172
|
+
}
|
|
173
|
+
export function maskKey(key) {
|
|
174
|
+
if (!key) {
|
|
175
|
+
return "<empty>";
|
|
176
|
+
}
|
|
177
|
+
if (key.length <= 8) {
|
|
178
|
+
return `${key.slice(0, 2)}...${key.slice(-2)}`;
|
|
179
|
+
}
|
|
180
|
+
return `${key.slice(0, 4)}...${key.slice(-4)} (len=${key.length})`;
|
|
181
|
+
}
|
|
182
|
+
const DOTENV_TEMPLATE = `# Agent-Sin secrets file
|
|
183
|
+
# Recommend: chmod 600 ~/.agent-sin/.env
|
|
184
|
+
# Lines starting with # are ignored.
|
|
185
|
+
#
|
|
186
|
+
# Uncomment the line for the provider you want to use and paste your key.
|
|
187
|
+
# After editing, run \`agent-sin setup\` again — the new keys will be detected.
|
|
188
|
+
|
|
189
|
+
# --- OpenAI (https://platform.openai.com/api-keys) ---
|
|
190
|
+
# OPENAI_API_KEY=sk-...
|
|
191
|
+
|
|
192
|
+
# --- Gemini / Google AI (https://aistudio.google.com/app/apikey) ---
|
|
193
|
+
# GEMINI_API_KEY=AIza...
|
|
194
|
+
|
|
195
|
+
# --- Anthropic API (https://console.anthropic.com/settings/keys) ---
|
|
196
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
197
|
+
|
|
198
|
+
# --- Ollama (local) ---
|
|
199
|
+
# No API key needed. Run \`ollama serve\` and \`ollama pull <model>\` first.
|
|
200
|
+
# Uncomment only if Ollama is not on the default localhost:11434.
|
|
201
|
+
# OLLAMA_HOST=http://localhost:11434
|
|
202
|
+
`;
|
|
203
|
+
export async function ensureDotenvSkeleton(workspace) {
|
|
204
|
+
const file = path.join(workspace, ".env");
|
|
205
|
+
try {
|
|
206
|
+
await stat(file);
|
|
207
|
+
return { created: false, path: file };
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// File does not exist; create skeleton.
|
|
211
|
+
}
|
|
212
|
+
await writeFile(file, DOTENV_TEMPLATE, { mode: 0o600 });
|
|
213
|
+
return { created: true, path: file };
|
|
214
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AppConfig } from "./config.js";
|
|
2
|
+
export declare const SERVICE_LABEL_DARWIN = "com.agent-sin.gateway";
|
|
3
|
+
export declare const SERVICE_TASK_NAME_WINDOWS = "Agent-Sin Gateway";
|
|
4
|
+
export interface ServiceStatusInfo {
|
|
5
|
+
installed: boolean;
|
|
6
|
+
manifestPath: string;
|
|
7
|
+
manifestKind: "plist" | "schtasks" | "none";
|
|
8
|
+
}
|
|
9
|
+
export interface ServiceProvider {
|
|
10
|
+
readonly platformId: "darwin" | "windows" | "unsupported";
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly supported: boolean;
|
|
13
|
+
install(config: AppConfig, options?: {
|
|
14
|
+
noStart?: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
start(config: AppConfig): Promise<void>;
|
|
17
|
+
stop(options?: {
|
|
18
|
+
quiet?: boolean;
|
|
19
|
+
wait?: boolean;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
manifestText(config: AppConfig): string;
|
|
22
|
+
status(config: AppConfig): Promise<ServiceStatusInfo>;
|
|
23
|
+
notSupportedReason(): string;
|
|
24
|
+
}
|
|
25
|
+
export declare function renderServiceManifestForPlatform(config: AppConfig, platform?: NodeJS.Platform): {
|
|
26
|
+
label: string;
|
|
27
|
+
manifestKind: "plist" | "schtasks";
|
|
28
|
+
text: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function getServiceProvider(): ServiceProvider;
|
|
31
|
+
export declare function serviceLabel(): string;
|
|
32
|
+
export declare function findAgentSinServiceProcesses(): Promise<string[]>;
|
|
33
|
+
export declare function isServiceCommandLine(line: string): boolean;
|
|
34
|
+
export declare function isSchedulerCommandLine(line: string): boolean;
|
|
35
|
+
export declare function isSchedulerProcessRunning(): Promise<boolean>;
|