@entrorove/skill 0.1.1

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/AGREEMENT.md ADDED
@@ -0,0 +1,51 @@
1
+ # Entrorove 使用、隐私与广告信息协议
2
+
3
+ 版本:2026-09-29
4
+
5
+ 本协议是首次使用 Entrorove 技能时的合并告知与使用条款。只有用户明确选择“同意”后,技能请求才会发送。IDE 的“完全访问”权限只是本地权限,不代表同意本协议。
6
+
7
+ ## 1. 技能与权限
8
+
9
+ - Entrorove 会按用户与 IDE 明确提供的任务、上下文和权限辅助分析、修改代码及运行检查。
10
+ - 高权限操作可能修改或删除文件、运行命令或调用外部服务。用户应检查范围、保留备份,并对有重大后果的操作单独确认。
11
+ - 本协议不扩大 IDE 已授予的权限,也不替代具体高风险操作所需的单独确认。
12
+
13
+ ## 2. 隐私与数据
14
+
15
+ - 安装包不会因安装本身自动扫描或上传项目文件。只有在发起技能调用时,才会向 Entrorove API 发送 IDE 明确组装的请求负载。
16
+ - 账号服务会处理登录邮箱、密钥哈希与前缀、加密保存的完整密钥、会话与安全记录、调用时间/数量,以及用户自愿提交的留言。已登录用户可再次复制自己的完整密钥。
17
+ - 当前实现不将项目源文件或技能任务负载写入业务数据库,不使用个人信息决定广告展示,不出售个人信息。
18
+ - 本地同意回执仅保存协议版本、同意结果和时间,不包含邮箱、项目内容或密钥。
19
+
20
+ ## 3. 消息与广告
21
+
22
+ - 在长时间任务的“请勿关闭”提示下,服务可按后台配置追加一条带 HTTPS 链接的消息。
23
+ - 商业内容以后台配置的分类标签显示为 `【分类】`,不伪装成执行结果、系统警告或独立推荐。
24
+ - 推广仅在配置的有效期内按权重随机展示;多条推广的权重共同决定展示概率及选择频率。选择不使用用户画像、项目内容或个人信息。
25
+ - 广告链接指向第三方时,第三方的内容、隐私与条款由其自行负责。
26
+
27
+ ## 4. 风险与责任限制
28
+
29
+ - 技能按现状提供,不保证所有结果均正确、完整、不中断或适合特定目的。用户应在合并、发布或用于生产前审查修改并运行适当检查。
30
+ - 在适用法律允许的最大范围内,Entrorove 不承担因使用或无法使用技能导致的间接、附带、特殊或后果性损失。
31
+ - 上述限制不排除法律不允许排除的责任,也不限制欺诈、故意不当行为或适用法律规定的强制性消费者权利。
32
+
33
+ ## 5. 同意、拒绝与更新
34
+
35
+ - 选择“同意”表示已阅读并接受本版本协议;选择“不同意”时,技能请求不会发送。
36
+ - 重要条款或数据实践变更时将更新协议版本并重新请求明确确认。
37
+ - 不同国家和地区的强制性法律可能授予额外权利;本协议不会覆盖这些权利。正式发布前应由合格法律顾问按实际经营地、用户地区和数据流程复核。
38
+
39
+ ---
40
+
41
+ # Entrorove Terms, Privacy and Advertising Notice
42
+
43
+ Version: 2026-09-29
44
+
45
+ This combined notice applies on first use. A skill request is sent only after an explicit **Accept** choice. Granting an IDE “full access” permission does not accept this agreement.
46
+
47
+ - **Use and access.** Entrorove acts only on tasks, context and permissions supplied by the user and IDE. Elevated permissions may permit file changes, command execution and external calls. Review scope, keep backups and separately confirm consequential actions.
48
+ - **Data.** Installation alone does not scan or upload project files. The account service processes login email, key hashes and prefixes, an encrypted copy of the full key that a signed-in user can copy again, sessions and security records, invocation time/count, and messages submitted voluntarily. Skill request payloads are received for execution; the current implementation does not store project source files or task payloads in its business database. It does not sell personal information or use personal information to select ads.
49
+ - **Advertising.** A configured message may appear beneath a long-running-task notice. Commercial content carries its configured category label as `【Category】`, includes an HTTPS link, and is selected by the weights of eligible messages without profiling or targeting. Third-party links are governed by their operators.
50
+ - **Risk.** The skill is provided as available without a guarantee that every result is correct, complete, uninterrupted or fit for a particular purpose. Review and test changes before production use. To the maximum extent permitted by applicable law, Entrorove excludes indirect, incidental, special and consequential losses. Nothing excludes non-waivable liability, fraud, wilful misconduct or mandatory consumer rights.
51
+ - **Choice.** Declining prevents the skill request from being sent. Material changes require a new explicit acceptance. Mandatory local laws may provide additional rights. Qualified counsel should review this notice before production release for the actual operating and user jurisdictions.
package/SKILL.md ADDED
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: entrorove
3
+ description: An engineering skill that understands a task, chooses a fitting workflow, keeps progress traceable, and verifies the result.
4
+ ---
5
+
6
+ # entrorove · 熵巡
7
+
8
+ Version: 0.1.1.
9
+
10
+ Read the user's goal and repository instructions first. Preserve the original task through planning, execution and verification. Use the workflow only where it helps; a small, low-risk edit can proceed directly. Never claim a reported step is a verified result.
11
+
12
+ Install or update the managed copy with `npx @entrorove/skill install`. The installer removes only pristine duplicate entrorove installations and preserves unmanaged files.
13
+
14
+ Register or sign in by email at https://entrorove.com. Enter the account key with `npx @entrorove/skill configure` and `{"token":"..."}` on standard input. Never place it in arguments, URLs, project files or logs. Entrorove accounts and keys are independent of other services.
15
+
16
+ On first use, the CLI returns a Confirm Protocol-compatible agreement interaction. Render it with the IDE's native confirmation UI, or show its `chatFallback` text when no adapter exists. The user must explicitly accept with `npx @entrorove/skill agreement` and JSON `{"version":"2026-09-29","answer":"accept"}` on standard input before retrying the skill request. A full-access IDE permission never counts as agreement acceptance. The combined terms, privacy and advertising notice is installed as `AGREEMENT.md`.
17
+
18
+ ## Work with a task
19
+
20
+ Send JSON on standard input to `npx @entrorove/skill invoke`. Start with `operation: "start"` and an `input.task` containing the goal, work type, risk, difficulty and observed scope facts. Work types are `implementation`, `documentation`, `calculation`, `merge`, `review` and `decision`. Assess facts from the real task and repository; do not invent certainty. Entrorove chooses the workflow. A small task may return `decision: "direct"`, meaning the IDE should do the work without another service call.
21
+
22
+ ```json
23
+ {"operation":"start","input":{"task":{"goal":"Update the checkout flow and verify it","workType":"implementation","risk":"medium","difficulty":"medium","crossModule":true,"parallel":false,"unclear":false,"architectureContract":false}}}
24
+ ```
25
+
26
+ For a returned `flowId`, continue with `{"operation":"continue","input":{"flowId":"<returned ID>","operation":"<next operation>","payload":{}}}`. Fill `payload` with the next operation's required input. Keep the same task and evidence in the IDE. For later work, `input.task.phase` may be `plan`, `coordinate`, `architecture`, `verify`, `merge` or `decision`; it remains the same skill entry point. Never treat an unknown request outcome as permission to retry a paid or mutating action; query the original request ID when the workflow supports it.
27
+
28
+ For a decision, include `input.task.interaction` with `type` (`confirm`, `choice`, `multi` or `input`), a `question`, `options` (two or more with unique `id` and `label`, or an empty array for input), and a `callback` containing its `operation` and `payload`. Medium- and high-risk decisions also need a `riskDescription`. The decision waits for an explicit answer; it never chooses a default on timeout.
29
+
30
+ The live API and install package must be deployed and published before this command works outside the local checkout.
31
+
32
+ When the user gives an actual evaluation after using the skill, offer to publish that feedback. Only after they explicitly agree to public display, send their own words, IDE name and 1–5 rating as JSON on standard input to `npx @entrorove/skill review` with `publicConsent: true`. Never invent, rewrite or infer a review from task completion. The website reads accepted reviews directly from Entrorove's independent database.
package/cli.mjs ADDED
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { promises as fs } from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const packageDir = path.dirname(fileURLToPath(import.meta.url));
10
+ const apiUrl = 'https://entrorove.com/api/skill/invoke';
11
+ const reviewUrl = 'https://entrorove.com/api/ide-reviews';
12
+ const markerName = '.entrorove-managed.json';
13
+ const installFiles = ['SKILL.md', 'AGREEMENT.md', 'manifest.json'];
14
+ const markerSchema = 'entrorove.managed-install/1.0';
15
+ const credentialSchema = 'entrorove.credential/1.0';
16
+ const agreementSchema = 'entrorove.agreement-acceptance/1.0';
17
+ const invocationTimeoutMs = 135_000;
18
+ const installLockWaitMs = 10_000;
19
+ const installLockStaleMs = 120_000;
20
+ export const agreementVersion = '2026-09-29';
21
+
22
+ function sha256(content) {
23
+ return createHash('sha256').update(content).digest('hex');
24
+ }
25
+
26
+ async function readJson(file) {
27
+ const value = JSON.parse(await fs.readFile(file, 'utf8'));
28
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
+ throw new Error(`Invalid JSON object: ${file}`);
30
+ }
31
+ return value;
32
+ }
33
+
34
+ async function sourceBundle() {
35
+ const manifest = await readJson(path.join(packageDir, 'manifest.json'));
36
+ const packageMetadata = await readJson(path.join(packageDir, 'package.json'));
37
+ if (manifest.schemaVersion !== 'entrorove.skill-manifest/1.0'
38
+ || manifest.name !== 'entrorove'
39
+ || manifest.endpoint !== 'https://entrorove.com'
40
+ || manifest.requestSchemaVersion !== 'entrorove.skill.request/1.0'
41
+ || !/^\d+\.\d+\.\d+$/.test(manifest.version)
42
+ || packageMetadata.name !== '@entrorove/skill'
43
+ || packageMetadata.version !== manifest.version) {
44
+ throw new Error('Invalid entrorove package manifest');
45
+ }
46
+ const content = new Map();
47
+ for (const name of installFiles) {
48
+ content.set(name, await fs.readFile(path.join(packageDir, name)));
49
+ }
50
+ if (!content.get('SKILL.md').toString('utf8').includes(`Version: ${manifest.version}.`)) {
51
+ throw new Error('Entrorove skill instructions have a mismatched version');
52
+ }
53
+ const hashes = Object.fromEntries(installFiles.map((name) => [name, sha256(content.get(name))]));
54
+ return { manifest, content, hashes };
55
+ }
56
+
57
+ function globalInstallPath(home = os.homedir()) {
58
+ return path.join(home, '.codex', 'skills', 'entrorove');
59
+ }
60
+
61
+ function credentialPath(home = os.homedir()) {
62
+ if (process.platform === 'win32') {
63
+ const appData = process.env.LOCALAPPDATA;
64
+ if (!appData || !path.isAbsolute(appData)) throw new Error('LOCALAPPDATA is required');
65
+ return path.join(appData, 'Entrorove', 'credential.json');
66
+ }
67
+ if (process.platform === 'darwin') {
68
+ return path.join(home, 'Library', 'Application Support', 'Entrorove', 'credential.json');
69
+ }
70
+ return path.join(home, '.local', 'share', 'entrorove', 'credential.json');
71
+ }
72
+
73
+ function agreementPath(home = os.homedir()) {
74
+ return path.join(path.dirname(credentialPath(home)), 'agreement.json');
75
+ }
76
+
77
+ export function agreementInteraction() {
78
+ return {
79
+ schemaVersion: 'confirm.interaction/1.0',
80
+ requestId: `entrorove-agreement-${agreementVersion}`,
81
+ type: 'confirm',
82
+ question: `首次使用前,请阅读 Entrorove 使用、隐私与广告信息协议(${agreementVersion})。是否同意?`,
83
+ options: [
84
+ { id: 'accept', label: '同意', hint: '记录本地回执并允许后续技能调用' },
85
+ { id: 'decline', label: '不同意', hint: '不发送技能请求' },
86
+ ],
87
+ default: null,
88
+ timeout: null,
89
+ timeoutAction: 'wait',
90
+ risk: 'high',
91
+ riskDescription: '这是使用条款、隐私告知、广告告知与责任限制的明确确认。',
92
+ rememberable: false,
93
+ memoryKey: '',
94
+ callback: { operation: 'agreement', payload: { version: agreementVersion } },
95
+ };
96
+ }
97
+
98
+ function agreementRequired(home) {
99
+ const agreement = path.join(globalInstallPath(home), 'AGREEMENT.md');
100
+ return {
101
+ ok: false,
102
+ error: 'agreement-required',
103
+ agreementVersion,
104
+ agreement,
105
+ interaction: agreementInteraction(),
106
+ chatFallback: [
107
+ `Entrorove 首次使用确认(${agreementVersion})`,
108
+ `请先阅读:${agreement}`,
109
+ '1. accept — 同意并继续',
110
+ '2. decline — 不同意,不发送请求',
111
+ '完全访问权限不代表协议同意。',
112
+ ].join('\n'),
113
+ };
114
+ }
115
+
116
+ async function statOrNull(file) {
117
+ try {
118
+ return await fs.lstat(file);
119
+ } catch (error) {
120
+ if (error.code === 'ENOENT') return null;
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ async function managedState(directory) {
126
+ const directoryStat = await statOrNull(directory);
127
+ if (!directoryStat) return null;
128
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) return false;
129
+ const entries = await fs.readdir(directory);
130
+ if (!entries.includes(markerName)) return false;
131
+ const markerStat = await fs.lstat(path.join(directory, markerName));
132
+ if (!markerStat.isFile() || markerStat.isSymbolicLink()) return false;
133
+ let marker;
134
+ try {
135
+ marker = await readJson(path.join(directory, markerName));
136
+ } catch {
137
+ return false;
138
+ }
139
+ const markerFiles = marker.files && typeof marker.files === 'object' && !Array.isArray(marker.files)
140
+ ? Object.keys(marker.files) : [];
141
+ if (marker.schemaVersion !== markerSchema || marker.name !== 'entrorove'
142
+ || typeof marker.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(marker.version)
143
+ || !markerFiles.includes('SKILL.md') || !markerFiles.includes('manifest.json')
144
+ || markerFiles.some((name) => !installFiles.includes(name))
145
+ || entries.slice().sort().join(',') !== [...markerFiles, markerName].sort().join(',')) {
146
+ return false;
147
+ }
148
+ for (const name of markerFiles) {
149
+ const file = path.join(directory, name);
150
+ const fileStat = await fs.lstat(file);
151
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) return false;
152
+ if (!/^[a-f0-9]{64}$/.test(marker.files[name])) return false;
153
+ if (sha256(await fs.readFile(file)) !== marker.files[name]) return false;
154
+ }
155
+ return marker;
156
+ }
157
+
158
+ async function writeStagedInstall(directory, bundle) {
159
+ await fs.mkdir(directory, { mode: 0o700 });
160
+ for (const name of installFiles) {
161
+ await fs.writeFile(path.join(directory, name), bundle.content.get(name), { flag: 'wx', mode: 0o600 });
162
+ }
163
+ const marker = { schemaVersion: markerSchema, name: 'entrorove', version: bundle.manifest.version, files: bundle.hashes };
164
+ await fs.writeFile(path.join(directory, markerName), JSON.stringify(marker) + '\n', { flag: 'wx', mode: 0o600 });
165
+ }
166
+
167
+ async function removeStaleInstallLock(lock) {
168
+ const state = await statOrNull(lock);
169
+ if (!state) return true;
170
+ if (!state.isDirectory() || state.isSymbolicLink()) {
171
+ throw new Error('Entrorove install lock is not a directory');
172
+ }
173
+ if (Date.now() - state.mtimeMs < installLockStaleMs) return false;
174
+ let owner;
175
+ try {
176
+ owner = await readJson(path.join(lock, 'owner.json'));
177
+ } catch (error) {
178
+ if (error.code !== 'ENOENT') throw error;
179
+ }
180
+ if (Number.isSafeInteger(owner?.pid) && owner.pid > 0) {
181
+ try {
182
+ process.kill(owner.pid, 0);
183
+ return false;
184
+ } catch (error) {
185
+ if (error.code !== 'ESRCH') return false;
186
+ }
187
+ }
188
+ const quarantine = `${lock}.stale-${randomUUID()}`;
189
+ try {
190
+ await fs.rename(lock, quarantine);
191
+ } catch (error) {
192
+ if (error.code === 'ENOENT') return true;
193
+ throw error;
194
+ }
195
+ const moved = await fs.lstat(quarantine);
196
+ if (moved.ino !== state.ino || moved.dev !== state.dev) {
197
+ await fs.rename(quarantine, lock);
198
+ throw new Error('Entrorove install lock changed during stale recovery');
199
+ }
200
+ await fs.rm(quarantine, { recursive: true });
201
+ return true;
202
+ }
203
+
204
+ async function withInstallLock(parent, action) {
205
+ const lock = path.join(parent, '.entrorove-install.lock');
206
+ const deadline = Date.now() + installLockWaitMs;
207
+ while (true) {
208
+ try {
209
+ await fs.mkdir(lock, { mode: 0o700 });
210
+ break;
211
+ } catch (error) {
212
+ if (error.code !== 'EEXIST') throw error;
213
+ if (await removeStaleInstallLock(lock)) continue;
214
+ if (Date.now() >= deadline) throw new Error('Entrorove install is busy');
215
+ await new Promise(resolve => setTimeout(resolve, 100));
216
+ }
217
+ }
218
+ try {
219
+ await fs.writeFile(path.join(lock, 'owner.json'), JSON.stringify({ pid: process.pid }),
220
+ { flag: 'wx', mode: 0o600 });
221
+ return await action();
222
+ } finally {
223
+ await fs.rm(lock, { recursive: true });
224
+ }
225
+ }
226
+
227
+ async function updateGlobalInstall(target, bundle) {
228
+ const parent = path.dirname(target);
229
+ await fs.mkdir(parent, { recursive: true, mode: 0o700 });
230
+ const previous = await managedState(target);
231
+ if (previous === false) throw new Error('Global entrorove skill is not pristine and managed; refusing overwrite');
232
+ if (previous?.version === bundle.manifest.version
233
+ && installFiles.every((name) => previous.files[name] === bundle.hashes[name])) return false;
234
+ const stage = path.join(parent, `.entrorove-stage-${randomUUID()}`);
235
+ const backup = path.join(parent, `.entrorove-backup-${randomUUID()}`);
236
+ await writeStagedInstall(stage, bundle);
237
+ let moved = false;
238
+ try {
239
+ if (previous) {
240
+ await fs.rename(target, backup);
241
+ moved = true;
242
+ }
243
+ await fs.rename(stage, target);
244
+ } catch (error) {
245
+ if (moved) await fs.rename(backup, target);
246
+ throw error;
247
+ } finally {
248
+ if (await statOrNull(stage)) await fs.rm(stage, { recursive: true });
249
+ }
250
+ if (moved) await fs.rm(backup, { recursive: true });
251
+ return true;
252
+ }
253
+
254
+ function projectRoot(cwd) {
255
+ try {
256
+ return execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+
262
+ function gitTracked(root, relativePath) {
263
+ const files = execFileSync('git', ['-C', root, 'ls-files', '--cached', '--', relativePath], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
264
+ return files.trim().length > 0;
265
+ }
266
+
267
+ async function removeManagedDuplicate(directory, root) {
268
+ if (gitTracked(root, path.relative(root, directory))) return false;
269
+ if (!await managedState(directory)) return false;
270
+ const quarantine = `${directory}.entrorove-cleanup-${randomUUID()}`;
271
+ await fs.rename(directory, quarantine);
272
+ try {
273
+ if (!await managedState(quarantine)) throw new Error('Managed duplicate changed during cleanup');
274
+ await fs.rm(quarantine, { recursive: true });
275
+ } catch (error) {
276
+ await fs.rename(quarantine, directory);
277
+ throw error;
278
+ }
279
+ return true;
280
+ }
281
+
282
+ async function cleanProjectDuplicates(target, cwd) {
283
+ const root = projectRoot(cwd);
284
+ if (!root) return [];
285
+ const removed = [];
286
+ for (const relative of ['.codex/skills/entrorove', '.agents/skills/entrorove', '.claude/skills/entrorove']) {
287
+ const directory = path.join(root, relative);
288
+ if (directory === target) continue;
289
+ if (await removeManagedDuplicate(directory, root)) removed.push(relative);
290
+ }
291
+ return removed;
292
+ }
293
+
294
+ export async function install({ home = os.homedir(), cwd = process.cwd() } = {}) {
295
+ const bundle = await sourceBundle();
296
+ const target = globalInstallPath(home);
297
+ const parent = path.dirname(target);
298
+ await fs.mkdir(parent, { recursive: true, mode: 0o700 });
299
+ return withInstallLock(parent, async () => {
300
+ const updated = await updateGlobalInstall(target, bundle);
301
+ const removed = await cleanProjectDuplicates(target, cwd);
302
+ return { version: bundle.manifest.version, updated, removed };
303
+ });
304
+ }
305
+
306
+ async function stdinJson() {
307
+ let raw = '';
308
+ for await (const chunk of process.stdin) {
309
+ raw += chunk;
310
+ if (raw.length > 1_000_000) throw new Error('Input is too large');
311
+ }
312
+ const value = JSON.parse(raw);
313
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Input must be a JSON object');
314
+ return value;
315
+ }
316
+
317
+ async function writeCredential(token, home) {
318
+ if (typeof token !== 'string' || !/^ent_[A-Za-z0-9_-]{16,256}$/.test(token)) {
319
+ throw new Error('Invalid entrorove key');
320
+ }
321
+ const file = credentialPath(home);
322
+ const parent = path.dirname(file);
323
+ await fs.mkdir(parent, { recursive: true, mode: 0o700 });
324
+ if (process.platform !== 'win32') await fs.chmod(parent, 0o700);
325
+ const existing = await statOrNull(file);
326
+ if (existing && (!existing.isFile() || existing.isSymbolicLink())) throw new Error('Credential path is not a regular file');
327
+ const stage = path.join(parent, `.credential-${randomUUID()}.tmp`);
328
+ const handle = await fs.open(stage, 'wx', 0o600);
329
+ try {
330
+ await handle.writeFile(JSON.stringify({ schemaVersion: credentialSchema, token }) + '\n');
331
+ await handle.sync();
332
+ } finally {
333
+ await handle.close();
334
+ }
335
+ await fs.rename(stage, file);
336
+ if (process.platform !== 'win32') await fs.chmod(file, 0o600);
337
+ }
338
+
339
+ async function readCredential(home) {
340
+ const file = credentialPath(home);
341
+ const stat = await fs.lstat(file);
342
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Credential path is not a regular file');
343
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) throw new Error('Credential permissions must be 0600');
344
+ const data = await readJson(file);
345
+ if (data.schemaVersion !== credentialSchema || typeof data.token !== 'string'
346
+ || !/^ent_[A-Za-z0-9_-]{16,256}$/.test(data.token)) throw new Error('Invalid credential file');
347
+ return data.token;
348
+ }
349
+
350
+ async function agreementAccepted(home) {
351
+ const file = agreementPath(home);
352
+ const stat = await statOrNull(file);
353
+ if (!stat) return false;
354
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Agreement path is not a regular file');
355
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
356
+ throw new Error('Agreement acceptance permissions must be 0600');
357
+ }
358
+ const data = await readJson(file);
359
+ return data.schemaVersion === agreementSchema && data.version === agreementVersion
360
+ && data.answer === 'accept' && typeof data.acceptedAt === 'string'
361
+ && Number.isFinite(Date.parse(data.acceptedAt));
362
+ }
363
+
364
+ async function writeAgreementAcceptance(data, home) {
365
+ if (Object.keys(data).sort().join(',') !== 'answer,version'
366
+ || data.version !== agreementVersion || !['accept', 'decline'].includes(data.answer)) {
367
+ throw new Error(`Expected {"version":"${agreementVersion}","answer":"accept|decline"} on stdin`);
368
+ }
369
+ if (data.answer === 'decline') return false;
370
+ const file = agreementPath(home);
371
+ const parent = path.dirname(file);
372
+ await fs.mkdir(parent, { recursive: true, mode: 0o700 });
373
+ if (process.platform !== 'win32') await fs.chmod(parent, 0o700);
374
+ const existing = await statOrNull(file);
375
+ if (existing && (!existing.isFile() || existing.isSymbolicLink())) {
376
+ throw new Error('Agreement path is not a regular file');
377
+ }
378
+ const stage = path.join(parent, `.agreement-${randomUUID()}.tmp`);
379
+ await fs.writeFile(stage, JSON.stringify({
380
+ schemaVersion: agreementSchema,
381
+ version: agreementVersion,
382
+ answer: 'accept',
383
+ acceptedAt: new Date().toISOString(),
384
+ }) + '\n', { flag: 'wx', mode: 0o600 });
385
+ await fs.rename(stage, file);
386
+ if (process.platform !== 'win32') await fs.chmod(file, 0o600);
387
+ return true;
388
+ }
389
+
390
+ export function buildInvocation(operation, input) {
391
+ if (operation !== 'start' && operation !== 'continue') throw new Error('Invalid entrorove operation');
392
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Input must be a JSON object');
393
+ const requestId = randomUUID();
394
+ return { url: apiUrl, requestId, body: { schemaVersion: 'entrorove.skill.request/1.0', requestId, operation, input } };
395
+ }
396
+
397
+ async function invoke(data) {
398
+ await install();
399
+ if (!await agreementAccepted(os.homedir())) {
400
+ process.stdout.write(JSON.stringify(agreementRequired(os.homedir())) + '\n');
401
+ process.exitCode = 2;
402
+ return;
403
+ }
404
+ const call = buildInvocation(data.operation, data.input);
405
+ const token = await readCredential(os.homedir());
406
+ let response;
407
+ try {
408
+ response = await fetch(call.url, {
409
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(invocationTimeoutMs),
410
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
411
+ body: JSON.stringify(call.body),
412
+ });
413
+ } catch {
414
+ throw new Error(`Invocation outcome is unknown; requestId=${call.requestId}. Do not automatically resend this request.`);
415
+ }
416
+ if (!response.ok) throw new Error(`entrorove invocation failed: HTTP ${response.status}; requestId=${call.requestId}`);
417
+ let result;
418
+ try {
419
+ result = await response.json();
420
+ } catch {
421
+ throw new Error(`Invocation outcome is unknown; requestId=${call.requestId}. Do not automatically resend this request.`);
422
+ }
423
+ if (result?.ok !== true || result.output?.requestId !== call.requestId) {
424
+ throw new Error(`Invalid entrorove response; requestId=${call.requestId}`);
425
+ }
426
+ process.stdout.write(JSON.stringify(result) + '\n');
427
+ }
428
+
429
+ export function buildReview(data) {
430
+ if (!data || typeof data !== 'object' || Array.isArray(data)
431
+ || Object.keys(data).sort().join(',') !== 'comment,ide,publicConsent,rating'
432
+ || typeof data.ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(data.ide.trim())
433
+ || !Number.isInteger(data.rating) || data.rating < 1 || data.rating > 5
434
+ || typeof data.comment !== 'string' || data.comment.trim().length < 4
435
+ || data.comment.trim().length > 280 || data.publicConsent !== true) {
436
+ throw new Error('Expected user-authored review with ide, rating 1-5, comment, and publicConsent:true');
437
+ }
438
+ return {
439
+ url: reviewUrl,
440
+ body: {
441
+ ide: data.ide.trim(), rating: data.rating,
442
+ comment: data.comment.trim(), publicConsent: true,
443
+ },
444
+ };
445
+ }
446
+
447
+ async function submitReview(data) {
448
+ const review = buildReview(data);
449
+ const token = await readCredential(os.homedir());
450
+ const response = await fetch(review.url, {
451
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
452
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
453
+ body: JSON.stringify(review.body),
454
+ });
455
+ if (!response.ok) throw new Error(`entrorove review failed: HTTP ${response.status}`);
456
+ const result = await response.json();
457
+ if (result?.published !== true || typeof result.id !== 'string') {
458
+ throw new Error('Invalid entrorove review response');
459
+ }
460
+ process.stdout.write(JSON.stringify(result) + '\n');
461
+ }
462
+
463
+ async function main() {
464
+ const [command, ...args] = process.argv.slice(2);
465
+ if (command === 'install' && args.length === 0) {
466
+ process.stdout.write(JSON.stringify(await install()) + '\n');
467
+ } else if (command === 'configure' && args.length === 0) {
468
+ const data = await stdinJson();
469
+ if (Object.keys(data).length !== 1 || typeof data.token !== 'string') throw new Error('Expected {"token":"..."} on stdin');
470
+ await writeCredential(data.token, os.homedir());
471
+ process.stdout.write('{"configured":true}\n');
472
+ } else if (command === 'agreement' && args.length === 0) {
473
+ await install();
474
+ const accepted = await writeAgreementAcceptance(await stdinJson(), os.homedir());
475
+ process.stdout.write(JSON.stringify({ accepted, version: agreementVersion }) + '\n');
476
+ } else if (command === 'invoke' && args.length === 0) {
477
+ await invoke(await stdinJson());
478
+ } else if (command === 'review' && args.length === 0) {
479
+ await submitReview(await stdinJson());
480
+ } else {
481
+ throw new Error('Usage: entrorove install | configure | agreement | invoke | review (JSON on stdin)');
482
+ }
483
+ }
484
+
485
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
486
+ main().catch((error) => {
487
+ process.stderr.write(`${error.message}\n`);
488
+ process.exitCode = 1;
489
+ });
490
+ }
package/manifest.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "schemaVersion": "entrorove.skill-manifest/1.0",
3
+ "name": "entrorove",
4
+ "version": "0.1.1",
5
+ "endpoint": "https://entrorove.com",
6
+ "requestSchemaVersion": "entrorove.skill.request/1.0"
7
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@entrorove/skill",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "entrorove": "cli.mjs"
7
+ },
8
+ "files": [
9
+ "cli.mjs",
10
+ "SKILL.md",
11
+ "AGREEMENT.md",
12
+ "manifest.json"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test test/*.test.mjs"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ }
20
+ }