agent-sin 0.1.12 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +126 -72
- package/builtin-skills/memo-list/skill.yaml +8 -14
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +9 -3
- package/dist/core/chat-engine.js +1263 -146
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +596 -18
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +1 -0
- package/dist/discord/bot.js +181 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +115 -7
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
|
@@ -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
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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;
|