agent-sin 0.1.0
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 +33 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/assets/logo.png +0 -0
- package/builtin-skills/_shared/_models_lib.py +227 -0
- package/builtin-skills/_shared/_profile_lib.py +98 -0
- package/builtin-skills/_shared/_schedules_lib.py +313 -0
- package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
- package/builtin-skills/_shared/i18n.py +26 -0
- package/builtin-skills/memo-delete/main.py +155 -0
- package/builtin-skills/memo-delete/skill.yaml +57 -0
- package/builtin-skills/memo-index/main.py +178 -0
- package/builtin-skills/memo-index/skill.yaml +53 -0
- package/builtin-skills/memo-save/README.md +5 -0
- package/builtin-skills/memo-save/main.py +74 -0
- package/builtin-skills/memo-save/skill.yaml +52 -0
- package/builtin-skills/memo-search/README.md +10 -0
- package/builtin-skills/memo-search/main.py +97 -0
- package/builtin-skills/memo-search/skill.yaml +51 -0
- package/builtin-skills/memo-vector-search/main.py +121 -0
- package/builtin-skills/memo-vector-search/skill.yaml +53 -0
- package/builtin-skills/model-add/main.py +180 -0
- package/builtin-skills/model-add/skill.yaml +112 -0
- package/builtin-skills/model-list/main.py +93 -0
- package/builtin-skills/model-list/skill.yaml +48 -0
- package/builtin-skills/model-set/main.py +123 -0
- package/builtin-skills/model-set/skill.yaml +69 -0
- package/builtin-skills/profile-delete/_profile_lib.py +98 -0
- package/builtin-skills/profile-delete/main.py +98 -0
- package/builtin-skills/profile-delete/skill.yaml +64 -0
- package/builtin-skills/profile-edit/_profile_lib.py +98 -0
- package/builtin-skills/profile-edit/main.py +97 -0
- package/builtin-skills/profile-edit/skill.yaml +72 -0
- package/builtin-skills/profile-save/main.py +52 -0
- package/builtin-skills/profile-save/skill.yaml +69 -0
- package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-add/main.py +137 -0
- package/builtin-skills/schedule-add/skill.yaml +94 -0
- package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-list/main.py +86 -0
- package/builtin-skills/schedule-list/skill.yaml +45 -0
- package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-remove/main.py +69 -0
- package/builtin-skills/schedule-remove/skill.yaml +49 -0
- package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-toggle/main.py +78 -0
- package/builtin-skills/schedule-toggle/skill.yaml +61 -0
- package/builtin-skills/skills-disable/main.py +63 -0
- package/builtin-skills/skills-disable/skill.yaml +52 -0
- package/builtin-skills/skills-enable/main.py +62 -0
- package/builtin-skills/skills-enable/skill.yaml +51 -0
- package/builtin-skills/todo-add/main.py +68 -0
- package/builtin-skills/todo-add/skill.yaml +53 -0
- package/builtin-skills/todo-delete/main.py +65 -0
- package/builtin-skills/todo-delete/skill.yaml +47 -0
- package/builtin-skills/todo-done/main.py +75 -0
- package/builtin-skills/todo-done/skill.yaml +47 -0
- package/builtin-skills/todo-list/main.py +91 -0
- package/builtin-skills/todo-list/skill.yaml +48 -0
- package/builtin-skills/todo-tick/main.py +125 -0
- package/builtin-skills/todo-tick/skill.yaml +48 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +142 -0
- package/dist/builder/build-commands.d.ts +19 -0
- package/dist/builder/build-commands.js +133 -0
- package/dist/builder/build-flow.d.ts +72 -0
- package/dist/builder/build-flow.js +416 -0
- package/dist/builder/builder-session.d.ts +117 -0
- package/dist/builder/builder-session.js +1129 -0
- package/dist/builder/conversation-router.d.ts +22 -0
- package/dist/builder/conversation-router.js +69 -0
- package/dist/builder/intent-runtime-store.d.ts +7 -0
- package/dist/builder/intent-runtime-store.js +60 -0
- package/dist/builder/progress-format.d.ts +7 -0
- package/dist/builder/progress-format.js +46 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2835 -0
- package/dist/cli/spinner.d.ts +30 -0
- package/dist/cli/spinner.js +164 -0
- package/dist/core/ai-provider.d.ts +75 -0
- package/dist/core/ai-provider.js +678 -0
- package/dist/core/builtin-skills.d.ts +27 -0
- package/dist/core/builtin-skills.js +120 -0
- package/dist/core/chat-engine.d.ts +70 -0
- package/dist/core/chat-engine.js +812 -0
- package/dist/core/config.d.ts +127 -0
- package/dist/core/config.js +1379 -0
- package/dist/core/daily-memory-promotion.d.ts +21 -0
- package/dist/core/daily-memory-promotion.js +422 -0
- package/dist/core/i18n.d.ts +23 -0
- package/dist/core/i18n.js +167 -0
- package/dist/core/info-lines.d.ts +5 -0
- package/dist/core/info-lines.js +39 -0
- package/dist/core/input-schema.d.ts +2 -0
- package/dist/core/input-schema.js +156 -0
- package/dist/core/intent-router.d.ts +27 -0
- package/dist/core/intent-router.js +160 -0
- package/dist/core/logger.d.ts +60 -0
- package/dist/core/logger.js +240 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/memory.js +72 -0
- package/dist/core/message-utils.d.ts +13 -0
- package/dist/core/message-utils.js +104 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +424 -0
- package/dist/core/output-writer.d.ts +13 -0
- package/dist/core/output-writer.js +100 -0
- package/dist/core/plan-decision.d.ts +16 -0
- package/dist/core/plan-decision.js +88 -0
- package/dist/core/profile-memory.d.ts +17 -0
- package/dist/core/profile-memory.js +142 -0
- package/dist/core/runtime.d.ts +50 -0
- package/dist/core/runtime.js +187 -0
- package/dist/core/scheduler.d.ts +28 -0
- package/dist/core/scheduler.js +155 -0
- package/dist/core/secrets.d.ts +31 -0
- package/dist/core/secrets.js +214 -0
- package/dist/core/service.d.ts +35 -0
- package/dist/core/service.js +479 -0
- package/dist/core/skill-planner.d.ts +24 -0
- package/dist/core/skill-planner.js +100 -0
- package/dist/core/skill-registry.d.ts +98 -0
- package/dist/core/skill-registry.js +319 -0
- package/dist/core/skill-scaffold.d.ts +33 -0
- package/dist/core/skill-scaffold.js +256 -0
- package/dist/core/skill-settings.d.ts +11 -0
- package/dist/core/skill-settings.js +63 -0
- package/dist/core/transfer.d.ts +31 -0
- package/dist/core/transfer.js +270 -0
- package/dist/core/update-notifier.d.ts +2 -0
- package/dist/core/update-notifier.js +140 -0
- package/dist/discord/bot.d.ts +96 -0
- package/dist/discord/bot.js +2424 -0
- package/dist/runtimes/codex-app-server.d.ts +53 -0
- package/dist/runtimes/codex-app-server.js +305 -0
- package/dist/runtimes/python-runner.d.ts +7 -0
- package/dist/runtimes/python-runner.js +302 -0
- package/dist/runtimes/typescript-runner.d.ts +5 -0
- package/dist/runtimes/typescript-runner.js +172 -0
- package/dist/skills-sdk/types.d.ts +38 -0
- package/dist/skills-sdk/types.js +1 -0
- package/dist/telegram/bot.d.ts +94 -0
- package/dist/telegram/bot.js +1219 -0
- package/install.ps1 +132 -0
- package/install.sh +130 -0
- package/package.json +60 -0
- package/templates/skill-python/main.py +74 -0
- package/templates/skill-python/skill.yaml +48 -0
- package/templates/skill-typescript/main.ts +87 -0
- package/templates/skill-typescript/skill.yaml +42 -0
|
@@ -0,0 +1,2835 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { findAgentSinServiceProcesses, getServiceProvider, isSchedulerCommandLine, isSchedulerProcessRunning, serviceLabel, } from "../core/service.js";
|
|
7
|
+
import { configPath, defaultWorkspace, ensureDailyMemoIndexSchedule, ensureWorkspaceDirs, loadConfig, loadModels, modelsPath, setRoleModel, setupWorkspace, SetupRequiredError, writeModelsYaml, detectAvailableProviders, deriveSetupChoicePairIds, PROVIDER_CATALOG, } from "../core/config.js";
|
|
8
|
+
import { migrateLegacyBuiltinCopies } from "../core/builtin-skills.js";
|
|
9
|
+
import { setSkillEnabled } from "../core/skill-settings.js";
|
|
10
|
+
import { exportWorkspace, formatBytes, importWorkspace, listArchiveEntries, pathExists, } from "../core/transfer.js";
|
|
11
|
+
import { appendEventLog, dailyConversationMemoryFile, listRunLogs, readEventLog, readRunLog, } from "../core/logger.js";
|
|
12
|
+
import { runSkill } from "../core/runtime.js";
|
|
13
|
+
import { listSkillManifests } from "../core/skill-registry.js";
|
|
14
|
+
import { buildChatLines, buildLines, buildListLines, buildStatusLines, buildTestLines, } from "../builder/build-commands.js";
|
|
15
|
+
import { classifyPendingHandoff, enterBuildMode, handleBuildModeMessage, renderBuildFooter, } from "../builder/build-flow.js";
|
|
16
|
+
import { scaffoldSkill, validateInstalledSkill } from "../core/skill-scaffold.js";
|
|
17
|
+
import { ensureDotenvSkeleton, getApiKeyResolution, loadDotenv, maskKey, readDotenvKeys, upsertDotenv } from "../core/secrets.js";
|
|
18
|
+
import { loadSchedules, matchesCron, nextRunAfter } from "../core/scheduler.js";
|
|
19
|
+
import { appendProfileMemory, ensureProfileMemoryFiles, parseProfileMemoryTarget, profileMemoryPath, readProfileMemoryFiles, } from "../core/profile-memory.js";
|
|
20
|
+
import { maybePromoteDailyMemory, promoteDailyMemory, } from "../core/daily-memory-promotion.js";
|
|
21
|
+
import { notify } from "../core/notifier.js";
|
|
22
|
+
import { consumeUpdateBanner, scheduleUpdateCheck } from "../core/update-notifier.js";
|
|
23
|
+
import { shutdownSharedCodexAppServer } from "../runtimes/codex-app-server.js";
|
|
24
|
+
import { runDiscordBot } from "../discord/bot.js";
|
|
25
|
+
import { extractTelegramIdentityCandidates, runTelegramBot, } from "../telegram/bot.js";
|
|
26
|
+
import { Spinner } from "./spinner.js";
|
|
27
|
+
import { formatModelRow, modelSummary, modelsLines, skillsLines, } from "../core/info-lines.js";
|
|
28
|
+
import { inferLocaleFromText, l, lLines, t, withLocale } from "../core/i18n.js";
|
|
29
|
+
import { appendHistory, chatRespond, makeSpinnerProgress, } from "../core/chat-engine.js";
|
|
30
|
+
async function main() {
|
|
31
|
+
const [command, ...args] = process.argv.slice(2);
|
|
32
|
+
try {
|
|
33
|
+
const dotenv = await loadDotenv();
|
|
34
|
+
if (dotenv.permission_warning) {
|
|
35
|
+
console.error(`[agent-sin] ${l("warning", "警告")}: ${dotenv.permission_warning}`);
|
|
36
|
+
}
|
|
37
|
+
if (shouldAutoSetup(command)) {
|
|
38
|
+
await ensureWorkspaceInitialized();
|
|
39
|
+
}
|
|
40
|
+
switch (command) {
|
|
41
|
+
case "setup":
|
|
42
|
+
return await cmdSetup(args);
|
|
43
|
+
case undefined:
|
|
44
|
+
return await cmdChat(args);
|
|
45
|
+
case "start":
|
|
46
|
+
return await cmdStart(args);
|
|
47
|
+
case "chat":
|
|
48
|
+
return await cmdChat(args);
|
|
49
|
+
case "skills":
|
|
50
|
+
return await cmdSkills(args);
|
|
51
|
+
case "run":
|
|
52
|
+
return await cmdRun(args);
|
|
53
|
+
case "build":
|
|
54
|
+
return await cmdBuild(args);
|
|
55
|
+
case "models":
|
|
56
|
+
return await cmdModels(args);
|
|
57
|
+
case "model":
|
|
58
|
+
return await cmdModel(args);
|
|
59
|
+
case "logs":
|
|
60
|
+
return await cmdLogs(args);
|
|
61
|
+
case "config":
|
|
62
|
+
return await cmdConfig();
|
|
63
|
+
case "skill":
|
|
64
|
+
return await cmdSkill(args);
|
|
65
|
+
case "profile":
|
|
66
|
+
return await cmdProfile(args);
|
|
67
|
+
case "daemon":
|
|
68
|
+
return await cmdDaemon(args);
|
|
69
|
+
case "gateway":
|
|
70
|
+
return await cmdGateway(args);
|
|
71
|
+
case "service":
|
|
72
|
+
return await cmdService(args);
|
|
73
|
+
case "schedules":
|
|
74
|
+
return await cmdSchedules(args);
|
|
75
|
+
case "notify":
|
|
76
|
+
return await cmdNotify(args);
|
|
77
|
+
case "discord":
|
|
78
|
+
return await cmdDiscord(args);
|
|
79
|
+
case "telegram":
|
|
80
|
+
return await cmdTelegram(args);
|
|
81
|
+
case "export":
|
|
82
|
+
return await cmdExport(args);
|
|
83
|
+
case "import":
|
|
84
|
+
return await cmdImport(args);
|
|
85
|
+
case "help":
|
|
86
|
+
case "--help":
|
|
87
|
+
case "-h":
|
|
88
|
+
printHelp();
|
|
89
|
+
return 0;
|
|
90
|
+
default:
|
|
91
|
+
console.error(l(`Unknown command: ${command}`, `不明なコマンドです: ${command}`));
|
|
92
|
+
printHelp();
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof SetupRequiredError) {
|
|
98
|
+
console.error(error.message);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
console.error(message);
|
|
103
|
+
await tryLogCliError(command, args, message);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function shouldAutoSetup(command) {
|
|
108
|
+
switch (command) {
|
|
109
|
+
case "setup":
|
|
110
|
+
case "help":
|
|
111
|
+
case "--help":
|
|
112
|
+
case "-h":
|
|
113
|
+
case "notify":
|
|
114
|
+
case "export":
|
|
115
|
+
case "import":
|
|
116
|
+
return false;
|
|
117
|
+
default:
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function ensureWorkspaceInitialized() {
|
|
122
|
+
const file = configPath();
|
|
123
|
+
try {
|
|
124
|
+
await stat(file);
|
|
125
|
+
const config = await loadConfig();
|
|
126
|
+
await ensureWorkspaceDirs(config);
|
|
127
|
+
await migrateLegacyBuiltinCopies(config.skills_dir);
|
|
128
|
+
await ensureProfileMemoryFiles(config);
|
|
129
|
+
await ensureDailyMemoIndexSchedule(config);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!isMissingFileError(error)) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
console.log(l("agent-sin: first run. Creating workspace...", "agent-sin: 初回起動です。ワークスペースを作成します…"));
|
|
138
|
+
const config = await setupWorkspace({});
|
|
139
|
+
console.log(`Workspace: ${config.workspace}`);
|
|
140
|
+
console.log(l(`Default model: ${config.chat_model_id}`, `既定モデル: ${config.chat_model_id}`));
|
|
141
|
+
console.log("");
|
|
142
|
+
}
|
|
143
|
+
function isMissingFileError(error) {
|
|
144
|
+
return (typeof error === "object" &&
|
|
145
|
+
error !== null &&
|
|
146
|
+
"code" in error &&
|
|
147
|
+
error.code === "ENOENT");
|
|
148
|
+
}
|
|
149
|
+
async function tryLogCliError(command, args, message) {
|
|
150
|
+
try {
|
|
151
|
+
const config = await loadConfig();
|
|
152
|
+
await appendEventLog(config, {
|
|
153
|
+
level: "error",
|
|
154
|
+
source: "cli",
|
|
155
|
+
event: "command_failed",
|
|
156
|
+
message,
|
|
157
|
+
details: { command: command || "<none>", args },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Logging must not mask the original error.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function cmdStart(args) {
|
|
165
|
+
const options = parseOptions(args);
|
|
166
|
+
if (options.daemon || options.gateway) {
|
|
167
|
+
return cmdGateway(args.filter((arg) => arg !== "--daemon" && arg !== "--gateway"));
|
|
168
|
+
}
|
|
169
|
+
if (options.service) {
|
|
170
|
+
return cmdService(["start"]);
|
|
171
|
+
}
|
|
172
|
+
if (options.install_service) {
|
|
173
|
+
return cmdService(["install"]);
|
|
174
|
+
}
|
|
175
|
+
if (options.status) {
|
|
176
|
+
return cmdService(["status"]);
|
|
177
|
+
}
|
|
178
|
+
return cmdChat(args);
|
|
179
|
+
}
|
|
180
|
+
async function cmdSetup(args) {
|
|
181
|
+
const options = parseOptions(args);
|
|
182
|
+
if (options.help) {
|
|
183
|
+
printSetupHelp();
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
let setupOptions;
|
|
187
|
+
try {
|
|
188
|
+
setupOptions = shouldPromptSetup(options)
|
|
189
|
+
? await promptSetupOptions(optionsToSetupOptions(options))
|
|
190
|
+
: optionsToSetupOptions(options);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
console.error("");
|
|
195
|
+
console.error(l(`x setup was interrupted: ${message}`, `✗ setup を中断しました: ${message}`));
|
|
196
|
+
console.error(l(" Run `agent-sin setup` again to retry.", " 入力をやり直す場合はもう一度 `agent-sin setup` を実行してください。"));
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
const config = await setupWorkspace(setupOptions);
|
|
200
|
+
await applyCodexCliOptions(config, options);
|
|
201
|
+
console.log(l(`Workspace ready: ${config.workspace}`, `ワークスペース準備完了: ${config.workspace}`));
|
|
202
|
+
console.log(`Config: ${configPath(config.workspace)}`);
|
|
203
|
+
console.log(`Notes: ${config.notes_dir}`);
|
|
204
|
+
console.log(`Skills: ${config.skills_dir}`);
|
|
205
|
+
console.log(l(`Default model: ${config.chat_model_id}`, `既定モデル: ${config.chat_model_id}`));
|
|
206
|
+
console.log(l(`Builder: ${config.builder_model_id}`, `ビルダー: ${config.builder_model_id}`));
|
|
207
|
+
console.log(l("Next: agent-sin start", "次: agent-sin start"));
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
// CLI 引数 (--chat-codex-model など) で渡された値だけを反映する。
|
|
211
|
+
// 対話的な追加プロンプトは promptSetupOptions が担当するので、ここでは聞かない。
|
|
212
|
+
async function applyCodexCliOptions(config, options) {
|
|
213
|
+
const mChat = stringOption(options["chat-codex-model"]) || stringOption(options["codex-model"]);
|
|
214
|
+
const eChat = stringOption(options["chat-effort"]) || stringOption(options["codex-effort"]);
|
|
215
|
+
const mBuilder = stringOption(options["builder-codex-model"]);
|
|
216
|
+
const eBuilder = stringOption(options["builder-effort"]) || stringOption(options["codex-builder-effort"]);
|
|
217
|
+
const updates = [];
|
|
218
|
+
if (mChat || eChat) {
|
|
219
|
+
updates.push({ entry: config.chat_model_id, model: mChat, effort: eChat });
|
|
220
|
+
}
|
|
221
|
+
if (mBuilder || eBuilder) {
|
|
222
|
+
updates.push({ entry: config.builder_model_id, model: mBuilder, effort: eBuilder });
|
|
223
|
+
}
|
|
224
|
+
if (updates.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
const written = await applyModelEntryUpdates(config, updates);
|
|
227
|
+
if (written.length > 0) {
|
|
228
|
+
console.log("");
|
|
229
|
+
console.log(l("Updated models.yaml:", "models.yaml を更新しました:"));
|
|
230
|
+
for (const line of written) {
|
|
231
|
+
console.log(` ${line}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function applyModelEntryUpdates(config, updates) {
|
|
236
|
+
const models = await loadModels(config.workspace);
|
|
237
|
+
const lines = [];
|
|
238
|
+
for (const update of updates) {
|
|
239
|
+
const entry = models.models[update.entry];
|
|
240
|
+
if (!entry)
|
|
241
|
+
continue;
|
|
242
|
+
const fields = [];
|
|
243
|
+
if (update.model) {
|
|
244
|
+
entry.model = update.model;
|
|
245
|
+
fields.push(`model=${update.model}`);
|
|
246
|
+
}
|
|
247
|
+
if (update.effort) {
|
|
248
|
+
entry.effort = update.effort;
|
|
249
|
+
fields.push(`effort=${update.effort}`);
|
|
250
|
+
}
|
|
251
|
+
if (fields.length > 0) {
|
|
252
|
+
lines.push(`${update.entry}: ${fields.join(", ")}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (lines.length > 0) {
|
|
256
|
+
await writeModelsYaml(modelsPath(config.workspace), models);
|
|
257
|
+
}
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
async function cmdSkills(args = []) {
|
|
261
|
+
const sub = args[0];
|
|
262
|
+
if (sub === "restore") {
|
|
263
|
+
return cmdSkillsRestore();
|
|
264
|
+
}
|
|
265
|
+
if (sub === "enable" || sub === "disable") {
|
|
266
|
+
return cmdSkillsToggle(sub, args.slice(1));
|
|
267
|
+
}
|
|
268
|
+
if (sub && sub !== "list") {
|
|
269
|
+
console.error(l(`Unknown subcommand: skills ${sub}`, `不明なサブコマンドです: skills ${sub}`));
|
|
270
|
+
console.error(l("Usage: agent-sin skills [list|enable <id>|disable <id>|restore]", "使い方: agent-sin skills [list|enable <id>|disable <id>|restore]"));
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
const config = await loadConfig();
|
|
274
|
+
const lines = await skillsLines(config);
|
|
275
|
+
for (const line of lines) {
|
|
276
|
+
console.log(line);
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
async function cmdSkillsToggle(action, rest) {
|
|
281
|
+
const id = rest[0];
|
|
282
|
+
if (!id) {
|
|
283
|
+
console.error(l(`Usage: agent-sin skills ${action} <skill-id>`, `使い方: agent-sin skills ${action} <skill-id>`));
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
const config = await loadConfig();
|
|
287
|
+
const skills = await listSkillManifests(config.skills_dir);
|
|
288
|
+
const skill = skills.find((entry) => entry.id === id);
|
|
289
|
+
if (!skill) {
|
|
290
|
+
console.error(l(`Skill not found: ${id}`, `スキルが見つかりません: ${id}`));
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
const enabled = action === "enable";
|
|
294
|
+
const { changed } = await setSkillEnabled(config.workspace, id, enabled);
|
|
295
|
+
if (!changed) {
|
|
296
|
+
console.log(enabled
|
|
297
|
+
? l(`${id} is already enabled.`, `${id} は既に有効です。`)
|
|
298
|
+
: l(`${id} is already disabled.`, `${id} は既に無効です。`));
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
console.log(enabled ? l(`Enabled ${id}.`, `${id} を有効化しました。`) : l(`Disabled ${id}.`, `${id} を無効化しました。`));
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
async function cmdSkillsRestore() {
|
|
305
|
+
const config = await loadConfig();
|
|
306
|
+
const report = await migrateLegacyBuiltinCopies(config.skills_dir);
|
|
307
|
+
if (report.deleted.length === 0 &&
|
|
308
|
+
report.archived.length === 0 &&
|
|
309
|
+
report.retained.length === 0) {
|
|
310
|
+
console.log(l("No workspace copies of core skills were found. Already in sync.", "コアスキルの workspace 内コピーは見つかりませんでした。整合済みです。"));
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
if (report.deleted.length > 0) {
|
|
314
|
+
console.log(l(`Resynced and deleted: ${report.deleted.join(", ")}`, `再同期 (削除) しました: ${report.deleted.join(", ")}`));
|
|
315
|
+
}
|
|
316
|
+
if (report.archived.length > 0) {
|
|
317
|
+
console.log(l("Archived changed copies:", "差分があったため退避しました:"));
|
|
318
|
+
for (const entry of report.archived) {
|
|
319
|
+
console.log(` - ${entry.id} → ${entry.archivePath}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (report.retained.length > 0) {
|
|
323
|
+
console.log(l(`Kept because override: true: ${report.retained.join(", ")}`, `override: true で保持中: ${report.retained.join(", ")}`));
|
|
324
|
+
}
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
async function cmdRun(args) {
|
|
328
|
+
const [skillId, ...rest] = args;
|
|
329
|
+
if (!skillId) {
|
|
330
|
+
console.error(l("Usage: agent-sin run <skill-id> [--key value]", "使い方: agent-sin run <skill-id> [--key value]"));
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
const options = parseOptions(rest);
|
|
334
|
+
const payload = options.payload ? parseJsonOption(options.payload, "--payload") : optionsToArgs(options);
|
|
335
|
+
const approved = Boolean(options.approve);
|
|
336
|
+
delete payload.approve;
|
|
337
|
+
if (options._.length > 0) {
|
|
338
|
+
const positional = options._.join(" ");
|
|
339
|
+
if (skillId === "memo-search" && !("query" in payload)) {
|
|
340
|
+
payload.query = positional;
|
|
341
|
+
}
|
|
342
|
+
else if (["todo-done", "todo-delete"].includes(skillId) &&
|
|
343
|
+
!("id" in payload)) {
|
|
344
|
+
payload.id = positional;
|
|
345
|
+
}
|
|
346
|
+
else if (!("text" in payload)) {
|
|
347
|
+
payload.text = positional;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const config = await loadConfig();
|
|
351
|
+
const response = await runSkill(config, skillId, payload, { approved });
|
|
352
|
+
console.log(response.result.title);
|
|
353
|
+
if (response.result.summary) {
|
|
354
|
+
console.log(response.result.summary);
|
|
355
|
+
}
|
|
356
|
+
for (const saved of response.saved_outputs.filter((item) => item.show_saved !== false)) {
|
|
357
|
+
console.log(l(`saved: ${saved.path}`, `保存: ${saved.path}`));
|
|
358
|
+
}
|
|
359
|
+
if (response.memory_path) {
|
|
360
|
+
console.log(l(`memory: ${response.memory_path}`, `記憶: ${response.memory_path}`));
|
|
361
|
+
}
|
|
362
|
+
if (response.attempts > 1) {
|
|
363
|
+
console.log(l(`attempts: ${response.attempts}`, `試行回数: ${response.attempts}`));
|
|
364
|
+
}
|
|
365
|
+
console.log(`run: ${response.run_id}`);
|
|
366
|
+
return response.result.status === "error" ? 1 : 0;
|
|
367
|
+
}
|
|
368
|
+
async function cmdChat(args) {
|
|
369
|
+
const config = await loadConfig();
|
|
370
|
+
scheduleUpdateCheck(config.workspace);
|
|
371
|
+
const history = [];
|
|
372
|
+
if (args.length > 0) {
|
|
373
|
+
const lines = await handleChatMessage(config, args.join(" "), history);
|
|
374
|
+
const banner = await consumeUpdateBanner(config.workspace);
|
|
375
|
+
if (banner) {
|
|
376
|
+
console.log(banner);
|
|
377
|
+
}
|
|
378
|
+
for (const line of lines) {
|
|
379
|
+
console.log(line);
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
await warnIfSchedulesNeedService(config);
|
|
384
|
+
const intentRuntime = {
|
|
385
|
+
pending: null,
|
|
386
|
+
pending_exit: null,
|
|
387
|
+
preferred_skill_id: null,
|
|
388
|
+
progress_detail: false,
|
|
389
|
+
enabled: true,
|
|
390
|
+
mode: "chat",
|
|
391
|
+
build: null,
|
|
392
|
+
};
|
|
393
|
+
const state = {
|
|
394
|
+
config,
|
|
395
|
+
models: await loadModels(config.workspace),
|
|
396
|
+
skills: await listSkillManifests(config.skills_dir),
|
|
397
|
+
};
|
|
398
|
+
const rl = createInterface({
|
|
399
|
+
input,
|
|
400
|
+
output,
|
|
401
|
+
completer: (line) => completeChatLine(line, state),
|
|
402
|
+
});
|
|
403
|
+
if (uiActive()) {
|
|
404
|
+
for (const line of renderStartupBanner(state)) {
|
|
405
|
+
console.log(line);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
console.log(l("agent-sin chat. /help / /reset / /exit (Tab completion)", "agent-sin chat. /help / /reset / /exit (Tabで補完)"));
|
|
410
|
+
console.log(l("mode: chat | build/edit mode is suggested automatically when useful", "mode: chat | 必要に応じてビルド/編集モードに自動で切替提案します"));
|
|
411
|
+
}
|
|
412
|
+
while (true) {
|
|
413
|
+
let raw;
|
|
414
|
+
const frameTop = renderInputFrameTop();
|
|
415
|
+
if (frameTop) {
|
|
416
|
+
console.log(frameTop);
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
raw = await rl.question(renderInputPromptPrefix(intentRuntime, state.config.chat_model_id));
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
rl.close();
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
const frameBottom = renderInputFrameBottom();
|
|
426
|
+
if (frameBottom) {
|
|
427
|
+
console.log(frameBottom);
|
|
428
|
+
const status = renderStatusLine(state, intentRuntime);
|
|
429
|
+
if (status) {
|
|
430
|
+
console.log(status);
|
|
431
|
+
}
|
|
432
|
+
console.log("");
|
|
433
|
+
}
|
|
434
|
+
const text = raw.trim();
|
|
435
|
+
if (["exit", "quit", "/exit", "/quit"].includes(text)) {
|
|
436
|
+
rl.close();
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
if (text === "/model" || text === "/model chat" || text === "/model builder") {
|
|
440
|
+
const presetRole = text === "/model chat" ? "chat" : text === "/model builder" ? "builder" : undefined;
|
|
441
|
+
await refreshChatState(state);
|
|
442
|
+
const lines = await interactiveModelPicker(state, rl, presetRole);
|
|
443
|
+
for (const line of lines) {
|
|
444
|
+
console.log(formatChatLine(line));
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (text === "/skills" || text === "skills" || text === "/skills --all" || text === "skills --all") {
|
|
449
|
+
await refreshChatState(state);
|
|
450
|
+
const showAll = text.endsWith("--all");
|
|
451
|
+
const lines = await interactiveSkillsPicker(state, rl, { showAll });
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
console.log(formatChatLine(line));
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const lines = await handleChatMessage(state.config, text, history, intentRuntime);
|
|
458
|
+
scheduleUpdateCheck(state.config.workspace);
|
|
459
|
+
const banner = await consumeUpdateBanner(state.config.workspace);
|
|
460
|
+
if (banner) {
|
|
461
|
+
console.log(formatChatLine(banner));
|
|
462
|
+
}
|
|
463
|
+
for (const line of lines) {
|
|
464
|
+
console.log(formatChatLine(line));
|
|
465
|
+
}
|
|
466
|
+
// モデル変更系のスキル (model-add / model-set) が呼ばれた場合に
|
|
467
|
+
// 次ターンへ反映できるよう、毎ターン state を再読込する。Discord / Telegram も
|
|
468
|
+
// 同じパターン (refreshStateConfig)。
|
|
469
|
+
await refreshChatState(state);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function refreshChatState(state) {
|
|
473
|
+
state.config = await loadConfig();
|
|
474
|
+
state.models = await loadModels(state.config.workspace);
|
|
475
|
+
state.skills = await listSkillManifests(state.config.skills_dir);
|
|
476
|
+
}
|
|
477
|
+
const SLASH_COMMANDS = [
|
|
478
|
+
"/skills",
|
|
479
|
+
"/run",
|
|
480
|
+
"/models",
|
|
481
|
+
"/model",
|
|
482
|
+
"/build",
|
|
483
|
+
"/logs",
|
|
484
|
+
"/config",
|
|
485
|
+
"/profile",
|
|
486
|
+
"/reset",
|
|
487
|
+
"/help",
|
|
488
|
+
"/exit",
|
|
489
|
+
];
|
|
490
|
+
function completeChatLine(line, state) {
|
|
491
|
+
const skillIds = state.skills.map((skill) => skill.id);
|
|
492
|
+
const modelIds = Object.keys(state.models.models);
|
|
493
|
+
if (line.startsWith("/run ")) {
|
|
494
|
+
const partial = line.slice(5);
|
|
495
|
+
const hits = skillIds.filter((id) => id.startsWith(partial));
|
|
496
|
+
return [hits.map((id) => `/run ${id}`), line];
|
|
497
|
+
}
|
|
498
|
+
if (line.startsWith("/model chat ") || line.startsWith("/model builder ")) {
|
|
499
|
+
const role = line.startsWith("/model chat ") ? "chat" : "builder";
|
|
500
|
+
const partial = line.slice(`/model ${role} `.length);
|
|
501
|
+
const hits = modelIds.filter((id) => id.startsWith(partial));
|
|
502
|
+
return [hits.map((id) => `/model ${role} ${id}`), line];
|
|
503
|
+
}
|
|
504
|
+
if (line.startsWith("/model set ")) {
|
|
505
|
+
const partial = line.slice(11);
|
|
506
|
+
const hits = modelIds.filter((id) => id.startsWith(partial));
|
|
507
|
+
return [hits.map((id) => `/model set ${id}`), line];
|
|
508
|
+
}
|
|
509
|
+
if (line.startsWith("/model ")) {
|
|
510
|
+
const partial = line.slice(7);
|
|
511
|
+
const candidates = ["chat", "builder", "set", ...modelIds];
|
|
512
|
+
const hits = candidates.filter((id) => id.startsWith(partial));
|
|
513
|
+
return [hits.map((id) => `/model ${id}`), line];
|
|
514
|
+
}
|
|
515
|
+
if (line.startsWith("/build ")) {
|
|
516
|
+
const partial = line.slice(7);
|
|
517
|
+
const candidates = ["list", "chat", "test", "status", ...skillIds];
|
|
518
|
+
const hits = candidates.filter((id) => id.startsWith(partial));
|
|
519
|
+
return [hits.map((id) => `/build ${id}`), line];
|
|
520
|
+
}
|
|
521
|
+
if (line.startsWith("/")) {
|
|
522
|
+
const hits = SLASH_COMMANDS.filter((command) => command.startsWith(line));
|
|
523
|
+
return [hits.length ? hits : SLASH_COMMANDS, line];
|
|
524
|
+
}
|
|
525
|
+
return [[], line];
|
|
526
|
+
}
|
|
527
|
+
async function interactiveModelPicker(state, rl, presetRole) {
|
|
528
|
+
const chatId = state.config.chat_model_id;
|
|
529
|
+
const builderId = state.config.builder_model_id;
|
|
530
|
+
const chatEntry = state.models.models[chatId];
|
|
531
|
+
const builderEntry = state.models.models[builderId];
|
|
532
|
+
let role = presetRole;
|
|
533
|
+
if (!role) {
|
|
534
|
+
console.log(l("Which role do you want to change?", "どちらを変更しますか?"));
|
|
535
|
+
console.log(l(` 1) chat current: ${chatEntry ? modelSummary(chatId, chatEntry) : chatId}`, ` 1) chat 現在: ${chatEntry ? modelSummary(chatId, chatEntry) : chatId}`));
|
|
536
|
+
console.log(l(` 2) builder current: ${builderEntry ? modelSummary(builderId, builderEntry) : builderId}`, ` 2) builder 現在: ${builderEntry ? modelSummary(builderId, builderEntry) : builderId}`));
|
|
537
|
+
const answer = (await rl.question(l("Enter a number (Enter to cancel): ", "番号を入力 (Enterでキャンセル): "))).trim();
|
|
538
|
+
if (!answer) {
|
|
539
|
+
return [l("No changes made.", "変更しませんでした。")];
|
|
540
|
+
}
|
|
541
|
+
if (answer === "1" || answer.toLowerCase() === "chat") {
|
|
542
|
+
role = "chat";
|
|
543
|
+
}
|
|
544
|
+
else if (answer === "2" || answer.toLowerCase() === "builder") {
|
|
545
|
+
role = "builder";
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
return [l("Invalid selection.", "無効な選択です。")];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const entries = Object.entries(state.models.models);
|
|
552
|
+
console.log(l(`Available models (set for ${role}):`, `利用可能モデル (${role} に設定):`));
|
|
553
|
+
entries.forEach(([id, model], index) => {
|
|
554
|
+
const formatted = formatModelRow(id, model, chatId, builderId);
|
|
555
|
+
console.log(` ${String(index + 1).padStart(2)}) ${formatted}`);
|
|
556
|
+
});
|
|
557
|
+
const pick = (await rl.question(l("Enter a number (Enter to keep current): ", "番号を入力 (Enterで変更しない): "))).trim();
|
|
558
|
+
if (!pick) {
|
|
559
|
+
return [l("No changes made.", "変更しませんでした。")];
|
|
560
|
+
}
|
|
561
|
+
const num = Number.parseInt(pick, 10);
|
|
562
|
+
if (!Number.isInteger(num) || num < 1 || num > entries.length) {
|
|
563
|
+
return [l("Invalid selection.", "無効な選択です。")];
|
|
564
|
+
}
|
|
565
|
+
const [newId] = entries[num - 1];
|
|
566
|
+
try {
|
|
567
|
+
await setRoleModel(role, newId);
|
|
568
|
+
await refreshChatState(state);
|
|
569
|
+
return [l(`Default ${role} model: ${newId}`, `既定の ${role} モデル: ${newId}`)];
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function summarizeBuiltinPrefixes(skills) {
|
|
576
|
+
const prefixes = new Set();
|
|
577
|
+
for (const skill of skills) {
|
|
578
|
+
const dash = skill.id.indexOf("-");
|
|
579
|
+
prefixes.add(dash > 0 ? skill.id.slice(0, dash) : skill.id);
|
|
580
|
+
}
|
|
581
|
+
return Array.from(prefixes).sort().join(" / ");
|
|
582
|
+
}
|
|
583
|
+
async function interactiveSkillsPicker(state, rl, options = {}) {
|
|
584
|
+
if (state.skills.length === 0) {
|
|
585
|
+
return [l("No skills registered.", "登録済みのスキルはありません。")];
|
|
586
|
+
}
|
|
587
|
+
const showAll = options.showAll === true;
|
|
588
|
+
const userSkills = state.skills.filter((skill) => skill.source !== "builtin");
|
|
589
|
+
const builtinSkills = state.skills.filter((skill) => skill.source === "builtin");
|
|
590
|
+
const visible = showAll ? state.skills : userSkills;
|
|
591
|
+
console.log(l("Registered skills:", "登録済みスキル:"));
|
|
592
|
+
if (visible.length === 0) {
|
|
593
|
+
console.log(l(" (no user-created skills yet)", " (ユーザースキルはまだありません)"));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
visible.forEach((skill, index) => {
|
|
597
|
+
const enabled = skill.enabled === false ? l("disabled", "無効") : l("enabled", "有効");
|
|
598
|
+
const builtin = skill.source === "builtin" ? " [builtin]" : "";
|
|
599
|
+
console.log(` ${String(index + 1).padStart(2)}) ${skill.id}${builtin}\t${skill.name}\t${enabled}`);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (!showAll && builtinSkills.length > 0) {
|
|
603
|
+
const groups = summarizeBuiltinPrefixes(builtinSkills);
|
|
604
|
+
console.log("");
|
|
605
|
+
console.log(l(`Builtins (${builtinSkills.length}): ${groups} — /skills --all to expand`, `ビルトイン (${builtinSkills.length} 個): ${groups} ─ /skills --all で展開`));
|
|
606
|
+
}
|
|
607
|
+
const pick = (await rl.question(l("Enter a number (Enter to go back): ", "番号を入力 (Enterで戻る): "))).trim();
|
|
608
|
+
if (!pick) {
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
611
|
+
const num = Number.parseInt(pick, 10);
|
|
612
|
+
if (!Number.isInteger(num) || num < 1 || num > visible.length) {
|
|
613
|
+
return [l("Invalid selection.", "無効な選択です。")];
|
|
614
|
+
}
|
|
615
|
+
const skill = visible[num - 1];
|
|
616
|
+
const isBuiltin = skill.source === "builtin";
|
|
617
|
+
console.log("");
|
|
618
|
+
console.log(`■ ${skill.id} (${skill.name})${isBuiltin ? " [builtin]" : ""}`);
|
|
619
|
+
console.log(` runtime: ${skill.runtime} / ${skill.enabled === false ? l("disabled", "無効") : l("enabled", "有効")}`);
|
|
620
|
+
if (skill.description) {
|
|
621
|
+
console.log(` ${skill.description}`);
|
|
622
|
+
}
|
|
623
|
+
console.log(` dir: ${skill.dir}`);
|
|
624
|
+
console.log("");
|
|
625
|
+
if (isBuiltin) {
|
|
626
|
+
console.log(l("i Builtin skills are loaded from the package and cannot be deleted here.", "ℹ ビルトインスキルはパッケージから読み込まれるため、ここから削除はできません。"));
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
const action = (await rl.question(l("[d] delete / [Enter] back: ", "[d] 削除 / [Enter] 戻る: "))).trim().toLowerCase();
|
|
630
|
+
if (action !== "d" && action !== "delete") {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
const confirm = (await rl.question(l(`Delete "${skill.id}"? [y/N]: `, `本当に "${skill.id}" を削除しますか? [y/N]: `))).trim().toLowerCase();
|
|
634
|
+
if (confirm !== "y" && confirm !== "yes") {
|
|
635
|
+
return [l("Deletion canceled.", "削除をキャンセルしました。")];
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
await rm(skill.dir, { recursive: true, force: true });
|
|
639
|
+
await refreshChatState(state);
|
|
640
|
+
return [l(`Deleted skill: ${skill.id}`, `スキルを削除しました: ${skill.id}`)];
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
return [l(`Delete failed: ${error instanceof Error ? error.message : String(error)}`, `削除に失敗しました: ${error instanceof Error ? error.message : String(error)}`)];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function cmdBuild(args) {
|
|
647
|
+
const config = await loadConfig();
|
|
648
|
+
if (args[0] === "list") {
|
|
649
|
+
const lines = await buildListLines(config);
|
|
650
|
+
for (const line of lines) {
|
|
651
|
+
console.log(line);
|
|
652
|
+
}
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
if (args[0] === "register") {
|
|
656
|
+
console.error(l("agent-sin build register is deprecated. Builder writes directly to ~/.agent-sin/skills/<id>/, so there is no registration step.", "agent-sin build register は廃止されました。Builder が ~/.agent-sin/skills/<id>/ に直接書き込むので、登録ステップはありません。"));
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
if (args[0] === "test") {
|
|
660
|
+
const [skillId, ...rest] = args.slice(1);
|
|
661
|
+
if (!skillId) {
|
|
662
|
+
console.error(l("Usage: agent-sin build test <skill-id> [--payload '{...}']", "使い方: agent-sin build test <skill-id> [--payload '{...}']"));
|
|
663
|
+
return 1;
|
|
664
|
+
}
|
|
665
|
+
const options = parseOptions(rest);
|
|
666
|
+
const lines = await buildTestLines(config, skillId, buildPayloadFromOptions(options));
|
|
667
|
+
for (const line of lines) {
|
|
668
|
+
console.log(line);
|
|
669
|
+
}
|
|
670
|
+
// ✅ marks the success path in the new conversational format.
|
|
671
|
+
return lines.some((line) => line.startsWith("✅") || line.includes("登録できます")) ? 0 : 1;
|
|
672
|
+
}
|
|
673
|
+
if (args[0] === "chat") {
|
|
674
|
+
const [skillId, ...messageParts] = args.slice(1);
|
|
675
|
+
if (!skillId || messageParts.length === 0) {
|
|
676
|
+
console.error(l('Usage: agent-sin build chat <skill-id> "message"', '使い方: agent-sin build chat <skill-id> "メッセージ"'));
|
|
677
|
+
return 1;
|
|
678
|
+
}
|
|
679
|
+
const message = messageParts.join(" ");
|
|
680
|
+
const lines = await withCliBuildHooks(config, message, (hooks) => buildChatLines(config, skillId, message, { onProgress: hooks.onProgress }));
|
|
681
|
+
for (const line of lines) {
|
|
682
|
+
console.log(line);
|
|
683
|
+
}
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
if (args[0] === "status") {
|
|
687
|
+
const skillId = args[1];
|
|
688
|
+
if (!skillId) {
|
|
689
|
+
console.error(l("Usage: agent-sin build status <skill-id>", "使い方: agent-sin build status <skill-id>"));
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
const lines = await buildStatusLines(config, skillId);
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
console.log(line);
|
|
695
|
+
}
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
const options = parseOptions(args);
|
|
699
|
+
const [skillId, ...messageParts] = options._;
|
|
700
|
+
const prompt = stringOption(options.prompt) || messageParts.join(" ");
|
|
701
|
+
const runtime = stringOption(options.runtime);
|
|
702
|
+
const builder = stringOption(options.builder);
|
|
703
|
+
const accessRaw = stringOption(options["access-mode"]);
|
|
704
|
+
const accessMode = accessRaw === "full" ? "full" : accessRaw === "approval" ? "approval" : undefined;
|
|
705
|
+
const lines = await withCliBuildHooks(config, prompt || skillId || "build", (hooks) => buildLines(config, skillId, {
|
|
706
|
+
prompt,
|
|
707
|
+
runtime: runtime === "typescript" ? "typescript" : runtime === "python" ? "python" : undefined,
|
|
708
|
+
builder,
|
|
709
|
+
accessMode,
|
|
710
|
+
onProgress: hooks.onProgress,
|
|
711
|
+
}));
|
|
712
|
+
for (const line of lines) {
|
|
713
|
+
console.log(line);
|
|
714
|
+
}
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
async function cmdSkill(args) {
|
|
718
|
+
const [sub, ...rest] = args;
|
|
719
|
+
switch (sub) {
|
|
720
|
+
case "new":
|
|
721
|
+
return cmdSkillNew(rest);
|
|
722
|
+
case "validate":
|
|
723
|
+
return cmdSkillValidate(rest);
|
|
724
|
+
case "test":
|
|
725
|
+
return cmdSkillTest(rest);
|
|
726
|
+
default:
|
|
727
|
+
console.error(l("Usage:\n" +
|
|
728
|
+
" agent-sin skill new <id> [--runtime python|typescript] [--name ...] [--description ...]\n" +
|
|
729
|
+
" agent-sin skill validate <id>\n" +
|
|
730
|
+
" agent-sin skill test <id> [--payload '{...}'] [--key value]", "使い方:\n" +
|
|
731
|
+
" agent-sin skill new <id> [--runtime python|typescript] [--name ...] [--description ...]\n" +
|
|
732
|
+
" agent-sin skill validate <id>\n" +
|
|
733
|
+
" agent-sin skill test <id> [--payload '{...}'] [--key value]"));
|
|
734
|
+
return 1;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async function cmdSkillNew(args) {
|
|
738
|
+
const options = parseOptions(args);
|
|
739
|
+
const id = String(options._[0] || "").trim();
|
|
740
|
+
if (!id) {
|
|
741
|
+
console.error(l("Usage: agent-sin skill new <id> [--runtime python|typescript] [--name ...] [--description ...]", "使い方: agent-sin skill new <id> [--runtime python|typescript] [--name ...] [--description ...]"));
|
|
742
|
+
return 1;
|
|
743
|
+
}
|
|
744
|
+
const runtimeRaw = stringOption(options.runtime) || "python";
|
|
745
|
+
if (runtimeRaw !== "python" && runtimeRaw !== "typescript") {
|
|
746
|
+
console.error(l(`Invalid runtime: ${runtimeRaw} (allowed: python, typescript)`, `runtime が不正です: ${runtimeRaw} (使用可: python, typescript)`));
|
|
747
|
+
return 1;
|
|
748
|
+
}
|
|
749
|
+
const config = await loadConfig();
|
|
750
|
+
const result = await scaffoldSkill(config, {
|
|
751
|
+
id,
|
|
752
|
+
runtime: runtimeRaw,
|
|
753
|
+
name: stringOption(options.name),
|
|
754
|
+
description: stringOption(options.description),
|
|
755
|
+
});
|
|
756
|
+
console.log(l(`Created skill: ${result.skill_id}`, `スキルを作成しました: ${result.skill_id}`));
|
|
757
|
+
console.log(l(`Path: ${result.skill_dir}`, `パス: ${result.skill_dir}`));
|
|
758
|
+
for (const file of result.files) {
|
|
759
|
+
console.log(` + ${file}`);
|
|
760
|
+
}
|
|
761
|
+
console.log(l("Next:", "次:"));
|
|
762
|
+
console.log(` ${process.env.EDITOR || "$EDITOR"} ${result.entry_path}`);
|
|
763
|
+
console.log(` agent-sin skill validate ${result.skill_id}`);
|
|
764
|
+
console.log(` agent-sin skill test ${result.skill_id} --payload '{"text":"hello"}'`);
|
|
765
|
+
await appendEventLog(config, {
|
|
766
|
+
level: "info",
|
|
767
|
+
source: "build",
|
|
768
|
+
event: "skill_scaffolded",
|
|
769
|
+
message: `Created ${result.skill_id} (${result.runtime})`,
|
|
770
|
+
details: { skill_id: result.skill_id, runtime: result.runtime, files: result.files.length },
|
|
771
|
+
});
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
async function cmdSkillValidate(args) {
|
|
775
|
+
const options = parseOptions(args);
|
|
776
|
+
const id = String(options._[0] || "").trim();
|
|
777
|
+
if (!id) {
|
|
778
|
+
console.error(l("Usage: agent-sin skill validate <id>", "使い方: agent-sin skill validate <id>"));
|
|
779
|
+
return 1;
|
|
780
|
+
}
|
|
781
|
+
const config = await loadConfig();
|
|
782
|
+
const result = await validateInstalledSkill(config, id);
|
|
783
|
+
for (const error of result.errors) {
|
|
784
|
+
console.log(`[error] ${error}`);
|
|
785
|
+
}
|
|
786
|
+
for (const warning of result.warnings) {
|
|
787
|
+
console.log(`[warn] ${warning}`);
|
|
788
|
+
}
|
|
789
|
+
if (result.ok && result.manifest) {
|
|
790
|
+
const manifest = result.manifest;
|
|
791
|
+
console.log(`[ok] ${manifest.id} (${manifest.runtime})`);
|
|
792
|
+
console.log(` dir: ${result.skill_dir}`);
|
|
793
|
+
console.log(` entry: ${path.join(result.skill_dir, manifest.entry)}`);
|
|
794
|
+
console.log(` outputs: ${manifest.outputs.length}`);
|
|
795
|
+
if (manifest.ai_steps && manifest.ai_steps.length > 0) {
|
|
796
|
+
console.log(` ai: ${manifest.ai_steps.map((step) => step.id).join(", ")}`);
|
|
797
|
+
}
|
|
798
|
+
if (manifest.memory) {
|
|
799
|
+
console.log(` memory: ${manifest.memory.namespace} (read=${manifest.memory.read ?? false}, write=${manifest.memory.write ?? false})`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
await appendEventLog(config, {
|
|
803
|
+
level: result.ok ? "info" : "error",
|
|
804
|
+
source: "build",
|
|
805
|
+
event: "skill_validated",
|
|
806
|
+
message: result.ok ? `valid ${id}` : `invalid ${id} (${result.errors.length} error(s))`,
|
|
807
|
+
details: {
|
|
808
|
+
skill_id: id,
|
|
809
|
+
ok: result.ok,
|
|
810
|
+
errors: result.errors.length,
|
|
811
|
+
warnings: result.warnings.length,
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
return result.ok ? 0 : 1;
|
|
815
|
+
}
|
|
816
|
+
async function cmdSkillTest(args) {
|
|
817
|
+
const options = parseOptions(args);
|
|
818
|
+
const id = String(options._[0] || "").trim();
|
|
819
|
+
if (!id) {
|
|
820
|
+
console.error(l("Usage: agent-sin skill test <id> [--payload '{...}'] [--key value]", "使い方: agent-sin skill test <id> [--payload '{...}'] [--key value]"));
|
|
821
|
+
return 1;
|
|
822
|
+
}
|
|
823
|
+
const payload = buildPayloadFromOptions(options) || {};
|
|
824
|
+
const positional = options._.slice(1);
|
|
825
|
+
if (positional.length > 0 && !("text" in payload)) {
|
|
826
|
+
payload.text = positional.join(" ");
|
|
827
|
+
}
|
|
828
|
+
const config = await loadConfig();
|
|
829
|
+
const response = await runSkill(config, id, payload, { dryRun: true });
|
|
830
|
+
console.log(`[dry-run] ${response.result.status} ${response.result.title}`);
|
|
831
|
+
if (response.result.summary) {
|
|
832
|
+
console.log(` ${response.result.summary}`);
|
|
833
|
+
}
|
|
834
|
+
if (response.result.outputs && Object.keys(response.result.outputs).length > 0) {
|
|
835
|
+
console.log(l(" would save outputs:", " 保存予定の出力:"));
|
|
836
|
+
for (const [outputId, output] of Object.entries(response.result.outputs)) {
|
|
837
|
+
const preview = JSON.stringify(output).slice(0, 200);
|
|
838
|
+
console.log(` ${outputId}: ${preview}${preview.length >= 200 ? "..." : ""}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (response.result.data && Object.keys(response.result.data).length > 0) {
|
|
842
|
+
console.log(` data: ${JSON.stringify(response.result.data)}`);
|
|
843
|
+
}
|
|
844
|
+
if (response.result.suggestions && response.result.suggestions.length > 0) {
|
|
845
|
+
for (const suggestion of response.result.suggestions) {
|
|
846
|
+
console.log(l(` suggest: ${suggestion}`, ` 提案: ${suggestion}`));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
console.log(l(`run: ${response.run_id} (no files saved, no memory updates)`, `run: ${response.run_id} (ファイル保存なし、記憶更新なし)`));
|
|
850
|
+
return response.result.status === "error" ? 1 : 0;
|
|
851
|
+
}
|
|
852
|
+
async function cmdProfile(args) {
|
|
853
|
+
const config = await loadConfig();
|
|
854
|
+
const lines = await profileLines(config, args);
|
|
855
|
+
for (const line of lines) {
|
|
856
|
+
console.log(line);
|
|
857
|
+
}
|
|
858
|
+
return 0;
|
|
859
|
+
}
|
|
860
|
+
async function profileLines(config, args) {
|
|
861
|
+
const [sub = "show", ...rest] = args;
|
|
862
|
+
if (sub === "init") {
|
|
863
|
+
const files = await ensureProfileMemoryFiles(config);
|
|
864
|
+
return [
|
|
865
|
+
l("profile memory ready", "プロフィール記憶を準備しました"),
|
|
866
|
+
`soul: ${files.paths.soul}`,
|
|
867
|
+
`user: ${files.paths.user}`,
|
|
868
|
+
`memory: ${files.paths.memory}`,
|
|
869
|
+
`daily: ${dailyConversationMemoryFile(config)}`,
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
if (sub === "path" || sub === "paths") {
|
|
873
|
+
return [
|
|
874
|
+
`soul: ${profileMemoryPath(config, "soul")}`,
|
|
875
|
+
`user: ${profileMemoryPath(config, "user")}`,
|
|
876
|
+
`memory: ${profileMemoryPath(config, "memory")}`,
|
|
877
|
+
`daily: ${dailyConversationMemoryFile(config)}`,
|
|
878
|
+
];
|
|
879
|
+
}
|
|
880
|
+
if (sub === "append") {
|
|
881
|
+
const target = parseProfileMemoryTarget(rest[0]);
|
|
882
|
+
const text = rest.slice(1).join(" ").trim();
|
|
883
|
+
if (!target || !text) {
|
|
884
|
+
return [l("Usage: agent-sin profile append <soul|user|memory> <text>", "使い方: agent-sin profile append <soul|user|memory> <text>")];
|
|
885
|
+
}
|
|
886
|
+
const file = await appendProfileMemory(config, target, text);
|
|
887
|
+
return [l(`saved: ${file}`, `保存: ${file}`)];
|
|
888
|
+
}
|
|
889
|
+
if (sub === "promote") {
|
|
890
|
+
const options = parseOptions(rest);
|
|
891
|
+
const result = await promoteDailyMemory(config, {
|
|
892
|
+
date: stringOption(options.date) || stringOption(options._[0]),
|
|
893
|
+
force: Boolean(options.force),
|
|
894
|
+
dryRun: Boolean(options.dry_run),
|
|
895
|
+
modelId: stringOption(options.model),
|
|
896
|
+
eventSource: "cli",
|
|
897
|
+
});
|
|
898
|
+
return formatPromotionLines(result, Boolean(options.dry_run));
|
|
899
|
+
}
|
|
900
|
+
if (sub === "show") {
|
|
901
|
+
const target = parseProfileMemoryTarget(rest[0]);
|
|
902
|
+
return showProfileLines(config, target);
|
|
903
|
+
}
|
|
904
|
+
const target = parseProfileMemoryTarget(sub);
|
|
905
|
+
if (target) {
|
|
906
|
+
return showProfileLines(config, target);
|
|
907
|
+
}
|
|
908
|
+
return [
|
|
909
|
+
l("Usage:", "使い方:"),
|
|
910
|
+
" agent-sin profile show [soul|user|memory]",
|
|
911
|
+
" agent-sin profile append <soul|user|memory> <text>",
|
|
912
|
+
" agent-sin profile promote [--date YYYY-MM-DD] [--force] [--dry-run]",
|
|
913
|
+
" agent-sin profile path",
|
|
914
|
+
];
|
|
915
|
+
}
|
|
916
|
+
function formatPromotionLines(result, dryRun = false) {
|
|
917
|
+
const prefix = dryRun ? "[dry-run] " : "";
|
|
918
|
+
if (result.status === "promoted") {
|
|
919
|
+
return [
|
|
920
|
+
l(`${prefix}promoted: ${result.date} → memory.md (${result.items.length} item(s))`, `${prefix}昇格: ${result.date} → memory.md (${result.items.length}件)`),
|
|
921
|
+
...result.items.map((item) => `- ${item}`),
|
|
922
|
+
];
|
|
923
|
+
}
|
|
924
|
+
if (result.status === "reviewed") {
|
|
925
|
+
return [l(`${prefix}reviewed: ${result.date} (no long-term memory items)`, `${prefix}確認済み: ${result.date} (長期記憶項目なし)`)];
|
|
926
|
+
}
|
|
927
|
+
if (result.status === "skipped") {
|
|
928
|
+
return [l(`skipped: ${result.date} already reviewed`, `スキップ: ${result.date} は確認済みです`)];
|
|
929
|
+
}
|
|
930
|
+
return [`${result.status}: ${result.message || result.date}`];
|
|
931
|
+
}
|
|
932
|
+
async function showProfileLines(config, target) {
|
|
933
|
+
const files = await readProfileMemoryFiles(config);
|
|
934
|
+
const lines = [];
|
|
935
|
+
const targets = target ? [target] : ["soul", "user", "memory"];
|
|
936
|
+
for (const item of targets) {
|
|
937
|
+
lines.push(`${item}: ${files.paths[item]}`);
|
|
938
|
+
lines.push(files[item].trim() || l("(empty)", "(空)"));
|
|
939
|
+
}
|
|
940
|
+
return lines;
|
|
941
|
+
}
|
|
942
|
+
function buildPayloadFromOptions(options) {
|
|
943
|
+
let payload = {};
|
|
944
|
+
const payloadOption = stringOption(options.payload);
|
|
945
|
+
if (payloadOption) {
|
|
946
|
+
payload = parseJsonOption(payloadOption, "--payload");
|
|
947
|
+
}
|
|
948
|
+
const reservedKeys = new Set(["_", "payload"]);
|
|
949
|
+
for (const [key, value] of Object.entries(options)) {
|
|
950
|
+
if (reservedKeys.has(key)) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (value === true) {
|
|
954
|
+
payload[key] = true;
|
|
955
|
+
}
|
|
956
|
+
else if (typeof value === "string" || typeof value === "number") {
|
|
957
|
+
payload[key] = value;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return Object.keys(payload).length > 0 ? payload : undefined;
|
|
961
|
+
}
|
|
962
|
+
async function cmdModels(args = []) {
|
|
963
|
+
const [sub, ...rest] = args;
|
|
964
|
+
if (sub === "keys") {
|
|
965
|
+
return cmdModelsKeys(rest);
|
|
966
|
+
}
|
|
967
|
+
const config = await loadConfig();
|
|
968
|
+
const lines = await modelsLines(config);
|
|
969
|
+
for (const line of lines) {
|
|
970
|
+
console.log(line);
|
|
971
|
+
}
|
|
972
|
+
return 0;
|
|
973
|
+
}
|
|
974
|
+
async function cmdModelsKeys(args) {
|
|
975
|
+
const options = parseOptions(args);
|
|
976
|
+
const filter = stringOption(options.provider);
|
|
977
|
+
const targets = [
|
|
978
|
+
{ id: "openai", provider: "openai", fallbacks: [] },
|
|
979
|
+
{ id: "gemini", provider: "gemini", fallbacks: ["google"] },
|
|
980
|
+
{ id: "anthropic", provider: "anthropic", fallbacks: [] },
|
|
981
|
+
];
|
|
982
|
+
const list = filter ? targets.filter((target) => target.id === filter) : targets;
|
|
983
|
+
if (list.length === 0) {
|
|
984
|
+
console.error(l(`Unknown provider: ${filter}`, `不明なプロバイダです: ${filter}`));
|
|
985
|
+
return 1;
|
|
986
|
+
}
|
|
987
|
+
let missing = 0;
|
|
988
|
+
for (const target of list) {
|
|
989
|
+
const resolution = getApiKeyResolution(target.provider, target.fallbacks);
|
|
990
|
+
if (resolution.keys.length === 0) {
|
|
991
|
+
console.log(`${target.id.padEnd(10)} ${l("not set", "未設定")}`);
|
|
992
|
+
missing += 1;
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const sources = new Set(resolution.sources.map((source) => source.envVar));
|
|
996
|
+
console.log(`${target.id.padEnd(10)} ${l(`${resolution.keys.length} key(s)`, `${resolution.keys.length}個のキー`)} via ${[...sources].join(", ")}`);
|
|
997
|
+
for (const [index, key] of resolution.keys.entries()) {
|
|
998
|
+
console.log(` ${index + 1}) ${maskKey(key)}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (options.check) {
|
|
1002
|
+
return missing > 0 ? 1 : 0;
|
|
1003
|
+
}
|
|
1004
|
+
return 0;
|
|
1005
|
+
}
|
|
1006
|
+
async function cmdModel(args) {
|
|
1007
|
+
const [subcommand, ...rest] = args;
|
|
1008
|
+
if (subcommand !== "set" || rest.length === 0) {
|
|
1009
|
+
console.error(l("Usage: agent-sin model set <model-id>", "使い方: agent-sin model set <model-id>"));
|
|
1010
|
+
console.error(" agent-sin model set chat <model-id>");
|
|
1011
|
+
console.error(" agent-sin model set builder <model-id>");
|
|
1012
|
+
return 1;
|
|
1013
|
+
}
|
|
1014
|
+
let role = "chat";
|
|
1015
|
+
let modelId;
|
|
1016
|
+
if (rest[0] === "chat" || rest[0] === "builder") {
|
|
1017
|
+
role = rest[0];
|
|
1018
|
+
modelId = rest[1] || "";
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
modelId = rest[0];
|
|
1022
|
+
}
|
|
1023
|
+
if (!modelId) {
|
|
1024
|
+
console.error(l(`Usage: agent-sin model set ${role} <model-id>`, `使い方: agent-sin model set ${role} <model-id>`));
|
|
1025
|
+
return 1;
|
|
1026
|
+
}
|
|
1027
|
+
const lines = await modelSetLines(role, modelId);
|
|
1028
|
+
for (const line of lines) {
|
|
1029
|
+
console.log(line);
|
|
1030
|
+
}
|
|
1031
|
+
return 0;
|
|
1032
|
+
}
|
|
1033
|
+
async function modelSetLines(role, modelId) {
|
|
1034
|
+
const config = await setRoleModel(role, modelId);
|
|
1035
|
+
const current = role === "chat" ? config.chat_model_id : config.builder_model_id;
|
|
1036
|
+
return [l(`Default ${role} model: ${current}`, `既定の ${role} モデル: ${current}`)];
|
|
1037
|
+
}
|
|
1038
|
+
async function cmdLogs(args) {
|
|
1039
|
+
const options = parseOptions(args);
|
|
1040
|
+
const config = await loadConfig();
|
|
1041
|
+
const lines = await logsLines(config, options);
|
|
1042
|
+
for (const line of lines) {
|
|
1043
|
+
console.log(line);
|
|
1044
|
+
}
|
|
1045
|
+
return 0;
|
|
1046
|
+
}
|
|
1047
|
+
async function logsLines(config, options) {
|
|
1048
|
+
if (typeof options.run === "string") {
|
|
1049
|
+
const record = await readRunLog(config, options.run);
|
|
1050
|
+
return [JSON.stringify(record, null, 2)];
|
|
1051
|
+
}
|
|
1052
|
+
if (options.events) {
|
|
1053
|
+
const tail = parsePositiveIntOption(options.tail, 50);
|
|
1054
|
+
const source = typeof options.source === "string" ? options.source : undefined;
|
|
1055
|
+
const level = typeof options.level === "string" ? options.level : undefined;
|
|
1056
|
+
const entries = await readEventLog(config, { tail, source, level });
|
|
1057
|
+
if (entries.length === 0) {
|
|
1058
|
+
return [l("No events.", "イベントはありません。")];
|
|
1059
|
+
}
|
|
1060
|
+
return entries.map((entry) => {
|
|
1061
|
+
const detail = entry.details ? ` ${JSON.stringify(entry.details)}` : "";
|
|
1062
|
+
const msg = entry.message ? ` ${entry.message}` : "";
|
|
1063
|
+
return `${entry.ts}\t${entry.level}\t${entry.source}\t${entry.event}${msg}${detail}`;
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const skill = typeof options.skill === "string" ? options.skill : undefined;
|
|
1067
|
+
const records = await listRunLogs(config, skill);
|
|
1068
|
+
if (records.length === 0) {
|
|
1069
|
+
return [l("No run logs.", "実行ログはありません。")];
|
|
1070
|
+
}
|
|
1071
|
+
return records
|
|
1072
|
+
.slice(0, 20)
|
|
1073
|
+
.map((record) => `${record.finished_at}\t${record.status}\t${record.skill_id}\t${record.run_id}`);
|
|
1074
|
+
}
|
|
1075
|
+
function parsePositiveIntOption(value, fallback) {
|
|
1076
|
+
if (typeof value === "string") {
|
|
1077
|
+
const n = Number.parseInt(value, 10);
|
|
1078
|
+
if (Number.isFinite(n) && n > 0) {
|
|
1079
|
+
return n;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1083
|
+
return value;
|
|
1084
|
+
}
|
|
1085
|
+
return fallback;
|
|
1086
|
+
}
|
|
1087
|
+
async function cmdConfig() {
|
|
1088
|
+
const config = await loadConfig();
|
|
1089
|
+
const lines = configLines(config);
|
|
1090
|
+
for (const line of lines) {
|
|
1091
|
+
console.log(line);
|
|
1092
|
+
}
|
|
1093
|
+
return 0;
|
|
1094
|
+
}
|
|
1095
|
+
function configLines(config) {
|
|
1096
|
+
return [
|
|
1097
|
+
`workspace: ${config.workspace}`,
|
|
1098
|
+
`config: ${configPath(config.workspace)}`,
|
|
1099
|
+
`notes_dir: ${config.notes_dir}`,
|
|
1100
|
+
`skills_dir: ${config.skills_dir}`,
|
|
1101
|
+
`memory_dir: ${config.memory_dir}`,
|
|
1102
|
+
`index_dir: ${config.index_dir}`,
|
|
1103
|
+
`logs_dir: ${config.logs_dir}`,
|
|
1104
|
+
`chat_model: ${config.chat_model_id}`,
|
|
1105
|
+
`builder: ${config.builder_model_id}`,
|
|
1106
|
+
];
|
|
1107
|
+
}
|
|
1108
|
+
async function handleChatMessage(config, text, history, intentRuntime) {
|
|
1109
|
+
return withLocale(inferLocaleFromText(text), () => handleChatMessageLocalized(config, text, history, intentRuntime));
|
|
1110
|
+
}
|
|
1111
|
+
async function handleChatMessageLocalized(config, text, history, intentRuntime) {
|
|
1112
|
+
const trimmed = text.trim();
|
|
1113
|
+
if (!trimmed) {
|
|
1114
|
+
return [l("Please enter a message.", "入力してください。")];
|
|
1115
|
+
}
|
|
1116
|
+
if (["help", "/help"].includes(trimmed)) {
|
|
1117
|
+
return chatHelpLines();
|
|
1118
|
+
}
|
|
1119
|
+
if (trimmed === "/reset") {
|
|
1120
|
+
history.length = 0;
|
|
1121
|
+
if (intentRuntime) {
|
|
1122
|
+
intentRuntime.pending = null;
|
|
1123
|
+
}
|
|
1124
|
+
return [t("chat.history_reset")];
|
|
1125
|
+
}
|
|
1126
|
+
if (intentRuntime?.mode === "build") {
|
|
1127
|
+
const buildLines = await withCliBuildHooks(config, trimmed, (hooks) => handleBuildModeMessage(config, trimmed, intentRuntime, { ...hooks, suggestExitOnOffTopic: true }));
|
|
1128
|
+
if (buildLines !== null) {
|
|
1129
|
+
return appendCliBuildFooter(intentRuntime, buildLines, trimmed);
|
|
1130
|
+
}
|
|
1131
|
+
// Auto-exited to chat: fall through and process this message as a chat reply.
|
|
1132
|
+
}
|
|
1133
|
+
if (intentRuntime?.pending && !trimmed.startsWith("/")) {
|
|
1134
|
+
const approval = await classifyPendingHandoff(config, trimmed, history, intentRuntime);
|
|
1135
|
+
if (approval.decision === "approve") {
|
|
1136
|
+
appendHistory(history, { role: "user", content: trimmed });
|
|
1137
|
+
const lines = await withCliBuildHooks(config, trimmed, (hooks) => enterBuildMode(config, history, intentRuntime, hooks, approval.carry_over_text));
|
|
1138
|
+
return appendCliBuildFooter(intentRuntime, lines, trimmed);
|
|
1139
|
+
}
|
|
1140
|
+
if (approval.decision === "reject") {
|
|
1141
|
+
intentRuntime.pending = null;
|
|
1142
|
+
return [l("OK. Staying in chat mode.", "了解。チャットモードのままにします。")];
|
|
1143
|
+
}
|
|
1144
|
+
// "discuss" → keep pending, fall through to chatRespond.
|
|
1145
|
+
}
|
|
1146
|
+
if (trimmed === "skills" || trimmed === "/skills") {
|
|
1147
|
+
return skillsLines(config);
|
|
1148
|
+
}
|
|
1149
|
+
if (trimmed === "/models") {
|
|
1150
|
+
return modelsLines(config);
|
|
1151
|
+
}
|
|
1152
|
+
if (trimmed === "/config") {
|
|
1153
|
+
return configLines(config);
|
|
1154
|
+
}
|
|
1155
|
+
if (trimmed === "/profile" || trimmed.startsWith("/profile ")) {
|
|
1156
|
+
const rest = trimmed === "/profile" ? ["show"] : trimmed.split(/\s+/).slice(1);
|
|
1157
|
+
return profileLines(config, rest);
|
|
1158
|
+
}
|
|
1159
|
+
if (trimmed === "/logs" || trimmed.startsWith("/logs ")) {
|
|
1160
|
+
const rest = trimmed === "/logs" ? [] : trimmed.split(/\s+/).slice(1);
|
|
1161
|
+
return logsLines(config, parseOptions(rest));
|
|
1162
|
+
}
|
|
1163
|
+
if (trimmed.startsWith("/model ")) {
|
|
1164
|
+
const parts = trimmed.split(/\s+/).slice(1);
|
|
1165
|
+
let role = "chat";
|
|
1166
|
+
let modelId;
|
|
1167
|
+
if (parts[0] === "set") {
|
|
1168
|
+
parts.shift();
|
|
1169
|
+
}
|
|
1170
|
+
if (parts[0] === "chat" || parts[0] === "builder") {
|
|
1171
|
+
role = parts[0];
|
|
1172
|
+
modelId = parts[1];
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
modelId = parts[0];
|
|
1176
|
+
}
|
|
1177
|
+
if (!modelId) {
|
|
1178
|
+
return [l("Usage: /model [chat|builder] <model-id>", "使い方: /model [chat|builder] <model-id>")];
|
|
1179
|
+
}
|
|
1180
|
+
return modelSetLines(role, modelId);
|
|
1181
|
+
}
|
|
1182
|
+
if (trimmed === "/build" || trimmed.startsWith("/build ")) {
|
|
1183
|
+
const rest = trimmed === "/build" ? [] : trimmed.split(/\s+/).slice(1);
|
|
1184
|
+
if (rest[0] === "list") {
|
|
1185
|
+
return buildListLines(config);
|
|
1186
|
+
}
|
|
1187
|
+
if (rest[0] === "register") {
|
|
1188
|
+
return [
|
|
1189
|
+
l("/build register is deprecated. Builder writes directly to skills/<id>/, so there is no registration step.", "/build register は廃止されました。Builder が skills/<id>/ に直接書き込むので、登録ステップはありません。"),
|
|
1190
|
+
];
|
|
1191
|
+
}
|
|
1192
|
+
if (rest[0] === "test") {
|
|
1193
|
+
return buildChatTestCommand(config, rest);
|
|
1194
|
+
}
|
|
1195
|
+
if (rest[0] === "chat") {
|
|
1196
|
+
return buildChatDraftCommand(config, rest);
|
|
1197
|
+
}
|
|
1198
|
+
if (rest[0] === "status") {
|
|
1199
|
+
return buildChatStatusCommand(config, rest);
|
|
1200
|
+
}
|
|
1201
|
+
const [skillId, ...messageParts] = rest;
|
|
1202
|
+
return withCliBuildHooks(config, messageParts.join(" "), (hooks) => buildLines(config, skillId, { prompt: messageParts.join(" "), onProgress: hooks.onProgress }));
|
|
1203
|
+
}
|
|
1204
|
+
if (trimmed.startsWith("/run ")) {
|
|
1205
|
+
const [, skillId, ...rest] = trimmed.split(/\s+/);
|
|
1206
|
+
const text = rest.join(" ");
|
|
1207
|
+
const payload = chatRunPayload(skillId, text);
|
|
1208
|
+
const response = await runSkill(config, skillId, payload);
|
|
1209
|
+
return [response.result.title, response.result.summary].filter(Boolean);
|
|
1210
|
+
}
|
|
1211
|
+
const preferredSkillId = intentRuntime?.preferred_skill_id || undefined;
|
|
1212
|
+
if (intentRuntime) {
|
|
1213
|
+
intentRuntime.preferred_skill_id = null;
|
|
1214
|
+
}
|
|
1215
|
+
const chatLines = await chatWithModel(config, trimmed, history, preferredSkillId, intentRuntime);
|
|
1216
|
+
return chatLines;
|
|
1217
|
+
}
|
|
1218
|
+
function appendCliBuildFooter(intentRuntime, lines, userText = "") {
|
|
1219
|
+
const footer = renderBuildFooter(intentRuntime, {
|
|
1220
|
+
exitPrefix: "/",
|
|
1221
|
+
languageHint: [userText, ...lines],
|
|
1222
|
+
});
|
|
1223
|
+
if (!footer)
|
|
1224
|
+
return lines;
|
|
1225
|
+
if (lines.some((line) => line.trim() === footer))
|
|
1226
|
+
return lines;
|
|
1227
|
+
return [...lines, "", footer];
|
|
1228
|
+
}
|
|
1229
|
+
function setPendingBuildSuggestion(intentRuntime, suggestion, userText) {
|
|
1230
|
+
if (!intentRuntime || intentRuntime.mode !== "chat")
|
|
1231
|
+
return;
|
|
1232
|
+
intentRuntime.pending = {
|
|
1233
|
+
type: suggestion.type,
|
|
1234
|
+
skill_id: suggestion.skill_id,
|
|
1235
|
+
original_text: userText,
|
|
1236
|
+
reason: suggestion.reason || "chat build suggestion",
|
|
1237
|
+
};
|
|
1238
|
+
intentRuntime.pending_exit = null;
|
|
1239
|
+
}
|
|
1240
|
+
const BRAND_GREEN_ANSI = "38;2;24;160;104";
|
|
1241
|
+
function renderPromptLabel(modelId, intentRuntime) {
|
|
1242
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
1243
|
+
const paint = (codes, text) => useColor ? `\x1b[${codes}m${text}\x1b[0m` : text;
|
|
1244
|
+
if (intentRuntime.mode === "build" && intentRuntime.build) {
|
|
1245
|
+
const isEdit = intentRuntime.build.type === "edit";
|
|
1246
|
+
const modeTag = isEdit ? "edit" : "build";
|
|
1247
|
+
const modeColor = isEdit ? "1;33" : "1;36";
|
|
1248
|
+
return `${paint(modeColor, `[${modeTag}:${intentRuntime.build.skill_id}]`)}`;
|
|
1249
|
+
}
|
|
1250
|
+
return `${paint(BRAND_GREEN_ANSI, "[chat]")} ${modelId}`;
|
|
1251
|
+
}
|
|
1252
|
+
function uiContext() {
|
|
1253
|
+
const tty = Boolean(process.stdout.isTTY);
|
|
1254
|
+
const ascii = process.env.AGENT_SIN_ASCII === "1" ||
|
|
1255
|
+
process.env.TERM === "dumb" ||
|
|
1256
|
+
!!process.env.AGENT_SIN_PLAIN_UI;
|
|
1257
|
+
const cols = process.stdout.columns;
|
|
1258
|
+
return {
|
|
1259
|
+
color: tty && !process.env.NO_COLOR,
|
|
1260
|
+
ascii,
|
|
1261
|
+
width: Math.max(40, Math.min(typeof cols === "number" && cols > 0 ? cols : 80, 80)),
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function uiActive() {
|
|
1265
|
+
return Boolean(process.stdout.isTTY) && process.env.AGENT_SIN_PLAIN_UI !== "1";
|
|
1266
|
+
}
|
|
1267
|
+
function paintCode(codes, text, ctx) {
|
|
1268
|
+
const c = ctx ?? uiContext();
|
|
1269
|
+
return c.color ? `\x1b[${codes}m${text}\x1b[0m` : text;
|
|
1270
|
+
}
|
|
1271
|
+
function boxChars(ctx) {
|
|
1272
|
+
if (ctx.ascii) {
|
|
1273
|
+
return { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
|
|
1274
|
+
}
|
|
1275
|
+
return { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
|
|
1276
|
+
}
|
|
1277
|
+
function glyph(name, ctx) {
|
|
1278
|
+
if (ctx.ascii) {
|
|
1279
|
+
const map = {
|
|
1280
|
+
spark: "*",
|
|
1281
|
+
bullet: "*",
|
|
1282
|
+
arrow: "->",
|
|
1283
|
+
check: "v",
|
|
1284
|
+
cross: "x",
|
|
1285
|
+
prompt: ">",
|
|
1286
|
+
dot: "·",
|
|
1287
|
+
};
|
|
1288
|
+
return map[name];
|
|
1289
|
+
}
|
|
1290
|
+
const map = {
|
|
1291
|
+
spark: "✻",
|
|
1292
|
+
bullet: "●",
|
|
1293
|
+
arrow: "→",
|
|
1294
|
+
check: "✓",
|
|
1295
|
+
cross: "✕",
|
|
1296
|
+
prompt: "›",
|
|
1297
|
+
dot: "·",
|
|
1298
|
+
};
|
|
1299
|
+
return map[name];
|
|
1300
|
+
}
|
|
1301
|
+
const SMALL_BLOCK_FONT = {
|
|
1302
|
+
A: ["▄▀▄", "█▀█", "▀ ▀"],
|
|
1303
|
+
G: ["█▀▀", "█ ▄", "▀▀▀"],
|
|
1304
|
+
E: ["█▀▀", "█▀▀", "▀▀▀"],
|
|
1305
|
+
N: ["█▄ █", "█ ██", "▀ ▀"],
|
|
1306
|
+
T: ["▀█▀", " █ ", " ▀ "],
|
|
1307
|
+
"-": [" ", "───", " "],
|
|
1308
|
+
S: ["▄▀▀", "▀▀▄", "▄▄▀"],
|
|
1309
|
+
I: ["█", "█", "▀"],
|
|
1310
|
+
};
|
|
1311
|
+
function smallBlockTitle(text) {
|
|
1312
|
+
const rows = ["", "", ""];
|
|
1313
|
+
const chars = text.toUpperCase().split("");
|
|
1314
|
+
chars.forEach((ch, idx) => {
|
|
1315
|
+
const figure = SMALL_BLOCK_FONT[ch];
|
|
1316
|
+
if (!figure) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
for (let i = 0; i < 3; i++) {
|
|
1320
|
+
rows[i] += figure[i];
|
|
1321
|
+
if (idx < chars.length - 1) {
|
|
1322
|
+
rows[i] += " ";
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
return rows;
|
|
1327
|
+
}
|
|
1328
|
+
function renderStartupBanner(state) {
|
|
1329
|
+
if (!uiActive()) {
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
const ctx = uiContext();
|
|
1333
|
+
const dim = (text) => paintCode("90", text, ctx);
|
|
1334
|
+
const bold = (text) => paintCode("1", text, ctx);
|
|
1335
|
+
const accent = (text) => paintCode(BRAND_GREEN_ANSI, text, ctx);
|
|
1336
|
+
const dot = dim(glyph("dot", ctx));
|
|
1337
|
+
const modelDisplay = resolveDisplayModel(state);
|
|
1338
|
+
if (ctx.ascii) {
|
|
1339
|
+
return [
|
|
1340
|
+
"",
|
|
1341
|
+
` ${bold("agent-sin")} ${dim("v" + AGENT_SIN_VERSION)} ${dot} ${dim("model:")} ${modelDisplay}`,
|
|
1342
|
+
` ${dim("/help · /reset · /exit")}`,
|
|
1343
|
+
"",
|
|
1344
|
+
];
|
|
1345
|
+
}
|
|
1346
|
+
const indent = " ";
|
|
1347
|
+
const title = smallBlockTitle("AGENT-SIN").map((line) => `${indent}${accent(line)}`);
|
|
1348
|
+
return [
|
|
1349
|
+
"",
|
|
1350
|
+
...title,
|
|
1351
|
+
"",
|
|
1352
|
+
`${indent}${bold("agent-sin")} ${dim("v" + AGENT_SIN_VERSION)} ${dot} ${dim("model:")} ${modelDisplay}`,
|
|
1353
|
+
`${indent}${dim("/help · /reset · /exit")}`,
|
|
1354
|
+
"",
|
|
1355
|
+
];
|
|
1356
|
+
}
|
|
1357
|
+
function resolveDisplayModel(state, intentRuntime) {
|
|
1358
|
+
const inBuild = intentRuntime?.mode === "build" && intentRuntime.build;
|
|
1359
|
+
const entryId = inBuild ? state.config.builder_model_id : state.config.chat_model_id;
|
|
1360
|
+
const entry = state.models?.models?.[entryId];
|
|
1361
|
+
return entry?.model || entryId;
|
|
1362
|
+
}
|
|
1363
|
+
function renderInputFrameTop() {
|
|
1364
|
+
if (!uiActive()) {
|
|
1365
|
+
return "";
|
|
1366
|
+
}
|
|
1367
|
+
const ctx = uiContext();
|
|
1368
|
+
const b = boxChars(ctx);
|
|
1369
|
+
return paintCode("90", `${b.tl}${b.h.repeat(ctx.width - 2)}${b.tr}`, ctx);
|
|
1370
|
+
}
|
|
1371
|
+
function renderInputFrameBottom() {
|
|
1372
|
+
if (!uiActive()) {
|
|
1373
|
+
return "";
|
|
1374
|
+
}
|
|
1375
|
+
const ctx = uiContext();
|
|
1376
|
+
const b = boxChars(ctx);
|
|
1377
|
+
return paintCode("90", `${b.bl}${b.h.repeat(ctx.width - 2)}${b.br}`, ctx);
|
|
1378
|
+
}
|
|
1379
|
+
function renderInputPromptPrefix(intentRuntime, modelId) {
|
|
1380
|
+
if (!uiActive()) {
|
|
1381
|
+
return `${renderPromptLabel(modelId, intentRuntime)} > `;
|
|
1382
|
+
}
|
|
1383
|
+
const ctx = uiContext();
|
|
1384
|
+
const b = boxChars(ctx);
|
|
1385
|
+
const sym = glyph("prompt", ctx);
|
|
1386
|
+
const promptColor = intentRuntime.mode === "build" && intentRuntime.build
|
|
1387
|
+
? intentRuntime.build.type === "edit"
|
|
1388
|
+
? "1;33"
|
|
1389
|
+
: "1;36"
|
|
1390
|
+
: BRAND_GREEN_ANSI;
|
|
1391
|
+
return `${paintCode("90", b.v, ctx)} ${paintCode(promptColor, sym, ctx)} `;
|
|
1392
|
+
}
|
|
1393
|
+
function renderStatusLine(state, intentRuntime) {
|
|
1394
|
+
if (!uiActive()) {
|
|
1395
|
+
return "";
|
|
1396
|
+
}
|
|
1397
|
+
const ctx = uiContext();
|
|
1398
|
+
const dim = (text) => paintCode("90", text, ctx);
|
|
1399
|
+
const dot = dim(glyph("dot", ctx));
|
|
1400
|
+
const accentColor = intentRuntime.mode === "build" && intentRuntime.build
|
|
1401
|
+
? intentRuntime.build.type === "edit"
|
|
1402
|
+
? "33"
|
|
1403
|
+
: "36"
|
|
1404
|
+
: BRAND_GREEN_ANSI;
|
|
1405
|
+
const accent = (text) => paintCode(accentColor, text, ctx);
|
|
1406
|
+
const modeLabel = intentRuntime.mode === "build" && intentRuntime.build
|
|
1407
|
+
? `${intentRuntime.build.type}:${intentRuntime.build.skill_id}`
|
|
1408
|
+
: "chat";
|
|
1409
|
+
return ` ${dim("mode:")} ${accent(modeLabel)} ${dot} ${dim("model:")} ${resolveDisplayModel(state, intentRuntime)}`;
|
|
1410
|
+
}
|
|
1411
|
+
function formatChatLine(line) {
|
|
1412
|
+
if (!uiActive() || !line) {
|
|
1413
|
+
return line;
|
|
1414
|
+
}
|
|
1415
|
+
const ctx = uiContext();
|
|
1416
|
+
// Tool announce lines start with `→ `, leave them but add color.
|
|
1417
|
+
if (line.startsWith("→ ")) {
|
|
1418
|
+
return paintCode(BRAND_GREEN_ANSI, line, ctx);
|
|
1419
|
+
}
|
|
1420
|
+
if (line.startsWith("saved: ")) {
|
|
1421
|
+
return ` ${paintCode(BRAND_GREEN_ANSI, glyph("check", ctx), ctx)} ${paintCode("90", line, ctx)}`;
|
|
1422
|
+
}
|
|
1423
|
+
if (line.startsWith("[skill error:") || line.startsWith("[skill not allowed:")) {
|
|
1424
|
+
return ` ${paintCode("31", glyph("cross", ctx), ctx)} ${paintCode("31", line, ctx)}`;
|
|
1425
|
+
}
|
|
1426
|
+
return line;
|
|
1427
|
+
}
|
|
1428
|
+
function formatAssistantNarrative(text) {
|
|
1429
|
+
if (!uiActive() || !text) {
|
|
1430
|
+
return text;
|
|
1431
|
+
}
|
|
1432
|
+
const ctx = uiContext();
|
|
1433
|
+
const bullet = paintCode(BRAND_GREEN_ANSI, glyph("bullet", ctx), ctx);
|
|
1434
|
+
const lines = text.split("\n");
|
|
1435
|
+
return lines
|
|
1436
|
+
.map((line, idx) => (idx === 0 ? `${bullet} ${line}` : ` ${line}`))
|
|
1437
|
+
.join("\n");
|
|
1438
|
+
}
|
|
1439
|
+
const AGENT_SIN_VERSION = "0.1.0";
|
|
1440
|
+
async function withCliBuildHooks(config, trimmed, run) {
|
|
1441
|
+
const hooks = cliBuildHooks(config, trimmed);
|
|
1442
|
+
try {
|
|
1443
|
+
return await run(hooks);
|
|
1444
|
+
}
|
|
1445
|
+
finally {
|
|
1446
|
+
hooks.finish();
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
function cliBuildHooks(config, trimmed) {
|
|
1450
|
+
const spinner = new Spinner();
|
|
1451
|
+
const labelBase = `build ${config.builder_model_id}`;
|
|
1452
|
+
const promptHint = trimmed.trim().replace(/\s+/g, " ").slice(0, 36);
|
|
1453
|
+
const baseLabel = promptHint ? `${labelBase}: ${promptHint}` : labelBase;
|
|
1454
|
+
const updateProgress = makeSpinnerProgress(spinner, baseLabel);
|
|
1455
|
+
let started = false;
|
|
1456
|
+
return {
|
|
1457
|
+
onProgress(event) {
|
|
1458
|
+
if (!started) {
|
|
1459
|
+
spinner.start(baseLabel);
|
|
1460
|
+
started = true;
|
|
1461
|
+
}
|
|
1462
|
+
updateProgress(event);
|
|
1463
|
+
},
|
|
1464
|
+
finish() {
|
|
1465
|
+
if (started) {
|
|
1466
|
+
spinner.stop();
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function chatHelpLines() {
|
|
1472
|
+
return lLines([
|
|
1473
|
+
"Free-form messages go to AI, and registered skills are called when useful.",
|
|
1474
|
+
"Slash commands:",
|
|
1475
|
+
" /skills [--all] List skills (--all includes builtins; select to inspect/delete)",
|
|
1476
|
+
" /models List models",
|
|
1477
|
+
" /model Pick chat/builder models interactively",
|
|
1478
|
+
" /build list List skill drafts",
|
|
1479
|
+
" /logs [--run id] [--skill id] Run logs",
|
|
1480
|
+
" /logs --events [--tail N] Conversation/CLI/skill event logs",
|
|
1481
|
+
" /config Show current settings",
|
|
1482
|
+
" /profile Show soul.md / user.md / memory.md",
|
|
1483
|
+
" /profile append <soul|user|memory> <text>",
|
|
1484
|
+
" /reset Reset chat history",
|
|
1485
|
+
" /help Show this help",
|
|
1486
|
+
" /exit Exit",
|
|
1487
|
+
"",
|
|
1488
|
+
"In build/edit mode, dedicated commands are available (/test /status /back; /register is shown for compatibility).",
|
|
1489
|
+
], [
|
|
1490
|
+
"自由な会話入力はAIに送られ、必要に応じて登録済みスキルが呼び出されます。",
|
|
1491
|
+
"スラッシュコマンド:",
|
|
1492
|
+
" /skills [--all] スキル一覧 (--all でビルトインも表示)",
|
|
1493
|
+
" /models モデル一覧",
|
|
1494
|
+
" /model 対話的にchat/builderモデルを選択",
|
|
1495
|
+
" /build list 作成中のスキル一覧",
|
|
1496
|
+
" /logs [--run id] [--skill id] 実行ログ",
|
|
1497
|
+
" /logs --events [--tail N] 会話/CLI/スキルのイベントログ",
|
|
1498
|
+
" /config 現在の設定を表示",
|
|
1499
|
+
" /profile soul.md / user.md / memory.md を表示",
|
|
1500
|
+
" /profile append <soul|user|memory> <text>",
|
|
1501
|
+
" /reset 会話履歴をリセット",
|
|
1502
|
+
" /help このヘルプ",
|
|
1503
|
+
" /exit 終了",
|
|
1504
|
+
"",
|
|
1505
|
+
"ビルド/編集モード中は専用コマンド (/test /status /back、/register は互換表示) に切り替わります。",
|
|
1506
|
+
]);
|
|
1507
|
+
}
|
|
1508
|
+
async function buildChatTestCommand(config, rest) {
|
|
1509
|
+
const [skillId, ...optionParts] = rest.slice(1);
|
|
1510
|
+
if (!skillId) {
|
|
1511
|
+
return [l("Usage: /build test <skill-id> [--payload '{...}']", "使い方: /build test <skill-id> [--payload '{...}']")];
|
|
1512
|
+
}
|
|
1513
|
+
const options = parseOptions(optionParts);
|
|
1514
|
+
return buildTestLines(config, skillId, buildPayloadFromOptions(options));
|
|
1515
|
+
}
|
|
1516
|
+
async function buildChatDraftCommand(config, rest) {
|
|
1517
|
+
const [skillId, ...messageParts] = rest.slice(1);
|
|
1518
|
+
const message = messageParts.join(" ").trim();
|
|
1519
|
+
if (!skillId || !message) {
|
|
1520
|
+
return [l('Usage: /build chat <skill-id> "message"', '使い方: /build chat <skill-id> "メッセージ"')];
|
|
1521
|
+
}
|
|
1522
|
+
return withCliBuildHooks(config, message, (hooks) => buildChatLines(config, skillId, message, { onProgress: hooks.onProgress }));
|
|
1523
|
+
}
|
|
1524
|
+
async function buildChatStatusCommand(config, rest) {
|
|
1525
|
+
const skillId = rest[1];
|
|
1526
|
+
if (!skillId) {
|
|
1527
|
+
return [l("Usage: /build status <skill-id>", "使い方: /build status <skill-id>")];
|
|
1528
|
+
}
|
|
1529
|
+
return buildStatusLines(config, skillId);
|
|
1530
|
+
}
|
|
1531
|
+
function chatRunPayload(skillId, text) {
|
|
1532
|
+
if (!text) {
|
|
1533
|
+
return {};
|
|
1534
|
+
}
|
|
1535
|
+
if (skillId === "memo-search") {
|
|
1536
|
+
return { query: text };
|
|
1537
|
+
}
|
|
1538
|
+
if (["todo-done", "todo-delete"].includes(skillId)) {
|
|
1539
|
+
return { id: text };
|
|
1540
|
+
}
|
|
1541
|
+
return { text };
|
|
1542
|
+
}
|
|
1543
|
+
async function chatWithModel(config, userText, history, preferredSkillId, intentRuntime) {
|
|
1544
|
+
return chatRespond(config, userText, history, {
|
|
1545
|
+
formatNarrative: formatAssistantNarrative,
|
|
1546
|
+
spinner: new Spinner(),
|
|
1547
|
+
eventSource: "chat",
|
|
1548
|
+
preferredSkillId,
|
|
1549
|
+
onBuildSuggestion: (suggestion) => setPendingBuildSuggestion(intentRuntime, suggestion, userText),
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
function parseOptions(args) {
|
|
1553
|
+
const options = { _: [] };
|
|
1554
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1555
|
+
const value = args[i];
|
|
1556
|
+
if (value.startsWith("--")) {
|
|
1557
|
+
const key = value.slice(2).replaceAll("-", "_");
|
|
1558
|
+
const next = args[i + 1];
|
|
1559
|
+
const optionValue = next && !next.startsWith("--") ? next : true;
|
|
1560
|
+
if (optionValue !== true) {
|
|
1561
|
+
i += 1;
|
|
1562
|
+
}
|
|
1563
|
+
const previous = options[key];
|
|
1564
|
+
if (previous === undefined) {
|
|
1565
|
+
options[key] = optionValue;
|
|
1566
|
+
}
|
|
1567
|
+
else if (Array.isArray(previous)) {
|
|
1568
|
+
options[key] = [...previous, optionValue];
|
|
1569
|
+
}
|
|
1570
|
+
else {
|
|
1571
|
+
options[key] = [previous, optionValue];
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
options._.push(value);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return options;
|
|
1579
|
+
}
|
|
1580
|
+
function optionsToSetupOptions(options) {
|
|
1581
|
+
const chatModel = stringOption(options.model) || stringOption(options.chat_model);
|
|
1582
|
+
const enableModels = listOption(options.enable);
|
|
1583
|
+
return {
|
|
1584
|
+
workspace: stringOption(options.workspace),
|
|
1585
|
+
notesDir: stringOption(options.notes_dir),
|
|
1586
|
+
skillsDir: stringOption(options.skills_dir),
|
|
1587
|
+
memoryDir: stringOption(options.memory_dir),
|
|
1588
|
+
indexDir: stringOption(options.index_dir),
|
|
1589
|
+
logsDir: stringOption(options.logs_dir),
|
|
1590
|
+
chatModel,
|
|
1591
|
+
builder: stringOption(options.builder),
|
|
1592
|
+
enableModels,
|
|
1593
|
+
forceReconfigure: Boolean(options.reconfigure),
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
function shouldPromptSetup(options) {
|
|
1597
|
+
if (options.keep || options.yes || options.defaults || options.no_input) {
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1600
|
+
if (options.wizard || options.reconfigure) {
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
const configurableOptions = [
|
|
1604
|
+
"workspace",
|
|
1605
|
+
"notes_dir",
|
|
1606
|
+
"skills_dir",
|
|
1607
|
+
"memory_dir",
|
|
1608
|
+
"index_dir",
|
|
1609
|
+
"logs_dir",
|
|
1610
|
+
"model",
|
|
1611
|
+
"chat_model",
|
|
1612
|
+
"builder",
|
|
1613
|
+
"enable",
|
|
1614
|
+
];
|
|
1615
|
+
return process.stdin.isTTY && !configurableOptions.some((key) => options[key] !== undefined);
|
|
1616
|
+
}
|
|
1617
|
+
// setup 画面の見た目を整えるためのヘルパー群。
|
|
1618
|
+
// stdout が TTY でない / NO_COLOR が設定されているときはエスケープを吐かない。
|
|
1619
|
+
const SETUP_COLOR_ENABLED = process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
1620
|
+
function setupTone(open, close, text) {
|
|
1621
|
+
return SETUP_COLOR_ENABLED ? `\x1b[${open}m${text}\x1b[${close}m` : text;
|
|
1622
|
+
}
|
|
1623
|
+
const setupTones = {
|
|
1624
|
+
bold: (s) => setupTone("1", "22", s),
|
|
1625
|
+
dim: (s) => setupTone("2", "22", s),
|
|
1626
|
+
cyan: (s) => setupTone("36", "39", s),
|
|
1627
|
+
};
|
|
1628
|
+
// 日本語等の East-Asian 文字を概ね幅 2 として扱う簡易計算。
|
|
1629
|
+
// 罫線パディングが大きくズレない程度に揃えば十分。
|
|
1630
|
+
function setupDisplayWidth(text) {
|
|
1631
|
+
let width = 0;
|
|
1632
|
+
for (const ch of text) {
|
|
1633
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
1634
|
+
width += cp >= 0x1100 ? 2 : 1;
|
|
1635
|
+
}
|
|
1636
|
+
return width;
|
|
1637
|
+
}
|
|
1638
|
+
const SETUP_DIVIDER_WIDTH = 46;
|
|
1639
|
+
function setupSectionDivider(title) {
|
|
1640
|
+
const left = "─── ";
|
|
1641
|
+
const right = " ";
|
|
1642
|
+
const used = setupDisplayWidth(left) + setupDisplayWidth(title) + setupDisplayWidth(right);
|
|
1643
|
+
const fill = Math.max(3, SETUP_DIVIDER_WIDTH - used);
|
|
1644
|
+
return setupTones.dim(left) + setupTones.bold(title) + setupTones.dim(right + "─".repeat(fill));
|
|
1645
|
+
}
|
|
1646
|
+
async function promptSetupOptions(base) {
|
|
1647
|
+
const workspace = base.workspace ? base.workspace : defaultWorkspace();
|
|
1648
|
+
const modelsFile = modelsPath(workspace);
|
|
1649
|
+
const modelsExisting = await pathExists(modelsFile);
|
|
1650
|
+
// 初回セットアップでも案内に使えるよう、ここで .env のひな形を作っておく。
|
|
1651
|
+
// 既存環境では何もしない。
|
|
1652
|
+
await mkdir(workspace, { recursive: true });
|
|
1653
|
+
const dotenv = await ensureDotenvSkeleton(workspace);
|
|
1654
|
+
await loadDotenv(workspace);
|
|
1655
|
+
const dotenvKeys = await readDotenvKeys(workspace);
|
|
1656
|
+
const hasConfiguredApiKey = Array.from(dotenvKeys).some((key) => /_API_KEY(S)?(_\d+)?$/.test(key) || /_KEY$/.test(key));
|
|
1657
|
+
console.log(setupSectionDivider(l("Agent-Sin setup", "Agent-Sin setup")));
|
|
1658
|
+
console.log("");
|
|
1659
|
+
console.log(`${setupTones.bold("Workspace")} : ${setupTones.cyan(workspace)}`);
|
|
1660
|
+
console.log(`${setupTones.bold(".env ")} : ${setupTones.cyan(dotenv.path)}`);
|
|
1661
|
+
console.log("");
|
|
1662
|
+
// .env に API キーが 1 件も書かれていないときは、初回 / 再設定どちらでも案内する。
|
|
1663
|
+
// (前回の自動実行などで models.yaml だけが先に作られているケースも拾える)
|
|
1664
|
+
if (!modelsExisting || !hasConfiguredApiKey) {
|
|
1665
|
+
console.log(l("Add the API keys you want to use to .env, then re-run setup so they get detected.", "先に .env に API キーを書いてください。サンプルはコメントアウト済みなので、"));
|
|
1666
|
+
console.log(l("Sample entries are already commented in the file — uncomment the ones you need.", "使う行を有効化して setup を再実行すると検出されます。"));
|
|
1667
|
+
console.log(setupTones.dim(l("Note: Codex CLI / Claude Code CLI just need a CLI login — no .env entry required.", "※ Codex CLI / Claude Code CLI はログイン済みなら .env 不要です。")));
|
|
1668
|
+
console.log("");
|
|
1669
|
+
}
|
|
1670
|
+
const interactive = process.stdin.isTTY;
|
|
1671
|
+
const scriptedAnswers = interactive ? null : await readSetupAnswersFromInput();
|
|
1672
|
+
let scriptIndex = 0;
|
|
1673
|
+
const ask = async (label, fallback) => {
|
|
1674
|
+
const bracket = setupTones.dim(`[${fallback}]`);
|
|
1675
|
+
if (interactive) {
|
|
1676
|
+
const rl = createInterface({ input, output });
|
|
1677
|
+
try {
|
|
1678
|
+
const answer = (await rl.question(` ${label} ${bracket}: `)).trim();
|
|
1679
|
+
return answer || fallback;
|
|
1680
|
+
}
|
|
1681
|
+
finally {
|
|
1682
|
+
rl.close();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
console.log(` ${label} ${bracket}:`);
|
|
1686
|
+
const answer = (scriptedAnswers?.[scriptIndex]?.trim() || "");
|
|
1687
|
+
scriptIndex += 1;
|
|
1688
|
+
return answer || fallback;
|
|
1689
|
+
};
|
|
1690
|
+
if (modelsExisting) {
|
|
1691
|
+
const current = await currentSetupDefaults(workspace);
|
|
1692
|
+
console.log(setupTones.bold(l("Current model settings", "現在のモデル設定")));
|
|
1693
|
+
console.log(` ${setupTones.dim("chat ")} ${current.chatLabel}`);
|
|
1694
|
+
console.log(` ${setupTones.dim("builder")} ${current.builderLabel}`);
|
|
1695
|
+
console.log("");
|
|
1696
|
+
const reconfigureLabel = l("Reconfigure models?", "モデルを再設定しますか?");
|
|
1697
|
+
const reconfigure = base.forceReconfigure
|
|
1698
|
+
? true
|
|
1699
|
+
: await askReconfigurePrompt(reconfigureLabel, interactive, async () => {
|
|
1700
|
+
const answer = scriptedAnswers?.[scriptIndex]?.trim() || "";
|
|
1701
|
+
scriptIndex += 1;
|
|
1702
|
+
return answer;
|
|
1703
|
+
});
|
|
1704
|
+
if (!reconfigure) {
|
|
1705
|
+
console.log(l("Keeping existing model settings.", "既存のモデル設定をそのまま使います。"));
|
|
1706
|
+
console.log("");
|
|
1707
|
+
return base;
|
|
1708
|
+
}
|
|
1709
|
+
console.log("");
|
|
1710
|
+
return promptSetupModelChoices(base, workspace, ask, {
|
|
1711
|
+
chat: current.chat,
|
|
1712
|
+
builder: current.builder,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
return promptSetupModelChoices(base, workspace, ask);
|
|
1716
|
+
}
|
|
1717
|
+
async function promptSetupModelChoices(base, workspace, ask, defaults = {}) {
|
|
1718
|
+
const detected = await detectAvailableProviders(workspace);
|
|
1719
|
+
if (detected.length > 0) {
|
|
1720
|
+
console.log(setupTones.bold(l("Available providers", "使えるプロバイダ")));
|
|
1721
|
+
const idWidth = Math.max(...detected.map((p) => p.id.length));
|
|
1722
|
+
const labelWidth = Math.max(...detected.map((p) => p.label.length));
|
|
1723
|
+
for (const [index, p] of detected.entries()) {
|
|
1724
|
+
const num = setupTones.cyan(String(index + 1).padStart(2));
|
|
1725
|
+
const id = p.id.padEnd(idWidth);
|
|
1726
|
+
const label = p.label.padEnd(labelWidth);
|
|
1727
|
+
const hint = setupTones.dim(`(${p.hint})`);
|
|
1728
|
+
console.log(` ${num} ${id} ${label} ${hint}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
console.log(setupTones.dim(l("No usable providers detected (no CLI or API key found in .env).", "使えそうなプロバイダは検出できませんでした (CLI も .env の API キーも見つからない)。")));
|
|
1733
|
+
console.log(setupTones.dim(l("You can continue and edit .env / models.yaml later.", "そのまま進めて後から .env / models.yaml を編集することもできます。")));
|
|
1734
|
+
}
|
|
1735
|
+
const fallbackProvider = pickFallbackProvider(detected);
|
|
1736
|
+
const detectedIds = detected.map((p) => p.id);
|
|
1737
|
+
const numericHint = detected.length > 0
|
|
1738
|
+
? l(`Enter a number (1-${detected.length}) or provider id. Empty Enter uses [default].`, `番号 (1-${detected.length}) かプロバイダ ID を入力。空 Enter で [既定値] 採用。`)
|
|
1739
|
+
: l("Press Enter to use the [default] value.", "空 Enter で [既定値] が使われます。");
|
|
1740
|
+
console.log("");
|
|
1741
|
+
console.log(setupSectionDivider(l("[1/2] Chat model", "[1/2] チャットで使うモデル")));
|
|
1742
|
+
console.log(setupTones.dim(numericHint));
|
|
1743
|
+
console.log("");
|
|
1744
|
+
const chat = await askProviderChoice(ask, "chat", fallbackProvider, defaults.chat, detectedIds);
|
|
1745
|
+
console.log("");
|
|
1746
|
+
console.log(setupSectionDivider(l("[2/2] Builder model (skill generation)", "[2/2] ビルダー (スキル生成) で使うモデル")));
|
|
1747
|
+
console.log(setupTones.dim(l("Recommended: a CLI provider (codex / claude-code). API providers also work for simple skills.", "推奨: CLI 系 (codex / claude-code) が安定。API 系でも作れますが簡単なスキル向きです。")));
|
|
1748
|
+
console.log(setupTones.dim(numericHint));
|
|
1749
|
+
console.log("");
|
|
1750
|
+
const builder = await askProviderChoice(ask, "builder", chat.provider || fallbackProvider, defaults.builder, detectedIds);
|
|
1751
|
+
const pairIds = deriveSetupChoicePairIds(chat, builder);
|
|
1752
|
+
return {
|
|
1753
|
+
...base,
|
|
1754
|
+
initialModels: { chat, builder },
|
|
1755
|
+
chatModel: pairIds.chat,
|
|
1756
|
+
builder: pairIds.builder,
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
async function askYesNo(ask, label, defaultValue) {
|
|
1760
|
+
const fallback = defaultValue ? "y" : "n";
|
|
1761
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
1762
|
+
const answer = (await ask(label, fallback)).trim().toLowerCase();
|
|
1763
|
+
if (["y", "yes"].includes(answer))
|
|
1764
|
+
return true;
|
|
1765
|
+
if (["n", "no"].includes(answer))
|
|
1766
|
+
return false;
|
|
1767
|
+
console.log(l(" ! Enter y or n.", " ⚠ y か n を入力してください。"));
|
|
1768
|
+
}
|
|
1769
|
+
throw new Error(l("Yes/no input could not be resolved. Aborting setup.", "確認入力が解決できませんでした。setup を中断します。"));
|
|
1770
|
+
}
|
|
1771
|
+
async function askReconfigurePrompt(label, interactive, readScripted) {
|
|
1772
|
+
const prompt = `${label}${setupTones.dim("[y/n]")}: `;
|
|
1773
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
1774
|
+
let raw;
|
|
1775
|
+
if (interactive) {
|
|
1776
|
+
const rl = createInterface({ input, output });
|
|
1777
|
+
try {
|
|
1778
|
+
raw = await rl.question(prompt);
|
|
1779
|
+
}
|
|
1780
|
+
finally {
|
|
1781
|
+
rl.close();
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
console.log(prompt);
|
|
1786
|
+
raw = await readScripted();
|
|
1787
|
+
}
|
|
1788
|
+
const answer = raw.trim().toLowerCase();
|
|
1789
|
+
if (["y", "yes"].includes(answer))
|
|
1790
|
+
return true;
|
|
1791
|
+
if (["n", "no"].includes(answer))
|
|
1792
|
+
return false;
|
|
1793
|
+
console.log(l(" ! Enter y or n.", " ⚠ y か n を入力してください。"));
|
|
1794
|
+
}
|
|
1795
|
+
throw new Error(l("Yes/no input could not be resolved. Aborting setup.", "確認入力が解決できませんでした。setup を中断します。"));
|
|
1796
|
+
}
|
|
1797
|
+
async function currentSetupDefaults(workspace) {
|
|
1798
|
+
const models = await loadModels(workspace);
|
|
1799
|
+
const chatId = currentRoleId(models, "chat");
|
|
1800
|
+
const builderId = currentRoleId(models, "builder");
|
|
1801
|
+
const chatEntry = chatId ? models.models[chatId] : undefined;
|
|
1802
|
+
const builderEntry = builderId ? models.models[builderId] : undefined;
|
|
1803
|
+
return {
|
|
1804
|
+
chat: choiceDefaultsFromEntry(chatId, chatEntry),
|
|
1805
|
+
builder: choiceDefaultsFromEntry(builderId, builderEntry),
|
|
1806
|
+
chatLabel: formatSetupModelSummary(chatId, chatEntry),
|
|
1807
|
+
builderLabel: formatSetupModelSummary(builderId, builderEntry),
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function currentRoleId(models, role) {
|
|
1811
|
+
const roleId = role === "chat" ? models.roles?.chat : models.roles?.builder;
|
|
1812
|
+
if (roleId && models.models[roleId])
|
|
1813
|
+
return roleId;
|
|
1814
|
+
if (models.models[role])
|
|
1815
|
+
return role;
|
|
1816
|
+
const defaultId = role === "chat" ? "codex-low" : "codex-xhigh";
|
|
1817
|
+
if (models.models[defaultId])
|
|
1818
|
+
return defaultId;
|
|
1819
|
+
const enabled = Object.entries(models.models).find(([, entry]) => entry.enabled);
|
|
1820
|
+
return enabled?.[0] || Object.keys(models.models)[0];
|
|
1821
|
+
}
|
|
1822
|
+
function choiceDefaultsFromEntry(id, entry) {
|
|
1823
|
+
if (!id || !entry)
|
|
1824
|
+
return undefined;
|
|
1825
|
+
const provider = inferSetupProvider(id, entry);
|
|
1826
|
+
return {
|
|
1827
|
+
provider,
|
|
1828
|
+
model: entry.model,
|
|
1829
|
+
effort: entry.effort,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
function formatSetupModelSummary(id, entry) {
|
|
1833
|
+
if (!id)
|
|
1834
|
+
return "-";
|
|
1835
|
+
if (!entry)
|
|
1836
|
+
return l(`${id} (missing)`, `${id} (見つかりません)`);
|
|
1837
|
+
const provider = inferSetupProvider(id, entry) || entry.type;
|
|
1838
|
+
const fields = [provider];
|
|
1839
|
+
if (entry.model)
|
|
1840
|
+
fields.push(entry.model);
|
|
1841
|
+
if (entry.effort)
|
|
1842
|
+
fields.push(`effort=${entry.effort}`);
|
|
1843
|
+
return `${id} (${fields.join(" / ")})`;
|
|
1844
|
+
}
|
|
1845
|
+
function inferSetupProvider(id, entry) {
|
|
1846
|
+
if (entry.provider)
|
|
1847
|
+
return entry.provider;
|
|
1848
|
+
if (entry.type === "ollama")
|
|
1849
|
+
return "ollama";
|
|
1850
|
+
const exact = PROVIDER_CATALOG.find((provider) => provider.id === id);
|
|
1851
|
+
if (exact)
|
|
1852
|
+
return exact.id;
|
|
1853
|
+
const prefixed = PROVIDER_CATALOG.find((provider) => id.startsWith(`${provider.id}-`));
|
|
1854
|
+
return prefixed?.id;
|
|
1855
|
+
}
|
|
1856
|
+
function pickFallbackProvider(detected) {
|
|
1857
|
+
if (detected.length > 0)
|
|
1858
|
+
return detected[0].id;
|
|
1859
|
+
return "codex";
|
|
1860
|
+
}
|
|
1861
|
+
const ALLOWED_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
1862
|
+
const MAX_PROMPT_ATTEMPTS = 5;
|
|
1863
|
+
async function askProviderChoice(ask, role, fallbackProvider, defaults, detectedIds = []) {
|
|
1864
|
+
const knownProviders = PROVIDER_CATALOG.map((p) => p.id);
|
|
1865
|
+
const providerDefault = knownProviders.includes(defaults?.provider || "")
|
|
1866
|
+
? defaults?.provider || fallbackProvider
|
|
1867
|
+
: fallbackProvider;
|
|
1868
|
+
// プロバイダ: 番号 (検出済みリストの順) か PROVIDER_CATALOG の id を受け付ける。
|
|
1869
|
+
// 大文字小文字や前後空白は許容して正規化する。
|
|
1870
|
+
let provider = "";
|
|
1871
|
+
let catalog;
|
|
1872
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
1873
|
+
const raw = await ask(l("Provider", "プロバイダ"), providerDefault);
|
|
1874
|
+
const trimmed = raw.trim();
|
|
1875
|
+
const numeric = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
|
|
1876
|
+
if (Number.isFinite(numeric) && numeric >= 1 && numeric <= detectedIds.length) {
|
|
1877
|
+
provider = detectedIds[numeric - 1];
|
|
1878
|
+
}
|
|
1879
|
+
else {
|
|
1880
|
+
provider = trimmed.toLowerCase();
|
|
1881
|
+
}
|
|
1882
|
+
catalog = PROVIDER_CATALOG.find((p) => p.id === provider);
|
|
1883
|
+
if (catalog)
|
|
1884
|
+
break;
|
|
1885
|
+
console.log(l(` ! Unknown provider "${raw}".`, ` ⚠ プロバイダ "${raw}" は不明です。`));
|
|
1886
|
+
if (detectedIds.length > 0) {
|
|
1887
|
+
console.log(l(` Enter a number (1-${detectedIds.length}) or one of: ${knownProviders.join(" / ")}`, ` 番号 (1-${detectedIds.length}) または次のいずれかを入力: ${knownProviders.join(" / ")}`));
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
console.log(l(` Available: ${knownProviders.join(" / ")}`, ` 使えるのは: ${knownProviders.join(" / ")}`));
|
|
1891
|
+
}
|
|
1892
|
+
console.log(l(` Press Enter to use [${providerDefault}].`, ` そのまま Enter で [${providerDefault}] を採用できます。`));
|
|
1893
|
+
catalog = undefined;
|
|
1894
|
+
}
|
|
1895
|
+
if (!catalog) {
|
|
1896
|
+
throw new Error(l(`Provider input could not be resolved after ${MAX_PROMPT_ATTEMPTS} attempts. Aborting setup.`, `プロバイダの入力が ${MAX_PROMPT_ATTEMPTS} 回試行しても解決できませんでした。setup を中断します。`));
|
|
1897
|
+
}
|
|
1898
|
+
// モデル: 必須なので空は弾く。ただしフォールバック (catalog.defaultModel) があれば空 Enter で採用。
|
|
1899
|
+
// フォールバックが無いプロバイダは任意で空可。
|
|
1900
|
+
let model;
|
|
1901
|
+
const sameProviderDefaults = defaults?.provider === provider ? defaults : undefined;
|
|
1902
|
+
const modelDefault = sameProviderDefaults?.model || catalog.defaultModel || "";
|
|
1903
|
+
if (modelDefault) {
|
|
1904
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
1905
|
+
const candidate = (await ask(l("Model", "モデル"), modelDefault)).trim();
|
|
1906
|
+
if (candidate) {
|
|
1907
|
+
model = candidate;
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
console.log(l(` ! Model name cannot be empty (Enter uses [${modelDefault}]).`, ` ⚠ モデル名は空にできません (Enter のみで [${modelDefault}] が入ります)。`));
|
|
1911
|
+
}
|
|
1912
|
+
if (!model) {
|
|
1913
|
+
throw new Error(l("Model name was not provided. Aborting setup.", "モデル名が入力できませんでした。setup を中断します。"));
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
else {
|
|
1917
|
+
const candidate = (await ask(l("Model (optional, can be empty)", "モデル (任意、空可)"), "")).trim();
|
|
1918
|
+
model = candidate || undefined;
|
|
1919
|
+
}
|
|
1920
|
+
// effort: 必要なプロバイダのみ。allowed なリストに無いと弾く。
|
|
1921
|
+
let effort;
|
|
1922
|
+
if (catalog.needsEffort) {
|
|
1923
|
+
const catalogEffortDefault = role === "chat" ? catalog.defaultChatEffort || "low" : catalog.defaultBuilderEffort || "xhigh";
|
|
1924
|
+
const effortDefault = sameProviderDefaults?.effort && ALLOWED_EFFORTS.includes(sameProviderDefaults.effort)
|
|
1925
|
+
? sameProviderDefaults.effort
|
|
1926
|
+
: catalogEffortDefault;
|
|
1927
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
1928
|
+
const raw = await ask(l("effort (low/medium/high/xhigh)", "effort (low/medium/high/xhigh)"), effortDefault);
|
|
1929
|
+
const normalized = raw.trim().toLowerCase();
|
|
1930
|
+
if (ALLOWED_EFFORTS.includes(normalized)) {
|
|
1931
|
+
effort = normalized;
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
console.log(l(` ! Invalid effort "${raw}". Use one of: ${ALLOWED_EFFORTS.join(" / ")}.`, ` ⚠ effort "${raw}" は無効。${ALLOWED_EFFORTS.join(" / ")} のいずれかを入力してください。`));
|
|
1935
|
+
}
|
|
1936
|
+
if (!effort) {
|
|
1937
|
+
throw new Error(l("effort was not provided. Aborting setup.", "effort が入力できませんでした。setup を中断します。"));
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return { provider, model, effort };
|
|
1941
|
+
}
|
|
1942
|
+
async function readSetupAnswersFromInput() {
|
|
1943
|
+
const chunks = [];
|
|
1944
|
+
for await (const chunk of process.stdin) {
|
|
1945
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1946
|
+
}
|
|
1947
|
+
return Buffer.concat(chunks).toString("utf8").split(/\r?\n/);
|
|
1948
|
+
}
|
|
1949
|
+
function stringOption(value) {
|
|
1950
|
+
if (typeof value === "string") {
|
|
1951
|
+
return value;
|
|
1952
|
+
}
|
|
1953
|
+
if (Array.isArray(value)) {
|
|
1954
|
+
const last = value.at(-1);
|
|
1955
|
+
return typeof last === "string" ? last : undefined;
|
|
1956
|
+
}
|
|
1957
|
+
return undefined;
|
|
1958
|
+
}
|
|
1959
|
+
function listOption(value) {
|
|
1960
|
+
if (typeof value === "string") {
|
|
1961
|
+
return splitListOption(value);
|
|
1962
|
+
}
|
|
1963
|
+
if (Array.isArray(value)) {
|
|
1964
|
+
return value.flatMap((item) => (typeof item === "string" ? splitListOption(item) : []));
|
|
1965
|
+
}
|
|
1966
|
+
return [];
|
|
1967
|
+
}
|
|
1968
|
+
function splitListOption(value) {
|
|
1969
|
+
return value
|
|
1970
|
+
.split(",")
|
|
1971
|
+
.map((item) => item.trim())
|
|
1972
|
+
.filter(Boolean);
|
|
1973
|
+
}
|
|
1974
|
+
function printSetupHelp() {
|
|
1975
|
+
console.log(l(`Agent-Sin setup
|
|
1976
|
+
|
|
1977
|
+
Usage:
|
|
1978
|
+
agent-sin setup [--model entry] [--builder entry]
|
|
1979
|
+
[--chat-codex-model name] [--chat-effort level]
|
|
1980
|
+
[--builder-codex-model name] [--builder-effort level]
|
|
1981
|
+
agent-sin setup --reconfigure
|
|
1982
|
+
agent-sin setup --keep
|
|
1983
|
+
agent-sin setup --yes
|
|
1984
|
+
|
|
1985
|
+
Examples:
|
|
1986
|
+
agent-sin setup
|
|
1987
|
+
agent-sin setup --model chat --builder builder
|
|
1988
|
+
agent-sin setup --chat-codex-model gpt-5.5 --chat-effort low
|
|
1989
|
+
agent-sin setup --builder-codex-model gpt-5.5 --builder-effort xhigh
|
|
1990
|
+
agent-sin setup --reconfigure
|
|
1991
|
+
agent-sin setup --keep
|
|
1992
|
+
agent-sin setup --enable chat,builder,openai
|
|
1993
|
+
agent-sin setup --yes`, `Agent-Sin セットアップ
|
|
1994
|
+
|
|
1995
|
+
使い方:
|
|
1996
|
+
agent-sin setup [--model entry] [--builder entry]
|
|
1997
|
+
[--chat-codex-model name] [--chat-effort level]
|
|
1998
|
+
[--builder-codex-model name] [--builder-effort level]
|
|
1999
|
+
agent-sin setup --reconfigure
|
|
2000
|
+
agent-sin setup --keep
|
|
2001
|
+
agent-sin setup --yes
|
|
2002
|
+
|
|
2003
|
+
例:
|
|
2004
|
+
agent-sin setup
|
|
2005
|
+
agent-sin setup --model chat --builder builder
|
|
2006
|
+
agent-sin setup --chat-codex-model gpt-5.5 --chat-effort low
|
|
2007
|
+
agent-sin setup --builder-codex-model gpt-5.5 --builder-effort xhigh
|
|
2008
|
+
agent-sin setup --reconfigure
|
|
2009
|
+
agent-sin setup --keep
|
|
2010
|
+
agent-sin setup --enable chat,builder,openai
|
|
2011
|
+
agent-sin setup --yes`));
|
|
2012
|
+
}
|
|
2013
|
+
function optionsToArgs(options) {
|
|
2014
|
+
const result = {};
|
|
2015
|
+
for (const [key, value] of Object.entries(options)) {
|
|
2016
|
+
if (key !== "_") {
|
|
2017
|
+
result[key] = value;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
function parseJsonOption(value, name) {
|
|
2023
|
+
if (typeof value !== "string") {
|
|
2024
|
+
throw new Error(l(`${name} requires a JSON object`, `${name} には JSON object が必要です`));
|
|
2025
|
+
}
|
|
2026
|
+
const parsed = JSON.parse(value);
|
|
2027
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2028
|
+
throw new Error(l(`${name} requires a JSON object`, `${name} には JSON object が必要です`));
|
|
2029
|
+
}
|
|
2030
|
+
return parsed;
|
|
2031
|
+
}
|
|
2032
|
+
function printHelp() {
|
|
2033
|
+
console.log(l(`Agent-Sin CLI
|
|
2034
|
+
|
|
2035
|
+
Usage:
|
|
2036
|
+
agent-sin setup [--yes] [--model model-id]
|
|
2037
|
+
agent-sin start [message]
|
|
2038
|
+
agent-sin start --daemon
|
|
2039
|
+
agent-sin chat [message]
|
|
2040
|
+
agent-sin skills
|
|
2041
|
+
agent-sin run memo-save --text "..."
|
|
2042
|
+
agent-sin run memo-search --query "..."
|
|
2043
|
+
agent-sin run <skill-id> [--key value]
|
|
2044
|
+
agent-sin build [skill-id] [--prompt "..."]
|
|
2045
|
+
agent-sin build list
|
|
2046
|
+
agent-sin build chat <skill-id> "change request"
|
|
2047
|
+
agent-sin build status <skill-id>
|
|
2048
|
+
agent-sin build test <skill-id> [--payload '{...}']
|
|
2049
|
+
agent-sin skill new <id> [--runtime python|typescript] [--name ...] [--description ...]
|
|
2050
|
+
agent-sin skill validate <id>
|
|
2051
|
+
agent-sin skill test <id> [--payload '{...}']
|
|
2052
|
+
agent-sin profile show [soul|user|memory]
|
|
2053
|
+
agent-sin profile append <soul|user|memory> <text>
|
|
2054
|
+
agent-sin models
|
|
2055
|
+
agent-sin models keys [--provider openai|gemini|anthropic] [--check]
|
|
2056
|
+
agent-sin model set [chat|builder] <model-id>
|
|
2057
|
+
agent-sin logs [--skill skill-id]
|
|
2058
|
+
agent-sin logs --run <run-id>
|
|
2059
|
+
agent-sin logs --events [--tail 50] [--source chat|skill|cli|setup|build|schedule|discord|telegram] [--level info|warn|error]
|
|
2060
|
+
agent-sin daemon [--once]
|
|
2061
|
+
agent-sin gateway [--no-discord] [--no-telegram]
|
|
2062
|
+
agent-sin service status|install|start|stop|restart
|
|
2063
|
+
agent-sin schedules
|
|
2064
|
+
agent-sin schedules trigger <id>
|
|
2065
|
+
agent-sin notify --title <title> --body <body> [--channel macos|discord|telegram|slack|mail|auto] [--thread-id <id>] [--to <addr>] [--subtitle <s>] [--sound]
|
|
2066
|
+
agent-sin discord
|
|
2067
|
+
agent-sin telegram
|
|
2068
|
+
agent-sin telegram id [--save]
|
|
2069
|
+
agent-sin export [--out <file>]
|
|
2070
|
+
agent-sin import <archive> [--force] [--dry-run] [--no-backup]
|
|
2071
|
+
agent-sin config`, `Agent-Sin CLI
|
|
2072
|
+
|
|
2073
|
+
使い方:
|
|
2074
|
+
agent-sin setup [--yes] [--model model-id]
|
|
2075
|
+
agent-sin start [message]
|
|
2076
|
+
agent-sin start --daemon
|
|
2077
|
+
agent-sin chat [message]
|
|
2078
|
+
agent-sin skills
|
|
2079
|
+
agent-sin run memo-save --text "..."
|
|
2080
|
+
agent-sin run memo-search --query "..."
|
|
2081
|
+
agent-sin run <skill-id> [--key value]
|
|
2082
|
+
agent-sin build [skill-id] [--prompt "..."]
|
|
2083
|
+
agent-sin build list
|
|
2084
|
+
agent-sin build chat <skill-id> "変更内容"
|
|
2085
|
+
agent-sin build status <skill-id>
|
|
2086
|
+
agent-sin build test <skill-id> [--payload '{...}']
|
|
2087
|
+
agent-sin skill new <id> [--runtime python|typescript] [--name ...] [--description ...]
|
|
2088
|
+
agent-sin skill validate <id>
|
|
2089
|
+
agent-sin skill test <id> [--payload '{...}']
|
|
2090
|
+
agent-sin profile show [soul|user|memory]
|
|
2091
|
+
agent-sin profile append <soul|user|memory> <text>
|
|
2092
|
+
agent-sin models
|
|
2093
|
+
agent-sin models keys [--provider openai|gemini|anthropic] [--check]
|
|
2094
|
+
agent-sin model set [chat|builder] <model-id>
|
|
2095
|
+
agent-sin logs [--skill skill-id]
|
|
2096
|
+
agent-sin logs --run <run-id>
|
|
2097
|
+
agent-sin logs --events [--tail 50] [--source chat|skill|cli|setup|build|schedule|discord|telegram] [--level info|warn|error]
|
|
2098
|
+
agent-sin daemon [--once]
|
|
2099
|
+
agent-sin gateway [--no-discord] [--no-telegram]
|
|
2100
|
+
agent-sin service status|install|start|stop|restart
|
|
2101
|
+
agent-sin schedules
|
|
2102
|
+
agent-sin schedules trigger <id>
|
|
2103
|
+
agent-sin notify --title <title> --body <body> [--channel macos|discord|telegram|slack|mail|auto] [--thread-id <id>] [--to <addr>] [--subtitle <s>] [--sound]
|
|
2104
|
+
agent-sin discord
|
|
2105
|
+
agent-sin telegram
|
|
2106
|
+
agent-sin telegram id [--save]
|
|
2107
|
+
agent-sin export [--out <file>]
|
|
2108
|
+
agent-sin import <archive> [--force] [--dry-run] [--no-backup]
|
|
2109
|
+
agent-sin config`));
|
|
2110
|
+
}
|
|
2111
|
+
async function cmdNotify(args) {
|
|
2112
|
+
const options = parseOptions(args);
|
|
2113
|
+
const positional = options._;
|
|
2114
|
+
const title = stringOption(options.title) || positional[0] || "";
|
|
2115
|
+
const body = stringOption(options.body) || positional.slice(1).join(" ") || "";
|
|
2116
|
+
const subtitle = stringOption(options.subtitle);
|
|
2117
|
+
const sound = Boolean(options.sound);
|
|
2118
|
+
const channelRaw = stringOption(options.channel) || "auto";
|
|
2119
|
+
const to = stringOption(options.to);
|
|
2120
|
+
const threadId = stringOption(options.thread_id);
|
|
2121
|
+
const discordThreadId = threadId || stringOption(options.discord_thread_id);
|
|
2122
|
+
const telegramThreadId = threadId || stringOption(options.telegram_thread_id);
|
|
2123
|
+
if (!title && !body) {
|
|
2124
|
+
console.error(l("Usage: agent-sin notify --title <title> --body <body> [--channel macos|discord|telegram|slack|mail|auto] [--thread-id <id>] [--to <addr>] [--subtitle <s>] [--sound]", "使い方: agent-sin notify --title <title> --body <body> [--channel macos|discord|telegram|slack|mail|auto] [--thread-id <id>] [--to <addr>] [--subtitle <s>] [--sound]"));
|
|
2125
|
+
return 1;
|
|
2126
|
+
}
|
|
2127
|
+
const allowed = ["auto", "macos", "discord", "telegram", "slack", "mail", "stderr"];
|
|
2128
|
+
if (!allowed.includes(channelRaw)) {
|
|
2129
|
+
console.error(l(`Invalid --channel: ${channelRaw} (allowed: ${allowed.join(", ")})`, `--channel が不正です: ${channelRaw} (使用可: ${allowed.join(", ")})`));
|
|
2130
|
+
return 1;
|
|
2131
|
+
}
|
|
2132
|
+
try {
|
|
2133
|
+
const result = await notify({
|
|
2134
|
+
title,
|
|
2135
|
+
body,
|
|
2136
|
+
subtitle,
|
|
2137
|
+
sound,
|
|
2138
|
+
channel: channelRaw,
|
|
2139
|
+
to,
|
|
2140
|
+
discordThreadId,
|
|
2141
|
+
telegramThreadId,
|
|
2142
|
+
});
|
|
2143
|
+
console.log(`notify: ${result.channel} ${result.ok ? "ok" : l("failed", "失敗")}${result.detail ? ` - ${result.detail}` : ""}`);
|
|
2144
|
+
return result.ok ? 0 : 1;
|
|
2145
|
+
}
|
|
2146
|
+
catch (error) {
|
|
2147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2148
|
+
console.error(l(`notify failed: ${message}`, `通知に失敗しました: ${message}`));
|
|
2149
|
+
return 1;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
async function cmdDiscord(args) {
|
|
2153
|
+
const options = parseOptions(args);
|
|
2154
|
+
if (options.help) {
|
|
2155
|
+
console.log(l(`Usage:
|
|
2156
|
+
agent-sin discord
|
|
2157
|
+
|
|
2158
|
+
Run as a Discord bot. Reads from environment variables:
|
|
2159
|
+
AGENT_SIN_DISCORD_BOT_TOKEN Discord bot token (required)
|
|
2160
|
+
AGENT_SIN_DISCORD_ALLOWED_USER_IDS Comma-separated Discord user IDs allowed to talk to the bot (required)
|
|
2161
|
+
AGENT_SIN_DISCORD_LISTEN_CHANNEL_IDS Optional: parent channels where @mentioning the bot auto-creates a
|
|
2162
|
+
new thread. Inside that thread no @mention is needed. Requires the
|
|
2163
|
+
privileged Message Content intent in the dev portal.
|
|
2164
|
+
|
|
2165
|
+
The bot reacts to your message in real time (received → thinking → skill → done / error).
|
|
2166
|
+
Created thread IDs are persisted under <workspace>/discord/bot-threads.json.
|
|
2167
|
+
Press Ctrl+C to stop.`, `使い方:
|
|
2168
|
+
agent-sin discord
|
|
2169
|
+
|
|
2170
|
+
Discord bot として起動します。環境変数を読み込みます:
|
|
2171
|
+
AGENT_SIN_DISCORD_BOT_TOKEN Discord bot token (必須)
|
|
2172
|
+
AGENT_SIN_DISCORD_ALLOWED_USER_IDS bot と会話できる Discord user ID のカンマ区切り (必須)
|
|
2173
|
+
AGENT_SIN_DISCORD_LISTEN_CHANNEL_IDS 任意: bot メンションで新規スレッドを作る親チャンネル
|
|
2174
|
+
|
|
2175
|
+
受信 → 考え中 → スキル → 完了 / エラーをリアルタイムに反映します。
|
|
2176
|
+
作成したスレッド ID は <workspace>/discord/bot-threads.json に保存します。
|
|
2177
|
+
停止は Ctrl+C です。`));
|
|
2178
|
+
return 0;
|
|
2179
|
+
}
|
|
2180
|
+
const config = await loadConfig();
|
|
2181
|
+
await warnIfSchedulesNeedService(config);
|
|
2182
|
+
return await runDiscordBot(config);
|
|
2183
|
+
}
|
|
2184
|
+
async function cmdTelegram(args) {
|
|
2185
|
+
const options = parseOptions(args);
|
|
2186
|
+
const subcommand = options._[0];
|
|
2187
|
+
if (subcommand === "id" || subcommand === "whoami") {
|
|
2188
|
+
return cmdTelegramId(options);
|
|
2189
|
+
}
|
|
2190
|
+
if (options.help) {
|
|
2191
|
+
console.log(l(`Usage:
|
|
2192
|
+
agent-sin telegram
|
|
2193
|
+
agent-sin telegram id [--save] [--wait seconds]
|
|
2194
|
+
|
|
2195
|
+
Run as a Telegram bot. Reads from environment variables:
|
|
2196
|
+
AGENT_SIN_TELEGRAM_BOT_TOKEN Telegram bot token from BotFather (required)
|
|
2197
|
+
AGENT_SIN_TELEGRAM_ALLOWED_USER_IDS Your Telegram user ID(s), comma-separated (required)
|
|
2198
|
+
AGENT_SIN_TELEGRAM_LISTEN_CHAT_IDS Optional: group chat IDs used for logging and notify fallback
|
|
2199
|
+
|
|
2200
|
+
The bot responds in DMs, when mentioned, or when you reply to one of its messages.
|
|
2201
|
+
Private chat topics are kept separate, and private chats use draft streaming while generating.
|
|
2202
|
+
Progress messages are quiet by default. Use /progress detail in a chat when needed.
|
|
2203
|
+
Polling offset is persisted under <workspace>/telegram/offset.json.
|
|
2204
|
+
Press Ctrl+C to stop.`, `使い方:
|
|
2205
|
+
agent-sin telegram
|
|
2206
|
+
agent-sin telegram id [--save] [--wait seconds]
|
|
2207
|
+
|
|
2208
|
+
Telegram bot として起動します。環境変数を読み込みます:
|
|
2209
|
+
AGENT_SIN_TELEGRAM_BOT_TOKEN BotFather の Telegram bot token (必須)
|
|
2210
|
+
AGENT_SIN_TELEGRAM_ALLOWED_USER_IDS Telegram user ID のカンマ区切り (必須)
|
|
2211
|
+
AGENT_SIN_TELEGRAM_LISTEN_CHAT_IDS 任意: ログや通知のフォールバックに使う group chat ID
|
|
2212
|
+
|
|
2213
|
+
DM、メンション、bot への返信に応答します。
|
|
2214
|
+
private chat は生成中に draft streaming を使います。
|
|
2215
|
+
進捗通知は静音が既定です。必要ならチャットで /progress detail を使ってください。
|
|
2216
|
+
Polling offset は <workspace>/telegram/offset.json に保存します。
|
|
2217
|
+
停止は Ctrl+C です。`));
|
|
2218
|
+
return 0;
|
|
2219
|
+
}
|
|
2220
|
+
if (subcommand) {
|
|
2221
|
+
console.error(l(`Unknown subcommand: telegram ${subcommand}`, `不明なサブコマンドです: telegram ${subcommand}`));
|
|
2222
|
+
console.error(l("Usage: agent-sin telegram [id --save]", "使い方: agent-sin telegram [id --save]"));
|
|
2223
|
+
return 1;
|
|
2224
|
+
}
|
|
2225
|
+
const config = await loadConfig();
|
|
2226
|
+
await warnIfSchedulesNeedService(config);
|
|
2227
|
+
return await runTelegramBot(config);
|
|
2228
|
+
}
|
|
2229
|
+
async function cmdTelegramId(options) {
|
|
2230
|
+
const token = (process.env.AGENT_SIN_TELEGRAM_BOT_TOKEN || "").trim();
|
|
2231
|
+
if (!token) {
|
|
2232
|
+
console.error(l("AGENT_SIN_TELEGRAM_BOT_TOKEN is not set. Add it to ~/.agent-sin/.env first.", "AGENT_SIN_TELEGRAM_BOT_TOKEN が未設定です。先に ~/.agent-sin/.env に追加してください。"));
|
|
2233
|
+
return 1;
|
|
2234
|
+
}
|
|
2235
|
+
const waitSeconds = parsePositiveInteger(stringOption(options.wait) || "5", 5);
|
|
2236
|
+
let updates;
|
|
2237
|
+
try {
|
|
2238
|
+
updates = await fetchTelegramUpdatesForIdentity(token, waitSeconds);
|
|
2239
|
+
}
|
|
2240
|
+
catch (error) {
|
|
2241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2242
|
+
console.error(l(`Telegram updates could not be read: ${message}`, `Telegram updates を読めませんでした: ${message}`));
|
|
2243
|
+
console.error(l("If another agent-sin telegram process is running, stop it briefly and run this command again.", "別の agent-sin telegram が起動中なら一度止めてから再実行してください。"));
|
|
2244
|
+
return 1;
|
|
2245
|
+
}
|
|
2246
|
+
const candidates = extractTelegramIdentityCandidates(updates);
|
|
2247
|
+
if (candidates.length === 0) {
|
|
2248
|
+
console.log(l("Telegram user ID was not found in pending updates.", "pending update から Telegram user ID が見つかりませんでした。"));
|
|
2249
|
+
console.log(l("Send a new DM such as /start or id to your bot, wait a few seconds, then run:", "bot に /start や id などの新しい DM を送り、数秒待ってから実行してください:"));
|
|
2250
|
+
console.log(" agent-sin telegram id");
|
|
2251
|
+
console.log(l("If it still stays empty, run: agent-sin telegram id --wait 30 and send the DM while it waits.", "まだ空なら agent-sin telegram id --wait 30 を実行し、待機中に DM を送ってください。"));
|
|
2252
|
+
return 1;
|
|
2253
|
+
}
|
|
2254
|
+
console.log(l("Telegram user ID candidates:", "Telegram user ID 候補:"));
|
|
2255
|
+
for (const candidate of candidates.slice(0, 5)) {
|
|
2256
|
+
console.log(` user_id=${candidate.userId} chat_id=${candidate.chatId} chat=${candidate.chatType} name=${candidate.displayName}`);
|
|
2257
|
+
}
|
|
2258
|
+
if (!options.save) {
|
|
2259
|
+
console.log("");
|
|
2260
|
+
console.log(l("To save the first private user to ~/.agent-sin/.env:", "最初の private user を ~/.agent-sin/.env に保存するには:"));
|
|
2261
|
+
console.log(" agent-sin telegram id --save");
|
|
2262
|
+
return 0;
|
|
2263
|
+
}
|
|
2264
|
+
const selected = selectTelegramIdentityCandidate(candidates, stringOption(options.user_id));
|
|
2265
|
+
if (!selected) {
|
|
2266
|
+
console.error(l("Could not choose a Telegram user ID. Re-run with --user-id <id>.", "Telegram user ID を選べませんでした。--user-id <id> を付けて再実行してください。"));
|
|
2267
|
+
return 1;
|
|
2268
|
+
}
|
|
2269
|
+
const config = await loadConfig();
|
|
2270
|
+
const entries = [{ key: "AGENT_SIN_TELEGRAM_ALLOWED_USER_IDS", value: selected.userId }];
|
|
2271
|
+
if (selected.chatType === "private" && !process.env.AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID && !process.env.AGENT_SIN_TELEGRAM_CHAT_ID) {
|
|
2272
|
+
entries.push({ key: "AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID", value: selected.chatId });
|
|
2273
|
+
}
|
|
2274
|
+
await upsertDotenv(config.workspace, entries);
|
|
2275
|
+
console.log(l(`Saved AGENT_SIN_TELEGRAM_ALLOWED_USER_IDS=${selected.userId}`, `保存しました AGENT_SIN_TELEGRAM_ALLOWED_USER_IDS=${selected.userId}`));
|
|
2276
|
+
if (entries.some((entry) => entry.key === "AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID")) {
|
|
2277
|
+
console.log(l(`Saved AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID=${selected.chatId}`, `保存しました AGENT_SIN_TELEGRAM_NOTIFY_CHAT_ID=${selected.chatId}`));
|
|
2278
|
+
}
|
|
2279
|
+
return 0;
|
|
2280
|
+
}
|
|
2281
|
+
function parsePositiveInteger(raw, fallback) {
|
|
2282
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2283
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
2284
|
+
}
|
|
2285
|
+
async function fetchTelegramUpdatesForIdentity(token, waitSeconds) {
|
|
2286
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/getUpdates`, {
|
|
2287
|
+
method: "POST",
|
|
2288
|
+
headers: { "content-type": "application/json" },
|
|
2289
|
+
body: JSON.stringify({
|
|
2290
|
+
timeout: waitSeconds,
|
|
2291
|
+
allowed_updates: ["message"],
|
|
2292
|
+
}),
|
|
2293
|
+
});
|
|
2294
|
+
const data = (await response.json().catch(() => ({})));
|
|
2295
|
+
if (!response.ok || !data.ok) {
|
|
2296
|
+
throw new Error(data.description || `Telegram API returned HTTP ${response.status}`);
|
|
2297
|
+
}
|
|
2298
|
+
return Array.isArray(data.result) ? data.result : [];
|
|
2299
|
+
}
|
|
2300
|
+
function selectTelegramIdentityCandidate(candidates, userId) {
|
|
2301
|
+
if (userId) {
|
|
2302
|
+
return candidates.find((candidate) => candidate.userId === userId) || null;
|
|
2303
|
+
}
|
|
2304
|
+
return candidates.find((candidate) => candidate.chatType === "private") || candidates[0] || null;
|
|
2305
|
+
}
|
|
2306
|
+
async function cmdSchedules(args) {
|
|
2307
|
+
const [sub, ...rest] = args;
|
|
2308
|
+
if (sub === "trigger") {
|
|
2309
|
+
return cmdSchedulesTrigger(rest);
|
|
2310
|
+
}
|
|
2311
|
+
if (sub && sub !== "list") {
|
|
2312
|
+
console.error(l(`Unknown subcommand: schedules ${sub}`, `不明なサブコマンドです: schedules ${sub}`));
|
|
2313
|
+
return 1;
|
|
2314
|
+
}
|
|
2315
|
+
const config = await loadConfig();
|
|
2316
|
+
const schedules = await loadSchedules(config.workspace);
|
|
2317
|
+
if (schedules.length === 0) {
|
|
2318
|
+
console.log(l(`No schedules. Run \`agent-sin run schedule-add --payload '{...}'\` or edit ${path.join(config.workspace, "schedules.yaml")}`, `スケジュールはありません。 \`agent-sin run schedule-add --payload '{...}'\` を実行するか ${path.join(config.workspace, "schedules.yaml")} を編集してください`));
|
|
2319
|
+
return 0;
|
|
2320
|
+
}
|
|
2321
|
+
const now = new Date();
|
|
2322
|
+
console.log("id\tcron\tskill\tenabled\tnext_run");
|
|
2323
|
+
for (const schedule of schedules) {
|
|
2324
|
+
const next = schedule.enabled ? nextRunAfter(schedule.expression, now) : null;
|
|
2325
|
+
const nextStr = next ? next.toISOString() : "-";
|
|
2326
|
+
console.log(`${schedule.id}\t${schedule.cron}\t${schedule.skill}\t${schedule.enabled}\t${nextStr}`);
|
|
2327
|
+
}
|
|
2328
|
+
await warnIfSchedulesNeedService(config, schedules);
|
|
2329
|
+
return 0;
|
|
2330
|
+
}
|
|
2331
|
+
async function cmdSchedulesTrigger(args) {
|
|
2332
|
+
const id = args[0];
|
|
2333
|
+
if (!id) {
|
|
2334
|
+
console.error(l("Usage: agent-sin schedules trigger <id>", "使い方: agent-sin schedules trigger <id>"));
|
|
2335
|
+
return 1;
|
|
2336
|
+
}
|
|
2337
|
+
const config = await loadConfig();
|
|
2338
|
+
const schedules = await loadSchedules(config.workspace);
|
|
2339
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
2340
|
+
if (!schedule) {
|
|
2341
|
+
console.error(l(`Schedule not found: ${id}`, `スケジュールが見つかりません: ${id}`));
|
|
2342
|
+
return 1;
|
|
2343
|
+
}
|
|
2344
|
+
const result = await runScheduledSkill(config, schedule, "trigger");
|
|
2345
|
+
return result;
|
|
2346
|
+
}
|
|
2347
|
+
async function cmdDaemon(args) {
|
|
2348
|
+
const options = parseOptions(args);
|
|
2349
|
+
const config = await loadConfig();
|
|
2350
|
+
return runScheduleDaemon(config, { once: Boolean(options.once) });
|
|
2351
|
+
}
|
|
2352
|
+
async function cmdGateway(args) {
|
|
2353
|
+
const options = parseOptions(args);
|
|
2354
|
+
if (options.help) {
|
|
2355
|
+
console.log(l(`Usage:
|
|
2356
|
+
agent-sin gateway [--no-discord] [--no-telegram] [--once]
|
|
2357
|
+
agent-sin start --daemon
|
|
2358
|
+
agent-sin service run
|
|
2359
|
+
|
|
2360
|
+
Run Agent-Sin as a long-lived gateway. It starts the scheduler when enabled
|
|
2361
|
+
schedules exist, and starts Discord / Telegram bots when their environment variables
|
|
2362
|
+
are configured. Use agent-sin service install on macOS to keep it running.`, `使い方:
|
|
2363
|
+
agent-sin gateway [--no-discord] [--no-telegram] [--once]
|
|
2364
|
+
agent-sin start --daemon
|
|
2365
|
+
agent-sin service run
|
|
2366
|
+
|
|
2367
|
+
Agent-Sin を常駐 gateway として起動します。有効なスケジュールがあれば scheduler を、
|
|
2368
|
+
環境変数が設定済みなら Discord / Telegram bot を起動します。
|
|
2369
|
+
macOS で常駐させるには agent-sin service install を使ってください。`));
|
|
2370
|
+
return 0;
|
|
2371
|
+
}
|
|
2372
|
+
const config = await loadConfig();
|
|
2373
|
+
await maybePromoteDailyMemory(config, { eventSource: "schedule" });
|
|
2374
|
+
const schedules = await loadSchedules(config.workspace);
|
|
2375
|
+
const enabled = schedules.filter((schedule) => schedule.enabled);
|
|
2376
|
+
const startScheduler = enabled.length > 0;
|
|
2377
|
+
const startDiscord = !options.no_discord && hasDiscordConfig();
|
|
2378
|
+
const startTelegram = !options.no_telegram && hasTelegramConfig();
|
|
2379
|
+
if (!startScheduler && !startDiscord && !startTelegram) {
|
|
2380
|
+
console.log(l("agent-sin gateway: no enabled schedules and chat bots are not configured.", "agent-sin gateway: 有効なスケジュールがなく、チャット bot も未設定です。"));
|
|
2381
|
+
console.log(l("Add schedules, or set Discord allowed user IDs / Telegram bot token.", "スケジュールを追加するか、Discord allowed user IDs / Telegram bot token を設定してください。"));
|
|
2382
|
+
return 0;
|
|
2383
|
+
}
|
|
2384
|
+
console.log(l(`agent-sin gateway: starting (${startScheduler ? `${enabled.length} schedule(s)` : "scheduler idle"}, ${startDiscord ? "discord on" : "discord off"}, ${startTelegram ? "telegram on" : "telegram off"})`, `agent-sin gateway: 起動します (${startScheduler ? `${enabled.length}件のスケジュール` : "scheduler 待機"}, ${startDiscord ? "discord 有効" : "discord 無効"}, ${startTelegram ? "telegram 有効" : "telegram 無効"})`));
|
|
2385
|
+
const tasks = [];
|
|
2386
|
+
if (startScheduler) {
|
|
2387
|
+
tasks.push(runScheduleDaemon(config, { once: Boolean(options.once) }));
|
|
2388
|
+
}
|
|
2389
|
+
if (startDiscord) {
|
|
2390
|
+
tasks.push(runDiscordBot(config));
|
|
2391
|
+
}
|
|
2392
|
+
else if (!options.no_discord && !startTelegram) {
|
|
2393
|
+
console.log(l("agent-sin gateway: Discord not configured.", "agent-sin gateway: Discord は未設定です。"));
|
|
2394
|
+
}
|
|
2395
|
+
if (startTelegram) {
|
|
2396
|
+
tasks.push(runTelegramBot(config));
|
|
2397
|
+
}
|
|
2398
|
+
else if (!options.no_telegram && !startDiscord) {
|
|
2399
|
+
console.log(l("agent-sin gateway: Telegram not configured.", "agent-sin gateway: Telegram は未設定です。"));
|
|
2400
|
+
}
|
|
2401
|
+
const results = await Promise.all(tasks);
|
|
2402
|
+
return Math.max(...results);
|
|
2403
|
+
}
|
|
2404
|
+
async function cmdService(args) {
|
|
2405
|
+
const [sub = "status", ...rest] = args;
|
|
2406
|
+
const options = parseOptions(rest);
|
|
2407
|
+
const config = await loadConfig();
|
|
2408
|
+
switch (sub) {
|
|
2409
|
+
case "run":
|
|
2410
|
+
return cmdGateway(rest);
|
|
2411
|
+
case "plist":
|
|
2412
|
+
case "manifest": {
|
|
2413
|
+
const provider = getServiceProvider();
|
|
2414
|
+
if (!provider.supported) {
|
|
2415
|
+
console.error(provider.notSupportedReason());
|
|
2416
|
+
return 1;
|
|
2417
|
+
}
|
|
2418
|
+
console.log(provider.manifestText(config));
|
|
2419
|
+
return 0;
|
|
2420
|
+
}
|
|
2421
|
+
case "install":
|
|
2422
|
+
return installService(config, options);
|
|
2423
|
+
case "start":
|
|
2424
|
+
return startService(config);
|
|
2425
|
+
case "stop":
|
|
2426
|
+
return stopService(config);
|
|
2427
|
+
case "restart": {
|
|
2428
|
+
await stopService(config, { quiet: true, wait: true });
|
|
2429
|
+
return startService(config);
|
|
2430
|
+
}
|
|
2431
|
+
case "status":
|
|
2432
|
+
return serviceStatus(config);
|
|
2433
|
+
case "help":
|
|
2434
|
+
case "--help":
|
|
2435
|
+
case "-h":
|
|
2436
|
+
printServiceHelp();
|
|
2437
|
+
return 0;
|
|
2438
|
+
default:
|
|
2439
|
+
console.error(l(`Unknown subcommand: service ${sub}`, `不明なサブコマンドです: service ${sub}`));
|
|
2440
|
+
printServiceHelp();
|
|
2441
|
+
return 1;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
async function runScheduleDaemon(config, options = {}) {
|
|
2445
|
+
await maybePromoteDailyMemory(config, { eventSource: "schedule" });
|
|
2446
|
+
const schedules = await loadSchedules(config.workspace);
|
|
2447
|
+
const enabled = schedules.filter((schedule) => schedule.enabled);
|
|
2448
|
+
if (enabled.length === 0) {
|
|
2449
|
+
console.log(l(`No enabled schedules. Run \`agent-sin run schedule-add --payload '{...}'\` (or schedule-toggle to re-enable) or edit ${path.join(config.workspace, "schedules.yaml")}`, `有効なスケジュールはありません。 \`agent-sin run schedule-add --payload '{...}'\` を実行するか schedule-toggle で再有効化、または ${path.join(config.workspace, "schedules.yaml")} を編集してください`));
|
|
2450
|
+
return 0;
|
|
2451
|
+
}
|
|
2452
|
+
console.log(l(`agent-sin daemon: ${enabled.length} active schedule(s)`, `agent-sin daemon: 有効なスケジュール ${enabled.length}件`));
|
|
2453
|
+
for (const schedule of enabled) {
|
|
2454
|
+
const next = nextRunAfter(schedule.expression, new Date());
|
|
2455
|
+
console.log(` ${schedule.id}\t${schedule.cron}\t${schedule.skill}\tnext=${next ? next.toISOString() : "-"}`);
|
|
2456
|
+
}
|
|
2457
|
+
await appendEventLog(config, {
|
|
2458
|
+
level: "info",
|
|
2459
|
+
source: "schedule",
|
|
2460
|
+
event: "daemon_started",
|
|
2461
|
+
details: { count: enabled.length, schedules: enabled.map((schedule) => schedule.id) },
|
|
2462
|
+
});
|
|
2463
|
+
const stop = options.stop || createStopController();
|
|
2464
|
+
while (!stop.signaled) {
|
|
2465
|
+
const now = new Date();
|
|
2466
|
+
const delay = msUntilNextMinute(now);
|
|
2467
|
+
await sleepInterruptible(delay, stop);
|
|
2468
|
+
if (stop.signaled) {
|
|
2469
|
+
break;
|
|
2470
|
+
}
|
|
2471
|
+
const tick = roundDownToMinute(new Date());
|
|
2472
|
+
for (const schedule of enabled) {
|
|
2473
|
+
if (matchesCron(schedule.expression, tick)) {
|
|
2474
|
+
await runScheduledSkill(config, schedule, "tick");
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (options.once) {
|
|
2478
|
+
break;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
await appendEventLog(config, {
|
|
2482
|
+
level: "info",
|
|
2483
|
+
source: "schedule",
|
|
2484
|
+
event: "daemon_stopped",
|
|
2485
|
+
});
|
|
2486
|
+
console.log(l("agent-sin daemon: stopped", "agent-sin daemon: 停止しました"));
|
|
2487
|
+
return 0;
|
|
2488
|
+
}
|
|
2489
|
+
async function runScheduledSkill(config, schedule, reason) {
|
|
2490
|
+
await appendEventLog(config, {
|
|
2491
|
+
level: "info",
|
|
2492
|
+
source: "schedule",
|
|
2493
|
+
event: "schedule_fired",
|
|
2494
|
+
message: `${schedule.id} → ${schedule.skill}`,
|
|
2495
|
+
details: { schedule_id: schedule.id, skill_id: schedule.skill, reason, cron: schedule.cron },
|
|
2496
|
+
});
|
|
2497
|
+
try {
|
|
2498
|
+
const response = await runSkill(config, schedule.skill, schedule.args, { approved: schedule.approve });
|
|
2499
|
+
console.log(`[${schedule.id}] ${response.result.status} ${response.result.title}${response.result.summary ? ` / ${response.result.summary}` : ""}`);
|
|
2500
|
+
for (const saved of response.saved_outputs.filter((item) => item.show_saved !== false)) {
|
|
2501
|
+
console.log(`[${schedule.id}] ${l(`saved: ${saved.path}`, `保存: ${saved.path}`)}`);
|
|
2502
|
+
}
|
|
2503
|
+
return response.result.status === "error" ? 1 : 0;
|
|
2504
|
+
}
|
|
2505
|
+
catch (error) {
|
|
2506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2507
|
+
console.error(`[${schedule.id}] ${l(`error: ${message}`, `エラー: ${message}`)}`);
|
|
2508
|
+
await appendEventLog(config, {
|
|
2509
|
+
level: "error",
|
|
2510
|
+
source: "schedule",
|
|
2511
|
+
event: "schedule_error",
|
|
2512
|
+
message,
|
|
2513
|
+
details: { schedule_id: schedule.id, skill_id: schedule.skill, reason },
|
|
2514
|
+
});
|
|
2515
|
+
return 1;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
function createStopController() {
|
|
2519
|
+
const controller = { signaled: false, resolveWaiters: new Set() };
|
|
2520
|
+
const handler = () => {
|
|
2521
|
+
if (controller.signaled) {
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
controller.signaled = true;
|
|
2525
|
+
for (const resolve of controller.resolveWaiters) {
|
|
2526
|
+
resolve();
|
|
2527
|
+
}
|
|
2528
|
+
controller.resolveWaiters.clear();
|
|
2529
|
+
};
|
|
2530
|
+
process.once("SIGINT", handler);
|
|
2531
|
+
process.once("SIGTERM", handler);
|
|
2532
|
+
return controller;
|
|
2533
|
+
}
|
|
2534
|
+
function sleepInterruptible(ms, controller) {
|
|
2535
|
+
if (controller.signaled || ms <= 0) {
|
|
2536
|
+
return Promise.resolve();
|
|
2537
|
+
}
|
|
2538
|
+
return new Promise((resolve) => {
|
|
2539
|
+
let settled = false;
|
|
2540
|
+
const settle = () => {
|
|
2541
|
+
if (settled) {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
settled = true;
|
|
2545
|
+
clearTimeout(timer);
|
|
2546
|
+
controller.resolveWaiters.delete(settle);
|
|
2547
|
+
resolve();
|
|
2548
|
+
};
|
|
2549
|
+
const timer = setTimeout(settle, ms);
|
|
2550
|
+
controller.resolveWaiters.add(settle);
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
function msUntilNextMinute(from) {
|
|
2554
|
+
const next = roundDownToMinute(new Date(from.getTime() + 60_000));
|
|
2555
|
+
return Math.max(0, next.getTime() - from.getTime());
|
|
2556
|
+
}
|
|
2557
|
+
function roundDownToMinute(date) {
|
|
2558
|
+
const copy = new Date(date.getTime());
|
|
2559
|
+
copy.setSeconds(0, 0);
|
|
2560
|
+
return copy;
|
|
2561
|
+
}
|
|
2562
|
+
function hasDiscordConfig() {
|
|
2563
|
+
return Boolean((process.env.AGENT_SIN_DISCORD_BOT_TOKEN || "").trim() &&
|
|
2564
|
+
(process.env.AGENT_SIN_DISCORD_ALLOWED_USER_IDS || "").trim());
|
|
2565
|
+
}
|
|
2566
|
+
function hasTelegramConfig() {
|
|
2567
|
+
return Boolean((process.env.AGENT_SIN_TELEGRAM_BOT_TOKEN || "").trim() &&
|
|
2568
|
+
(process.env.AGENT_SIN_TELEGRAM_ALLOWED_USER_IDS || "").trim());
|
|
2569
|
+
}
|
|
2570
|
+
async function warnIfSchedulesNeedService(config, knownSchedules) {
|
|
2571
|
+
const schedules = knownSchedules || (await loadSchedules(config.workspace));
|
|
2572
|
+
const enabled = schedules.filter((schedule) => schedule.enabled);
|
|
2573
|
+
if (enabled.length === 0) {
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
if (await isSchedulerProcessRunning()) {
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
console.log(l(`warning: ${enabled.length} enabled schedule(s), but no scheduler daemon is running. Run: agent-sin start --daemon`, `警告: 有効なスケジュールが ${enabled.length}件ありますが、scheduler daemon が起動していません。実行: agent-sin start --daemon`));
|
|
2580
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
2581
|
+
console.log(l(" For always-on startup: agent-sin service install", " 常駐起動するには: agent-sin service install"));
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
function printServiceHelp() {
|
|
2585
|
+
const platformLine = process.platform === "darwin"
|
|
2586
|
+
? l("On macOS, install writes a LaunchAgent that keeps agent-sin service run alive.", "macOS では install が LaunchAgent を作成し、agent-sin service run を常駐させます。")
|
|
2587
|
+
: process.platform === "win32"
|
|
2588
|
+
? l("On Windows, install creates a Task Scheduler logon task that keeps agent-sin service run alive.", "Windows では install が Task Scheduler のログオンタスクを作成し、agent-sin service run を常駐させます。")
|
|
2589
|
+
: l("agent-sin service is supported on macOS (launchd) and Windows (Task Scheduler).", "agent-sin service は macOS (launchd) と Windows (Task Scheduler) で対応しています。");
|
|
2590
|
+
const manifestCmd = process.platform === "win32" ? "agent-sin service manifest" : "agent-sin service plist";
|
|
2591
|
+
console.log(l(`Agent-Sin service
|
|
2592
|
+
|
|
2593
|
+
Usage:
|
|
2594
|
+
agent-sin service status
|
|
2595
|
+
agent-sin service install [--no-start]
|
|
2596
|
+
agent-sin service start
|
|
2597
|
+
agent-sin service stop
|
|
2598
|
+
agent-sin service restart
|
|
2599
|
+
${manifestCmd}
|
|
2600
|
+
agent-sin service run
|
|
2601
|
+
|
|
2602
|
+
${platformLine}
|
|
2603
|
+
The service runs the scheduler and, when configured, Discord / Telegram bots.`, `Agent-Sin service
|
|
2604
|
+
|
|
2605
|
+
使い方:
|
|
2606
|
+
agent-sin service status
|
|
2607
|
+
agent-sin service install [--no-start]
|
|
2608
|
+
agent-sin service start
|
|
2609
|
+
agent-sin service stop
|
|
2610
|
+
agent-sin service restart
|
|
2611
|
+
${manifestCmd}
|
|
2612
|
+
agent-sin service run
|
|
2613
|
+
|
|
2614
|
+
${platformLine}
|
|
2615
|
+
service は scheduler と、設定済みの場合は Discord / Telegram bot を起動します。`));
|
|
2616
|
+
}
|
|
2617
|
+
async function installService(config, options) {
|
|
2618
|
+
const provider = getServiceProvider();
|
|
2619
|
+
if (!provider.supported) {
|
|
2620
|
+
console.error(provider.notSupportedReason());
|
|
2621
|
+
return 1;
|
|
2622
|
+
}
|
|
2623
|
+
try {
|
|
2624
|
+
await provider.install(config, { noStart: Boolean(options.no_start) });
|
|
2625
|
+
}
|
|
2626
|
+
catch (error) {
|
|
2627
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2628
|
+
return 1;
|
|
2629
|
+
}
|
|
2630
|
+
const status = await provider.status(config);
|
|
2631
|
+
console.log(l(`service installed: ${status.manifestPath}`, `service をインストールしました: ${status.manifestPath}`));
|
|
2632
|
+
if (options.no_start) {
|
|
2633
|
+
console.log(l("service not started (--no-start).", "service は起動していません (--no-start)。"));
|
|
2634
|
+
}
|
|
2635
|
+
else {
|
|
2636
|
+
console.log(l(`service started: ${serviceLabel()}`, `service を起動しました: ${serviceLabel()}`));
|
|
2637
|
+
}
|
|
2638
|
+
return 0;
|
|
2639
|
+
}
|
|
2640
|
+
async function startService(config) {
|
|
2641
|
+
const provider = getServiceProvider();
|
|
2642
|
+
if (!provider.supported) {
|
|
2643
|
+
console.error(provider.notSupportedReason());
|
|
2644
|
+
return 1;
|
|
2645
|
+
}
|
|
2646
|
+
try {
|
|
2647
|
+
await provider.start(config);
|
|
2648
|
+
}
|
|
2649
|
+
catch (error) {
|
|
2650
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2651
|
+
return 1;
|
|
2652
|
+
}
|
|
2653
|
+
console.log(l(`service started: ${serviceLabel()}`, `service を起動しました: ${serviceLabel()}`));
|
|
2654
|
+
return 0;
|
|
2655
|
+
}
|
|
2656
|
+
async function stopService(_config, options = {}) {
|
|
2657
|
+
const provider = getServiceProvider();
|
|
2658
|
+
if (!provider.supported) {
|
|
2659
|
+
if (!options.quiet)
|
|
2660
|
+
console.error(provider.notSupportedReason());
|
|
2661
|
+
return 1;
|
|
2662
|
+
}
|
|
2663
|
+
try {
|
|
2664
|
+
await provider.stop({ quiet: options.quiet, wait: options.wait });
|
|
2665
|
+
}
|
|
2666
|
+
catch (error) {
|
|
2667
|
+
if (!options.quiet)
|
|
2668
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2669
|
+
return 1;
|
|
2670
|
+
}
|
|
2671
|
+
if (!options.quiet) {
|
|
2672
|
+
console.log(l(`service stopped: ${serviceLabel()}`, `service を停止しました: ${serviceLabel()}`));
|
|
2673
|
+
}
|
|
2674
|
+
return 0;
|
|
2675
|
+
}
|
|
2676
|
+
async function serviceStatus(config) {
|
|
2677
|
+
const provider = getServiceProvider();
|
|
2678
|
+
const schedules = await loadSchedules(config.workspace);
|
|
2679
|
+
const enabled = schedules.filter((schedule) => schedule.enabled);
|
|
2680
|
+
const processes = await findAgentSinServiceProcesses();
|
|
2681
|
+
console.log(`service: ${serviceLabel()}`);
|
|
2682
|
+
console.log(`workspace: ${config.workspace}`);
|
|
2683
|
+
if (provider.supported) {
|
|
2684
|
+
const status = await provider.status(config);
|
|
2685
|
+
const manifestKindLabel = status.manifestKind === "schtasks" ? "schtasks" : "plist";
|
|
2686
|
+
console.log(`${manifestKindLabel}: ${status.manifestPath || "(none)"} ${status.installed ? "installed" : "missing"}`);
|
|
2687
|
+
}
|
|
2688
|
+
else {
|
|
2689
|
+
console.log(l(`platform: not supported on ${process.platform}`, `platform: ${process.platform} は未対応です`));
|
|
2690
|
+
}
|
|
2691
|
+
console.log(l(`enabled schedules: ${enabled.length}`, `有効なスケジュール: ${enabled.length}`));
|
|
2692
|
+
console.log(l(`scheduler process: ${processes.some((line) => isSchedulerCommandLine(line)) ? "running" : "not running"}`, `scheduler process: ${processes.some((line) => isSchedulerCommandLine(line)) ? "起動中" : "未起動"}`));
|
|
2693
|
+
console.log(l(`discord process: ${processes.some((line) => /\sdiscord(?:\s|$)/.test(line)) ? "running" : "not running"}`, `discord process: ${processes.some((line) => /\sdiscord(?:\s|$)/.test(line)) ? "起動中" : "未起動"}`));
|
|
2694
|
+
console.log(l(`telegram process: ${processes.some((line) => /\stelegram(?:\s|$)/.test(line)) ? "running" : "not running"}`, `telegram process: ${processes.some((line) => /\stelegram(?:\s|$)/.test(line)) ? "起動中" : "未起動"}`));
|
|
2695
|
+
if (processes.length > 0) {
|
|
2696
|
+
for (const line of processes) {
|
|
2697
|
+
console.log(` ${line}`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (enabled.length > 0 && !processes.some((line) => isSchedulerCommandLine(line)) && provider.supported) {
|
|
2701
|
+
console.log(l("next: agent-sin service install", "次: agent-sin service install"));
|
|
2702
|
+
}
|
|
2703
|
+
return 0;
|
|
2704
|
+
}
|
|
2705
|
+
async function cmdExport(args) {
|
|
2706
|
+
const options = parseOptions(args);
|
|
2707
|
+
if (options.help) {
|
|
2708
|
+
printExportHelp();
|
|
2709
|
+
return 0;
|
|
2710
|
+
}
|
|
2711
|
+
const workspace = stringOption(options.workspace) || defaultWorkspace();
|
|
2712
|
+
if (!(await pathExists(workspace))) {
|
|
2713
|
+
console.error(l(`Workspace not found: ${workspace}`, `ワークスペースが見つかりません: ${workspace}`));
|
|
2714
|
+
console.error(l("Run `agent-sin setup` first.", "先に `agent-sin setup` を実行してください。"));
|
|
2715
|
+
return 1;
|
|
2716
|
+
}
|
|
2717
|
+
const result = await exportWorkspace({
|
|
2718
|
+
workspace,
|
|
2719
|
+
outFile: stringOption(options.out),
|
|
2720
|
+
includeSecrets: Boolean(options.include_secrets),
|
|
2721
|
+
includeLogs: Boolean(options.include_logs),
|
|
2722
|
+
includeIndex: Boolean(options.include_index),
|
|
2723
|
+
});
|
|
2724
|
+
if (result.includedItems.includes(".env")) {
|
|
2725
|
+
console.log(l("! Includes .env. Treat this archive as containing API keys.", "⚠ .env を含みます。APIキー入りアーカイブとして扱ってください。"));
|
|
2726
|
+
}
|
|
2727
|
+
console.log(`Archive: ${result.archivePath}`);
|
|
2728
|
+
console.log(`Size: ${formatBytes(result.byteSize)}`);
|
|
2729
|
+
console.log(`Items: ${result.includedItems.join(", ")}`);
|
|
2730
|
+
for (const warning of result.warnings) {
|
|
2731
|
+
console.log(l(`note: ${warning}`, `注: ${warning}`));
|
|
2732
|
+
}
|
|
2733
|
+
console.log("");
|
|
2734
|
+
console.log(l("Restore on another computer with:", "移行先のPCで以下を実行して復元できます:"));
|
|
2735
|
+
console.log(` agent-sin import ${result.archivePath}`);
|
|
2736
|
+
return 0;
|
|
2737
|
+
}
|
|
2738
|
+
function printExportHelp() {
|
|
2739
|
+
console.log(l(`Agent-Sin export
|
|
2740
|
+
|
|
2741
|
+
Usage:
|
|
2742
|
+
agent-sin export [--out <file>]
|
|
2743
|
+
|
|
2744
|
+
Defaults to a migration backup with .env, logs, index, skills, memory, notes, schedules.yaml, Discord / Telegram state, config.toml, and models.yaml.
|
|
2745
|
+
Excludes only regenerated or bulky runtime folders such as .venv, node_modules, and .DS_Store.
|
|
2746
|
+
Outputs to ./agent-sin-backup-YYYYMMDD-HHmmss.tar.gz when --out is omitted.`, `Agent-Sin export
|
|
2747
|
+
|
|
2748
|
+
使い方:
|
|
2749
|
+
agent-sin export [--out <file>]
|
|
2750
|
+
|
|
2751
|
+
.env、logs、index、skills、memory、notes、schedules.yaml、Discord / Telegram 状態、config.toml、models.yaml を含む移行バックアップを作成します。
|
|
2752
|
+
.venv、node_modules、.DS_Store など再生成可能または大きい実行時フォルダだけ除外します。
|
|
2753
|
+
--out を省略すると ./agent-sin-backup-YYYYMMDD-HHmmss.tar.gz に出力します。`));
|
|
2754
|
+
}
|
|
2755
|
+
async function cmdImport(args) {
|
|
2756
|
+
const options = parseOptions(args);
|
|
2757
|
+
if (options.help) {
|
|
2758
|
+
printImportHelp();
|
|
2759
|
+
return 0;
|
|
2760
|
+
}
|
|
2761
|
+
const archive = options._[0];
|
|
2762
|
+
if (!archive) {
|
|
2763
|
+
console.error(l("Usage: agent-sin import <archive> [--force] [--dry-run] [--no-backup]", "使い方: agent-sin import <archive> [--force] [--dry-run] [--no-backup]"));
|
|
2764
|
+
return 1;
|
|
2765
|
+
}
|
|
2766
|
+
const dryRun = Boolean(options.dry_run);
|
|
2767
|
+
const force = Boolean(options.force);
|
|
2768
|
+
const backup = !options.no_backup;
|
|
2769
|
+
const workspace = stringOption(options.workspace) || defaultWorkspace();
|
|
2770
|
+
if (dryRun) {
|
|
2771
|
+
const entries = await listArchiveEntries(archive);
|
|
2772
|
+
console.log(`Archive: ${path.resolve(archive)}`);
|
|
2773
|
+
console.log(`Workspace (target): ${workspace}`);
|
|
2774
|
+
console.log(`Entries: ${entries.length}`);
|
|
2775
|
+
for (const entry of entries) {
|
|
2776
|
+
console.log(` ${entry}`);
|
|
2777
|
+
}
|
|
2778
|
+
return 0;
|
|
2779
|
+
}
|
|
2780
|
+
if (await pathExists(workspace)) {
|
|
2781
|
+
if (!force && process.stdin.isTTY) {
|
|
2782
|
+
const rl = createInterface({ input, output });
|
|
2783
|
+
try {
|
|
2784
|
+
const message = backup
|
|
2785
|
+
? l(`Back up and overwrite existing workspace ${workspace}. Continue? [y/N]: `, `既存ワークスペース ${workspace} を退避してから上書きします。続行しますか? [y/N]: `)
|
|
2786
|
+
: l(`Overwrite existing workspace ${workspace} without backup. Continue? [y/N]: `, `既存ワークスペース ${workspace} を上書きします (バックアップなし)。続行しますか? [y/N]: `);
|
|
2787
|
+
const answer = (await rl.question(message)).trim().toLowerCase();
|
|
2788
|
+
if (answer !== "y" && answer !== "yes") {
|
|
2789
|
+
console.log(l("Canceled.", "中止しました。"));
|
|
2790
|
+
return 1;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
finally {
|
|
2794
|
+
rl.close();
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
const result = await importWorkspace({
|
|
2799
|
+
archivePath: archive,
|
|
2800
|
+
workspace,
|
|
2801
|
+
backup,
|
|
2802
|
+
});
|
|
2803
|
+
console.log(`Workspace: ${result.workspace}`);
|
|
2804
|
+
console.log(`Restored entries: ${result.entries.length}`);
|
|
2805
|
+
if (result.backupPath) {
|
|
2806
|
+
console.log(`Backup: ${result.backupPath}`);
|
|
2807
|
+
}
|
|
2808
|
+
console.log("");
|
|
2809
|
+
console.log(l("Next: agent-sin start", "次: agent-sin start"));
|
|
2810
|
+
return 0;
|
|
2811
|
+
}
|
|
2812
|
+
function printImportHelp() {
|
|
2813
|
+
console.log(l(`Agent-Sin import
|
|
2814
|
+
|
|
2815
|
+
Usage:
|
|
2816
|
+
agent-sin import <archive.tar.gz> [--force] [--dry-run] [--no-backup]
|
|
2817
|
+
|
|
2818
|
+
--force Skip the overwrite confirmation prompt.
|
|
2819
|
+
--dry-run List archive entries without extracting.
|
|
2820
|
+
--no-backup Skip renaming the existing workspace to a .bak directory.`, `Agent-Sin import
|
|
2821
|
+
|
|
2822
|
+
使い方:
|
|
2823
|
+
agent-sin import <archive.tar.gz> [--force] [--dry-run] [--no-backup]
|
|
2824
|
+
|
|
2825
|
+
--force 上書き確認を省略します。
|
|
2826
|
+
--dry-run 展開せずアーカイブの中身だけ表示します。
|
|
2827
|
+
--no-backup 既存ワークスペースを .bak に退避しません。`));
|
|
2828
|
+
}
|
|
2829
|
+
main()
|
|
2830
|
+
.then((code) => {
|
|
2831
|
+
process.exitCode = code;
|
|
2832
|
+
})
|
|
2833
|
+
.finally(async () => {
|
|
2834
|
+
await shutdownSharedCodexAppServer().catch(() => undefined);
|
|
2835
|
+
});
|