agent-sin 0.1.12 → 0.1.16

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 (97) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +2 -1
  3. package/builtin-skills/_shared/_todo_lib.py +290 -0
  4. package/builtin-skills/even-g2-setup/main.ts +896 -0
  5. package/builtin-skills/even-g2-setup/skill.yaml +133 -0
  6. package/builtin-skills/memo-delete/main.py +28 -107
  7. package/builtin-skills/memo-delete/skill.yaml +10 -21
  8. package/builtin-skills/memo-index/main.py +96 -64
  9. package/builtin-skills/memo-index/skill.yaml +4 -10
  10. package/builtin-skills/memo-list/main.py +126 -72
  11. package/builtin-skills/memo-list/skill.yaml +8 -14
  12. package/builtin-skills/memo-save/main.py +191 -25
  13. package/builtin-skills/memo-save/skill.yaml +29 -5
  14. package/builtin-skills/memo-search/main.py +38 -18
  15. package/builtin-skills/memo-vector-search/main.py +11 -6
  16. package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
  17. package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
  18. package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
  19. package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
  20. package/builtin-skills/schedule-add/main.py +26 -0
  21. package/builtin-skills/service-restart/main.ts +249 -0
  22. package/builtin-skills/service-restart/skill.yaml +49 -0
  23. package/builtin-skills/todo-add/main.py +3 -1
  24. package/builtin-skills/todo-delete/main.py +3 -1
  25. package/builtin-skills/todo-done/main.py +3 -1
  26. package/builtin-skills/todo-list/main.py +4 -1
  27. package/builtin-skills/todo-tick/main.py +3 -1
  28. package/builtin-skills/topic-knowledge-read/main.py +118 -0
  29. package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
  30. package/dist/builder/build-action-classifier.d.ts +18 -0
  31. package/dist/builder/build-action-classifier.js +82 -1
  32. package/dist/builder/build-flow.d.ts +33 -4
  33. package/dist/builder/build-flow.js +251 -89
  34. package/dist/builder/builder-session.d.ts +1 -1
  35. package/dist/builder/builder-session.js +112 -7
  36. package/dist/builder/conversation-router.d.ts +4 -2
  37. package/dist/builder/conversation-router.js +19 -2
  38. package/dist/cli/index.js +323 -20
  39. package/dist/core/ai-provider.d.ts +1 -0
  40. package/dist/core/ai-provider.js +8 -3
  41. package/dist/core/chat-engine.d.ts +9 -3
  42. package/dist/core/chat-engine.js +1263 -146
  43. package/dist/core/config.d.ts +4 -0
  44. package/dist/core/config.js +82 -0
  45. package/dist/core/daily-memory-promotion.d.ts +7 -0
  46. package/dist/core/daily-memory-promotion.js +596 -18
  47. package/dist/core/image-attachments.d.ts +31 -0
  48. package/dist/core/image-attachments.js +237 -0
  49. package/dist/core/logger.d.ts +2 -1
  50. package/dist/core/logger.js +77 -1
  51. package/dist/core/memo-migration.d.ts +3 -0
  52. package/dist/core/memo-migration.js +422 -0
  53. package/dist/core/native-modules.d.ts +24 -0
  54. package/dist/core/native-modules.js +99 -0
  55. package/dist/core/notifier.d.ts +8 -3
  56. package/dist/core/notifier.js +191 -17
  57. package/dist/core/obsidian-vault.d.ts +19 -0
  58. package/dist/core/obsidian-vault.js +477 -0
  59. package/dist/core/operating-model.d.ts +2 -0
  60. package/dist/core/operating-model.js +15 -0
  61. package/dist/core/output-writer.d.ts +3 -2
  62. package/dist/core/output-writer.js +108 -7
  63. package/dist/core/profile-memory.js +22 -1
  64. package/dist/core/runtime.d.ts +2 -0
  65. package/dist/core/runtime.js +9 -1
  66. package/dist/core/secrets.d.ts +4 -0
  67. package/dist/core/secrets.js +34 -0
  68. package/dist/core/skill-history.d.ts +44 -0
  69. package/dist/core/skill-history.js +329 -0
  70. package/dist/core/skill-registry.d.ts +5 -0
  71. package/dist/core/skill-registry.js +11 -0
  72. package/dist/discord/bot.d.ts +1 -0
  73. package/dist/discord/bot.js +181 -10
  74. package/dist/even-g2/gateway.d.ts +15 -0
  75. package/dist/even-g2/gateway.js +868 -0
  76. package/dist/runtimes/codex-app-server.d.ts +5 -1
  77. package/dist/runtimes/codex-app-server.js +147 -8
  78. package/dist/runtimes/python-runner.js +82 -0
  79. package/dist/runtimes/typescript-runner.js +13 -1
  80. package/dist/skills-sdk/types.d.ts +19 -4
  81. package/dist/telegram/bot.d.ts +1 -0
  82. package/dist/telegram/bot.js +115 -7
  83. package/package.json +3 -1
  84. package/templates/even-g2-agent/README.md +83 -0
  85. package/templates/even-g2-agent/app.json +20 -0
  86. package/templates/even-g2-agent/index.html +31 -0
  87. package/templates/even-g2-agent/package-lock.json +1836 -0
  88. package/templates/even-g2-agent/package.json +22 -0
  89. package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
  90. package/templates/even-g2-agent/src/embedded-config.ts +4 -0
  91. package/templates/even-g2-agent/src/main.ts +539 -0
  92. package/templates/even-g2-agent/src/style.css +70 -0
  93. package/templates/even-g2-agent/tsconfig.json +11 -0
  94. package/templates/skill-python/main.py +20 -2
  95. package/templates/skill-python/skill.yaml +9 -0
  96. package/templates/skill-typescript/main.ts +40 -5
  97. package/templates/skill-typescript/skill.yaml +9 -0
