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
@@ -0,0 +1,133 @@
1
+ # Builtin: even-g2-setup
2
+ # Even G2 メガネ用のセットアップを一括で行う。.env 保存 / app.json whitelist 更新 / .ehpk ビルドまで。
3
+
4
+ id: even-g2-setup
5
+ name: Even G2 Setup
6
+ name_i18n:
7
+ en: Even G2 Setup
8
+ ja: Even G2 セットアップ
9
+ description: Set up Even G2 glasses end-to-end (gateway env, whitelist, .ehpk build).
10
+ description_i18n:
11
+ en: Set up Even G2 glasses end-to-end (gateway env, whitelist, .ehpk build).
12
+ ja: Even G2 メガネのセットアップ(gateway 設定・whitelist 更新・.ehpk ビルド)を一括で行う
13
+ runtime: typescript
14
+ entry: main.ts
15
+ handler: run
16
+ output_mode: raw
17
+ side_effect: true
18
+
19
+ invocation:
20
+ command: even.g2.setup
21
+ phrases:
22
+ - G2のセットアップして
23
+ - Even G2をセットアップして
24
+ - G2を使えるようにして
25
+ - メガネ用のアプリを作って
26
+ - G2のehpkを作って
27
+ phrases_i18n:
28
+ en:
29
+ - set up even g2
30
+ - setup g2 glasses
31
+ - build the g2 ehpk
32
+ - prepare even g2 app
33
+ ja:
34
+ - G2のセットアップして
35
+ - Even G2をセットアップして
36
+ - G2を使えるようにして
37
+ - メガネ用のアプリを作って
38
+ - G2のehpkを作って
39
+
40
+ input:
41
+ schema:
42
+ type: object
43
+ additionalProperties: false
44
+ properties:
45
+ package_id:
46
+ type: string
47
+ pattern: "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)+$"
48
+ description: Reverse-DNS package id written into app.json (e.g. com.you.agentsin). Defaults to existing value, then a value derived from git user.name, then com.example.agentsin.
49
+ description_i18n:
50
+ en: Reverse-DNS package id written into app.json (e.g. com.you.agentsin). Defaults to existing value, then a value derived from git user.name, then com.example.agentsin.
51
+ ja: app.json に書き込む reverse-DNS のパッケージ ID(例 com.you.agentsin)。既存値 → git user.name 由来 → com.example.agentsin の順でフォールバック
52
+ history_channel:
53
+ type: string
54
+ enum: [discord, telegram, none, auto]
55
+ description: Where to mirror G2 conversation history.
56
+ description_i18n:
57
+ en: Where to mirror G2 conversation history.
58
+ ja: G2 の会話履歴のミラー先(discord/telegram/none/auto)
59
+ discord_thread:
60
+ type: string
61
+ pattern: "^(auto|off|[0-9]+)$"
62
+ description: "Discord thread handling: auto, off, or numeric thread id."
63
+ description_i18n:
64
+ en: "Discord thread handling: auto, off, or numeric thread id."
65
+ ja: Discord スレッドの扱い(auto/off/スレッドID)
66
+ host:
67
+ type: string
68
+ description: Gateway bind host (defaults to 0.0.0.0).
69
+ description_i18n:
70
+ en: Gateway bind host (defaults to 0.0.0.0).
71
+ ja: gateway のバインドホスト(既定は 0.0.0.0)
72
+ port:
73
+ type: integer
74
+ minimum: 1
75
+ maximum: 65535
76
+ description: Gateway port (defaults to 8765).
77
+ description_i18n:
78
+ en: Gateway port (defaults to 8765).
79
+ ja: gateway のポート(既定は 8765)
80
+ token:
81
+ type: string
82
+ minLength: 8
83
+ description: Gateway token. Auto-generated when omitted.
84
+ description_i18n:
85
+ en: Gateway token. Auto-generated when omitted.
86
+ ja: gateway トークン。省略時は自動生成
87
+ host_lan_ip:
88
+ type: string
89
+ description: This machine's LAN IP added to the whitelist. Auto-detected if omitted.
90
+ description_i18n:
91
+ en: This machine's LAN IP added to the whitelist. Auto-detected if omitted.
92
+ ja: 母艦のLAN IP。省略時は自動検出
93
+ tailscale_host:
94
+ type: string
95
+ description: Tailscale MagicDNS host to add to the whitelist (no scheme, no port).
96
+ description_i18n:
97
+ en: Tailscale MagicDNS host to add to the whitelist (no scheme, no port).
98
+ ja: whitelist に追加する Tailscale MagicDNS ホスト
99
+ server_url:
100
+ type: string
101
+ description: Explicit gateway URL to embed in the .ehpk (overrides auto-derivation). Use this when fronting the gateway via Tailscale Serve or another HTTPS proxy. Also added to the whitelist.
102
+ description_i18n:
103
+ en: Explicit gateway URL to embed in the .ehpk (overrides auto-derivation). Use this when fronting the gateway via Tailscale Serve or another HTTPS proxy. Also added to the whitelist.
104
+ ja: .ehpk に埋め込む gateway URL を明示指定(自動導出を上書き)。Tailscale Serve など HTTPS プロキシ経由で公開している場合に使う。whitelist にも自動追加されます
105
+ extra_hosts:
106
+ type: array
107
+ items:
108
+ type: string
109
+ description: Additional host[:port] entries to add to the whitelist.
110
+ description_i18n:
111
+ en: Additional host[:port] entries to add to the whitelist.
112
+ ja: whitelist に追加したい host[:port] のリスト
113
+ skip_install:
114
+ type: boolean
115
+ default: false
116
+ description: Skip npm install even when node_modules is missing.
117
+ description_i18n:
118
+ en: Skip npm install even when node_modules is missing.
119
+ ja: node_modules が無くても npm install をスキップする
120
+ skip_pack:
121
+ type: boolean
122
+ default: false
123
+ description: Only write .env and app.json; do not build the .ehpk.
124
+ description_i18n:
125
+ en: Only write .env and app.json; do not build the .ehpk.
126
+ ja: .env と app.json の更新だけ行い、.ehpk のビルドはスキップ
127
+ force:
128
+ type: boolean
129
+ default: false
130
+ description: Re-copy template files even when the workspace copy already exists.
131
+ description_i18n:
132
+ en: Re-copy template files even when the workspace copy already exists.
133
+ ja: ワークスペースに既存のコピーがあっても、テンプレを再コピーする
@@ -1,149 +1,70 @@
1
1
  """Builtin: memo-delete
2
2
 
3
- memo-save が書き込むデイリーノートからメモを1件削除する。
4
- バレット行 + 続く 2 スペースインデントの継続行をまとめて取り除く。
3
+ notes/memo/{title}.md を削除する。
4
+ - title が完全一致するファイルがあれば削除
5
+ - 無ければファイル名に部分一致するものを探し、1件なら削除、複数なら曖昧と判定
5
6
  """
