agent-sin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/assets/logo.png +0 -0
  5. package/builtin-skills/_shared/_models_lib.py +227 -0
  6. package/builtin-skills/_shared/_profile_lib.py +98 -0
  7. package/builtin-skills/_shared/_schedules_lib.py +313 -0
  8. package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
  9. package/builtin-skills/_shared/i18n.py +26 -0
  10. package/builtin-skills/memo-delete/main.py +155 -0
  11. package/builtin-skills/memo-delete/skill.yaml +57 -0
  12. package/builtin-skills/memo-index/main.py +178 -0
  13. package/builtin-skills/memo-index/skill.yaml +53 -0
  14. package/builtin-skills/memo-save/README.md +5 -0
  15. package/builtin-skills/memo-save/main.py +74 -0
  16. package/builtin-skills/memo-save/skill.yaml +52 -0
  17. package/builtin-skills/memo-search/README.md +10 -0
  18. package/builtin-skills/memo-search/main.py +97 -0
  19. package/builtin-skills/memo-search/skill.yaml +51 -0
  20. package/builtin-skills/memo-vector-search/main.py +121 -0
  21. package/builtin-skills/memo-vector-search/skill.yaml +53 -0
  22. package/builtin-skills/model-add/main.py +180 -0
  23. package/builtin-skills/model-add/skill.yaml +112 -0
  24. package/builtin-skills/model-list/main.py +93 -0
  25. package/builtin-skills/model-list/skill.yaml +48 -0
  26. package/builtin-skills/model-set/main.py +123 -0
  27. package/builtin-skills/model-set/skill.yaml +69 -0
  28. package/builtin-skills/profile-delete/_profile_lib.py +98 -0
  29. package/builtin-skills/profile-delete/main.py +98 -0
  30. package/builtin-skills/profile-delete/skill.yaml +64 -0
  31. package/builtin-skills/profile-edit/_profile_lib.py +98 -0
  32. package/builtin-skills/profile-edit/main.py +97 -0
  33. package/builtin-skills/profile-edit/skill.yaml +72 -0
  34. package/builtin-skills/profile-save/main.py +52 -0
  35. package/builtin-skills/profile-save/skill.yaml +69 -0
  36. package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
  37. package/builtin-skills/schedule-add/main.py +137 -0
  38. package/builtin-skills/schedule-add/skill.yaml +94 -0
  39. package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
  40. package/builtin-skills/schedule-list/main.py +86 -0
  41. package/builtin-skills/schedule-list/skill.yaml +45 -0
  42. package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
  43. package/builtin-skills/schedule-remove/main.py +69 -0
  44. package/builtin-skills/schedule-remove/skill.yaml +49 -0
  45. package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
  46. package/builtin-skills/schedule-toggle/main.py +78 -0
  47. package/builtin-skills/schedule-toggle/skill.yaml +61 -0
  48. package/builtin-skills/skills-disable/main.py +63 -0
  49. package/builtin-skills/skills-disable/skill.yaml +52 -0
  50. package/builtin-skills/skills-enable/main.py +62 -0
  51. package/builtin-skills/skills-enable/skill.yaml +51 -0
  52. package/builtin-skills/todo-add/main.py +68 -0
  53. package/builtin-skills/todo-add/skill.yaml +53 -0
  54. package/builtin-skills/todo-delete/main.py +65 -0
  55. package/builtin-skills/todo-delete/skill.yaml +47 -0
  56. package/builtin-skills/todo-done/main.py +75 -0
  57. package/builtin-skills/todo-done/skill.yaml +47 -0
  58. package/builtin-skills/todo-list/main.py +91 -0
  59. package/builtin-skills/todo-list/skill.yaml +48 -0
  60. package/builtin-skills/todo-tick/main.py +125 -0
  61. package/builtin-skills/todo-tick/skill.yaml +48 -0
  62. package/dist/builder/build-action-classifier.d.ts +18 -0
  63. package/dist/builder/build-action-classifier.js +142 -0
  64. package/dist/builder/build-commands.d.ts +19 -0
  65. package/dist/builder/build-commands.js +133 -0
  66. package/dist/builder/build-flow.d.ts +72 -0
  67. package/dist/builder/build-flow.js +416 -0
  68. package/dist/builder/builder-session.d.ts +117 -0
  69. package/dist/builder/builder-session.js +1129 -0
  70. package/dist/builder/conversation-router.d.ts +22 -0
  71. package/dist/builder/conversation-router.js +69 -0
  72. package/dist/builder/intent-runtime-store.d.ts +7 -0
  73. package/dist/builder/intent-runtime-store.js +60 -0
  74. package/dist/builder/progress-format.d.ts +7 -0
  75. package/dist/builder/progress-format.js +46 -0
  76. package/dist/cli/index.d.ts +2 -0
  77. package/dist/cli/index.js +2835 -0
  78. package/dist/cli/spinner.d.ts +30 -0
  79. package/dist/cli/spinner.js +164 -0
  80. package/dist/core/ai-provider.d.ts +75 -0
  81. package/dist/core/ai-provider.js +678 -0
  82. package/dist/core/builtin-skills.d.ts +27 -0
  83. package/dist/core/builtin-skills.js +120 -0
  84. package/dist/core/chat-engine.d.ts +70 -0
  85. package/dist/core/chat-engine.js +812 -0
  86. package/dist/core/config.d.ts +127 -0
  87. package/dist/core/config.js +1379 -0
  88. package/dist/core/daily-memory-promotion.d.ts +21 -0
  89. package/dist/core/daily-memory-promotion.js +422 -0
  90. package/dist/core/i18n.d.ts +23 -0
  91. package/dist/core/i18n.js +167 -0
  92. package/dist/core/info-lines.d.ts +5 -0
  93. package/dist/core/info-lines.js +39 -0
  94. package/dist/core/input-schema.d.ts +2 -0
  95. package/dist/core/input-schema.js +156 -0
  96. package/dist/core/intent-router.d.ts +27 -0
  97. package/dist/core/intent-router.js +160 -0
  98. package/dist/core/logger.d.ts +60 -0
  99. package/dist/core/logger.js +240 -0
  100. package/dist/core/memory.d.ts +10 -0
  101. package/dist/core/memory.js +72 -0
  102. package/dist/core/message-utils.d.ts +13 -0
  103. package/dist/core/message-utils.js +104 -0
  104. package/dist/core/notifier.d.ts +17 -0
  105. package/dist/core/notifier.js +424 -0
  106. package/dist/core/output-writer.d.ts +13 -0
  107. package/dist/core/output-writer.js +100 -0
  108. package/dist/core/plan-decision.d.ts +16 -0
  109. package/dist/core/plan-decision.js +88 -0
  110. package/dist/core/profile-memory.d.ts +17 -0
  111. package/dist/core/profile-memory.js +142 -0
  112. package/dist/core/runtime.d.ts +50 -0
  113. package/dist/core/runtime.js +187 -0
  114. package/dist/core/scheduler.d.ts +28 -0
  115. package/dist/core/scheduler.js +155 -0
  116. package/dist/core/secrets.d.ts +31 -0
  117. package/dist/core/secrets.js +214 -0
  118. package/dist/core/service.d.ts +35 -0
  119. package/dist/core/service.js +479 -0
  120. package/dist/core/skill-planner.d.ts +24 -0
  121. package/dist/core/skill-planner.js +100 -0
  122. package/dist/core/skill-registry.d.ts +98 -0
  123. package/dist/core/skill-registry.js +319 -0
  124. package/dist/core/skill-scaffold.d.ts +33 -0
  125. package/dist/core/skill-scaffold.js +256 -0
  126. package/dist/core/skill-settings.d.ts +11 -0
  127. package/dist/core/skill-settings.js +63 -0
  128. package/dist/core/transfer.d.ts +31 -0
  129. package/dist/core/transfer.js +270 -0
  130. package/dist/core/update-notifier.d.ts +2 -0
  131. package/dist/core/update-notifier.js +140 -0
  132. package/dist/discord/bot.d.ts +96 -0
  133. package/dist/discord/bot.js +2424 -0
  134. package/dist/runtimes/codex-app-server.d.ts +53 -0
  135. package/dist/runtimes/codex-app-server.js +305 -0
  136. package/dist/runtimes/python-runner.d.ts +7 -0
  137. package/dist/runtimes/python-runner.js +302 -0
  138. package/dist/runtimes/typescript-runner.d.ts +5 -0
  139. package/dist/runtimes/typescript-runner.js +172 -0
  140. package/dist/skills-sdk/types.d.ts +38 -0
  141. package/dist/skills-sdk/types.js +1 -0
  142. package/dist/telegram/bot.d.ts +94 -0
  143. package/dist/telegram/bot.js +1219 -0
  144. package/install.ps1 +132 -0
  145. package/install.sh +130 -0
  146. package/package.json +60 -0
  147. package/templates/skill-python/main.py +74 -0
  148. package/templates/skill-python/skill.yaml +48 -0
  149. package/templates/skill-typescript/main.ts +87 -0
  150. package/templates/skill-typescript/skill.yaml +42 -0
