agent-policy-kit 0.0.0-stage → 0.1.0

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/src/detect.mjs ADDED
@@ -0,0 +1,136 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+
7
+ const CLIENTS = ['codex','claude','kimi','agy'];
8
+ const OUTPUT_LIMIT = 65536;
9
+ const VERSION = String.raw`(\d{1,4}\.\d{1,4}\.\d{1,4}(?:-[A-Za-z0-9][A-Za-z0-9.-]{0,63})?(?:\+[A-Za-z0-9][A-Za-z0-9.-]{0,63})?)`;
10
+ function failure(code) { return Object.assign(new Error(code),{code}); }
11
+ function options({env=process.env,hostHome=os.homedir(),timeoutMs=5000} = {}) {
12
+ if (!env || typeof env !== 'object' || typeof hostHome !== 'string' || !path.isAbsolute(hostHome) || hostHome.includes('\0') || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw failure('INVALID_DETECTION_OPTIONS');
13
+ return {env,hostHome:path.resolve(hostHome),timeoutMs};
14
+ }
15
+ function candidates(client,{env,hostHome}) {
16
+ const dirs = typeof env.PATH === 'string' ? env.PATH.split(path.delimiter) : [];
17
+ const names = process.platform === 'win32' ? [client + '.exe',client] : [client];
18
+ const result = [];
19
+ for (const dir of dirs) {
20
+ // Never search the working directory through empty/relative PATH segments.
21
+ if (!path.isAbsolute(dir) || dir.includes('\0')) continue;
22
+ for (const name of names) result.push({file:path.join(dir,name),origin:'path'});
23
+ }
24
+ const conventional = client === 'kimi' ? ['.kimi-code/bin/kimi']
25
+ : client === 'claude' ? ['.local/bin/claude','.claude/local/claude']
26
+ : [`.local/bin/${client}`];
27
+ for (const relative of conventional) result.push({file:path.join(hostHome,relative),origin:'known_location'});
28
+ return result.filter((candidate,index) => result.findIndex(c=>c.file === candidate.file) === index);
29
+ }
30
+ async function inspect(file) {
31
+ try {
32
+ const executable = await fs.realpath(file);
33
+ const stat = await fs.stat(executable);
34
+ if (!stat.isFile()) return {status:'invalid'};
35
+ await fs.access(executable,constants.X_OK);
36
+ return {status:'executable',executable};
37
+ } catch (error) {
38
+ return {status:error.code === 'ENOENT' || error.code === 'ENOTDIR' ? 'not_found' : 'invalid'};
39
+ }
40
+ }
41
+ function probe(executable,arg,{env,timeoutMs}) {
42
+ return new Promise(resolve => {
43
+ let child, timer, settled = false, size = 0;
44
+ const ownGroup = process.platform !== 'win32';
45
+ const buffers = [];
46
+ const killOwned = () => {
47
+ if (!child?.pid) return;
48
+ try { if (ownGroup) process.kill(-child.pid,'SIGKILL'); else child.kill('SIGKILL'); } catch {}
49
+ };
50
+ const alive = () => {
51
+ if (!child?.pid) return false;
52
+ if (!ownGroup) return child.exitCode === null && child.signalCode === null;
53
+ try { process.kill(-child.pid,0); return true; } catch (error) { return error.code !== 'ESRCH'; }
54
+ };
55
+ const finish = async (status) => {
56
+ if (settled) return;
57
+ settled = true;
58
+ clearTimeout(timer);
59
+ killOwned();
60
+ child?.stdout?.destroy(); child?.stderr?.destroy();
61
+ for (let elapsed = 0; alive() && elapsed < 500; elapsed += 20) await new Promise(done=>setTimeout(done,20));
62
+ if (status === 'ok' && alive()) status = 'invalid';
63
+ resolve({status,output:status === 'ok' ? Buffer.concat(buffers).toString('utf8') : ''});
64
+ };
65
+ try { child = spawn(executable,[arg],{env,stdio:['ignore','pipe','pipe'],shell:false,windowsHide:true,detached:ownGroup}); }
66
+ catch { finish('invalid'); return; }
67
+ timer = setTimeout(()=>finish('timeout'),timeoutMs);
68
+ for (const stream of [child.stdout,child.stderr]) stream.on('data',chunk=>{
69
+ size += chunk.length;
70
+ if (size > OUTPUT_LIMIT) finish('invalid'); else buffers.push(chunk);
71
+ });
72
+ // A wrapper can exit successfully while descendants retain inherited pipes.
73
+ // Its dedicated POSIX group remains ours; stop it before waiting for close.
74
+ child.on('exit',killOwned);
75
+ child.on('error',()=>finish('invalid'));
76
+ child.on('close',code=>finish(code === 0 ? 'ok' : 'invalid'));
77
+ });
78
+ }
79
+ function identity(client,output) {
80
+ const patterns = {
81
+ codex:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
82
+ claude:[new RegExp(String.raw`^\s*v?${VERSION}\s+\(Claude Code\)(?:\s|$)`,'im'),new RegExp(String.raw`^\s*Claude Code\s+v?${VERSION}(?:\s|$)`,'im')],
83
+ kimi:[new RegExp(String.raw`^\s*kimi(?:\s+code(?:\s+cli)?)?(?:,?\s+version)?\s+v?${VERSION}(?:\s|$)`,'im')],
84
+ agy:[new RegExp(String.raw`^\s*(?:Antigravity|agy)\s+v?${VERSION}(?:\s|$)`,'im')],
85
+ };
86
+ for (const pattern of patterns[client]) { const match = output.match(pattern); if (match && match[1].length <= 128) return match[1]; }
87
+ return null;
88
+ }
89
+ async function verify(client,executable,opts) {
90
+ const version = await probe(executable,'--version',opts);
91
+ if (version.status !== 'ok') return {status:version.status};
92
+ const branded = identity(client,version.output);
93
+ if (branded) return {status:'detected',version:branded};
94
+ if (client !== 'kimi' && client !== 'agy') return {status:'invalid'};
95
+ const number = version.output.match(new RegExp(String.raw`^\s*v?${VERSION}(?:\s|$)`));
96
+ if (!number || number[1].length > 128) return {status:'invalid'};
97
+ const help = await probe(executable,'--help',opts);
98
+ if (help.status !== 'ok') return {status:help.status};
99
+ const known = client === 'kimi' ? /\bKimi (?:Code(?: CLI)?|CLI)\b/i.test(help.output)
100
+ && (/\busage:\s*kimi\b/i.test(help.output) || /moonshotai\.github\.io\/kimi-code\//i.test(help.output))
101
+ : /\bAntigravity\b/i.test(help.output) && /\busage:\s*agy\b/i.test(help.output)
102
+ || /Usage of agy:/i.test(help.output) && /Available subcommands:/i.test(help.output)
103
+ && ['--agent','--model','--effort'].every(flag=>help.output.includes(flag));
104
+ return known ? {status:'detected',version:number[1]} : {status:'invalid'};
105
+ }
106
+
107
+ export async function resolveClient(client,opts = {}) {
108
+ if (!CLIENTS.includes(client)) throw failure('INVALID_CLIENT');
109
+ const settings = options(opts);
110
+ let best = {client,status:'not_found',version:null,origin:null,executable:null};
111
+ const checked = new Set();
112
+ for (const candidate of candidates(client,settings)) {
113
+ const inspected = await inspect(candidate.file);
114
+ if (inspected.status === 'not_found') continue;
115
+ if (inspected.status === 'invalid') {
116
+ if (best.status === 'not_found') best = {...best,status:'invalid',origin:candidate.origin};
117
+ continue;
118
+ }
119
+ if (checked.has(inspected.executable)) continue;
120
+ checked.add(inspected.executable);
121
+ const result = await verify(client,inspected.executable,settings);
122
+ if (result.status === 'detected') return {client,status:'detected',version:result.version,origin:candidate.origin,executable:inspected.executable};
123
+ if (result.status === 'timeout' || best.status === 'not_found') best = {...best,status:result.status,origin:candidate.origin};
124
+ }
125
+ return best;
126
+ }
127
+ export async function detectClients(opts = {}) {
128
+ return Promise.all(CLIENTS.map(client=>resolveClient(client,opts)));
129
+ }
130
+ export function summarizeDetection(records) {
131
+ return records.filter(r=>CLIENTS.includes(r?.client)).map(({client,status,version,origin})=>{
132
+ status = ['detected','not_found','invalid','timeout'].includes(status) ? status : 'invalid';
133
+ if (status === 'detected' && (typeof version !== 'string' || version.length > 128 || !new RegExp(`^${VERSION}$`).test(version))) status = 'invalid';
134
+ return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location'].includes(origin) ? origin : null};
135
+ });
136
+ }
@@ -0,0 +1,82 @@
1
+ // Pure command codec, also embedded in the offline configuration page.
2
+ export const TOOL_FLAGS=Object.freeze({codex:'codex',claude:'claude',kimi:'kimi',antigravity:'agy'});
3
+ export const EXPLICIT_FIELDS=Object.freeze(['roles','max-concurrent',...Object.keys(TOOL_FLAGS),...Object.keys(TOOL_FLAGS).map(flag=>'avoid-'+flag)]);
4
+ const EXPLICIT_MODEL=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
5
+ const EXPLICIT_EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
6
+ function invalidExplicit(){throw Object.assign(new Error('Invalid explicit configuration.'),{code:'INVALID_EXPLICIT_CONFIG'});}
7
+ function explicitList(value,limit){
8
+ if(typeof value!=='string'||!value.length)invalidExplicit();
9
+ const list=value.split(',').map(value=>value.trim());
10
+ if(!list.length||list.length>limit||list.some(value=>!value)||new Set(list).size!==list.length)invalidExplicit();
11
+ return list;
12
+ }
13
+
14
+ export function parseExplicitOptions(options,roleIds){
15
+ if(!options||typeof options!=='object'||Array.isArray(options))invalidExplicit();
16
+ const fields=Object.getOwnPropertyDescriptors(options);
17
+ if(Object.entries(fields).some(([key,value])=>!EXPLICIT_FIELDS.includes(key)||value.get||value.set||typeof value.value!=='string'))invalidExplicit();
18
+ options=Object.fromEntries(Object.entries(fields).map(([key,value])=>[key,value.value]));
19
+ if(Object.values(fields).reduce((size,value)=>size+value.value.length,0)>65536)invalidExplicit();
20
+ const requested=explicitList(options.roles,roleIds.length);
21
+ if(requested.some(role=>!roleIds.includes(role)))invalidExplicit();
22
+ const roles=roleIds.filter(role=>requested.includes(role)),maxConcurrent=Number(options['max-concurrent']);
23
+ if(!/^[1-9]\d*$/.test(options['max-concurrent'])||!Number.isInteger(maxConcurrent)||maxConcurrent>32)invalidExplicit();
24
+ for(const flag of Object.keys(TOOL_FLAGS))if(options['avoid-'+flag]!==undefined&&options[flag]===undefined)invalidExplicit();
25
+ const clients={};
26
+ for(const flag of Object.keys(options).filter(flag=>Object.hasOwn(TOOL_FLAGS,flag))){
27
+ const client=TOOL_FLAGS[flag],choices={};
28
+ for(const entry of explicitList(options[flag],roles.length)){
29
+ const at=entry.indexOf('='),role=entry.slice(0,at).trim(),value=entry.slice(at+1).trim();
30
+ if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
31
+ let model=value,effort=null;
32
+ if(client==='agy'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
33
+ else{
34
+ const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
35
+ model=value.slice(0,colon);const level=value.slice(colon+1);
36
+ if(level!=='native'&&!EXPLICIT_EFFORTS.includes(level))invalidExplicit();effort=level==='native'?null:level;
37
+ }
38
+ if(!EXPLICIT_MODEL.test(model))invalidExplicit();choices[role]={model,effort};
39
+ }
40
+ if(Object.keys(choices).length!==roles.length)invalidExplicit();
41
+ const avoidModels=options['avoid-'+flag]===undefined?[]:explicitList(options['avoid-'+flag],32);
42
+ if(avoidModels.some(model=>!EXPLICIT_MODEL.test(model)))invalidExplicit();
43
+ const first=choices[roles[0]];
44
+ clients[client]={code:{...first},light:{...first},roles:Object.fromEntries(roles.map(role=>[role,choices[role]])),avoidModels};
45
+ }
46
+ if(!Object.keys(clients).length)invalidExplicit();
47
+ return {schemaVersion:1,maxConcurrent,...(roles.length===roleIds.length?{}:{selectedRoles:roles}),clients};
48
+ }
49
+
50
+ export function explicitOptions(config,roleCatalog){
51
+ const legacy={worker:'code',reviewer:'code',explorer:'light',routine:'light'};
52
+ if(!Array.isArray(roleCatalog)||!roleCatalog.length)invalidExplicit();
53
+ const definitions=roleCatalog.map(role=>typeof role==='string'?{id:role,kind:legacy[role]}:role);
54
+ if(definitions.some(role=>!role||typeof role.id!=='string'||!['code','light'].includes(role.kind))||new Set(definitions.map(role=>role.id)).size!==definitions.length)invalidExplicit();
55
+ const roleIds=definitions.map(role=>role.id),kinds=new Map(definitions.map(role=>[role.id,role.kind]));
56
+ if(!config||config.schemaVersion!==1||!config.clients||typeof config.clients!=='object'||Array.isArray(config.clients))invalidExplicit();
57
+ const selected=config.selectedRoles??roleIds;
58
+ if(!Array.isArray(selected)||!selected.length||new Set(selected).size!==selected.length||selected.some(role=>!roleIds.includes(role)))invalidExplicit();
59
+ const roles=roleIds.filter(role=>selected.includes(role));
60
+ const options={roles:roles.join(','),'max-concurrent':String(config.maxConcurrent)};
61
+ for(const [client,preferences]of Object.entries(config.clients)){
62
+ const flag=Object.keys(TOOL_FLAGS).find(flag=>TOOL_FLAGS[flag]===client);if(!flag||!preferences)invalidExplicit();
63
+ options[flag]=roles.map(role=>{
64
+ const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
65
+ if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
66
+ if(client==='agy'&&selected.effort!==null)invalidExplicit();
67
+ return `${role}=${selected.model}${client==='agy'?'':':'+(selected.effort??'native')}`;
68
+ }).join(',');
69
+ if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
70
+ if(preferences.avoidModels.length)options['avoid-'+flag]=preferences.avoidModels.join(',');
71
+ }
72
+ parseExplicitOptions(options,roleIds);
73
+ return options;
74
+ }
75
+
76
+ export function formatExplicitCommand(config,version,roleCatalog){
77
+ if(typeof version!=='string'||!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))throw new Error('Invalid release version.');
78
+ const options=explicitOptions(config,roleCatalog),quote=value=>"'"+value.replaceAll("'","'\"'\"'")+"'";
79
+ const lines=[`npx --yes --ignore-scripts --package agent-policy-kit@${version} agent-policy setup`];
80
+ for(const [name,value]of Object.entries(options))lines.push(`--${name} ${name==='max-concurrent'?value:quote(value)}`);
81
+ lines.push('--apply');return lines.join(' \\\n ');
82
+ }