6
7
 
7
8
  from __future__ import annotations
8
9
 
9
10
  import os
10
- import re
11
11
  import sys
12
- import tempfile
13
- from datetime import datetime
12
+ from pathlib import Path
14
13
 
15
14
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
16
15
  from i18n import localizer # noqa: E402
17
16
 
18
17
 
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
18
  async def run(ctx, input):
25
19
  loc = localizer(input)
26
20
  args = input.get("args", {}) or {}
27
- workspace = input.get("sources", {}).get("workspace", "")
28
21
  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 のどちらかを指定してください"))
22
+ if not notes_dir:
23
+ return _err(loc.t("Notes unavailable", "ノート不明"), loc.t("notes_dir is unavailable.", "notes_dir が取得できません"))
45
24
 
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} のメモファイルがありません"))
25
+ title = str(args.get("title", "")).strip()
26
+ if not title:
27
+ return _err(loc.t("Title required", "タイトル必須"), loc.t("Specify the memo title.", "タイトルを指定してください"))
50
28
 
51
- with open(path, "r", encoding="utf-8") as f:
52
- raw = f.read()
53
- lines = raw.splitlines(keepends=False)
29
+ memo_dir = Path(notes_dir) / "memo"
30
+ if not memo_dir.exists():
31
+ return _err(loc.t("No memos", "メモなし"), loc.t("There are no memos yet.", "まだメモはありません"))
54
32
 
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]
33
+ exact = memo_dir / f"{title}.md"
34
+ if exact.exists():
35
+ target = exact
64
36
  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
- ]
37
+ lowered = title.lower()
38
+ candidates = [p for p in memo_dir.glob("*.md") if lowered in p.stem.lower()]
69
39
  if not candidates:
