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.
Files changed (150) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/assets/logo.png +0 -0
  5. package/builtin-skills/_shared/_models_lib.py +227 -0
  6. package/builtin-skills/_shared/_profile_lib.py +98 -0
  7. package/builtin-skills/_shared/_schedules_lib.py +313 -0
  8. package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
  9. package/builtin-skills/_shared/i18n.py +26 -0
  10. package/builtin-skills/memo-delete/main.py +155 -0
  11. package/builtin-skills/memo-delete/skill.yaml +57 -0
  12. package/builtin-skills/memo-index/main.py +178 -0
  13. package/builtin-skills/memo-index/skill.yaml +53 -0
  14. package/builtin-skills/memo-save/README.md +5 -0
  15. package/builtin-skills/memo-save/main.py +74 -0
  16. package/builtin-skills/memo-save/skill.yaml +52 -0
  17. package/builtin-skills/memo-search/README.md +10 -0
  18. package/builtin-skills/memo-search/main.py +97 -0
  19. package/builtin-skills/memo-search/skill.yaml +51 -0
  20. package/builtin-skills/memo-vector-search/main.py +121 -0
  21. package/builtin-skills/memo-vector-search/skill.yaml +53 -0
  22. package/builtin-skills/model-add/main.py +180 -0
  23. package/builtin-skills/model-add/skill.yaml +112 -0
  24. package/builtin-skills/model-list/main.py +93 -0
  25. package/builtin-skills/model-list/skill.yaml +48 -0
  26. package/builtin-skills/model-set/main.py +123 -0
  27. package/builtin-skills/model-set/skill.yaml +69 -0
  28. package/builtin-skills/profile-delete/_profile_lib.py +98 -0
  29. package/builtin-skills/profile-delete/main.py +98 -0
  30. package/builtin-skills/profile-delete/skill.yaml +64 -0
  31. package/builtin-skills/profile-edit/_profile_lib.py +98 -0
  32. package/builtin-skills/profile-edit/main.py +97 -0
  33. package/builtin-skills/profile-edit/skill.yaml +72 -0
  34. package/builtin-skills/profile-save/main.py +52 -0
  35. package/builtin-skills/profile-save/skill.yaml +69 -0
  36. package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
  37. package/builtin-skills/schedule-add/main.py +137 -0
  38. package/builtin-skills/schedule-add/skill.yaml +94 -0
  39. package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
  40. package/builtin-skills/schedule-list/main.py +86 -0
  41. package/builtin-skills/schedule-list/skill.yaml +45 -0
  42. package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
  43. package/builtin-skills/schedule-remove/main.py +69 -0
  44. package/builtin-skills/schedule-remove/skill.yaml +49 -0
  45. package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
  46. package/builtin-skills/schedule-toggle/main.py +78 -0
  47. package/builtin-skills/schedule-toggle/skill.yaml +61 -0
  48. package/builtin-skills/skills-disable/main.py +63 -0
  49. package/builtin-skills/skills-disable/skill.yaml +52 -0
  50. package/builtin-skills/skills-enable/main.py +62 -0
  51. package/builtin-skills/skills-enable/skill.yaml +51 -0
  52. package/builtin-skills/todo-add/main.py +68 -0
  53. package/builtin-skills/todo-add/skill.yaml +53 -0
  54. package/builtin-skills/todo-delete/main.py +65 -0
  55. package/builtin-skills/todo-delete/skill.yaml +47 -0
  56. package/builtin-skills/todo-done/main.py +75 -0
  57. package/builtin-skills/todo-done/skill.yaml +47 -0
  58. package/builtin-skills/todo-list/main.py +91 -0
  59. package/builtin-skills/todo-list/skill.yaml +48 -0
  60. package/builtin-skills/todo-tick/main.py +125 -0
  61. package/builtin-skills/todo-tick/skill.yaml +48 -0
  62. package/dist/builder/build-action-classifier.d.ts +18 -0
  63. package/dist/builder/build-action-classifier.js +142 -0
  64. package/dist/builder/build-commands.d.ts +19 -0
  65. package/dist/builder/build-commands.js +133 -0
  66. package/dist/builder/build-flow.d.ts +72 -0
  67. package/dist/builder/build-flow.js +416 -0
  68. package/dist/builder/builder-session.d.ts +117 -0
  69. package/dist/builder/builder-session.js +1129 -0
  70. package/dist/builder/conversation-router.d.ts +22 -0
  71. package/dist/builder/conversation-router.js +69 -0
  72. package/dist/builder/intent-runtime-store.d.ts +7 -0
  73. package/dist/builder/intent-runtime-store.js +60 -0
  74. package/dist/builder/progress-format.d.ts +7 -0
  75. package/dist/builder/progress-format.js +46 -0
  76. package/dist/cli/index.d.ts +2 -0
  77. package/dist/cli/index.js +2835 -0
  78. package/dist/cli/spinner.d.ts +30 -0
  79. package/dist/cli/spinner.js +164 -0
  80. package/dist/core/ai-provider.d.ts +75 -0
  81. package/dist/core/ai-provider.js +678 -0
  82. package/dist/core/builtin-skills.d.ts +27 -0
  83. package/dist/core/builtin-skills.js +120 -0
  84. package/dist/core/chat-engine.d.ts +70 -0
  85. package/dist/core/chat-engine.js +812 -0
  86. package/dist/core/config.d.ts +127 -0
  87. package/dist/core/config.js +1379 -0
  88. package/dist/core/daily-memory-promotion.d.ts +21 -0
  89. package/dist/core/daily-memory-promotion.js +422 -0
  90. package/dist/core/i18n.d.ts +23 -0
  91. package/dist/core/i18n.js +167 -0
  92. package/dist/core/info-lines.d.ts +5 -0
  93. package/dist/core/info-lines.js +39 -0
  94. package/dist/core/input-schema.d.ts +2 -0
  95. package/dist/core/input-schema.js +156 -0
  96. package/dist/core/intent-router.d.ts +27 -0
  97. package/dist/core/intent-router.js +160 -0
  98. package/dist/core/logger.d.ts +60 -0
  99. package/dist/core/logger.js +240 -0
  100. package/dist/core/memory.d.ts +10 -0
  101. package/dist/core/memory.js +72 -0
  102. package/dist/core/message-utils.d.ts +13 -0
  103. package/dist/core/message-utils.js +104 -0
  104. package/dist/core/notifier.d.ts +17 -0
  105. package/dist/core/notifier.js +424 -0
  106. package/dist/core/output-writer.d.ts +13 -0
  107. package/dist/core/output-writer.js +100 -0
  108. package/dist/core/plan-decision.d.ts +16 -0
  109. package/dist/core/plan-decision.js +88 -0
  110. package/dist/core/profile-memory.d.ts +17 -0
  111. package/dist/core/profile-memory.js +142 -0
  112. package/dist/core/runtime.d.ts +50 -0
  113. package/dist/core/runtime.js +187 -0
  114. package/dist/core/scheduler.d.ts +28 -0
  115. package/dist/core/scheduler.js +155 -0
  116. package/dist/core/secrets.d.ts +31 -0
  117. package/dist/core/secrets.js +214 -0
  118. package/dist/core/service.d.ts +35 -0
  119. package/dist/core/service.js +479 -0
  120. package/dist/core/skill-planner.d.ts +24 -0
  121. package/dist/core/skill-planner.js +100 -0
  122. package/dist/core/skill-registry.d.ts +98 -0
  123. package/dist/core/skill-registry.js +319 -0
  124. package/dist/core/skill-scaffold.d.ts +33 -0
  125. package/dist/core/skill-scaffold.js +256 -0
  126. package/dist/core/skill-settings.d.ts +11 -0
  127. package/dist/core/skill-settings.js +63 -0
  128. package/dist/core/transfer.d.ts +31 -0
  129. package/dist/core/transfer.js +270 -0
  130. package/dist/core/update-notifier.d.ts +2 -0
  131. package/dist/core/update-notifier.js +140 -0
  132. package/dist/discord/bot.d.ts +96 -0
  133. package/dist/discord/bot.js +2424 -0
  134. package/dist/runtimes/codex-app-server.d.ts +53 -0
  135. package/dist/runtimes/codex-app-server.js +305 -0
  136. package/dist/runtimes/python-runner.d.ts +7 -0
  137. package/dist/runtimes/python-runner.js +302 -0
  138. package/dist/runtimes/typescript-runner.d.ts +5 -0
  139. package/dist/runtimes/typescript-runner.js +172 -0
  140. package/dist/skills-sdk/types.d.ts +38 -0
  141. package/dist/skills-sdk/types.js +1 -0
  142. package/dist/telegram/bot.d.ts +94 -0
  143. package/dist/telegram/bot.js +1219 -0
  144. package/install.ps1 +132 -0
  145. package/install.sh +130 -0
  146. package/package.json +60 -0
  147. package/templates/skill-python/main.py +74 -0
  148. package/templates/skill-python/skill.yaml +48 -0
  149. package/templates/skill-typescript/main.ts +87 -0
  150. package/templates/skill-typescript/skill.yaml +42 -0
