agent-policy-kit 0.2.0 → 0.3.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 CHANGED
@@ -4,17 +4,65 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
 
7
- const CLIENTS = ['codex','claude','kimi','agy','grok'];
7
+ const CLIENTS = ['chatgpt','claude','kimi','gemini','grok'];
8
+ // Each tool's own executable keeps its native name: ChatGPT runs Codex, and Gemini runs Antigravity (agy).
9
+ const EXECUTABLES = {chatgpt:'codex',claude:'claude',kimi:'kimi',gemini:'agy',grok:'grok'};
8
10
  const OUTPUT_LIMIT = 65536;
9
11
  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
12
  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};
13
+ const absolute = value => typeof value === 'string' && path.isAbsolute(value) && !value.includes('\0');
14
+ function options({env=process.env,hostHome=os.homedir(),timeoutMs=5000,platform=process.platform,systemApplications} = {}) {
15
+ // AGENT_POLICY_APPLICATIONS_DIR replaces /Applications, so hermetic runs never see the host's installed apps.
16
+ systemApplications ??= (typeof env?.AGENT_POLICY_APPLICATIONS_DIR === 'string' && env.AGENT_POLICY_APPLICATIONS_DIR) || '/Applications';
17
+ if (!env || typeof env !== 'object' || !absolute(hostHome) || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000 || typeof platform !== 'string' || !absolute(systemApplications)) throw failure('INVALID_DETECTION_OPTIONS');
18
+ return {env,hostHome:path.resolve(hostHome),timeoutMs,platform,systemApplications:path.resolve(systemApplications)};
14
19
  }
