agent-sin 0.1.12 → 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.
- package/CHANGELOG.md +66 -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 +568 -14
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Builtin: memo-list
|
|
2
2
|
|
|
3
|
-
memo
|
|
4
|
-
|
|
3
|
+
notes/memo/*.md を更新日時の新しい順で列挙する。
|
|
4
|
+
タイトル(=ファイル名のstem)+本文先頭60文字+更新日時を返す。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -10,15 +10,14 @@ import os
|
|
|
10
10
|
import re
|
|
11
11
|
import sys
|
|
12
12
|
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
|
|
15
16
|
from i18n import localizer # noqa: E402
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
_CONTINUATION_RE = re.compile(r"^ \S")
|
|
21
|
-
_MEMO_HEAD_RE = re.compile(r"^-\s+(?:\d{4}-\d{2}-\d{2}T\S+\s+)?")
|
|
19
|
+
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?", re.DOTALL)
|
|
20
|
+
HASH_TAG_LINE_RE = re.compile(r"^\s*(?:#\S+\s*)+$")
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
async def run(ctx, input):
|
|
@@ -28,87 +27,142 @@ async def run(ctx, input):
|
|
|
28
27
|
if not notes_dir:
|
|
29
28
|
return _err(loc.t("Notes unavailable", "ノート不明"), loc.t("notes_dir is unavailable.", "notes_dir が取得できません"))
|
|
30
29
|
|
|
31
|
-
date_str = str(args.get("date", "")).strip()
|
|
32
|
-
if not date_str:
|
|
33
|
-
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
34
|
-
if not _DATE_RE.match(date_str):
|
|
35
|
-
return _err(loc.t("Invalid date", "日付不正"), loc.t("Use YYYY-MM-DD for date.", "date は YYYY-MM-DD 形式で指定してください"))
|
|
36
|
-
|
|
37
30
|
limit = int(args.get("limit", 20))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
memo_dir = Path(notes_dir) / "memo"
|
|
32
|
+
if not memo_dir.exists():
|
|
33
|
+
return _ok(loc.t("No memos", "メモなし"), loc.t("There are no memos yet.", "まだメモはありません"), [])
|
|
34
|
+
|
|
35
|
+
entries = []
|
|
36
|
+
for path in memo_dir.glob("*.md"):
|
|
37
|
+
try:
|
|
38
|
+
stat = path.stat()
|
|
39
|
+
raw = path.read_text(encoding="utf-8", errors="ignore")
|
|
40
|
+
except OSError:
|
|
41
|
+
continue
|
|
42
|
+
updated_iso = _extract_frontmatter_field(raw, "updated") or _extract_frontmatter_field(raw, "created")
|
|
43
|
+
sort_key = _to_timestamp(updated_iso) or stat.st_mtime
|
|
44
|
+
snippet = _first_body_line(raw)
|
|
45
|
+
entries.append(
|
|
46
|
+
{
|
|
47
|
+
"title": path.stem,
|
|
48
|
+
"file": str(path.relative_to(notes_dir)),
|
|
49
|
+
"snippet": snippet,
|
|
50
|
+
"updated": updated_iso or datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
|
51
|
+
"_sort": sort_key,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
entries.sort(key=lambda e: e["_sort"], reverse=True)
|
|
56
|
+
shown = entries[:limit]
|
|
57
|
+
for entry in shown:
|
|
58
|
+
entry.pop("_sort", None)
|
|
48
59
|
|
|
49
|
-
memos = _collect_memos(raw.splitlines(keepends=False))
|
|
50
|
-
shown = memos[:limit]
|
|
51
60
|
if not shown:
|
|
52
|
-
return _ok(loc.t("No memos", "メモなし"), loc.t(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return _ok(title, "\n".join(lines), shown, date_str, path, total=len(memos))
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _collect_memos(lines):
|
|
65
|
-
items = []
|
|
66
|
-
i = 0
|
|
67
|
-
while i < len(lines):
|
|
68
|
-
if _MEMO_LINE_RE.match(lines[i]):
|
|
69
|
-
start = i
|
|
70
|
-
body = [_clean_head(lines[i])]
|
|
71
|
-
j = i + 1
|
|
72
|
-
while j < len(lines) and _CONTINUATION_RE.match(lines[j]):
|
|
73
|
-
body.append(lines[j].strip())
|
|
74
|
-
j += 1
|
|
75
|
-
text = " / ".join(part for part in body if part)
|
|
76
|
-
items.append(
|
|
77
|
-
{
|
|
78
|
-
"index": len(items) + 1,
|
|
79
|
-
"line": start + 1,
|
|
80
|
-
"text": _snippet(text),
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
i = j
|
|
61
|
+
return _ok(loc.t("No memos", "メモなし"), loc.t("There are no memos yet.", "まだメモはありません"), [])
|
|
62
|
+
|
|
63
|
+
header = loc.t(f"Latest {len(shown)} memos:", f"最近のメモ {len(shown)}件:")
|
|
64
|
+
lines = [header]
|
|
65
|
+
for idx, entry in enumerate(shown, start=1):
|
|
66
|
+
rel_time = _relative_time(entry["updated"], loc)
|
|
67
|
+
snippet = entry["snippet"]
|
|
68
|
+
if snippet:
|
|
69
|
+
lines.append(f"{idx}. {entry['title']} — {snippet} ({rel_time})")
|
|
84
70
|
else:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
if
|
|
71
|
+
lines.append(f"{idx}. {entry['title']} ({rel_time})")
|
|
72
|
+
if len(entries) > len(shown):
|
|
73
|
+
lines.append(loc.t(f"...and {len(entries) - len(shown)} more", f"...他 {len(entries) - len(shown)} 件"))
|
|
74
|
+
|
|
75
|
+
title = loc.t(f"{len(entries)} memos", f"メモ {len(entries)}件")
|
|
76
|
+
return _ok(title, "\n".join(lines), shown, total=len(entries))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_frontmatter_field(raw: str, field: str):
|
|
80
|
+
match = FRONTMATTER_RE.match(raw)
|
|
81
|
+
if not match:
|
|
82
|
+
return None
|
|
83
|
+
block = match.group(1)
|
|
84
|
+
for line in block.splitlines():
|
|
85
|
+
if not line.strip():
|
|
86
|
+
continue
|
|
87
|
+
key, _, val = line.partition(":")
|
|
88
|
+
if key.strip() == field:
|
|
89
|
+
v = val.strip()
|
|
90
|
+
if (v.startswith("\"") and v.endswith("\"")) or (v.startswith("'") and v.endswith("'")):
|
|
91
|
+
v = v[1:-1]
|
|
92
|
+
return v or None
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _first_body_line(raw: str) -> str:
|
|
97
|
+
body = raw
|
|
98
|
+
match = FRONTMATTER_RE.match(raw)
|
|
99
|
+
if match:
|
|
100
|
+
body = raw[match.end():]
|
|
101
|
+
for line in body.splitlines():
|
|
102
|
+
text = line.strip()
|
|
103
|
+
if not text:
|
|
104
|
+
continue
|
|
105
|
+
if HASH_TAG_LINE_RE.match(text):
|
|
106
|
+
continue
|
|
107
|
+
if text.startswith("#") and not text.startswith("##"):
|
|
108
|
+
continue
|
|
109
|
+
return _snippet(text)
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _snippet(text: str) -> str:
|
|
114
|
+
compact = " ".join(text.split())
|
|
115
|
+
if len(compact) <= 60:
|
|
96
116
|
return compact
|
|
97
|
-
return compact[:
|
|
117
|
+
return compact[:57] + "..."
|
|
98
118
|
|
|
99
119
|
|
|
100
|
-
def
|
|
120
|
+
def _parse_datetime(iso: str):
|
|
121
|
+
if not iso:
|
|
122
|
+
return None
|
|
123
|
+
text = iso.replace("Z", "+00:00")
|
|
124
|
+
if " " in text and "T" not in text:
|
|
125
|
+
text = text.replace(" ", "T", 1)
|
|
126
|
+
try:
|
|
127
|
+
return datetime.fromisoformat(text)
|
|
128
|
+
except (ValueError, TypeError):
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _to_timestamp(iso: str):
|
|
133
|
+
dt = _parse_datetime(iso)
|
|
134
|
+
return dt.timestamp() if dt else None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _relative_time(iso: str, loc) -> str:
|
|
138
|
+
when = _parse_datetime(iso)
|
|
139
|
+
if when is None:
|
|
140
|
+
return iso or ""
|
|
141
|
+
now = datetime.now().astimezone()
|
|
142
|
+
if when.tzinfo is None:
|
|
143
|
+
when = when.astimezone()
|
|
144
|
+
delta = (now - when).total_seconds()
|
|
145
|
+
if delta < 60:
|
|
146
|
+
return loc.t("just now", "たった今")
|
|
147
|
+
if delta < 3600:
|
|
148
|
+
return loc.t(f"{int(delta // 60)} min ago", f"{int(delta // 60)}分前")
|
|
149
|
+
if delta < 86400:
|
|
150
|
+
return loc.t(f"{int(delta // 3600)}h ago", f"{int(delta // 3600)}時間前")
|
|
151
|
+
if delta < 86400 * 7:
|
|
152
|
+
return loc.t(f"{int(delta // 86400)}d ago", f"{int(delta // 86400)}日前")
|
|
153
|
+
return when.strftime("%Y-%m-%d")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _ok(title, summary, memos, total=None):
|
|
101
157
|
return {
|
|
102
158
|
"status": "ok",
|
|
103
159
|
"title": title,
|
|
104
160
|
"summary": summary,
|
|
105
161
|
"outputs": {},
|
|
106
162
|
"data": {
|
|
107
|
-
"date": date_str,
|
|
108
163
|
"memos": memos,
|
|
109
164
|
"count": len(memos),
|
|
110
|
-
"total":
|
|
111
|
-
"path": path,
|
|
165
|
+
"total": total if total is not None else len(memos),
|
|
112
166
|
},
|
|
113
167
|
"suggestions": [],
|
|
114
168
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Builtin: memo-list
|
|
2
|
-
# memo
|
|
2
|
+
# notes/memo/ 配下のメモファイルを更新日時の新しい順で一覧表示する。
|
|
3
3
|
|
|
4
4
|
id: memo-list
|
|
5
5
|
name: Memo List
|
|
6
6
|
name_i18n:
|
|
7
7
|
en: Memo List
|
|
8
8
|
ja: メモ一覧
|
|
9
|
-
description:
|
|
9
|
+
description: 最近更新したMarkdownメモを一覧表示する
|
|
10
10
|
description_i18n:
|
|
11
|
-
en: List Markdown memos
|
|
12
|
-
ja:
|
|
11
|
+
en: List recently updated Markdown memos
|
|
12
|
+
ja: 最近更新したMarkdownメモを一覧表示する
|
|
13
13
|
runtime: python
|
|
14
14
|
output_mode: raw
|
|
15
15
|
|
|
@@ -18,32 +18,26 @@ invocation:
|
|
|
18
18
|
phrases:
|
|
19
19
|
- メモ一覧
|
|
20
20
|
- メモを見せて
|
|
21
|
-
-
|
|
21
|
+
- 最近のメモ
|
|
22
22
|
phrases_i18n:
|
|
23
23
|
en:
|
|
24
24
|
- list memos
|
|
25
25
|
- show memos
|
|
26
|
-
-
|
|
26
|
+
- recent memos
|
|
27
27
|
ja:
|
|
28
28
|
- メモ一覧
|
|
29
29
|
- メモを見せて
|
|
30
|
-
-
|
|
30
|
+
- 最近のメモ
|
|
31
31
|
|
|
32
32
|
input:
|
|
33
33
|
schema:
|
|
34
34
|
type: object
|
|
35
35
|
additionalProperties: false
|
|
36
36
|
properties:
|
|
37
|
-
date:
|
|
38
|
-
type: string
|
|
39
|
-
description: "対象日 (YYYY-MM-DD)。省略時は今日"
|
|
40
|
-
description_i18n:
|
|
41
|
-
en: "Target date (YYYY-MM-DD). Defaults to today"
|
|
42
|
-
ja: "対象日 (YYYY-MM-DD)。省略時は今日"
|
|
43
37
|
limit:
|
|
44
38
|
type: integer
|
|
45
39
|
minimum: 1
|
|
46
|
-
maximum:
|
|
40
|
+
maximum: 100
|
|
47
41
|
default: 20
|
|
48
42
|
description: 表示する最大件数
|
|
49
43
|
description_i18n:
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
"""Builtin: memo-save
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
入力:
|
|
9
|
-
args.text: 保存する本文 (必須, 改行可)
|
|
10
|
-
出力:
|
|
11
|
-
outputs.note: 追記用の Markdown。保存先の表示は skill.yaml 側で抑制する。
|
|
3
|
+
1メモ=1ファイル方式で notes/memo/{title}.md に保存する。
|
|
4
|
+
- AI でタイトルを生成し、サニタイズしてファイル名にする
|
|
5
|
+
- 同名既存ファイルがあれば本文末尾に追記し、updated を更新する (runtime 側 update_or_append)
|
|
6
|
+
- 旧バレット形式は廃止
|
|
12
7
|
"""
|
|
13
8
|
|
|
14
9
|
from __future__ import annotations
|
|
15
10
|
|
|
16
11
|
import os
|
|
12
|
+
import re
|
|
17
13
|
import sys
|
|
18
14
|
from datetime import datetime
|
|
19
15
|
|
|
@@ -21,6 +17,42 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(
|
|
|
21
17
|
from i18n import localizer # noqa: E402
|
|
22
18
|
|
|
23
19
|
|
|
20
|
+
MAX_TAGS = 3
|
|
21
|
+
MAX_TITLE_LEN = 48
|
|
22
|
+
GENERIC_TAGS = {
|
|
23
|
+
"memo",
|
|
24
|
+
"memos",
|
|
25
|
+
"note",
|
|
26
|
+
"notes",
|
|
27
|
+
"daily",
|
|
28
|
+
"メモ",
|
|
29
|
+
"ノート",
|
|
30
|
+
"日記",
|
|
31
|
+
}
|
|
32
|
+
KEYWORD_TAGS = [
|
|
33
|
+
("agent-sin", ("agent-sin", "agent sin", "agentsin")),
|
|
34
|
+
("kingcoding", ("kingcoding", "king coding", "king-coding")),
|
|
35
|
+
("obsidian", ("obsidian",)),
|
|
36
|
+
("ai", ("ai", "openai", "codex", "gpt", "claude", "gemini", "ollama", "llm", "生成ai", "生成AI")),
|
|
37
|
+
("仕事", ("仕事", "業務", "プロジェクト", "案件", "顧客", "client", "project")),
|
|
38
|
+
("会議", ("会議", "打ち合わせ", "ミーティング", "mtg", "meeting")),
|
|
39
|
+
("todo", ("todo", "タスク", "やること", "対応", "締切", "deadline")),
|
|
40
|
+
("アイデア", ("アイデア", "idea", "思いつき", "案", "企画")),
|
|
41
|
+
("バグ", ("バグ", "不具合", "エラー", "修正", "bug", "error", "fix")),
|
|
42
|
+
("学習", ("学習", "勉強", "読書", "本", "study", "learning", "book")),
|
|
43
|
+
("健康", ("健康", "体調", "睡眠", "運動", "散歩", "病院", "health")),
|
|
44
|
+
("お金", ("お金", "支払い", "請求", "家計", "税金", "money", "invoice")),
|
|
45
|
+
("予定", ("予定", "予約", "スケジュール", "schedule", "appointment")),
|
|
46
|
+
("買い物", ("買い物", "購入", "買う", "shopping", "buy")),
|
|
47
|
+
("食事", ("食事", "昼食", "夕食", "ランチ", "店", "restaurant", "food")),
|
|
48
|
+
("生活", ("生活", "掃除", "洗濯", "家事", "home")),
|
|
49
|
+
("旅行", ("旅行", "ホテル", "航空券", "travel", "hotel", "flight")),
|
|
50
|
+
]
|
|
51
|
+
HASH_TAG_RE = re.compile(r"(?<!\S)#([^\s#]+)")
|
|
52
|
+
FORBIDDEN_FILENAME_RE = re.compile(r"[\\/:*?\"<>|#\^\[\]]")
|
|
53
|
+
CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f]")
|
|
54
|
+
|
|
55
|
+
|
|
24
56
|
async def run(ctx, input):
|
|
25
57
|
loc = localizer(input)
|
|
26
58
|
args = input.get("args", {})
|
|
@@ -38,37 +70,171 @@ async def run(ctx, input):
|
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
ctx.log.info(f"memo-save: saving {len(text)} chars")
|
|
41
|
-
timestamp = input.get("trigger", {}).get("time") or datetime.now().isoformat()
|
|
42
|
-
|
|
73
|
+
timestamp = input.get("trigger", {}).get("time") or datetime.now().astimezone().isoformat()
|
|
74
|
+
tags = build_tags(text, args.get("tags"), args.get("auto_tags", True) is not False)
|
|
75
|
+
|
|
76
|
+
explicit_title = str(args.get("title", "")).strip()
|
|
77
|
+
raw_title = explicit_title or await generate_title(ctx, text)
|
|
78
|
+
sanitized = sanitize_filename(raw_title) or fallback_title(timestamp)
|
|
79
|
+
filename = f"{sanitized}.md"
|
|
80
|
+
|
|
81
|
+
content = format_memo_body(text, tags)
|
|
82
|
+
iso_now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
43
83
|
|
|
44
84
|
return {
|
|
45
85
|
"status": "ok",
|
|
46
86
|
"title": loc.t("Saved", "保存しました"),
|
|
47
|
-
"summary": loc.t("Memo saved.", "
|
|
87
|
+
"summary": loc.t(f"Memo saved as {filename}.", f"メモを保存しました: {filename}"),
|
|
48
88
|
"outputs": {
|
|
49
89
|
"note": {
|
|
90
|
+
"filename": filename,
|
|
50
91
|
"content": content,
|
|
51
92
|
"frontmatter": {
|
|
52
|
-
"tags": ["memo"]
|
|
93
|
+
"tags": ["memo"] + [t for t in tags if t not in ("memo",)],
|
|
94
|
+
"created": iso_now,
|
|
95
|
+
"updated": iso_now,
|
|
53
96
|
},
|
|
54
97
|
}
|
|
55
98
|
},
|
|
56
99
|
"data": {
|
|
57
|
-
"
|
|
100
|
+
"title": sanitized,
|
|
101
|
+
"filename": filename,
|
|
102
|
+
"length": len(text),
|
|
103
|
+
"tags": tags,
|
|
58
104
|
},
|
|
59
105
|
"suggestions": [],
|
|
60
106
|
}
|
|
61
107
|
|
|
62
108
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
async def generate_title(ctx, text: str) -> str:
|
|
110
|
+
if os.environ.get("AGENT_SIN_FAKE_PROVIDER") == "1":
|
|
111
|
+
return fallback_from_text(text)
|
|
112
|
+
prompt = build_title_prompt(text)
|
|
113
|
+
try:
|
|
114
|
+
result = await ctx.ai.run("title", prompt)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
ctx.log.warn(f"memo-save: title generation failed: {e}")
|
|
117
|
+
return fallback_from_text(text)
|
|
118
|
+
if not isinstance(result, dict) or result.get("status") != "ok":
|
|
119
|
+
reason = (result or {}).get("reason") if isinstance(result, dict) else None
|
|
120
|
+
ctx.log.warn(f"memo-save: title generation returned no result: {reason}")
|
|
121
|
+
return fallback_from_text(text)
|
|
122
|
+
raw = str(result.get("text") or "").strip()
|
|
123
|
+
cleaned = strip_quotes_and_lines(raw)
|
|
124
|
+
if not cleaned:
|
|
125
|
+
return fallback_from_text(text)
|
|
126
|
+
return cleaned
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_title_prompt(text: str) -> str:
|
|
130
|
+
truncated = text if len(text) <= 600 else (text[:600] + "...")
|
|
131
|
+
return (
|
|
132
|
+
"次のメモ本文から、24文字以下の短いタイトルを1つだけ返してください。\n"
|
|
133
|
+
"引用符・カギ括弧・句読点・絵文字・説明文は付けず、タイトル本文のみを1行で返してください。\n"
|
|
134
|
+
"本文の言語に合わせて日本語/英語で返してください。\n\n"
|
|
135
|
+
f"本文:\n{truncated}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def strip_quotes_and_lines(text: str) -> str:
|
|
140
|
+
first_line = text.splitlines()[0] if text else ""
|
|
141
|
+
return first_line.strip().strip("「」『』\"'`").strip()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def fallback_from_text(text: str) -> str:
|
|
145
|
+
head = text.strip().splitlines()[0] if text.strip() else ""
|
|
146
|
+
if len(head) > 40:
|
|
147
|
+
head = head[:40]
|
|
148
|
+
return head
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def fallback_title(timestamp: str) -> str:
|
|
152
|
+
dt = parse_datetime(timestamp) or datetime.now().astimezone()
|
|
153
|
+
return f"untitled-{dt.strftime('%H%M%S')}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def sanitize_filename(raw: str) -> str:
|
|
157
|
+
if not raw:
|
|
158
|
+
return ""
|
|
159
|
+
cleaned = CONTROL_CHARS_RE.sub("", raw)
|
|
160
|
+
cleaned = FORBIDDEN_FILENAME_RE.sub("-", cleaned)
|
|
161
|
+
cleaned = cleaned.replace("\n", " ").replace("\r", " ")
|
|
162
|
+
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
|
163
|
+
cleaned = cleaned.strip("._- ")
|
|
164
|
+
if not cleaned:
|
|
68
165
|
return ""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
166
|
+
if len(cleaned) > MAX_TITLE_LEN:
|
|
167
|
+
cleaned = cleaned[:MAX_TITLE_LEN].rstrip("._- ")
|
|
168
|
+
if cleaned in (".", ".."):
|
|
169
|
+
return ""
|
|
170
|
+
return cleaned
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def format_memo_body(text: str, tags) -> str:
|
|
174
|
+
body = text.rstrip()
|
|
175
|
+
if tags:
|
|
176
|
+
tag_line = " ".join(f"#{tag}" for tag in tags)
|
|
177
|
+
return f"{body}\n\n{tag_line}\n"
|
|
178
|
+
return f"{body}\n"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def build_tags(text, explicit_tags=None, auto_tags=True):
|
|
182
|
+
tags = []
|
|
183
|
+
for tag in parse_tag_values(explicit_tags):
|
|
184
|
+
add_tag(tags, tag)
|
|
185
|
+
for tag in extract_hash_tags(text):
|
|
186
|
+
add_tag(tags, tag)
|
|
187
|
+
if auto_tags:
|
|
188
|
+
haystack = text.lower()
|
|
189
|
+
for tag, keywords in KEYWORD_TAGS:
|
|
190
|
+
if any(str(keyword).lower() in haystack for keyword in keywords):
|
|
191
|
+
add_tag(tags, tag)
|
|
192
|
+
if len(tags) >= MAX_TAGS:
|
|
193
|
+
break
|
|
194
|
+
return tags[:MAX_TAGS]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_tag_values(value):
|
|
198
|
+
if value is None:
|
|
199
|
+
return []
|
|
200
|
+
if isinstance(value, list):
|
|
201
|
+
return value
|
|
202
|
+
if isinstance(value, str):
|
|
203
|
+
return [part for part in re.split(r"[,、\s]+", value) if part.strip()]
|
|
204
|
+
return [value]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def extract_hash_tags(text):
|
|
208
|
+
return [match.group(1) for match in HASH_TAG_RE.finditer(text)]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def add_tag(tags, value):
|
|
212
|
+
tag = normalize_tag(value)
|
|
213
|
+
if not tag or tag in tags:
|
|
214
|
+
return
|
|
215
|
+
tags.append(tag)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def normalize_tag(value):
|
|
219
|
+
tag = str(value).strip().lstrip("##")
|
|
220
|
+
if not tag:
|
|
221
|
+
return None
|
|
222
|
+
tag = re.sub(r"\s+", "-", tag)
|
|
223
|
+
tag = re.sub(r"[^\w\-/一-龯ぁ-んァ-ヶー]+", "", tag, flags=re.UNICODE)
|
|
224
|
+
tag = tag.strip("-_/")
|
|
225
|
+
if not tag or tag.isdigit():
|
|
226
|
+
return None
|
|
227
|
+
if re.fullmatch(r"[A-Za-z0-9_\-/]+", tag):
|
|
228
|
+
tag = tag.lower()
|
|
229
|
+
if tag.lower() in GENERIC_TAGS:
|
|
230
|
+
return None
|
|
231
|
+
return tag[:32]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parse_datetime(value):
|
|
235
|
+
if not value:
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
239
|
+
except ValueError:
|
|
240
|
+
return None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Builtin: memo-save
|
|
2
2
|
# 会話の skill-call または agent-sin run memo-save --text "..." で呼ばれる。
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# 1メモ=1ファイル形式で notes/memo/{title}.md に保存する低リスクスキル。
|
|
4
|
+
# AIで短いタイトルを生成し、同名既存ファイルがあれば末尾に追記して updated を更新する。
|
|
5
5
|
|
|
6
6
|
id: memo-save
|
|
7
7
|
name: Memo Save
|
|
@@ -35,15 +35,39 @@ input:
|
|
|
35
35
|
properties:
|
|
36
36
|
text:
|
|
37
37
|
type: string
|
|
38
|
+
title:
|
|
39
|
+
type: string
|
|
40
|
+
description: Optional explicit title. When provided, used as the filename without AI generation.
|
|
41
|
+
description_i18n:
|
|
42
|
+
en: Optional explicit title. When provided, used as the filename without AI generation.
|
|
43
|
+
ja: 任意のタイトル。指定するとAIタイトル生成をスキップしてそのまま使う
|
|
44
|
+
tags:
|
|
45
|
+
description: Optional Obsidian tags for this memo. Use 1-3 short tags without leading #, based on the memo content.
|
|
46
|
+
description_i18n:
|
|
47
|
+
en: Optional Obsidian tags for this memo. Use 1-3 short tags without leading #, based on the memo content.
|
|
48
|
+
ja: メモ内容に合うObsidianタグ。#なしで1〜3個まで。
|
|
49
|
+
auto_tags:
|
|
50
|
+
type: boolean
|
|
51
|
+
default: true
|
|
52
|
+
description: Add simple content-based tags when tags are not enough.
|
|
53
|
+
description_i18n:
|
|
54
|
+
en: Add simple content-based tags when tags are not enough.
|
|
55
|
+
ja: 本文から簡単なタグを補完する
|
|
38
56
|
required:
|
|
39
57
|
- text
|
|
40
58
|
|
|
59
|
+
ai_steps:
|
|
60
|
+
- id: title
|
|
61
|
+
purpose: Generate a short title (within 24 characters, no quotes or punctuation) summarizing the memo body. Return only the title text on a single line.
|
|
62
|
+
model: chat
|
|
63
|
+
optional: true
|
|
64
|
+
|
|
41
65
|
outputs:
|
|
42
66
|
- id: note
|
|
43
67
|
type: markdown
|
|
44
|
-
path: notes/
|
|
45
|
-
filename: "
|
|
46
|
-
|
|
68
|
+
path: notes/memo
|
|
69
|
+
filename: "untitled.md"
|
|
70
|
+
merge_mode: update_or_append
|
|
47
71
|
show_saved: false
|
|
48
72
|
|
|
49
73
|
memory:
|