cli-aimlock 7.0.37 → 7.0.39

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/broker.mjs CHANGED
@@ -1,25 +1,17 @@
1
1
  import { createHash, randomUUID } from 'node:crypto'
2
- import { constants } from 'node:fs'
3
- import { lstat, open } from 'node:fs/promises'
4
- import { resolve, win32 } from 'node:path'
2
+ import { brainClientAuthorization } from './broker-credentials.mjs'
3
+ import { prepareOfficialSkillUse, withUpgradeMetadata } from './official-skill-update.mjs'
4
+ export { brainClientAuthorization, brainClientTokenPath } from './broker-credentials.mjs'
5
+ export { LOOKUP_TIMEOUT_MS } from './official-skill-update.mjs'
5
6
  import { OfficialSkillInvocationError, OfficialSkillResponseError, transportFailureCode, transportDiagnostics } from './broker-failures.mjs'
6
7
  import { queryOfficialSkillReceipt, SKILL_RECEIPT_HEADER, SKILL_RECEIPT_SCHEMA } from './broker-recovery.mjs'
7
8
  export { officialSkillFailureResponse, transportFailureCode } from './broker-failures.mjs'
8
9
 
9
- export const LOOKUP_TIMEOUT_MS = 8000
10
10
  export const CALL_TIMEOUT_MS = 120_000
11
- const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
12
- const TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
13
- const TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
14
- const AUTH_SCHEME = 'BrainClient'
15
- const TOKEN_FILE_MAX_BYTES = 16_384
16
- const POSIX_TOKEN_FILE_MODE = 0o600
17
- const WINDOWS_BROKER_DIRECTORY = ['CLI.Tax', 'broker']
18
11
  const FEEDBACK_COMMENT_MAX = 500
19
12
  const EVALUATION_DURATION_MAX = 86_400_000
20
13
  const SCORE_MIN = 0
21
14
  const SCORE_MAX = 100
22
- const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
23
15
  const 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
24
16
  const DIGEST_PATTERN = /^[0-9a-f]{64}$/
25
17
  const PROTOCOL_STATUSES = new Set(['succeeded', 'blocked', 'failed'])
@@ -48,84 +40,6 @@ function boundedInteger(value, label) {
48
40
  return value
49
41
  }
50
42
 
