agent-sin 0.1.10 → 0.1.12

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 CHANGED
@@ -15,6 +15,27 @@ See the [compatibility policy](https://agent.shingoirie.com/versioning) for deta
15
15
 
16
16
  ---
17
17
 
18
+ ## [0.1.12] — 2026-05-15
19
+
20
+ ### Added
21
+
22
+ - `/memo` slash command on Discord (`add` / `list` / `delete`), plus a new built-in `memo-list` skill that backs it.
23
+
24
+ ### Fixed
25
+
26
+ - Telegram replies no longer leak internal `skill-call` / `agent-sin-build-suggestion` fence blocks. Both the live draft preview and the final sent message now strip these control blocks, including malformed variants where the language tag ends up on its own line.
27
+ - Chat skill invocation is more tolerant of malformed `skill-call` blocks, and exact read-only triggers such as `todolist` run without waiting for the model to format a call.
28
+
29
+ ---
30
+
31
+ ## [0.1.11] — 2026-05-14
32
+
33
+ ### Fixed
34
+
35
+ - The interactive startup banner now uses a plain `AGENT-SIN` wordmark on Windows. The previous half-block terminal logo could render slightly misaligned in Windows Terminal depending on font and line-height settings.
36
+
37
+ ---
38
+
18
39
  ## [0.1.10] — 2026-05-14
19
40
 
20
41
  ### Fixed
@@ -0,0 +1,125 @@
1
+ """Builtin: memo-list
2
+
3
+ memo-save が書き込むデイリーノートからメモ行を読み、削除時に使える
4
+ 1始まり番号つきで表示する。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import sys
12
+ from datetime import datetime
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
15
+ from i18n import localizer # noqa: E402
16
+
17
+
18
+ _DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
19
+ _MEMO_LINE_RE = re.compile(r"^-\s+")
20
+ _CONTINUATION_RE = re.compile(r"^ \S")
21
+ _MEMO_HEAD_RE = re.compile(r"^-\s+(?:\d{4}-\d{2}-\d{2}T\S+\s+)?")
22
+
23
+
24
+ async def run(ctx, input):
25
+ loc = localizer(input)
26
+ args = input.get("args", {}) or {}
27
+ notes_dir = input.get("sources", {}).get("notes_dir", "")
28
+ if not notes_dir:
29
+ return _err(loc.t("Notes unavailable", "ノート不明"), loc.t("notes_dir is unavailable.", "notes_dir が取得できません"))
30
+
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
+ limit = int(args.get("limit", 20))
38
+ year, month, _ = date_str.split("-")
39
+ path = os.path.join(notes_dir, year, month, f"{date_str}.md")
40
+ if not os.path.exists(path):
41
+ return _ok(loc.t("No memos", "メモなし"), loc.t(f"No memos for {date_str}.", f"{date_str} のメモはありません"), [], date_str, path)
42
+
43
+ try:
44
+ with open(path, "r", encoding="utf-8") as f:
45
+ raw = f.read()
46
+ except OSError as e:
47
+ return _err(loc.t("Cannot read", "読み取り失敗"), loc.t(f"Failed to read {path}: {e}", f"{path} を読み取れませんでした: {e}"))
48
+
49
+ memos = _collect_memos(raw.splitlines(keepends=False))
50
+ shown = memos[:limit]
51
+ if not shown:
52
+ return _ok(loc.t("No memos", "メモなし"), loc.t(f"No memo lines for {date_str}.", f"{date_str} にメモ行はありません"), [], date_str, path)
53
+
54
+ lines = [loc.t(f"Memos for {date_str}:", f"{date_str} のメモ:")]
55
+ for item in shown:
56
+ lines.append(f"{item['index']}. {item['text']}")
57
+ if len(memos) > len(shown):
58
+ lines.append(loc.t(f"...and {len(memos) - len(shown)} more", f"...他 {len(memos) - len(shown)} 件"))
59
+
60
+ title = loc.t(f"{len(memos)} memos", f"メモ {len(memos)}件")
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
84
+ else:
85
+ i += 1
86
+ return items
87
+
88
+
89
+ def _clean_head(line):
90
+ return _MEMO_HEAD_RE.sub("", line, count=1).strip()
91
+
92
+
93
+ def _snippet(text):
94
+ compact = " ".join(str(text).split())
95
+ if len(compact) <= 160:
96
+ return compact
97
+ return compact[:157] + "..."
98
+
99
+
100
+ def _ok(title, summary, memos, date_str, path, total=None):
101
+ return {
102
+ "status": "ok",
103
+ "title": title,
104
+ "summary": summary,
105
+ "outputs": {},
106
+ "data": {
107
+ "date": date_str,
108
+ "memos": memos,
109
+ "count": len(memos),
110
+ "total": len(memos) if total is None else total,
111
+ "path": path,
112
+ },
113
+ "suggestions": [],
114
+ }
115
+
116
+
117
+ def _err(title, summary):
118
+ return {
119
+ "status": "error",
120
+ "title": title,
121
+ "summary": summary,
122
+ "outputs": {},
123
+ "data": {},
124
+ "suggestions": [],
125
+ }
@@ -0,0 +1,57 @@
1
+ # Builtin: memo-list
2
+ # memo-save が書き込むデイリーノートのメモ行を一覧表示する。
3
+
4
+ id: memo-list
5
+ name: Memo List
6
+ name_i18n:
7
+ en: Memo List
8
+ ja: メモ一覧
9
+ description: 指定日のMarkdownメモを一覧表示する
10
+ description_i18n:
11
+ en: List Markdown memos for a date
12
+ ja: 指定日のMarkdownメモを一覧表示する
13
+ runtime: python
14
+ output_mode: raw
15
+
16
+ invocation:
17
+ command: memo.list
18
+ phrases:
19
+ - メモ一覧
20
+ - メモを見せて
21
+ - 今日のメモ
22
+ phrases_i18n:
23
+ en:
24
+ - list memos
25
+ - show memos
26
+ - today's memos
27
+ ja:
28
+ - メモ一覧
29
+ - メモを見せて
30
+ - 今日のメモ
31
+
32
+ input:
33
+ schema:
34
+ type: object
35
+ additionalProperties: false
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
+ limit:
44
+ type: integer
45
+ minimum: 1
46
+ maximum: 50
47
+ default: 20
48
+ description: 表示する最大件数
49
+ description_i18n:
50
+ en: Maximum number of memos to show
51
+ ja: 表示する最大件数
52
+ required: []
53
+
54
+ memory:
55
+ namespace: memo
56
+ read: true
57
+ write: false
package/dist/cli/index.js CHANGED
@@ -1347,9 +1347,10 @@ function renderStartupBanner(state) {
1347
1347
  const accent = (text) => paintCode(BRAND_GREEN_ANSI, text, ctx);
1348
1348
  const dot = dim(glyph("dot", ctx));
1349
1349
  const modelDisplay = resolveDisplayModel(state);
1350
- if (ctx.ascii) {
1350
+ if (ctx.ascii || process.platform === "win32") {
1351
1351
  return [
1352
1352
  "",
1353
+ ` ${accent("AGENT-SIN")}`,
1353
1354
  ` ${bold("agent-sin")} ${dim("v" + AGENT_SIN_VERSION)} ${dot} ${dim("model:")} ${modelDisplay}`,
1354
1355
  ` ${dim("/help · /reset · /exit")}`,
1355
1356
  "",
@@ -62,6 +62,7 @@ export declare function stripSkillCalls(text: string): string;
62
62
  export declare function extractSkillCallBlocks(text: string): string;
63
63
  export declare function parseBuildSuggestion(text: string): ChatBuildSuggestion | null;
64
64
  export declare function stripBuildSuggestions(text: string): string;
65
+ export declare function stripInternalControlBlocks(text: string): string;
65
66
  export declare function toolResultJson(id: string, status: string, summary: string, saved?: string[], data?: unknown): string;
66
67
  export declare function toAiMessages(history: ChatTurn[], multimodalTurn?: {
67
68
  index: number;
@@ -9,8 +9,9 @@ import { maybePromoteDailyMemory } from "./daily-memory-promotion.js";
9
9
  import { l, t } from "./i18n.js";
10
10
  export const HISTORY_LIMIT = 20;
11
11
  export const TOOL_CALL_MAX_ITERATIONS = 3;
12
- const SKILL_CALL_PATTERN = /```skill-call\s*\n([\s\S]*?)\n```/g;
13
- const BUILD_SUGGESTION_PATTERN = /```agent-sin-build-suggestion\s*\n([\s\S]*?)\n```/g;
12
+ const SKILL_CALL_BLOCK = "skill-call";
13
+ const BUILD_SUGGESTION_BLOCK = "agent-sin-build-suggestion";
14
+ const INTERNAL_BLOCK_NAMES = [SKILL_CALL_BLOCK, BUILD_SUGGESTION_BLOCK];
14
15
  const REPAIR_FAILURE_PATTERN = /(traceback|exception|runtimeerror|syntaxerror|typeerror|nameerror|valueerror|importerror|module not found|exited with code|did not return valid json|handler not found|entry file not found|実行時|例外|エラー|失敗|読み取れません|できませんでした)/i;
15
16
  const USER_FIXABLE_FAILURE_PATTERN = /(missing required env vars|invalid input for|is disabled|skill not found|not allowed|設定してください|必要な設定|未入力|アプリパスワード|api[_ -]?key|token|credentials?)/i;
16
17
  export async function chatRespond(config, userText, history, options = {}) {
@@ -33,6 +34,8 @@ export async function chatRespond(config, userText, history, options = {}) {
33
34
  const profileMemory = await readProfileMemoryForPrompt(config);
34
35
  const systemPrompt = buildSystemPrompt(tools, options.preferredSkillId, profileMemory);
35
36
  const modelDisplay = await resolveDisplayModelName(config);
37
+ const directSkillCall = resolveDirectSkillCall(tools, userText, options.preferredSkillId);
38
+ let queuedAssistantText = directSkillCall ? formatSkillCallBlock(directSkillCall) : null;
36
39
  appendHistory(history, { role: "user", content: userText });
37
40
  const userTurnIndex = history.length - 1;
38
41
  const userImages = options.userImages || [];
@@ -63,91 +66,97 @@ export async function chatRespond(config, userText, history, options = {}) {
63
66
  for (let iteration = 0; iteration < TOOL_CALL_MAX_ITERATIONS; iteration += 1) {
64
67
  let assistantText;
65
68
  let buildSuggestion = null;
66
- const baseLabel = `${modelDisplay}: ${t("spinner.thinking")}`;
67
- if (spinner)
68
- spinner.start(baseLabel);
69
- emitProgress({ kind: "thinking", iteration });
70
- const spinnerProgress = spinner ? makeSpinnerProgress(spinner, baseLabel) : null;
71
- const providerProgress = spinnerProgress || options.onAiProgress
72
- ? (event) => {
73
- if (spinnerProgress) {
74
- spinnerProgress(event);
75
- }
76
- if (options.onAiProgress) {
77
- options.onAiProgress(event);
69
+ if (queuedAssistantText) {
70
+ assistantText = queuedAssistantText;
71
+ queuedAssistantText = null;
72
+ }
73
+ else {
74
+ const baseLabel = `${modelDisplay}: ${t("spinner.thinking")}`;
75
+ if (spinner)
76
+ spinner.start(baseLabel);
77
+ emitProgress({ kind: "thinking", iteration });
78
+ const spinnerProgress = spinner ? makeSpinnerProgress(spinner, baseLabel) : null;
79
+ const providerProgress = spinnerProgress || options.onAiProgress
80
+ ? (event) => {
81
+ if (spinnerProgress) {
82
+ spinnerProgress(event);
83
+ }
84
+ if (options.onAiProgress) {
85
+ options.onAiProgress(event);
86
+ }
78
87
  }
79
- }
80
- : undefined;
81
- try {
82
- const messages = [
83
- { role: "system", content: systemPrompt },
84
- ...toAiMessages(history, userImages.length > 0 ? { index: userTurnIndex, images: userImages } : undefined),
85
- ];
86
- const provider = getAiProvider();
87
- const response = await provider(config, {
88
- model_id: config.chat_model_id,
89
- messages,
90
- onProgress: providerProgress,
91
- });
92
- buildSuggestion = parseBuildSuggestion(response.text);
93
- assistantText = stripBuildSuggestions(response.text);
94
- if (shouldRetryEmptyAssistantReply(assistantText, buildSuggestion)) {
95
- await appendEventLog(config, {
96
- level: "warn",
97
- source: eventSource,
98
- event: "empty_model_reply_retry",
99
- message: "model returned an empty chat reply; retrying once",
100
- details: { model_id: config.chat_model_id, iteration },
88
+ : undefined;
89
+ try {
90
+ const messages = [
91
+ { role: "system", content: systemPrompt },
92
+ ...toAiMessages(history, userImages.length > 0 ? { index: userTurnIndex, images: userImages } : undefined),
93
+ ];
94
+ const provider = getAiProvider();
95
+ const response = await provider(config, {
96
+ model_id: config.chat_model_id,
97
+ messages,
98
+ onProgress: providerProgress,
101
99
  });
102
- try {
103
- const retryResponse = await provider(config, {
104
- model_id: config.chat_model_id,
105
- messages: [
106
- ...messages,
107
- { role: "system", content: emptyReplyRetryPrompt() },
108
- ],
109
- onProgress: providerProgress,
110
- });
111
- buildSuggestion = parseBuildSuggestion(retryResponse.text);
112
- assistantText = stripBuildSuggestions(retryResponse.text);
113
- }
114
- catch (retryError) {
115
- const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
100
+ buildSuggestion = parseBuildSuggestion(response.text);
101
+ assistantText = stripBuildSuggestions(response.text);
102
+ if (shouldRetryEmptyAssistantReply(assistantText, buildSuggestion)) {
116
103
  await appendEventLog(config, {
117
104
  level: "warn",
118
105
  source: eventSource,
119
- event: "empty_model_reply_retry_failed",
120
- message: retryMessage,
106
+ event: "empty_model_reply_retry",
107
+ message: "model returned an empty chat reply; retrying once",
121
108
  details: { model_id: config.chat_model_id, iteration },
122
109
  });
110
+ try {
111
+ const retryResponse = await provider(config, {
112
+ model_id: config.chat_model_id,
113
+ messages: [
114
+ ...messages,
115
+ { role: "system", content: emptyReplyRetryPrompt() },
116
+ ],
117
+ onProgress: providerProgress,
118
+ });
119
+ buildSuggestion = parseBuildSuggestion(retryResponse.text);
120
+ assistantText = stripBuildSuggestions(retryResponse.text);
121
+ }
122
+ catch (retryError) {
123
+ const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
124
+ await appendEventLog(config, {
125
+ level: "warn",
126
+ source: eventSource,
127
+ event: "empty_model_reply_retry_failed",
128
+ message: retryMessage,
129
+ details: { model_id: config.chat_model_id, iteration },
130
+ });
131
+ }
123
132
  }
124
- }
125
- if (buildSuggestion && options.onBuildSuggestion) {
126
- try {
127
- options.onBuildSuggestion(buildSuggestion);
128
- }
129
- catch {
130
- // Build suggestions are optional metadata; never break chat.
133
+ if (buildSuggestion && options.onBuildSuggestion) {
134
+ try {
135
+ options.onBuildSuggestion(buildSuggestion);
136
+ }
137
+ catch {
138
+ // Build suggestions are optional metadata; never break chat.
139
+ }
131
140
  }
132
141
  }
133
- }
134
- catch (error) {
142
+ catch (error) {
143
+ if (spinner)
144
+ spinner.stop();
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ history.pop();
147
+ await appendEventLog(config, {
148
+ level: "error",
149
+ source: eventSource,
150
+ event: "model_failed",
151
+ message,
152
+ details: { model_id: config.chat_model_id },
153
+ });
154
+ emitProgress({ kind: "model_failed", message });
155
+ return [t("chat.model_unreachable", { model: config.chat_model_id, message })];
156
+ }
135
157
  if (spinner)
136
158
  spinner.stop();
137
- const message = error instanceof Error ? error.message : String(error);
138
- history.pop();
139
- await appendEventLog(config, {
140
- level: "error",
141
- source: eventSource,
142
- event: "model_failed",
143
- message,
144
- details: { model_id: config.chat_model_id },
145
- });
146
- emitProgress({ kind: "model_failed", message });
147
- return [t("chat.model_unreachable", { model: config.chat_model_id, message })];
148
159
  }
149
- if (spinner)
150
- spinner.stop();
151
160
  const calls = parseSkillCalls(assistantText);
152
161
  const narrative = stripSkillCalls(assistantText).trim();
153
162
  // When the response invokes a side-effect skill (add/delete/send/save),
@@ -720,18 +729,18 @@ function stableStringify(value) {
720
729
  }
721
730
  export function parseSkillCalls(text) {
722
731
  const calls = [];
723
- SKILL_CALL_PATTERN.lastIndex = 0;
724
- let match;
725
- while ((match = SKILL_CALL_PATTERN.exec(text)) !== null) {
732
+ for (const block of findInternalControlBlocks(text, [SKILL_CALL_BLOCK])) {
726
733
  try {
727
- const parsed = JSON.parse(match[1]);
728
- if (typeof parsed.id !== "string") {
734
+ const parsed = JSON.parse(block.payload);
735
+ const id = parsed.id ?? parsed.skill_id ?? parsed.skillId;
736
+ if (typeof id !== "string") {
729
737
  continue;
730
738
  }
731
- const args = parsed.args && typeof parsed.args === "object" && !Array.isArray(parsed.args)
732
- ? parsed.args
739
+ const rawArgs = parsed.args ?? parsed.arguments ?? parsed.input;
740
+ const args = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)
741
+ ? rawArgs
733
742
  : {};
734
- calls.push({ id: parsed.id, args });
743
+ calls.push({ id, args });
735
744
  }
736
745
  catch {
737
746
  continue;
@@ -740,35 +749,275 @@ export function parseSkillCalls(text) {
740
749
  return calls;
741
750
  }
742
751
  export function stripSkillCalls(text) {
743
- return text.replace(SKILL_CALL_PATTERN, "").trim();
752
+ return removeInternalControlBlocks(text, [SKILL_CALL_BLOCK]).trim();
744
753
  }
745
754
  export function extractSkillCallBlocks(text) {
746
- const matches = text.match(SKILL_CALL_PATTERN);
747
- return matches ? matches.join("\n") : "";
755
+ const blocks = findInternalControlBlocks(text, [SKILL_CALL_BLOCK]);
756
+ return blocks.map((block) => text.slice(block.start, block.end).trim()).join("\n");
748
757
  }
749
758
  export function parseBuildSuggestion(text) {
750
- BUILD_SUGGESTION_PATTERN.lastIndex = 0;
751
- const match = BUILD_SUGGESTION_PATTERN.exec(text);
752
- if (!match)
759
+ for (const block of findInternalControlBlocks(text, [BUILD_SUGGESTION_BLOCK])) {
760
+ try {
761
+ const parsed = JSON.parse(block.payload);
762
+ const type = parsed.type === "edit" ? "edit" : "create";
763
+ const skillId = sanitizeSuggestedSkillId(String(parsed.skill_id || ""));
764
+ if (!skillId)
765
+ continue;
766
+ return {
767
+ type,
768
+ skill_id: skillId,
769
+ reason: typeof parsed.reason === "string" ? parsed.reason.slice(0, 240) : undefined,
770
+ };
771
+ }
772
+ catch {
773
+ continue;
774
+ }
775
+ }
776
+ return null;
777
+ }
778
+ export function stripBuildSuggestions(text) {
779
+ return removeInternalControlBlocks(text, [BUILD_SUGGESTION_BLOCK]).trim();
780
+ }
781
+ export function stripInternalControlBlocks(text) {
782
+ return removeInternalControlBlocks(text, INTERNAL_BLOCK_NAMES)
783
+ .replace(/\n{3,}/g, "\n\n")
784
+ .trim();
785
+ }
786
+ function findInternalControlBlocks(text, names) {
787
+ const wanted = new Set(names.map(normalizeInternalBlockName));
788
+ const lines = splitLinesWithOffsets(text);
789
+ const blocks = [];
790
+ for (let index = 0; index < lines.length; index += 1) {
791
+ const line = lines[index];
792
+ const fence = line.text.match(/^\s*(`{3,}|~{3,})\s*([A-Za-z][A-Za-z0-9_-]*)?\s*$/);
793
+ if (fence) {
794
+ let name = normalizeInternalBlockName(fence[2] || "");
795
+ let payloadStartLine = index + 1;
796
+ if (!name && payloadStartLine < lines.length) {
797
+ const splitName = normalizeInternalBlockName(lines[payloadStartLine].text.trim());
798
+ if (wanted.has(splitName)) {
799
+ name = splitName;
800
+ payloadStartLine += 1;
801
+ }
802
+ }
803
+ if (wanted.has(name)) {
804
+ const closeLine = findClosingFenceLine(lines, payloadStartLine, fence[1]);
805
+ const payloadStart = payloadStartLine < lines.length ? lines[payloadStartLine].start : line.end;
806
+ const payloadEnd = closeLine >= 0 ? lines[closeLine].start : text.length;
807
+ const end = closeLine >= 0 ? lines[closeLine].end : text.length;
808
+ blocks.push({
809
+ name,
810
+ payload: text.slice(payloadStart, payloadEnd).trim(),
811
+ start: line.start,
812
+ end,
813
+ });
814
+ index = closeLine >= 0 ? closeLine : lines.length;
815
+ continue;
816
+ }
817
+ }
818
+ const bareName = normalizeInternalBlockName(line.text.trim());
819
+ if (!wanted.has(bareName)) {
820
+ continue;
821
+ }
822
+ const payloadLine = nextNonEmptyLine(lines, index + 1);
823
+ if (payloadLine < 0) {
824
+ continue;
825
+ }
826
+ const jsonStart = firstNonWhitespaceIndex(text, lines[payloadLine].start);
827
+ if (jsonStart < 0 || text[jsonStart] !== "{") {
828
+ continue;
829
+ }
830
+ const jsonEnd = findJsonObjectEnd(text, jsonStart);
831
+ if (!jsonEnd) {
832
+ continue;
833
+ }
834
+ blocks.push({
835
+ name: bareName,
836
+ payload: text.slice(jsonStart, jsonEnd).trim(),
837
+ start: line.start,
838
+ end: consumeTrailingLineWhitespace(text, jsonEnd),
839
+ });
840
+ index = lineIndexForOffset(lines, jsonEnd);
841
+ }
842
+ return mergeInternalBlocks(blocks);
843
+ }
844
+ function removeInternalControlBlocks(text, names) {
845
+ const blocks = findInternalControlBlocks(text, names);
846
+ if (blocks.length === 0) {
847
+ return text;
848
+ }
849
+ let out = "";
850
+ let cursor = 0;
851
+ for (const block of blocks) {
852
+ if (block.start < cursor) {
853
+ continue;
854
+ }
855
+ out += text.slice(cursor, block.start);
856
+ cursor = block.end;
857
+ }
858
+ out += text.slice(cursor);
859
+ return out;
860
+ }
861
+ function splitLinesWithOffsets(text) {
862
+ const lines = [];
863
+ let start = 0;
864
+ while (start < text.length) {
865
+ const newline = text.indexOf("\n", start);
866
+ const end = newline >= 0 ? newline + 1 : text.length;
867
+ const contentEnd = newline >= 0 ? newline : end;
868
+ const raw = text.slice(start, contentEnd).replace(/\r$/, "");
869
+ lines.push({ text: raw, start, end });
870
+ start = end;
871
+ }
872
+ if (text.length === 0) {
873
+ lines.push({ text: "", start: 0, end: 0 });
874
+ }
875
+ return lines;
876
+ }
877
+ function findClosingFenceLine(lines, startLine, openingFence) {
878
+ const char = openingFence[0];
879
+ const minLength = openingFence.length;
880
+ for (let index = startLine; index < lines.length; index += 1) {
881
+ const trimmed = lines[index].text.trim();
882
+ if (trimmed.length < minLength) {
883
+ continue;
884
+ }
885
+ if ([...trimmed].every((item) => item === char)) {
886
+ return index;
887
+ }
888
+ }
889
+ return -1;
890
+ }
891
+ function nextNonEmptyLine(lines, startLine) {
892
+ for (let index = startLine; index < lines.length; index += 1) {
893
+ if (lines[index].text.trim()) {
894
+ return index;
895
+ }
896
+ }
897
+ return -1;
898
+ }
899
+ function firstNonWhitespaceIndex(text, start) {
900
+ for (let index = start; index < text.length; index += 1) {
901
+ if (!/\s/.test(text[index])) {
902
+ return index;
903
+ }
904
+ }
905
+ return -1;
906
+ }
907
+ function findJsonObjectEnd(text, start) {
908
+ let depth = 0;
909
+ let inString = false;
910
+ let escaped = false;
911
+ for (let index = start; index < text.length; index += 1) {
912
+ const char = text[index];
913
+ if (inString) {
914
+ if (escaped) {
915
+ escaped = false;
916
+ }
917
+ else if (char === "\\") {
918
+ escaped = true;
919
+ }
920
+ else if (char === '"') {
921
+ inString = false;
922
+ }
923
+ continue;
924
+ }
925
+ if (char === '"') {
926
+ inString = true;
927
+ continue;
928
+ }
929
+ if (char === "{") {
930
+ depth += 1;
931
+ continue;
932
+ }
933
+ if (char === "}") {
934
+ depth -= 1;
935
+ if (depth === 0) {
936
+ return index + 1;
937
+ }
938
+ }
939
+ }
940
+ return null;
941
+ }
942
+ function consumeTrailingLineWhitespace(text, start) {
943
+ let index = start;
944
+ while (index < text.length && (text[index] === " " || text[index] === "\t" || text[index] === "\r")) {
945
+ index += 1;
946
+ }
947
+ if (text[index] === "\n") {
948
+ index += 1;
949
+ }
950
+ return index;
951
+ }
952
+ function lineIndexForOffset(lines, offset) {
953
+ for (let index = 0; index < lines.length; index += 1) {
954
+ if (offset <= lines[index].end) {
955
+ return index;
956
+ }
957
+ }
958
+ return lines.length - 1;
959
+ }
960
+ function mergeInternalBlocks(blocks) {
961
+ const sorted = blocks.sort((a, b) => a.start - b.start || b.end - a.end);
962
+ const merged = [];
963
+ for (const block of sorted) {
964
+ const previous = merged[merged.length - 1];
965
+ if (previous && block.start < previous.end) {
966
+ continue;
967
+ }
968
+ merged.push(block);
969
+ }
970
+ return merged;
971
+ }
972
+ function normalizeInternalBlockName(value) {
973
+ return value.trim().toLowerCase().replaceAll("_", "-");
974
+ }
975
+ function formatSkillCallBlock(call) {
976
+ return ["```skill-call", safeJson({ id: call.id, args: call.args }), "```"].join("\n");
977
+ }
978
+ function resolveDirectSkillCall(skills, userText, preferredSkillId) {
979
+ const normalizedText = normalizeSkillTrigger(userText);
980
+ if (!normalizedText) {
753
981
  return null;
754
- try {
755
- const parsed = JSON.parse(match[1]);
756
- const type = parsed.type === "edit" ? "edit" : "create";
757
- const skillId = sanitizeSuggestedSkillId(String(parsed.skill_id || ""));
758
- if (!skillId)
759
- return null;
760
- return {
761
- type,
762
- skill_id: skillId,
763
- reason: typeof parsed.reason === "string" ? parsed.reason.slice(0, 240) : undefined,
764
- };
765
982
  }
766
- catch {
983
+ const candidates = skills
984
+ .filter((skill) => skill.output_mode === "raw" && !skill.side_effect && hasNoRequiredArgs(skill))
985
+ .map((skill) => ({
986
+ skill,
987
+ phrases: directSkillPhrases(skill),
988
+ }))
989
+ .filter((entry) => entry.phrases.some((phrase) => normalizeSkillTrigger(phrase) === normalizedText));
990
+ if (preferredSkillId) {
991
+ const preferred = candidates.find((entry) => entry.skill.id === preferredSkillId);
992
+ if (preferred) {
993
+ return { id: preferred.skill.id, args: {} };
994
+ }
995
+ }
996
+ if (candidates.length !== 1) {
767
997
  return null;
768
998
  }
999
+ return { id: candidates[0].skill.id, args: {} };
769
1000
  }
770
- export function stripBuildSuggestions(text) {
771
- return text.replace(BUILD_SUGGESTION_PATTERN, "").trim();
1001
+ function directSkillPhrases(skill) {
1002
+ return [
1003
+ skill.id,
1004
+ skill.name,
1005
+ skill.invocation?.command || "",
1006
+ ...(skill.invocation?.phrases || []),
1007
+ ].filter((phrase) => phrase.trim().length > 0);
1008
+ }
1009
+ function hasNoRequiredArgs(skill) {
1010
+ const required = skill.input?.schema?.required;
1011
+ return !Array.isArray(required) || required.length === 0;
1012
+ }
1013
+ function normalizeSkillTrigger(value) {
1014
+ return value
1015
+ .normalize("NFKC")
1016
+ .toLowerCase()
1017
+ .replace(/^\/+/, "")
1018
+ .replace(/(?:を|の)?(?:一覧|リスト)?(?:出して|表示して|見せて|教えて|ください|お願い|して)$/u, "")
1019
+ .replace(/[\s_\-./::・、。!!??"'`]+/g, "")
1020
+ .trim();
772
1021
  }
773
1022
  function sanitizeSuggestedSkillId(raw) {
774
1023
  return raw
@@ -75,6 +75,18 @@ export type TodoSlashParse = {
75
75
  args: Record<string, unknown>;
76
76
  };
77
77
  export declare function parseTodoSlashCommand(text: string): TodoSlashParse | null;
78
+ export type MemoSlashParse = {
79
+ kind: "help";
80
+ lines: string[];
81
+ } | {
82
+ kind: "error";
83
+ lines: string[];
84
+ } | {
85
+ kind: "run";
86
+ skillId: "memo-save" | "memo-list" | "memo-delete";
87
+ args: Record<string, unknown>;
88
+ };
89
+ export declare function parseMemoSlashCommand(text: string): MemoSlashParse | null;
78
90
  export type ModelSlashParse = {
79
91
  kind: "help";
80
92
  lines: string[];
@@ -761,6 +761,9 @@ async function handleMessage(state, message) {
761
761
  if (await tryRunTodoSlashCommand(state, message, cleanText)) {
762
762
  return;
763
763
  }
764
+ if (await tryRunMemoSlashCommand(state, message, cleanText)) {
765
+ return;
766
+ }
764
767
  if (await tryRunModelSlashCommand(state, message, cleanText)) {
765
768
  return;
766
769
  }
@@ -1224,6 +1227,179 @@ async function tryRunTodoSlashCommand(state, message, cleanText) {
1224
1227
  });
1225
1228
  return true;
1226
1229
  }
1230
+ export function parseMemoSlashCommand(text) {
1231
+ const trimmed = (text || "").trim();
1232
+ if (trimmed !== "/memo" && !/^\/memo\s/.test(trimmed)) {
1233
+ return null;
1234
+ }
1235
+ const rest = trimmed === "/memo" ? "" : trimmed.replace(/^\/memo\s+/, "").trim();
1236
+ if (!rest || rest === "help" || rest === "--help" || rest === "-h") {
1237
+ return { kind: "help", lines: memoSlashHelpLines() };
1238
+ }
1239
+ const firstSpace = rest.search(/\s/);
1240
+ const sub = (firstSpace >= 0 ? rest.slice(0, firstSpace) : rest).toLowerCase();
1241
+ const remainder = firstSpace >= 0 ? rest.slice(firstSpace + 1).trim() : "";
1242
+ if (sub === "add" || sub === "save") {
1243
+ if (!remainder) {
1244
+ return { kind: "error", lines: [l("Usage: /memo add <text>", "使い方: /memo add <本文>")] };
1245
+ }
1246
+ return { kind: "run", skillId: "memo-save", args: { text: remainder } };
1247
+ }
1248
+ if (sub === "list" || sub === "ls") {
1249
+ const parsed = parseMemoListArgs(remainder);
1250
+ if (parsed.kind === "error")
1251
+ return parsed;
1252
+ return { kind: "run", skillId: "memo-list", args: parsed.args };
1253
+ }
1254
+ if (sub === "delete" || sub === "remove" || sub === "del") {
1255
+ const parsed = parseMemoDeleteArgs(remainder);
1256
+ if (parsed.kind === "error")
1257
+ return parsed;
1258
+ return { kind: "run", skillId: "memo-delete", args: parsed.args };
1259
+ }
1260
+ return {
1261
+ kind: "error",
1262
+ lines: [
1263
+ l(`Unknown subcommand: ${sub}.`, `未対応のサブコマンドです: ${sub}。`),
1264
+ ...memoSlashHelpLines(),
1265
+ ],
1266
+ };
1267
+ }
1268
+ function parseMemoListArgs(rest) {
1269
+ let body = rest.trim();
1270
+ const dateFlag = extractValueFlag(body, "date");
1271
+ body = dateFlag.text;
1272
+ const limitFlag = extractValueFlag(body, "limit");
1273
+ body = limitFlag.text;
1274
+ const args = {};
1275
+ const bodyParts = body.split(/\s+/).filter(Boolean);
1276
+ if (dateFlag.value) {
1277
+ args.date = dateFlag.value;
1278
+ }
1279
+ else if (bodyParts.length > 0) {
1280
+ args.date = bodyParts[0];
1281
+ bodyParts.shift();
1282
+ }
1283
+ if (bodyParts.length > 0) {
1284
+ return { kind: "error", lines: [l("Usage: /memo list [YYYY-MM-DD] [--limit 20]", "使い方: /memo list [YYYY-MM-DD] [--limit 20]")] };
1285
+ }
1286
+ if (limitFlag.value) {
1287
+ const limit = parseIntegerOption(limitFlag.value);
1288
+ if (!limit || limit < 1 || limit > 50) {
1289
+ return { kind: "error", lines: [l("limit must be an integer from 1 to 50.", "limit は 1〜50 の整数で指定してください。")] };
1290
+ }
1291
+ args.limit = limit;
1292
+ }
1293
+ return { kind: "ok", args };
1294
+ }
1295
+ function parseMemoDeleteArgs(rest) {
1296
+ let target = rest.trim();
1297
+ const dateFlag = extractValueFlag(target, "date");
1298
+ target = dateFlag.text.trim();
1299
+ const args = {};
1300
+ if (dateFlag.value)
1301
+ args.date = dateFlag.value;
1302
+ if (!target) {
1303
+ return { kind: "error", lines: [l("Usage: /memo delete <index|text> [--date YYYY-MM-DD]", "使い方: /memo delete <番号|本文> [--date YYYY-MM-DD]")] };
1304
+ }
1305
+ const numeric = parseIntegerOption(target);
1306
+ if (numeric && String(numeric) === target) {
1307
+ args.index = numeric;
1308
+ }
1309
+ else {
1310
+ args.match = target;
1311
+ }
1312
+ return { kind: "ok", args };
1313
+ }
1314
+ function extractValueFlag(rest, name) {
1315
+ const pattern = new RegExp(`(^|\\s)--${escapeRegExp(name)}\\s+(\\S+)(?=\\s|$)`);
1316
+ const match = rest.match(pattern);
1317
+ if (!match)
1318
+ return { text: rest.trim() };
1319
+ const start = match.index ?? 0;
1320
+ const before = rest.slice(0, start);
1321
+ const after = rest.slice(start + match[0].length);
1322
+ const text = [before, after].map((segment) => segment.trim()).filter(Boolean).join(" ").trim();
1323
+ return { text, value: match[2] };
1324
+ }
1325
+ function parseIntegerOption(value) {
1326
+ if (!/^\d+$/.test(value.trim()))
1327
+ return null;
1328
+ const parsed = Number.parseInt(value, 10);
1329
+ return Number.isSafeInteger(parsed) ? parsed : null;
1330
+ }
1331
+ function escapeRegExp(value) {
1332
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1333
+ }
1334
+ function memoSlashHelpLines() {
1335
+ return lLines([
1336
+ "Memo commands:",
1337
+ "/memo add <text> - save a memo",
1338
+ "/memo list [YYYY-MM-DD] [--limit 20] - list memos",
1339
+ "/memo delete <index|text> [--date YYYY-MM-DD] - delete a memo",
1340
+ ], [
1341
+ "メモのショートカット:",
1342
+ "/memo add <本文> - メモを追加",
1343
+ "/memo list [YYYY-MM-DD] [--limit 20] - メモを一覧表示",
1344
+ "/memo delete <番号|本文> [--date YYYY-MM-DD] - メモを削除",
1345
+ ]);
1346
+ }
1347
+ async function tryRunMemoSlashCommand(state, message, cleanText) {
1348
+ const parsed = await withLocale(inferLocaleFromText(cleanText), () => Promise.resolve(parseMemoSlashCommand(cleanText)));
1349
+ if (!parsed)
1350
+ return false;
1351
+ await withLocale(inferLocaleFromText(cleanText), async () => {
1352
+ const status = createStatusReactor(state, message.channel_id, message.id);
1353
+ await status.set("received");
1354
+ if (parsed.kind === "help") {
1355
+ await status.set("done");
1356
+ await sendChannelMessage(state, message.channel_id, parsed.lines.join("\n"));
1357
+ return;
1358
+ }
1359
+ if (parsed.kind === "error") {
1360
+ await status.set("error");
1361
+ await sendChannelMessage(state, message.channel_id, parsed.lines.join("\n"));
1362
+ return;
1363
+ }
1364
+ await status.set("tool");
1365
+ try {
1366
+ const response = await runSkill(state.config, parsed.skillId, parsed.args);
1367
+ const display = response.result.summary ||
1368
+ response.result.title ||
1369
+ l("(no response)", "(応答なし)");
1370
+ await status.set(response.result.status === "ok" ? "done" : "error");
1371
+ await sendChannelMessage(state, message.channel_id, display);
1372
+ await appendEventLog(state.config, {
1373
+ level: "info",
1374
+ source: "discord",
1375
+ event: "memo_slash_ran",
1376
+ message: response.result.title || undefined,
1377
+ details: {
1378
+ skill_id: parsed.skillId,
1379
+ status: response.result.status,
1380
+ channel_id: message.channel_id,
1381
+ },
1382
+ });
1383
+ }
1384
+ catch (error) {
1385
+ await status.set("error");
1386
+ const detail = error instanceof SkillRunError
1387
+ ? error.originalMessage
1388
+ : error instanceof Error
1389
+ ? error.message
1390
+ : String(error);
1391
+ await sendChannelMessage(state, message.channel_id, l(`Error: ${detail}`, `エラー: ${detail}`));
1392
+ await appendEventLog(state.config, {
1393
+ level: "error",
1394
+ source: "discord",
1395
+ event: "memo_slash_failed",
1396
+ message: detail.slice(0, 200),
1397
+ details: { skill_id: parsed.skillId, channel_id: message.channel_id },
1398
+ });
1399
+ }
1400
+ });
1401
+ return true;
1402
+ }
1227
1403
  export function parseModelSlashCommand(text) {
1228
1404
  const trimmed = (text || "").trim();
1229
1405
  if (trimmed !== "/model" && !/^\/model\s/.test(trimmed)) {
@@ -1428,6 +1604,84 @@ const TODO_SLASH_COMMAND_DEFINITION = {
1428
1604
  },
1429
1605
  ],
1430
1606
  };
1607
+ const MEMO_SLASH_COMMAND_DEFINITION = {
1608
+ name: "memo",
1609
+ description: "Memo shortcut commands",
1610
+ description_localizations: { ja: "メモのショートカット" },
1611
+ type: 1,
1612
+ dm_permission: true,
1613
+ options: [
1614
+ {
1615
+ type: 1,
1616
+ name: "add",
1617
+ description: "Save a memo",
1618
+ description_localizations: { ja: "メモを追加" },
1619
+ options: [
1620
+ {
1621
+ type: 3,
1622
+ name: "text",
1623
+ description: "Memo text",
1624
+ description_localizations: { ja: "メモ本文" },
1625
+ required: true,
1626
+ },
1627
+ ],
1628
+ },
1629
+ {
1630
+ type: 1,
1631
+ name: "list",
1632
+ description: "List memos",
1633
+ description_localizations: { ja: "メモを一覧表示" },
1634
+ options: [
1635
+ {
1636
+ type: 3,
1637
+ name: "date",
1638
+ description: "Target date (YYYY-MM-DD, default today)",
1639
+ description_localizations: { ja: "対象日(YYYY-MM-DD、省略時は今日)" },
1640
+ required: false,
1641
+ },
1642
+ {
1643
+ type: 4,
1644
+ name: "limit",
1645
+ description: "Maximum number of memos",
1646
+ description_localizations: { ja: "表示する最大件数" },
1647
+ required: false,
1648
+ min_value: 1,
1649
+ max_value: 50,
1650
+ },
1651
+ ],
1652
+ },
1653
+ {
1654
+ type: 1,
1655
+ name: "delete",
1656
+ description: "Delete a memo",
1657
+ description_localizations: { ja: "メモを削除" },
1658
+ options: [
1659
+ {
1660
+ type: 4,
1661
+ name: "index",
1662
+ description: "Memo number from /memo list",
1663
+ description_localizations: { ja: "/memo list の番号" },
1664
+ required: false,
1665
+ min_value: 1,
1666
+ },
1667
+ {
1668
+ type: 3,
1669
+ name: "match",
1670
+ description: "Text contained in the memo",
1671
+ description_localizations: { ja: "メモに含まれる文字" },
1672
+ required: false,
1673
+ },
1674
+ {
1675
+ type: 3,
1676
+ name: "date",
1677
+ description: "Target date (YYYY-MM-DD, default today)",
1678
+ description_localizations: { ja: "対象日(YYYY-MM-DD、省略時は今日)" },
1679
+ required: false,
1680
+ },
1681
+ ],
1682
+ },
1683
+ ],
1684
+ };
1431
1685
  const MODEL_SLASH_COMMAND_DEFINITION = {
1432
1686
  name: "model",
1433
1687
  description: "Show or switch the chat model",
@@ -1447,6 +1701,7 @@ const MODEL_SLASH_COMMAND_DEFINITION = {
1447
1701
  };
1448
1702
  const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
1449
1703
  TODO_SLASH_COMMAND_DEFINITION,
1704
+ MEMO_SLASH_COMMAND_DEFINITION,
1450
1705
  MODEL_SLASH_COMMAND_DEFINITION,
1451
1706
  ];
1452
1707
  const DISCORD_SLASH_OPTION_TYPE_CODE = {
@@ -1590,6 +1845,12 @@ async function handleInteraction(state, interaction) {
1590
1845
  }
1591
1846
  return;
1592
1847
  }
1848
+ if (name === "memo") {
1849
+ if (interaction.type === 2) {
1850
+ await handleMemoInteraction(state, interaction);
1851
+ }
1852
+ return;
1853
+ }
1593
1854
  if (name === "model") {
1594
1855
  if (interaction.type === 4) {
1595
1856
  await handleModelAutocomplete(state, interaction);
@@ -1774,6 +2035,106 @@ async function handleTodoInteraction(state, interaction) {
1774
2035
  }
1775
2036
  });
1776
2037
  }
2038
+ async function handleMemoInteraction(state, interaction) {
2039
+ const userId = await ensureInteractionUserAllowed(state, interaction, "memo");
2040
+ if (!userId)
2041
+ return;
2042
+ const sub = interaction.data?.options?.[0];
2043
+ const subName = sub?.name || "";
2044
+ const optMap = new Map();
2045
+ for (const opt of sub?.options || []) {
2046
+ optMap.set(opt.name, opt.value);
2047
+ }
2048
+ await withLocale(inferLocaleFromText(extractInteractionLocaleHint(sub)), async () => {
2049
+ let skillId;
2050
+ let args;
2051
+ if (subName === "add") {
2052
+ const text = String(optMap.get("text") || "").trim();
2053
+ if (!text) {
2054
+ await respondInteraction(state, interaction, {
2055
+ content: l("Memo text is required.", "メモ本文を指定してください。"),
2056
+ ephemeral: true,
2057
+ });
2058
+ return;
2059
+ }
2060
+ skillId = "memo-save";
2061
+ args = { text };
2062
+ }
2063
+ else if (subName === "list") {
2064
+ skillId = "memo-list";
2065
+ args = {};
2066
+ const date = String(optMap.get("date") || "").trim();
2067
+ if (date)
2068
+ args.date = date;
2069
+ const limit = optMap.get("limit");
2070
+ if (typeof limit === "number")
2071
+ args.limit = limit;
2072
+ }
2073
+ else if (subName === "delete") {
2074
+ skillId = "memo-delete";
2075
+ args = {};
2076
+ const date = String(optMap.get("date") || "").trim();
2077
+ const match = String(optMap.get("match") || "").trim();
2078
+ const index = optMap.get("index");
2079
+ if (date)
2080
+ args.date = date;
2081
+ if (typeof index === "number")
2082
+ args.index = index;
2083
+ if (match)
2084
+ args.match = match;
2085
+ if (args.index === undefined && !args.match) {
2086
+ await respondInteraction(state, interaction, {
2087
+ content: l("Specify a memo number or matching text.", "番号か一致する本文を指定してください。"),
2088
+ ephemeral: true,
2089
+ });
2090
+ return;
2091
+ }
2092
+ }
2093
+ else {
2094
+ await respondInteraction(state, interaction, {
2095
+ content: l(`Unknown subcommand: ${subName || "(none)"}.`, `未対応のサブコマンドです: ${subName || "(なし)"}。`),
2096
+ ephemeral: true,
2097
+ });
2098
+ return;
2099
+ }
2100
+ const ack = await deferInteraction(state, interaction);
2101
+ if (!ack)
2102
+ return;
2103
+ try {
2104
+ const response = await runSkill(state.config, skillId, args);
2105
+ const display = response.result.summary ||
2106
+ response.result.title ||
2107
+ l("(no response)", "(応答なし)");
2108
+ await editInteractionOriginal(state, interaction, display);
2109
+ await appendEventLog(state.config, {
2110
+ level: "info",
2111
+ source: "discord",
2112
+ event: "memo_slash_ran",
2113
+ message: response.result.title || undefined,
2114
+ details: {
2115
+ skill_id: skillId,
2116
+ status: response.result.status,
2117
+ kind: "interaction",
2118
+ },
2119
+ });
2120
+ }
2121
+ catch (error) {
2122
+ const detail = error instanceof SkillRunError
2123
+ ? error.originalMessage
2124
+ : error instanceof Error
2125
+ ? error.message
2126
+ : String(error);
2127
+ await editInteractionOriginal(state, interaction, l(`Error: ${detail}`, `エラー: ${detail}`));
2128
+ await appendEventLog(state.config, {
2129
+ level: "error",
2130
+ source: "discord",
2131
+ event: "memo_slash_failed",
2132
+ message: detail.slice(0, 200),
2133
+ details: { skill_id: skillId, kind: "interaction" },
2134
+ });
2135
+ }
2136
+ });
2137
+ }
1777
2138
  async function handleModelInteraction(state, interaction) {
1778
2139
  const userId = await ensureInteractionUserAllowed(state, interaction, "model");
1779
2140
  if (!userId)
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { loadConfig } from "../core/config.js";
4
4
  import { appendEventLog } from "../core/logger.js";
5
+ import { stripInternalControlBlocks, } from "../core/chat-engine.js";
5
6
  import { createIntentRuntime, renderBuildFooter, shouldShowBuildFooter, } from "../builder/build-flow.js";
6
7
  import { routeConversationMessage, } from "../builder/conversation-router.js";
7
8
  import { cleanProgressText, formatBuildProgress, progressIntervalMs } from "../builder/progress-format.js";
@@ -527,31 +528,9 @@ function helpText() {
527
528
  return lLines([
528
529
  "Welcome to the Agent-Sin Telegram bot.",
529
530
  "It responds in DMs, mentions, and replies to the bot. Registered skills are called automatically when useful.",
530
- "",
531
- "Commands:",
532
- " /help Show this help",
533
- " /reset Reset this chat history",
534
- " /progress quiet|detail Toggle progress messages",
535
- " /back Return from build/edit mode to chat mode",
536
- " /build [skill-id] [request] Create a new skill",
537
- ' /build chat <id> "message" Revise through conversation',
538
- " /build list List drafts",
539
- " /build test <id> Test a draft",
540
- " /build status <id> Show status",
541
531
  ], [
542
532
  "Agent-Sin Telegram bot へようこそ。",
543
533
  "DM、メンション、bot への返信に反応します。登録済みスキルも自動で呼び出されます。",
544
- "",
545
- "コマンド:",
546
- " /help この使い方を表示",
547
- " /reset このチャットの会話履歴をリセット",
548
- " /progress quiet|detail 進捗通知の量を切り替え",
549
- " /back build / edit モードから chat モードに戻る",
550
- " /build [skill-id] [要望] 新規スキルを作成",
551
- ' /build chat <id> "メッセージ" 会話しながら修正',
552
- " /build list 作成中の一覧",
553
- " /build test <id> 動作確認",
554
- " /build status <id> 状態を見る",
555
534
  ]).join("\n");
556
535
  }
557
536
  async function formatTelegramUserMessageForChat(state, message) {
@@ -1024,7 +1003,7 @@ function telegramDraftIntervalMs() {
1024
1003
  return 1500;
1025
1004
  }
1026
1005
  function cleanDraftText(text) {
1027
- const cleaned = text
1006
+ const cleaned = stripInternalControlBlocks(text)
1028
1007
  .replace(/```/g, "")
1029
1008
  .replace(/\r\n/g, "\n")
1030
1009
  .replace(/\r/g, "\n")
@@ -1058,7 +1037,11 @@ function startTypingKeepalive(state, chatId, threadId) {
1058
1037
  };
1059
1038
  }
1060
1039
  async function sendTelegramMessage(state, chatId, content, options = {}) {
1061
- const chunks = chunkTelegramMessage(content);
1040
+ const sanitized = stripInternalControlBlocks(content);
1041
+ if (!sanitized.trim()) {
1042
+ return;
1043
+ }
1044
+ const chunks = chunkTelegramMessage(sanitized);
1062
1045
  for (let index = 0; index < chunks.length; index += 1) {
1063
1046
  const chunk = chunks[index];
1064
1047
  const payload = telegramSendPayload(chatId, chunk, index === 0 ? options : { threadId: options.threadId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sin",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Program Skill-first personal AI agent OS CLI.",
5
5
  "type": "module",
6
6
  "license": "MIT",