agent-sin 0.1.12 → 0.1.16
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 +79 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +126 -72
- package/builtin-skills/memo-list/skill.yaml +8 -14
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +9 -3
- package/dist/core/chat-engine.js +1263 -146
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +596 -18
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +1 -0
- package/dist/discord/bot.js +181 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +115 -7
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,85 @@ See the [compatibility policy](https://agent.shingoirie.com/versioning) for deta
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## [0.1.16] — 2026-05-24
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Long-term memory promotion now treats casual conversation as a source of meta-level user understanding, extracting durable preferences, concerns, lifestyle patterns, and decision criteria without storing raw small talk.
|
|
23
|
+
- Memory consolidation keeps broad health and self-improvement concerns when useful, while continuing to avoid detailed sensitive health, finance, family, or third-party information.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `memory.md` consolidation now preserves the generated `Recent 7-day topics` / `直近1週間のトピック` section instead of dropping previously retained days when the long-term body is reorganized.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [0.1.15] — 2026-05-23
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`skill_outputs_dir` (`workspace/skill-outputs/<skill-id>/`).** Every skill now has a dedicated user-visible output directory, symlinked into the Obsidian Vault as `06 Skills/<skill-id>/`. Skills receive the path via `input.sources.skill_output_dir` / `skillOutputDir` and can write free-form Markdown without polluting the workspace root.
|
|
36
|
+
- **Image attachments propagate to the bot UIs.** Generated images and file paths returned from a skill's `data.filePaths` / `data.images` / etc. are forwarded to the Discord and Telegram bots through `onLocalAttachments`, so they appear inline in chat.
|
|
37
|
+
- **Memo flat-structure migration.** Memos created under date subdirectories are automatically migrated to the new flat layout on `setup`, and `memo-list` / `memo-search` index both layouts.
|
|
38
|
+
- **Skill history via `ctx.history`.** Per-skill SQLite stores accumulate at `data/<skill-id>.db`, giving long-running skills (e.g. `nightly-topic-knowledge`) a writable log they can read on the next run.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Chat output rule: reply OR skill output, never both.** When the model writes a reply, the buffered skill output for that turn is dropped (no duplication). When the model leaves reply empty, the skill output passes through as-is. Intermediate iterations are discarded automatically when chaining skills.
|
|
43
|
+
- Self-repair flow now hands the failed run log (including `ctx_logs`) to the builder, so the rewrite has access to the actual stack trace and warn/error entries from the failing run.
|
|
44
|
+
- Skill results now include `ctx_logs` and `notes` so the model and the user can see degraded paths (e.g. cached fallback) instead of silent successes.
|
|
45
|
+
- Builtin memo / todo / nightly-topic-knowledge skills migrated to the new `skill-outputs` layout and the unified `ctx.history` API.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- `runSkill` no longer fails when the workspace lacks `skill-outputs/<skill-id>/`; the directory is created on demand.
|
|
50
|
+
- The update banner regex test is now locale-agnostic and works whether the host runs in Japanese or English.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## [0.1.14] — 2026-05-21
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **Two-way ToDo sync with Obsidian.** `workspace/todo/todos.md` is now the single source of truth. Toggling `- [ ] / - [x]`, adding lines, or deleting lines from the Obsidian `05 ToDo` Vault is picked up on the next todo skill run. `^id-xxxx` block anchors keep each line bound to its underlying ToDo id.
|
|
59
|
+
- **Automatic past-conversation context.** When the user references prior conversations ("覚えてる?", "yesterday's …", etc.), relevant excerpts from `logs/conversations/*.jsonl` are attached as read-only context. Date references also include the surrounding days.
|
|
60
|
+
- **Auto Obsidian tags for memos.** `memo-save` now derives up to three Obsidian tags from the memo body, explicit `tags`, and inline `#hashtag` mentions. Controlled via `tags` / `auto_tags` arguments.
|
|
61
|
+
- **memory.md promotion diff log.** Whenever daily-memory promotion rewrites `memory.md`, the unified diff is recorded as a `memory_promotion_diff` event in `logs/events.jsonl`.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- Memo timestamps switched from full ISO strings to a short local `HH:MM` display. `memo-index` reads both the old and new formats, and daily-memory headings are unified to `## HH:MM chat user`.
|
|
66
|
+
- Long-term memory promotion budget raised from 3 → 5 items, and the recent-topics list from 8 → 10 entries. Prompts are stricter so assistant turns serve as context only and are no longer treated as standalone facts to promote.
|
|
67
|
+
- `build_suggestion` replies now strip leaked internal phrasing such as "ビルドモードに渡します" / "build mode" and always end with a short summary plus a confirmation line ("この内容で直しますか?" / "この内容で作りますか?"). When the very first build request also describes a schedule (e.g. "毎朝4時の…"), the recurring schedule is registered alongside the build draft.
|
|
68
|
+
- `schedule-add` is now idempotent against content duplicates: the same cron/skill/args/description combination cannot be registered twice under different ids.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- Obsidian Vault symlinks (e.g. `05 ToDo`) that pointed at a missing target are now rebuilt on `setup` instead of being left dangling.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## [0.1.13] — 2026-05-19
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Even G2 glasses integration.** Talk to your agent through Even Realities G2 glasses via a new on-device gateway.
|
|
81
|
+
- `agent-sin g2` / `agent-sin g2 setup` CLI commands and a `--g2` flag on `agent-sin gateway` to run the WebSocket+HTTP bridge that handles voice transcription, chat replies, and inbox.
|
|
82
|
+
- `templates/even-g2-agent/` — minimal Even Hub app template (Vite + Even SDK) with a glasses UI that supports Talk / Send / Cancel and on-glasses transcript display.
|
|
83
|
+
- `even-g2-setup` builtin skill that automates the full local setup: gateway `.env`, template copy into `~/.agent-sin/even-g2-agent/`, `app.json` whitelist generation, build-time embedding of the server URL and token, and `npm run pack` to produce a ready-to-upload `.ehpk`.
|
|
84
|
+
- Tailscale / Tailscale Serve detection: the skill refuses to build with helpful instructions when Tailscale is required but missing or misconfigured.
|
|
85
|
+
- Discord / Telegram mirror of G2 conversations via `--history-channel` (with optional auto-created Discord thread).
|
|
86
|
+
- End-to-end docs at [`docs/even-g2-setup.md`](docs/even-g2-setup.md) (English) and [`docs/even-g2-setup.ja.md`](docs/even-g2-setup.ja.md) (Japanese).
|
|
87
|
+
- **`service-restart` builtin skill.** Restart the Agent-Sin background service from Discord, Telegram, or chat. Supports launchd on macOS and Task Scheduler on Windows.
|
|
88
|
+
- **Nightly topic knowledge.** A new `nightly-topic-knowledge` skill distills the previous day's conversations, daily memory, and notes into per-topic lightweight knowledge files. A companion `topic-knowledge-read` skill lets the chat engine pull full details for topics that look relevant during a conversation.
|
|
89
|
+
- **Obsidian Vault view.** `agent-sin setup` now provisions an Obsidian Vault at `~/Obsidian/Agent-Sin/` so memory, notes, and daily logs can be read and edited directly in Obsidian without changing the underlying workspace layout.
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- Setup and the always-on service understand the new G2 environment variables (`AGENT_SIN_G2_ENABLED`, `_HOST`, `_PORT`, `_TOKEN`, `_HISTORY_CHANNEL`, `_DISCORD_THREAD`) and start G2 automatically when enabled.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
18
97
|
## [0.1.12] — 2026-05-15
|
|
19
98
|
|
|
20
99
|
### Added
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ When a new capability is needed, Build Mode uses Claude Code or Codex to generat
|
|
|
41
41
|
- **Free model mix** — pick a light model for chat and a stronger one for skill authoring.
|
|
42
42
|
- **Always-on gateway** — starts at login and bundles the scheduler with the Discord / Telegram bots.
|
|
43
43
|
- **Long-term memory** — agent persona, your profile, and daily context persist across sessions.
|
|
44
|
+
- **Obsidian-ready** — setup generates an [Obsidian Vault view](https://agent.shingoirie.com/concepts?lang=en#obsidian-vault) at `~/Obsidian/Agent-Sin/` so you can read and edit memory, notes, and daily logs directly in Obsidian.
|
|
44
45
|
|
|
45
46
|
## Install
|
|
46
47
|
|
|
@@ -68,7 +69,7 @@ Full walkthrough: [Getting Started](https://agent.shingoirie.com/getting-started
|
|
|
68
69
|
|
|
69
70
|
- [Overview](https://agent.shingoirie.com/overview?lang=en) — the big picture
|
|
70
71
|
- [Getting Started](https://agent.shingoirie.com/getting-started?lang=en) — install to first conversation
|
|
71
|
-
- [Concepts](https://agent.shingoirie.com/concepts?lang=en) — design and
|
|
72
|
+
- [Concepts](https://agent.shingoirie.com/concepts?lang=en) — design, data layout, and the [Obsidian Vault view](https://agent.shingoirie.com/concepts?lang=en#obsidian-vault)
|
|
72
73
|
- [Skill Authoring](https://agent.shingoirie.com/skill-authoring?lang=en) — write your own skill
|
|
73
74
|
- [Built-in Skills](https://agent.shingoirie.com/built-in-skills?lang=en) — bundled skills
|
|
74
75
|
- [CLI Reference](https://agent.shingoirie.com/cli?lang=en) — every command
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""ToDo sync helpers shared by todo-add/done/delete/list/tick.
|
|
2
|
+
|
|
3
|
+
todos.md は workspace/todo/todos.md に単一ファイルで保存する。
|
|
4
|
+
Obsidian の `05 ToDo` symlink からそのまま編集できる。
|
|
5
|
+
|
|
6
|
+
同期方針 (Obsidian 優先):
|
|
7
|
+
- スキル実行のたびに sync_todos() を呼ぶ
|
|
8
|
+
- todos.md に行があるが items に無い → 新規追加 (id を採番)
|
|
9
|
+
- items にあるが todos.md に無い → 削除
|
|
10
|
+
- checkbox の状態 (open/done) は todos.md を真実とする
|
|
11
|
+
- text や due (📅) も todos.md 側を優先
|
|
12
|
+
|
|
13
|
+
ファイル書式:
|
|
14
|
+
- [ ] 牛乳を買う 📅 2026-05-22 ^id-abc
|
|
15
|
+
- [x] PR レビュー ✅ 2026-05-21 ^id-xyz
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import secrets
|
|
23
|
+
import tempfile
|
|
24
|
+
from datetime import datetime, timedelta, timezone
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DONE_RETENTION_DAYS = 90
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def todos_path(workspace: str) -> str:
|
|
32
|
+
return os.path.join(workspace, "todo", "todos.md")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# `^anchor-id` パターン (Obsidian のブロック参照と同じ記法)
|
|
36
|
+
ANCHOR_RE = re.compile(r"\s*\^([A-Za-z0-9_-]+)\s*$")
|
|
37
|
+
DUE_RE = re.compile(r"\s*📅\s*([0-9T:\-+Z.]+)")
|
|
38
|
+
DONE_RE = re.compile(r"\s*✅\s*([0-9T:\-+Z.]+)")
|
|
39
|
+
CHECKBOX_RE = re.compile(r"^\s*-\s*\[([ xX])\]\s*(.*)$")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_todos_md(content: str) -> List[Dict[str, Any]]:
|
|
43
|
+
"""Markdown から todo 一覧を取り出す。順序は保つ。"""
|
|
44
|
+
items: List[Dict[str, Any]] = []
|
|
45
|
+
for line in content.splitlines():
|
|
46
|
+
match = CHECKBOX_RE.match(line)
|
|
47
|
+
if not match:
|
|
48
|
+
continue
|
|
49
|
+
mark, body = match.group(1), match.group(2)
|
|
50
|
+
status = "done" if mark.strip().lower() == "x" else "open"
|
|
51
|
+
|
|
52
|
+
anchor: Optional[str] = None
|
|
53
|
+
anchor_match = ANCHOR_RE.search(body)
|
|
54
|
+
if anchor_match:
|
|
55
|
+
anchor = anchor_match.group(1)
|
|
56
|
+
body = body[: anchor_match.start()].rstrip()
|
|
57
|
+
|
|
58
|
+
due: Optional[str] = None
|
|
59
|
+
due_match = DUE_RE.search(body)
|
|
60
|
+
if due_match:
|
|
61
|
+
due = due_match.group(1)
|
|
62
|
+
body = (body[: due_match.start()] + body[due_match.end():]).strip()
|
|
63
|
+
|
|
64
|
+
completed_at: Optional[str] = None
|
|
65
|
+
done_match = DONE_RE.search(body)
|
|
66
|
+
if done_match:
|
|
67
|
+
completed_at = done_match.group(1)
|
|
68
|
+
body = (body[: done_match.start()] + body[done_match.end():]).strip()
|
|
69
|
+
|
|
70
|
+
text = body.strip()
|
|
71
|
+
if not text:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
item: Dict[str, Any] = {"text": text, "status": status}
|
|
75
|
+
if anchor:
|
|
76
|
+
item["id"] = anchor
|
|
77
|
+
if due:
|
|
78
|
+
item["due"] = due
|
|
79
|
+
if completed_at:
|
|
80
|
+
item["completed_at"] = completed_at
|
|
81
|
+
items.append(item)
|
|
82
|
+
return items
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def format_todos_md(items: List[Dict[str, Any]]) -> str:
|
|
86
|
+
"""items 配列のうち open のみを todos.md に書き出す。
|
|
87
|
+
|
|
88
|
+
Done は Obsidian に出さない (内部メモリには残るが、見た目を軽くするため)。
|
|
89
|
+
完了タスクの履歴を Obsidian 側で確認したい場合は別ビューを検討する。
|
|
90
|
+
"""
|
|
91
|
+
open_items = [i for i in items if i.get("status") != "done"]
|
|
92
|
+
if not open_items:
|
|
93
|
+
return "# ToDo\n\n_(まだ ToDo はありません)_\n"
|
|
94
|
+
lines = ["# ToDo", ""]
|
|
95
|
+
for item in open_items:
|
|
96
|
+
lines.append(_format_line(item))
|
|
97
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _format_line(item: Dict[str, Any]) -> str:
|
|
101
|
+
mark = "x" if item.get("status") == "done" else " "
|
|
102
|
+
text = str(item.get("text", "")).strip()
|
|
103
|
+
parts = [f"- [{mark}] {text}"]
|
|
104
|
+
due = item.get("due")
|
|
105
|
+
if due:
|
|
106
|
+
parts.append(f"📅 {due}")
|
|
107
|
+
completed = item.get("completed_at")
|
|
108
|
+
if completed:
|
|
109
|
+
parts.append(f"✅ {completed}")
|
|
110
|
+
item_id = item.get("id")
|
|
111
|
+
if item_id:
|
|
112
|
+
parts.append(f"^{item_id}")
|
|
113
|
+
return " ".join(parts)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _read_file(path: str) -> str:
|
|
117
|
+
try:
|
|
118
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
119
|
+
return fh.read()
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _write_atomic(path: str, content: str) -> None:
|
|
125
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
126
|
+
fd, tmp = tempfile.mkstemp(prefix=".todos.", suffix=".md", dir=os.path.dirname(path))
|
|
127
|
+
try:
|
|
128
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
129
|
+
fh.write(content)
|
|
130
|
+
os.replace(tmp, path)
|
|
131
|
+
except Exception:
|
|
132
|
+
try:
|
|
133
|
+
os.unlink(tmp)
|
|
134
|
+
except OSError:
|
|
135
|
+
pass
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _new_id() -> str:
|
|
140
|
+
return secrets.token_hex(3)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def merge_obsidian_into_memory(
|
|
144
|
+
mem_items: List[Dict[str, Any]],
|
|
145
|
+
file_items: List[Dict[str, Any]],
|
|
146
|
+
now_iso: str,
|
|
147
|
+
) -> List[Dict[str, Any]]:
|
|
148
|
+
"""Obsidian (file_items) と memory をマージする。
|
|
149
|
+
|
|
150
|
+
Obsidian は Open のみを表示する仕様なので、file_items に無い open は「Obsidian で削除された」と判定して削除する。
|
|
151
|
+
一方 done はファイルに出ていないだけなので、memory 側の done は file に無くても保持する。
|
|
152
|
+
|
|
153
|
+
- file_items に出ている行: 内容を file 優先で取り込む (id 採番、text/status/due/completed_at の上書き)
|
|
154
|
+
- file_items に出ていない memory open: 削除
|
|
155
|
+
- file_items に出ていない memory done: 保持 (後段の purge_old_done で古いものだけ落とす)
|
|
156
|
+
"""
|
|
157
|
+
mem_by_id: Dict[str, Dict[str, Any]] = {}
|
|
158
|
+
for item in mem_items:
|
|
159
|
+
item_id = item.get("id")
|
|
160
|
+
if item_id:
|
|
161
|
+
mem_by_id[item_id] = item
|
|
162
|
+
|
|
163
|
+
file_ids_seen: set = set()
|
|
164
|
+
merged: List[Dict[str, Any]] = []
|
|
165
|
+
|
|
166
|
+
for file_item in file_items:
|
|
167
|
+
file_id = file_item.get("id")
|
|
168
|
+
base: Dict[str, Any]
|
|
169
|
+
if file_id and file_id in mem_by_id:
|
|
170
|
+
base = dict(mem_by_id[file_id])
|
|
171
|
+
base["text"] = file_item["text"]
|
|
172
|
+
new_status = file_item.get("status", "open")
|
|
173
|
+
old_status = base.get("status")
|
|
174
|
+
base["status"] = new_status
|
|
175
|
+
if "due" in file_item:
|
|
176
|
+
base["due"] = file_item["due"]
|
|
177
|
+
else:
|
|
178
|
+
base.pop("due", None)
|
|
179
|
+
if file_item.get("completed_at"):
|
|
180
|
+
base["completed_at"] = file_item["completed_at"]
|
|
181
|
+
elif new_status == "done" and old_status != "done":
|
|
182
|
+
base["completed_at"] = now_iso
|
|
183
|
+
elif new_status != "done":
|
|
184
|
+
base.pop("completed_at", None)
|
|
185
|
+
file_ids_seen.add(file_id)
|
|
186
|
+
else:
|
|
187
|
+
base = {
|
|
188
|
+
"id": file_id or _new_id(),
|
|
189
|
+
"text": file_item["text"],
|
|
190
|
+
"status": file_item.get("status", "open"),
|
|
191
|
+
"created_at": now_iso,
|
|
192
|
+
}
|
|
193
|
+
if file_item.get("due"):
|
|
194
|
+
base["due"] = file_item["due"]
|
|
195
|
+
if base["status"] == "done":
|
|
196
|
+
base["completed_at"] = file_item.get("completed_at") or now_iso
|
|
197
|
+
if base.get("id"):
|
|
198
|
+
file_ids_seen.add(base["id"])
|
|
199
|
+
merged.append(base)
|
|
200
|
+
|
|
201
|
+
# ファイルに出ていない memory 側 done を末尾に残す (open はファイル側で削除されたとみなして落とす)
|
|
202
|
+
for mem_item in mem_items:
|
|
203
|
+
if mem_item.get("status") != "done":
|
|
204
|
+
continue
|
|
205
|
+
mem_id = mem_item.get("id")
|
|
206
|
+
if mem_id and mem_id in file_ids_seen:
|
|
207
|
+
continue
|
|
208
|
+
merged.append(dict(mem_item))
|
|
209
|
+
|
|
210
|
+
return merged
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _parse_iso(value: Any) -> Optional[datetime]:
|
|
214
|
+
if not value:
|
|
215
|
+
return None
|
|
216
|
+
try:
|
|
217
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
218
|
+
except ValueError:
|
|
219
|
+
return None
|
|
220
|
+
if parsed.tzinfo is None:
|
|
221
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
222
|
+
return parsed
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def purge_old_done(items: List[Dict[str, Any]], now: Optional[datetime] = None) -> List[Dict[str, Any]]:
|
|
226
|
+
"""completed_at が DONE_RETENTION_DAYS を超えた done items を内部メモリから消す。"""
|
|
227
|
+
reference = now or datetime.now(timezone.utc)
|
|
228
|
+
if reference.tzinfo is None:
|
|
229
|
+
reference = reference.replace(tzinfo=timezone.utc)
|
|
230
|
+
cutoff = reference - timedelta(days=DONE_RETENTION_DAYS)
|
|
231
|
+
kept: List[Dict[str, Any]] = []
|
|
232
|
+
for item in items:
|
|
233
|
+
if item.get("status") == "done":
|
|
234
|
+
completed = _parse_iso(item.get("completed_at"))
|
|
235
|
+
if completed is not None and completed < cutoff:
|
|
236
|
+
continue
|
|
237
|
+
kept.append(item)
|
|
238
|
+
return kept
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def sync_todos(ctx, input: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
242
|
+
"""スキル実行の冒頭で呼ぶ。Obsidian → memory に取り込んで items を返す。
|
|
243
|
+
|
|
244
|
+
read-only スキル (memory.write: false) では memory への書き込みが拒否されるため、
|
|
245
|
+
その場合はマージ結果だけ返して memory は更新しない (次の write 可能スキルで永続化される)。
|
|
246
|
+
"""
|
|
247
|
+
workspace = (input.get("sources") or {}).get("workspace")
|
|
248
|
+
now_iso = (input.get("trigger", {}) or {}).get("time") or datetime.now().isoformat()
|
|
249
|
+
now_dt = _parse_iso(now_iso)
|
|
250
|
+
|
|
251
|
+
if not workspace:
|
|
252
|
+
return purge_old_done(list((await ctx.memory.get("items")) or []), now_dt)
|
|
253
|
+
|
|
254
|
+
path = todos_path(workspace)
|
|
255
|
+
file_content = _read_file(path)
|
|
256
|
+
mem_items = list((await ctx.memory.get("items")) or [])
|
|
257
|
+
|
|
258
|
+
if not file_content.strip():
|
|
259
|
+
# ファイルが無いか空 → memory が真実。初回保存のため後段で flush される。
|
|
260
|
+
return purge_old_done(mem_items, now_dt)
|
|
261
|
+
|
|
262
|
+
file_items = parse_todos_md(file_content)
|
|
263
|
+
merged = merge_obsidian_into_memory(mem_items, file_items, now_iso)
|
|
264
|
+
merged = purge_old_done(merged, now_dt)
|
|
265
|
+
try:
|
|
266
|
+
await ctx.memory.set("items", merged)
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
# read-only スキルでの書き込み拒否は想定内。それ以外もブロックしない。
|
|
269
|
+
try:
|
|
270
|
+
ctx.log.info(f"todo sync: memory write skipped ({exc})")
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
return merged
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def flush_todos(ctx, input: Dict[str, Any], items: List[Dict[str, Any]]) -> Optional[str]:
|
|
277
|
+
"""items を todos.md にフルダンプする。書き込み先パスを返す (workspaceが取れなければ None)。"""
|
|
278
|
+
workspace = (input.get("sources") or {}).get("workspace")
|
|
279
|
+
if not workspace:
|
|
280
|
+
return None
|
|
281
|
+
path = todos_path(workspace)
|
|
282
|
+
_write_atomic(path, format_todos_md(items))
|
|
283
|
+
return path
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def sync_and_get(ctx, input: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
287
|
+
"""sync_todos の薄いラッパー。同期後の items と「同期前の memory items」を返す。"""
|
|
288
|
+
mem_items_before = list((await ctx.memory.get("items")) or [])
|
|
289
|
+
merged = await sync_todos(ctx, input)
|
|
290
|
+
return merged, mem_items_before
|