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,98 @@
1
+ export type SkillRuntime = "python" | "typescript";
2
+ export type SkillSource = "builtin" | "user";
3
+ export interface SkillOutputDefinition {
4
+ id: string;
5
+ type: "markdown" | "json";
6
+ path: string;
7
+ filename: string;
8
+ append?: boolean;
9
+ show_saved?: boolean;
10
+ }
11
+ export type DiscordSlashOptionType = "string" | "integer" | "number" | "boolean";
12
+ export interface DiscordSlashOptionChoice {
13
+ name: string;
14
+ value: string | number;
15
+ }
16
+ export interface DiscordSlashOption {
17
+ name: string;
18
+ type: DiscordSlashOptionType;
19
+ description?: string;
20
+ description_ja?: string;
21
+ required?: boolean;
22
+ choices?: DiscordSlashOptionChoice[];
23
+ }
24
+ export interface DiscordSlashInvocation {
25
+ description?: string;
26
+ description_ja?: string;
27
+ options?: DiscordSlashOption[];
28
+ }
29
+ export interface SkillManifest {
30
+ id: string;
31
+ name: string;
32
+ description?: string;
33
+ runtime: SkillRuntime;
34
+ entry: string;
35
+ handler: string;
36
+ invocation?: {
37
+ command?: string;
38
+ phrases?: string[];
39
+ discord_slash?: DiscordSlashInvocation;
40
+ };
41
+ input: {
42
+ schema: Record<string, unknown>;
43
+ };
44
+ outputs: SkillOutputDefinition[];
45
+ ai_steps?: Array<{
46
+ id: string;
47
+ purpose: string;
48
+ model: string;
49
+ optional?: boolean;
50
+ }>;
51
+ memory?: {
52
+ namespace: string;
53
+ read?: boolean;
54
+ write?: boolean;
55
+ };
56
+ retry?: {
57
+ max_attempts?: number;
58
+ delay_ms?: number;
59
+ };
60
+ required_env?: Array<{
61
+ name: string;
62
+ description?: string;
63
+ optional?: boolean;
64
+ }>;
65
+ enabled?: boolean;
66
+ output_mode?: string;
67
+ /**
68
+ * Marks a skill as performing user-visible side effects (add, delete, send,
69
+ * update, save). The chat engine drops the LLM's narrative for any turn that
70
+ * invokes a side-effect skill so the deterministic skill result is the only
71
+ * record — both for the user and in the conversation history. This prevents
72
+ * future-tense announcements like "I'll add it now" from sitting next to a
73
+ * successful tool result and getting misread as "not done yet" on the next
74
+ * turn.
75
+ */
76
+ side_effect?: boolean;
77
+ dir: string;
78
+ /**
79
+ * Where this skill was loaded from. `builtin` = packaged with agent-sin
80
+ * (read-only, structurally protected from deletion). `user` = lives in the
81
+ * user's workspace skills directory. Derived from the resolved path; the
82
+ * value in skill.yaml itself is ignored to prevent a workspace file from
83
+ * impersonating a builtin.
84
+ */
85
+ source: SkillSource;
86
+ /**
87
+ * Set to true in a workspace skill manifest to deliberately override a
88
+ * builtin skill with the same id. Without this flag, workspace copies of
89
+ * builtin ids are ignored (and cleaned up by migration).
90
+ */
91
+ override?: boolean;
92
+ }
93
+ export declare function builtinSkillsDir(): string;
94
+ export declare function listBuiltinSkillIds(): Promise<Set<string>>;
95
+ export declare function listSkillManifests(workspaceSkillsDir: string): Promise<SkillManifest[]>;
96
+ export declare function findSkillManifest(skillsDir: string, skillId: string): Promise<SkillManifest>;
97
+ export declare function loadSkillManifest(skillDir: string): Promise<SkillManifest>;
98
+ export declare function resolveSkillEntryPath(manifest: SkillManifest): Promise<string>;
@@ -0,0 +1,319 @@
1
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import YAML from "yaml";
5
+ import { loadSkillSettings } from "./skill-settings.js";
6
+ import { l, localizeObject } from "./i18n.js";
7
+ export function builtinSkillsDir() {
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ return path.resolve(here, "../..", "builtin-skills");
10
+ }
11
+ export async function listBuiltinSkillIds() {
12
+ return new Set(await readSkillDirIds(builtinSkillsDir()));
13
+ }
14
+ export async function listSkillManifests(workspaceSkillsDir) {
15
+ const builtinManifests = await readManifestsFromDir(builtinSkillsDir(), "builtin");
16
+ const builtinIds = new Set(builtinManifests.map((m) => m.id));
17
+ const workspaceManifests = await readManifestsFromDir(workspaceSkillsDir, "user");
18
+ const byId = new Map();
19
+ for (const manifest of builtinManifests) {
20
+ byId.set(manifest.id, manifest);
21
+ }
22
+ for (const manifest of workspaceManifests) {
23
+ if (builtinIds.has(manifest.id) && manifest.override !== true) {
24
+ // Workspace dir collides with a builtin id but did not opt in to override.
25
+ // Ignore it here — migration archives or deletes it on the next startup.
26
+ continue;
27
+ }
28
+ byId.set(manifest.id, manifest);
29
+ }
30
+ const workspace = path.dirname(workspaceSkillsDir);
31
+ const settings = await loadSkillSettings(workspace);
32
+ for (const manifest of byId.values()) {
33
+ if (settings.disabled.has(manifest.id)) {
34
+ manifest.enabled = false;
35
+ }
36
+ }
37
+ return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
38
+ }
39
+ export async function findSkillManifest(skillsDir, skillId) {
40
+ const manifests = await listSkillManifests(skillsDir);
41
+ const manifest = manifests.find((item) => item.id === skillId);
42
+ if (!manifest) {
43
+ throw new Error(l(`Skill not found: ${skillId}`, `スキルが見つかりません: ${skillId}`));
44
+ }
45
+ return manifest;
46
+ }
47
+ export async function loadSkillManifest(skillDir) {
48
+ const raw = await readFile(path.join(skillDir, "skill.yaml"), "utf8");
49
+ const parsedRaw = YAML.parse(raw);
50
+ const parsed = applyManifestDefaults(localizeObject(parsedRaw));
51
+ validateManifest(parsed);
52
+ const source = isInsideBuiltin(skillDir) ? "builtin" : "user";
53
+ const override = parsed.override === true && source === "user";
54
+ return {
55
+ ...parsed,
56
+ outputs: parsed.outputs || [],
57
+ ai_steps: parsed.ai_steps || [],
58
+ required_env: Array.isArray(parsed.required_env)
59
+ ? parsed.required_env
60
+ .filter((entry) => entry && typeof entry.name === "string" && entry.name.length > 0)
61
+ .map((entry) => ({
62
+ name: entry.name,
63
+ description: typeof entry.description === "string" ? entry.description : undefined,
64
+ optional: entry.optional === true,
65
+ }))
66
+ : undefined,
67
+ enabled: parsed.enabled ?? true,
68
+ output_mode: parsed.output_mode,
69
+ side_effect: parsed.side_effect === true,
70
+ dir: skillDir,
71
+ source,
72
+ override,
73
+ };
74
+ }
75
+ export async function resolveSkillEntryPath(manifest) {
76
+ assertSafeRelativeSkillPath(manifest.entry, "entry");
77
+ const logicalDir = path.resolve(manifest.dir);
78
+ const dir = await realpath(manifest.dir);
79
+ const candidate = path.resolve(manifest.dir, manifest.entry);
80
+ if (!isPathInside(logicalDir, candidate)) {
81
+ throw new Error(l(`Skill entry must stay inside skill directory: ${manifest.entry}`, `スキル entry はスキルディレクトリ内にある必要があります: ${manifest.entry}`));
82
+ }
83
+ let resolved;
84
+ try {
85
+ resolved = await realpath(candidate);
86
+ }
87
+ catch {
88
+ throw new Error(l(`Entry file not found: ${path.join(manifest.dir, manifest.entry)}`, `entry ファイルが見つかりません: ${path.join(manifest.dir, manifest.entry)}`));
89
+ }
90
+ if (!isPathInside(dir, resolved)) {
91
+ throw new Error(l(`Skill entry must stay inside skill directory: ${manifest.entry}`, `スキル entry はスキルディレクトリ内にある必要があります: ${manifest.entry}`));
92
+ }
93
+ const info = await stat(resolved);
94
+ if (!info.isFile()) {
95
+ throw new Error(l(`Skill entry is not a file: ${manifest.entry}`, `スキル entry がファイルではありません: ${manifest.entry}`));
96
+ }
97
+ return resolved;
98
+ }
99
+ async function readSkillDirIds(skillsDir) {
100
+ let entries;
101
+ try {
102
+ entries = await readdir(skillsDir, { withFileTypes: true });
103
+ }
104
+ catch {
105
+ return [];
106
+ }
107
+ const ids = [];
108
+ for (const entry of entries) {
109
+ if (!entry.isDirectory()) {
110
+ continue;
111
+ }
112
+ if (entry.name.startsWith(".")) {
113
+ continue;
114
+ }
115
+ const manifestPath = path.join(skillsDir, entry.name, "skill.yaml");
116
+ try {
117
+ await stat(manifestPath);
118
+ ids.push(entry.name);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ }
124
+ return ids;
125
+ }
126
+ async function readManifestsFromDir(skillsDir, source) {
127
+ const ids = await readSkillDirIds(skillsDir);
128
+ const manifests = [];
129
+ for (const id of ids) {
130
+ const skillDir = path.join(skillsDir, id);
131
+ try {
132
+ const manifest = await loadSkillManifest(skillDir);
133
+ // loadSkillManifest derives source from the actual path. If the caller
134
+ // expected this dir to map to `source` but the path is elsewhere
135
+ // (e.g. symlinks), prefer the path-derived value — it's authoritative.
136
+ manifests.push(manifest.source === source ? manifest : { ...manifest, source });
137
+ }
138
+ catch (error) {
139
+ if (source === "builtin") {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ throw new Error(l(`Invalid builtin skill "${id}": ${message}`, `ビルトインスキル "${id}" が不正です: ${message}`));
142
+ }
143
+ continue;
144
+ }
145
+ }
146
+ return manifests;
147
+ }
148
+ function isInsideBuiltin(dir) {
149
+ const root = builtinSkillsDir();
150
+ const resolved = path.resolve(dir);
151
+ return resolved === root || resolved.startsWith(root + path.sep);
152
+ }
153
+ function applyManifestDefaults(raw) {
154
+ const manifest = { ...raw };
155
+ if (typeof manifest.name !== "string" && typeof manifest.id === "string") {
156
+ manifest.name = manifest.id;
157
+ }
158
+ delete manifest.triggers;
159
+ if (typeof manifest.handler !== "string" || manifest.handler.length === 0) {
160
+ manifest.handler = "run";
161
+ }
162
+ if (typeof manifest.entry !== "string" || manifest.entry.length === 0) {
163
+ if (manifest.runtime === "python")
164
+ manifest.entry = "main.py";
165
+ else if (manifest.runtime === "typescript")
166
+ manifest.entry = "main.ts";
167
+ }
168
+ if (!manifest.input ||
169
+ typeof manifest.input !== "object" ||
170
+ Array.isArray(manifest.input) ||
171
+ !manifest.input.schema) {
172
+ manifest.input = { schema: { type: "object", additionalProperties: true } };
173
+ }
174
+ if (!Array.isArray(manifest.outputs))
175
+ manifest.outputs = [];
176
+ if (typeof manifest.output_mode !== "string" || manifest.output_mode.trim().length === 0) {
177
+ delete manifest.output_mode;
178
+ }
179
+ return manifest;
180
+ }
181
+ function validateManifest(manifest) {
182
+ // Hard requirements only — handler/entry are auto-filled from runtime defaults.
183
+ const required = ["id", "runtime"];
184
+ for (const key of required) {
185
+ if (!(key in manifest) || manifest[key] === undefined) {
186
+ throw new Error(l(`Invalid skill manifest: missing ${key}`, `skill manifest が不正です: ${key} がありません`));
187
+ }
188
+ }
189
+ if (!["python", "typescript"].includes(manifest.runtime)) {
190
+ throw new Error(l(`Invalid skill runtime: ${manifest.runtime}`, `skill runtime が不正です: ${manifest.runtime}`));
191
+ }
192
+ if (!/^[a-z][a-z0-9-]*$/.test(manifest.id)) {
193
+ throw new Error(l(`Invalid skill id: ${manifest.id}`, `skill id が不正です: ${manifest.id}`));
194
+ }
195
+ if (typeof manifest.entry !== "string" || manifest.entry.length === 0) {
196
+ throw new Error(l("Invalid skill manifest: missing entry", "skill manifest が不正です: entry がありません"));
197
+ }
198
+ if (typeof manifest.handler !== "string" || manifest.handler.length === 0) {
199
+ throw new Error(l("Invalid skill manifest: missing handler", "skill manifest が不正です: handler がありません"));
200
+ }
201
+ assertSafeRelativeSkillPath(manifest.entry, "entry");
202
+ if (manifest.retry?.max_attempts !== undefined && manifest.retry.max_attempts < 0) {
203
+ throw new Error(l("Invalid skill manifest: retry.max_attempts must be 0 or greater", "skill manifest が不正です: retry.max_attempts は0以上である必要があります"));
204
+ }
205
+ validateInvocation(manifest);
206
+ }
207
+ function validateInvocation(manifest) {
208
+ if (!manifest.invocation) {
209
+ return;
210
+ }
211
+ const command = manifest.invocation.command;
212
+ if (command !== undefined && (typeof command !== "string" || command.trim().length === 0)) {
213
+ throw new Error(l("Invalid skill manifest: invocation.command must be a non-empty string", "skill manifest が不正です: invocation.command は空でない string である必要があります"));
214
+ }
215
+ const phrases = manifest.invocation.phrases;
216
+ if (phrases !== undefined) {
217
+ if (!Array.isArray(phrases) || phrases.some((phrase) => typeof phrase !== "string" || phrase.trim().length === 0)) {
218
+ throw new Error(l("Invalid skill manifest: invocation.phrases must be a list of non-empty strings", "skill manifest が不正です: invocation.phrases は空でない string のリストである必要があります"));
219
+ }
220
+ }
221
+ validateDiscordSlash(manifest);
222
+ }
223
+ const DISCORD_SLASH_OPTION_TYPES = [
224
+ "string",
225
+ "integer",
226
+ "number",
227
+ "boolean",
228
+ ];
229
+ const DISCORD_SLASH_NAME_RE = /^[\w-]{1,32}$/;
230
+ function validateDiscordSlash(manifest) {
231
+ const slash = manifest.invocation?.discord_slash;
232
+ if (slash === undefined)
233
+ return;
234
+ if (typeof slash !== "object" || slash === null || Array.isArray(slash)) {
235
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash must be an object", "skill manifest が不正です: invocation.discord_slash はオブジェクトである必要があります"));
236
+ }
237
+ if (slash.description !== undefined && (typeof slash.description !== "string" || slash.description.length === 0)) {
238
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.description must be a non-empty string", "skill manifest が不正です: invocation.discord_slash.description は空でない string である必要があります"));
239
+ }
240
+ if (slash.description_ja !== undefined && (typeof slash.description_ja !== "string" || slash.description_ja.length === 0)) {
241
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.description_ja must be a non-empty string", "skill manifest が不正です: invocation.discord_slash.description_ja は空でない string である必要があります"));
242
+ }
243
+ if (slash.options !== undefined) {
244
+ if (!Array.isArray(slash.options)) {
245
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.options must be a list", "skill manifest が不正です: invocation.discord_slash.options は配列である必要があります"));
246
+ }
247
+ const seen = new Set();
248
+ for (const option of slash.options) {
249
+ validateDiscordSlashOption(option, seen);
250
+ }
251
+ }
252
+ }
253
+ function validateDiscordSlashOption(option, seen) {
254
+ if (!option || typeof option !== "object" || Array.isArray(option)) {
255
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.options[*] must be an object", "skill manifest が不正です: invocation.discord_slash.options の要素はオブジェクトである必要があります"));
256
+ }
257
+ const opt = option;
258
+ const name = opt.name;
259
+ if (typeof name !== "string" || !DISCORD_SLASH_NAME_RE.test(name)) {
260
+ throw new Error(l(`Invalid skill manifest: invocation.discord_slash.options[*].name must match ${DISCORD_SLASH_NAME_RE} (got ${JSON.stringify(name)})`, `skill manifest が不正です: invocation.discord_slash.options[*].name は ${DISCORD_SLASH_NAME_RE} に一致する必要があります (${JSON.stringify(name)})`));
261
+ }
262
+ if (seen.has(name)) {
263
+ throw new Error(l(`Invalid skill manifest: duplicate discord_slash option name "${name}"`, `skill manifest が不正です: discord_slash の option name "${name}" が重複しています`));
264
+ }
265
+ seen.add(name);
266
+ const type = opt.type;
267
+ if (typeof type !== "string" || !DISCORD_SLASH_OPTION_TYPES.includes(type)) {
268
+ throw new Error(l(`Invalid skill manifest: invocation.discord_slash.options[*].type must be one of ${DISCORD_SLASH_OPTION_TYPES.join("|")} (got ${JSON.stringify(type)})`, `skill manifest が不正です: invocation.discord_slash.options[*].type は ${DISCORD_SLASH_OPTION_TYPES.join("|")} のいずれかである必要があります (${JSON.stringify(type)})`));
269
+ }
270
+ if (opt.description !== undefined && (typeof opt.description !== "string" || opt.description.length === 0)) {
271
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.options[*].description must be a non-empty string", "skill manifest が不正です: invocation.discord_slash.options[*].description は空でない string である必要があります"));
272
+ }
273
+ if (opt.description_ja !== undefined && (typeof opt.description_ja !== "string" || opt.description_ja.length === 0)) {
274
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.options[*].description_ja must be a non-empty string", "skill manifest が不正です: invocation.discord_slash.options[*].description_ja は空でない string である必要があります"));
275
+ }
276
+ if (opt.required !== undefined && typeof opt.required !== "boolean") {
277
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.options[*].required must be a boolean", "skill manifest が不正です: invocation.discord_slash.options[*].required は boolean である必要があります"));
278
+ }
279
+ if (opt.choices !== undefined) {
280
+ if (type === "boolean") {
281
+ throw new Error(l("Invalid skill manifest: discord_slash boolean option cannot have choices", "skill manifest が不正です: discord_slash の boolean option には choices を指定できません"));
282
+ }
283
+ if (!Array.isArray(opt.choices)) {
284
+ throw new Error(l("Invalid skill manifest: invocation.discord_slash.options[*].choices must be a list", "skill manifest が不正です: invocation.discord_slash.options[*].choices は配列である必要があります"));
285
+ }
286
+ for (const choice of opt.choices) {
287
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
288
+ throw new Error(l("Invalid skill manifest: discord_slash choice must be an object", "skill manifest が不正です: discord_slash の choice はオブジェクトである必要があります"));
289
+ }
290
+ const c = choice;
291
+ if (typeof c.name !== "string" || c.name.length === 0) {
292
+ throw new Error(l("Invalid skill manifest: discord_slash choice.name must be a non-empty string", "skill manifest が不正です: discord_slash の choice.name は空でない string である必要があります"));
293
+ }
294
+ const expectedValueType = type === "string" ? "string" : "number";
295
+ if (typeof c.value !== expectedValueType) {
296
+ throw new Error(l(`Invalid skill manifest: discord_slash choice.value must be a ${expectedValueType} for option type ${type}`, `skill manifest が不正です: discord_slash の choice.value は option type ${type} に対して ${expectedValueType} である必要があります`));
297
+ }
298
+ }
299
+ }
300
+ }
301
+ function assertSafeRelativeSkillPath(value, label) {
302
+ if (typeof value !== "string" || value.trim().length === 0) {
303
+ throw new Error(l(`Invalid skill manifest: ${label} must be a non-empty relative path`, `skill manifest が不正です: ${label} は空でない相対パスである必要があります`));
304
+ }
305
+ if (value.includes("\0")) {
306
+ throw new Error(l(`Invalid skill manifest: ${label} contains a null byte`, `skill manifest が不正です: ${label} に null byte が含まれています`));
307
+ }
308
+ if (path.isAbsolute(value)) {
309
+ throw new Error(l(`Invalid skill manifest: ${label} must be relative`, `skill manifest が不正です: ${label} は相対パスである必要があります`));
310
+ }
311
+ const normalized = path.normalize(value);
312
+ if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
313
+ throw new Error(l(`Invalid skill manifest: ${label} must stay inside the skill directory`, `skill manifest が不正です: ${label} はスキルディレクトリ内にある必要があります`));
314
+ }
315
+ }
316
+ function isPathInside(root, target) {
317
+ const relative = path.relative(root, target);
318
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
319
+ }
@@ -0,0 +1,33 @@
1
+ import { type AppConfig } from "./config.js";
2
+ import { type SkillManifest } from "./skill-registry.js";
3
+ export type SkillRuntimeChoice = "python" | "typescript";
4
+ export interface SkillScaffoldOptions {
5
+ id: string;
6
+ runtime: SkillRuntimeChoice;
7
+ name?: string;
8
+ description?: string;
9
+ templateRoot?: string;
10
+ }
11
+ export interface SkillScaffoldResult {
12
+ skill_id: string;
13
+ skill_dir: string;
14
+ manifest_path: string;
15
+ entry_path: string;
16
+ files: string[];
17
+ runtime: SkillRuntimeChoice;
18
+ }
19
+ export declare function scaffoldSkill(config: AppConfig, options: SkillScaffoldOptions): Promise<SkillScaffoldResult>;
20
+ export declare function validateSkillId(id: string): void;
21
+ export interface ValidateSkillResult {
22
+ ok: boolean;
23
+ manifest?: SkillManifest;
24
+ skill_dir: string;
25
+ errors: string[];
26
+ warnings: string[];
27
+ }
28
+ export interface ValidateSkillOptions {
29
+ knownModelIds?: Set<string>;
30
+ }
31
+ export declare function validateInstalledSkill(config: AppConfig, skillId: string): Promise<ValidateSkillResult>;
32
+ export declare function validateSkillDirectory(skillDir: string, skillId: string, options?: ValidateSkillOptions): Promise<ValidateSkillResult>;
33
+ export declare function loadKnownModelIds(config: AppConfig): Promise<Set<string>>;