70
- return _err(loc.t("Not found", "見つかりません"), loc.t(f'No memo matches "{match}".', f'"{match}" に一致するメモがありません'))
40
+ return _err(loc.t("Not found", "見つかりません"), loc.t(f'No memo matches "{title}".', f'"{title}" に一致するメモがありません'))
71
41
  if len(candidates) > 1:
72
- preview = "\n".join(
73
- f" {idx + 1}. {lines[start]}" for idx, (start, _end) in enumerate(candidates[:5])
74
- )
42
+ preview = "\n".join(f" - {p.stem}" for p in candidates[:5])
75
43
  return _err(
76
44
  loc.t("Ambiguous match", "曖昧です"),
77
- loc.t(f"{len(candidates)} memos matched. Specify index:\n{preview}", f"{len(candidates)} 件一致しました。index で指定してください:\n{preview}"),
45
+ loc.t(f"{len(candidates)} memos matched. Specify a more exact title:\n{preview}", f"{len(candidates)} 件一致しました。より正確なタイトルで指定してください:\n{preview}"),
78
46
  )
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")
47
+ target = candidates[0]
89
48
 
90
49
  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}"))
50
+ target.unlink()
51
+ except OSError as e:
52
+ return _err(loc.t("Delete failed", "削除失敗"), loc.t(f"Failed to delete {target}: {e}", f"{target} の削除に失敗しました: {e}"))
94
53
 
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
54
+ ctx.log.info(f"memo-delete: removed {target}")
101
55
  return {
102
56
  "status": "ok",
103
57
  "title": loc.t("Deleted", "削除"),
104
- "summary": loc.t(f"Deleted memo from {date_str}: {preview}", f"{date_str} のメモを削除しました: {preview}"),
58
+ "summary": loc.t(f"Deleted memo: {target.stem}", f"メモを削除しました: {target.stem}"),
105
59
  "outputs": {},
106
60
  "data": {
107
- "date": date_str,
108
- "removed_lines": removed_lines,
109
- "remaining_memos": remaining,
110
- "path": path,
61
+ "title": target.stem,
62
+ "file": str(target.relative_to(notes_dir)),
111
63
  },
112
64
  "suggestions": [],
113
65
  }
114
66
 
115
67
 
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
68
  def _err(title, summary):
