@rover-studio/answer-me 0.1.0-rc.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.
Files changed (39) hide show
  1. package/bin/answerme-toolkit.mjs +6 -0
  2. package/distribution/npm/migrations.json +605 -0
  3. package/distribution/npm/package-manifest.json +192 -0
  4. package/distribution/npm/skills/answerme/SKILL.md +94 -0
  5. package/distribution/npm/skills/answerme/agents/openai.yaml +4 -0
  6. package/distribution/npm/skills/answerme/references/api.md +135 -0
  7. package/distribution/npm/skills/answerme/references/creator-credential-deployment.md +19 -0
  8. package/distribution/npm/skills/answerme/references/creator-credential-recovery.md +24 -0
  9. package/distribution/npm/skills/answerme/references/errors.md +44 -0
  10. package/distribution/npm/skills/answerme/references/handoff.md +46 -0
  11. package/distribution/npm/skills/answerme/references/install-self-test.md +28 -0
  12. package/distribution/npm/skills/answerme/references/result-token-store.md +50 -0
  13. package/distribution/npm/skills/answerme/references/templates.md +139 -0
  14. package/distribution/npm/skills/answerme/scripts/answerme-api-base-url.ps1 +40 -0
  15. package/distribution/npm/skills/answerme/scripts/create-answerme.ps1 +1892 -0
  16. package/distribution/npm/skills/answerme/scripts/creator-credential-store.windows.ps1 +503 -0
  17. package/distribution/npm/skills/answerme/scripts/deploy-answerme-creator-credential.ps1 +447 -0
  18. package/distribution/npm/skills/answerme/scripts/enroll-answerme-creator.ps1 +764 -0
  19. package/distribution/npm/skills/answerme/scripts/open-answerme-page.windows.ps1 +272 -0
  20. package/distribution/npm/skills/answerme/scripts/remove-answerme-result-token.ps1 +63 -0
  21. package/distribution/npm/skills/answerme/scripts/result-token-store.windows.ps1 +261 -0
  22. package/distribution/npm/skills/answerme/scripts/test-answerme-installation.ps1 +498 -0
  23. package/distribution/npm/skills/answerme/scripts/wait-answerme-result.ps1 +908 -0
  24. package/distribution/npm/skills/answerme/scripts/windows-crypto.ps1 +57 -0
  25. package/distribution/npm/skills/answerme/scripts/windows-http.ps1 +45 -0
  26. package/distribution/npm/skills/answerme/scripts/windows-process-start-info.ps1 +76 -0
  27. package/distribution/npm/skills/ask-when-needed/SKILL.md +164 -0
  28. package/distribution/npm/skills/ask-when-needed/agents/openai.yaml +4 -0
  29. package/distribution/npm/skills/ask-when-needed/references/interview-strategies.md +43 -0
  30. package/lib/npm-cli/commands.mjs +247 -0
  31. package/lib/npm-cli/constants.mjs +51 -0
  32. package/lib/npm-cli/errors.mjs +15 -0
  33. package/lib/npm-cli/filesystem.mjs +193 -0
  34. package/lib/npm-cli/host-discovery.mjs +404 -0
  35. package/lib/npm-cli/main.mjs +42 -0
  36. package/lib/npm-cli/package-integrity.mjs +212 -0
  37. package/lib/npm-cli/transaction.mjs +375 -0
  38. package/lib/npm-cli/usage-validation.mjs +349 -0
  39. package/package.json +17 -0
