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.
Files changed (97) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +2 -1
  3. package/builtin-skills/_shared/_todo_lib.py +290 -0
  4. package/builtin-skills/even-g2-setup/main.ts +896 -0
  5. package/builtin-skills/even-g2-setup/skill.yaml +133 -0
  6. package/builtin-skills/memo-delete/main.py +28 -107
  7. package/builtin-skills/memo-delete/skill.yaml +10 -21
  8. package/builtin-skills/memo-index/main.py +96 -64
  9. package/builtin-skills/memo-index/skill.yaml +4 -10
  10. package/builtin-skills/memo-list/main.py +126 -72
  11. package/builtin-skills/memo-list/skill.yaml +8 -14
  12. package/builtin-skills/memo-save/main.py +191 -25
  13. package/builtin-skills/memo-save/skill.yaml +29 -5
  14. package/builtin-skills/memo-search/main.py +38 -18
  15. package/builtin-skills/memo-vector-search/main.py +11 -6
  16. package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
  17. package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
  18. package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
  19. package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
  20. package/builtin-skills/schedule-add/main.py +26 -0
  21. package/builtin-skills/service-restart/main.ts +249 -0
  22. package/builtin-skills/service-restart/skill.yaml +49 -0
  23. package/builtin-skills/todo-add/main.py +3 -1
  24. package/builtin-skills/todo-delete/main.py +3 -1
  25. package/builtin-skills/todo-done/main.py +3 -1
  26. package/builtin-skills/todo-list/main.py +4 -1
  27. package/builtin-skills/todo-tick/main.py +3 -1
  28. package/builtin-skills/topic-knowledge-read/main.py +118 -0
  29. package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
  30. package/dist/builder/build-action-classifier.d.ts +18 -0
  31. package/dist/builder/build-action-classifier.js +82 -1
  32. package/dist/builder/build-flow.d.ts +33 -4
  33. package/dist/builder/build-flow.js +251 -89
  34. package/dist/builder/builder-session.d.ts +1 -1
  35. package/dist/builder/builder-session.js +112 -7
  36. package/dist/builder/conversation-router.d.ts +4 -2
  37. package/dist/builder/conversation-router.js +19 -2
  38. package/dist/cli/index.js +323 -20
  39. package/dist/core/ai-provider.d.ts +1 -0
  40. package/dist/core/ai-provider.js +8 -3
  41. package/dist/core/chat-engine.d.ts +9 -3
  42. package/dist/core/chat-engine.js +1263 -146
  43. package/dist/core/config.d.ts +4 -0
  44. package/dist/core/config.js +82 -0
  45. package/dist/core/daily-memory-promotion.d.ts +7 -0
  46. package/dist/core/daily-memory-promotion.js +568 -14
  47. package/dist/core/image-attachments.d.ts +31 -0
  48. package/dist/core/image-attachments.js +237 -0
  49. package/dist/core/logger.d.ts +2 -1
  50. package/dist/core/logger.js +77 -1
  51. package/dist/core/memo-migration.d.ts +3 -0
  52. package/dist/core/memo-migration.js +422 -0
  53. package/dist/core/native-modules.d.ts +24 -0
  54. package/dist/core/native-modules.js +99 -0
  55. package/dist/core/notifier.d.ts +8 -3
  56. package/dist/core/notifier.js +191 -17
  57. package/dist/core/obsidian-vault.d.ts +19 -0
  58. package/dist/core/obsidian-vault.js +477 -0
  59. package/dist/core/operating-model.d.ts +2 -0
  60. package/dist/core/operating-model.js +15 -0
  61. package/dist/core/output-writer.d.ts +3 -2
  62. package/dist/core/output-writer.js +108 -7
  63. package/dist/core/profile-memory.js +22 -1
  64. package/dist/core/runtime.d.ts +2 -0
  65. package/dist/core/runtime.js +9 -1
  66. package/dist/core/secrets.d.ts +4 -0
  67. package/dist/core/secrets.js +34 -0
  68. package/dist/core/skill-history.d.ts +44 -0
  69. package/dist/core/skill-history.js +329 -0
  70. package/dist/core/skill-registry.d.ts +5 -0
  71. package/dist/core/skill-registry.js +11 -0
  72. package/dist/discord/bot.d.ts +1 -0
  73. package/dist/discord/bot.js +181 -10
  74. package/dist/even-g2/gateway.d.ts +15 -0
  75. package/dist/even-g2/gateway.js +868 -0
  76. package/dist/runtimes/codex-app-server.d.ts +5 -1
  77. package/dist/runtimes/codex-app-server.js +147 -8
  78. package/dist/runtimes/python-runner.js +82 -0
  79. package/dist/runtimes/typescript-runner.js +13 -1
  80. package/dist/skills-sdk/types.d.ts +19 -4
  81. package/dist/telegram/bot.d.ts +1 -0
  82. package/dist/telegram/bot.js +115 -7
  83. package/package.json +3 -1
  84. package/templates/even-g2-agent/README.md +83 -0
  85. package/templates/even-g2-agent/app.json +20 -0
  86. package/templates/even-g2-agent/index.html +31 -0
  87. package/templates/even-g2-agent/package-lock.json +1836 -0
  88. package/templates/even-g2-agent/package.json +22 -0
  89. package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
  90. package/templates/even-g2-agent/src/embedded-config.ts +4 -0
  91. package/templates/even-g2-agent/src/main.ts +539 -0
  92. package/templates/even-g2-agent/src/style.css +70 -0
  93. package/templates/even-g2-agent/tsconfig.json +11 -0
  94. package/templates/skill-python/main.py +20 -2
  95. package/templates/skill-python/skill.yaml +9 -0
  96. package/templates/skill-typescript/main.ts +40 -5
  97. package/templates/skill-typescript/skill.yaml +9 -0
