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,313 @@
|
|
|
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
|
+
if os.path.exists(target):
|
|
45
|
+
return
|
|
46
|
+
legacy = legacy_schedules_path(workspace)
|
|
47
|
+
if not os.path.exists(legacy):
|
|
48
|
+
return
|
|
49
|
+
with open(legacy, "r", encoding="utf-8") as src:
|
|
50
|
+
content = src.read()
|
|
51
|
+
with open(target, "w", encoding="utf-8") as dst:
|
|
52
|
+
dst.write(content)
|
|
53
|
+
try:
|
|
54
|
+
os.remove(legacy)
|
|
55
|
+
os.rmdir(os.path.dirname(legacy))
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_schedules(workspace: str) -> List[Dict[str, Any]]:
|
|
61
|
+
path = schedules_path(workspace)
|
|
62
|
+
if not os.path.exists(path):
|
|
63
|
+
legacy = legacy_schedules_path(workspace)
|
|
64
|
+
if os.path.exists(legacy):
|
|
65
|
+
path = legacy
|
|
66
|
+
else:
|
|
67
|
+
return []
|
|
68
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
69
|
+
raw = f.read()
|
|
70
|
+
return _parse_schedules(raw)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_schedules(raw: str) -> List[Dict[str, Any]]:
|
|
74
|
+
if not raw.strip():
|
|
75
|
+
return []
|
|
76
|
+
if HAS_PYYAML:
|
|
77
|
+
data = _yaml.safe_load(raw) or {}
|
|
78
|
+
else:
|
|
79
|
+
data = _minimal_parse(raw)
|
|
80
|
+
items = data.get("schedules") if isinstance(data, dict) else None
|
|
81
|
+
return list(items) if isinstance(items, list) else []
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _minimal_parse(raw: str) -> Dict[str, Any]:
|
|
85
|
+
schedules: List[Dict[str, Any]] = []
|
|
86
|
+
current: Dict[str, Any] | None = None
|
|
87
|
+
in_args = False
|
|
88
|
+
in_schedules = False
|
|
89
|
+
for line in raw.splitlines():
|
|
90
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
91
|
+
continue
|
|
92
|
+
if line.startswith("schedules:"):
|
|
93
|
+
in_schedules = True
|
|
94
|
+
rest = line[len("schedules:"):].strip()
|
|
95
|
+
if rest == "[]":
|
|
96
|
+
return {"schedules": []}
|
|
97
|
+
continue
|
|
98
|
+
if not in_schedules:
|
|
99
|
+
continue
|
|
100
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
101
|
+
body = line.strip()
|
|
102
|
+
if body.startswith("- "):
|
|
103
|
+
if current is not None:
|
|
104
|
+
schedules.append(current)
|
|
105
|
+
current = {}
|
|
106
|
+
in_args = False
|
|
107
|
+
body = body[2:].strip()
|
|
108
|
+
if ":" in body:
|
|
109
|
+
k, _, v = body.partition(":")
|
|
110
|
+
current[k.strip()] = _parse_scalar(v.strip())
|
|
111
|
+
continue
|
|
112
|
+
if current is None:
|
|
113
|
+
continue
|
|
114
|
+
if in_args and indent >= 6:
|
|
115
|
+
if ":" in body:
|
|
116
|
+
k, _, v = body.partition(":")
|
|
117
|
+
current.setdefault("args", {})
|
|
118
|
+
current["args"][k.strip()] = _parse_scalar(v.strip())
|
|
119
|
+
continue
|
|
120
|
+
if ":" in body:
|
|
121
|
+
k, _, v = body.partition(":")
|
|
122
|
+
key = k.strip()
|
|
123
|
+
value = v.strip()
|
|
124
|
+
if key == "args" and value in ("", "{}"):
|
|
125
|
+
in_args = value == ""
|
|
126
|
+
current["args"] = {}
|
|
127
|
+
else:
|
|
128
|
+
in_args = False
|
|
129
|
+
current[key] = _parse_scalar(value)
|
|
130
|
+
if current is not None:
|
|
131
|
+
schedules.append(current)
|
|
132
|
+
return {"schedules": schedules}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _parse_scalar(value: str) -> Any:
|
|
136
|
+
if value == "":
|
|
137
|
+
return ""
|
|
138
|
+
if value.startswith('"') and value.endswith('"'):
|
|
139
|
+
try:
|
|
140
|
+
return bytes(value[1:-1], "utf-8").decode("unicode_escape")
|
|
141
|
+
except Exception:
|
|
142
|
+
return value[1:-1]
|
|
143
|
+
if value.startswith("'") and value.endswith("'"):
|
|
144
|
+
return value[1:-1]
|
|
145
|
+
low = value.lower()
|
|
146
|
+
if low == "true":
|
|
147
|
+
return True
|
|
148
|
+
if low == "false":
|
|
149
|
+
return False
|
|
150
|
+
if low in ("null", "~"):
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
if "." in value:
|
|
154
|
+
return float(value)
|
|
155
|
+
return int(value)
|
|
156
|
+
except ValueError:
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def dump_schedules(items: List[Dict[str, Any]]) -> str:
|
|
161
|
+
if not items:
|
|
162
|
+
return "schedules: []\n"
|
|
163
|
+
if HAS_PYYAML:
|
|
164
|
+
cleaned = [_clean_item(item) for item in items]
|
|
165
|
+
return _yaml.safe_dump(
|
|
166
|
+
{"schedules": cleaned},
|
|
167
|
+
allow_unicode=True,
|
|
168
|
+
sort_keys=False,
|
|
169
|
+
default_flow_style=False,
|
|
170
|
+
)
|
|
171
|
+
lines = ["schedules:"]
|
|
172
|
+
for item in items:
|
|
173
|
+
cleaned = _clean_item(item)
|
|
174
|
+
keys = [k for k in CANONICAL_KEY_ORDER if k in cleaned]
|
|
175
|
+
keys.extend(k for k in cleaned.keys() if k not in keys)
|
|
176
|
+
first = True
|
|
177
|
+
for k in keys:
|
|
178
|
+
v = cleaned[k]
|
|
179
|
+
prefix = " - " if first else " "
|
|
180
|
+
first = False
|
|
181
|
+
if k == "args":
|
|
182
|
+
if not v:
|
|
183
|
+
lines.append(f"{prefix}args: {{}}")
|
|
184
|
+
continue
|
|
185
|
+
lines.append(f"{prefix}args:")
|
|
186
|
+
for ak, av in v.items():
|
|
187
|
+
lines.append(f" {ak}: {_yaml_scalar(av)}")
|
|
188
|
+
else:
|
|
189
|
+
lines.append(f"{prefix}{k}: {_yaml_scalar(v)}")
|
|
190
|
+
lines.append("")
|
|
191
|
+
return "\n".join(lines)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _clean_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
195
|
+
return {k: v for k, v in item.items() if v is not None}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _yaml_scalar(v: Any) -> str:
|
|
199
|
+
if v is None:
|
|
200
|
+
return "null"
|
|
201
|
+
if isinstance(v, bool):
|
|
202
|
+
return "true" if v else "false"
|
|
203
|
+
if isinstance(v, (int, float)):
|
|
204
|
+
return str(v)
|
|
205
|
+
s = str(v)
|
|
206
|
+
if _needs_quote(s):
|
|
207
|
+
escaped = s.replace("\\", "\\\\").replace("\"", "\\\"")
|
|
208
|
+
return f'"{escaped}"'
|
|
209
|
+
return s
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _needs_quote(s: str) -> bool:
|
|
213
|
+
if s == "":
|
|
214
|
+
return True
|
|
215
|
+
if s.strip() != s:
|
|
216
|
+
return True
|
|
217
|
+
if s.lower() in ("null", "true", "false", "yes", "no", "on", "off", "~"):
|
|
218
|
+
return True
|
|
219
|
+
bad_start = set("-?:[]{},#&*!|>'\"%@`")
|
|
220
|
+
if s[0] in bad_start:
|
|
221
|
+
return True
|
|
222
|
+
for ch in s:
|
|
223
|
+
if ch in (":", "#", "\n"):
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def write_schedules_atomic(workspace: str, items: List[Dict[str, Any]]) -> str:
|
|
229
|
+
_migrate_legacy_schedules(workspace)
|
|
230
|
+
path = schedules_path(workspace)
|
|
231
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
232
|
+
text = dump_schedules(items)
|
|
233
|
+
# round-trip check before writing
|
|
234
|
+
roundtrip = _parse_schedules(text)
|
|
235
|
+
if len(roundtrip) != len(items):
|
|
236
|
+
raise RuntimeError(
|
|
237
|
+
"Round-trip check failed: parsed entry count "
|
|
238
|
+
f"{len(roundtrip)} != original {len(items)}",
|
|
239
|
+
)
|
|
240
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
241
|
+
prefix=".schedules.", suffix=".yaml.tmp", dir=os.path.dirname(path)
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
245
|
+
f.write(text)
|
|
246
|
+
os.replace(tmp_path, path)
|
|
247
|
+
except Exception:
|
|
248
|
+
try:
|
|
249
|
+
os.unlink(tmp_path)
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
raise
|
|
253
|
+
return path
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def validate_cron(raw: str) -> None:
|
|
257
|
+
fields = raw.strip().split()
|
|
258
|
+
if len(fields) != 5:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f'Cron must have 5 fields ("min hour dom month dow"): "{raw}"',
|
|
261
|
+
)
|
|
262
|
+
spec = [
|
|
263
|
+
(0, 59, "minute"),
|
|
264
|
+
(0, 23, "hour"),
|
|
265
|
+
(1, 31, "day-of-month"),
|
|
266
|
+
(1, 12, "month"),
|
|
267
|
+
(0, 6, "day-of-week"),
|
|
268
|
+
]
|
|
269
|
+
for field, (mn, mx, label) in zip(fields, spec):
|
|
270
|
+
_validate_cron_field(field, mn, mx, label)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _validate_cron_field(field: str, mn: int, mx: int, label: str) -> None:
|
|
274
|
+
for part in field.split(","):
|
|
275
|
+
seg = part.strip()
|
|
276
|
+
if not seg:
|
|
277
|
+
raise ValueError(f'Empty segment in {label} field: "{field}"')
|
|
278
|
+
body = seg
|
|
279
|
+
step = 1
|
|
280
|
+
if "/" in body:
|
|
281
|
+
body, _, step_raw = body.partition("/")
|
|
282
|
+
try:
|
|
283
|
+
step = int(step_raw)
|
|
284
|
+
except ValueError as exc:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f'Invalid step "{step_raw}" in {label} field: "{field}"',
|
|
287
|
+
) from exc
|
|
288
|
+
if step <= 0:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f'Invalid step "{step_raw}" in {label} field: "{field}"',
|
|
291
|
+
)
|
|
292
|
+
if body == "" or body == "*":
|
|
293
|
+
continue
|
|
294
|
+
if "-" in body:
|
|
295
|
+
a, _, b = body.partition("-")
|
|
296
|
+
try:
|
|
297
|
+
fr = int(a)
|
|
298
|
+
to = int(b)
|
|
299
|
+
except ValueError as exc:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f'Invalid value "{body}" in {label} field: "{field}"',
|
|
302
|
+
) from exc
|
|
303
|
+
else:
|
|
304
|
+
try:
|
|
305
|
+
fr = to = int(body)
|
|
306
|
+
except ValueError as exc:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
f'Invalid value "{body}" in {label} field: "{field}"',
|
|
309
|
+
) from exc
|
|
310
|
+
if fr < mn or to > mx or fr > to:
|
|
311
|
+
raise ValueError(
|
|
312
|
+
f'{label} value out of range ({fr}-{to}); allowed {mn}-{mx} for "{field}"',
|
|
313
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Common helpers for skills-disable / skills-enable builtin skills.
|
|
2
|
+
|
|
3
|
+
Reads/writes ~/.agent-sin/skill-settings.yaml in the same format used by
|
|
4
|
+
src/core/skill-settings.ts. PyYAML is used if available; otherwise a minimal
|
|
5
|
+
serializer for the single-key `disabled: [...]` document is used.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from typing import Any, Dict, List, Tuple
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import yaml as _yaml # type: ignore
|
|
16
|
+
HAS_PYYAML = True
|
|
17
|
+
except Exception:
|
|
18
|
+
HAS_PYYAML = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def skill_settings_path(workspace: str) -> str:
|
|
22
|
+
return os.path.join(workspace, "skill-settings.yaml")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_skill_settings(workspace: str) -> Dict[str, Any]:
|
|
26
|
+
path = skill_settings_path(workspace)
|
|
27
|
+
if not os.path.exists(path):
|
|
28
|
+
return {"disabled": []}
|
|
29
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
30
|
+
raw = f.read()
|
|
31
|
+
if not raw.strip():
|
|
32
|
+
return {"disabled": []}
|
|
33
|
+
if HAS_PYYAML:
|
|
34
|
+
data = _yaml.safe_load(raw) or {}
|
|
35
|
+
else:
|
|
36
|
+
data = _minimal_parse(raw)
|
|
37
|
+
disabled = data.get("disabled") if isinstance(data, dict) else None
|
|
38
|
+
items: List[str] = []
|
|
39
|
+
if isinstance(disabled, list):
|
|
40
|
+
for entry in disabled:
|
|
41
|
+
if isinstance(entry, str) and entry.strip():
|
|
42
|
+
items.append(entry.strip())
|
|
43
|
+
return {"disabled": items}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dump_skill_settings(settings: Dict[str, Any]) -> str:
|
|
47
|
+
disabled = sorted(set(settings.get("disabled") or []))
|
|
48
|
+
if not disabled:
|
|
49
|
+
return "disabled: []\n"
|
|
50
|
+
if HAS_PYYAML:
|
|
51
|
+
return _yaml.safe_dump(
|
|
52
|
+
{"disabled": disabled},
|
|
53
|
+
allow_unicode=True,
|
|
54
|
+
sort_keys=False,
|
|
55
|
+
default_flow_style=False,
|
|
56
|
+
)
|
|
57
|
+
lines = ["disabled:"]
|
|
58
|
+
for entry in disabled:
|
|
59
|
+
lines.append(f" - {_yaml_scalar(entry)}")
|
|
60
|
+
lines.append("")
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def save_skill_settings(workspace: str, settings: Dict[str, Any]) -> str:
|
|
65
|
+
path = skill_settings_path(workspace)
|
|
66
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
67
|
+
text = dump_skill_settings(settings)
|
|
68
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
69
|
+
prefix=".skill-settings.", suffix=".yaml.tmp", dir=os.path.dirname(path) or "."
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
73
|
+
f.write(text)
|
|
74
|
+
os.replace(tmp_path, path)
|
|
75
|
+
except Exception:
|
|
76
|
+
try:
|
|
77
|
+
os.unlink(tmp_path)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
raise
|
|
81
|
+
return path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_skill_enabled(
|
|
85
|
+
workspace: str, skill_id: str, enabled: bool
|
|
86
|
+
) -> Tuple[bool, Dict[str, Any]]:
|
|
87
|
+
settings = load_skill_settings(workspace)
|
|
88
|
+
disabled: List[str] = list(settings.get("disabled") or [])
|
|
89
|
+
was_disabled = skill_id in disabled
|
|
90
|
+
if enabled:
|
|
91
|
+
if not was_disabled:
|
|
92
|
+
return False, settings
|
|
93
|
+
disabled = [d for d in disabled if d != skill_id]
|
|
94
|
+
else:
|
|
95
|
+
if was_disabled:
|
|
96
|
+
return False, settings
|
|
97
|
+
disabled.append(skill_id)
|
|
98
|
+
settings["disabled"] = disabled
|
|
99
|
+
save_skill_settings(workspace, settings)
|
|
100
|
+
return True, settings
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _minimal_parse(raw: str) -> Dict[str, Any]:
|
|
104
|
+
disabled: List[str] = []
|
|
105
|
+
in_disabled = False
|
|
106
|
+
for line in raw.splitlines():
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if not stripped or stripped.startswith("#"):
|
|
109
|
+
continue
|
|
110
|
+
if line.startswith("disabled:"):
|
|
111
|
+
rest = line[len("disabled:"):].strip()
|
|
112
|
+
if rest == "[]" or rest == "":
|
|
113
|
+
in_disabled = rest == ""
|
|
114
|
+
continue
|
|
115
|
+
if rest.startswith("[") and rest.endswith("]"):
|
|
116
|
+
body = rest[1:-1]
|
|
117
|
+
for token in body.split(","):
|
|
118
|
+
item = token.strip().strip('"').strip("'")
|
|
119
|
+
if item:
|
|
120
|
+
disabled.append(item)
|
|
121
|
+
in_disabled = False
|
|
122
|
+
continue
|
|
123
|
+
if in_disabled:
|
|
124
|
+
if stripped.startswith("- "):
|
|
125
|
+
value = stripped[2:].strip().strip('"').strip("'")
|
|
126
|
+
if value:
|
|
127
|
+
disabled.append(value)
|
|
128
|
+
else:
|
|
129
|
+
in_disabled = False
|
|
130
|
+
return {"disabled": disabled}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _yaml_scalar(value: str) -> str:
|
|
134
|
+
if _needs_quote(value):
|
|
135
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
136
|
+
return f'"{escaped}"'
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _needs_quote(value: str) -> bool:
|
|
141
|
+
if value == "":
|
|
142
|
+
return True
|
|
143
|
+
if value.strip() != value:
|
|
144
|
+
return True
|
|
145
|
+
if value.lower() in ("null", "true", "false", "yes", "no", "on", "off", "~"):
|
|
146
|
+
return True
|
|
147
|
+
bad_start = set("-?:[]{},#&*!|>'\"%@`")
|
|
148
|
+
if value[0] in bad_start:
|
|
149
|
+
return True
|
|
150
|
+
for ch in value:
|
|
151
|
+
if ch in (":", "#", "\n"):
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Localizer:
|
|
7
|
+
def __init__(self, locale: str):
|
|
8
|
+
self.locale = locale
|
|
9
|
+
|
|
10
|
+
def t(self, en: str, ja: str) -> str:
|
|
11
|
+
return ja if self.locale == "ja" else en
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def localizer(input_payload: dict | None = None) -> Localizer:
|
|
15
|
+
return Localizer(detect_locale(input_payload))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect_locale(input_payload: dict | None = None) -> str:
|
|
19
|
+
sources = (input_payload or {}).get("sources") or {}
|
|
20
|
+
explicit = str(sources.get("locale") or os.environ.get("AGENT_SIN_LOCALE") or "").strip().lower()
|
|
21
|
+
if explicit in {"en", "ja"}:
|
|
22
|
+
return explicit
|
|
23
|
+
lang = str(os.environ.get("LC_ALL") or os.environ.get("LANG") or "").strip().lower()
|
|
24
|
+
if lang:
|
|
25
|
+
return "ja" if lang.startswith("ja") else "en"
|
|
26
|
+
return "en"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Builtin: memo-delete
|
|
2
|
+
|
|
3
|
+
memo-save が書き込むデイリーノートからメモを1件削除する。
|
|
4
|
+
バレット行 + 続く 2 スペースインデントの継続行をまとめて取り除く。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
|
|
16
|
+
from i18n import localizer # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
20
|
+
_MEMO_LINE_RE = re.compile(r"^-\s+")
|
|
21
|
+
_CONTINUATION_RE = re.compile(r"^ \S")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def run(ctx, input):
|
|
25
|
+
loc = localizer(input)
|
|
26
|
+
args = input.get("args", {}) or {}
|
|
27
|
+
workspace = input.get("sources", {}).get("workspace", "")
|
|
28
|
+
notes_dir = input.get("sources", {}).get("notes_dir", "")
|
|
29
|
+
if not workspace or not notes_dir:
|
|
30
|
+
return _err(loc.t("Workspace unavailable", "ワークスペース不明"), loc.t("workspace / notes_dir is unavailable.", "workspace / notes_dir が取得できません"))
|
|
31
|
+
|
|
32
|
+
date_str = str(args.get("date", "")).strip()
|
|
33
|
+
if not date_str:
|
|
34
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
35
|
+
if not _DATE_RE.match(date_str):
|
|
36
|
+
return _err(loc.t("Invalid date", "日付不正"), loc.t("Use YYYY-MM-DD for date.", "date は YYYY-MM-DD 形式で指定してください"))
|
|
37
|
+
|
|
38
|
+
match = args.get("match")
|
|
39
|
+
match = str(match).strip() if match else None
|
|
40
|
+
index = args.get("index")
|
|
41
|
+
if index is not None and not isinstance(index, int):
|
|
42
|
+
return _err(loc.t("Invalid index", "index不正"), loc.t("index must be a positive integer.", "index は正の整数で指定してください"))
|
|
43
|
+
if not match and index is None:
|
|
44
|
+
return _err(loc.t("Cannot identify memo", "特定不可"), loc.t("Specify either match or index.", "match か index のどちらかを指定してください"))
|
|
45
|
+
|
|
46
|
+
year, month, _ = date_str.split("-")
|
|
47
|
+
path = os.path.join(notes_dir, year, month, f"{date_str}.md")
|
|
48
|
+
if not os.path.exists(path):
|
|
49
|
+
return _err(loc.t("File not found", "ファイルなし"), loc.t(f"No memo file exists for {date_str}.", f"{date_str} のメモファイルがありません"))
|
|
50
|
+
|
|
51
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
52
|
+
raw = f.read()
|
|
53
|
+
lines = raw.splitlines(keepends=False)
|
|
54
|
+
|
|
55
|
+
memo_ranges = _collect_memo_ranges(lines)
|
|
56
|
+
if not memo_ranges:
|
|
57
|
+
return _err(loc.t("No memos", "メモなし"), loc.t(f"No memo lines can be removed for {date_str}.", f"{date_str} に削除対象のメモ行がありません"))
|
|
58
|
+
|
|
59
|
+
target_range = None
|
|
60
|
+
if index is not None:
|
|
61
|
+
if index < 1 or index > len(memo_ranges):
|
|
62
|
+
return _err(loc.t("Invalid index", "index不正"), loc.t(f"index {index} is out of range ({len(memo_ranges)} memos).", f"index {index} は範囲外です (メモ {len(memo_ranges)} 件)"))
|
|
63
|
+
target_range = memo_ranges[index - 1]
|
|
64
|
+
else:
|
|
65
|
+
candidates = [
|
|
66
|
+
r for r in memo_ranges
|
|
67
|
+
if any(match in lines[i] for i in range(r[0], r[1]))
|
|
68
|
+
]
|
|
69
|
+
if not candidates:
|
|
70
|
+
return _err(loc.t("Not found", "見つかりません"), loc.t(f'No memo matches "{match}".', f'"{match}" に一致するメモがありません'))
|
|
71
|
+
if len(candidates) > 1:
|
|
72
|
+
preview = "\n".join(
|
|
73
|
+
f" {idx + 1}. {lines[start]}" for idx, (start, _end) in enumerate(candidates[:5])
|
|
74
|
+
)
|
|
75
|
+
return _err(
|
|
76
|
+
loc.t("Ambiguous match", "曖昧です"),
|
|
77
|
+
loc.t(f"{len(candidates)} memos matched. Specify index:\n{preview}", f"{len(candidates)} 件一致しました。index で指定してください:\n{preview}"),
|
|
78
|
+
)
|
|
79
|
+
target_range = candidates[0]
|
|
80
|
+
|
|
81
|
+
start, end = target_range
|
|
82
|
+
removed_lines = lines[start:end]
|
|
83
|
+
new_lines = lines[:start] + lines[end:]
|
|
84
|
+
new_content = "\n".join(new_lines)
|
|
85
|
+
if raw.endswith("\n") and not new_content.endswith("\n"):
|
|
86
|
+
new_content += "\n"
|
|
87
|
+
if not raw.endswith("\n") and new_content.endswith("\n"):
|
|
88
|
+
new_content = new_content.rstrip("\n")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
_write_atomic(path, new_content)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return _err(loc.t("Save failed", "保存失敗"), loc.t(f"Failed to write {path}: {e}", f"{path} への書き込みに失敗しました: {e}"))
|
|
94
|
+
|
|
95
|
+
ctx.log.info(f"memo-delete: {date_str} {len(removed_lines)} line(s) removed")
|
|
96
|
+
|
|
97
|
+
preview = removed_lines[0].lstrip("- ").strip()
|
|
98
|
+
if len(preview) > 60:
|
|
99
|
+
preview = preview[:57] + "..."
|
|
100
|
+
remaining = len(memo_ranges) - 1
|
|
101
|
+
return {
|
|
102
|
+
"status": "ok",
|
|
103
|
+
"title": loc.t("Deleted", "削除"),
|
|
104
|
+
"summary": loc.t(f"Deleted memo from {date_str}: {preview}", f"{date_str} のメモを削除しました: {preview}"),
|
|
105
|
+
"outputs": {},
|
|
106
|
+
"data": {
|
|
107
|
+
"date": date_str,
|
|
108
|
+
"removed_lines": removed_lines,
|
|
109
|
+
"remaining_memos": remaining,
|
|
110
|
+
"path": path,
|
|
111
|
+
},
|
|
112
|
+
"suggestions": [],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _collect_memo_ranges(lines):
|
|
117
|
+
ranges = []
|
|
118
|
+
i = 0
|
|
119
|
+
while i < len(lines):
|
|
120
|
+
if _MEMO_LINE_RE.match(lines[i]):
|
|
121
|
+
start = i
|
|
122
|
+
j = i + 1
|
|
123
|
+
while j < len(lines) and _CONTINUATION_RE.match(lines[j]):
|
|
124
|
+
j += 1
|
|
125
|
+
ranges.append((start, j))
|
|
126
|
+
i = j
|
|
127
|
+
else:
|
|
128
|
+
i += 1
|
|
129
|
+
return ranges
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _write_atomic(path, content):
|
|
133
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
134
|
+
fd, tmp = tempfile.mkstemp(prefix=".memo.", suffix=".md.tmp", dir=os.path.dirname(path))
|
|
135
|
+
try:
|
|
136
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
137
|
+
f.write(content)
|
|
138
|
+
os.replace(tmp, path)
|
|
139
|
+
except Exception:
|
|
140
|
+
try:
|
|
141
|
+
os.unlink(tmp)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _err(title, summary):
|
|
148
|
+
return {
|
|
149
|
+
"status": "error",
|
|
150
|
+
"title": title,
|
|
151
|
+
"summary": summary,
|
|
152
|
+
"outputs": {},
|
|
153
|
+
"data": {},
|
|
154
|
+
"suggestions": [],
|
|
155
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Builtin: memo-delete
|
|
2
|
+
# デイリーノートの memo 行を削除する。memo-save と対称。
|
|
3
|
+
|
|
4
|
+
id: memo-delete
|
|
5
|
+
name: Memo Delete
|
|
6
|
+
name_i18n:
|
|
7
|
+
en: Memo Delete
|
|
8
|
+
ja: メモ削除
|
|
9
|
+
description: メモの行を削除する
|
|
10
|
+
description_i18n:
|
|
11
|
+
en: Delete a memo line from the daily note
|
|
12
|
+
ja: メモの行を削除する
|
|
13
|
+
runtime: python
|
|
14
|
+
output_mode: raw
|
|
15
|
+
side_effect: true
|
|
16
|
+
|
|
17
|
+
invocation:
|
|
18
|
+
command: memo.delete
|
|
19
|
+
phrases:
|
|
20
|
+
- メモを消して
|
|
21
|
+
- メモ削除
|
|
22
|
+
- さっきのメモを消して
|
|
23
|
+
phrases_i18n:
|
|
24
|
+
en:
|
|
25
|
+
- delete memo
|
|
26
|
+
- remove that memo
|
|
27
|
+
- delete the previous memo
|
|
28
|
+
ja:
|
|
29
|
+
- メモを消して
|
|
30
|
+
- メモ削除
|
|
31
|
+
- さっきのメモを消して
|
|
32
|
+
|
|
33
|
+
input:
|
|
34
|
+
schema:
|
|
35
|
+
type: object
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
properties:
|
|
38
|
+
date:
|
|
39
|
+
type: string
|
|
40
|
+
description: "対象日 (YYYY-MM-DD)。省略時は今日"
|
|
41
|
+
description_i18n:
|
|
42
|
+
en: "Target date (YYYY-MM-DD). Defaults to today"
|
|
43
|
+
ja: "対象日 (YYYY-MM-DD)。省略時は今日"
|
|
44
|
+
match:
|
|
45
|
+
type: string
|
|
46
|
+
minLength: 1
|
|
47
|
+
description: メモ行に含まれる文字列で特定する
|
|
48
|
+
description_i18n:
|
|
49
|
+
en: Text contained in the memo line to identify
|
|
50
|
+
ja: メモ行に含まれる文字列で特定する
|
|
51
|
+
index:
|
|
52
|
+
type: integer
|
|
53
|
+
minimum: 1
|
|
54
|
+
description: 該当日メモ行の1始まり番号で特定する
|
|
55
|
+
description_i18n:
|
|
56
|
+
en: 1-based memo line number for the target day
|
|
57
|
+
ja: 該当日メモ行の1始まり番号で特定する
|