15
- function candidates(client,{env,hostHome}) {
20
+ // Desktop apps ship their own copy of a CLI and keep it off PATH. ChatGPT Desktop runs Codex from a
21
+ // fixed bundle path; Claude Desktop keeps one Claude Code per <version>/<build> directory.
22
+ const CHATGPT_CODEX = 'ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
23
+ const CLAUDE_DESKTOP = 'Library/Application Support/Claude/claude-code';
24
+ const DESKTOP_VERSION = /^(\d{1,4})\.(\d{1,4})\.(\d{1,4})$/;
25
+ const DESKTOP_BUILD = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
26
+ const DESKTOP_LIMIT = 8;
27
+ async function directories(dir) {
28
+ try { return (await fs.readdir(dir,{withFileTypes:true})).filter(entry=>entry.isDirectory()).map(entry=>entry.name); }
29
+ catch { return []; }
30
+ }
31
+ function newerFirst(a,b) {
32
+ const [x,y] = [a,b].map(name=>name.match(DESKTOP_VERSION).slice(1).map(Number));
33
+ return y[0]-x[0] || y[1]-x[1] || y[2]-x[2];
34
+ }
35
+ async function desktopCandidates(client,{hostHome,platform,systemApplications}) {
36
+ if (platform !== 'darwin') return [];
37
+ if (client === 'chatgpt') return [systemApplications,path.join(hostHome,'Applications')].map(dir=>path.join(dir,CHATGPT_CODEX));
38
+ if (client !== 'claude') return [];
39
+ const base = path.join(hostHome,CLAUDE_DESKTOP), files = [];
40
+ for (const version of (await directories(base)).filter(name=>DESKTOP_VERSION.test(name)).sort(newerFirst)) {
41
+ // Only builds that contain the CLI count toward the limit, so empty or partial folders cannot hide a working one.
42
+ // Within one version the most recently written CLI comes first; the name breaks ties.
43
+ const builds = [];
44
+ for (const build of (await directories(path.join(base,version))).filter(name=>DESKTOP_BUILD.test(name))) {
45
+ const file = path.join(base,version,build,'claude.app/Contents/MacOS/claude');
46
+ try { const stat = await fs.stat(file); if (stat.isFile()) builds.push({file,build,mtime:stat.mtimeMs}); } catch {}
47
+ }
48
+ builds.sort((a,b)=>b.mtime-a.mtime || (a.build < b.build ? -1 : a.build > b.build ? 1 : 0));
49
+ for (const {file} of builds) {
50
+ files.push(file);
51
+ // The limit bounds detection time: each candidate that exists is started once with --version.
52
+ if (files.length === DESKTOP_LIMIT) return files;
53
+ }
54
+ }
55
+ return files;
56
+ }
57
+ async function candidates(client,settings) {
58
+ const result = cliCandidates(client,settings);
59
+ for (const file of await desktopCandidates(client,settings)) result.push({file,origin:'desktop_app'});
60
+ return result.filter((candidate,index) => result.findIndex(c=>c.file === candidate.file) === index);
61
+ }
62
+ function cliCandidates(client,{env,hostHome}) {
16
63
  const dirs = typeof env.PATH === 'string' ? env.PATH.split(path.delimiter) : [];
17
- const names = process.platform === 'win32' ? [client + '.exe',client] : [client];
64
+ const executable = EXECUTABLES[client];
65
+ const names = process.platform === 'win32' ? [executable + '.exe',executable] : [executable];
18
66
  const result = [];
19
67
  for (const dir of dirs) {
20
68
  // Never search the working directory through empty/relative PATH segments.
@@ -23,9 +71,9 @@ function candidates(client,{env,hostHome}) {
23
71
  }
24
72
  const conventional = client === 'kimi' ? ['.kimi-code/bin/kimi']
25
73
  : client === 'claude' ? ['.local/bin/claude','.claude/local/claude']
26
- : [`.local/bin/${client}`];
74
+ : [`.local/bin/${executable}`];
27
75
  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);
76
+ return result;
29
77
  }
30
78
  async function inspect(file) {
31
79
  try {
@@ -78,10 +126,10 @@ function probe(executable,arg,{env,timeoutMs}) {
78
126
  }
79
127
  function identity(client,output) {
80
128
  const patterns = {
81
- codex:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
129
+ chatgpt:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
82
130
  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
131
  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')],
132
+ gemini:[new RegExp(String.raw`^\s*(?:Antigravity|agy)\s+v?${VERSION}(?:\s|$)`,'im')],
85
133
  grok:[new RegExp(String.raw`^\s*grok\s+v?${VERSION}(?:\s|$)`,'im')],
86
134
  };
87
135
  for (const pattern of patterns[client]) { const match = output.match(pattern); if (match && match[1].length <= 128) return match[1]; }
@@ -92,7 +140,7 @@ async function verify(client,executable,opts) {
92
140
  if (version.status !== 'ok') return {status:version.status};
93
141
  const branded = identity(client,version.output);
94
142
  if (branded) return {status:'detected',version:branded};
95
- if (client !== 'kimi' && client !== 'agy') return {status:'invalid'};
143
+ if (client !== 'kimi' && client !== 'gemini') return {status:'invalid'};
96
144
  const number = version.output.match(new RegExp(String.raw`^\s*v?${VERSION}(?:\s|$)`));
97
145
  if (!number || number[1].length > 128) return {status:'invalid'};
98
146
  const help = await probe(executable,'--help',opts);
@@ -110,7 +158,7 @@ export async function resolveClient(client,opts = {}) {
110
158
  const settings = options(opts);
111
159
  let best = {client,status:'not_found',version:null,origin:null,executable:null};
112
160
  const checked = new Set();
113
- for (const candidate of candidates(client,settings)) {
161
+ for (const candidate of await candidates(client,settings)) {
114
162
  const inspected = await inspect(candidate.file);
115
163
  if (inspected.status === 'not_found') continue;
116
164
  if (inspected.status === 'invalid') {
@@ -132,6 +180,6 @@ export function summarizeDetection(records) {
132
180
  return records.filter(r=>CLIENTS.includes(r?.client)).map(({client,status,version,origin})=>{
133
181
  status = ['detected','not_found','invalid','timeout'].includes(status) ? status : 'invalid';
134
182
  if (status === 'detected' && (typeof version !== 'string' || version.length > 128 || !new RegExp(`^${VERSION}$`).test(version))) status = 'invalid';
135
- return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location'].includes(origin) ? origin : null};
183
+ return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location','desktop_app'].includes(origin) ? origin : null};
136
184
  });
137
185
  }
package/src/explicit.mjs CHANGED
@@ -1,5 +1,5 @@
1
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',grok:'grok'});
2
+ export const TOOL_FLAGS=Object.freeze({chatgpt:'chatgpt',claude:'claude',kimi:'kimi',gemini:'gemini',grok:'grok'});
3
3
  export const EXPLICIT_FIELDS=Object.freeze(['roles','max-concurrent',...Object.keys(TOOL_FLAGS),...Object.keys(TOOL_FLAGS).map(flag=>'avoid-'+flag),...Object.keys(TOOL_FLAGS).map(flag=>flag+'-max-concurrent')]);
4
4
  const EXPLICIT_MODEL=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
5
5
  const EXPLICIT_EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
@@ -31,7 +31,7 @@ export function parseExplicitOptions(options,roleIds){
31
31
  const at=entry.indexOf('='),role=entry.slice(0,at).trim(),value=entry.slice(at+1).trim();
32
32
  if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
33
33
  let model=value,effort=null;
34
- if(client==='agy'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
34
+ if(client==='gemini'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
35
35
  else if(client==='grok'){if(grokEffortSuffix(value))invalidExplicit();}
36
36
  else{
37
37
  const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
@@ -68,9 +68,9 @@ export function explicitOptions(config,roleCatalog){
68
68
  options[flag]=roles.map(role=>{
69
69
  const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
70
70
  if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
71
- if(['agy','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
71
+ if(['gemini','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
72
72
  if(client==='grok'&&grokEffortSuffix(selected.model))invalidExplicit();
73
- return `${role}=${selected.model}${['agy','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
73
+ return `${role}=${selected.model}${['gemini','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
74
74
  }).join(',');
75
75
  if(preferences.maxConcurrent!==undefined)options[flag+'-max-concurrent']=String(preferences.maxConcurrent);
76
76
  if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
package/src/inspect.mjs CHANGED
@@ -5,8 +5,8 @@ import {parseToml} from './toml.mjs';
5
5
  import {CLIENTS} from './policy.mjs';
6
6
  import {inspectConfiguration} from './install.mjs';
7
7
 
8
- const ROOTS={codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini',grok:'.grok'};
9
- const ENV_ROOTS={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'};
8
+ const ROOTS={chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
9
+ const ENV_ROOTS={chatgpt:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'};
10
10
  const MAX_FILE=1024*1024,MAX_TOTAL=4*MAX_FILE,MAX_ENTRIES=256;
11
11
  const identifier=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
12
12
  const namePattern=/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,79}$/;
@@ -88,7 +88,7 @@ function extractSettings(client,id,native,env,models){
88
88
  if(subagents.max_concurrent!==undefined)client.concurrency.value=cap(subagents.max_concurrent);
89
89
  return;
90
90
  }
91
- if(id==='codex'){
91
+ if(id==='chatgpt'){
92
92
  const agents=native.agents??{};if(!record(agents))throw failure('UNSUPPORTED_NATIVE_SETTING');
93
93
  client.defaults.model=optionalString(agents.default_subagent_model);
94
94
  client.defaults.effort=optionalString(agents.default_subagent_reasoning_effort);
@@ -132,14 +132,14 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
132
132
  try{
133
133
  if(entry.isSymbolicLink())throw failure('UNSAFE_FILE');
134
134
  if(entry.isDirectory()){
135
- if(id==='codex')continue;
135
+ if(id==='chatgpt')continue;
136
136
  if(depth>=4)throw failure('INSPECTION_LIMIT');
137
137
  await rolesIn(file,client,id,budget,models,depth+1);continue;
138
138
  }
139
- if(!entry.name.endsWith(id==='codex'?'.toml':'.md'))continue;
139
+ if(!entry.name.endsWith(id==='chatgpt'?'.toml':'.md'))continue;
140
140
  const body=await readFile(file,budget);if(body===null)throw failure('CONFIG_CHANGED_DURING_READ');
141
- const fields=id==='codex'?parseToml(body):frontmatter(body);
142
- const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='codex'?fields.model_reasoning_effort:fields.effort)};
141
+ const fields=id==='chatgpt'?parseToml(body):frontmatter(body);
142
+ const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='chatgpt'?fields.model_reasoning_effort:fields.effort)};
143
143
  if(id==='kimi'){
144
144
  // APK's Markdown role files contain explicit routing guidance, not a
145
145
  // native model binding. Other prose is never interpreted as configuration.
@@ -154,7 +154,7 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
154
154
  role.model=models.get(role.name)??role.model;
155
155
  if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
156
156
  }
157
- if(id==='agy'){
157
+ if(id==='gemini'){
158
158
  if(role.model!==null&&!['pro','flash','flash_lite','inherit'].includes(role.model))throw failure('UNSUPPORTED_NATIVE_SETTING');
159
159
  role.enabled=fields.subagent??null;
160
160
  if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
@@ -179,19 +179,19 @@ export async function readConfiguration({client=null,home,env=process.env,explic
179
179
  const report={schemaVersion:1,scope:'user',modelExecution:'not_checked',clients:{}};
180
180
  for(const id of client?[client]:CLIENTS){
181
181
  const root=path.resolve(!explicitHome&&env[ENV_ROOTS[id]]?env[ENV_ROOTS[id]]:path.join(home,ROOTS[id]));
182
- const c={status:'not_configured',defaults:{model:null,effort:null,enabled:null},concurrency:{value:null,kind:['codex','claude','grok'].includes(id)?'native_setting':'guidance'},roles:[],pool:[],warnings:[],apk:{status:'not_checked'}};
182
+ const c={status:'not_configured',defaults:{model:null,effort:null,enabled:null},concurrency:{value:null,kind:['chatgpt','claude','grok'].includes(id)?'native_setting':'guidance'},roles:[],pool:[],warnings:[],apk:{status:'not_checked'}};
183
183
  report.clients[id]=c;const budget={bytes:0,entries:0},models=new Map();
184
184
  try{
185
185
  await safePath(root);
186
- if(id!=='agy'){
186
+ if(id!=='gemini'){
187
187
  try{
188
188
  const text=await readFile(path.join(root,id==='claude'?'settings.json':'config.toml'),budget);
189
189
  if(text!==null){let native;if(id==='claude'){try{native=JSON.parse(text);}catch{throw failure('INVALID_JSON');}}else native=parseToml(text);extractSettings(c,id,native,env,models);}
190
190
  else if(id==='claude')extractSettings(c,id,{},env,models);
191
191
  }catch(error){warning(c,error);}
192
192
  }
193
- await rolesIn(path.join(root,id==='agy'?'config/agents':'agents'),c,id,budget,models);
194
- if(id==='kimi'||id==='agy'){try{await readGuidance(root,c,budget);}catch(error){warning(c,error);}}
193
+ await rolesIn(path.join(root,id==='gemini'?'config/agents':'agents'),c,id,budget,models);
194
+ if(id==='kimi'||id==='gemini'){try{await readGuidance(root,c,budget);}catch(error){warning(c,error);}}
195
195
  if(c.status!=='attention'&&(c.roles.length||c.pool.length||c.defaults.model!==null||c.defaults.effort!==null||c.defaults.enabled!==null||c.concurrency.value!==null||Object.keys(c.overrides??{}).length))c.status='configured';
196
196
  if(root===path.resolve(home,ROOTS[id])){
197
197
  try{const installed=await inspectConfiguration({client:id,home,stateDir});c.apk.status=installed.status;if(installed.status==='verified'){c.apk.releaseVersion=installed.releaseVersion;c.apk.preset=installed.preset;}}
package/src/install.mjs CHANGED
@@ -10,7 +10,7 @@ import catalog from '../models/catalog.json' with {type:'json'};
10
10
  import { parseToml, setTomlValue, restoreTomlValue, removeTomlValue } from './toml.mjs';
11
11
  import { nativeLocations, nativeRoles, instructionBlock, nativeWarnings, BEGIN, END } from './native.mjs';
12
12
 
13
- const CLIENTS = ['codex', 'claude', 'kimi', 'agy', 'grok'];
13
+ const CLIENTS = ['chatgpt', 'claude', 'kimi', 'gemini', 'grok'];
14
14
  const LIMIT = 1024 * 1024;
15
15
  const digest = value => value === null ? null : createHash('sha256').update(value).digest('hex');
16
16
  const json = value => JSON.stringify(value, null, 2) + '\n';
@@ -155,7 +155,7 @@ function selectedAliases(preferences,selectedRoles=ROLE_IDS) {
155
155
  }
156
156
  function tomlSettings(client,preferences,maxConcurrent,selectedRoles=ROLE_IDS) {
157
157
  const worker=rolePreference(preferences,selectedRoles[0]);
158
- if(client==='codex')return [[['agents','max_concurrent_threads_per_session'],maxConcurrent],[['agents','default_subagent_model'],worker.model],...(worker.effort===null?[]:[[['agents','default_subagent_reasoning_effort'],worker.effort]])];
158
+ if(client==='chatgpt')return [[['agents','max_concurrent_threads_per_session'],maxConcurrent],[['agents','default_subagent_model'],worker.model],...(worker.effort===null?[]:[[['agents','default_subagent_reasoning_effort'],worker.effort]])];
159
159
  if(client==='grok')return [[['subagents','max_concurrent'],maxConcurrent],...selectedRoles.map(role=>[['subagents','models','apk-'+role],rolePreference(preferences,role).model]),[['subagents','models','general-purpose'],worker.model]];
160
160
  if(client!=='kimi')return [];
161
161
  const selected=selectedAliases(preferences,selectedRoles),roles=effectiveRoles(preferences,selectedRoles);
@@ -185,7 +185,7 @@ function scalar(op,target,type) {
185
185
  }
186
186
  function validateTomlSnapshot(file,client,schemaVersion,selectedRoles=ROLE_IDS) {
187
187
  if(!Array.isArray(file.ops)||!file.ops.every(op=>object(op)&&Array.isArray(op.path)&&op.path.every(key=>typeof key==='string')&&op.path.length<=4))fail('INVALID_RECEIPT');
188
- if(client==='codex') {
188
+ if(client==='chatgpt') {
189
189
  if(!(schemaVersion===2?[3]:[2,3]).includes(file.ops.length))fail('INVALID_RECEIPT');
190
190
  const cap=scalar(file.ops[0],['agents','max_concurrent_threads_per_session'],'number');
191
191
  if(cap<1||cap>32)fail('INVALID_RECEIPT');
@@ -389,8 +389,8 @@ export async function planInstall(options) {
389
389
  const cap=subagents?.max_concurrent;
390
390
  if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
391
391
  }
392
- if(plan.client==='codex') {
393
- if(native.agents&&own(native.agents,'max_threads'))fail('CODEX_CAP_ALIAS_CONFLICT');
392
+ if(plan.client==='chatgpt') {
393
+ if(native.agents&&own(native.agents,'max_threads'))fail('CHATGPT_CAP_ALIAS_CONFLICT');
394
394
  const cap=native.agents?.max_concurrent_threads_per_session;
395
395
  if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
396
396
  }
package/src/native.mjs CHANGED
@@ -4,25 +4,25 @@ import {ROLE_DEFINITIONS,ROLE_IDS} from './roles.mjs';
4
4
 
5
5
  export const BEGIN = '<!-- BEGIN agent-policy-kit -->';
6
6
  export const END = '<!-- END agent-policy-kit -->';
7
- const directory = {codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini',grok:'.grok'};
7
+ const directory = {chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
8
8
  export function nativeHomeConflict(client,home,env) {
9
- const variable={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'}[client];
9
+ const variable={chatgpt:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'}[client];
10
10
  const active=variable&&env[variable];
11
11
  return typeof active==='string'&&active.length>0&&path.resolve(active)!==path.resolve(home,directory[client]);
12
12
  }
13
13
  export function nativeLocations(client,home) {
14
14
  const root=path.join(home,directory[client]);
15
15
  return {
16
- config:client==='agy'?null:path.join(root,client==='claude'?'settings.json':'config.toml'),
16
+ config:client==='gemini'?null:path.join(root,client==='claude'?'settings.json':'config.toml'),
17
17
  instructions:path.join(root,client==='claude'?'CLAUDE.md':'AGENTS.md'),
18
- roles:Object.keys(ROLES).map(role=>path.join(root,client==='agy'?'config/agents':'agents',`apk-${role}.${client==='codex'?'toml':'md'}`))
18
+ roles:Object.keys(ROLES).map(role=>path.join(root,client==='gemini'?'config/agents':'agents',`apk-${role}.${client==='chatgpt'?'toml':'md'}`))
19
19
  };
20
20
  }
21
21
  export function nativeRoles(client,config,selectedRoles=ROLE_IDS) {
22
22
  return selectedRoles.map(role=>{
23
23
  const selected=rolePreference(config,role),name='apk-'+role;
24
24
  const {description,prompt}=ROLE_DEFINITIONS.find(value=>value.id===role);
25
- if(client==='codex') return Object.entries({name,description,model:selected.model,...(selected.effort===null?{}:{model_reasoning_effort:selected.effort}),developer_instructions:prompt}).map(([k,v])=>`${k} = ${JSON.stringify(v)}\n`).join('');
25
+ if(client==='chatgpt') return Object.entries({name,description,model:selected.model,...(selected.effort===null?{}:{model_reasoning_effort:selected.effort}),developer_instructions:prompt}).map(([k,v])=>`${k} = ${JSON.stringify(v)}\n`).join('');
26
26
  const fields=[`name: ${name}`,`description: ${JSON.stringify(description)}`];
27
27
  // Grok selects the model from config.toml [subagents.models]; read-only roles use the bundled plan permission mode.
28
28
  if(client==='grok') {
@@ -47,18 +47,18 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
47
47
  for(const role of selectedRoles) {const selected=rolePreference(config,role);text.push(`For apk-${role}, select pool alias ${selected.model} (effort ${selected.effort??'native default'}).`);}
48
48
  text.push('Roles sharing an alias share its native effort default; an explicit effort pin for that alias also affects its native-default roles.');
49
49
  text.push("Use the Agent tool's model selection when spawning; the role file alone does not select a model. The concurrency limit is guidance; no native Agent cap is configured.");
50
- } else if(client==='agy') {
50
+ } else if(client==='gemini') {
51
51
  for(const role of selectedRoles)text.push(`When delegating to apk-${role}, use model tier ${rolePreference(config,role).model}.`);
52
52
  text.push('Per-role effort and a hard concurrency cap are not supported by the documented native agent settings.');
53
53
  }
54
54
  else if(client==='grok')text.push('Use the named role definitions for model selection. Per-role effort is not supported by the documented native agent settings; subagents use the main session effort. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
55
55
  else if(client==='claude')text.push('Use the named role definitions for model and effort selection. Session, project, managed settings, environment overrides and some spawn paths can supersede these user defaults; the native cap applies to ordinary Agent-tool spawns with documented exceptions.');
56
56
  else text.push('Use the named role definitions for model and effort selection. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
57
- if(client==='codex'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
57
+ if(client==='chatgpt'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
58
58
  if(config.avoidModels.length)text.push(`Avoid these models when delegating: ${config.avoidModels.join(', ')}. This is routing guidance.`);
59
59
  text.push(END);
60
60
  return text.join('\n')+'\n';
61
61
  }
62
62
  export function nativeWarnings(client) {
63
- return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='agy'?['agy-concurrency-guidance-only','agy-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['codex-native-precedence-limits'];
63
+ return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='gemini'?['gemini-concurrency-guidance-only','gemini-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['chatgpt-native-precedence-limits'];
64
64
  }
package/src/output.mjs CHANGED
@@ -1,17 +1,17 @@
1
1
  import {PRESETS} from './release.mjs';
2
- const LABELS={codex:'Codex',claude:'Claude Code',kimi:'Kimi',agy:'Antigravity',grok:'Grok Build'};
2
+ const LABELS={chatgpt:'ChatGPT',claude:'Claude',kimi:'Kimi',gemini:'Gemini',grok:'Grok'};
3
3
  const label=client=>LABELS[client]??'Tool';
4
4
  const code=value=>typeof value==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(value)?value:'OPERATION_FAILED';
5
5
  const PRESET_LABELS=Object.fromEntries(PRESETS.map(entry=>[entry.id,entry.label]));
6
6
  const preset=value=>Object.hasOwn(PRESET_LABELS,value)?' · '+PRESET_LABELS[value]:'';
7
7
  const NOTES={
8
- 'codex-native-precedence-limits':'Project and explicit client settings can override these defaults.',
8
+ 'chatgpt-native-precedence-limits':'Project and explicit client settings can override these defaults.',
9
9
  'claude-native-precedence-limits':'Project and explicit client settings can override these defaults.',
10
10
  'kimi-concurrency-guidance-only':'Kimi: the concurrency limit is guidance.',
11
11
  'kimi-model-pool-routing-guidance':'Kimi: the parent selects the model alias for each role.',
12
- 'agy-concurrency-guidance-only':'Antigravity: the concurrency limit is guidance.',
13
- 'agy-per-role-effort-unsupported':'Antigravity: native tiers do not support per-role effort.',
14
- 'grok-per-role-effort-unsupported':'Grok Build: native agent settings do not support per-role effort.',
12
+ 'gemini-concurrency-guidance-only':'Gemini: the concurrency limit is guidance.',
13
+ 'gemini-per-role-effort-unsupported':'Gemini: native tiers do not support per-role effort.',
14
+ 'grok-per-role-effort-unsupported':'Grok: native agent settings do not support per-role effort.',
15
15
  'grok-native-precedence-limits':'Project and explicit client settings can override these defaults.',
16
16
  'kimi-catalog-alias-resolved':'Kimi: an existing matching model alias was selected.',
17
17
  };
package/src/policy.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import catalog from '../models/catalog.json' with {type:'json'};
2
2
  import {ROLES,ROLE_IDS,selectedRoleIds} from './roles.mjs';
3
3
  export {ROLES} from './roles.mjs';
4
- export const CLIENTS = Object.freeze(['codex', 'claude', 'kimi', 'agy', 'grok']);
4
+ export const CLIENTS = Object.freeze(['chatgpt', 'claude', 'kimi', 'gemini', 'grok']);
5
5
  export const rolePreference=(config,role)=>config.roles?.[role]??config[ROLES[role]];
6
6
  export const NATIVE_EFFORTS=Object.freeze(Object.fromEntries(CLIENTS.map(client=>[client,Object.freeze([...catalog.clients[client].efforts])])));
7
7
  const identifier = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
@@ -27,7 +27,7 @@ export function validatePolicy(value) {
27
27
  for (const selected of [config.code,config.light,...Object.values(config.roles??{})]) {
28
28
  object(selected,['model','effort']); model(selected.model);
29
29
  if (client === 'kimi' && selected.model === 'primary') invalid();
30
- if(client==='agy'&&!['pro','flash','flash_lite','inherit'].includes(selected.model))invalid();
30
+ if(client==='gemini'&&!['pro','flash','flash_lite','inherit'].includes(selected.model))invalid();
31
31
  if(selected.effort!==null){
32
32
  if(!NATIVE_EFFORTS[client].includes(selected.effort))invalid();
33
33
  const known=catalog.clients[client].models.find(model=>model.id===selected.model);
package/src/release.mjs CHANGED
@@ -4,7 +4,7 @@ import {validatePolicy} from './policy.mjs';
4
4
 
5
5
  export const PACKAGE_VERSION=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
6
6
  const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonical(value[key])])):value;
7
- export const PRESETS=Object.freeze([{id:'fast',label:'Light',description:'Light models for every role. Limits last longest.'},{id:'balanced',label:'Standard',description:'Stronger models for code, review, tests and docs. Fits most plans.'},{id:'quality',label:'Top-tier',description:'High-end models for most roles. Uses limits fastest.',topModels:Object.freeze({codex:'gpt-6-astra',claude:'claude-fable-5-1',kimi:'kimi-code/k3.1-pro'})}].map(Object.freeze));
7
+ export const PRESETS=Object.freeze([{id:'fast',label:'Light',description:'Light models for every role. Limits last longest.'},{id:'balanced',label:'Standard',description:'Stronger models for code, review, tests and docs. Fits most plans.'},{id:'quality',label:'Top-tier',description:'High-end models for most roles. Uses limits fastest.',topModels:Object.freeze({chatgpt:'gpt-6-astra',claude:'claude-fable-5-1',kimi:'kimi-code/k3.1-pro'})}].map(Object.freeze));
8
8
  export function presetPreferences(id='balanced'){
9
9
  if(!PRESETS.some(preset=>preset.id===id))throw Object.assign(new Error('Unknown preset.'),{code:'UNKNOWN_PRESET'});
10
10
  return validatePolicy(JSON.parse(fs.readFileSync(new URL(`../policies/${id}.json`,import.meta.url),'utf8')));
package/src/setup.mjs CHANGED
@@ -4,7 +4,7 @@ import { nativeHomeConflict } from './native.mjs';
4
4
  import { PACKAGE_VERSION } from './release.mjs';
5
5
  import { CLIENTS } from './policy.mjs';
6
6
 
7
- const ERRORS = new Set(['INVALID_PRESET','INVALID_RELEASE','LEGACY_RUNTIME_RECEIPT','INVALID_CLIENT','INVALID_POLICY','INVALID_JSON','INVALID_TOML','UNSUPPORTED_CONFIG','UNSUPPORTED_TOML_STRUCTURE','UNSUPPORTED_TOML_VALUE','CODEX_CAP_ALIAS_CONFLICT','OWNERSHIP_CONFLICT','INVALID_RECEIPT','OWNED_DRIFT','INSTALLATION_CONFLICT','POLICY_CONFLICT','UNSAFE_PATH','UNSAFE_FILE','STALE_PLAN','RECOVERY_REQUIRED','ROLLBACK_CONFLICT','INVALID_PLAN','PLATFORM_UNSUPPORTED','INSTALL_LOCKED','INSTALL_FAILED','EACCES','EPERM','ENOENT','ENOSPC','EIO','EROFS']);
7
+ const ERRORS = new Set(['INVALID_PRESET','INVALID_RELEASE','LEGACY_RUNTIME_RECEIPT','INVALID_CLIENT','INVALID_POLICY','INVALID_JSON','INVALID_TOML','UNSUPPORTED_CONFIG','UNSUPPORTED_TOML_STRUCTURE','UNSUPPORTED_TOML_VALUE','CHATGPT_CAP_ALIAS_CONFLICT','OWNERSHIP_CONFLICT','INVALID_RECEIPT','OWNED_DRIFT','INSTALLATION_CONFLICT','POLICY_CONFLICT','UNSAFE_PATH','UNSAFE_FILE','STALE_PLAN','RECOVERY_REQUIRED','ROLLBACK_CONFLICT','INVALID_PLAN','PLATFORM_UNSUPPORTED','INSTALL_LOCKED','INSTALL_FAILED','EACCES','EPERM','ENOENT','ENOSPC','EIO','EROFS']);
8
8
  const safeError = error => ERRORS.has(error?.code) ? error.code : 'SETUP_FAILED';
9
9
  function actions(client,warnings) {
10
10
  return warnings.map(code=>({client,code,automatable:false}));
package/src/share.mjs CHANGED
@@ -55,7 +55,7 @@ function shareRelease(release){
55
55
  }
56
56
  function shareChoice(choice,id,metadata){
57
57
  shareRecord(choice,['model','effort']);
58
- if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['agy','grok'].includes(id)||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
58
+ if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['gemini','grok'].includes(id)||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
59
59
  return {model:choice.model,effort:choice.effort};
60
60
  }
61
61
  function sharePayload(payload,release,metadata){