@@ -0,0 +1,249 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const DARWIN_LABEL = "com.agent-sin.gateway";
7
+ const WINDOWS_TASK_NAME = "Agent-Sin Gateway";
8
+ const DEFAULT_DELAY_MS = 12000;
9
+
10
+ type SkillInput = {
11
+ args?: Record<string, unknown>;
12
+ sources?: {
13
+ workspace?: string;
14
+ locale?: string;
15
+ };
16
+ };
17
+
18
+ type SkillResult = {
19
+ status: "ok" | "error" | "skipped";
20
+ title: string;
21
+ summary: string;
22
+ outputs: Record<string, unknown>;
23
+ data: Record<string, unknown>;
24
+ suggestions: unknown[];
25
+ };
26
+
27
+ export async function run(_ctx: unknown, input: SkillInput): Promise<SkillResult> {
28
+ const locale = input.sources?.locale === "ja" ? "ja" : "en";
29
+ const workspace = input.sources?.workspace || process.cwd();
30
+ const delayMs = clampDelay(input.args?.delay_ms);
31
+
32
+ if (process.platform === "darwin") {
33
+ return scheduleDarwinRestart(locale, workspace, delayMs);
34
+ }
35
+ if (process.platform === "win32") {
36
+ return scheduleWindowsRestart(locale, workspace, delayMs);
37
+ }
38
+
39
+ return errorResult(
40
+ locale,
41
+ "Restart is not supported",
42
+ "自動再起動は未対応です",
43
+ "Automatic service restart is supported on macOS and Windows. Run `agent-sin service restart` from a terminal on this platform.",
44
+ "この環境では自動再起動に未対応です。ターミナルで `agent-sin service restart` を実行してください。",
45
+ { platform: process.platform },
46
+ );
47
+ }
48
+
49
+ async function scheduleDarwinRestart(locale: "en" | "ja", workspace: string, delayMs: number): Promise<SkillResult> {
50
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${DARWIN_LABEL}.plist`);
51
+ try {
52
+ await access(plistPath);
53
+ } catch {
54
+ return errorResult(
55
+ locale,
56
+ "Service is not installed",
57
+ "サービス未登録",
58
+ "Agent-Sin service is not installed. Run `agent-sin service install` first.",
59
+ "Agent-Sin service が登録されていません。先に `agent-sin service install` を実行してください。",
60
+ { platform: "darwin", plist_path: plistPath },
61
+ );
62
+ }
63
+
64
+ const uid = typeof process.getuid === "function" ? process.getuid() : os.userInfo().uid;
65
+ const target = `gui/${uid}/${DARWIN_LABEL}`;
66
+ const loaded = await commandSucceeds("launchctl", ["print", target]);
67
+ const helperScript = loaded ? darwinKickstartScript() : cliRestartScript();
68
+ const helperEnv = loaded
69
+ ? { AGENT_SIN_RESTART_DELAY_MS: String(delayMs), AGENT_SIN_SERVICE_TARGET: target }
70
+ : cliRestartEnv(workspace, delayMs);
71
+
72
+ try {
73
+ await spawnDetachedHelper(helperScript, helperEnv, workspace);
74
+ } catch (error) {
75
+ return errorResult(
76
+ locale,
77
+ "Restart failed",
78
+ "再起動できませんでした",
79
+ `Could not schedule restart: ${errorMessage(error)}`,
80
+ `再起動を予約できませんでした: ${errorMessage(error)}`,
81
+ { platform: "darwin" },
82
+ );
83
+ }
84
+
85
+ return okResult(locale, delayMs, loaded ? "launchctl-kickstart" : "service-restart");
86
+ }
87
+
88
+ async function scheduleWindowsRestart(locale: "en" | "ja", workspace: string, delayMs: number): Promise<SkillResult> {
89
+ const installed = await commandSucceeds("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME]);
90
+ if (!installed) {
91
+ return errorResult(
92
+ locale,
93
+ "Service is not installed",
94
+ "サービス未登録",
95
+ "Agent-Sin service is not installed. Run `agent-sin service install` first.",
96
+ "Agent-Sin service が登録されていません。先に `agent-sin service install` を実行してください。",
97
+ { platform: "win32", task_name: WINDOWS_TASK_NAME },
98
+ );
99
+ }
100
+
101
+ try {
102
+ await spawnDetachedHelper(windowsRestartScript(), {
103
+ AGENT_SIN_RESTART_DELAY_MS: String(delayMs),
104
+ AGENT_SIN_WINDOWS_TASK_NAME: WINDOWS_TASK_NAME,
105
+ }, workspace);
106
+ } catch (error) {
107
+ return errorResult(
108
+ locale,
109
+ "Restart failed",
110
+ "再起動できませんでした",
111
+ `Could not schedule restart: ${errorMessage(error)}`,
112
+ `再起動を予約できませんでした: ${errorMessage(error)}`,
113
+ { platform: "win32" },
114
+ );
115
+ }
116
+
117
+ return okResult(locale, delayMs, "schtasks-restart");
118
+ }
119
+
120
+ function darwinKickstartScript(): string {
121
+ return `
122
+ const { spawn } = require("node:child_process");
123
+ const delay = Number(process.env.AGENT_SIN_RESTART_DELAY_MS || ${DEFAULT_DELAY_MS});
124
+ const target = process.env.AGENT_SIN_SERVICE_TARGET;
125
+ setTimeout(() => {
126
+ const child = spawn("launchctl", ["kickstart", "-k", target], { detached: true, stdio: "ignore" });
127
+ child.unref();
128
+ }, delay);
129
+ setTimeout(() => process.exit(0), delay + 2000);
130
+ `;
131
+ }
132
+
133
+ function windowsRestartScript(): string {
134
+ return `
135
+ const { spawn } = require("node:child_process");
136
+ const delay = Number(process.env.AGENT_SIN_RESTART_DELAY_MS || ${DEFAULT_DELAY_MS});
137
+ const taskName = process.env.AGENT_SIN_WINDOWS_TASK_NAME;
138
+ function run(args) {
139
+ return new Promise((resolve) => {
140
+ const child = spawn("schtasks", args, { stdio: "ignore", windowsHide: true });
141
+ child.on("error", () => resolve());
142
+ child.on("exit", () => resolve());
143
+ });
144
+ }
145
+ setTimeout(async () => {
146
+ await run(["/End", "/TN", taskName]);
147
+ setTimeout(async () => {
148
+ await run(["/Run", "/TN", taskName]);
149
+ process.exit(0);
150
+ }, 1000);
151
+ }, delay);
152
+ setTimeout(() => process.exit(0), delay + 15000);
153
+ `;
154
+ }
155
+
156
+ function cliRestartScript(): string {
157
+ return `
158
+ const { spawn } = require("node:child_process");
159
+ const delay = Number(process.env.AGENT_SIN_RESTART_DELAY_MS || ${DEFAULT_DELAY_MS});
160
+ const cli = process.env.AGENT_SIN_CLI_PATH;
161
+ const workspace = process.env.AGENT_SIN_WORKSPACE || process.cwd();
162
+ setTimeout(() => {
163
+ const child = spawn(process.execPath, [cli, "service", "restart"], {
164
+ detached: true,
165
+ stdio: "ignore",
166
+ cwd: workspace,
167
+ env: process.env,
168
+ windowsHide: true,
169
+ });
170
+ child.unref();
171
+ }, delay);
172
+ setTimeout(() => process.exit(0), delay + 5000);
173
+ `;
174
+ }
175
+
176
+ function cliRestartEnv(workspace: string, delayMs: number): Record<string, string> {
177
+ return {
178
+ AGENT_SIN_RESTART_DELAY_MS: String(delayMs),
179
+ AGENT_SIN_CLI_PATH: process.argv[1] || "agent-sin",
180
+ AGENT_SIN_WORKSPACE: workspace,
181
+ AGENT_SIN_HOME: process.env.AGENT_SIN_HOME || workspace,
182
+ };
183
+ }
184
+
185
+ function spawnDetachedHelper(script: string, extraEnv: Record<string, string>, cwd: string): Promise<void> {
186
+ return new Promise((resolve, reject) => {
187
+ const child = spawn(process.execPath, ["-e", script], {
188
+ detached: true,
189
+ stdio: "ignore",
190
+ cwd,
191
+ env: { ...process.env, ...extraEnv },
192
+ windowsHide: true,
193
+ });
194
+ child.once("error", reject);
195
+ child.once("spawn", () => {
196
+ child.unref();
197
+ resolve();
198
+ });
199
+ });
200
+ }
201
+
202
+ function commandSucceeds(command: string, args: string[]): Promise<boolean> {
203
+ return new Promise((resolve) => {
204
+ execFile(command, args, { windowsHide: true }, (error) => {
205
+ resolve(!error);
206
+ });
207
+ });
208
+ }
209
+
210
+ function clampDelay(value: unknown): number {
211
+ const parsed = Number(value);
212
+ if (!Number.isFinite(parsed)) return DEFAULT_DELAY_MS;
213
+ return Math.max(DEFAULT_DELAY_MS, Math.min(30000, Math.floor(parsed)));
214
+ }
215
+
216
+ function okResult(locale: "en" | "ja", delayMs: number, action: string): SkillResult {
217
+ return {
218
+ status: "ok",
219
+ title: locale === "ja" ? "再起動します" : "Restarting",
220
+ summary: locale === "ja"
221
+ ? "Agent-Sinを再起動します。数秒後に一度切断され、再接続します。続きの確認は再接続後にもう一度送ってください。"
222
+ : "Restarting Agent-Sin. It will disconnect briefly and reconnect in a few seconds. Send the next check again after it reconnects.",
223
+ outputs: {},
224
+ data: { delay_ms: delayMs, action },
225
+ suggestions: [],
226
+ };
227
+ }
228
+
229
+ function errorResult(
230
+ locale: "en" | "ja",
231
+ titleEn: string,
232
+ titleJa: string,
233
+ summaryEn: string,
234
+ summaryJa: string,
235
+ data: Record<string, unknown>,
236
+ ): SkillResult {
237
+ return {
238
+ status: "error",
239
+ title: locale === "ja" ? titleJa : titleEn,
240
+ summary: locale === "ja" ? summaryJa : summaryEn,
241
+ outputs: {},
242
+ data,
243
+ suggestions: [],
244
+ };
245
+ }
246
+
247
+ function errorMessage(error: unknown): string {
248
+ return error instanceof Error ? error.message : String(error);
249
+ }
@@ -0,0 +1,49 @@
1
+ # Builtin: service-restart
2
+ # Agent-Sin の常駐サービスを再起動する。Discord / Telegram から設定反映が必要なときに使う。
3
+
4
+ id: service-restart
5
+ name: Agent-Sin Restart
6
+ name_i18n:
7
+ en: Agent-Sin Restart
8
+ ja: Agent-Sin再起動
9
+ description: Agent-Sinの常駐サービスを再起動する
10
+ description_i18n:
11
+ en: Restart the Agent-Sin background service
12
+ ja: Agent-Sinの常駐サービスを再起動する
13
+ runtime: typescript
14
+ entry: main.ts
15
+ handler: run
16
+ output_mode: raw
17
+ side_effect: true
18
+
19
+ invocation:
20
+ command: service.restart
21
+ phrases:
22
+ - Agent-Sinを再起動して
23
+ - agent-sinを再起動して
24
+ - 再起動して
25
+ - ボットを再起動して
26
+ phrases_i18n:
27
+ en:
28
+ - restart agent-sin
29
+ - restart the bot
30
+ - restart the service
31
+ ja:
32
+ - Agent-Sinを再起動して
33
+ - agent-sinを再起動して
34
+ - 再起動して
35
+ - ボットを再起動して
36
+
37
+ input:
38
+ schema:
39
+ type: object
40
+ additionalProperties: false
41
+ properties:
42
+ delay_ms:
43
+ type: integer
44
+ minimum: 12000
45
+ maximum: 30000
46
+ description: Restart delay in milliseconds
47
+ description_i18n:
48
+ en: Restart delay in milliseconds
49
+ ja: 再起動までの待ち時間(ミリ秒)
@@ -19,6 +19,7 @@ from datetime import datetime
19
19
 
20
20
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
21
21
  from i18n import localizer # noqa: E402
22
+ from _todo_lib import sync_todos, flush_todos # noqa: E402
22
23
 
23
24
 
24
25
  async def run(ctx, input):
@@ -30,7 +31,7 @@ async def run(ctx, input):
30
31
  if not text:
31
32
  return result_skipped(loc.t("No ToDo", "ToDoなし"), loc.t("There is nothing to add.", "追加する内容がありません"))
32
33
 
33
- items = list((await ctx.memory.get("items")) or [])
34
+ items = await sync_todos(ctx, input)
34
35
  now = input.get("trigger", {}).get("time") or datetime.now().isoformat()
35
36
  item = {
36
37
  "id": secrets.token_hex(3),
@@ -42,6 +43,7 @@ async def run(ctx, input):
42
43
  item["due"] = due
43
44
  items.append(item)
44
45
  await ctx.memory.set("items", items)
46
+ await flush_todos(ctx, input, items)
45
47
  ctx.log.info(f"todo-add: id={item['id']} total={len(items)}")
46
48
 
47
49
  summary = loc.t(f"Added: {text}", f"追加: {text}")
@@ -16,6 +16,7 @@ import sys
16
16
 
17
17
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
18
18
  from i18n import localizer # noqa: E402
19
+ from _todo_lib import sync_todos, flush_todos # noqa: E402
19
20
 
20
21
 
21
22
  async def run(ctx, input):
@@ -26,7 +27,7 @@ async def run(ctx, input):
26
27
  if not target_id:
27
28
  return error_result(loc.t("ID is missing", "IDが指定されていません"), loc.t("Specify the ToDo ID to delete.", "削除するToDoのIDを指定してください"))
28
29
 
29
- items = list((await ctx.memory.get("items")) or [])
30
+ items = await sync_todos(ctx, input)
30
31
  if not items:
31
32
  return error_result(loc.t("No ToDos", "ToDoがありません"), loc.t("There are no registered ToDos yet.", "ToDoがまだ登録されていません"))
32
33
 
@@ -42,6 +43,7 @@ async def run(ctx, input):
42
43
  target = matches[0]
43
44
  remaining = [i for i in items if i is not target]
44
45
  await ctx.memory.set("items", remaining)
46
+ await flush_todos(ctx, input, remaining)
45
47
  ctx.log.info(f"todo-delete: id={target.get('id')} remaining={len(remaining)}")
46
48
 
47
49
  return {
@@ -16,6 +16,7 @@ from datetime import datetime
16
16
 
17
17
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
18
18
  from i18n import localizer # noqa: E402
19
+ from _todo_lib import sync_todos, flush_todos # noqa: E402
19
20
 
20
21
 
21
22
  async def run(ctx, input):
@@ -26,7 +27,7 @@ async def run(ctx, input):
26
27
  if not target_id:
27
28
  return error_result(loc.t("ID is missing", "IDが指定されていません"), loc.t("Specify the ToDo ID to complete.", "完了するToDoのIDを指定してください"))
28
29
 
29
- items = list((await ctx.memory.get("items")) or [])
30
+ items = await sync_todos(ctx, input)
30
31
  if not items:
31
32
  return error_result(loc.t("No ToDos", "ToDoがありません"), loc.t("There are no registered ToDos yet.", "ToDoがまだ登録されていません"))
32
33
 
@@ -48,6 +49,7 @@ async def run(ctx, input):
48
49
  target["status"] = "done"
49
50
  target["completed_at"] = now
50
51
  await ctx.memory.set("items", items)
52
+ await flush_todos(ctx, input, items)
51
53
  ctx.log.info(f"todo-done: id={target.get('id')}")
52
54
 
53
55
  return result_ok(loc.t("Done", "完了"), loc.t(f"Done: {text}", f"完了: {text}"), target)
@@ -18,6 +18,7 @@ from datetime import datetime, timezone
18
18
 
19
19
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
20
20
  from i18n import localizer # noqa: E402
21
+ from _todo_lib import sync_todos # noqa: E402
21
22
 
22
23
 
23
24
  async def run(ctx, input):
@@ -25,7 +26,9 @@ async def run(ctx, input):
25
26
  args = input.get("args", {})
26
27
  status = str(args.get("status", "open")).strip() or "open"
27
28
 
28
- items = list((await ctx.memory.get("items")) or [])
29
+ # todo-list read-only スキル。Obsidian 側のマージ結果を表示するだけで、
30
+ # memory / ファイルへの永続化は行わない (次の write 可能スキルで反映される)。
31
+ items = await sync_todos(ctx, input)
29
32
  open_items = [i for i in items if i.get("status") == "open"]
30
33
  done_items = [i for i in items if i.get("status") == "done"]
31
34
  if status == "open":
@@ -20,6 +20,7 @@ from datetime import datetime, timezone
20
20
 
21
21
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
22
22
  from i18n import localizer # noqa: E402
23
+ from _todo_lib import sync_todos, flush_todos # noqa: E402
23
24
 
24
25
 
25
26
  async def run(ctx, input):
@@ -27,7 +28,7 @@ async def run(ctx, input):
27
28
  args = input.get("args", {})
28
29
  channel = str(args.get("channel", "auto")).strip() or "auto"
29
30
 
30
- items = list((await ctx.memory.get("items")) or [])
31
+ items = await sync_todos(ctx, input)
31
32
  if not items:
32
33
  return result_ok(loc.t("Nothing due", "対象なし"), loc.t("There are no ToDos.", "ToDoはありません"), [], 0)
33
34
 
@@ -70,6 +71,7 @@ async def run(ctx, input):
70
71
 
71
72
  if fired:
72
73
  await ctx.memory.set("items", items)
74
+ await flush_todos(ctx, input, items)
73
75
 
74
76
  pending = sum(
75
77
  1 for i in items
@@ -0,0 +1,118 @@
1
+ """Builtin: topic-knowledge-read
2
+
3
+ Read full per-topic knowledge files produced by nightly-topic-knowledge.
4
+
5
+ Read-only. The chat engine includes a short topic index on every turn; the AI
6
+ calls this skill to load the full content of the few topics it actually needs.
7
+ Missing / unsanitizable ids are reported in `data.missing` rather than failing
8
+ the whole call so the model can still use what it got.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ from typing import Any
18
+
19
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
20
+ from i18n import localizer # noqa: E402
21
+
22
+
23
+ TOPIC_ID_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
24
+
25
+
26
+ async def run(ctx, input):
27
+ loc = localizer(input)
28
+ args = input.get("args", {}) or {}
29
+ sources = input.get("sources", {}) or {}
30
+ memory_dir = sources.get("memory_dir")
31
+ if not memory_dir:
32
+ return _err(
33
+ loc.t("Workspace unavailable", "ワークスペース不明"),
34
+ loc.t("memory_dir is not provided.", "memory_dir が取得できません"),
35
+ )
36
+
37
+ raw_ids = args.get("topic_ids")
38
+ if not isinstance(raw_ids, list) or not raw_ids:
39
+ return _err(
40
+ loc.t("Missing input", "入力不足"),
41
+ loc.t("topic_ids is required and must not be empty.", "topic_ids は必須かつ空にできません"),
42
+ )
43
+
44
+ topics_root = os.path.join(memory_dir, "topic-knowledge", "topics")
45
+ found: list[dict[str, Any]] = []
46
+ missing: list[str] = []
47
+ seen: set[str] = set()
48
+ for raw in raw_ids:
49
+ tid = _sanitize(raw)
50
+ if not tid or tid in seen:
51
+ if isinstance(raw, str) and raw.strip():
52
+ missing.append(raw.strip())
53
+ continue
54
+ seen.add(tid)
55
+ file_path = os.path.join(topics_root, f"{tid}.json")
56
+ if not os.path.isfile(file_path):
57
+ missing.append(tid)
58
+ continue
59
+ try:
60
+ with open(file_path, "r", encoding="utf-8") as f:
61
+ data = json.load(f)
62
+ except Exception as e:
63
+ ctx.log.warn(f"topic-knowledge-read: failed to read {tid}: {e}")
64
+ missing.append(tid)
65
+ continue
66
+ found.append({"topic_id": tid, "path": file_path, "data": data})
67
+
68
+ if not found:
69
+ return {
70
+ "status": "skipped",
71
+ "title": loc.t("No topics found", "該当なし"),
72
+ "summary": loc.t(
73
+ f"No knowledge files found for: {', '.join(missing) or '(none)'}",
74
+ f"該当するトピックナレッジが見つかりません: {', '.join(missing) or '(なし)'}",
75
+ ),
76
+ "outputs": {},
77
+ "data": {"found": [], "missing": missing},
78
+ "suggestions": [],
79
+ }
80
+
81
+ ctx.log.info(
82
+ f"topic-knowledge-read: loaded {len(found)} topic(s), missing={len(missing)}"
83
+ )
84
+ missing_joined = ", ".join(missing)
85
+ en_suffix = f" (missing: {missing_joined})" if missing else ""
86
+ ja_suffix = f"(不在: {missing_joined})" if missing else ""
87
+ return {
88
+ "status": "ok",
89
+ "title": loc.t("Topics loaded", "読み込み完了"),
90
+ "summary": loc.t(
91
+ f"Loaded {len(found)} topic(s){en_suffix}",
92
+ f"{len(found)}件のトピックを読み込みました{ja_suffix}",
93
+ ),
94
+ "outputs": {},
95
+ "data": {"found": found, "missing": missing},
96
+ "suggestions": [],
97
+ }
98
+
99
+
100
+ def _sanitize(raw: object) -> str | None:
101
+ if not isinstance(raw, str):
102
+ return None
103
+ text = raw.strip().lower()
104
+ text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
105
+ if not text or not TOPIC_ID_PATTERN.match(text):
106
+ return None
107
+ return text
108
+
109
+
110
+ def _err(title: str, summary: str) -> dict[str, Any]:
111
+ return {
112
+ "status": "error",
113
+ "title": title,
114
+ "summary": summary,
115
+ "outputs": {},
116
+ "data": {},
117
+ "suggestions": [],
118
+ }
@@ -0,0 +1,49 @@
1
+ # Builtin: topic-knowledge-read
2
+ # Read one or more per-topic knowledge files generated by nightly-topic-knowledge.
3
+ # The chat engine surfaces a short topic index on every turn; the model uses this
4
+ # read skill to pull full details for the few topics that look relevant.
5
+
6
+ id: topic-knowledge-read
7
+ name: Topic Knowledge Read
8
+ name_i18n:
9
+ en: Topic Knowledge Read
10
+ ja: トピックナレッジ参照
11
+ description: Read full knowledge files for the given topic ids
12
+ description_i18n:
13
+ en: Read full knowledge files for the given topic ids
14
+ ja: 指定したトピックIDのナレッジ本文を読み出す
15
+ runtime: python
16
+ side_effect: false
17
+
18
+ invocation:
19
+ command: knowledge.read
20
+ phrases:
21
+ - read topic knowledge
22
+ - load topic details
23
+ phrases_i18n:
24
+ en:
25
+ - read topic knowledge
26
+ - load topic details
27
+ ja:
28
+ - トピックナレッジを読んで
29
+ - トピックの詳細を見せて
30
+
31
+ input:
32
+ schema:
33
+ type: object
34
+ additionalProperties: false
35
+ properties:
36
+ topic_ids:
37
+ type: array
38
+ minItems: 1
39
+ maxItems: 8
40
+ items:
41
+ type: string
42
+ minLength: 1
43
+ maxLength: 64
44
+ description: Topic ids to load. See `<topic-knowledge-index>` in the chat context for the available ids.
45
+ description_i18n:
46
+ en: Topic ids to load. See `<topic-knowledge-index>` in the chat context for the available ids.
47
+ ja: 読み込むトピックIDの配列。利用可能なIDは会話コンテキストの `<topic-knowledge-index>` を参照
48
+ required:
49
+ - topic_ids
@@ -10,9 +10,27 @@ export interface BuildModeActionDecision {
10
10
  action: "exit" | "register" | "test" | "continue";
11
11
  reason?: string;
12
12
  }
13
+ export interface ScheduleRequestPayload {
14
+ id: string;
15
+ cron: string;
16
+ skill: string;
17
+ description?: string;
18
+ args?: Record<string, unknown>;
19
+ enabled?: boolean;
20
+ approve?: boolean;
21
+ }
22
+ export type ScheduleRequestDecision = {
23
+ matched: false;
24
+ reason?: string;
25
+ } | {
26
+ matched: true;
27
+ payload: ScheduleRequestPayload;
28
+ reason?: string;
29
+ };
13
30
  interface ClassifyOptions {
14
31
  modelId?: string;
15
32
  }
16
33
  export declare function classifyHandoffApproval(config: AppConfig, userText: string, history: ChatTurn[], pending: PendingHandoff, options?: ClassifyOptions): Promise<HandoffApprovalDecision>;
17
34
  export declare function classifyBuildModeAction(config: AppConfig, userText: string, history: ChatTurn[], build: BuildModeState, options?: ClassifyOptions): Promise<BuildModeActionDecision>;
35
+ export declare function classifyScheduleRequest(config: AppConfig, userText: string, history: ChatTurn[], build: BuildModeState, options?: ClassifyOptions): Promise<ScheduleRequestDecision>;
18
36
  export {};