148
69
  return {
149
70
  "status": "error",
@@ -1,15 +1,15 @@
1
1
  # Builtin: memo-delete
2
- # デイリーノートの memo 行を削除する。memo-save と対称。
2
+ # notes/memo/{title}.md を削除する。memo-save と対称。
3
3
 
4
4
  id: memo-delete
5
5
  name: Memo Delete
6
6
  name_i18n:
7
7
  en: Memo Delete
8
8
  ja: メモ削除
9
- description: メモの行を削除する
9
+ description: タイトル指定でメモファイルを削除する
10
10
  description_i18n:
11
- en: Delete a memo line from the daily note
12
- ja: メモの行を削除する
11
+ en: Delete a memo file by title
12
+ ja: タイトル指定でメモファイルを削除する
13
13
  runtime: python
14
14
  output_mode: raw
15
15
  side_effect: true
@@ -35,23 +35,12 @@ input:
35
35
  type: object
36
36
  additionalProperties: false
37
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:
38
+ title:
45
39
  type: string
46
40
  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始まり番号で特定する
41
+ description: 削除するメモのタイトル(完全一致または部分一致)
55
42
  description_i18n:
56
- en: 1-based memo line number for the target day
57
- ja: 該当日メモ行の1始まり番号で特定する
43
+ en: Title of the memo to delete (exact or partial match)
44
+ ja: 削除するメモのタイトル(完全一致または部分一致)
45
+ required:
46
+ - title
@@ -1,22 +1,20 @@
1
1
  """Builtin: memo-index
2
2
 
3
- notes_dir 配下の Markdown を読んで、各 "- {timestamp} {text}" 行を
4
- Chroma の collection (default: memo) に upsert する。
3
+ notes/memo/*.md を1ファイル=1ドキュメントとして Chroma collection upsert する。
5
4
 
6
5
  入力:
7
- args.since: ISO8601 文字列 (この時刻より古いメモはスキップ、任意)
8
6
  args.collection: 索引コレクション名 (default: memo)
9
7
 
10
8
  出力:
11
- data: {added, skipped, total, collection}
9
+ data: {added, updated, skipped, total, collection}
12
10
  """
13
11
 
14
12
  from __future__ import annotations
15
13
 
16
14
  import hashlib
17
15
  import os
16
+ import re
18
17
  import sys
19
- from datetime import datetime
20
18
  from pathlib import Path
21
19
 
22
20
  sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
@@ -24,12 +22,13 @@ from i18n import localizer # noqa: E402
24
22
 
25
23
 
26
24
  EMBED_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
25
+ FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?", re.DOTALL)
26
+ MAX_DOC_CHARS = 8000
27
27
 
28
28
 
29
29
  async def run(ctx, input):
30
30
  loc = localizer(input)
31
31
  args = input.get("args", {})
32
- since_str = args.get("since")
33
32
  collection_name = args.get("collection", "memo")
34
33
  sources = input.get("sources", {})
35
34
  notes_dir = sources.get("notes_dir")
@@ -59,13 +58,13 @@ async def run(ctx, input):
59
58
  chroma_path = Path(index_dir).expanduser().resolve() / "local-index" / "chroma"
60
59
  chroma_path.mkdir(parents=True, exist_ok=True)
61
60
 
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")
61
+ memo_dir = Path(notes_dir).expanduser().resolve() / "memo"
62
+ if not memo_dir.exists():
63
+ ctx.log.info("memo-index: memo dir does not exist yet")
65
64
  return result_ok(
66
65
  loc.t("Nothing to index", "索引化対象なし"),
67
66
  loc.t("There are no memos yet.", "まだメモがありません"),
68
- {"added": 0, "skipped": 0, "total": 0, "collection": collection_name},
67
+ {"added": 0, "updated": 0, "skipped": 0, "total": 0, "collection": collection_name},
69
68
  )
70
69
 
71
70
  try:
@@ -80,66 +79,73 @@ async def run(ctx, input):
80
79
  client = chromadb.PersistentClient(path=str(chroma_path))
81
80
  col = client.get_or_create_collection(name=collection_name, embedding_function=ef)
82
81
 
82
+ existing = {}
83
83
  try:
84
- existing_ids = set(col.get()["ids"])
84
+ snapshot = col.get()
85
+ for doc_id, meta in zip(snapshot.get("ids", []), snapshot.get("metadatas", [])):
86
+ existing[doc_id] = meta or {}
85
87
  except Exception:
86
- existing_ids = set()
88
+ existing = {}
87
89
 
88
- since = parse_iso(since_str) if since_str else None
89
- total = added = skipped = 0
90
- documents, ids, metadatas = [], [], []
90
+ notes_root = Path(notes_dir).expanduser().resolve()
91
+ add_docs, add_ids, add_metas = [], [], []
92
+ upd_docs, upd_ids, upd_metas = [], [], []
93
+ total = added = updated = skipped = 0
91
94
 
92
- for md in sorted(notes_root.rglob("*.md")):
95
+ for md in sorted(memo_dir.glob("*.md")):
93
96
  try:
94
- relative = md.relative_to(notes_root)
95
- except ValueError:
97
+ raw = md.read_text(encoding="utf-8", errors="ignore")
98
+ except OSError:
96
99
  continue
97
- # reports サブディレクトリは出力先なので索引対象外
98
- if "reports" in relative.parts:
100
+ total += 1
101
+ fm, body = split_frontmatter(raw)
102
+ body_clean = body.strip()
103
+ if not body_clean:
104
+ skipped += 1
99
105
  continue
100
- try:
101
- lines = md.read_text(encoding="utf-8", errors="ignore").splitlines()
102
- except OSError:
106
+ if len(body_clean) > MAX_DOC_CHARS:
107
+ body_clean = body_clean[:MAX_DOC_CHARS]
108
+
109
+ file_rel = str(md.relative_to(notes_root))
110
+ doc_id = sha1(file_rel)
111
+ meta = {
112
+ "file": file_rel,
113
+ "title": md.stem,
114
+ "tags": ", ".join(_as_str_list(fm.get("tags"))),
115
+ "created": _as_str(fm.get("created")),
116
+ "updated": _as_str(fm.get("updated")) or _as_str(fm.get("created")),
117
+ }
118
+
119
+ prev = existing.get(doc_id)
120
+ if prev and prev.get("updated") == meta["updated"]:
121
+ skipped += 1
103
122
  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
- )
123
+ if prev:
124
+ upd_docs.append(body_clean)
125
+ upd_ids.append(doc_id)
126
+ upd_metas.append(meta)
127
+ updated += 1
128
+ else:
129
+ add_docs.append(body_clean)
130
+ add_ids.append(doc_id)
131
+ add_metas.append(meta)
132
132
  added += 1
