agent-sin 0.1.12 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +126 -72
- package/builtin-skills/memo-list/skill.yaml +8 -14
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +9 -3
- package/dist/core/chat-engine.js +1263 -146
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +596 -18
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +1 -0
- package/dist/discord/bot.js +181 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +115 -7
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { access, chmod, cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
type Locale = "en" | "ja";
|
|
9
|
+
|
|
10
|
+
type SkillInput = {
|
|
11
|
+
args?: Record<string, unknown>;
|
|
12
|
+
sources?: {
|
|
13
|
+
workspace?: string;
|
|
14
|
+
locale?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type SkillResult = {
|
|
19
|
+
status: "ok" | "error" | "skipped";
|
|
20
|
+
title: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
outputs: Record<string, unknown>;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
suggestions: unknown[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type HistoryChannel = "discord" | "telegram" | "none" | "auto";
|
|
28
|
+
|
|
29
|
+
type ResolvedInputs = {
|
|
30
|
+
locale: Locale;
|
|
31
|
+
workspace: string;
|
|
32
|
+
envPath: string;
|
|
33
|
+
templateSrc: string;
|
|
34
|
+
templateDst: string;
|
|
35
|
+
packageId: string;
|
|
36
|
+
historyChannel: HistoryChannel;
|
|
37
|
+
discordThread: string | null;
|
|
38
|
+
host: string;
|
|
39
|
+
port: number;
|
|
40
|
+
token: string;
|
|
41
|
+
lanIp: string | null;
|
|
42
|
+
tailscaleHost: string | null;
|
|
43
|
+
serverUrl: string | null;
|
|
44
|
+
extraHosts: string[];
|
|
45
|
+
skipInstall: boolean;
|
|
46
|
+
skipPack: boolean;
|
|
47
|
+
force: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const GATEWAY_PORT_DEFAULT = 8765;
|
|
51
|
+
const VITE_DEV_PORT = 5173;
|
|
52
|
+
|
|
53
|
+
export async function run(_ctx: unknown, input: SkillInput): Promise<SkillResult> {
|
|
54
|
+
const locale: Locale = input.sources?.locale === "ja" ? "ja" : "en";
|
|
55
|
+
const args = (input.args || {}) as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
let inputs: ResolvedInputs;
|
|
58
|
+
try {
|
|
59
|
+
inputs = resolveInputs(args, input.sources?.workspace, locale);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return errorResult(locale, "Setup failed", "セットアップに失敗しました", error, {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const logs: string[] = [];
|
|
65
|
+
const warnings: string[] = [];
|
|
66
|
+
|
|
67
|
+
const tailscaleHostnames = collectTailscaleHostnames(inputs);
|
|
68
|
+
if (tailscaleHostnames.length > 0) {
|
|
69
|
+
const tailscaleCheck = checkTailscale(tailscaleHostnames);
|
|
70
|
+
if (!tailscaleCheck.ok) {
|
|
71
|
+
return errorResult(locale, "Tailscale not ready", "Tailscale が使えません", new Error(tailscaleCheck.message), {
|
|
72
|
+
hostnames: tailscaleHostnames,
|
|
73
|
+
hint: tailscaleCheck.hint,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
logs.push(
|
|
77
|
+
locale === "ja"
|
|
78
|
+
? `Tailscale OK(自分のホスト: ${tailscaleCheck.selfHost})`
|
|
79
|
+
: `Tailscale OK (self: ${tailscaleCheck.selfHost})`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (inputs.serverUrl && /^https:/i.test(inputs.serverUrl)) {
|
|
83
|
+
const serveCheck = checkTailscaleServe(inputs.serverUrl, inputs.port);
|
|
84
|
+
if (!serveCheck.ok) {
|
|
85
|
+
return errorResult(locale, "Tailscale Serve not configured", "Tailscale Serve 未設定", new Error(serveCheck.message), {
|
|
86
|
+
server_url: inputs.serverUrl,
|
|
87
|
+
gateway_port: inputs.port,
|
|
88
|
+
hint: serveCheck.hint,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
logs.push(
|
|
92
|
+
locale === "ja"
|
|
93
|
+
? `Tailscale Serve OK(${serveCheck.matched})`
|
|
94
|
+
: `Tailscale Serve OK (${serveCheck.matched})`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await mkdir(inputs.templateDst, { recursive: true });
|
|
101
|
+
const copyReport = await copyTemplate(inputs.templateSrc, inputs.templateDst, {
|
|
102
|
+
force: inputs.force,
|
|
103
|
+
});
|
|
104
|
+
logs.push(
|
|
105
|
+
locale === "ja"
|
|
106
|
+
? `テンプレを ${inputs.templateDst} に配置(コピー: ${copyReport.copied}, スキップ: ${copyReport.skipped})`
|
|
107
|
+
: `Prepared template at ${inputs.templateDst} (copied: ${copyReport.copied}, skipped: ${copyReport.skipped})`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const extrasWithServer = inputs.serverUrl
|
|
111
|
+
? [...inputs.extraHosts, inputs.serverUrl]
|
|
112
|
+
: inputs.extraHosts;
|
|
113
|
+
const whitelist = buildWhitelist({
|
|
114
|
+
lanIp: inputs.lanIp,
|
|
115
|
+
tailscaleHost: inputs.tailscaleHost,
|
|
116
|
+
extraHosts: extrasWithServer,
|
|
117
|
+
gatewayPort: inputs.port,
|
|
118
|
+
viteDevPort: VITE_DEV_PORT,
|
|
119
|
+
});
|
|
120
|
+
await writeAppJson(path.join(inputs.templateDst, "app.json"), inputs.templateSrc, whitelist, inputs.packageId);
|
|
121
|
+
logs.push(
|
|
122
|
+
locale === "ja"
|
|
123
|
+
? `app.json を更新(package_id=${inputs.packageId}, whitelist ${whitelist.length} 件)`
|
|
124
|
+
: `Updated app.json (package_id=${inputs.packageId}, whitelist ${whitelist.length} entries)`,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const embeddedServer = pickEmbeddedServer(inputs);
|
|
128
|
+
await writeEmbeddedConfig(path.join(inputs.templateDst, "src", "embedded-config.ts"), embeddedServer, inputs.token);
|
|
129
|
+
logs.push(
|
|
130
|
+
locale === "ja"
|
|
131
|
+
? `embedded-config.ts に server=${embeddedServer || "(なし)"} と token を埋め込み`
|
|
132
|
+
: `Embedded server=${embeddedServer || "(none)"} and token into embedded-config.ts`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const dotenvEntries: Array<{ key: string; value: string }> = [
|
|
136
|
+
{ key: "AGENT_SIN_G2_ENABLED", value: "1" },
|
|
137
|
+
{ key: "AGENT_SIN_G2_HOST", value: inputs.host },
|
|
138
|
+
{ key: "AGENT_SIN_G2_PORT", value: String(inputs.port) },
|
|
139
|
+
{ key: "AGENT_SIN_G2_TOKEN", value: inputs.token },
|
|
140
|
+
{ key: "AGENT_SIN_G2_HISTORY_CHANNEL", value: inputs.historyChannel },
|
|
141
|
+
];
|
|
142
|
+
if (inputs.discordThread !== null) {
|
|
143
|
+
dotenvEntries.push({ key: "AGENT_SIN_G2_DISCORD_THREAD", value: inputs.discordThread });
|
|
144
|
+
}
|
|
145
|
+
await upsertDotenv(inputs.envPath, dotenvEntries);
|
|
146
|
+
logs.push(locale === "ja" ? `${inputs.envPath} に G2 設定を保存` : `Wrote G2 env to ${inputs.envPath}`);
|
|
147
|
+
|
|
148
|
+
let ehpkPath: string | null = null;
|
|
149
|
+
let partial = false;
|
|
150
|
+
|
|
151
|
+
const npmAvailable = await commandExists("npm");
|
|
152
|
+
if (!npmAvailable) {
|
|
153
|
+
partial = true;
|
|
154
|
+
warnings.push(
|
|
155
|
+
locale === "ja"
|
|
156
|
+
? "npm が見つからないため install / pack をスキップしました。Node.js 22+ を入れ直してから再実行してください"
|
|
157
|
+
: "npm is not available; skipped install/pack. Install Node.js 22+ and re-run this skill.",
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
const nodeModulesExists = existsSync(path.join(inputs.templateDst, "node_modules"));
|
|
161
|
+
if (!inputs.skipInstall && (!nodeModulesExists || inputs.force)) {
|
|
162
|
+
const installResult = await runNpm(inputs.templateDst, ["install"]);
|
|
163
|
+
if (installResult.code !== 0) {
|
|
164
|
+
return errorResult(
|
|
165
|
+
locale,
|
|
166
|
+
"npm install failed",
|
|
167
|
+
"npm install に失敗しました",
|
|
168
|
+
new Error(installResult.stderr.trim() || installResult.stdout.trim() || `exit ${installResult.code}`),
|
|
169
|
+
{ dotenv_path: inputs.envPath, template_dir: inputs.templateDst, install_log: tail(installResult.stderr, 2000) },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
logs.push(locale === "ja" ? "npm install 完了" : "npm install completed");
|
|
173
|
+
} else {
|
|
174
|
+
logs.push(locale === "ja" ? "npm install はスキップ" : "Skipped npm install");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!inputs.skipPack) {
|
|
178
|
+
const packResult = await runNpm(inputs.templateDst, ["run", "pack"]);
|
|
179
|
+
const candidatePath = path.join(inputs.templateDst, "agent-sin-g2.ehpk");
|
|
180
|
+
if (packResult.code !== 0 || !existsSync(candidatePath)) {
|
|
181
|
+
const detail = packResult.stderr.trim() || packResult.stdout.trim() || `exit ${packResult.code}`;
|
|
182
|
+
return errorResult(
|
|
183
|
+
locale,
|
|
184
|
+
"npm run pack failed",
|
|
185
|
+
"npm run pack に失敗しました",
|
|
186
|
+
new Error(detail),
|
|
187
|
+
{
|
|
188
|
+
dotenv_path: inputs.envPath,
|
|
189
|
+
template_dir: inputs.templateDst,
|
|
190
|
+
pack_stdout: tail(packResult.stdout, 2000),
|
|
191
|
+
pack_stderr: tail(packResult.stderr, 2000),
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
ehpkPath = candidatePath;
|
|
196
|
+
logs.push(locale === "ja" ? `.ehpk を ${ehpkPath} に出力` : `Built ${ehpkPath}`);
|
|
197
|
+
} else {
|
|
198
|
+
logs.push(locale === "ja" ? "npm run pack はスキップ" : "Skipped npm run pack");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return okResult(locale, {
|
|
203
|
+
envPath: inputs.envPath,
|
|
204
|
+
templateDir: inputs.templateDst,
|
|
205
|
+
ehpkPath,
|
|
206
|
+
whitelist,
|
|
207
|
+
lanIp: inputs.lanIp,
|
|
208
|
+
historyChannel: inputs.historyChannel,
|
|
209
|
+
packageId: inputs.packageId,
|
|
210
|
+
partial,
|
|
211
|
+
warnings,
|
|
212
|
+
logs,
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return errorResult(locale, "Setup failed", "セットアップに失敗しました", error, {
|
|
216
|
+
template_dir: inputs.templateDst,
|
|
217
|
+
env_path: inputs.envPath,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveInputs(args: Record<string, unknown>, workspaceHint: string | undefined, locale: Locale): ResolvedInputs {
|
|
223
|
+
const workspace = workspaceHint || process.env.AGENT_SIN_HOME || path.join(os.homedir(), ".agent-sin");
|
|
224
|
+
const envPath = path.join(workspace, ".env");
|
|
225
|
+
const templateDst = path.join(workspace, "even-g2-agent");
|
|
226
|
+
const templateSrc = resolvePackageRoot();
|
|
227
|
+
const packageId = resolvePackageId(args.package_id, path.join(templateDst, "app.json"));
|
|
228
|
+
|
|
229
|
+
const historyChannel = normalizeHistoryChannel(args.history_channel) ?? "none";
|
|
230
|
+
const discordThread = normalizeDiscordThread(args.discord_thread);
|
|
231
|
+
|
|
232
|
+
const host = typeof args.host === "string" && args.host.trim() ? args.host.trim() : "0.0.0.0";
|
|
233
|
+
const port = typeof args.port === "number" && Number.isInteger(args.port) ? args.port : GATEWAY_PORT_DEFAULT;
|
|
234
|
+
const tokenInput = typeof args.token === "string" && args.token.trim().length >= 8 ? args.token.trim() : null;
|
|
235
|
+
const existingToken = process.env.AGENT_SIN_G2_TOKEN && process.env.AGENT_SIN_G2_TOKEN.trim();
|
|
236
|
+
const token = tokenInput || existingToken || randomBytes(16).toString("hex");
|
|
237
|
+
|
|
238
|
+
const lanIp = typeof args.host_lan_ip === "string" && args.host_lan_ip.trim()
|
|
239
|
+
? args.host_lan_ip.trim()
|
|
240
|
+
: detectLanIp();
|
|
241
|
+
const tailscaleHost = typeof args.tailscale_host === "string" && args.tailscale_host.trim()
|
|
242
|
+
? args.tailscale_host.trim()
|
|
243
|
+
: null;
|
|
244
|
+
const serverUrl = typeof args.server_url === "string" && args.server_url.trim()
|
|
245
|
+
? args.server_url.trim().replace(/\/$/, "")
|
|
246
|
+
: null;
|
|
247
|
+
if (serverUrl && !/^https?:\/\//i.test(serverUrl)) {
|
|
248
|
+
throw new Error(`server_url must start with http:// or https:// (got: ${serverUrl})`);
|
|
249
|
+
}
|
|
250
|
+
const extraHosts = Array.isArray(args.extra_hosts)
|
|
251
|
+
? (args.extra_hosts.filter((value): value is string => typeof value === "string" && value.trim().length > 0))
|
|
252
|
+
: [];
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
locale,
|
|
256
|
+
workspace,
|
|
257
|
+
envPath,
|
|
258
|
+
templateSrc: path.join(templateSrc, "templates", "even-g2-agent"),
|
|
259
|
+
templateDst,
|
|
260
|
+
packageId,
|
|
261
|
+
historyChannel,
|
|
262
|
+
discordThread,
|
|
263
|
+
host,
|
|
264
|
+
port,
|
|
265
|
+
token,
|
|
266
|
+
lanIp,
|
|
267
|
+
tailscaleHost,
|
|
268
|
+
serverUrl,
|
|
269
|
+
extraHosts,
|
|
270
|
+
skipInstall: Boolean(args.skip_install),
|
|
271
|
+
skipPack: Boolean(args.skip_pack),
|
|
272
|
+
force: Boolean(args.force),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const PACKAGE_ID_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/;
|
|
277
|
+
const FALLBACK_PACKAGE_ID = "com.example.agentsin";
|
|
278
|
+
|
|
279
|
+
export function isValidPackageId(value: string): boolean {
|
|
280
|
+
return PACKAGE_ID_PATTERN.test(value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function sanitizeIdentifierSegment(value: string): string {
|
|
284
|
+
const stripped = value
|
|
285
|
+
.normalize("NFKD")
|
|
286
|
+
.replace(/[̀-ͯ]/g, "")
|
|
287
|
+
.toLowerCase()
|
|
288
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
289
|
+
return stripped.replace(/^[^a-z]+/, "");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function deriveFromGitUserName(): string | null {
|
|
293
|
+
try {
|
|
294
|
+
const result = spawnSync("git", ["config", "--global", "user.name"], { encoding: "utf8" });
|
|
295
|
+
if (result.status !== 0) return null;
|
|
296
|
+
const segment = sanitizeIdentifierSegment(result.stdout.trim());
|
|
297
|
+
if (!segment) return null;
|
|
298
|
+
return `com.${segment}.agentsin`;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readExistingPackageId(appJsonPath: string): string | null {
|
|
305
|
+
try {
|
|
306
|
+
if (!existsSync(appJsonPath)) return null;
|
|
307
|
+
const text = readFileSync(appJsonPath, "utf8");
|
|
308
|
+
const parsed = JSON.parse(text);
|
|
309
|
+
const value = typeof parsed?.package_id === "string" ? parsed.package_id.trim() : "";
|
|
310
|
+
return value && value !== FALLBACK_PACKAGE_ID ? value : null;
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function resolvePackageId(arg: unknown, existingAppJsonPath: string): string {
|
|
317
|
+
if (typeof arg === "string" && arg.trim()) {
|
|
318
|
+
const explicit = arg.trim();
|
|
319
|
+
if (!isValidPackageId(explicit)) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Invalid package_id: "${explicit}". Use lowercase reverse-DNS (e.g. com.you.agentsin).`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return explicit;
|
|
325
|
+
}
|
|
326
|
+
const existing = readExistingPackageId(existingAppJsonPath);
|
|
327
|
+
if (existing && isValidPackageId(existing)) return existing;
|
|
328
|
+
const derived = deriveFromGitUserName();
|
|
329
|
+
if (derived && isValidPackageId(derived)) return derived;
|
|
330
|
+
return FALLBACK_PACKAGE_ID;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizeHistoryChannel(value: unknown): HistoryChannel | null {
|
|
334
|
+
if (typeof value !== "string") return null;
|
|
335
|
+
const normalized = value.trim().toLowerCase();
|
|
336
|
+
if (normalized === "discord" || normalized === "telegram" || normalized === "none" || normalized === "auto") {
|
|
337
|
+
return normalized;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeDiscordThread(value: unknown): string | null {
|
|
343
|
+
if (typeof value !== "string") return null;
|
|
344
|
+
const trimmed = value.trim();
|
|
345
|
+
if (!trimmed) return null;
|
|
346
|
+
if (/^(auto|create|1|true|yes)$/i.test(trimmed)) return "auto";
|
|
347
|
+
if (/^(none|off|false|0)$/i.test(trimmed)) return "off";
|
|
348
|
+
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function resolvePackageRoot(): string {
|
|
353
|
+
const candidates: string[] = [];
|
|
354
|
+
const addAncestry = (start: string) => {
|
|
355
|
+
let current = path.resolve(start);
|
|
356
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
357
|
+
candidates.push(current);
|
|
358
|
+
const parent = path.dirname(current);
|
|
359
|
+
if (parent === current) break;
|
|
360
|
+
current = parent;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
if (process.env.AGENT_SIN_PACKAGE_ROOT) candidates.push(process.env.AGENT_SIN_PACKAGE_ROOT);
|
|
364
|
+
const argv1 = process.argv[1];
|
|
365
|
+
if (argv1) {
|
|
366
|
+
addAncestry(path.dirname(argv1));
|
|
367
|
+
try {
|
|
368
|
+
const resolved = realpathSync(argv1);
|
|
369
|
+
addAncestry(path.dirname(resolved));
|
|
370
|
+
} catch {
|
|
371
|
+
// argv1 may not exist when running from REPL — fine
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
candidates.push(process.cwd());
|
|
375
|
+
for (const root of candidates) {
|
|
376
|
+
if (!root) continue;
|
|
377
|
+
if (existsSync(path.join(root, "templates", "even-g2-agent", "app.json"))) {
|
|
378
|
+
return root;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
throw new Error(
|
|
382
|
+
"Could not locate the agent-sin package root with templates/even-g2-agent/. Set AGENT_SIN_PACKAGE_ROOT to override.",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function detectLanIp(): string | null {
|
|
387
|
+
const interfaces = os.networkInterfaces();
|
|
388
|
+
for (const list of Object.values(interfaces)) {
|
|
389
|
+
if (!list) continue;
|
|
390
|
+
for (const info of list) {
|
|
391
|
+
if (info.family === "IPv4" && !info.internal) {
|
|
392
|
+
return info.address;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function buildWhitelist(input: {
|
|
400
|
+
lanIp: string | null;
|
|
401
|
+
tailscaleHost: string | null;
|
|
402
|
+
extraHosts: string[];
|
|
403
|
+
gatewayPort: number;
|
|
404
|
+
viteDevPort: number;
|
|
405
|
+
}): string[] {
|
|
406
|
+
const entries: string[] = [];
|
|
407
|
+
entries.push(`http://127.0.0.1:${input.gatewayPort}`);
|
|
408
|
+
entries.push(`http://127.0.0.1:${input.viteDevPort}`);
|
|
409
|
+
if (input.lanIp) {
|
|
410
|
+
entries.push(`http://${input.lanIp}:${input.gatewayPort}`);
|
|
411
|
+
entries.push(`http://${input.lanIp}:${input.viteDevPort}`);
|
|
412
|
+
}
|
|
413
|
+
if (input.tailscaleHost) {
|
|
414
|
+
entries.push(`http://${input.tailscaleHost}:${input.gatewayPort}`);
|
|
415
|
+
}
|
|
416
|
+
for (const raw of input.extraHosts) {
|
|
417
|
+
entries.push(normalizeWhitelistEntry(raw, input.gatewayPort));
|
|
418
|
+
}
|
|
419
|
+
const seen = new Set<string>();
|
|
420
|
+
const result: string[] = [];
|
|
421
|
+
for (const entry of entries) {
|
|
422
|
+
const key = normalizeUrl(entry);
|
|
423
|
+
if (seen.has(key)) continue;
|
|
424
|
+
seen.add(key);
|
|
425
|
+
result.push(key);
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function normalizeWhitelistEntry(value: string, defaultPort: number): string {
|
|
431
|
+
const raw = value.trim();
|
|
432
|
+
if (!raw) return raw;
|
|
433
|
+
const hadScheme = /^https?:\/\//i.test(raw);
|
|
434
|
+
const withScheme = hadScheme ? raw : `http://${raw}`;
|
|
435
|
+
try {
|
|
436
|
+
const parsed = new URL(withScheme);
|
|
437
|
+
if (!hadScheme && !parsed.port) {
|
|
438
|
+
parsed.port = String(defaultPort);
|
|
439
|
+
}
|
|
440
|
+
return normalizeUrl(parsed.toString());
|
|
441
|
+
} catch {
|
|
442
|
+
return normalizeUrl(withScheme);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function normalizeUrl(value: string): string {
|
|
447
|
+
try {
|
|
448
|
+
const url = new URL(value);
|
|
449
|
+
url.hostname = url.hostname.toLowerCase();
|
|
450
|
+
let out = url.toString();
|
|
451
|
+
if (out.endsWith("/") && !value.endsWith("/")) {
|
|
452
|
+
out = out.slice(0, -1);
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
} catch {
|
|
456
|
+
return value.trim();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function collectTailscaleHostnames(inputs: ResolvedInputs): string[] {
|
|
461
|
+
const hostnames: string[] = [];
|
|
462
|
+
if (inputs.tailscaleHost) hostnames.push(inputs.tailscaleHost);
|
|
463
|
+
if (inputs.serverUrl) {
|
|
464
|
+
try {
|
|
465
|
+
const { hostname } = new URL(inputs.serverUrl);
|
|
466
|
+
if (/\.ts\.net$/i.test(hostname)) hostnames.push(hostname);
|
|
467
|
+
} catch {
|
|
468
|
+
// ignore
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return Array.from(new Set(hostnames.map((host) => host.toLowerCase())));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function checkTailscale(requestedHostnames: string[]): { ok: true; selfHost: string } | { ok: false; message: string; hint: string } {
|
|
475
|
+
const versionProbe = spawnSync("tailscale", ["--version"], { encoding: "utf8" });
|
|
476
|
+
if (versionProbe.error || versionProbe.status !== 0) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
message: "tailscale CLI was not found. Install Tailscale (https://tailscale.com/download) and run `tailscale up` before re-running this skill.",
|
|
480
|
+
hint: "brew install --cask tailscale",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const statusProbe = spawnSync("tailscale", ["status", "--json"], { encoding: "utf8" });
|
|
484
|
+
if (statusProbe.error || statusProbe.status !== 0) {
|
|
485
|
+
return {
|
|
486
|
+
ok: false,
|
|
487
|
+
message: "tailscale status failed. Run `tailscale up` and sign in, then re-run this skill.",
|
|
488
|
+
hint: "tailscale up",
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
let parsed: any;
|
|
492
|
+
try {
|
|
493
|
+
parsed = JSON.parse(statusProbe.stdout);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
return {
|
|
496
|
+
ok: false,
|
|
497
|
+
message: `Could not parse tailscale status: ${errorMessage(error)}`,
|
|
498
|
+
hint: "tailscale status --json",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (parsed?.BackendState !== "Running") {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
message: `Tailscale is not running (state: ${parsed?.BackendState || "unknown"}). Run \`tailscale up\` and try again.`,
|
|
505
|
+
hint: "tailscale up",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const selfHost = String(parsed?.Self?.DNSName || "").replace(/\.$/, "").toLowerCase();
|
|
509
|
+
if (!selfHost) {
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
message: "Could not determine this machine's Tailscale MagicDNS hostname.",
|
|
513
|
+
hint: "tailscale status --self",
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
for (const requested of requestedHostnames) {
|
|
517
|
+
if (requested.toLowerCase() !== selfHost) {
|
|
518
|
+
return {
|
|
519
|
+
ok: false,
|
|
520
|
+
message: `Tailscale hostname mismatch: requested "${requested}" but this machine is "${selfHost}". Use the matching host or update your input.`,
|
|
521
|
+
hint: `--tailscale-host ${selfHost}`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return { ok: true, selfHost };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function checkTailscaleServe(serverUrl: string, gatewayPort: number): { ok: true; matched: string } | { ok: false; message: string; hint: string } {
|
|
529
|
+
const probe = spawnSync("tailscale", ["serve", "status", "--json"], { encoding: "utf8" });
|
|
530
|
+
if (probe.error || probe.status !== 0) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
message: "Could not query Tailscale Serve status. Set it up so the gateway is reachable, then retry.",
|
|
534
|
+
hint: `tailscale serve --bg --https=443 / proxy http://localhost:${gatewayPort}`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
let parsed: any;
|
|
538
|
+
try {
|
|
539
|
+
parsed = JSON.parse(probe.stdout);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
message: `Could not parse tailscale serve status: ${errorMessage(error)}`,
|
|
544
|
+
hint: `tailscale serve --bg --https=443 / proxy http://localhost:${gatewayPort}`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
let url: URL;
|
|
548
|
+
try {
|
|
549
|
+
url = new URL(serverUrl);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
message: `Invalid server_url: ${errorMessage(error)}`,
|
|
554
|
+
hint: "Pass --server-url https://<your-tailnet>.ts.net",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
const host = url.hostname.toLowerCase();
|
|
558
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
559
|
+
const targetPath = url.pathname && url.pathname !== "/" ? url.pathname : "/";
|
|
560
|
+
const web = parsed?.Web || {};
|
|
561
|
+
const entry = web[`${host}:${port}`];
|
|
562
|
+
if (!entry) {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
message: `Tailscale Serve has no handler for ${host}:${port}. Set one up so the gateway is reachable.`,
|
|
566
|
+
hint: `tailscale serve --bg --https=${port} / proxy http://localhost:${gatewayPort}`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const handlers = entry.Handlers || {};
|
|
570
|
+
const matched = Object.entries(handlers).find(([handlerPath, conf]) => {
|
|
571
|
+
if (handlerPath !== targetPath) return false;
|
|
572
|
+
const proxy = String((conf as any)?.Proxy || "");
|
|
573
|
+
return proxy.includes(`localhost:${gatewayPort}`) || proxy.includes(`127.0.0.1:${gatewayPort}`);
|
|
574
|
+
});
|
|
575
|
+
if (!matched) {
|
|
576
|
+
const current = Object.entries(handlers).map(([path, conf]) => `${path} -> ${(conf as any)?.Proxy || "?"}`).join(", ") || "(none)";
|
|
577
|
+
return {
|
|
578
|
+
ok: false,
|
|
579
|
+
message: `Tailscale Serve does not proxy ${host}:${port}${targetPath} to localhost:${gatewayPort}. Current: ${current}.`,
|
|
580
|
+
hint: `tailscale serve --bg --https=${port} ${targetPath} proxy http://localhost:${gatewayPort}`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return { ok: true, matched: `${host}:${port}${targetPath} → localhost:${gatewayPort}` };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function pickEmbeddedServer(inputs: ResolvedInputs): string {
|
|
587
|
+
if (inputs.serverUrl) return inputs.serverUrl;
|
|
588
|
+
if (inputs.tailscaleHost) return `http://${inputs.tailscaleHost}:${inputs.port}`;
|
|
589
|
+
if (inputs.lanIp) return `http://${inputs.lanIp}:${inputs.port}`;
|
|
590
|
+
return "";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function writeEmbeddedConfig(targetPath: string, server: string, token: string): Promise<void> {
|
|
594
|
+
const escape = (value: string): string => value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
595
|
+
const content = [
|
|
596
|
+
"// Auto-generated by the even-g2-setup skill at build time.",
|
|
597
|
+
"// Do not commit values from this file when sharing the template.",
|
|
598
|
+
`export const EMBEDDED_SERVER = "${escape(server)}";`,
|
|
599
|
+
`export const EMBEDDED_TOKEN = "${escape(token)}";`,
|
|
600
|
+
"",
|
|
601
|
+
].join("\n");
|
|
602
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
603
|
+
await writeFile(targetPath, content, "utf8");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function writeAppJson(
|
|
607
|
+
targetPath: string,
|
|
608
|
+
templateDir: string,
|
|
609
|
+
whitelist: string[],
|
|
610
|
+
packageId: string,
|
|
611
|
+
): Promise<void> {
|
|
612
|
+
let baseJson: Record<string, unknown> | null = null;
|
|
613
|
+
try {
|
|
614
|
+
const existing = await readFile(targetPath, "utf8");
|
|
615
|
+
baseJson = JSON.parse(existing) as Record<string, unknown>;
|
|
616
|
+
} catch {
|
|
617
|
+
baseJson = null;
|
|
618
|
+
}
|
|
619
|
+
if (!baseJson) {
|
|
620
|
+
try {
|
|
621
|
+
const text = await readFile(path.join(templateDir, "app.json"), "utf8");
|
|
622
|
+
baseJson = JSON.parse(text) as Record<string, unknown>;
|
|
623
|
+
} catch (error) {
|
|
624
|
+
throw new Error(`Failed to read template app.json: ${errorMessage(error)}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
baseJson.package_id = packageId;
|
|
628
|
+
const permissions = Array.isArray(baseJson.permissions) ? [...(baseJson.permissions as unknown[])] : [];
|
|
629
|
+
if (permissions.length === 0) {
|
|
630
|
+
permissions.push({
|
|
631
|
+
name: "network",
|
|
632
|
+
desc: "Connects to the user's Agent-Sin gateway for private agent messages.",
|
|
633
|
+
whitelist: [],
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const first = { ...(permissions[0] as Record<string, unknown>) };
|
|
637
|
+
first.whitelist = whitelist;
|
|
638
|
+
permissions[0] = first;
|
|
639
|
+
baseJson.permissions = permissions;
|
|
640
|
+
await writeFile(targetPath, `${JSON.stringify(baseJson, null, 2)}\n`, "utf8");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function copyTemplate(
|
|
644
|
+
src: string,
|
|
645
|
+
dst: string,
|
|
646
|
+
options: { force: boolean },
|
|
647
|
+
): Promise<{ copied: number; skipped: number }> {
|
|
648
|
+
const counters = { copied: 0, skipped: 0 };
|
|
649
|
+
try {
|
|
650
|
+
await access(src);
|
|
651
|
+
} catch {
|
|
652
|
+
throw new Error(`Template source not found: ${src}`);
|
|
653
|
+
}
|
|
654
|
+
await cp(src, dst, {
|
|
655
|
+
recursive: true,
|
|
656
|
+
force: options.force,
|
|
657
|
+
errorOnExist: false,
|
|
658
|
+
dereference: false,
|
|
659
|
+
filter: (source) => {
|
|
660
|
+
const rel = path.relative(src, source);
|
|
661
|
+
if (!rel) return true;
|
|
662
|
+
const first = rel.split(path.sep)[0];
|
|
663
|
+
if (first === "node_modules" || first === "dist" || first === ".git") {
|
|
664
|
+
counters.skipped += 1;
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
if (rel.endsWith(".ehpk")) {
|
|
668
|
+
counters.skipped += 1;
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if (path.basename(rel) === "app.json") {
|
|
672
|
+
counters.skipped += 1;
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
counters.copied += 1;
|
|
676
|
+
return true;
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
return counters;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function formatDotenvValue(value: string): string {
|
|
683
|
+
if (value === "") return "";
|
|
684
|
+
if (/^[A-Za-z0-9_./:@~+\-]+$/.test(value)) return value;
|
|
685
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
686
|
+
return `"${escaped}"`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function upsertDotenvContent(existing: string, entries: Array<{ key: string; value: string }>): string {
|
|
690
|
+
const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
|
|
691
|
+
let trailingEmpty = false;
|
|
692
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
693
|
+
lines.pop();
|
|
694
|
+
trailingEmpty = true;
|
|
695
|
+
}
|
|
696
|
+
for (const entry of entries) {
|
|
697
|
+
const formatted = `${entry.key}=${formatDotenvValue(entry.value)}`;
|
|
698
|
+
const pattern = new RegExp(`^\\s*${entry.key.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*=.*$`);
|
|
699
|
+
const index = lines.findIndex((line) => pattern.test(line));
|
|
700
|
+
if (index >= 0) {
|
|
701
|
+
lines[index] = formatted;
|
|
702
|
+
} else {
|
|
703
|
+
lines.push(formatted);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return `${lines.join("\n")}${trailingEmpty || lines.length > 0 ? "\n" : ""}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function upsertDotenv(envPath: string, entries: Array<{ key: string; value: string }>): Promise<void> {
|
|
710
|
+
let existing = "";
|
|
711
|
+
try {
|
|
712
|
+
existing = await readFile(envPath, "utf8");
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
715
|
+
}
|
|
716
|
+
await mkdir(path.dirname(envPath), { recursive: true });
|
|
717
|
+
const next = upsertDotenvContent(existing, entries);
|
|
718
|
+
await writeFile(envPath, next, "utf8");
|
|
719
|
+
try {
|
|
720
|
+
await chmod(envPath, 0o600);
|
|
721
|
+
} catch {
|
|
722
|
+
// Permissions on Windows are best-effort; ignore.
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
727
|
+
return await new Promise((resolve) => {
|
|
728
|
+
const probe = spawn(command, ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
|
|
729
|
+
probe.once("error", () => resolve(false));
|
|
730
|
+
probe.once("exit", (code) => resolve(code === 0));
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function runNpm(cwd: string, args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
735
|
+
return await new Promise((resolve, reject) => {
|
|
736
|
+
const child = spawn("npm", args, {
|
|
737
|
+
cwd,
|
|
738
|
+
env: process.env,
|
|
739
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
740
|
+
shell: process.platform === "win32",
|
|
741
|
+
});
|
|
742
|
+
let stdout = "";
|
|
743
|
+
let stderr = "";
|
|
744
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
745
|
+
stdout += chunk.toString("utf8");
|
|
746
|
+
});
|
|
747
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
748
|
+
stderr += chunk.toString("utf8");
|
|
749
|
+
});
|
|
750
|
+
child.once("error", reject);
|
|
751
|
+
child.once("exit", (code) => {
|
|
752
|
+
resolve({ code: code ?? -1, stdout, stderr });
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function tail(text: string, limit: number): string {
|
|
758
|
+
if (text.length <= limit) return text;
|
|
759
|
+
return `…${text.slice(text.length - limit)}`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function errorMessage(error: unknown): string {
|
|
763
|
+
return error instanceof Error ? error.message : String(error);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function okResult(
|
|
767
|
+
locale: Locale,
|
|
768
|
+
data: {
|
|
769
|
+
envPath: string;
|
|
770
|
+
templateDir: string;
|
|
771
|
+
ehpkPath: string | null;
|
|
772
|
+
whitelist: string[];
|
|
773
|
+
lanIp: string | null;
|
|
774
|
+
historyChannel: HistoryChannel;
|
|
775
|
+
packageId: string;
|
|
776
|
+
partial: boolean;
|
|
777
|
+
warnings: string[];
|
|
778
|
+
logs: string[];
|
|
779
|
+
},
|
|
780
|
+
): SkillResult {
|
|
781
|
+
const summary = locale === "ja" ? buildSummaryJa(data) : buildSummaryEn(data);
|
|
782
|
+
return {
|
|
783
|
+
status: "ok",
|
|
784
|
+
title: locale === "ja" ? "Even G2 セットアップ完了" : "Even G2 setup completed",
|
|
785
|
+
summary,
|
|
786
|
+
outputs: {},
|
|
787
|
+
data: {
|
|
788
|
+
env_path: data.envPath,
|
|
789
|
+
template_dir: data.templateDir,
|
|
790
|
+
ehpk_path: data.ehpkPath,
|
|
791
|
+
whitelist: data.whitelist,
|
|
792
|
+
lan_ip: data.lanIp,
|
|
793
|
+
history_channel: data.historyChannel,
|
|
794
|
+
package_id: data.packageId,
|
|
795
|
+
partial: data.partial,
|
|
796
|
+
warnings: data.warnings,
|
|
797
|
+
logs: data.logs,
|
|
798
|
+
},
|
|
799
|
+
suggestions: [],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function errorResult(
|
|
804
|
+
locale: Locale,
|
|
805
|
+
titleEn: string,
|
|
806
|
+
titleJa: string,
|
|
807
|
+
error: unknown,
|
|
808
|
+
data: Record<string, unknown>,
|
|
809
|
+
): SkillResult {
|
|
810
|
+
return {
|
|
811
|
+
status: "error",
|
|
812
|
+
title: locale === "ja" ? titleJa : titleEn,
|
|
813
|
+
summary: locale === "ja"
|
|
814
|
+
? `セットアップ中にエラーが発生しました: ${errorMessage(error)}`
|
|
815
|
+
: `Setup failed: ${errorMessage(error)}`,
|
|
816
|
+
outputs: {},
|
|
817
|
+
data: { ...data, error: errorMessage(error) },
|
|
818
|
+
suggestions: [],
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function buildSummaryJa(data: {
|
|
823
|
+
envPath: string;
|
|
824
|
+
templateDir: string;
|
|
825
|
+
ehpkPath: string | null;
|
|
826
|
+
whitelist: string[];
|
|
827
|
+
packageId: string;
|
|
828
|
+
partial: boolean;
|
|
829
|
+
warnings: string[];
|
|
830
|
+
}): string {
|
|
831
|
+
const lines: string[] = [];
|
|
832
|
+
lines.push("Even G2 のセットアップが完了しました。");
|
|
833
|
+
lines.push("");
|
|
834
|
+
lines.push(`package_id: ${data.packageId}`);
|
|
835
|
+
lines.push(`.env: ${data.envPath}`);
|
|
836
|
+
lines.push(`テンプレ作業ディレクトリ: ${data.templateDir}`);
|
|
837
|
+
if (data.ehpkPath) {
|
|
838
|
+
lines.push(`.ehpk: ${data.ehpkPath}`);
|
|
839
|
+
}
|
|
840
|
+
lines.push("");
|
|
841
|
+
lines.push("次は人間操作:");
|
|
842
|
+
lines.push(" 1. https://hub.evenrealities.com/ にサインインして My projects → Upload package で .ehpk をアップロード");
|
|
843
|
+
lines.push(" 2. スマホの Even Realities アプリで Even Hub を開き、右上の Developer Hub(メガネアイコン)から Agent-Sin をインストール");
|
|
844
|
+
lines.push("");
|
|
845
|
+
lines.push("詳しい手順: docs/even-g2-setup.ja.md");
|
|
846
|
+
if (data.warnings.length > 0) {
|
|
847
|
+
lines.push("");
|
|
848
|
+
lines.push("注意:");
|
|
849
|
+
for (const warning of data.warnings) {
|
|
850
|
+
lines.push(` - ${warning}`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (data.partial) {
|
|
854
|
+
lines.push("");
|
|
855
|
+
lines.push("一部の処理がスキップされたため、.ehpk の生成は完了していません。");
|
|
856
|
+
}
|
|
857
|
+
return lines.join("\n");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function buildSummaryEn(data: {
|
|
861
|
+
envPath: string;
|
|
862
|
+
templateDir: string;
|
|
863
|
+
ehpkPath: string | null;
|
|
864
|
+
whitelist: string[];
|
|
865
|
+
packageId: string;
|
|
866
|
+
partial: boolean;
|
|
867
|
+
warnings: string[];
|
|
868
|
+
}): string {
|
|
869
|
+
const lines: string[] = [];
|
|
870
|
+
lines.push("Even G2 setup is complete.");
|
|
871
|
+
lines.push("");
|
|
872
|
+
lines.push(`package_id: ${data.packageId}`);
|
|
873
|
+
lines.push(`.env: ${data.envPath}`);
|
|
874
|
+
lines.push(`Template workspace: ${data.templateDir}`);
|
|
875
|
+
if (data.ehpkPath) {
|
|
876
|
+
lines.push(`.ehpk: ${data.ehpkPath}`);
|
|
877
|
+
}
|
|
878
|
+
lines.push("");
|
|
879
|
+
lines.push("Next, manual steps:");
|
|
880
|
+
lines.push(" 1. Sign in to https://hub.evenrealities.com/, open My projects, and upload the .ehpk via Upload package.");
|
|
881
|
+
lines.push(" 2. In the Even Realities phone app, open Even Hub, tap the Developer Hub (glasses icon) in the top-right, then install Agent-Sin.");
|
|
882
|
+
lines.push("");
|
|
883
|
+
lines.push("See docs/even-g2-setup.md for the full walkthrough.");
|
|
884
|
+
if (data.warnings.length > 0) {
|
|
885
|
+
lines.push("");
|
|
886
|
+
lines.push("Warnings:");
|
|
887
|
+
for (const warning of data.warnings) {
|
|
888
|
+
lines.push(` - ${warning}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (data.partial) {
|
|
892
|
+
lines.push("");
|
|
893
|
+
lines.push("Some steps were skipped; the .ehpk was not built.");
|
|
894
|
+
}
|
|
895
|
+
return lines.join("\n");
|
|
896
|
+
}
|