agent-sin 0.1.11 → 0.1.15

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 +179 -0
  11. package/builtin-skills/memo-list/skill.yaml +51 -0
  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 +10 -3
  42. package/dist/core/chat-engine.js +1563 -197
  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 +568 -14
  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 +13 -0
  73. package/dist/discord/bot.js +542 -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 +122 -31
  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
@@ -9,7 +9,7 @@ memo-index で索引化した Chroma collection に対してクエリを embeddi
9
9
  args.collection: 索引コレクション名 (default: memo)
10
10
 
11
11
  出力:
12
- data.matches: [{text, file, line, timestamp, distance}]
12
+ data.matches: [{title, file, tags, updated, distance, snippet}]
13
13
  """
14
14
 
15
15
  from __future__ import annotations
@@ -86,13 +86,17 @@ async def run(ctx, input):
86
86
  dists = (r.get("distances") or [[]])[0]
87
87
  for doc, meta, dist in zip(docs, metas, dists):
88
88
  meta = meta or {}
89
+ snippet = (doc or "").strip().splitlines()[0] if doc else ""
90
+ if len(snippet) > 100:
91
+ snippet = snippet[:97] + "..."
89
92
  matches.append(
90
93
  {
91
- "text": doc,
94
+ "title": meta.get("title", ""),
92
95
  "file": meta.get("file", ""),
93
- "line": meta.get("line", 0),
94
- "timestamp": meta.get("timestamp", ""),
96
+ "tags": meta.get("tags", ""),
97
+ "updated": meta.get("updated", ""),
95
98
  "distance": float(dist),
99
+ "snippet": snippet,
96
100
  }
97
101
  )
98
102
 
@@ -103,8 +107,9 @@ async def run(ctx, input):
103
107
 
104
108
  lines = [loc.t("Closest memos:", "近いメモ:")]
105
109
  for i, m in enumerate(matches, start=1):
106
- lines.append(f" {i}) {m['file']}:{m['line']} dist={m['distance']:.3f}")
107
- lines.append(f" {m['text'][:80]}")
110
+ lines.append(f" {i}) {m['title']} dist={m['distance']:.3f}")
111
+ if m["snippet"]:
112
+ lines.append(f" {m['snippet']}")
108
113
  return result_value(
109
114
  "ok", loc.t(f"{len(matches)} matches", f"{len(matches)}件見つかりました"), "\n".join(lines), matches
110
115
  )
@@ -0,0 +1,391 @@
1
+ """Helpers for the skill-feedback AI step of nightly-topic-knowledge.
2
+
3
+ Reads the current skill catalog (id / name / description / phrases / enabled
4
+ state) from the builtin and user skills directories, builds an LLM prompt that
5
+ asks for actionable suggestions derived from the previous day's sources, and
6
+ parses the JSON response into a normalized list of proposals that can be
7
+ appended to ctx.history and broadcast over the configured notification
8
+ channel.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ from typing import Any, Iterable
17
+
18
+ try: # PyYAML is already used by other builtin skills.
19
+ import yaml as _yaml # type: ignore
20
+ HAS_PYYAML = True
21
+ except Exception:
22
+ HAS_PYYAML = False
23
+
24
+
25
+ PROPOSAL_TYPES = ("misalign", "repeat", "unused", "user-feedback")
26
+ TARGET_KINDS = ("existing", "new", "rule")
27
+ TARGET_CHANGES = (
28
+ "description",
29
+ "phrases",
30
+ "main",
31
+ "new",
32
+ "claude-md",
33
+ "other",
34
+ )
35
+
36
+ MAX_PROPOSALS = 12
37
+ MAX_TEXT_LEN = 600
38
+ MAX_TITLE_LEN = 120
39
+
40
+
41
+ # ---------- skill catalog ----------
42
+
43
+ def collect_skill_summary(skills_dir: str | None, builtin_skills_dir: str | None) -> list[dict[str, Any]]:
44
+ """Walk both skill roots and return a deduped list of skill summaries.
45
+
46
+ User skills shadow builtin ids when override=true; otherwise builtins win,
47
+ matching the runtime's registry behaviour. Keeps the prompt small by only
48
+ extracting fields the LLM actually needs.
49
+ """
50
+ by_id: dict[str, dict[str, Any]] = {}
51
+ for source, root in (("builtin", builtin_skills_dir), ("user", skills_dir)):
52
+ if not root or not os.path.isdir(root):
53
+ continue
54
+ for entry in sorted(os.listdir(root)):
55
+ if entry.startswith(".") or entry.startswith("_"):
56
+ continue
57
+ yaml_path = os.path.join(root, entry, "skill.yaml")
58
+ if not os.path.isfile(yaml_path):
59
+ continue
60
+ manifest = _read_yaml(yaml_path)
61
+ if not isinstance(manifest, dict):
62
+ continue
63
+ summary = _summarize_manifest(manifest, source)
64
+ if not summary:
65
+ continue
66
+ sid = summary["id"]
67
+ if source == "user" and sid in by_id and not bool(manifest.get("override")):
68
+ continue
69
+ by_id[sid] = summary
70
+ return sorted(by_id.values(), key=lambda s: s["id"])
71
+
72
+
73
+ def _read_yaml(path: str) -> Any:
74
+ try:
75
+ with open(path, "r", encoding="utf-8") as f:
76
+ text = f.read()
77
+ except Exception:
78
+ return None
79
+ if HAS_PYYAML:
80
+ try:
81
+ return _yaml.safe_load(text)
82
+ except Exception:
83
+ return None
84
+ return _fallback_skill_yaml(text)
85
+
86
+
87
+ def _fallback_skill_yaml(text: str) -> dict[str, Any]:
88
+ """Minimal scalar/list extractor used only when PyYAML is missing.
89
+
90
+ Skill.yaml uses nested structures for invocation/input/etc. that the
91
+ fallback can't fully parse — but the few top-level fields we care about
92
+ (id / name / description / enabled) are simple strings, so we can salvage
93
+ enough to keep the prompt useful instead of failing the whole step.
94
+ """
95
+ out: dict[str, Any] = {}
96
+ for line in text.splitlines():
97
+ match = re.match(r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*?)\s*$", line)
98
+ if not match:
99
+ continue
100
+ key, raw = match.group(1), match.group(2)
101
+ if not raw or raw.startswith("#"):
102
+ continue
103
+ if raw.startswith(('"', "'")) and raw.endswith(raw[0]):
104
+ raw = raw[1:-1]
105
+ if key in out:
106
+ continue
107
+ out[key] = raw
108
+ return out
109
+
110
+
111
+ def _summarize_manifest(manifest: dict[str, Any], source: str) -> dict[str, Any] | None:
112
+ sid = manifest.get("id")
113
+ if not isinstance(sid, str) or not sid:
114
+ return None
115
+ description = _pick_localized(manifest, "description")
116
+ invocation = manifest.get("invocation") or {}
117
+ phrases = _pick_localized_list(invocation, "phrases")
118
+ command = invocation.get("command") if isinstance(invocation, dict) else None
119
+ enabled = manifest.get("enabled")
120
+ return {
121
+ "id": sid,
122
+ "name": _pick_localized(manifest, "name") or sid,
123
+ "description": description or "",
124
+ "phrases": phrases,
125
+ "command": command if isinstance(command, str) else "",
126
+ "source": source,
127
+ "enabled": False if enabled is False else True,
128
+ }
129
+
130
+
131
+ def _pick_localized(manifest: dict[str, Any], key: str) -> str:
132
+ i18n = manifest.get(f"{key}_i18n")
133
+ if isinstance(i18n, dict):
134
+ ja = i18n.get("ja")
135
+ if isinstance(ja, str) and ja.strip():
136
+ return ja.strip()
137
+ en = i18n.get("en")
138
+ if isinstance(en, str) and en.strip():
139
+ return en.strip()
140
+ value = manifest.get(key)
141
+ if isinstance(value, str):
142
+ return value.strip()
143
+ return ""
144
+
145
+
146
+ def _pick_localized_list(node: Any, key: str) -> list[str]:
147
+ if not isinstance(node, dict):
148
+ return []
149
+ i18n = node.get(f"{key}_i18n")
150
+ if isinstance(i18n, dict):
151
+ ja = i18n.get("ja")
152
+ if isinstance(ja, list):
153
+ return [str(x).strip() for x in ja if isinstance(x, str) and x.strip()]
154
+ en = i18n.get("en")
155
+ if isinstance(en, list):
156
+ return [str(x).strip() for x in en if isinstance(x, str) and x.strip()]
157
+ raw = node.get(key)
158
+ if isinstance(raw, list):
159
+ return [str(x).strip() for x in raw if isinstance(x, str) and x.strip()]
160
+ return []
161
+
162
+
163
+ # ---------- prompt ----------
164
+
165
+ def build_feedback_prompt(
166
+ date_str: str,
167
+ source_bundle: str,
168
+ skill_catalog: Iterable[dict[str, Any]],
169
+ ) -> str:
170
+ catalog_lines: list[str] = []
171
+ for skill in skill_catalog:
172
+ state = "" if skill.get("enabled") else " [disabled]"
173
+ phrases = skill.get("phrases") or []
174
+ phrases_text = " / ".join(phrases[:6]) if phrases else ""
175
+ line = f"- {skill['id']}{state}: {skill.get('description', '').strip()}"
176
+ if phrases_text:
177
+ line += f" (phrases: {phrases_text})"
178
+ catalog_lines.append(line)
179
+ catalog_text = "\n".join(catalog_lines) if catalog_lines else "(no skills registered)"
180
+
181
+ schema = {
182
+ "proposals": [
183
+ {
184
+ "type": "<one of: misalign | repeat | unused | user-feedback>",
185
+ "title": "<short Japanese title under 60 chars>",
186
+ "citation": "<short quote / timestamp showing where in the sources this came from>",
187
+ "proposal": "<concrete change: what to add/remove/rename and where>",
188
+ "target_kind": "<existing | new | rule>",
189
+ "target_skill": "<existing skill id, or proposed new skill id (kebab-case), or empty for rule>",
190
+ "target_change": "<description | phrases | main | new | claude-md | other>",
191
+ }
192
+ ]
193
+ }
194
+
195
+ instructions = [
196
+ "You are reviewing one day of an individual user's conversations / daily memory / notes",
197
+ "to look for ways to improve the assistant's skill set. The goal is to surface concrete,",
198
+ "actionable proposals that the user can later batch-apply by replying with item numbers.",
199
+ "",
200
+ "Detect four categories — keep the `type` field to one of these literal strings:",
201
+ "- misalign: the assistant misread the user's intent, took the wrong tool, or had to redo work.",
202
+ "- repeat: the user asked for the same hand-driven task more than once → should become a skill.",
203
+ "- unused: an existing skill was not called when it should have been, or its phrases / description",
204
+ " did not match how the user actually asked for it.",
205
+ "- user-feedback: explicit corrective or affirming feedback the user gave that should change",
206
+ " default behaviour (rule / description / phrases).",
207
+ "",
208
+ "Hard rules:",
209
+ "- Output ONE JSON object only. No prose, no markdown fences.",
210
+ f"- At most {MAX_PROPOSALS} proposals. Skip trivial or speculative ones.",
211
+ "- Skip anything already obviously satisfied by the existing skills below.",
212
+ "- For `target_kind: existing`, `target_skill` MUST match an id from the skill catalog.",
213
+ "- For `target_kind: new`, propose a fresh kebab-case id that does NOT collide with any existing id.",
214
+ "- For `target_kind: rule`, leave `target_skill` empty and explain in `proposal` where the rule lives",
215
+ " (e.g. CLAUDE.md, feedback memory, runtime gate).",
216
+ "- `citation` must include a short verbatim quote or timestamp so the user can verify.",
217
+ "- All text in Japanese. Keep each field under 600 characters.",
218
+ "",
219
+ f"Date: {date_str}",
220
+ "",
221
+ "Current skill catalog (id [disabled?]: description (phrases: ...)):",
222
+ catalog_text,
223
+ "",
224
+ "JSON schema (output shape):",
225
+ json.dumps(schema, ensure_ascii=False, indent=2),
226
+ "",
227
+ "Sources:",
228
+ source_bundle or "(empty)",
229
+ ]
230
+ return "\n".join(instructions)
231
+
232
+
233
+ # ---------- response parsing ----------
234
+
235
+ _JSON_OBJECT_PATTERN = re.compile(r"\{[\s\S]*\}")
236
+
237
+
238
+ def parse_feedback_response(text: str) -> list[dict[str, Any]]:
239
+ raw = (text or "").strip()
240
+ if not raw:
241
+ return []
242
+ fenced = re.match(r"^```(?:json)?\s*([\s\S]*?)\s*```$", raw)
243
+ if fenced:
244
+ raw = fenced.group(1).strip()
245
+ parsed: Any
246
+ try:
247
+ parsed = json.loads(raw)
248
+ except Exception:
249
+ match = _JSON_OBJECT_PATTERN.search(raw)
250
+ if not match:
251
+ return []
252
+ try:
253
+ parsed = json.loads(match.group(0))
254
+ except Exception:
255
+ return []
256
+ if not isinstance(parsed, dict):
257
+ return []
258
+ items = parsed.get("proposals")
259
+ if not isinstance(items, list):
260
+ return []
261
+ out: list[dict[str, Any]] = []
262
+ known_ids: set[str] = set()
263
+ for raw_item in items:
264
+ normalized = _normalize_proposal(raw_item, known_ids)
265
+ if normalized is None:
266
+ continue
267
+ out.append(normalized)
268
+ if len(out) >= MAX_PROPOSALS:
269
+ break
270
+ return out
271
+
272
+
273
+ def _normalize_proposal(item: Any, known_ids: set[str]) -> dict[str, Any] | None:
274
+ if not isinstance(item, dict):
275
+ return None
276
+ ptype = _clamp_enum(item.get("type"), PROPOSAL_TYPES)
277
+ if not ptype:
278
+ return None
279
+ title = _clean_str(item.get("title"), MAX_TITLE_LEN)
280
+ if not title:
281
+ return None
282
+ citation = _clean_str(item.get("citation"), MAX_TEXT_LEN)
283
+ proposal = _clean_str(item.get("proposal"), MAX_TEXT_LEN)
284
+ if not proposal:
285
+ return None
286
+ target_kind = _clamp_enum(item.get("target_kind"), TARGET_KINDS) or "rule"
287
+ target_skill = _sanitize_skill_id(item.get("target_skill"))
288
+ if target_kind == "existing" and not target_skill:
289
+ return None
290
+ if target_kind == "new" and not target_skill:
291
+ return None
292
+ target_change = _clamp_enum(item.get("target_change"), TARGET_CHANGES) or "other"
293
+ # Avoid duplicate identical proposals from the model.
294
+ fingerprint = f"{ptype}|{title}|{target_kind}|{target_skill}"
295
+ if fingerprint in known_ids:
296
+ return None
297
+ known_ids.add(fingerprint)
298
+ return {
299
+ "type": ptype,
300
+ "title": title,
301
+ "citation": citation,
302
+ "proposal": proposal,
303
+ "target_kind": target_kind,
304
+ "target_skill": target_skill,
305
+ "target_change": target_change,
306
+ }
307
+
308
+
309
+ def _clean_str(value: Any, cap: int) -> str:
310
+ if not isinstance(value, str):
311
+ return ""
312
+ text = value.strip()
313
+ if len(text) > cap:
314
+ text = text[:cap].rstrip() + "…"
315
+ return text
316
+
317
+
318
+ def _clamp_enum(value: Any, allowed: Iterable[str]) -> str | None:
319
+ if not isinstance(value, str):
320
+ return None
321
+ text = value.strip().lower()
322
+ for candidate in allowed:
323
+ if text == candidate:
324
+ return candidate
325
+ return None
326
+
327
+
328
+ def _sanitize_skill_id(value: Any) -> str:
329
+ if not isinstance(value, str):
330
+ return ""
331
+ text = value.strip().lower()
332
+ text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
333
+ if not text:
334
+ return ""
335
+ if len(text) > 48:
336
+ text = text[:48].rstrip("-")
337
+ if not re.match(r"^[a-z][a-z0-9-]*$", text):
338
+ return ""
339
+ return text
340
+
341
+
342
+ # ---------- formatting for Telegram + history ----------
343
+
344
+ TYPE_LABEL = {
345
+ "misalign": "ズレ",
346
+ "repeat": "繰り返し",
347
+ "unused": "未活用",
348
+ "user-feedback": "フィードバック",
349
+ }
350
+
351
+ TARGET_KIND_LABEL = {
352
+ "existing": "既存",
353
+ "new": "新規",
354
+ "rule": "ルール",
355
+ }
356
+
357
+
358
+ def format_proposal_for_history(date_str: str, index: int, proposal: dict[str, Any]) -> str:
359
+ type_label = TYPE_LABEL.get(proposal["type"], proposal["type"])
360
+ kind_label = TARGET_KIND_LABEL.get(proposal["target_kind"], proposal["target_kind"])
361
+ target = proposal.get("target_skill") or ""
362
+ target_block = (
363
+ f"{kind_label}: {target}" if target else kind_label
364
+ )
365
+ parts = [
366
+ f"#{index}. [{type_label}] {proposal['title']}",
367
+ f"対象: {target_block} / {proposal.get('target_change') or 'other'}",
368
+ ]
369
+ citation = proposal.get("citation")
370
+ if citation:
371
+ parts.append(f"該当: {citation}")
372
+ parts.append(f"提案: {proposal['proposal']}")
373
+ return "\n".join(parts)
374
+
375
+
376
+ def format_notification(date_str: str, proposals: list[dict[str, Any]], char_budget: int = 3500) -> str:
377
+ if not proposals:
378
+ return ""
379
+ header = f"📋 スキル振り返り {date_str} ({len(proposals)}件)\n返信例: 「1, 3 を適用」「2 は不要」"
380
+ lines: list[str] = [header]
381
+ used = len(header)
382
+ for index, proposal in enumerate(proposals, start=1):
383
+ body = format_proposal_for_history(date_str, index, proposal)
384
+ addition = "\n\n" + body
385
+ if used + len(addition) > char_budget:
386
+ remaining = len(proposals) - (index - 1)
387
+ lines.append(f"\n\n…他 {remaining} 件は保存済み(履歴参照)")
388
+ break
389
+ lines.append(addition)
390
+ used += len(addition)
391
+ return "".join(lines)