133
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)
134
+ if add_docs:
135
+ ctx.log.info(f"memo-index: adding {len(add_docs)} new docs to {collection_name}")
136
+ col.add(documents=add_docs, ids=add_ids, metadatas=add_metas)
137
+ if upd_docs:
138
+ ctx.log.info(f"memo-index: updating {len(upd_docs)} docs in {collection_name}")
139
+ col.update(documents=upd_docs, ids=upd_ids, metadatas=upd_metas)
137
140
 
138
- summary = loc.t(f"Added {added} / skipped {skipped} / found {total}", f"追加 {added} / スキップ {skipped} / 検出 {total}")
141
+ summary = loc.t(
142
+ f"Added {added} / updated {updated} / skipped {skipped} / found {total}",
143
+ f"追加 {added} / 更新 {updated} / スキップ {skipped} / 検出 {total}",
144
+ )
139
145
  return result_ok(
140
- loc.t(f"Indexed {added} entries", f"{added}件を索引化しました"),
146
+ loc.t(f"Indexed {added + updated} entries", f"{added + updated}件を索引化しました"),
141
147
  summary,
142
- {"added": added, "skipped": skipped, "total": total, "collection": collection_name},
148
+ {"added": added, "updated": updated, "skipped": skipped, "total": total, "collection": collection_name},
143
149
  )
144
150
 
145
151
 
@@ -147,13 +153,39 @@ def sha1(value):
147
153
  return hashlib.sha1(value.encode("utf-8")).hexdigest()
148
154
 
149
155
 
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
156
+ def split_frontmatter(raw: str):
157
+ match = FRONTMATTER_RE.match(raw)
158
+ if not match:
159
+ return {}, raw
160
+ fm = {}
161
+ for line in match.group(1).splitlines():
162
+ if not line.strip() or ":" not in line:
163
+ continue
164
+ key, _, val = line.partition(":")
165
+ v = val.strip()
166
+ if v.startswith("[") and v.endswith("]"):
167
+ inner = v[1:-1]
168
+ items = [item.strip().strip("\"'") for item in inner.split(",") if item.strip()]
169
+ fm[key.strip()] = items
170
+ else:
171
+ if (v.startswith("\"") and v.endswith("\"")) or (v.startswith("'") and v.endswith("'")):
172
+ v = v[1:-1]
173
+ fm[key.strip()] = v
174
+ return fm, raw[match.end():]
175
+
176
+
177
+ def _as_str(value):
178
+ if value is None:
179
+ return ""
180
+ return str(value)
181
+
182
+
183
+ def _as_str_list(value):
184
+ if value is None:
185
+ return []
186
+ if isinstance(value, list):
187
+ return [str(v) for v in value]
188
+ return [str(value)]
157
189
 
158
190
 
159
191
  def result_ok(title, summary, data):
@@ -1,5 +1,5 @@
1
1
  # Builtin: memo-index
2
- # notes_dir 配下のメモ行を Chroma ベクター DB に索引化する。
2
+ # notes/memo/*.md を1ファイル=1ドキュメントとして Chroma ベクター DB に索引化する。
3
3
  # 索引は ~/.agent-sin/index/local-index/chroma に永続化、collection は memo (デフォルト)。
4
4
  # 多言語対応のため SentenceTransformer (paraphrase-multilingual-MiniLM-L12-v2) を使う。
5
5
  # Python 依存: chromadb, sentence-transformers
@@ -12,10 +12,10 @@ name: Memo Index
12
12
  name_i18n:
13
13
  en: Memo Index
14
14
  ja: メモ索引化
15
- description: notes_dirのメモをChromaベクターDBに索引化する
15
+ description: notes/memo配下のメモをChromaベクターDBに索引化する
16
16
  description_i18n:
17
- en: Index notes_dir memos into a Chroma vector database
18
- ja: notes_dirのメモをChromaベクターDBに索引化する
17
+ en: Index notes/memo files into a Chroma vector database
18
+ ja: notes/memo配下のメモをChromaベクターDBに索引化する
19
19
  runtime: python
20
20
 
21
21
  invocation:
@@ -36,12 +36,6 @@ input:
36
36
  type: object
37
37
  additionalProperties: false
38
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
39
  collection:
46
40
  type: string
47
41
  default: memo