@@ -0,0 +1,178 @@
1
+ """Builtin: memo-index
2
+
3
+ notes_dir 配下の Markdown を読んで、各 "- {timestamp} {text}" 行を
4
+ Chroma の collection (default: memo) に upsert する。
5
+
6
+ 入力:
7
+ args.since: ISO8601 文字列 (この時刻より古いメモはスキップ、任意)
8
+ args.collection: 索引コレクション名 (default: memo)
9
+
10
+ 出力:
11
+ data: {added, skipped, total, collection}
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import os
18
+ import sys
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
23
+ from i18n import localizer # noqa: E402
24
+
25
+
26
+ EMBED_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
27
+
28
+
29
+ async def run(ctx, input):
30
+ loc = localizer(input)
31
+ args = input.get("args", {})
32
+ since_str = args.get("since")
33
+ collection_name = args.get("collection", "memo")
34
+ sources = input.get("sources", {})
35
+ notes_dir = sources.get("notes_dir")
36
+ index_dir = sources.get("index_dir")
37
+
38
+ if not notes_dir or not index_dir:
39
+ return result_error(
40
+ loc.t("Cannot index", "索引化できません"),
41
+ loc.t("notes_dir or index_dir was not provided by the runtime.", "notes_dir または index_dir が Runtime から渡されていません"),
42
+ )
43
+
44
+ try:
45
+ import chromadb
46
+ from chromadb.utils.embedding_functions import (
47
+ SentenceTransformerEmbeddingFunction,
48
+ )
49
+ except ImportError as e:
50
+ ctx.log.error(f"memo-index: missing dependency: {e}")
51
+ return result_error(
52
+ loc.t("chromadb not found", "chromadb が見つかりません"),
53
+ loc.t(
54
+ "Run: python3 -m venv ~/.agent-sin/.venv && ~/.agent-sin/.venv/bin/pip install chromadb sentence-transformers",
55
+ "次を実行: python3 -m venv ~/.agent-sin/.venv && ~/.agent-sin/.venv/bin/pip install chromadb sentence-transformers",
56
+ ),
57
+ )
58
+
59
+ chroma_path = Path(index_dir).expanduser().resolve() / "local-index" / "chroma"
60
+ chroma_path.mkdir(parents=True, exist_ok=True)
61
+
62
+ notes_root = Path(notes_dir).expanduser().resolve()
63
+ if not notes_root.exists():
64
+ ctx.log.info("memo-index: notes_dir does not exist yet")
65
+ return result_ok(
66
+ loc.t("Nothing to index", "索引化対象なし"),
67
+ loc.t("There are no memos yet.", "まだメモがありません"),
68
+ {"added": 0, "skipped": 0, "total": 0, "collection": collection_name},
69
+ )
70
+
71
+ try:
72
+ ef = SentenceTransformerEmbeddingFunction(model_name=EMBED_MODEL)
73
+ except Exception as e:
74
+ ctx.log.error(f"memo-index: failed to load embedder: {e}")
75
+ return result_error(
76
+ loc.t("Could not initialize embedding model", "embedding モデルを初期化できません"),
77
+ loc.t(f"{e}. Check that sentence-transformers is installed.", f"{e}. sentence-transformers のインストールを確認してください"),
78
+ )
79
+
80
+ client = chromadb.PersistentClient(path=str(chroma_path))
81
+ col = client.get_or_create_collection(name=collection_name, embedding_function=ef)
82
+
83
+ try:
84
+ existing_ids = set(col.get()["ids"])
85
+ except Exception:
86
+ existing_ids = set()
87
+
88
+ since = parse_iso(since_str) if since_str else None
89
+ total = added = skipped = 0
90
+ documents, ids, metadatas = [], [], []
91
+
92
+ for md in sorted(notes_root.rglob("*.md")):
93
+ try:
94
+ relative = md.relative_to(notes_root)
95
+ except ValueError:
96
+ continue
97
+ # reports サブディレクトリは出力先なので索引対象外
98
+ if "reports" in relative.parts:
99
+ continue
100
+ try:
101
+ lines = md.read_text(encoding="utf-8", errors="ignore").splitlines()
102
+ except OSError:
103
+ continue
104
+ for line_no, raw in enumerate(lines, start=1):
105
+ text = raw.strip()
106
+ if not text.startswith("- "):
107
+ continue
108
+ body = text[2:].strip()
109
+ if not body:
110
+ continue
111
+ first, _, rest = body.partition(" ")
112
+ ts = parse_iso(first)
113
+ if ts and rest:
114
+ body = rest.strip()
115
+ if since and ts and ts < since:
116
+ continue
117
+ total += 1
118
+ file_rel = str(relative)
119
+ doc_id = sha1(f"{file_rel}:{line_no}:{body}")
120
+ if doc_id in existing_ids:
121
+ skipped += 1
122
+ continue
123
+ documents.append(body)
124
+ ids.append(doc_id)
125
+ metadatas.append(
126
+ {
127
+ "file": file_rel,
128
+ "line": line_no,
129
+ "timestamp": ts.isoformat() if ts else "",
130
+ }
131
+ )
132
+ added += 1
133
+
134
+ if documents:
135
+ ctx.log.info(f"memo-index: adding {len(documents)} new chunks to {collection_name}")
136
+ col.add(documents=documents, ids=ids, metadatas=metadatas)
137
+
138
+ summary = loc.t(f"Added {added} / skipped {skipped} / found {total}", f"追加 {added} / スキップ {skipped} / 検出 {total}")
139
+ return result_ok(
140
+ loc.t(f"Indexed {added} entries", f"{added}件を索引化しました"),
141
+ summary,
142
+ {"added": added, "skipped": skipped, "total": total, "collection": collection_name},
143
+ )
144
+
145
+
146
+ def sha1(value):
147
+ return hashlib.sha1(value.encode("utf-8")).hexdigest()
148
+
149
+
150
+ def parse_iso(value):
151
+ if not value:
152
+ return None
153
+ try:
154
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
155
+ except ValueError:
156
+ return None
157
+
158
+
159
+ def result_ok(title, summary, data):
160
+ return {
161
+ "status": "ok",
162
+ "title": title,
163
+ "summary": summary,
164
+ "outputs": {},
165
+ "data": data,
166
+ "suggestions": [],
167
+ }
168
+
169
+
170
+ def result_error(title, summary):
171
+ return {
172
+ "status": "error",
173
+ "title": title,
174
+ "summary": summary,
175
+ "outputs": {},
176
+ "data": {},
177
+ "suggestions": [],
178
+ }
@@ -0,0 +1,53 @@
1
+ # Builtin: memo-index
2
+ # notes_dir 配下のメモ行を Chroma ベクター DB に索引化する。
3
+ # 索引は ~/.agent-sin/index/local-index/chroma に永続化、collection は memo (デフォルト)。
4
+ # 多言語対応のため SentenceTransformer (paraphrase-multilingual-MiniLM-L12-v2) を使う。
5
+ # Python 依存: chromadb, sentence-transformers
6
+ # 推奨インストール先: ~/.agent-sin/.venv (auto-detect される)
7
+ # python3 -m venv ~/.agent-sin/.venv
8
+ # ~/.agent-sin/.venv/bin/pip install chromadb sentence-transformers
9
+
10
+ id: memo-index
11
+ name: Memo Index
12
+ name_i18n:
13
+ en: Memo Index
14
+ ja: メモ索引化
15
+ description: notes_dirのメモをChromaベクターDBに索引化する
16
+ description_i18n:
17
+ en: Index notes_dir memos into a Chroma vector database
18
+ ja: notes_dirのメモをChromaベクターDBに索引化する
19
+ runtime: python
20
+
21
+ invocation:
22
+ command: memo.index
23
+ phrases:
24
+ - メモを索引化
25
+ - インデックス更新
26
+ phrases_i18n:
27
+ en:
28
+ - index memos
29
+ - update memo index
30
+ ja:
31
+ - メモを索引化
32
+ - インデックス更新
33
+
34
+ input:
35
+ schema:
36
+ type: object
37
+ additionalProperties: false
38
+ properties:
39
+ since:
40
+ type: string
41
+ description: ISO8601 (このタイムスタンプより新しい行のみ索引化)
42
+ description_i18n:
43
+ en: ISO8601 timestamp; only newer lines are indexed
44
+ ja: ISO8601 (このタイムスタンプより新しい行のみ索引化)
45
+ collection:
46
+ type: string
47
+ default: memo
48
+ required: []
49
+
50
+ memory:
51
+ namespace: memo-index
52
+ read: true
53
+ write: true
@@ -0,0 +1,5 @@
1
+ # Memo Save
2
+
3
+ 会話やCLIから渡された本文を、日付ごとのMarkdownメモへ追記するProgram Skillです。
4
+
5
+ 保存先やファイル名は `skill.yaml` の `outputs` に定義し、実際の保存はRuntimeが担当します。
@@ -0,0 +1,74 @@
1
+ """Builtin: memo-save
2
+
3
+ 入力 text を Markdown メモのデイリーファイルに追記する。複数行に対応し、
4
+ 1メモ = 1バレット + 継続行(2スペースインデント)の Markdown リスト形式で書き出す。
5
+ Runtime が outputs.note を skill.yaml の outputs[id=note] に従って保存するため、
6
+ このスキル自身は **content を返すだけ**でファイルには触らない。
7
+
8
+ 入力:
9
+ args.text: 保存する本文 (必須, 改行可)
10
+ 出力:
11
+ outputs.note: 追記用の Markdown。保存先の表示は skill.yaml 側で抑制する。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import sys
18
+ from datetime import datetime
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
21
+ from i18n import localizer # noqa: E402
22
+
23
+
24
+ async def run(ctx, input):
25
+ loc = localizer(input)
26
+ args = input.get("args", {})
27
+ text = str(args.get("text", "")).strip()
28
+
29
+ if not text:
30
+ ctx.log.warn("memo-save: empty text, skipping")
31
+ return {
32
+ "status": "skipped",
33
+ "title": loc.t("No memo", "メモなし"),
34
+ "summary": loc.t("There is no text to save.", "保存する本文がありません"),
35
+ "outputs": {},
36
+ "data": {},
37
+ "suggestions": [],
38
+ }
39
+
40
+ ctx.log.info(f"memo-save: saving {len(text)} chars")
41
+ timestamp = input.get("trigger", {}).get("time") or datetime.now().isoformat()
42
+ content = format_memo(timestamp, text)
43
+
44
+ return {
45
+ "status": "ok",
46
+ "title": loc.t("Saved", "保存しました"),
47
+ "summary": loc.t("Memo saved.", "メモを保存しました"),
48
+ "outputs": {
49
+ "note": {
50
+ "content": content,
51
+ "frontmatter": {
52
+ "tags": ["memo"]
53
+ },
54
+ }
55
+ },
56
+ "data": {
57
+ "length": len(text)
58
+ },
59
+ "suggestions": [],
60
+ }
61
+
62
+
63
+ def format_memo(timestamp, text):
64
+ # 改行を正規化し、空行はメモ区切りと衝突するため除外する
65
+ raw_lines = [line.rstrip() for line in text.replace("\r\n", "\n").split("\n")]
66
+ body_lines = [line for line in raw_lines if line.strip()]
67
+ if not body_lines:
68
+ return ""
69
+ first, *rest = body_lines
70
+ head = f"- {timestamp} {first}"
71
+ if not rest:
72
+ return head + "\n"
73
+ indented = "\n".join(f" {line}" for line in rest)
74
+ return f"{head}\n{indented}\n"
@@ -0,0 +1,52 @@
1
+ # Builtin: memo-save
2
+ # 会話の skill-call または agent-sin run memo-save --text "..." で呼ばれる。
3
+ # Markdown のデイリーノートに追記する低リスクスキル。複数行テキストにも対応する。
4
+ # ユーザースキル著作のリファレンスとしても読まれる。詳細は https://agent.shingoirie.com/skill-authoring を参照。
5
+
6
+ id: memo-save
7
+ name: Memo Save
8
+ name_i18n:
9
+ en: Memo Save
10
+ ja: メモ保存
11
+ description: 会話内容をMarkdownメモとして保存する
12
+ description_i18n:
13
+ en: Save conversation text as a Markdown memo
14
+ ja: 会話内容をMarkdownメモとして保存する
15
+ runtime: python
16
+ side_effect: true
17
+
18
+ invocation:
19
+ command: memo.save
20
+ phrases:
21
+ - メモしといて
22
+ - メモして
23
+ phrases_i18n:
24
+ en:
25
+ - save this memo
26
+ - take a memo
27
+ - remember this
28
+ ja:
29
+ - メモしといて
30
+ - メモして
31
+
32
+ input:
33
+ schema:
34
+ type: object
35
+ properties:
36
+ text:
37
+ type: string
38
+ required:
39
+ - text
40
+
41
+ outputs:
42
+ - id: note
43
+ type: markdown
44
+ path: notes/{{yyyy}}/{{MM}}
45
+ filename: "{{date}}.md"
46
+ append: true
47
+ show_saved: false
48
+
49
+ memory:
50
+ namespace: memo
51
+ read: true
52
+ write: true
@@ -0,0 +1,10 @@
1
+ # Memo Search
2
+
3
+ 保存済みのMarkdownメモをキーワード検索するProgram Skillです。
4
+
5
+ 検索対象はRuntimeから渡されるノート保存先に限定されます。外部送信やAIステップは使いません。
6
+
7
+ ```bash
8
+ agent-sin run memo-search --query "探したい言葉"
9
+ agent-sin chat "メモ検索 探したい言葉"
10
+ ```
@@ -0,0 +1,97 @@
1
+ """Builtin: memo-search
2
+
3
+ input.sources.notes_dir 配下の *.md を全文走査し、query にすべての語が含まれる
4
+ 最初の 1 行を結果に含める。outputs は持たず data.matches に結果を返すのみ。
5
+
6
+ 入力:
7
+ args.query: 検索語 (空白区切りで AND 検索)
8
+ args.limit: 上限 (1..50, default 10)
9
+ 出力:
10
+ data.matches: [{file, line, text}]
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import sys
17
+ from pathlib import Path
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
+ async def run(ctx, input):
24
+ loc = localizer(input)
25
+ args = input.get("args", {})
26
+ query = str(args.get("query", "")).strip()
27
+ limit = int(args.get("limit", 10))
28
+ notes_dir = input.get("sources", {}).get("notes_dir")
29
+
30
+ if not query:
31
+ ctx.log.warn("memo-search: empty query")
32
+ return result("skipped", loc.t("No query", "検索語なし"), loc.t("There is no keyword to search.", "検索するキーワードがありません"), [])
33
+ if not notes_dir:
34
+ ctx.log.error("memo-search: notes_dir not provided by runtime")
35
+ return result("error", loc.t("Cannot search", "検索できません"), loc.t("The notes directory was not provided by the runtime.", "ノート保存先がRuntimeから渡されていません"), [])
36
+
37
+ root = Path(notes_dir).expanduser().resolve()
38
+ if not root.exists():
39
+ ctx.log.info(f"memo-search: notes_dir does not exist yet: {root}")
40
+ return result("ok", loc.t("0 matches", "0件見つかりました"), loc.t("There are no memos yet.", "まだメモがありません"), [])
41
+
42
+ terms = [term.casefold() for term in query.split() if term]
43
+ ctx.log.info(f"memo-search: searching {len(terms)} term(s) in {root}, limit={limit}")
44
+ matches = search_notes(root, terms, limit)
45
+ if not matches:
46
+ return result("ok", loc.t("0 matches", "0件見つかりました"), loc.t(f'No memos contain "{query}".', f"「{query}」を含むメモは見つかりませんでした"), [])
47
+
48
+ lines = [loc.t("Found memos:", "見つかったメモ:")]
49
+ for item in matches:
50
+ lines.append(f"- {item['file']}:{item['line']} {item['text']}")
51
+ return result("ok", loc.t(f"{len(matches)} matches", f"{len(matches)}件見つかりました"), "\n".join(lines), matches)
52
+
53
+
54
+ def search_notes(root, terms, limit):
55
+ matches = []
56
+ for file in sorted(root.rglob("*.md")):
57
+ if len(matches) >= limit:
58
+ break
59
+ try:
60
+ resolved = file.resolve()
61
+ if root != resolved and root not in resolved.parents:
62
+ continue
63
+ lines = file.read_text(encoding="utf-8", errors="ignore").splitlines()
64
+ except OSError:
65
+ continue
66
+
67
+ for index, line in enumerate(lines, start=1):
68
+ if all(term in line.casefold() for term in terms):
69
+ matches.append(
70
+ {
71
+ "file": str(file.relative_to(root)),
72
+ "line": index,
73
+ "text": snippet(line),
74
+ }
75
+ )
76
+ break
77
+ return matches
78
+
79
+
80
+ def snippet(line):
81
+ text = " ".join(line.strip().split())
82
+ if len(text) <= 160:
83
+ return text
84
+ return f"{text[:157]}..."
85
+
86
+
87
+ def result(status, title, summary, matches):
88
+ return {
89
+ "status": status,
90
+ "title": title,
91
+ "summary": summary,
92
+ "outputs": {},
93
+ "data": {
94
+ "matches": matches,
95
+ },
96
+ "suggestions": [],
97
+ }
@@ -0,0 +1,51 @@
1
+ # Builtin: memo-search
2
+ # notes_dir 配下の Markdown を全文 grep する読み取り専用スキル。
3
+ # memory namespace は memo (memo-save と共有) だが write=false で読み出しのみ。
4
+
5
+ id: memo-search
6
+ name: Memo Search
7
+ name_i18n:
8
+ en: Memo Search
9
+ ja: メモ検索
10
+ description: 保存済みMarkdownメモをキーワード検索する
11
+ description_i18n:
12
+ en: Search saved Markdown memos by keyword
13
+ ja: 保存済みMarkdownメモをキーワード検索する
14
+ runtime: python
15
+
16
+ invocation:
17
+ command: memo.search
18
+ phrases:
19
+ - メモ検索
20
+ - メモを検索して
21
+ - 検索して
22
+ phrases_i18n:
23
+ en:
24
+ - search memos
25
+ - search my notes
26
+ - find a memo
27
+ ja:
28
+ - メモ検索
29
+ - メモを検索して
30
+ - 検索して
31
+
32
+ input:
33
+ schema:
34
+ type: object
35
+ additionalProperties: false
36
+ properties:
37
+ query:
38
+ type: string
39
+ minLength: 1
40
+ limit:
41
+ type: integer
42
+ minimum: 1
43
+ maximum: 50
44
+ default: 10
45
+ required:
46
+ - query
47
+
48
+ memory:
49
+ namespace: memo
50
+ read: true
51
+ write: false
@@ -0,0 +1,121 @@
1
+ """Builtin: memo-vector-search
2
+
3
+ memo-index で索引化した Chroma collection に対してクエリを embedding 化し、
4
+ 意味的に近いメモを top-K で返す。先に agent-sin run memo-index を実行する必要あり。
5
+
6
+ 入力:
7
+ args.query: 検索クエリ (必須, 1 文字以上)
8
+ args.limit: 上位件数 (1..50, default 5)
9
+ args.collection: 索引コレクション名 (default: memo)
10
+
11
+ 出力:
12
+ data.matches: [{text, file, line, timestamp, distance}]
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
22
+ from i18n import localizer # noqa: E402
23
+
24
+
25
+ EMBED_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
26
+
27
+
28
+ async def run(ctx, input):
29
+ loc = localizer(input)
30
+ args = input.get("args", {})
31
+ query = str(args.get("query", "")).strip()
32
+ limit = int(args.get("limit", 5))
33
+ collection_name = args.get("collection", "memo")
34
+ sources = input.get("sources", {})
35
+ index_dir = sources.get("index_dir")
36
+
37
+ if not query:
38
+ ctx.log.warn("memo-vector-search: empty query")
39
+ return result_value("skipped", loc.t("No query", "クエリなし"), loc.t("There is no keyword to search.", "検索するキーワードがありません"), [])
40
+ if not index_dir:
41
+ ctx.log.error("memo-vector-search: index_dir not provided")
42
+ return result_value("error", loc.t("Cannot search", "検索できません"), loc.t("index_dir was not provided.", "index_dir が渡されていません"), [])
43
+
44
+ try:
45
+ import chromadb
46
+ from chromadb.utils.embedding_functions import (
47
+ SentenceTransformerEmbeddingFunction,
48
+ )
49
+ except ImportError as e:
50
+ ctx.log.error(f"memo-vector-search: missing dependency: {e}")
51
+ return result_value(
52
+ "error",
53
+ loc.t("chromadb not found", "chromadb が見つかりません"),
54
+ loc.t(
55
+ "Run: python3 -m venv ~/.agent-sin/.venv && ~/.agent-sin/.venv/bin/pip install chromadb sentence-transformers",
56
+ "次を実行: python3 -m venv ~/.agent-sin/.venv && ~/.agent-sin/.venv/bin/pip install chromadb sentence-transformers",
57
+ ),
58
+ [],
59
+ )
60
+
61
+ chroma_path = Path(index_dir).expanduser().resolve() / "local-index" / "chroma"
62
+ if not chroma_path.exists():
63
+ return result_value(
64
+ "ok",
65
+ loc.t("0 matches", "0件"),
66
+ loc.t("The index is empty. Run agent-sin run memo-index first.", "索引が空です。先に agent-sin run memo-index を実行してください"),
67
+ [],
68
+ )
69
+
70
+ ctx.log.info(f"memo-vector-search: query='{query[:40]}' limit={limit}")
71
+
72
+ try:
73
+ ef = SentenceTransformerEmbeddingFunction(model_name=EMBED_MODEL)
74
+ client = chromadb.PersistentClient(path=str(chroma_path))
75
+ col = client.get_or_create_collection(
76
+ name=collection_name, embedding_function=ef
77
+ )
78
+ r = col.query(query_texts=[query], n_results=limit)
79
+ except Exception as e:
80
+ ctx.log.error(f"memo-vector-search: query failed: {e}")
81
+ return result_value("error", loc.t("Search error", "検索エラー"), str(e), [])
82
+
83
+ matches = []
84
+ docs = (r.get("documents") or [[]])[0]
85
+ metas = (r.get("metadatas") or [[]])[0]
86
+ dists = (r.get("distances") or [[]])[0]
87
+ for doc, meta, dist in zip(docs, metas, dists):
88
+ meta = meta or {}
89
+ matches.append(
90
+ {
91
+ "text": doc,
92
+ "file": meta.get("file", ""),
93
+ "line": meta.get("line", 0),
94
+ "timestamp": meta.get("timestamp", ""),
95
+ "distance": float(dist),
96
+ }
97
+ )
98
+
99
+ if not matches:
100
+ return result_value(
101
+ "ok", loc.t("0 matches", "0件"), loc.t(f'No memos are close to "{query}".', f"「{query}」に近いメモは見つかりませんでした"), []
102
+ )
103
+
104
+ lines = [loc.t("Closest memos:", "近いメモ:")]
105
+ 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]}")
108
+ return result_value(
109
+ "ok", loc.t(f"{len(matches)} matches", f"{len(matches)}件見つかりました"), "\n".join(lines), matches
110
+ )
111
+
112
+
113
+ def result_value(status, title, summary, matches):
114
+ return {
115
+ "status": status,
116
+ "title": title,
117
+ "summary": summary,
118
+ "outputs": {},
119
+ "data": {"matches": matches},
120
+ "suggestions": [],
121
+ }