@@ -0,0 +1,349 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { lstat, realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {
5
+ MAX_BUFFER,
6
+ OFFICIAL_API_ROOT,
7
+ USAGE_VALIDATION_TIMEOUT_SECONDS,
8
+ } from './constants.mjs';
9
+ import { normalizePath, pathExists } from './filesystem.mjs';
10
+
11
+ const ENROLLMENT_EVENT = 'answerme.creator-enrollment.verification-url';
12
+ const ENROLLMENT_STAGE = 'verification-handoff';
13
+ const MAX_ENROLLMENT_EVENT_BYTES = 4096;
14
+ const SELF_TEST_OUTPUT_KEYS = Object.freeze([
15
+ 'cleanupWarning',
16
+ 'code',
17
+ 'message',
18
+ 'networkCalled',
19
+ 'ok',
20
+ 'resultTokenCleanup',
21
+ 'stage',
22
+ 'status',
23
+ ]);
24
+ const SELF_TEST_ISSUES = Object.freeze({
25
+ 'self-test-dependency-missing': Object.freeze({
26
+ stage: 'preflight',
27
+ message: 'A required AnswerMe self-test adapter is missing.',
28
+ }),
29
+ 'self-test-resume-unsupported': Object.freeze({
30
+ stage: 'preflight',
31
+ message: 'Installation usage validation must create and wait for its own fixed questionnaire.',
32
+ }),
33
+ 'self-test-create-stderr-invalid': Object.freeze({
34
+ stage: 'create',
35
+ message: 'The AnswerMe create adapter emitted an untrusted stderr record.',
36
+ }),
37
+ 'self-test-create-validation-failed': Object.freeze({
38
+ stage: 'create',
39
+ message: 'The AnswerMe self-test questionnaire contract could not be validated.',
40
+ }),
41
+ 'self-test-create-failed': Object.freeze({
42
+ stage: 'create',
43
+ message: 'The AnswerMe self-test questionnaire could not be created and verified.',
44
+ }),
45
+ 'self-test-wait-failed': Object.freeze({
46
+ stage: 'wait',
47
+ message: 'The AnswerMe self-test did not reach a readable terminal result.',
48
+ }),
49
+ 'self-test-result-invalid': Object.freeze({
50
+ stage: 'mapping',
51
+ message: 'The AnswerMe self-test result did not match the fixed three-question contract.',
52
+ }),
53
+ });
54
+ const CLEANUP_WARNING = Object.freeze({
55
+ code: 'self-test-cleanup-failed',
56
+ message: 'The protected self-test Result Token could not be confirmed removed.',
57
+ });
58
+
59
+ function isExactOfficialApiRoot(value = OFFICIAL_API_ROOT) {
60
+ try {
61
+ const url = new URL(value);
62
+ return url.protocol === 'https:'
63
+ && url.origin === value
64
+ && url.pathname === '/'
65
+ && !url.username
66
+ && !url.password
67
+ && !url.search
68
+ && !url.hash;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ if (!isExactOfficialApiRoot()) throw new Error('invalid official AnswerMe API root');
75
+
76
+ function isRecord(value) {
77
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
78
+ }
79
+
80
+ function hasExactKeys(value, keys) {
81
+ return isRecord(value)
82
+ && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
83
+ }
84
+
85
+ function exactCleanup(value, attempted, removed) {
86
+ return hasExactKeys(value, ['attempted', 'removed'])
87
+ && value.attempted === attempted
88
+ && value.removed === removed;
89
+ }
90
+
91
+ function exactCleanupWarning(value) {
92
+ return hasExactKeys(value, ['code', 'message'])
93
+ && value.code === CLEANUP_WARNING.code
94
+ && value.message === CLEANUP_WARNING.message;
95
+ }
96
+
97
+ function validateSelfTestOutput(value) {
98
+ if (!hasExactKeys(value, SELF_TEST_OUTPUT_KEYS) || typeof value.networkCalled !== 'boolean') return false;
99
+ if (value.ok === true
100
+ && value.status === 'passed'
101
+ && value.stage === 'complete'
102
+ && value.code === null
103
+ && value.message === null
104
+ && value.networkCalled === true
105
+ && (exactCleanup(value.resultTokenCleanup, true, true)
106
+ || exactCleanup(value.resultTokenCleanup, true, false))) {
107
+ return value.resultTokenCleanup.removed === true
108
+ ? value.cleanupWarning === null
109
+ : value.resultTokenCleanup.removed === false && exactCleanupWarning(value.cleanupWarning);
110
+ }
111
+ const issue = typeof value.code === 'string' ? SELF_TEST_ISSUES[value.code] : null;
112
+ return value.ok === false
113
+ && value.status === 'issue'
114
+ && issue !== null
115
+ && value.stage === issue.stage
116
+ && value.message === issue.message
117
+ && exactCleanup(value.resultTokenCleanup, false, false)
118
+ && value.cleanupWarning === null;
119
+ }
120
+
121
+ function parseSingleJson(stdout) {
122
+ const lines = String(stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
123
+ if (lines.length !== 1 || Buffer.byteLength(lines[0], 'utf8') > MAX_BUFFER) return null;
124
+ try {
125
+ const value = JSON.parse(lines[0]);
126
+ return isRecord(value) ? value : null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function parseEnrollmentEvent(line) {
133
+ const text = String(line ?? '').trim();
134
+ if (!text || Buffer.byteLength(text, 'utf8') > MAX_ENROLLMENT_EVENT_BYTES) return null;
135
+ let value;
136
+ try {
137
+ value = JSON.parse(text);
138
+ } catch {
139
+ return null;
140
+ }
141
+ if (!isRecord(value)
142
+ || JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(['event', 'stage', 'verificationUrl'])
143
+ || value.event !== ENROLLMENT_EVENT
144
+ || value.stage !== ENROLLMENT_STAGE
145
+ || typeof value.verificationUrl !== 'string') {
146
+ return null;
147
+ }
148
+ let verificationUrl;
149
+ try {
150
+ verificationUrl = new URL(value.verificationUrl);
151
+ } catch {
152
+ return null;
153
+ }
154
+ if (verificationUrl.protocol !== 'https:'
155
+ || verificationUrl.origin !== OFFICIAL_API_ROOT
156
+ || verificationUrl.username
157
+ || verificationUrl.password
158
+ || verificationUrl.hash) {
159
+ return null;
160
+ }
161
+ return JSON.stringify({
162
+ event: ENROLLMENT_EVENT,
163
+ stage: ENROLLMENT_STAGE,
164
+ verificationUrl: verificationUrl.href,
165
+ });
166
+ }
167
+
168
+ function safeProcessError(code) {
169
+ return Object.assign(new Error(code), { code });
170
+ }
171
+
172
+ function invokeValidationProcess(executable, args, options, dependencies = {}) {
173
+ return new Promise((resolve, reject) => {
174
+ const spawn = dependencies.spawn ?? nodeSpawn;
175
+ const stderrWriter = dependencies.stderrWriter ?? ((text) => process.stderr.write(text));
176
+ let child;
177
+ try {
178
+ child = spawn(executable, args, {
179
+ shell: false,
180
+ windowsHide: true,
181
+ env: options.env,
182
+ stdio: ['ignore', 'pipe', 'pipe'],
183
+ });
184
+ } catch {
185
+ reject(safeProcessError('usage-validation-process-start-failed'));
186
+ return;
187
+ }
188
+ if (!child?.stdout || !child?.stderr || typeof child.kill !== 'function') {
189
+ try { child?.kill?.(); } catch { /* the process handle is already invalid */ }
190
+ reject(safeProcessError('usage-validation-process-invalid'));
191
+ return;
192
+ }
193
+
194
+ let settled = false;
195
+ let stdout = '';
196
+ let stdoutBytes = 0;
197
+ let stderrPending = '';
198
+ let stderrBytes = 0;
199
+ let enrollmentEventCount = 0;
200
+ let failureCode = null;
201
+ let timedOut = false;
202
+
203
+ const stop = (code) => {
204
+ if (!failureCode) failureCode = code;
205
+ try { child.kill(); } catch { /* close/error settles the result */ }
206
+ };
207
+ const consumeStderrLine = (line) => {
208
+ if (!line) {
209
+ stop('usage-validation-stderr-invalid');
210
+ return;
211
+ }
212
+ const safeEvent = parseEnrollmentEvent(line);
213
+ if (!safeEvent || enrollmentEventCount !== 0) {
214
+ stop('usage-validation-stderr-invalid');
215
+ return;
216
+ }
217
+ enrollmentEventCount += 1;
218
+ try {
219
+ stderrWriter(`${safeEvent}\n`);
220
+ } catch {
221
+ stop('usage-validation-stderr-relay-failed');
222
+ }
223
+ };
224
+ const timer = setTimeout(() => {
225
+ timedOut = true;
226
+ stop('ETIMEDOUT');
227
+ }, options.timeoutMs);
228
+ const finish = (error, value) => {
229
+ if (settled) return;
230
+ settled = true;
231
+ clearTimeout(timer);
232
+ if (error) reject(error);
233
+ else resolve(value);
234
+ };
235
+
236
+ child.stdout.setEncoding('utf8');
237
+ child.stderr.setEncoding('utf8');
238
+ child.stdout.on('data', (chunk) => {
239
+ stdoutBytes += Buffer.byteLength(chunk, 'utf8');
240
+ if (stdoutBytes > options.maxBuffer) {
241
+ stop('usage-validation-stdout-too-large');
242
+ return;
243
+ }
244
+ stdout += chunk;
245
+ });
246
+ child.stderr.on('data', (chunk) => {
247
+ stderrBytes += Buffer.byteLength(chunk, 'utf8');
248
+ if (stderrBytes > options.maxBuffer) {
249
+ stop('usage-validation-stderr-too-large');
250
+ return;
251
+ }
252
+ stderrPending += chunk;
253
+ let newline;
254
+ while ((newline = stderrPending.indexOf('\n')) !== -1) {
255
+ const line = stderrPending.slice(0, newline).replace(/\r$/, '');
256
+ stderrPending = stderrPending.slice(newline + 1);
257
+ consumeStderrLine(line);
258
+ }
259
+ });
260
+ child.stdout.once('error', () => stop('usage-validation-stdout-read-failed'));
261
+ child.stderr.once('error', () => stop('usage-validation-stderr-read-failed'));
262
+ child.once('error', () => finish(safeProcessError('usage-validation-process-error')));
263
+ child.once('close', (code, signal) => {
264
+ if (stderrPending) stop('usage-validation-stderr-invalid');
265
+ if (timedOut) {
266
+ finish(safeProcessError('ETIMEDOUT'));
267
+ } else if (failureCode) {
268
+ finish(safeProcessError(failureCode));
269
+ } else if (code !== 0 || signal) {
270
+ finish(safeProcessError('usage-validation-process-exit'));
271
+ } else {
272
+ finish(null, { stdout });
273
+ }
274
+ });
275
+ });
276
+ }
277
+
278
+ async function validatePowerShell(candidate, dependencies) {
279
+ if (typeof candidate !== 'string' || !path.isAbsolute(candidate) || path.basename(candidate).toLowerCase() !== 'powershell.exe') {
280
+ return false;
281
+ }
282
+ if (typeof dependencies.inspectPowerShell === 'function') return dependencies.inspectPowerShell(candidate);
283
+ const info = await lstat(candidate).catch(() => null);
284
+ if (!info?.isFile() || info.isSymbolicLink()) return false;
285
+ return normalizePath(await realpath(candidate)) === normalizePath(candidate);
286
+ }
287
+
288
+ function result(status, code, cleanupWarning = null) {
289
+ return { status, code, cleanupWarning };
290
+ }
291
+
292
+ function windowsPowerShellEnvironment(powershellExecutable, source = process.env) {
293
+ const environment = {};
294
+ for (const [key, value] of Object.entries(source ?? {})) {
295
+ if (key.toLowerCase() !== 'psmodulepath' && value !== undefined) environment[key] = value;
296
+ }
297
+ environment.PSModulePath = path.join(path.dirname(powershellExecutable), 'Modules');
298
+ return environment;
299
+ }
300
+
301
+ export async function runUsageValidation({ skillsRoot, timeoutSeconds = USAGE_VALIDATION_TIMEOUT_SECONDS }, dependencies = {}) {
302
+ const script = path.join(skillsRoot, 'answerme', 'scripts', 'test-answerme-installation.ps1');
303
+ const powershell = dependencies.powershellExecutable
304
+ ?? path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
305
+ if (timeoutSeconds !== USAGE_VALIDATION_TIMEOUT_SECONDS
306
+ || !(await pathExists(script))
307
+ || !(await validatePowerShell(powershell, dependencies))) {
308
+ return result('issue', 'usage-validation-runner-unavailable');
309
+ }
310
+ try {
311
+ const runner = dependencies.invokeProcess ?? invokeValidationProcess;
312
+ const executed = await runner(powershell, [
313
+ '-NoLogo',
314
+ '-NoProfile',
315
+ '-NonInteractive',
316
+ '-ExecutionPolicy', 'Bypass',
317
+ '-File', script,
318
+ '-ApiBaseUrl', OFFICIAL_API_ROOT,
319
+ '-WaitTimeoutSeconds', String(USAGE_VALIDATION_TIMEOUT_SECONDS),
320
+ ], {
321
+ timeoutMs: (USAGE_VALIDATION_TIMEOUT_SECONDS + 30) * 1000,
322
+ maxBuffer: MAX_BUFFER,
323
+ env: windowsPowerShellEnvironment(powershell, dependencies.environment ?? process.env),
324
+ }, dependencies);
325
+ const parsed = parseSingleJson(executed?.stdout ?? executed);
326
+ if (!parsed || !validateSelfTestOutput(parsed)) return result('issue', 'usage-validation-output-invalid');
327
+ const cleanupWarning = parsed.cleanupWarning ? 'cleanup-warning' : null;
328
+ if (parsed.status === 'passed' && parsed.ok === true) {
329
+ return result('passed', 'usage-validation-passed', cleanupWarning);
330
+ }
331
+ if (parsed.status === 'issue' && parsed.ok === false) {
332
+ return result('issue', 'usage-validation-issue', cleanupWarning);
333
+ }
334
+ return result('issue', 'usage-validation-output-invalid', cleanupWarning);
335
+ } catch (error) {
336
+ const timedOut = error?.code === 'ETIMEDOUT' || error?.killed === true || error?.signal === 'SIGTERM';
337
+ return result('issue', timedOut ? 'usage-validation-timeout' : 'usage-validation-issue');
338
+ }
339
+ }
340
+
341
+ export const __test = Object.freeze({
342
+ parseSingleJson,
343
+ parseEnrollmentEvent,
344
+ validateSelfTestOutput,
345
+ isExactOfficialApiRoot,
346
+ invokeValidationProcess,
347
+ validatePowerShell,
348
+ windowsPowerShellEnvironment,
349
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@rover-studio/answer-me",
3
+ "version": "0.1.0-rc.1",
4
+ "description": "Deterministic AnswerMe Toolkit installer for Codex on Windows x64",
5
+ "type": "module",
6
+ "bin": {
7
+ "answerme-toolkit": "bin/answerme-toolkit.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=20",
11
+ "npm": ">=7"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "license": "UNLICENSED"
17
+ }