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,303 @@
|
|
|
1
|
+
"""Common helpers for schedule-* builtin skills.
|
|
2
|
+
|
|
3
|
+
Provides load / dump / cron-validation helpers for the
|
|
4
|
+
~/.agent-sin/schedules.yaml file. PyYAML is used if available;
|
|
5
|
+
otherwise a minimal hand-written parser/serializer (sufficient for the
|
|
6
|
+
restricted schedules schema) is used.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import yaml as _yaml # type: ignore
|
|
17
|
+
HAS_PYYAML = True
|
|
18
|
+
except Exception:
|
|
19
|
+
HAS_PYYAML = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CANONICAL_KEY_ORDER = [
|
|
23
|
+
"id",
|
|
24
|
+
"description",
|
|
25
|
+
"cron",
|
|
26
|
+
"skill",
|
|
27
|
+
"args",
|
|
28
|
+
"enabled",
|
|
29
|
+
"approve",
|
|
30
|
+
"timezone",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def schedules_path(workspace: str) -> str:
|
|
35
|
+
return os.path.join(workspace, "schedules.yaml")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def legacy_schedules_path(workspace: str) -> str:
|
|
39
|
+
return os.path.join(workspace, "schedules", "schedules.yaml")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def migrate_legacy_schedules(workspace: str) -> None:
|
|
43
|
+
target = schedules_path(workspace)
|
|
44
|
+
legacy = legacy_schedules_path(workspace)
|
|
45
|
+
if os.path.exists(target) or not os.path.exists(legacy):
|
|
46
|
+
return
|
|
47
|
+
with open(legacy, "r", encoding="utf-8") as src:
|
|
48
|
+
content = src.read()
|
|
49
|
+
with open(target, "w", encoding="utf-8") as dst:
|
|
50
|
+
dst.write(content)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_schedules(workspace: str) -> List[Dict[str, Any]]:
|
|
54
|
+
migrate_legacy_schedules(workspace)
|
|
55
|
+
path = schedules_path(workspace)
|
|
56
|
+
if not os.path.exists(path):
|
|
57
|
+
return []
|
|
58
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
59
|
+
raw = f.read()
|
|
60
|
+
return _parse_schedules(raw)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_schedules(raw: str) -> List[Dict[str, Any]]:
|
|
64
|
+
if not raw.strip():
|
|
65
|
+
return []
|
|
66
|
+
if HAS_PYYAML:
|
|
67
|
+
data = _yaml.safe_load(raw) or {}
|
|
68
|
+
else:
|
|
69
|
+
data = _minimal_parse(raw)
|
|
70
|
+
items = data.get("schedules") if isinstance(data, dict) else None
|
|
71
|
+
return list(items) if isinstance(items, list) else []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _minimal_parse(raw: str) -> Dict[str, Any]:
|
|
75
|
+
schedules: List[Dict[str, Any]] = []
|
|
76
|
+
current: Dict[str, Any] | None = None
|
|
77
|
+
in_args = False
|
|
78
|
+
in_schedules = False
|
|
79
|
+
for line in raw.splitlines():
|
|
80
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
81
|
+
continue
|
|
82
|
+
if line.startswith("schedules:"):
|
|
83
|
+
in_schedules = True
|
|
84
|
+
rest = line[len("schedules:"):].strip()
|
|
85
|
+
if rest == "[]":
|
|
86
|
+
return {"schedules": []}
|
|
87
|
+
continue
|
|
88
|
+
if not in_schedules:
|
|
89
|
+
continue
|
|
90
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
91
|
+
body = line.strip()
|
|
92
|
+
if body.startswith("- "):
|
|
93
|
+
if current is not None:
|
|
94
|
+
schedules.append(current)
|
|
95
|
+
current = {}
|
|
96
|
+
in_args = False
|
|
97
|
+
body = body[2:].strip()
|
|
98
|
+
if ":" in body:
|
|
99
|
+
k, _, v = body.partition(":")
|
|
100
|
+
current[k.strip()] = _parse_scalar(v.strip())
|
|
101
|
+
continue
|
|
102
|
+
if current is None:
|
|
103
|
+
continue
|
|
104
|
+
if in_args and indent >= 6:
|
|
105
|
+
if ":" in body:
|
|
106
|
+
k, _, v = body.partition(":")
|
|
107
|
+
current.setdefault("args", {})
|
|
108
|
+
current["args"][k.strip()] = _parse_scalar(v.strip())
|
|
109
|
+
continue
|
|
110
|
+
if ":" in body:
|
|
111
|
+
k, _, v = body.partition(":")
|
|
112
|
+
key = k.strip()
|
|
113
|
+
value = v.strip()
|
|
114
|
+
if key == "args" and value in ("", "{}"):
|
|
115
|
+
in_args = value == ""
|
|
116
|
+
current["args"] = {}
|
|
117
|
+
else:
|
|
118
|
+
in_args = False
|
|
119
|
+
current[key] = _parse_scalar(value)
|
|
120
|
+
if current is not None:
|
|
121
|
+
schedules.append(current)
|
|
122
|
+
return {"schedules": schedules}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_scalar(value: str) -> Any:
|
|
126
|
+
if value == "":
|
|
127
|
+
return ""
|
|
128
|
+
if value.startswith('"') and value.endswith('"'):
|
|
129
|
+
try:
|
|
130
|
+
return bytes(value[1:-1], "utf-8").decode("unicode_escape")
|
|
131
|
+
except Exception:
|
|
132
|
+
return value[1:-1]
|
|
133
|
+
if value.startswith("'") and value.endswith("'"):
|
|
134
|
+
return value[1:-1]
|
|
135
|
+
low = value.lower()
|
|
136
|
+
if low == "true":
|
|
137
|
+
return True
|
|
138
|
+
if low == "false":
|
|
139
|
+
return False
|
|
140
|
+
if low in ("null", "~"):
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
if "." in value:
|
|
144
|
+
return float(value)
|
|
145
|
+
return int(value)
|
|
146
|
+
except ValueError:
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def dump_schedules(items: List[Dict[str, Any]]) -> str:
|
|
151
|
+
if not items:
|
|
152
|
+
return "schedules: []\n"
|
|
153
|
+
if HAS_PYYAML:
|
|
154
|
+
cleaned = [_clean_item(item) for item in items]
|
|
155
|
+
return _yaml.safe_dump(
|
|
156
|
+
{"schedules": cleaned},
|
|
157
|
+
allow_unicode=True,
|
|
158
|
+
sort_keys=False,
|
|
159
|
+
default_flow_style=False,
|
|
160
|
+
)
|
|
161
|
+
lines = ["schedules:"]
|
|
162
|
+
for item in items:
|
|
163
|
+
cleaned = _clean_item(item)
|
|
164
|
+
keys = [k for k in CANONICAL_KEY_ORDER if k in cleaned]
|
|
165
|
+
keys.extend(k for k in cleaned.keys() if k not in keys)
|
|
166
|
+
first = True
|
|
167
|
+
for k in keys:
|
|
168
|
+
v = cleaned[k]
|
|
169
|
+
prefix = " - " if first else " "
|
|
170
|
+
first = False
|
|
171
|
+
if k == "args":
|
|
172
|
+
if not v:
|
|
173
|
+
lines.append(f"{prefix}args: {{}}")
|
|
174
|
+
continue
|
|
175
|
+
lines.append(f"{prefix}args:")
|
|
176
|
+
for ak, av in v.items():
|
|
177
|
+
lines.append(f" {ak}: {_yaml_scalar(av)}")
|
|
178
|
+
else:
|
|
179
|
+
lines.append(f"{prefix}{k}: {_yaml_scalar(v)}")
|
|
180
|
+
lines.append("")
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _clean_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
185
|
+
return {k: v for k, v in item.items() if v is not None}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _yaml_scalar(v: Any) -> str:
|
|
189
|
+
if v is None:
|
|
190
|
+
return "null"
|
|
191
|
+
if isinstance(v, bool):
|
|
192
|
+
return "true" if v else "false"
|
|
193
|
+
if isinstance(v, (int, float)):
|
|
194
|
+
return str(v)
|
|
195
|
+
s = str(v)
|
|
196
|
+
if _needs_quote(s):
|
|
197
|
+
escaped = s.replace("\\", "\\\\").replace("\"", "\\\"")
|
|
198
|
+
return f'"{escaped}"'
|
|
199
|
+
return s
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _needs_quote(s: str) -> bool:
|
|
203
|
+
if s == "":
|
|
204
|
+
return True
|
|
205
|
+
if s.strip() != s:
|
|
206
|
+
return True
|
|
207
|
+
if s.lower() in ("null", "true", "false", "yes", "no", "on", "off", "~"):
|
|
208
|
+
return True
|
|
209
|
+
bad_start = set("-?:[]{},#&*!|>'\"%@`")
|
|
210
|
+
if s[0] in bad_start:
|
|
211
|
+
return True
|
|
212
|
+
for ch in s:
|
|
213
|
+
if ch in (":", "#", "\n"):
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def write_schedules_atomic(workspace: str, items: List[Dict[str, Any]]) -> str:
|
|
219
|
+
migrate_legacy_schedules(workspace)
|
|
220
|
+
path = schedules_path(workspace)
|
|
221
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
222
|
+
text = dump_schedules(items)
|
|
223
|
+
# round-trip check before writing
|
|
224
|
+
roundtrip = _parse_schedules(text)
|
|
225
|
+
if len(roundtrip) != len(items):
|
|
226
|
+
raise RuntimeError(
|
|
227
|
+
"Round-trip check failed: parsed entry count "
|
|
228
|
+
f"{len(roundtrip)} != original {len(items)}",
|
|
229
|
+
)
|
|
230
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
231
|
+
prefix=".schedules.", suffix=".yaml.tmp", dir=os.path.dirname(path)
|
|
232
|
+
)
|
|
233
|
+
try:
|
|
234
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
235
|
+
f.write(text)
|
|
236
|
+
os.replace(tmp_path, path)
|
|
237
|
+
except Exception:
|
|
238
|
+
try:
|
|
239
|
+
os.unlink(tmp_path)
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
raise
|
|
243
|
+
return path
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def validate_cron(raw: str) -> None:
|
|
247
|
+
fields = raw.strip().split()
|
|
248
|
+
if len(fields) != 5:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f'Cron must have 5 fields ("min hour dom month dow"): "{raw}"',
|
|
251
|
+
)
|
|
252
|
+
spec = [
|
|
253
|
+
(0, 59, "minute"),
|
|
254
|
+
(0, 23, "hour"),
|
|
255
|
+
(1, 31, "day-of-month"),
|
|
256
|
+
(1, 12, "month"),
|
|
257
|
+
(0, 6, "day-of-week"),
|
|
258
|
+
]
|
|
259
|
+
for field, (mn, mx, label) in zip(fields, spec):
|
|
260
|
+
_validate_cron_field(field, mn, mx, label)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _validate_cron_field(field: str, mn: int, mx: int, label: str) -> None:
|
|
264
|
+
for part in field.split(","):
|
|
265
|
+
seg = part.strip()
|
|
266
|
+
if not seg:
|
|
267
|
+
raise ValueError(f'Empty segment in {label} field: "{field}"')
|
|
268
|
+
body = seg
|
|
269
|
+
step = 1
|
|
270
|
+
if "/" in body:
|
|
271
|
+
body, _, step_raw = body.partition("/")
|
|
272
|
+
try:
|
|
273
|
+
step = int(step_raw)
|
|
274
|
+
except ValueError as exc:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
f'Invalid step "{step_raw}" in {label} field: "{field}"',
|
|
277
|
+
) from exc
|
|
278
|
+
if step <= 0:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f'Invalid step "{step_raw}" in {label} field: "{field}"',
|
|
281
|
+
)
|
|
282
|
+
if body == "" or body == "*":
|
|
283
|
+
continue
|
|
284
|
+
if "-" in body:
|
|
285
|
+
a, _, b = body.partition("-")
|
|
286
|
+
try:
|
|
287
|
+
fr = int(a)
|
|
288
|
+
to = int(b)
|
|
289
|
+
except ValueError as exc:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f'Invalid value "{body}" in {label} field: "{field}"',
|
|
292
|
+
) from exc
|
|
293
|
+
else:
|
|
294
|
+
try:
|
|
295
|
+
fr = to = int(body)
|
|
296
|
+
except ValueError as exc:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f'Invalid value "{body}" in {label} field: "{field}"',
|
|
299
|
+
) from exc
|
|
300
|
+
if fr < mn or to > mx or fr > to:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f'{label} value out of range ({fr}-{to}); allowed {mn}-{mx} for "{field}"',
|
|
303
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Builtin: schedule-add
|
|
2
|
+
|
|
3
|
+
スケジュールを ~/.agent-sin/schedules.yaml に1件追加する。
|
|
4
|
+
同じ id + 同じ内容で再度呼ばれた場合は冪等に登録済みを返す。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
from i18n import localizer # noqa: E402
|
|
15
|
+
from _schedules_lib import ( # noqa: E402
|
|
16
|
+
load_schedules,
|
|
17
|
+
validate_cron,
|
|
18
|
+
write_schedules_atomic,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_COMPARABLE_KEYS = ("cron", "skill", "description", "args", "approve")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def run(ctx, input):
|
|
26
|
+
loc = localizer(input)
|
|
27
|
+
args = input.get("args", {}) or {}
|
|
28
|
+
workspace = input.get("sources", {}).get("workspace", "")
|
|
29
|
+
if not workspace:
|
|
30
|
+
return _err(loc.t("Workspace unavailable", "ワークスペース不明"), loc.t("The workspace path is unavailable.", "workspace パスが取得できません"))
|
|
31
|
+
|
|
32
|
+
schedule_id = str(args.get("id", "")).strip()
|
|
33
|
+
cron = str(args.get("cron", "")).strip()
|
|
34
|
+
skill = str(args.get("skill", "")).strip()
|
|
35
|
+
if not schedule_id or not cron or not skill:
|
|
36
|
+
return _err(loc.t("Missing input", "入力不足"), loc.t("id / cron / skill are required.", "id / cron / skill は必須です"))
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
validate_cron(cron)
|
|
40
|
+
except ValueError as e:
|
|
41
|
+
return _err(loc.t("Invalid cron", "cron不正"), str(e))
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
entries = load_schedules(workspace)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return _err(loc.t("Load failed", "読込失敗"), loc.t(f"Could not read schedules.yaml: {e}", f"schedules.yaml を読めませんでした: {e}"))
|
|
47
|
+
|
|
48
|
+
entry = {"id": schedule_id, "cron": cron, "skill": skill}
|
|
49
|
+
description = args.get("description")
|
|
50
|
+
if description:
|
|
51
|
+
entry["description"] = str(description)
|
|
52
|
+
extra_args = args.get("args")
|
|
53
|
+
if isinstance(extra_args, dict) and extra_args:
|
|
54
|
+
entry["args"] = extra_args
|
|
55
|
+
if "enabled" in args:
|
|
56
|
+
entry["enabled"] = bool(args["enabled"])
|
|
57
|
+
if "approve" in args:
|
|
58
|
+
entry["approve"] = bool(args["approve"])
|
|
59
|
+
|
|
60
|
+
existing = next(
|
|
61
|
+
(item for item in entries if isinstance(item, dict) and item.get("id") == schedule_id),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
if existing is not None:
|
|
65
|
+
if _same_schedule(existing, entry):
|
|
66
|
+
ctx.log.info(f"schedule-add: id={schedule_id} already exists (idempotent)")
|
|
67
|
+
disabled_note = " (disabled)" if _enabled_value(existing) is False else ""
|
|
68
|
+
return {
|
|
69
|
+
"status": "ok",
|
|
70
|
+
"title": loc.t("Already registered", "登録済み"),
|
|
71
|
+
"summary": loc.t(f"Already registered: {schedule_id}: {cron} -> {skill}{disabled_note}", f"登録済みです: {schedule_id}: {cron} → {skill}{disabled_note}"),
|
|
72
|
+
"outputs": {},
|
|
73
|
+
"data": {
|
|
74
|
+
"entry": existing,
|
|
75
|
+
"total": len(entries),
|
|
76
|
+
"path": _schedules_path(workspace),
|
|
77
|
+
"already_registered": True,
|
|
78
|
+
},
|
|
79
|
+
"suggestions": [],
|
|
80
|
+
}
|
|
81
|
+
return _err(
|
|
82
|
+
loc.t("Duplicate ID", "ID重複"),
|
|
83
|
+
loc.t(f'Schedule ID "{schedule_id}" is already registered with different settings. Remove it first or use another ID.', f'スケジュールID "{schedule_id}" は別の内容で既に登録されています。先に削除してから追加するか、別のIDを使ってください'),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
entries.append(entry)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
path = write_schedules_atomic(workspace, entries)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return _err(loc.t("Save failed", "保存失敗"), loc.t(f"Failed to write schedules.yaml: {e}", f"schedules.yaml への書き込みに失敗しました: {e}"))
|
|
92
|
+
|
|
93
|
+
ctx.log.info(f"schedule-add: id={schedule_id} cron={cron} skill={skill}")
|
|
94
|
+
|
|
95
|
+
disabled_note = " (disabled)" if entry.get("enabled") is False else ""
|
|
96
|
+
return {
|
|
97
|
+
"status": "ok",
|
|
98
|
+
"title": loc.t("Registered", "登録"),
|
|
99
|
+
"summary": loc.t(f"Registered: {schedule_id}: {cron} -> {skill}{disabled_note}", f"登録しました: {schedule_id}: {cron} → {skill}{disabled_note}"),
|
|
100
|
+
"outputs": {},
|
|
101
|
+
"data": {"entry": entry, "total": len(entries), "path": path},
|
|
102
|
+
"suggestions": [],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _same_schedule(existing: dict, candidate: dict) -> bool:
|
|
107
|
+
for key in _COMPARABLE_KEYS:
|
|
108
|
+
if existing.get(key) != candidate.get(key):
|
|
109
|
+
return False
|
|
110
|
+
# enabled defaults to True when unspecified; treat missing as True so a
|
|
111
|
+
# re-add that omits "enabled" still counts as identical to an entry that
|
|
112
|
+
# is enabled by default.
|
|
113
|
+
if _enabled_value(existing) != _enabled_value(candidate):
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _enabled_value(entry: dict) -> bool:
|
|
119
|
+
value = entry.get("enabled")
|
|
120
|
+
if value is None:
|
|
121
|
+
return True
|
|
122
|
+
return bool(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _schedules_path(workspace: str) -> str:
|
|
126
|
+
return os.path.join(workspace, "schedules.yaml")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _err(title, summary):
|
|
130
|
+
return {
|
|
131
|
+
"status": "error",
|
|
132
|
+
"title": title,
|
|
133
|
+
"summary": summary,
|
|
134
|
+
"outputs": {},
|
|
135
|
+
"data": {},
|
|
136
|
+
"suggestions": [],
|
|
137
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Builtin: schedule-add
|
|
2
|
+
# ~/.agent-sin/schedules.yaml にスケジュールを1件追加する。
|
|
3
|
+
|
|
4
|
+
id: schedule-add
|
|
5
|
+
name: Schedule Add
|
|
6
|
+
name_i18n:
|
|
7
|
+
en: Schedule Add
|
|
8
|
+
ja: スケジュール追加
|
|
9
|
+
description: 定期実行(cron)スケジュールを1件追加する
|
|
10
|
+
description_i18n:
|
|
11
|
+
en: Add one recurring cron schedule
|
|
12
|
+
ja: 定期実行(cron)スケジュールを1件追加する
|
|
13
|
+
runtime: python
|
|
14
|
+
output_mode: raw
|
|
15
|
+
side_effect: true
|
|
16
|
+
|
|
17
|
+
invocation:
|
|
18
|
+
command: schedule.add
|
|
19
|
+
phrases:
|
|
20
|
+
- スケジュール追加
|
|
21
|
+
- 定期実行を追加
|
|
22
|
+
- cronを追加
|
|
23
|
+
- 毎日X時に動かして
|
|
24
|
+
- 毎時X分に動かして
|
|
25
|
+
phrases_i18n:
|
|
26
|
+
en:
|
|
27
|
+
- add schedule
|
|
28
|
+
- add recurring task
|
|
29
|
+
- add cron
|
|
30
|
+
- run this every day
|
|
31
|
+
- run this every hour
|
|
32
|
+
ja:
|
|
33
|
+
- スケジュール追加
|
|
34
|
+
- 定期実行を追加
|
|
35
|
+
- cronを追加
|
|
36
|
+
- 毎日X時に動かして
|
|
37
|
+
- 毎時X分に動かして
|
|
38
|
+
|
|
39
|
+
input:
|
|
40
|
+
schema:
|
|
41
|
+
type: object
|
|
42
|
+
additionalProperties: false
|
|
43
|
+
properties:
|
|
44
|
+
id:
|
|
45
|
+
type: string
|
|
46
|
+
minLength: 1
|
|
47
|
+
pattern: "^[A-Za-z0-9_-]+$"
|
|
48
|
+
description: スケジュールの一意ID
|
|
49
|
+
description_i18n:
|
|
50
|
+
en: Unique schedule ID
|
|
51
|
+
ja: スケジュールの一意ID
|
|
52
|
+
cron:
|
|
53
|
+
type: string
|
|
54
|
+
minLength: 1
|
|
55
|
+
description: 5フィールドのcron式 "min hour dom month dow"
|
|
56
|
+
description_i18n:
|
|
57
|
+
en: 5-field cron expression, "min hour dom month dow"
|
|
58
|
+
ja: 5フィールドのcron式 "min hour dom month dow"
|
|
59
|
+
skill:
|
|
60
|
+
type: string
|
|
61
|
+
minLength: 1
|
|
62
|
+
description: 実行するスキルID
|
|
63
|
+
description_i18n:
|
|
64
|
+
en: Skill ID to run
|
|
65
|
+
ja: 実行するスキルID
|
|
66
|
+
args:
|
|
67
|
+
type: object
|
|
68
|
+
additionalProperties: true
|
|
69
|
+
description: スキルに渡す引数(任意)
|
|
70
|
+
description_i18n:
|
|
71
|
+
en: Optional arguments passed to the skill
|
|
72
|
+
ja: スキルに渡す引数(任意)
|
|
73
|
+
description:
|
|
74
|
+
type: string
|
|
75
|
+
description: スケジュールの説明文(任意)
|
|
76
|
+
description_i18n:
|
|
77
|
+
en: Optional schedule description
|
|
78
|
+
ja: スケジュールの説明文(任意)
|
|
79
|
+
enabled:
|
|
80
|
+
type: boolean
|
|
81
|
+
description: 有効/無効 (default true)
|
|
82
|
+
description_i18n:
|
|
83
|
+
en: Enabled or disabled (default true)
|
|
84
|
+
ja: 有効/無効 (default true)
|
|
85
|
+
approve:
|
|
86
|
+
type: boolean
|
|
87
|
+
description: approve required フラグ (default false)
|
|
88
|
+
description_i18n:
|
|
89
|
+
en: Approval required flag (default false)
|
|
90
|
+
ja: approve required フラグ (default false)
|
|
91
|
+
required:
|
|
92
|
+
- id
|
|
93
|
+
- cron
|
|
94
|
+
- skill
|