51
- function insideWindowsDirectory(candidate, directory) {
52
- const relative = win32.relative(directory, candidate)
53
- return relative === '' || (!relative.startsWith('..\\') && relative !== '..' && !win32.isAbsolute(relative))
54
- }
55
-
56
- export function brainClientTokenPath(environment, platform = process.platform) {
57
- const configured = requiredString(environment[TOKEN_FILE_ENV], TOKEN_FILE_ENV)
58
- if (platform !== 'win32') return resolve(configured)
59
- if (!win32.isAbsolute(configured)) {
60
- throw new Error('Windows Brain Client token file path must be absolute')
61
- }
62
- const localAppData = requiredString(environment.LOCALAPPDATA, 'LOCALAPPDATA')
63
- const brokerDirectory = win32.resolve(localAppData, ...WINDOWS_BROKER_DIRECTORY)
64
- const candidate = win32.resolve(configured)
65
- if (!insideWindowsDirectory(candidate, brokerDirectory)) {
66
- throw new Error(`Windows Brain Client token file must be inside ${brokerDirectory}`)
67
- }
68
- return candidate
69
- }
70
-
71
- function assertTokenFileStatus(status, platform, currentUserId) {
72
- if (!status.isFile() || status.size < 1 || status.size > TOKEN_FILE_MAX_BYTES) {
73
- throw new Error('Brain Client token file must be a non-empty restricted file')
74
- }
75
- if (platform === 'win32') return
76
- if (!Number.isInteger(currentUserId)) {
77
- throw new Error('Brain Client token file ownership cannot be verified')
78
- }
79
- if (status.uid !== currentUserId || (status.mode & 0o777) !== POSIX_TOKEN_FILE_MODE) {
80
- throw new Error('Brain Client token file must be owned by the current user with mode 0600')
81
- }
82
- }
83
-
84
- function parseTokenFile(source) {
85
- let tokenFile
86
- try {
87
- tokenFile = asObject(JSON.parse(source), 'Brain Client token file')
88
- } catch {
89
- throw new Error('Brain Client token file must contain valid JSON')
90
- }
91
- const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
92
- if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
93
- throw new Error('Brain Client token file contains unknown or missing fields')
94
- }
95
- return tokenFile
96
- }
97
-
98
- export async function brainClientAuthorization(context, environment, dependencies = {}) {
99
- const platform = dependencies.platform ?? process.platform
100
- const tokenFilePath = brainClientTokenPath(environment, platform)
101
- const inspectPath = dependencies.lstat ?? lstat
102
- const openPath = dependencies.open ?? open
103
- const currentUserId = platform === 'win32'
104
- ? null
105
- : (dependencies.getuid ?? process.getuid)?.()
106
- const linkStatus = await inspectPath(tokenFilePath)
107
- if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
108
- const noFollow = platform === 'win32' ? 0 : (constants.O_NOFOLLOW ?? 0)
109
- const handle = await openPath(tokenFilePath, constants.O_RDONLY | noFollow)
110
- try {
111
- const status = await handle.stat()
112
- assertTokenFileStatus(status, platform, currentUserId)
113
- const tokenFile = parseTokenFile(await handle.readFile('utf8'))
114
- const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
115
- if (tokenFile.schemaVersion !== TOKEN_FILE_VERSION
116
- || tokenFile.authorizationScheme !== AUTH_SCHEME
117
- || endpoint.origin !== new URL(context.endpoint).origin
118
- || endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
119
- || endpoint.username || endpoint.password
120
- || !TOKEN_PATTERN.test(tokenFile.token)) {
121
- throw new Error('Brain Client token file authority is invalid')
122
- }
123
- return `${AUTH_SCHEME} ${tokenFile.token}`
124
- } finally {
125
- await handle.close()
126
- }
127
- }
128
-
129
43
  function canonicalJson(value) {
130
44
  if (value === null || typeof value === 'string' || typeof value === 'boolean') {
131
45
  return JSON.stringify(value)
@@ -205,6 +119,9 @@ export function authoritativeEvaluation(value, expected) {
205
119
 
206
120
  async function responsePayload(response, context, request) {
207
121
  const label = `${context.displayName} ${request.operation} response`
122
+ if (response.status === 401 || response.status === 403) {
123
+ throw new OfficialSkillResponseError(request, 'http-response', 'Brain Client authorization was rejected; copy the current authenticated setup from CLI.Tax and run configure again. Revoked credentials cannot renew themselves.')
124
+ }
208
125
  let payload
209
126
  try {
210
127
  payload = await response.json()
@@ -235,6 +152,9 @@ function invocationRequest(context, operation, input) {
235
152
  }
236
153
 
237
154
  export async function invokeOfficialSkill(context, operation, input, dependencies) {
155
+ const prepared = await prepareOfficialSkillUse(context, 'broker.mjs', dependencies)
156
+ if (prepared.module !== null) return withUpgradeMetadata(
157
+ await prepared.module.invokeOfficialSkill(prepared.context, operation, input, dependencies), prepared.upgrade)
238
158
  const environment = asObject(dependencies.environment, 'broker environment')
239
159
  if (typeof dependencies.request !== 'function') {
240
160
  throw new Error('broker request dependency is required')
@@ -252,14 +172,14 @@ export async function invokeOfficialSkill(context, operation, input, dependencie
252
172
  })
253
173
  } catch (error) {
254
174
  const failure = new OfficialSkillInvocationError(context, requestEnvelope, transportFailureCode(error), 'request', error?.transport)
255
- return recoverTransportFailure(context, requestEnvelope, dependencies, authorization, failure)
175
+ return withUpgradeMetadata(await recoverTransportFailure(context, requestEnvelope, dependencies, authorization, failure), prepared.upgrade)
256
176
  }
257
177
  let payload
258
178
  try {
259
179
  payload = await responsePayload(response, context, requestEnvelope)
260
180
  } catch (error) {
261
181
  if (!(error instanceof OfficialSkillInvocationError)) throw error
262
- return recoverTransportFailure(context, requestEnvelope, dependencies, authorization, error)
182
+ return withUpgradeMetadata(await recoverTransportFailure(context, requestEnvelope, dependencies, authorization, error), prepared.upgrade)
263
183
  }
264
184
  if (!response.ok || payload.ok !== true) {
265
185
  throw new OfficialSkillResponseError(requestEnvelope, 'http-response', `${context.displayName} ${operation} failed: HTTP ${response.status}`)
@@ -267,7 +187,7 @@ export async function invokeOfficialSkill(context, operation, input, dependencie
267
187
  try {
268
188
  const invocation = validateInvocationResponse(context, operation, payload, requestEnvelope)
269
189
  const transport = transportDiagnostics(response.transport)
270
- return transport ? { ...invocation, transport } : invocation
190
+ return withUpgradeMetadata(transport ? { ...invocation, transport } : invocation, prepared.upgrade)
271
191
  } catch (error) {
272
192
  throw new OfficialSkillResponseError(requestEnvelope, 'response-validation',
273
193
  error instanceof Error ? error.message : 'Skill response validation failed')
package/cli.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { TASKS_USAGE, runTasksCli } from './aimlock-tasks-cli.mjs'
2
3
  import { realpathSync } from 'node:fs'
3
4
  import { dirname, resolve } from 'node:path'
4
5
  import { cwd, stdin, stdout } from 'node:process'
@@ -95,7 +96,7 @@ export function localAimlockApplicability(facts) {
95
96
  export function aimlockUsage(context) {
96
97
  const usage = defaultUsage(context)
97
98
  if (!usage.includes(COMMON_RUN_USAGE)) throw new Error('Shared CLI run usage contract changed')
98
- return usage.replace(COMMON_RUN_USAGE, AIMLOCK_RUN_USAGE) + '\n\n' + CHAIN_USAGE
99
+ return usage.replace(COMMON_RUN_USAGE, AIMLOCK_RUN_USAGE) + '\n\n' + CHAIN_USAGE + '\n\n' + TASKS_USAGE
99
100
  + '\n\nRead-time renewal: local budget-auto-renew-request <repositoryRoot> prepares one Confirm Protocol approval;'
100
101
  + '\nlocal budget-auto-renew activates the approved chain/scope/policy; budget-auto-renew-stop revokes or completes it.'
101
102
  }
@@ -196,6 +197,8 @@ const cliPath = fileURLToPath(import.meta.url)
196
197
  if (process.argv[1] && realpathSync(resolve(process.argv[1])) === cliPath) {
197
198
  if (process.argv[2] === 'brain') {
198
199
  await runBrainCli(process.argv.slice(3))
200
+ } else if (process.argv[2] === 'tasks') {
201
+ await runTasksCli(process.argv.slice(3))
199
202
  } else if (process.argv[2] === 'chain') {
200
203
  await runChainCli(process.argv.slice(3))
201
204
  } else if (process.argv[2] === 'local') {
@@ -0,0 +1,205 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { existsSync, readFileSync } from 'node:fs'
4
+ import { cp, lstat, mkdir, readdir, rename, rm, writeFile } from 'node:fs/promises'
5
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
6
+ import { promisify } from 'node:util'
7
+ import { assertAccountAncestors, currentAccountHome } from './broker-account-storage.mjs'
8
+
9
+ const runFile = promisify(execFile)
10
+ const INSTALL_META = 'install-meta.json'
11
+ const MAX_SKILL_FILES = 256
12
+ const GIT_TIMEOUT_MS = 8000
13
+
14
+ function object(value, label) {
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(label + ' must be an object')
16
+ return value
17
+ }
18
+
19
+ export function readInstallMeta(target) {
20
+ const path = join(target, INSTALL_META)
21
+ if (!existsSync(path)) return null
22
+ return object(JSON.parse(readFileSync(path, 'utf8')), INSTALL_META)
23
+ }
24
+
25
+ export function installTarget(skillName, explicit, environment = process.env, workingDirectory = process.cwd()) {
26
+ if (explicit !== undefined) {
27
+ if (typeof explicit !== 'string' || !explicit.trim()) throw new Error('Install directory must be non-empty')
28
+ return resolve(explicit)
29
+ }
30
+ if (environment.CODEX_HOME !== undefined) {
31
+ if (typeof environment.CODEX_HOME !== 'string' || !isAbsolute(environment.CODEX_HOME)) throw new Error('CODEX_HOME must be absolute')
32
+ return join(environment.CODEX_HOME, 'skills', skillName)
33
+ }
34
+ return join(workingDirectory, '.codex', 'skills', skillName)
35
+ }
36
+
37
+ function contained(root, path) {
38
+ const inside = relative(root, path)
39
+ return inside === '' || (!inside.startsWith('..' + '/') && inside !== '..' && !isAbsolute(inside)
40
+ && !inside.startsWith('..' + '\\'))
41
+ }
42
+
43
+ function sameIdentity(meta, context) {
44
+ return meta.source === context.runtimeCode && meta.slug === context.skillName && meta.endpoint === context.endpoint
45
+ && typeof meta.version === 'string' && /^v?\d+\.\d+\.\d+$/.test(meta.version)
46
+ && typeof meta.packageVersion === 'string' && /^\d+\.\d+\.\d+$/.test(meta.packageVersion)
47
+ }
48
+
49
+ function newerVersion(installed, selected) {
50
+ const parts = version => {
51
+ if (typeof version !== 'string' || !/^v?\d+\.\d+\.\d+$/.test(version)) {
52
+ throw new Error('Managed skill comparison requires a semantic release version')
53
+ }
54
+ return version.replace(/^v/, '').split('.').map(value => BigInt(value))
55
+ }
56
+ const left = parts(installed), right = parts(selected)
57
+ const index = left.findIndex((part, position) => part !== right[position])
58
+ return index !== -1 && left[index] > right[index]
59
+ }
60
+
61
+ function assertNoManagedDowngrade(previous, context) {
62
+ if (previous !== null && (newerVersion(previous.version, context.skillVersion)
63
+ || newerVersion(previous.packageVersion, context.packageVersion))) {
64
+ throw new Error('The managed skill is newer than the selected release; refusing to downgrade its installation')
65
+ }
66
+ }
67
+
68
+ async function targetExists(path) {
69
+ try { return await lstat(path) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
70
+ }
71
+
72
+ async function trackedSource(target) {
73
+ let current = dirname(target)
74
+ for (;;) {
75
+ if (await targetExists(join(current, '.git')) !== null) {
76
+ try {
77
+ const result = await runFile('git', ['-C', current, 'ls-files', '--', relative(current, target)],
78
+ { timeout: GIT_TIMEOUT_MS, maxBuffer: 1_048_576 })
79
+ return result.stdout.trim().length > 0
80
+ } catch { throw new Error('Cannot verify whether the install target contains repository source files') }
81
+ }
82
+ const parent = dirname(current)
83
+ if (parent === current) return false
84
+ current = parent
85
+ }
86
+ }
87
+
88
+ async function managedTarget(context, target, allowCreate) {
89
+ const path = resolve(target)
90
+ await assertAccountAncestors(dirname(path))
91
+ if (contained(resolve(context.packageRoot), path) || contained(path, resolve(context.packageRoot))) {
92
+ return { path, status: 'skipped', reason: 'package-source' }
93
+ }
94
+ const status = await targetExists(path)
95
+ if (status === null) return allowCreate ? { path, status: 'new', meta: null }
96
+ : { path, status: 'skipped', reason: 'not-installed' }
97
+ if (!status.isDirectory() || status.isSymbolicLink()) throw new Error('Installed skill target must be a regular directory')
98
+ if (await trackedSource(path)) return { path, status: 'skipped', reason: 'repository-source' }
99
+ const metaStatus = await targetExists(join(path, INSTALL_META))
100
+ if (metaStatus === null) return { path, status: 'skipped', reason: 'unmanaged' }
101
+ if (!metaStatus.isFile() || metaStatus.isSymbolicLink()) throw new Error('Install metadata must be a regular file')
102
+ if (process.platform !== 'win32' && (status.uid !== process.getuid() || metaStatus.uid !== process.getuid())) throw new Error('Managed skill installation must belong to the current account')
103
+ const meta = readInstallMeta(path)
104
+ if (!sameIdentity(meta, context)) return { path, status: 'skipped', reason: 'identity-mismatch' }
105
+ return { path, status: 'managed', meta }
106
+ }
107
+
108
+ async function verifySkillSource(directory, context) {
109
+ let files = 0
110
+ async function visit(path) {
111
+ const status = await lstat(path)
112
+ if (status.isSymbolicLink()) throw new Error('Skill package cannot contain symbolic links')
113
+ if (status.isDirectory()) {
114
+ for (const entry of await readdir(path)) await visit(join(path, entry))
115
+ return
116
+ }
117
+ if (!status.isFile() || ++files > MAX_SKILL_FILES) throw new Error('Skill package contains an unsupported or excessive file set')
118
+ }
119
+ await assertAccountAncestors(directory)
120
+ await visit(directory)
121
+ const skill = object(JSON.parse(readFileSync(join(directory, 'skill.json'), 'utf8')), 'skill.json')
122
+ if (skill.name !== context.skillName || skill.endpoint !== context.endpoint || skill.version !== context.skillVersion) {
123
+ throw new Error('Skill package documentation identity does not match the selected version')
124
+ }
125
+ const markdown = await lstat(join(directory, 'SKILL.md'))
126
+ if (!markdown.isFile() || markdown.size === 0) throw new Error('Skill package must contain its actual SKILL.md')
127
+ }
128
+
129
+ async function restoreInstallation(target, backup, placed, renamePath) {
130
+ if (placed) await rm(target, { recursive: true })
131
+ if (backup !== null) await renamePath(backup, target)
132
+ }
133
+
134
+ async function replaceInstallation(context, target, previous, dependencies) {
135
+ const renamePath = dependencies.rename === undefined ? rename : dependencies.rename
136
+ const suffix = randomUUID()
137
+ const stage = join(dirname(target), '.' + context.skillName + '.stage-' + suffix)
138
+ const backup = previous === null ? null : join(dirname(target), '.' + context.skillName + '.backup-' + suffix)
139
+ let moved = false, placed = false
140
+ await mkdir(stage, { mode: 0o700 })
141
+ try {
142
+ await verifySkillSource(context.skillDir, context)
143
+ for (const entry of await readdir(context.skillDir)) {
144
+ await cp(join(context.skillDir, entry), join(stage, entry), { recursive: true, force: false, errorOnExist: true })
145
+ }
146
+ await writeFile(join(stage, INSTALL_META), JSON.stringify({ source: context.runtimeCode, slug: context.skillName,
147
+ version: context.skillVersion, packageVersion: context.packageVersion, endpoint: context.endpoint,
148
+ installedAt: new Date().toISOString() }) + '\n', { flag: 'wx', mode: 0o600 })
149
+ await verifySkillSource(stage, context)
150
+ if (backup !== null) { await renamePath(target, backup); moved = true }
151
+ await renamePath(stage, target)
152
+ placed = true
153
+ return { path: target, status: previous === null ? 'installed' : 'updated',
154
+ previousVersion: previous === null ? null : previous.version, version: context.skillVersion,
155
+ documentationPath: join(target, 'SKILL.md'), backupPath: backup }
156
+ } catch (error) {
157
+ if (moved || placed) await restoreInstallation(target, moved ? backup : null, placed, renamePath)
158
+ throw error
159
+ } finally {
160
+ const remaining = await targetExists(stage)
161
+ if (remaining !== null) await rm(stage, { recursive: true })
162
+ }
163
+ }
164
+
165
+ export async function writeManagedSkill(context, target, options = {}) {
166
+ const allowCreate = options.allowCreate === true
167
+ const candidate = await managedTarget(context, target, allowCreate)
168
+ if (candidate.status === 'skipped') return candidate
169
+ assertNoManagedDowngrade(candidate.meta, context)
170
+ if (candidate.meta !== null && candidate.meta.version === context.skillVersion
171
+ && candidate.meta.packageVersion === context.packageVersion) {
172
+ await verifySkillSource(candidate.path, context)
173
+ return { path: candidate.path, status: 'current', version: context.skillVersion,
174
+ documentationPath: join(candidate.path, 'SKILL.md') }
175
+ }
176
+ await mkdir(dirname(candidate.path), { recursive: true })
177
+ await assertAccountAncestors(dirname(candidate.path))
178
+ const lock = join(dirname(candidate.path), '.' + context.skillName + '.install-lock')
179
+ try { await mkdir(lock, { mode: 0o700 }) } catch (error) {
180
+ if (error.code === 'EEXIST') throw new Error('Skill installation is already locked; inspect its pending update before retrying')
181
+ throw error
182
+ }
183
+ try {
184
+ const checked = await managedTarget(context, candidate.path, allowCreate)
185
+ if (checked.status === 'skipped') throw new Error('Skill installation ownership changed before update')
186
+ assertNoManagedDowngrade(checked.meta, context)
187
+ return await replaceInstallation(context, checked.path, checked.meta, options)
188
+ } finally { await rm(lock, { recursive: true }) }
189
+ }
190
+
191
+ export async function refreshManagedSkillCopies(context, dependencies = {}) {
192
+ const environment = dependencies.environment === undefined ? process.env : dependencies.environment
193
+ const home = dependencies.homeDirectory === undefined ? currentAccountHome() : dependencies.homeDirectory
194
+ const workingDirectory = dependencies.workingDirectory === undefined ? process.cwd() : dependencies.workingDirectory
195
+ if (!isAbsolute(home) || !isAbsolute(workingDirectory)) throw new Error('Account and project directories must be absolute')
196
+ const accountCodex = environment.CODEX_HOME === undefined ? join(home, '.codex') : environment.CODEX_HOME
197
+ if (typeof accountCodex !== 'string' || !isAbsolute(accountCodex) || !contained(home, accountCodex)) {
198
+ throw new Error('Automatic skill refresh requires CODEX_HOME inside the current account home')
199
+ }
200
+ const targets = [...new Set([join(workingDirectory, '.codex', 'skills', context.skillName),
201
+ join(accountCodex, 'skills', context.skillName)])]
202
+ const results = []
203
+ for (const target of targets) results.push(await writeManagedSkill(context, target, dependencies))
204
+ return results
205
+ }
package/installer.mjs CHANGED
@@ -1,10 +1,14 @@
1
+ import { latestOfficialSkillContext } from './official-skill-update.mjs'
2
+ import { installTarget, writeManagedSkill } from './installer-storage.mjs'
3
+ export { installTarget, readInstallMeta } from './installer-storage.mjs'
4
+ import { configureBrainClientCredential } from './broker-credentials.mjs'
1
5
  /**
2
6
  * 八个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
3
7
  * 禁止第二套超时、第二套版本来源、第二套 bin 名。
4
8
  */
5
- import { existsSync, readFileSync } from 'node:fs'
6
- import { cp, mkdir, rm, writeFile } from 'node:fs/promises'
7
- import { dirname, join, resolve } from 'node:path'
9
+ import { readFileSync } from 'node:fs'
10
+ import { writeFile } from 'node:fs/promises'
11
+ import { dirname, join } from 'node:path'
8
12
  import { stdin, stdout } from 'node:process'
9
13
  import { createInterface } from 'node:readline/promises'
10
14
  import { fileURLToPath } from 'node:url'
@@ -33,7 +37,6 @@ export {
33
37
 
34
38
  import { createBrokerTransport } from './broker-transport.mjs'
35
39
 
36
- const INSTALL_META = 'install-meta.json'
37
40
  const BROKER_STDIN_MAX_BYTES = 1_048_576
38
41
 
39
42
  function asObject(value, label) {
@@ -81,19 +84,6 @@ export function loadOfficialSkillContext(packageRoot) {
81
84
  }
82
85
  }
83
86
 
84
- export function readInstallMeta(target) {
85
- const path = join(target, INSTALL_META)
86
- if (!existsSync(path)) return null
87
- return asObject(JSON.parse(readFileSync(path, 'utf8')), INSTALL_META)
88
- }
89
-
90
- export function installTarget(skillName, explicit) {
91
- if (explicit) return resolve(explicit)
92
- const codexHome = process.env.CODEX_HOME?.trim()
93
- if (codexHome) return join(codexHome, 'skills', skillName)
94
- return join(process.cwd(), '.codex', 'skills', skillName)
95
- }
96
-
97
87
  export async function fetchLatestVersion(context) {
98
88
  const request = createBrokerTransport({ environment: process.env })
99
89
  const response = await request(context.latestEndpoint, { signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) })
@@ -105,51 +95,38 @@ export async function fetchLatestVersion(context) {
105
95
  }
106
96
  }
107
97
 
108
- export async function installOfficialSkill(context, explicit) {
109
- const target = installTarget(context.skillName, explicit)
110
- await mkdir(target, { recursive: true })
111
- const previous = readInstallMeta(target)
112
- await rm(join(target, 'references'), { recursive: true, force: true })
113
- await cp(context.skillDir, target, { recursive: true, force: true })
114
- const installed = asObject(JSON.parse(readFileSync(join(target, 'skill.json'), 'utf8')), 'installed skill.json')
115
- const installedVersion = requiredString(installed.version, 'installed skill.json version')
116
- await writeFile(join(target, INSTALL_META), `${JSON.stringify({
117
- source: context.runtimeCode,
118
- slug: context.skillName,
119
- version: installedVersion,
120
- packageVersion: context.packageVersion,
121
- endpoint: context.endpoint,
122
- installedAt: new Date().toISOString(),
123
- }, null, 2)}\n`)
124
- if (previous?.version && previous.version !== installedVersion) {
125
- console.log(`${context.displayName} skill updated: ${target}`)
126
- console.log(` ${previous.version} → ${installedVersion}`)
127
- } else {
128
- console.log(`${context.displayName} skill installed: ${target} (${installedVersion})`)
129
- }
130
- console.log('Next: return to your IDE and state the goal. The agent reads the installed SKILL.md.')
98
+ function writeInstallationResult(runtime, value) {
99
+ const line = JSON.stringify(value) + '\n'
100
+ if (runtime.writeOutput === undefined) process.stdout.write(line)
101
+ else runtime.writeOutput(line)
131
102
  }
132
103
 
133
- export async function checkOfficialSkill(context, explicit) {
134
- const target = installTarget(context.skillName, explicit)
135
- const current = readInstallMeta(target)
136
- if (!current) {
137
- console.log(`${context.displayName} skill is not installed. Run: npx ${context.npmName}@latest install`)
138
- process.exitCode = 1
139
- return
140
- }
141
- const installedVersion = requiredString(current.version, 'install-meta.json version')
142
- const packageVersion = requiredString(current.packageVersion, 'install-meta.json packageVersion')
143
- console.log(`Installed: ${installedVersion} (package ${packageVersion})`)
144
- const latest = await fetchLatestVersion(context)
145
- console.log(`Latest on cli.tax: ${latest.version}`)
146
- if (installedVersion === latest.version) {
147
- console.log('Up to date.')
148
- return
149
- }
150
- console.log(`Update available: ${installedVersion} → ${latest.version}`)
151
- console.log(`Run: npx ${context.npmName}@latest install`)
152
- process.exitCode = 1
104
+ function installationDependencies(dependencies) {
105
+ return dependencies === undefined ? brokerDependencies() : dependencies
106
+ }
107
+
108
+ export async function installOfficialSkill(context, explicit, dependencies) {
109
+ const runtime = installationDependencies(dependencies)
110
+ const selected = await latestOfficialSkillContext(context, runtime)
111
+ const environment = runtime.environment === undefined ? process.env : runtime.environment
112
+ const workingDirectory = runtime.workingDirectory === undefined ? process.cwd() : runtime.workingDirectory
113
+ const target = installTarget(selected.skillName, explicit, environment, workingDirectory)
114
+ const installed = await writeManagedSkill(selected, target, { ...runtime, allowCreate: true })
115
+ if (installed.status === 'skipped') throw new Error('Skill install refused: ' + installed.reason)
116
+ writeInstallationResult(runtime, { installed, reloadRequired: installed.status !== 'current' })
117
+ return installed
118
+ }
119
+
120
+ export async function checkOfficialSkill(context, explicit, dependencies) {
121
+ const runtime = installationDependencies(dependencies)
122
+ const selected = await latestOfficialSkillContext(context, runtime)
123
+ const environment = runtime.environment === undefined ? process.env : runtime.environment
124
+ const workingDirectory = runtime.workingDirectory === undefined ? process.cwd() : runtime.workingDirectory
125
+ const target = installTarget(selected.skillName, explicit, environment, workingDirectory)
126
+ const installed = await writeManagedSkill(selected, target, runtime)
127
+ if (installed.status === 'skipped') throw new Error('Skill check could not update its managed target: ' + installed.reason)
128
+ writeInstallationResult(runtime, { installed, reloadRequired: installed.status === 'updated' })
129
+ return installed
153
130
  }
154
131
 
155
132
  export function defaultUsage(context, extraLines) {
@@ -157,10 +134,12 @@ export function defaultUsage(context, extraLines) {
157
134
  `${context.npmName} — install and run the ${context.displayName} skill from CLI.Tax`,
158
135
  '',
159
136
  'Usage:',
137
+ ' configure < credential.json',
138
+ ' Store the Brain Client credential for this account; never put tokens in command arguments.',
160
139
  ` npx ${context.npmName}@latest install [directory]`,
161
140
  ` Install the ${context.displayName} skill for the current IDE.`,
162
141
  ` npx ${context.npmName}@latest check [directory]`,
163
- ' Check whether the installed skill has a newer version.',
142
+ ' Check and atomically update an already managed skill to the current official release.',
164
143
  ` npx ${context.npmName}@latest run`,
165
144
  " Run this skill's applicability or onboarding flow; only a real HTTP invocation can trigger automatic evaluation.",
166
145
  ` npx ${context.npmName}@latest invoke <operation> <JSON-object>`,
@@ -169,7 +148,8 @@ export function defaultUsage(context, extraLines) {
169
148
  ' Read one {"operation":"...","input":{...}} request from JSON stdin.',
170
149
  ` npx ${context.npmName}@latest recover <operation> <requestId>`,
171
150
  ' Query an uncertain invocation without resending or charging again.',
172
- 'Credential: CLITAX_BRAIN_CLIENT_TOKEN_FILE (the broker reads it; never pass the token).',
151
+ 'Credential: configure reads a token-file JSON document from stdin and stores it once for the current account.',
152
+ 'An explicit CLITAX_BRAIN_CLIENT_TOKEN_FILE must be absolute. Revoked keys require a fresh authenticated copy from CLI.Tax.',
173
153
  `Endpoint: ${context.endpoint}`,
174
154
  ]
175
155
  if (extraLines?.length) lines.push('', ...extraLines)
@@ -270,7 +250,12 @@ export async function dispatchOfficialSkillCli(options) {
270
250
  const command = args[0] ?? 'help'
271
251
  const argument = args[1]
272
252
  try {
273
- if (command === 'install') await installOfficialSkill(context, argument)
253
+ if (command === 'configure') {
254
+ if (args.length !== 1) throw new Error('configure accepts credentials only through JSON stdin')
255
+ const configured = await configureBrainClientCredential(await readBrokerSource(stdin))
256
+ process.stdout.write(JSON.stringify(configured) + '\n')
257
+ }
258
+ else if (command === 'install') await installOfficialSkill(context, argument)
274
259
  else if (command === 'check') await checkOfficialSkill(context, argument)
275
260
  else if (command === 'run') await options.runCommand(context)
276
261
  else if (command === 'recover') await runBrokerRecovery(context, args)