@@ -0,0 +1,256 @@
1
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { loadModels } from "./config.js";
5
+ import { loadSkillManifest, resolveSkillEntryPath } from "./skill-registry.js";
6
+ import { l } from "./i18n.js";
7
+ export async function scaffoldSkill(config, options) {
8
+ validateSkillId(options.id);
9
+ const templateDir = options.templateRoot
10
+ ? path.join(options.templateRoot, templateDirName(options.runtime))
11
+ : resolveDefaultTemplateDir(options.runtime);
12
+ await assertTemplateExists(templateDir);
13
+ const target = path.join(config.skills_dir, options.id);
14
+ if (await exists(target)) {
15
+ throw new Error(l(`Skill directory already exists: ${target}`, `スキルディレクトリが既に存在します: ${target}`));
16
+ }
17
+ await mkdir(target, { recursive: true });
18
+ const written = [];
19
+ await copyTemplate(templateDir, target, target, written);
20
+ await rewriteManifest(path.join(target, "skill.yaml"), {
21
+ ...options,
22
+ name: options.name || titleizeSkillId(options.id),
23
+ });
24
+ const entryName = options.runtime === "python" ? "main.py" : "main.ts";
25
+ return {
26
+ skill_id: options.id,
27
+ skill_dir: target,
28
+ manifest_path: path.join(target, "skill.yaml"),
29
+ entry_path: path.join(target, entryName),
30
+ files: written.sort(),
31
+ runtime: options.runtime,
32
+ };
33
+ }
34
+ export function validateSkillId(id) {
35
+ if (!id) {
36
+ throw new Error(l("Skill id is required", "Skill id は必須です"));
37
+ }
38
+ if (!/^[a-z][a-z0-9-]*$/.test(id)) {
39
+ throw new Error(l(`Invalid skill id: '${id}' (must be kebab-case, start with a lowercase letter, allow [a-z0-9-])`, `skill id が不正です: '${id}' (kebab-case、小文字開始、[a-z0-9-] のみ使用可)`));
40
+ }
41
+ if (id.length > 64) {
42
+ throw new Error(l(`Skill id is too long (max 64 chars): ${id}`, `Skill id が長すぎます (最大64文字): ${id}`));
43
+ }
44
+ }
45
+ function templateDirName(runtime) {
46
+ return runtime === "python" ? "skill-python" : "skill-typescript";
47
+ }
48
+ function titleizeSkillId(id) {
49
+ return id
50
+ .split("-")
51
+ .filter((part) => part.length > 0)
52
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
53
+ .join(" ");
54
+ }
55
+ function resolveDefaultTemplateDir(runtime) {
56
+ const here = path.dirname(fileURLToPath(import.meta.url));
57
+ const root = path.resolve(here, "../..");
58
+ return path.join(root, "templates", templateDirName(runtime));
59
+ }
60
+ async function assertTemplateExists(dir) {
61
+ try {
62
+ const info = await stat(dir);
63
+ if (!info.isDirectory()) {
64
+ throw new Error(l(`Template path is not a directory: ${dir}`, `テンプレートパスがディレクトリではありません: ${dir}`));
65
+ }
66
+ }
67
+ catch (error) {
68
+ if (error instanceof Error && error.message.startsWith("Template path")) {
69
+ throw error;
70
+ }
71
+ throw new Error(l(`Skill template not found: ${dir}`, `スキルテンプレートが見つかりません: ${dir}`));
72
+ }
73
+ }
74
+ async function copyTemplate(templateDir, targetRoot, current, written) {
75
+ const sourceCurrent = current === targetRoot ? templateDir : current.replace(targetRoot, templateDir);
76
+ for (const entry of await readdir(sourceCurrent, { withFileTypes: true })) {
77
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) {
78
+ continue;
79
+ }
80
+ const sourcePath = path.join(sourceCurrent, entry.name);
81
+ const targetPath = path.join(current, entry.name);
82
+ if (entry.isDirectory()) {
83
+ await mkdir(targetPath, { recursive: true });
84
+ await copyTemplate(templateDir, targetRoot, targetPath, written);
85
+ continue;
86
+ }
87
+ if (entry.isFile()) {
88
+ await copyFile(sourcePath, targetPath);
89
+ written.push(path.relative(targetRoot, targetPath));
90
+ }
91
+ }
92
+ }
93
+ async function rewriteManifest(file, options) {
94
+ const raw = await readFile(file, "utf8");
95
+ let next = replaceLine(raw, /^id:\s+.+$/m, `id: ${options.id}`);
96
+ if (options.name) {
97
+ next = replaceLine(next, /^name:\s+.+$/m, `name: ${options.name}`);
98
+ }
99
+ if (options.description) {
100
+ next = replaceLine(next, /^description:\s+.+$/m, `description: ${options.description}`);
101
+ }
102
+ next = next.replace(/^(\s*namespace:)\s+.+$/m, `$1 ${options.id}`);
103
+ await writeFile(file, next, "utf8");
104
+ }
105
+ function replaceLine(content, pattern, replacement) {
106
+ if (pattern.test(content)) {
107
+ return content.replace(pattern, replacement);
108
+ }
109
+ return `${replacement}\n${content}`;
110
+ }
111
+ async function exists(target) {
112
+ try {
113
+ await stat(target);
114
+ return true;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ export async function validateInstalledSkill(config, skillId) {
121
+ return validateSkillDirectory(path.join(config.skills_dir, skillId), skillId, {
122
+ knownModelIds: await loadKnownModelIds(config),
123
+ });
124
+ }
125
+ export async function validateSkillDirectory(skillDir, skillId, options = {}) {
126
+ const errors = [];
127
+ const warnings = [];
128
+ if (!(await exists(skillDir))) {
129
+ errors.push(l(`Skill directory not found: ${skillDir}`, `スキルディレクトリが見つかりません: ${skillDir}`));
130
+ return { ok: false, skill_dir: skillDir, errors, warnings };
131
+ }
132
+ let manifest;
133
+ try {
134
+ manifest = await loadSkillManifest(skillDir);
135
+ }
136
+ catch (error) {
137
+ errors.push(error instanceof Error ? error.message : String(error));
138
+ return { ok: false, skill_dir: skillDir, errors, warnings };
139
+ }
140
+ if (manifest.id !== skillId) {
141
+ errors.push(l(`Manifest id "${manifest.id}" does not match directory name "${skillId}"`, `Manifest id "${manifest.id}" がディレクトリ名 "${skillId}" と一致しません`));
142
+ }
143
+ if (!manifest.description || manifest.description.trim().length === 0) {
144
+ warnings.push(l("description is empty (chat mode uses it to decide when to call this skill)", "description が空です (chat mode がこのスキルを呼ぶ判断に使います)"));
145
+ }
146
+ const phrases = manifest.invocation?.phrases?.filter((phrase) => typeof phrase === "string" && phrase.trim()) || [];
147
+ const hasCommand = typeof manifest.invocation?.command === "string" && manifest.invocation.command.trim().length > 0;
148
+ if (!hasCommand && phrases.length === 0) {
149
+ errors.push(l("invocation.phrases is required so chat mode can call this skill (or set invocation.command).", "invocation.phrases は必須です。チャットからスキルを呼ぶための代表的な発話を3個以上書いてください (または invocation.command を設定)。"));
150
+ }
151
+ let entryPath = "";
152
+ try {
153
+ entryPath = await resolveSkillEntryPath(manifest);
154
+ if (manifest.runtime === "python" && !manifest.entry.endsWith(".py")) {
155
+ warnings.push(l(`runtime=python but entry "${manifest.entry}" does not end with .py`, `runtime=python ですが entry "${manifest.entry}" が .py で終わっていません`));
156
+ }
157
+ else if (manifest.runtime === "typescript" && !/\.(ts|tsx|mts|cts|js|mjs|cjs)$/.test(manifest.entry)) {
158
+ warnings.push(l(`runtime=typescript but entry "${manifest.entry}" has unsupported extension`, `runtime=typescript ですが entry "${manifest.entry}" の拡張子が未対応です`));
159
+ }
160
+ }
161
+ catch (error) {
162
+ errors.push(error instanceof Error ? error.message : String(error));
163
+ }
164
+ validateRequiredEnvConventions(manifest, errors);
165
+ for (const output of manifest.outputs) {
166
+ if (!output.id) {
167
+ errors.push(l("outputs[].id is required", "outputs[].id は必須です"));
168
+ }
169
+ if (output.type !== "markdown" && output.type !== "json") {
170
+ errors.push(l(`outputs[${output.id}].type must be 'markdown' or 'json' (got '${output.type}')`, `outputs[${output.id}].type は 'markdown' または 'json' である必要があります ('${output.type}')`));
171
+ }
172
+ if (!output.path) {
173
+ warnings.push(l(`outputs[${output.id}].path is empty`, `outputs[${output.id}].path が空です`));
174
+ }
175
+ if (!output.filename) {
176
+ warnings.push(l(`outputs[${output.id}].filename is empty`, `outputs[${output.id}].filename が空です`));
177
+ }
178
+ }
179
+ if (manifest.memory && !manifest.memory.namespace) {
180
+ errors.push(l("memory.namespace is required when memory section is present", "memory セクションがある場合 memory.namespace は必須です"));
181
+ }
182
+ if (manifest.ai_steps) {
183
+ const seen = new Set();
184
+ for (const step of manifest.ai_steps) {
185
+ if (!step.id) {
186
+ errors.push(l("ai_steps[].id is required", "ai_steps[].id は必須です"));
187
+ continue;
188
+ }
189
+ if (seen.has(step.id)) {
190
+ errors.push(l(`ai_steps[].id duplicated: ${step.id}`, `ai_steps[].id が重複しています: ${step.id}`));
191
+ }
192
+ seen.add(step.id);
193
+ if (!step.model) {
194
+ warnings.push(l(`ai_steps[${step.id}].model is empty`, `ai_steps[${step.id}].model が空です`));
195
+ }
196
+ else if (options.knownModelIds && !options.knownModelIds.has(step.model)) {
197
+ errors.push(l(`ai_steps[${step.id}].model references unknown model id "${step.model}"`, `ai_steps[${step.id}].model が不明な model id "${step.model}" を参照しています`));
198
+ }
199
+ if (!step.purpose) {
200
+ warnings.push(l(`ai_steps[${step.id}].purpose is empty (helps the model behave correctly)`, `ai_steps[${step.id}].purpose が空です (モデルの挙動安定に役立ちます)`));
201
+ }
202
+ }
203
+ }
204
+ if (entryPath) {
205
+ await validateSourceConventions(entryPath, errors);
206
+ }
207
+ return { ok: errors.length === 0, manifest, skill_dir: skillDir, errors, warnings };
208
+ }
209
+ export async function loadKnownModelIds(config) {
210
+ try {
211
+ const models = await loadModels(config.workspace);
212
+ const ids = new Set(Object.keys(models.models || {}));
213
+ for (const role of Object.keys(models.roles || {})) {
214
+ ids.add(role);
215
+ }
216
+ return ids;
217
+ }
218
+ catch {
219
+ return new Set();
220
+ }
221
+ }
222
+ function validateRequiredEnvConventions(manifest, errors) {
223
+ for (const entry of manifest.required_env || []) {
224
+ const name = entry.name.trim();
225
+ if (/^AGENT_SIN_/i.test(name)) {
226
+ errors.push(l(`required_env "${name}" is reserved for agent-sin runtime settings`, `required_env "${name}" は agent-sin 本体設定のため予約されています`));
227
+ }
228
+ if (name === "DISCORD_WEBHOOK_URL") {
229
+ errors.push(l('required_env "DISCORD_WEBHOOK_URL" must not be used; call agent-sin notify for Discord notifications', 'required_env "DISCORD_WEBHOOK_URL" は使えません。Discord通知は agent-sin notify を使ってください'));
230
+ }
231
+ if (name === "TELEGRAM_BOT_TOKEN") {
232
+ errors.push(l('required_env "TELEGRAM_BOT_TOKEN" must not be used; call agent-sin notify for Telegram notifications', 'required_env "TELEGRAM_BOT_TOKEN" は使えません。Telegram通知は agent-sin notify を使ってください'));
233
+ }
234
+ }
235
+ }
236
+ async function validateSourceConventions(entryPath, errors) {
237
+ let source = "";
238
+ try {
239
+ source = await readFile(entryPath, "utf8");
240
+ }
241
+ catch {
242
+ return;
243
+ }
244
+ if (/\bDISCORD_WEBHOOK_URL\b/.test(source)) {
245
+ errors.push(l('entry file must not read DISCORD_WEBHOOK_URL; use "agent-sin notify --channel discord"', 'entry file は DISCORD_WEBHOOK_URL を読んではいけません。"agent-sin notify --channel discord" を使ってください'));
246
+ }
247
+ if (/discord\.com\/api\/webhooks/i.test(source)) {
248
+ errors.push(l('entry file must not post directly to Discord webhooks; use "agent-sin notify --channel discord"', 'entry file は Discord webhook へ直接POSTしてはいけません。"agent-sin notify --channel discord" を使ってください'));
249
+ }
250
+ if (/\bTELEGRAM_BOT_TOKEN\b/.test(source)) {
251
+ errors.push(l('entry file must not read TELEGRAM_BOT_TOKEN; use "agent-sin notify --channel telegram"', 'entry file は TELEGRAM_BOT_TOKEN を読んではいけません。"agent-sin notify --channel telegram" を使ってください'));
252
+ }
253
+ if (/api\.telegram\.org\/bot/i.test(source)) {
254
+ errors.push(l('entry file must not post directly to Telegram Bot API; use "agent-sin notify --channel telegram"', 'entry file は Telegram Bot API へ直接POSTしてはいけません。"agent-sin notify --channel telegram" を使ってください'));
255
+ }
256
+ }
@@ -0,0 +1,11 @@
1
+ export interface SkillSettings {
2
+ /** Skill ids the user has explicitly disabled. */
3
+ disabled: Set<string>;
4
+ }
5
+ export declare function skillSettingsPath(workspace: string): string;
6
+ export declare function loadSkillSettings(workspace: string): Promise<SkillSettings>;
7
+ export declare function saveSkillSettings(workspace: string, settings: SkillSettings): Promise<string>;
8
+ export declare function setSkillEnabled(workspace: string, skillId: string, enabled: boolean): Promise<{
9
+ changed: boolean;
10
+ settings: SkillSettings;
11
+ }>;
@@ -0,0 +1,63 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ export function skillSettingsPath(workspace) {
5
+ return path.join(workspace, "skill-settings.yaml");
6
+ }
7
+ export async function loadSkillSettings(workspace) {
8
+ const file = skillSettingsPath(workspace);
9
+ try {
10
+ await stat(file);
11
+ }
12
+ catch {
13
+ return { disabled: new Set() };
14
+ }
15
+ let raw;
16
+ try {
17
+ raw = await readFile(file, "utf8");
18
+ }
19
+ catch {
20
+ return { disabled: new Set() };
21
+ }
22
+ const parsed = (raw.trim() ? YAML.parse(raw) : null) || {};
23
+ const disabled = new Set();
24
+ const rawDisabled = parsed.disabled;
25
+ if (Array.isArray(rawDisabled)) {
26
+ for (const id of rawDisabled) {
27
+ if (typeof id === "string" && id.trim().length > 0) {
28
+ disabled.add(id.trim());
29
+ }
30
+ }
31
+ }
32
+ return { disabled };
33
+ }
34
+ export async function saveSkillSettings(workspace, settings) {
35
+ const file = skillSettingsPath(workspace);
36
+ await mkdir(path.dirname(file), { recursive: true });
37
+ const disabled = Array.from(settings.disabled).sort();
38
+ const payload = {};
39
+ if (disabled.length > 0) {
40
+ payload.disabled = disabled;
41
+ }
42
+ const content = disabled.length === 0 ? "disabled: []\n" : YAML.stringify(payload);
43
+ await writeFile(file, content, "utf8");
44
+ return file;
45
+ }
46
+ export async function setSkillEnabled(workspace, skillId, enabled) {
47
+ const settings = await loadSkillSettings(workspace);
48
+ const wasDisabled = settings.disabled.has(skillId);
49
+ if (enabled) {
50
+ if (!wasDisabled) {
51
+ return { changed: false, settings };
52
+ }
53
+ settings.disabled.delete(skillId);
54
+ }
55
+ else {
56
+ if (wasDisabled) {
57
+ return { changed: false, settings };
58
+ }
59
+ settings.disabled.add(skillId);
60
+ }
61
+ await saveSkillSettings(workspace, settings);
62
+ return { changed: true, settings };
63
+ }
@@ -0,0 +1,31 @@
1
+ export interface ExportOptions {
2
+ workspace?: string;
3
+ outFile?: string;
4
+ includeSecrets?: boolean;
5
+ includeLogs?: boolean;
6
+ includeIndex?: boolean;
7
+ }
8
+ export interface ExportResult {
9
+ archivePath: string;
10
+ byteSize: number;
11
+ includedItems: string[];
12
+ warnings: string[];
13
+ }
14
+ export interface ImportOptions {
15
+ archivePath: string;
16
+ workspace?: string;
17
+ dryRun?: boolean;
18
+ backup?: boolean;
19
+ }
20
+ export interface ImportResult {
21
+ workspace: string;
22
+ archivePath: string;
23
+ entries: string[];
24
+ backupPath?: string;
25
+ dryRun: boolean;
26
+ }
27
+ export declare function exportWorkspace(options?: ExportOptions): Promise<ExportResult>;
28
+ export declare function listArchiveEntries(archivePath: string): Promise<string[]>;
29
+ export declare function importWorkspace(options: ImportOptions): Promise<ImportResult>;
30
+ export declare function pathExists(file: string): Promise<boolean>;
31
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,270 @@
1
+ import { spawn } from "node:child_process";
2
+ import { copyFile, lstat, mkdir, mkdtemp, readdir, rename, rm, stat } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { defaultWorkspace } from "./config.js";
6
+ import { l } from "./i18n.js";
7
+ const DEFAULT_INCLUDE = [
8
+ ".env",
9
+ "config.toml",
10
+ "models.yaml",
11
+ "skill-settings.yaml",
12
+ "skills",
13
+ "memory",
14
+ "notes",
15
+ "schedules.yaml",
16
+ "discord",
17
+ "telegram",
18
+ "logs",
19
+ "index",
20
+ ];
21
+ const ALWAYS_EXCLUDE = [".venv", "node_modules", ".DS_Store"];
22
+ export async function exportWorkspace(options = {}) {
23
+ const workspace = options.workspace || defaultWorkspace();
24
+ if (!(await pathExists(workspace))) {
25
+ throw new Error(l(`Workspace not found: ${workspace}`, `ワークスペースが見つかりません: ${workspace}`));
26
+ }
27
+ const include = [...DEFAULT_INCLUDE];
28
+ if (options.includeSecrets && !include.includes(".env"))
29
+ include.push(".env");
30
+ if (options.includeLogs && !include.includes("logs"))
31
+ include.push("logs");
32
+ if (options.includeIndex && !include.includes("index"))
33
+ include.push("index");
34
+ const present = [];
35
+ const missing = [];
36
+ for (const item of include) {
37
+ if (await pathExists(path.join(workspace, item))) {
38
+ present.push(item);
39
+ }
40
+ else {
41
+ missing.push(item);
42
+ }
43
+ }
44
+ if (present.length === 0) {
45
+ throw new Error(l("No transferable workspace data was found.", "ワークスペースに移行可能なデータが見つかりません。"));
46
+ }
47
+ const archivePath = path.resolve(options.outFile || defaultArchivePath());
48
+ await mkdir(path.dirname(archivePath), { recursive: true });
49
+ const args = ["-czf", archivePath, "-C", workspace];
50
+ for (const exc of ALWAYS_EXCLUDE) {
51
+ args.push(`--exclude=${exc}`);
52
+ }
53
+ args.push(...present);
54
+ await runTar(args);
55
+ const stats = await stat(archivePath);
56
+ return {
57
+ archivePath,
58
+ byteSize: stats.size,
59
+ includedItems: present,
60
+ warnings: missing.length > 0 ? [l(`Missing items: ${missing.join(", ")}`, `存在しなかった項目: ${missing.join(", ")}`)] : [],
61
+ };
62
+ }
63
+ export async function listArchiveEntries(archivePath) {
64
+ if (!(await pathExists(archivePath))) {
65
+ throw new Error(l(`Archive not found: ${archivePath}`, `アーカイブが見つかりません: ${archivePath}`));
66
+ }
67
+ const entries = await listArchiveMetadata(path.resolve(archivePath));
68
+ assertSafeArchiveEntries(entries);
69
+ return entries.map((entry) => entry.path);
70
+ }
71
+ export async function importWorkspace(options) {
72
+ if (!(await pathExists(options.archivePath))) {
73
+ throw new Error(l(`Archive not found: ${options.archivePath}`, `アーカイブが見つかりません: ${options.archivePath}`));
74
+ }
75
+ const archivePath = path.resolve(options.archivePath);
76
+ const workspace = options.workspace || defaultWorkspace();
77
+ const archiveEntries = await listArchiveMetadata(archivePath);
78
+ assertSafeArchiveEntries(archiveEntries);
79
+ const entries = archiveEntries.map((entry) => entry.path);
80
+ if (options.dryRun) {
81
+ return { workspace, archivePath, entries, dryRun: true };
82
+ }
83
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "agent-sin-import-"));
84
+ try {
85
+ await runTar(["-xzf", archivePath, "-C", tempRoot]);
86
+ await assertExtractedTreeSafe(tempRoot);
87
+ let backupPath;
88
+ if ((options.backup ?? true) && (await pathExists(workspace))) {
89
+ backupPath = await backupExistingWorkspace(workspace);
90
+ }
91
+ await mkdir(workspace, { recursive: true });
92
+ await copySafeTree(tempRoot, workspace);
93
+ return { workspace, archivePath, entries, backupPath, dryRun: false };
94
+ }
95
+ finally {
96
+ await rm(tempRoot, { recursive: true, force: true });
97
+ }
98
+ }
99
+ async function backupExistingWorkspace(workspace) {
100
+ const ts = formatTimestamp(new Date());
101
+ const backupPath = `${workspace}.bak-${ts}`;
102
+ await rename(workspace, backupPath);
103
+ return backupPath;
104
+ }
105
+ function defaultArchivePath() {
106
+ const ts = formatTimestamp(new Date());
107
+ return path.resolve(process.cwd(), `agent-sin-backup-${ts}.tar.gz`);
108
+ }
109
+ function formatTimestamp(date) {
110
+ const yyyy = date.getFullYear();
111
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
112
+ const dd = String(date.getDate()).padStart(2, "0");
113
+ const hh = String(date.getHours()).padStart(2, "0");
114
+ const mi = String(date.getMinutes()).padStart(2, "0");
115
+ const ss = String(date.getSeconds()).padStart(2, "0");
116
+ return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
117
+ }
118
+ export async function pathExists(file) {
119
+ try {
120
+ await stat(file);
121
+ return true;
122
+ }
123
+ catch (error) {
124
+ if (isMissingFile(error))
125
+ return false;
126
+ throw error;
127
+ }
128
+ }
129
+ function isMissingFile(error) {
130
+ return (typeof error === "object" &&
131
+ error !== null &&
132
+ "code" in error &&
133
+ error.code === "ENOENT");
134
+ }
135
+ function runTar(args) {
136
+ return new Promise((resolve, reject) => {
137
+ const child = spawn("tar", args, { stdio: ["ignore", "ignore", "pipe"] });
138
+ let stderr = "";
139
+ child.stderr.on("data", (chunk) => {
140
+ stderr += chunk.toString();
141
+ });
142
+ child.once("error", (error) => {
143
+ if (error.code === "ENOENT") {
144
+ reject(new Error(l("tar command not found. Install tar first.", "tar コマンドが見つかりません。tar をインストールしてください。")));
145
+ }
146
+ else {
147
+ reject(error);
148
+ }
149
+ });
150
+ child.once("close", (code) => {
151
+ if (code === 0) {
152
+ resolve();
153
+ }
154
+ else {
155
+ reject(new Error(l(`tar failed (code ${code}): ${stderr.trim()}`, `tar が失敗しました (code ${code}): ${stderr.trim()}`)));
156
+ }
157
+ });
158
+ });
159
+ }
160
+ function runTarCapture(args) {
161
+ return new Promise((resolve, reject) => {
162
+ const child = spawn("tar", args, { stdio: ["ignore", "pipe", "pipe"] });
163
+ let stdout = "";
164
+ let stderr = "";
165
+ child.stdout.on("data", (chunk) => {
166
+ stdout += chunk.toString();
167
+ });
168
+ child.stderr.on("data", (chunk) => {
169
+ stderr += chunk.toString();
170
+ });
171
+ child.once("error", (error) => {
172
+ if (error.code === "ENOENT") {
173
+ reject(new Error(l("tar command not found. Install tar first.", "tar コマンドが見つかりません。tar をインストールしてください。")));
174
+ }
175
+ else {
176
+ reject(error);
177
+ }
178
+ });
179
+ child.once("close", (code) => {
180
+ if (code === 0) {
181
+ resolve(stdout);
182
+ }
183
+ else {
184
+ reject(new Error(l(`tar failed (code ${code}): ${stderr.trim()}`, `tar が失敗しました (code ${code}): ${stderr.trim()}`)));
185
+ }
186
+ });
187
+ });
188
+ }
189
+ async function listArchiveMetadata(archivePath) {
190
+ const namesOut = await runTarCapture(["-tzf", archivePath]);
191
+ const names = namesOut
192
+ .split("\n")
193
+ .map((line) => line.trim())
194
+ .filter((line) => line.length > 0);
195
+ const verboseOut = await runTarCapture(["-tvzf", archivePath]);
196
+ const types = verboseOut
197
+ .split("\n")
198
+ .map((line) => line.trimStart())
199
+ .filter((line) => line.length > 0)
200
+ .map((line) => line.charAt(0));
201
+ if (types.length !== names.length) {
202
+ throw new Error(l("The archive could not be inspected safely.", "アーカイブの内容を安全に検査できませんでした。"));
203
+ }
204
+ return names.map((entryPath, index) => ({ path: entryPath, type: types[index] }));
205
+ }
206
+ function assertSafeArchiveEntries(entries) {
207
+ for (const entry of entries) {
208
+ const name = entry.path;
209
+ if (!name || name.includes("\0")) {
210
+ throw new Error(l("The archive contains an invalid path.", "アーカイブに不正なパスが含まれています。"));
211
+ }
212
+ if (name.includes("\\") || path.isAbsolute(name) || /^[A-Za-z]:/.test(name)) {
213
+ throw new Error(l(`The archive contains a disallowed path: ${name}`, `アーカイブに許可されないパスが含まれています: ${name}`));
214
+ }
215
+ const normalized = path.posix.normalize(name);
216
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../") || name.split("/").includes("..")) {
217
+ throw new Error(l(`The archive contains a path outside the workspace: ${name}`, `アーカイブにワークスペース外へ出るパスが含まれています: ${name}`));
218
+ }
219
+ if (entry.type !== "-" && entry.type !== "d") {
220
+ throw new Error(l(`The archive contains an entry that is not a regular file or directory: ${name}`, `アーカイブに通常ファイル/ディレクトリ以外が含まれています: ${name}`));
221
+ }
222
+ }
223
+ }
224
+ async function assertExtractedTreeSafe(root) {
225
+ async function walk(dir) {
226
+ for (const entry of await readdir(dir)) {
227
+ const full = path.join(dir, entry);
228
+ const info = await lstat(full);
229
+ if (info.isSymbolicLink()) {
230
+ throw new Error(l(`The archive contains a symbolic link: ${entry}`, `アーカイブにシンボリックリンクが含まれています: ${entry}`));
231
+ }
232
+ if (info.isDirectory()) {
233
+ await walk(full);
234
+ continue;
235
+ }
236
+ if (!info.isFile()) {
237
+ throw new Error(l(`The archive contains an entry that is not a regular file or directory: ${entry}`, `アーカイブに通常ファイル/ディレクトリ以外が含まれています: ${entry}`));
238
+ }
239
+ }
240
+ }
241
+ await walk(root);
242
+ }
243
+ async function copySafeTree(source, target) {
244
+ await mkdir(target, { recursive: true });
245
+ for (const entry of await readdir(source, { withFileTypes: true })) {
246
+ const src = path.join(source, entry.name);
247
+ const dest = path.join(target, entry.name);
248
+ if (entry.isDirectory()) {
249
+ await copySafeTree(src, dest);
250
+ continue;
251
+ }
252
+ if (!entry.isFile()) {
253
+ throw new Error(l(`Cannot restore this file type: ${entry.name}`, `復元できないファイル種別です: ${entry.name}`));
254
+ }
255
+ await mkdir(path.dirname(dest), { recursive: true });
256
+ await copyFile(src, dest);
257
+ }
258
+ }
259
+ export function formatBytes(bytes) {
260
+ if (bytes < 1024)
261
+ return `${bytes} B`;
262
+ const units = ["KB", "MB", "GB"];
263
+ let value = bytes / 1024;
264
+ for (const unit of units) {
265
+ if (value < 1024)
266
+ return `${value.toFixed(1)} ${unit}`;
267
+ value /= 1024;
268
+ }
269
+ return `${value.toFixed(1)} TB`;
270
+ }
@@ -0,0 +1,2 @@
1
+ export declare function scheduleUpdateCheck(workspace?: string): void;
2
+ export declare function consumeUpdateBanner(workspace?: string): Promise<string | null>;