@@ -41,6 +41,56 @@ export async function classifyBuildModeAction(config, userText, history, build,
41
41
  reason: typeof parsed.reason === "string" ? parsed.reason.slice(0, 240) : undefined,
42
42
  };
43
43
  }
44
+ export async function classifyScheduleRequest(config, userText, history, build, options = {}) {
45
+ const trimmed = userText.trim();
46
+ if (!trimmed) {
47
+ return { matched: false, reason: "empty input" };
48
+ }
49
+ const system = scheduleRequestPrompt(build);
50
+ const messages = buildMessages(system, history, trimmed);
51
+ const parsed = await callJsonClassifier(config, messages, options.modelId);
52
+ if (!parsed) {
53
+ return { matched: false, reason: "classifier fallback" };
54
+ }
55
+ const matched = parsed.matched === true;
56
+ const reason = typeof parsed.reason === "string" ? parsed.reason.slice(0, 240) : undefined;
57
+ if (!matched) {
58
+ return { matched: false, reason };
59
+ }
60
+ const cron = typeof parsed.cron === "string" ? parsed.cron.trim() : "";
61
+ if (!cron) {
62
+ return { matched: false, reason: reason || "missing cron" };
63
+ }
64
+ const skillRaw = typeof parsed.skill === "string" ? parsed.skill.trim() : "";
65
+ const skill = skillRaw || build.skill_id;
66
+ const idRaw = typeof parsed.id === "string" ? parsed.id.trim() : "";
67
+ const id = idRaw && /^[A-Za-z0-9_-]+$/.test(idRaw)
68
+ ? idRaw
69
+ : autoScheduleId(skill);
70
+ const payload = { id, cron, skill };
71
+ if (typeof parsed.description === "string" && parsed.description.trim()) {
72
+ payload.description = parsed.description.trim();
73
+ }
74
+ if (parsed.args && typeof parsed.args === "object" && !Array.isArray(parsed.args)) {
75
+ const inner = parsed.args;
76
+ if (Object.keys(inner).length > 0) {
77
+ payload.args = inner;
78
+ }
79
+ }
80
+ if (typeof parsed.enabled === "boolean") {
81
+ payload.enabled = parsed.enabled;
82
+ }
83
+ if (typeof parsed.approve === "boolean") {
84
+ payload.approve = parsed.approve;
85
+ }
86
+ return { matched: true, payload, reason };
87
+ }
88
+ function autoScheduleId(skill) {
89
+ const base = skill.replace(/[^A-Za-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
90
+ const suffix = Math.floor(Date.now() / 1000).toString(36);
91
+ const prefix = base || "schedule";
92
+ return `${prefix}-${suffix}`;
93
+ }
44
94
  function buildMessages(system, history, userText) {
45
95
  const recent = history.slice(-RECENT_HISTORY_TURNS).map((turn) => ({
46
96
  role: (turn.role === "tool" ? "tool" : turn.role),
@@ -123,16 +173,47 @@ function buildModeActionPrompt(build) {
123
173
  "Choose one of four actions:",
124
174
  '- "exit": the user wants to leave build mode and return to plain chat ("stop", "go back", "/back", やめる, 戻る, もういい, 中止, キャンセル, or any natural cancel/exit phrase in any language).',
125
175
  '- "register": the user wants to register/install the current draft as a usable skill ("register it", "install", 登録して, 本登録, これでOK, これで保存, 公開して).',
126
- '- "test": the user wants to test/run the current draft ("test it", "run it", 動かして, 試して, テスト).',
176
+ '- "test": the user wants a one-off run of the current draft ("test it", "run it", 動かして, 試して, テスト).',
127
177
  '- "continue": anything else — additional requirements, edits, design discussion, environment variables, default request to keep building.',
128
178
  "",
129
179
  "If the user is asking to fix or change implementation details, prefer 'continue' over 'register' or 'test'.",
180
+ "If the message includes any time-of-day, day-of-week, interval, recurrence, or cron hint (\"every day\", \"weekly\", \"every 30 minutes\", \"cron\", \"毎日X時\", \"毎週\", \"毎時\", \"スケジュール\", etc.), choose 'continue' — schedule registration is handled separately, not by 'test'.",
130
181
  "If unsure, choose 'continue'.",
131
182
  "",
132
183
  "Return exactly one JSON object. No markdown fences:",
133
184
  '{"action":"exit|register|test|continue","reason":"short why"}',
134
185
  ].join("\n");
135
186
  }
187
+ function scheduleRequestPrompt(build) {
188
+ const typeLabel = build.type === "edit" ? "editing an existing skill" : "creating a new skill";
189
+ return [
190
+ "You decide whether the user wants to register a recurring (cron) schedule for the skill currently being built.",
191
+ "Current build session:",
192
+ `- mode: ${typeLabel}`,
193
+ `- skill_id: ${build.skill_id}`,
194
+ "",
195
+ "Return matched=true ONLY when the user clearly asks for a recurring/timed run (examples: \"run this every day at 9am\", \"毎日9時に動かして\", \"weekly on monday\", \"スケジュールに追加して\", \"cron 0 9 * * *\"). A bare \"run it\" / \"動かして\" / \"テスト\" without time/recurrence is NOT a schedule.",
196
+ "",
197
+ "When matched, output:",
198
+ '{"matched":true,"cron":"min hour dom month dow","skill":"<skill id>","id":"<short kebab id>","description":"...","args":{},"enabled":true,"approve":false,"reason":"..."}',
199
+ "",
200
+ "Rules:",
201
+ "- cron must be a valid 5-field POSIX cron \"min hour dom month dow\". Convert natural language carefully:",
202
+ " - \"every day at 9am\" / \"毎日9時\" -> \"0 9 * * *\"",
203
+ " - \"every hour at :15\" / \"毎時15分\" -> \"15 * * * *\"",
204
+ " - \"every monday 8am\" / \"毎週月曜8時\" -> \"0 8 * * 1\"",
205
+ " - \"every 30 minutes\" / \"30分ごと\" -> \"*/30 * * * *\"",
206
+ `- skill defaults to "${build.skill_id}" unless the user explicitly names a different existing skill.`,
207
+ "- id should be a short kebab-case identifier (letters/digits/-/_). Combine skill + cadence (e.g. \"my-skill-daily-9am\", \"my-skill-weekly-mon\").",
208
+ "- Include args only if the user explicitly provided arguments for the skill. Otherwise omit args.",
209
+ "- Omit description unless the user gave one.",
210
+ "- enabled defaults to true; approve defaults to false. Only include when the user explicitly asked otherwise.",
211
+ "",
212
+ "If the user is NOT clearly asking to schedule, return {\"matched\":false,\"reason\":\"...\"}. When unsure, choose matched=false.",
213
+ "",
214
+ "Return exactly one JSON object. No markdown fences.",
215
+ ].join("\n");
216
+ }
136
217
  function truncate(text, max) {
137
218
  if (!text)
138
219
  return "";
@@ -11,7 +11,7 @@ export interface PendingHandoff {
11
11
  export interface PendingBuildExit {
12
12
  reason: string;
13
13
  }
14
- export type BuilderEventSource = "discord" | "telegram" | "cli";
14
+ export type BuilderEventSource = "discord" | "telegram" | "g2" | "cli";
15
15
  export interface BuildModeState {
16
16
  type: "create" | "edit";
17
17
  skill_id: string;
@@ -31,14 +31,30 @@ export interface IntentRuntime {
31
31
  build: BuildModeState | null;
32
32
  }
33
33
  export declare function createIntentRuntime(enabled?: boolean): IntentRuntime;
34
+ /**
35
+ * Parse `/build <skill-id>` (or `build <skill-id>`) from a chat line. Returns
36
+ * the requested skill id, or null when the input is not a direct build slash
37
+ * command. Subcommand-style first words (list/register/test/chat/status) are
38
+ * not treated as skill ids so the existing CLI subcommands keep working.
39
+ */
40
+ export declare function parseSlashBuildDirect(text: string): string | null;
41
+ export declare function parseSwitchEditTarget(text: string): string | null;
42
+ export declare function isExplicitBuildModeStartRequest(text: string): boolean;
43
+ export declare function renderBuildSuggestionConfirmation(type: "create" | "edit"): string[];
44
+ /**
45
+ * When the chat engine emits build_suggestion, keep whatever summary the model
46
+ * wrote in its visible narrative (what will be fixed / created), strip any
47
+ * sentences that leak internal handoff terminology ("渡します", "build mode",
48
+ * etc.), and append the confirmation question if the model did not already ask
49
+ * it. This way the user sees "<what will change> → この内容で直しますか?"
50
+ * instead of just the bare confirmation.
51
+ */
52
+ export declare function composeBuildSuggestionReply(modelLines: string[], type: "create" | "edit"): string[];
34
53
  export declare function parseEnvDirective(text: string): {
35
54
  name: string;
36
55
  value: string;
37
56
  } | null;
38
57
  export declare function isReservedAgentSinEnv(name: string): boolean;
39
- export declare function looksLikeRawSecretValue(text: string): boolean;
40
- export declare function extractAutoSaveSecretValue(text: string, envName?: string): string | null;
41
- export declare function tryAutoSaveBuildEnv(config: AppConfig, build: BuildModeState, text: string): Promise<string[] | null>;
42
58
  export interface BuildHandoffApproval {
43
59
  decision: "approve" | "reject" | "discuss";
44
60
  carry_over_text?: string;
@@ -68,5 +84,18 @@ export interface BuildAutoExitDecision {
68
84
  preferred_skill_id: string | null;
69
85
  reason: string;
70
86
  }
87
+ /**
88
+ * Switch the active edit target to a different skill from within build mode.
89
+ * Equivalent to `enterEditModeForSkill` but does not announce "Entered" —
90
+ * it tells the user we switched. Used when the user mentions another skill
91
+ * in the middle of an existing build session.
92
+ */
93
+ export declare function switchEditTargetSkill(config: AppConfig, skillId: string, intentRuntime: IntentRuntime, eventSource?: BuilderEventSource): Promise<string[]>;
94
+ /**
95
+ * Put `intentRuntime` directly into edit mode for an existing skill, without
96
+ * calling the builder agent. Used by deterministic UI flows like the CLI/Discord
97
+ * `/build` picker where the user has already chosen the target skill.
98
+ */
99
+ export declare function enterEditModeForSkill(config: AppConfig, skillId: string, intentRuntime: IntentRuntime, eventSource?: BuilderEventSource): Promise<string[]>;
71
100
  export declare function enterBuildMode(config: AppConfig, history: ChatTurn[], intentRuntime: IntentRuntime, hooks?: BuildModeHandlerOptions, extraText?: string, eventSource?: BuilderEventSource): Promise<string[]>;
72
101
  export declare function handleBuildModeMessage(config: AppConfig, text: string, intentRuntime: IntentRuntime, hooks?: BuildModeHandlerOptions, eventSource?: BuilderEventSource): Promise<string[] | null>;
@@ -2,11 +2,9 @@ import { classifyIntent } from "../core/intent-router.js";
2
2
  import { listSkillManifests } from "../core/skill-registry.js";
3
3
  import { buildDraftWithAgent, prepareEditDraft, } from "./builder-session.js";
4
4
  import { buildRegisterLines } from "./build-commands.js";
5
- import { loadDotenv, upsertDotenv } from "../core/secrets.js";
6
- import { createBuildSession } from "./builder-session.js";
7
- import { findMissingRequiredEnv } from "../core/runtime.js";
8
- import { loadSkillManifest } from "../core/skill-registry.js";
9
- import { classifyBuildModeAction, classifyHandoffApproval, } from "./build-action-classifier.js";
5
+ import { upsertDotenv } from "../core/secrets.js";
6
+ import { runSkill } from "../core/runtime.js";
7
+ import { classifyBuildModeAction, classifyHandoffApproval, classifyScheduleRequest, } from "./build-action-classifier.js";
10
8
  import { detectLocale, l } from "../core/i18n.js";
11
9
  export function createIntentRuntime(enabled = true) {
12
10
  return {
@@ -28,7 +26,60 @@ const SLASH_EXIT_COMMANDS = new Set([
28
26
  "!exit-build",
29
27
  ]);
30
28
  const SLASH_REGISTER_COMMANDS = new Set(["/register", "!register"]);
29
+ const SLASH_BUILD_SUBCOMMANDS = new Set(["list", "register", "test", "chat", "status"]);
30
+ /**
31
+ * Parse `/build <skill-id>` (or `build <skill-id>`) from a chat line. Returns
32
+ * the requested skill id, or null when the input is not a direct build slash
33
+ * command. Subcommand-style first words (list/register/test/chat/status) are
34
+ * not treated as skill ids so the existing CLI subcommands keep working.
35
+ */
36
+ export function parseSlashBuildDirect(text) {
37
+ if (!text)
38
+ return null;
39
+ const match = text.match(/^\s*\/?build\s+(.+)$/i);
40
+ if (!match)
41
+ return null;
42
+ const rest = match[1].trim();
43
+ if (!rest)
44
+ return null;
45
+ const first = rest.split(/\s+/)[0] || "";
46
+ if (SLASH_BUILD_SUBCOMMANDS.has(first.toLowerCase()))
47
+ return null;
48
+ return first;
49
+ }
31
50
  const SLASH_TEST_COMMANDS = new Set(["/test", "!test"]);
51
+ const SLASH_EDIT_SWITCH_PATTERN = /^\s*[\/!]?(?:edit|switch|target)\s+([a-z][a-z0-9-]*)\s*$/i;
52
+ export function parseSwitchEditTarget(text) {
53
+ if (!text)
54
+ return null;
55
+ const match = text.trim().match(SLASH_EDIT_SWITCH_PATTERN);
56
+ if (!match)
57
+ return null;
58
+ return match[1].toLowerCase();
59
+ }
60
+ const EDIT_VERB_PATTERN = /(直[しすせる]|修正|編集|改善|変更|手を入れ|触っ|更新|fix|update|edit|tweak|change)/i;
61
+ async function detectImplicitEditSwitch(config, text, currentSkillId) {
62
+ if (!text || !EDIT_VERB_PATTERN.test(text))
63
+ return null;
64
+ let skills;
65
+ try {
66
+ skills = await listSkillManifests(config.skills_dir);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ const candidates = skills.map((skill) => skill.id).filter((id) => id !== currentSkillId);
72
+ if (candidates.length === 0)
73
+ return null;
74
+ const sorted = [...candidates].sort((a, b) => b.length - a.length);
75
+ for (const id of sorted) {
76
+ const escaped = id.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
77
+ const re = new RegExp(`(?:^|[^a-z0-9-])${escaped}(?:[^a-z0-9-]|$)`, "i");
78
+ if (re.test(text))
79
+ return id;
80
+ }
81
+ return null;
82
+ }
32
83
  function isSlashExitCommand(text) {
33
84
  return SLASH_EXIT_COMMANDS.has(text.trim().toLowerCase());
34
85
  }
@@ -38,6 +89,64 @@ function isSlashRegisterCommand(text) {
38
89
  function isSlashTestCommand(text) {
39
90
  return SLASH_TEST_COMMANDS.has(text.trim().toLowerCase());
40
91
  }
92
+ export function isExplicitBuildModeStartRequest(text) {
93
+ const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
94
+ if (!normalized)
95
+ return false;
96
+ if (/(?:作って|つくって|作成して|作成し(?:て|よう)|スキル化して|実装して|組んで|直して|修正して|改善して|ビルドして|この方向で進めて|それで進めて|その内容で進めて)/u.test(normalized)) {
97
+ return true;
98
+ }
99
+ if (/(?:^|\b)(?:build|create|make|implement|fix|update) (?:it|this|that|the skill)(?:\b|$)/i.test(normalized)) {
100
+ return true;
101
+ }
102
+ if (/(?:^|\b)(?:go ahead|do it|please proceed|let's do it|looks good,? proceed)(?:\b|$)/i.test(normalized)) {
103
+ return true;
104
+ }
105
+ return /^(?:ok|okay|yes|yep|sure),? (?:go ahead|do it|proceed)$/i.test(normalized);
106
+ }
107
+ export function renderBuildSuggestionConfirmation(type) {
108
+ return [
109
+ type === "edit"
110
+ ? l("I can fix it with that approach. Should I proceed?", "この内容で直しますか?")
111
+ : l("I can create it with that approach. Should I proceed?", "この内容で作りますか?"),
112
+ ];
113
+ }
114
+ /**
115
+ * When the chat engine emits build_suggestion, keep whatever summary the model
116
+ * wrote in its visible narrative (what will be fixed / created), strip any
117
+ * sentences that leak internal handoff terminology ("渡します", "build mode",
118
+ * etc.), and append the confirmation question if the model did not already ask
119
+ * it. This way the user sees "<what will change> → この内容で直しますか?"
120
+ * instead of just the bare confirmation.
121
+ */
122
+ export function composeBuildSuggestionReply(modelLines, type) {
123
+ const confirmation = type === "edit"
124
+ ? l("I can fix it with that approach. Should I proceed?", "この内容で直しますか?")
125
+ : l("I can create it with that approach. Should I proceed?", "この内容で作りますか?");
126
+ const cleaned = [];
127
+ for (const raw of modelLines) {
128
+ const sanitized = stripHandoffSentences(raw);
129
+ if (sanitized.trim().length > 0)
130
+ cleaned.push(sanitized);
131
+ }
132
+ const joined = cleaned.join("\n");
133
+ if (!/直しますか|作りますか|Should I proceed/i.test(joined)) {
134
+ cleaned.push(confirmation);
135
+ }
136
+ return cleaned.length > 0 ? cleaned : [confirmation];
137
+ }
138
+ const HANDOFF_LEAK_PATTERN = /(?:ビルドモード(?:に|へ)?(?:渡|移|入|切り替)|渡します|渡しますね|渡しますか|build\s*mode|hand(?:ed|ing)?\s*off|hand\s+off|handoff|pass(?:ing)?\s+(?:it|this|the\s+task))/i;
139
+ function stripHandoffSentences(line) {
140
+ if (!line)
141
+ return "";
142
+ const sentences = line.split(/(?<=[。!?\.!?])\s*/);
143
+ const kept = sentences.filter((sentence) => {
144
+ if (!sentence.trim())
145
+ return false;
146
+ return !HANDOFF_LEAK_PATTERN.test(sentence);
147
+ });
148
+ return kept.join("");
149
+ }
41
150
  export function parseEnvDirective(text) {
42
151
  const trimmed = text.trim();
43
152
  const match = trimmed.match(/^env\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]*)$/i);
@@ -48,84 +157,6 @@ export function parseEnvDirective(text) {
48
157
  export function isReservedAgentSinEnv(name) {
49
158
  return /^AGENT_SIN_/i.test(name);
50
159
  }
51
- export function looksLikeRawSecretValue(text) {
52
- const trimmed = text.trim();
53
- if (!trimmed)
54
- return false;
55
- if (/\s/.test(trimmed))
56
- return false;
57
- if (trimmed.length < 16 || trimmed.length > 512)
58
- return false;
59
- if (!/^[A-Za-z0-9._\-:/+=~]+$/.test(trimmed))
60
- return false;
61
- if (!/[0-9]/.test(trimmed) && !/[A-Z]/.test(trimmed))
62
- return false;
63
- return true;
64
- }
65
- export function extractAutoSaveSecretValue(text, envName) {
66
- const candidates = collectAutoSaveSecretCandidates(text);
67
- const values = Array.from(new Set(candidates
68
- .map((candidate) => normalizeAutoSaveSecretCandidate(candidate, envName))
69
- .filter((value) => Boolean(value))));
70
- return values.length === 1 ? values[0] : null;
71
- }
72
- function collectAutoSaveSecretCandidates(text) {
73
- const candidates = [];
74
- const assignmentPattern = /\b[A-Za-z_][A-Za-z0-9_]*\s*=\s*([A-Za-z0-9._\-:/+=~]{16,512})/g;
75
- for (const match of text.matchAll(assignmentPattern)) {
76
- candidates.push(match[0], match[1]);
77
- }
78
- const tokenPattern = /[A-Za-z0-9_][A-Za-z0-9._\-:/+=~]{15,511}/g;
79
- for (const match of text.matchAll(tokenPattern)) {
80
- candidates.push(match[0]);
81
- }
82
- return candidates;
83
- }
84
- function normalizeAutoSaveSecretCandidate(raw, envName) {
85
- let value = raw
86
- .trim()
87
- .replace(/^[`"'「『]+/g, "")
88
- .replace(/[`"'」』、。,.]+$/g, "");
89
- const inlineAssignment = value.match(/^[A-Za-z_][A-Za-z0-9_]*=(.+)$/);
90
- if (inlineAssignment) {
91
- value = inlineAssignment[1].trim();
92
- }
93
- if (/^https?:\/\//i.test(value) && !allowsUrlSecret(envName)) {
94
- return null;
95
- }
96
- return looksLikeRawSecretValue(value) ? value : null;
97
- }
98
- function allowsUrlSecret(envName) {
99
- return Boolean(envName && /(?:URL|URI|ENDPOINT|WEBHOOK)/i.test(envName));
100
- }
101
- export async function tryAutoSaveBuildEnv(config, build, text) {
102
- if (collectAutoSaveSecretCandidates(text).length === 0)
103
- return null;
104
- let manifest;
105
- try {
106
- const session = await createBuildSession(config, build.skill_id);
107
- manifest = await loadSkillManifest(session.draft_dir);
108
- }
109
- catch {
110
- return null;
111
- }
112
- await loadDotenv(config.workspace);
113
- const missing = findMissingRequiredEnv(manifest).filter((entry) => !isReservedAgentSinEnv(entry.name));
114
- if (missing.length !== 1)
115
- return null;
116
- const envName = missing[0].name;
117
- const value = extractAutoSaveSecretValue(text, envName);
118
- if (!value)
119
- return null;
120
- try {
121
- const result = await upsertDotenv(config.workspace, [{ key: envName, value }]);
122
- return [l(`Saved ${envName} to ${result.path}.`, `${envName} を ${result.path} に保存しました。`)];
123
- }
124
- catch (error) {
125
- const message = error instanceof Error ? error.message : String(error);
126
- return [l(`Failed to save environment variable: ${message}`, `環境変数の保存に失敗: ${message}`)];
127
- }
128
- }
129
160
  export async function classifyPendingHandoff(config, text, history, intentRuntime) {
130
161
  const pending = intentRuntime.pending;
131
162
  if (!pending) {
@@ -262,6 +293,69 @@ async function detectBuildAutoExit(config, text, history) {
262
293
  }
263
294
  return null;
264
295
  }
296
+ /**
297
+ * Switch the active edit target to a different skill from within build mode.
298
+ * Equivalent to `enterEditModeForSkill` but does not announce "Entered" —
299
+ * it tells the user we switched. Used when the user mentions another skill
300
+ * in the middle of an existing build session.
301
+ */
302
+ export async function switchEditTargetSkill(config, skillId, intentRuntime, eventSource) {
303
+ try {
304
+ await prepareEditDraft(config, skillId);
305
+ }
306
+ catch (error) {
307
+ return [await renderEditModeFailureMessage(config, skillId, error)];
308
+ }
309
+ const displayName = await resolveSkillDisplayName(config, skillId);
310
+ intentRuntime.pending = null;
311
+ intentRuntime.pending_exit = null;
312
+ intentRuntime.preferred_skill_id = null;
313
+ intentRuntime.mode = "build";
314
+ intentRuntime.build = {
315
+ type: "edit",
316
+ skill_id: skillId,
317
+ skill_name: displayName,
318
+ context_seed: [],
319
+ context_consumed: true,
320
+ original_text: "",
321
+ event_source: eventSource ?? intentRuntime.build?.event_source,
322
+ };
323
+ const label = displayName?.trim() || skillId;
324
+ return [
325
+ l(`Switched edit target to "${label}". Tell me what to change.`, `編集対象を「${label}」に切り替えました。直したい内容を教えてください。`),
326
+ ];
327
+ }
328
+ /**
329
+ * Put `intentRuntime` directly into edit mode for an existing skill, without
330
+ * calling the builder agent. Used by deterministic UI flows like the CLI/Discord
331
+ * `/build` picker where the user has already chosen the target skill.
332
+ */
333
+ export async function enterEditModeForSkill(config, skillId, intentRuntime, eventSource) {
334
+ try {
335
+ await prepareEditDraft(config, skillId);
336
+ }
337
+ catch (error) {
338
+ return [await renderEditModeFailureMessage(config, skillId, error)];
339
+ }
340
+ const displayName = await resolveSkillDisplayName(config, skillId);
341
+ intentRuntime.pending = null;
342
+ intentRuntime.pending_exit = null;
343
+ intentRuntime.preferred_skill_id = null;
344
+ intentRuntime.mode = "build";
345
+ intentRuntime.build = {
346
+ type: "edit",
347
+ skill_id: skillId,
348
+ skill_name: displayName,
349
+ context_seed: [],
350
+ context_consumed: true,
351
+ original_text: "",
352
+ event_source: eventSource,
353
+ };
354
+ const label = displayName?.trim() || skillId;
355
+ return [
356
+ l(`Entered edit mode for "${label}". Tell me what to change.`, `「${label}」の編集モードに入りました。直したい内容を教えてください。`),
357
+ ];
358
+ }
265
359
  export async function enterBuildMode(config, history, intentRuntime, hooks = {}, extraText, eventSource) {
266
360
  const pending = intentRuntime.pending;
267
361
  if (!pending) {
@@ -294,7 +388,29 @@ export async function enterBuildMode(config, history, intentRuntime, hooks = {},
294
388
  const initialText = extraText && extraText.trim().length > 0
295
389
  ? `${pending.original_text}\n\n[追加要件]\n${extraText.trim()}`
296
390
  : pending.original_text;
297
- return forwardToBuilder(config, intentRuntime.build, initialText, hooks);
391
+ const replyLines = await forwardToBuilder(config, intentRuntime.build, initialText, hooks);
392
+ const scheduleLines = await maybeAutoRegisterSchedule(config, intentRuntime.build, initialText, hooks.history || []);
393
+ return scheduleLines.length > 0 ? [...replyLines, ...scheduleLines] : replyLines;
394
+ }
395
+ /**
396
+ * After a build draft is created from the user's original request, also register
397
+ * a recurring schedule when that request clearly asked for one (e.g. "毎朝4時の
398
+ * ブリーフィング"). The classifier already covers follow-up messages inside build
399
+ * mode; this fills the gap for the very first creation prompt.
400
+ */
401
+ async function maybeAutoRegisterSchedule(config, build, originalText, history) {
402
+ const trimmed = originalText.trim();
403
+ if (!trimmed)
404
+ return [];
405
+ try {
406
+ const decision = await classifyScheduleRequest(config, trimmed, history, build);
407
+ if (!decision.matched)
408
+ return [];
409
+ return await handleScheduleAddAction(config, decision.payload);
410
+ }
411
+ catch {
412
+ return [];
413
+ }
298
414
  }
299
415
  export async function handleBuildModeMessage(config, text, intentRuntime, hooks = {}, eventSource) {
300
416
  const build = intentRuntime.build;
@@ -309,6 +425,7 @@ export async function handleBuildModeMessage(config, text, intentRuntime, hooks
309
425
  // Deterministic slash-command exit.
310
426
  if (isSlashExitCommand(text)) {
311
427
  intentRuntime.mode = "chat";
428
+ intentRuntime.preferred_skill_id = build.skill_id;
312
429
  intentRuntime.build = null;
313
430
  intentRuntime.pending_exit = null;
314
431
  return [l("Back to chat.", "◀︎ チャットに戻りました。")];
@@ -332,11 +449,6 @@ export async function handleBuildModeMessage(config, text, intentRuntime, hooks
332
449
  return [l(`Failed to save environment variable: ${message}`, `環境変数の保存に失敗: ${message}`)];
333
450
  }
334
451
  }
335
- // User pasted a raw API key value — auto-save into the only missing required_env if any.
336
- const autoSaved = await tryAutoSaveBuildEnv(config, build, text);
337
- if (autoSaved) {
338
- return autoSaved;
339
- }
340
452
  // Deterministic slash-command shortcuts for register / test.
341
453
  if (isSlashRegisterCommand(text)) {
342
454
  return handleRegisterAction(config, build, intentRuntime);
@@ -344,10 +456,23 @@ export async function handleBuildModeMessage(config, text, intentRuntime, hooks
344
456
  if (isSlashTestCommand(text)) {
345
457
  return handleTestAction(build, intentRuntime);
346
458
  }
459
+ // Explicit "/edit <skill-id>" switches the current edit target without
460
+ // leaving build mode, so the user can fix multiple skills in one session.
461
+ const explicitSwitch = parseSwitchEditTarget(text);
462
+ if (explicitSwitch && explicitSwitch !== build.skill_id) {
463
+ return switchEditTargetSkill(config, explicitSwitch, intentRuntime, eventSource);
464
+ }
465
+ // Implicit switch: user mentions another known skill id together with an
466
+ // edit verb (e.g. "memo-save も直して").
467
+ const implicitSwitch = await detectImplicitEditSwitch(config, text, build.skill_id);
468
+ if (implicitSwitch) {
469
+ return switchEditTargetSkill(config, implicitSwitch, intentRuntime, eventSource);
470
+ }
347
471
  // Classify the user's intent inside build mode (exit / register / test / continue).
348
472
  const action = await classifyBuildModeAction(config, text, hooks.history || [], build);
349
473
  if (action.action === "exit") {
350
474
  intentRuntime.mode = "chat";
475
+ intentRuntime.preferred_skill_id = build.skill_id;
351
476
  intentRuntime.build = null;
352
477
  intentRuntime.pending_exit = null;
353
478
  return [l("Back to chat.", "◀︎ チャットに戻りました。")];
@@ -358,6 +483,12 @@ export async function handleBuildModeMessage(config, text, intentRuntime, hooks
358
483
  if (action.action === "test") {
359
484
  return handleTestAction(build, intentRuntime);
360
485
  }
486
+ // Recurring schedule requests should be honored inside build mode so the user
487
+ // doesn't have to leave the session just to call schedule-add.
488
+ const scheduleDecision = await classifyScheduleRequest(config, text, hooks.history || [], build);
489
+ if (scheduleDecision.matched) {
490
+ return handleScheduleAddAction(config, scheduleDecision.payload);
491
+ }
361
492
  // If the user's message looks like a routine chat / existing-skill request
362
493
  // (e.g. "メール調べて…"), quietly leave build mode so the normal chat engine
363
494
  // can run skills. Return null so the router falls through to chatRespond.
@@ -378,6 +509,37 @@ function handleTestAction(build, intentRuntime) {
378
509
  intentRuntime.pending_exit = null;
379
510
  return null;
380
511
  }
512
+ async function handleScheduleAddAction(config, payload) {
513
+ const args = {
514
+ id: payload.id,
515
+ cron: payload.cron,
516
+ skill: payload.skill,
517
+ };
518
+ if (payload.description)
519
+ args.description = payload.description;
520
+ if (payload.args)
521
+ args.args = payload.args;
522
+ if (payload.enabled !== undefined)
523
+ args.enabled = payload.enabled;
524
+ if (payload.approve !== undefined)
525
+ args.approve = payload.approve;
526
+ try {
527
+ const response = await runSkill(config, "schedule-add", args);
528
+ const summary = response.result.summary?.trim();
529
+ if (response.result.status === "ok") {
530
+ return [summary || l("Schedule registered.", "スケジュールを登録しました。")];
531
+ }
532
+ return [
533
+ summary
534
+ ? l(`Could not register schedule: ${summary}`, `スケジュールを登録できませんでした: ${summary}`)
535
+ : l("Could not register schedule.", "スケジュールを登録できませんでした。"),
536
+ ];
537
+ }
538
+ catch (error) {
539
+ const message = error instanceof Error ? error.message : String(error);
540
+ return [l(`Could not register schedule: ${message}`, `スケジュールを登録できませんでした: ${message}`)];
541
+ }
542
+ }
381
543
  async function handleRegisterAction(config, build, intentRuntime) {
382
544
  try {
383
545
  const lines = await buildRegisterLines(config, build.skill_id, {
@@ -44,7 +44,7 @@ export interface BuilderHandoffTurn {
44
44
  role: "user" | "assistant" | "tool";
45
45
  content: string;
46
46
  }
47
- export type BuilderEventSource = "discord" | "telegram" | "cli";
47
+ export type BuilderEventSource = "discord" | "telegram" | "g2" | "cli";
48
48
  export interface BuildTestResult {
49
49
  session: BuildSession;
50
50
  validation: ValidateSkillResult;