cli-confirm-protocol 7.0.18
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/README.md +9 -0
- package/cli.mjs +17 -0
- package/installer.mjs +358 -0
- package/package.json +21 -0
- package/skill/SKILL.md +57 -0
- package/skill/skill.json +10 -0
package/README.md
ADDED
package/cli.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dispatchOfficialSkillCli, runIntakeHandshake } from './installer.mjs'
|
|
5
|
+
|
|
6
|
+
await dispatchOfficialSkillCli({
|
|
7
|
+
packageRoot: dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
runCommand: (context) => runIntakeHandshake(context, {
|
|
9
|
+
questions: [{
|
|
10
|
+
id: 'interaction',
|
|
11
|
+
prompt: 'Which user decision should be converted into a structured confirmation?',
|
|
12
|
+
required: true,
|
|
13
|
+
example: 'Confirm the approved document output path.',
|
|
14
|
+
}],
|
|
15
|
+
outputFile: 'CONFIRM-PROTOCOL-REQUIREMENTS.json',
|
|
16
|
+
}),
|
|
17
|
+
})
|
package/installer.mjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 八个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
|
|
3
|
+
* 禁止第二套超时、第二套版本来源、第二套 bin 名。
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import { constants, existsSync, readFileSync } from 'node:fs'
|
|
7
|
+
import { cp, lstat, mkdir, open, rm, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { dirname, join, resolve } from 'node:path'
|
|
9
|
+
import { stdin, stdout } from 'node:process'
|
|
10
|
+
import { createInterface } from 'node:readline/promises'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
export const LOOKUP_TIMEOUT_MS = 8000
|
|
14
|
+
export const CALL_TIMEOUT_MS = 120_000
|
|
15
|
+
const INSTALL_META = 'install-meta.json'
|
|
16
|
+
const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
|
|
17
|
+
const BRAIN_CLIENT_TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
|
|
18
|
+
const BRAIN_CLIENT_TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
|
|
19
|
+
const BRAIN_CLIENT_AUTH_SCHEME = 'BrainClient'
|
|
20
|
+
const BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES = 16_384
|
|
21
|
+
const BRAIN_CLIENT_TOKEN_FILE_MODE = 0o600
|
|
22
|
+
const FEEDBACK_COMMENT_MAX = 500
|
|
23
|
+
const FEEDBACK_SCORE_MIN = 0
|
|
24
|
+
const FEEDBACK_SCORE_MAX = 100
|
|
25
|
+
const BRAIN_CLIENT_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
|
|
26
|
+
const FEEDBACK_INVOCATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
27
|
+
const FEEDBACK_SCORE_PATTERN = /^(?:0|[1-9]\d{0,2})$/
|
|
28
|
+
|
|
29
|
+
function asObject(value, label) {
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
31
|
+
throw new Error(`${label} must be an object`)
|
|
32
|
+
}
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requiredString(value, label) {
|
|
37
|
+
const text = typeof value === 'string' ? value.trim() : ''
|
|
38
|
+
if (!text) throw new Error(`${label} is required`)
|
|
39
|
+
return text
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadOfficialSkillContext(packageRoot) {
|
|
43
|
+
const pkg = asObject(JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')), 'package.json')
|
|
44
|
+
const skill = asObject(JSON.parse(readFileSync(join(packageRoot, 'skill/skill.json'), 'utf8')), 'skill.json')
|
|
45
|
+
const npmName = requiredString(pkg.name, 'package.json name')
|
|
46
|
+
const packageVersion = requiredString(pkg.version, 'package.json version')
|
|
47
|
+
const displayName = requiredString(skill.displayName, 'skill.json displayName')
|
|
48
|
+
const skillName = requiredString(skill.name, 'skill.json name')
|
|
49
|
+
const schemaVersion = requiredString(skill.schemaVersion, 'skill.json schemaVersion')
|
|
50
|
+
const endpoint = requiredString(skill.endpoint, 'skill.json endpoint')
|
|
51
|
+
const skillVersion = requiredString(skill.version, 'skill.json version')
|
|
52
|
+
const runtimeCode = requiredString(endpoint.replace(/^https:\/\/cli\.tax\//, ''), 'runtime code')
|
|
53
|
+
if (!/^[A-Za-z0-9]{10}$/.test(runtimeCode)) {
|
|
54
|
+
throw new Error(`skill.json endpoint must be https://cli.tax/{10-char-code}: ${endpoint}`)
|
|
55
|
+
}
|
|
56
|
+
if (skillVersion.replace(/^v/i, '') !== packageVersion.replace(/^v/i, '')) {
|
|
57
|
+
throw new Error(`skill.json ${skillVersion} must match package.json ${packageVersion}`)
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
packageRoot,
|
|
61
|
+
npmName,
|
|
62
|
+
packageVersion,
|
|
63
|
+
displayName,
|
|
64
|
+
skillName,
|
|
65
|
+
schemaVersion,
|
|
66
|
+
endpoint,
|
|
67
|
+
skillVersion,
|
|
68
|
+
runtimeCode,
|
|
69
|
+
latestEndpoint: `https://cli.tax/api/public/skills/${runtimeCode}`,
|
|
70
|
+
skillDir: join(packageRoot, 'skill'),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readInstallMeta(target) {
|
|
75
|
+
const path = join(target, INSTALL_META)
|
|
76
|
+
if (!existsSync(path)) return null
|
|
77
|
+
return asObject(JSON.parse(readFileSync(path, 'utf8')), INSTALL_META)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function installTarget(skillName, explicit) {
|
|
81
|
+
if (explicit) return resolve(explicit)
|
|
82
|
+
const codexHome = process.env.CODEX_HOME?.trim()
|
|
83
|
+
if (codexHome) return join(codexHome, 'skills', skillName)
|
|
84
|
+
return join(process.cwd(), '.codex', 'skills', skillName)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function fetchLatestVersion(context) {
|
|
88
|
+
const response = await fetch(context.latestEndpoint, { signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) })
|
|
89
|
+
if (!response.ok) throw new Error(`cli.tax skill lookup failed: HTTP ${response.status}`)
|
|
90
|
+
const data = asObject(await response.json(), 'cli.tax skill lookup')
|
|
91
|
+
return {
|
|
92
|
+
version: requiredString(data.version, 'cli.tax skill lookup version'),
|
|
93
|
+
displayName: requiredString(data.displayName, 'cli.tax skill lookup displayName'),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function callOfficialSkill(context, operation, input) {
|
|
98
|
+
const requestId = `${context.npmName}-${Date.now()}`
|
|
99
|
+
const response = await fetch(context.endpoint, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
input: {
|
|
104
|
+
schemaVersion: context.schemaVersion,
|
|
105
|
+
requestId,
|
|
106
|
+
operation,
|
|
107
|
+
input,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
111
|
+
})
|
|
112
|
+
let payload
|
|
113
|
+
try {
|
|
114
|
+
payload = await response.json()
|
|
115
|
+
} catch {
|
|
116
|
+
throw new Error(`${context.displayName} ${operation} failed: non-JSON response (HTTP ${response.status}). Check ${context.endpoint}.`)
|
|
117
|
+
}
|
|
118
|
+
if (!response.ok || payload?.ok !== true) {
|
|
119
|
+
const message = payload?.error?.message
|
|
120
|
+
if (typeof message !== 'string' || !message.trim()) {
|
|
121
|
+
throw new Error(`${context.displayName} ${operation} failed: HTTP ${response.status}`)
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`${context.displayName} ${operation} failed: ${message}`)
|
|
124
|
+
}
|
|
125
|
+
return payload
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function feedbackCommandInput(args) {
|
|
129
|
+
const invocationId = requiredString(args[1], 'feedback invocation id')
|
|
130
|
+
if (!FEEDBACK_INVOCATION_PATTERN.test(invocationId)) {
|
|
131
|
+
throw new Error('feedback invocation id must be the UUID returned by a real skill response')
|
|
132
|
+
}
|
|
133
|
+
const scoreText = requiredString(args[2], 'feedback score')
|
|
134
|
+
if (!FEEDBACK_SCORE_PATTERN.test(scoreText)) {
|
|
135
|
+
throw new Error(`feedback score must be an integer between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
|
|
136
|
+
}
|
|
137
|
+
const score = Number(scoreText)
|
|
138
|
+
if (!Number.isInteger(score) || score < FEEDBACK_SCORE_MIN || score > FEEDBACK_SCORE_MAX) {
|
|
139
|
+
throw new Error(`feedback score must be between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
|
|
140
|
+
}
|
|
141
|
+
const userComment = args.slice(3).join(' ').trim()
|
|
142
|
+
if (!userComment) throw new Error('feedback comment is required')
|
|
143
|
+
if (userComment.length > FEEDBACK_COMMENT_MAX) {
|
|
144
|
+
throw new Error(`feedback comment must be at most ${FEEDBACK_COMMENT_MAX} characters`)
|
|
145
|
+
}
|
|
146
|
+
return { invocationId, score, userComment }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function brainClientAuthorization(context, environment) {
|
|
150
|
+
const configuredPath = typeof environment[BRAIN_CLIENT_TOKEN_FILE_ENV] === 'string'
|
|
151
|
+
? environment[BRAIN_CLIENT_TOKEN_FILE_ENV].trim() : ''
|
|
152
|
+
if (!configuredPath) throw new Error(`${BRAIN_CLIENT_TOKEN_FILE_ENV} is required`)
|
|
153
|
+
if (process.platform === 'win32' || typeof process.getuid !== 'function') {
|
|
154
|
+
throw new Error('Brain Client token file ownership cannot be verified')
|
|
155
|
+
}
|
|
156
|
+
const tokenFilePath = resolve(configuredPath)
|
|
157
|
+
const linkStatus = await lstat(tokenFilePath)
|
|
158
|
+
if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
|
|
159
|
+
const handle = await open(tokenFilePath, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
160
|
+
try {
|
|
161
|
+
const status = await handle.stat()
|
|
162
|
+
if (!status.isFile() || status.uid !== process.getuid()
|
|
163
|
+
|| (status.mode & 0o777) !== BRAIN_CLIENT_TOKEN_FILE_MODE
|
|
164
|
+
|| status.size < 1 || status.size > BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES) {
|
|
165
|
+
throw new Error('Brain Client token file must be owned by the current user with mode 0600')
|
|
166
|
+
}
|
|
167
|
+
const tokenFile = asObject(JSON.parse(await handle.readFile('utf8')), 'Brain Client token file')
|
|
168
|
+
const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
|
|
169
|
+
if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
|
|
170
|
+
throw new Error('Brain Client token file contains unknown or missing fields')
|
|
171
|
+
}
|
|
172
|
+
const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
|
|
173
|
+
if (tokenFile.schemaVersion !== BRAIN_CLIENT_TOKEN_FILE_VERSION
|
|
174
|
+
|| tokenFile.authorizationScheme !== BRAIN_CLIENT_AUTH_SCHEME
|
|
175
|
+
|| endpoint.origin !== new URL(context.endpoint).origin
|
|
176
|
+
|| endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
|
|
177
|
+
|| endpoint.username || endpoint.password
|
|
178
|
+
|| !BRAIN_CLIENT_TOKEN_PATTERN.test(tokenFile.token)) {
|
|
179
|
+
throw new Error('Brain Client token file authority is invalid')
|
|
180
|
+
}
|
|
181
|
+
return `${BRAIN_CLIENT_AUTH_SCHEME} ${tokenFile.token}`
|
|
182
|
+
} finally {
|
|
183
|
+
await handle.close()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function submitOfficialSkillFeedback(context, args, environment, request) {
|
|
188
|
+
const input = feedbackCommandInput(args)
|
|
189
|
+
const authorization = await brainClientAuthorization(context, environment)
|
|
190
|
+
const requestId = `${context.runtimeCode}-${randomUUID()}`
|
|
191
|
+
let response
|
|
192
|
+
try {
|
|
193
|
+
response = await request(new URL(FEEDBACK_API_PATH, context.endpoint), {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
Authorization: authorization,
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
requestId,
|
|
201
|
+
skillId: context.runtimeCode,
|
|
202
|
+
invocationId: input.invocationId,
|
|
203
|
+
score: input.score,
|
|
204
|
+
userComment: input.userComment,
|
|
205
|
+
}),
|
|
206
|
+
signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
|
|
207
|
+
})
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error('cli.tax feedback request failed')
|
|
210
|
+
}
|
|
211
|
+
let payload
|
|
212
|
+
try {
|
|
213
|
+
payload = asObject(await response.json(), 'cli.tax feedback response')
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof Error && error.message.startsWith('cli.tax feedback response')) throw error
|
|
216
|
+
throw new Error(`cli.tax feedback failed: non-JSON response (HTTP ${response.status})`)
|
|
217
|
+
}
|
|
218
|
+
if (!response.ok || payload.ok !== true) {
|
|
219
|
+
throw new Error(`cli.tax feedback failed: HTTP ${response.status}`)
|
|
220
|
+
}
|
|
221
|
+
if (payload.requestId !== requestId || typeof payload.id !== 'string'
|
|
222
|
+
|| !FEEDBACK_INVOCATION_PATTERN.test(payload.id)
|
|
223
|
+
|| typeof payload.duplicated !== 'boolean') {
|
|
224
|
+
throw new Error('cli.tax feedback response authority is invalid')
|
|
225
|
+
}
|
|
226
|
+
return { id: payload.id, requestId, duplicated: payload.duplicated }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function installOfficialSkill(context, explicit) {
|
|
230
|
+
const target = installTarget(context.skillName, explicit)
|
|
231
|
+
await mkdir(target, { recursive: true })
|
|
232
|
+
const previous = readInstallMeta(target)
|
|
233
|
+
await rm(join(target, 'references'), { recursive: true, force: true })
|
|
234
|
+
await cp(context.skillDir, target, { recursive: true, force: true })
|
|
235
|
+
const installed = asObject(JSON.parse(readFileSync(join(target, 'skill.json'), 'utf8')), 'installed skill.json')
|
|
236
|
+
const installedVersion = requiredString(installed.version, 'installed skill.json version')
|
|
237
|
+
await writeFile(join(target, INSTALL_META), `${JSON.stringify({
|
|
238
|
+
source: context.runtimeCode,
|
|
239
|
+
slug: context.skillName,
|
|
240
|
+
version: installedVersion,
|
|
241
|
+
packageVersion: context.packageVersion,
|
|
242
|
+
endpoint: context.endpoint,
|
|
243
|
+
installedAt: new Date().toISOString(),
|
|
244
|
+
}, null, 2)}\n`)
|
|
245
|
+
if (previous?.version && previous.version !== installedVersion) {
|
|
246
|
+
console.log(`${context.displayName} skill updated: ${target}`)
|
|
247
|
+
console.log(` ${previous.version} → ${installedVersion}`)
|
|
248
|
+
} else {
|
|
249
|
+
console.log(`${context.displayName} skill installed: ${target} (${installedVersion})`)
|
|
250
|
+
}
|
|
251
|
+
console.log('Next: return to your IDE and state the goal. The agent reads the installed SKILL.md.')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function checkOfficialSkill(context, explicit) {
|
|
255
|
+
const target = installTarget(context.skillName, explicit)
|
|
256
|
+
const current = readInstallMeta(target)
|
|
257
|
+
if (!current) {
|
|
258
|
+
console.log(`${context.displayName} skill is not installed. Run: npx ${context.npmName}@latest install`)
|
|
259
|
+
process.exitCode = 1
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
const installedVersion = requiredString(current.version, 'install-meta.json version')
|
|
263
|
+
const packageVersion = requiredString(current.packageVersion, 'install-meta.json packageVersion')
|
|
264
|
+
console.log(`Installed: ${installedVersion} (package ${packageVersion})`)
|
|
265
|
+
const latest = await fetchLatestVersion(context)
|
|
266
|
+
console.log(`Latest on cli.tax: ${latest.version}`)
|
|
267
|
+
if (installedVersion === latest.version) {
|
|
268
|
+
console.log('Up to date.')
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
console.log(`Update available: ${installedVersion} → ${latest.version}`)
|
|
272
|
+
console.log(`Run: npx ${context.npmName}@latest install`)
|
|
273
|
+
process.exitCode = 1
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function defaultUsage(context, extraLines) {
|
|
277
|
+
const lines = [
|
|
278
|
+
`${context.npmName} — install and run the ${context.displayName} skill from CLI.Tax`,
|
|
279
|
+
'',
|
|
280
|
+
'Usage:',
|
|
281
|
+
` npx ${context.npmName}@latest install [directory]`,
|
|
282
|
+
` Install the ${context.displayName} skill for the current IDE.`,
|
|
283
|
+
` npx ${context.npmName}@latest check [directory]`,
|
|
284
|
+
' Check whether the installed skill has a newer version.',
|
|
285
|
+
` npx ${context.npmName}@latest run`,
|
|
286
|
+
' Run the skill handshake: discover capabilities and collect intake answers.',
|
|
287
|
+
`Endpoint: ${context.endpoint}`,
|
|
288
|
+
]
|
|
289
|
+
if (extraLines?.length) lines.push('', ...extraLines)
|
|
290
|
+
return lines.join('\n')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function runIntakeHandshake(context, spec) {
|
|
294
|
+
const capabilities = await callOfficialSkill(context, 'capabilities', {})
|
|
295
|
+
const output = capabilities.output && typeof capabilities.output === 'object' ? capabilities.output : {}
|
|
296
|
+
const skill = output.skill && typeof output.skill === 'object' ? output.skill : {}
|
|
297
|
+
const version = typeof skill.version === 'string' && skill.version.trim()
|
|
298
|
+
? skill.version.trim()
|
|
299
|
+
: context.skillVersion
|
|
300
|
+
console.log(`${context.displayName} ${version}`)
|
|
301
|
+
if (typeof spec.afterCapabilities === 'function') spec.afterCapabilities(output)
|
|
302
|
+
const readline = createInterface({ input: stdin, output: stdout })
|
|
303
|
+
const answers = []
|
|
304
|
+
try {
|
|
305
|
+
for (const question of spec.questions) {
|
|
306
|
+
const requiredMark = question.required ? ' (required)' : ''
|
|
307
|
+
console.log(`\n${question.prompt}${requiredMark}`)
|
|
308
|
+
console.log(`Example: ${question.example}`)
|
|
309
|
+
for (;;) {
|
|
310
|
+
const answer = (await readline.question('> ')).trim()
|
|
311
|
+
if (answer) {
|
|
312
|
+
answers.push({ id: question.id, prompt: question.prompt, answer })
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
if (!question.required) break
|
|
316
|
+
console.log('This question is required. Please answer before continuing.')
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} finally {
|
|
320
|
+
readline.close()
|
|
321
|
+
}
|
|
322
|
+
const target = join(process.cwd(), spec.outputFile)
|
|
323
|
+
await writeFile(target, `${JSON.stringify({
|
|
324
|
+
schemaVersion: context.schemaVersion,
|
|
325
|
+
endpoint: context.endpoint,
|
|
326
|
+
createdAt: new Date().toISOString(),
|
|
327
|
+
answers,
|
|
328
|
+
}, null, 2)}\n`)
|
|
329
|
+
console.log(`\nRequirements saved: ${target}`)
|
|
330
|
+
console.log('Next: continue in your IDE agent with this file.')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function dispatchOfficialSkillCli(options) {
|
|
334
|
+
const packageRoot = options.packageRoot ?? dirname(fileURLToPath(options.importMetaUrl))
|
|
335
|
+
const context = loadOfficialSkillContext(packageRoot)
|
|
336
|
+
const args = process.argv.slice(2)
|
|
337
|
+
const command = args[0] ?? 'help'
|
|
338
|
+
const argument = args[1]
|
|
339
|
+
try {
|
|
340
|
+
if (command === 'install') await installOfficialSkill(context, argument)
|
|
341
|
+
else if (command === 'check') await checkOfficialSkill(context, argument)
|
|
342
|
+
else if (command === 'run') await options.runCommand(context)
|
|
343
|
+
else if (command === 'feedback') {
|
|
344
|
+
const receipt = await submitOfficialSkillFeedback(context, args, process.env, fetch)
|
|
345
|
+
console.log(`${context.displayName} feedback accepted: ${receipt.id}`)
|
|
346
|
+
}
|
|
347
|
+
else if (command === 'help' || command === '--help' || command === '-h') {
|
|
348
|
+
console.log(options.usage ? options.usage(context) : defaultUsage(context, options.extraUsageLines))
|
|
349
|
+
} else {
|
|
350
|
+
console.error(`Unknown command: ${command}`)
|
|
351
|
+
console.log(options.usage ? options.usage(context) : defaultUsage(context, options.extraUsageLines))
|
|
352
|
+
process.exitCode = 1
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(error instanceof Error ? error.message : error)
|
|
356
|
+
process.exitCode = 1
|
|
357
|
+
}
|
|
358
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"cli-confirm-protocol": "./cli.mjs"
|
|
4
|
+
},
|
|
5
|
+
"description": "Confirm Protocol skill installer for CLI.Tax: structured confirmation requests, answers, memory, batching, and audit.",
|
|
6
|
+
"files": [
|
|
7
|
+
"cli.mjs",
|
|
8
|
+
"installer.mjs",
|
|
9
|
+
"README.md",
|
|
10
|
+
"skill/SKILL.md",
|
|
11
|
+
"skill/skill.json"
|
|
12
|
+
],
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"name": "cli-confirm-protocol",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://gitee.com/Alyr_space/CLITax.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"version": "7.0.18"
|
|
21
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: confirm-protocol
|
|
3
|
+
description: '把技能需要的用户确认转换为统一 interaction 协议,并返回结构化答案、聊天降级文本、低风险记忆状态、批次和审计记录。用于确认、单选、多选或输入交互;不用于普通聊天,也不代替客户端 UI。'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Confirm Protocol
|
|
7
|
+
|
|
8
|
+
Package version: v7.0.18
|
|
9
|
+
|
|
10
|
+
Endpoint: https://cli.tax/Cf8Pr7Tm2Q
|
|
11
|
+
|
|
12
|
+
Request schema: `confirm-protocol.skill.request/1.0`
|
|
13
|
+
|
|
14
|
+
Confirm Protocol 是技能链的统一确认交互层。它只定义协议并验证答案,不替代业务技能,也不把“等待用户”伪装成成功。
|
|
15
|
+
|
|
16
|
+
## 强制流程
|
|
17
|
+
|
|
18
|
+
1. 调用 `capabilities`,读取全部 `operationSchemas` 与真实能力状态。
|
|
19
|
+
2. 业务技能构造 `confirm.interaction/1.0`,调用 `interaction-request`。
|
|
20
|
+
3. 客户端优先用 IDE 原生 UI;没有适配器时必须显示返回的 `chatFallback`。
|
|
21
|
+
4. 用户作答后调用 `interaction-answer`,得到不可歧义的 `callbackRequest` 和审计记录。
|
|
22
|
+
5. 只有 `risk=low + rememberable=true` 才能调用 `memory-set`。高风险永远不可记忆、不可批量、不可默认超时放行。
|
|
23
|
+
|
|
24
|
+
## 操作
|
|
25
|
+
|
|
26
|
+
- `capabilities` / `help`:能力、JSON Schema 与实现边界。
|
|
27
|
+
- `interaction-request`:验证并返回 interaction 与聊天降级文本。
|
|
28
|
+
- `interaction-answer`:验证答案,生成 callback 请求和审计记录。
|
|
29
|
+
- `chat-render`:把同一 interaction 渲染为编号聊天文本。
|
|
30
|
+
- `memory-set` / `memory-list` / `memory-clear`:调用方持有的低风险记忆状态。
|
|
31
|
+
- `batch-request`:每批最多三个低风险确认;高风险始终独立。
|
|
32
|
+
- `audit-query`:查询调用方提供的审计记录。
|
|
33
|
+
|
|
34
|
+
## 风险规则
|
|
35
|
+
|
|
36
|
+
- `risk=high` 必须带非空风险说明、`rememberable=false`、`timeoutAction=wait`。
|
|
37
|
+
- `confirm` / `choice` 只能返回一个合法 option id;`multi` 返回去重后的 id 数组;`input` 返回非空文本。
|
|
38
|
+
- callback 的 operation 和原 payload 由请求方声明;答案只能追加到副本,不能篡改原 interaction。
|
|
39
|
+
- 记忆与审计状态由已认证客户端或平台持久化。纯运行时是无状态协议层,不宣称已经写入数据库。
|
|
40
|
+
|
|
41
|
+
## 实现状态
|
|
42
|
+
|
|
43
|
+
| 能力 | 状态 | 边界 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| interaction 请求/回答闭环 | 已实现 | 返回统一 callbackRequest 与 auditEntry |
|
|
46
|
+
| C 档聊天渲染 | 已实现 | 同一协议生成编号文本 |
|
|
47
|
+
| 低风险记忆/批量/审计 | 已实现(调用方持有) | 运行时返回更新后的状态,不伪造平台持久化 |
|
|
48
|
+
| IDE 原生适配器 | local notifier required | 由具体 IDE 插件渲染 |
|
|
49
|
+
| OS 原生 Confirm Notifier | local notifier required | 当前不宣称已弹出系统窗口 |
|
|
50
|
+
| 疲劳检测/移动端转发 | 规划中 | 不进入首版通过证据 |
|
|
51
|
+
|
|
52
|
+
## 反馈通道
|
|
53
|
+
|
|
54
|
+
评价只能由 IDE / 智能体对一次真实成功调用自动回传。人类不得选择分数或填写技能评价;日常交流不计入评价。
|
|
55
|
+
真实成功响应顶层包含 `feedbackInvocationId`;自动评价必须携带该标识,禁止自行生成调用记录或复用其他调用的标识。
|
|
56
|
+
自动回传命令为 `npx cli-confirm-protocol@latest feedback <feedbackInvocationId> <0-100整数分> <真实评语>`;不得向人类展示评分控件。
|
|
57
|
+
身份凭证只从 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 指向的本地受限文件读取,评价按会员账号实名入库。禁止伪造、猜测、复用任何调用标识,禁止让人类选择分数;任何 HTTP 失败都必须视为回传失败。
|
package/skill/skill.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "confirm-protocol",
|
|
3
|
+
"displayName": "Confirm Protocol",
|
|
4
|
+
"description": "统一结构化确认协议:确认请求、结构化答案、聊天渲染、低风险记忆、批量与审计。",
|
|
5
|
+
"schemaVersion": "confirm-protocol.skill.request/1.0",
|
|
6
|
+
"endpoint": "https://cli.tax/Cf8Pr7Tm2Q",
|
|
7
|
+
"method": "POST",
|
|
8
|
+
"version": "v7.0.18",
|
|
9
|
+
"type": "Skill"
|
|
10
|
+
}
|