@aglit-ai/cli 1.0.17300 → 1.0.17343

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 CHANGED
@@ -9,7 +9,6 @@ After the CLI and a compatible signed desktop release have been published:
9
9
  pnpm add -g @aglit-ai/cli
10
10
  aglit
11
11
  aglit ui
12
- aglit start
13
12
  aglit status
14
13
  aglit disable
15
14
  aglit enable
@@ -20,9 +19,8 @@ Run `aglit` in an interactive terminal to open the guided setup. `aglit init`
20
19
  also runs this flow and installs the signed runtime if it is missing. Setup asks
21
20
  for a computer name and AI provider, then enables Anywhere Access. It runs in
22
21
  the background during setup, then opens the Desktop window when setup succeeds.
23
- `aglit start` and `aglit ui` open the window using the same app and profile.
24
- Use `aglit init --headless` or `aglit start --headless` to keep the window hidden
25
- without disabling its connection. `aglit --help` (or `aglit -h`) lists commands.
22
+ `aglit ui` opens the window using the same app and profile.
23
+ Close the window to leave Aglit running in the background; quit explicitly from its tray/menu icon. `aglit --help` (or `aglit -h`) lists commands.
26
24
 
27
25
  Setup opens Aglit's Google login in the browser,
28
26
  detects existing Codex/Claude logins, and uses a configured authenticated provider
@@ -32,22 +30,22 @@ accounts remain owned by the user; uninstall does not delete their original logi
32
30
 
33
31
  ```sh
34
32
  aglit init --name "Office Mac" --provider codex
35
- aglit providers
33
+ aglit models accounts
36
34
  aglit rename --name "Office Mac"
37
35
  aglit console
38
- aglit team --name "My team"
36
+ aglit teams init --name "My team"
39
37
  aglit teams --open
40
38
  aglit pipelines
41
39
  aglit pipelines --open
42
- aglit invite USER_EMAIL --team TEAM_ID
40
+ aglit teams invite USER_EMAIL --team TEAM_ID
43
41
  ```
44
42
 
45
43
  Setup registers a Personal computer with your signed-in account. It asks for its
46
44
  name and AI provider, requests the required OS permissions, enables Anywhere
47
45
  Access, then offers the Aglit AI Chrome extension. Team selection and
48
46
  pipeline discovery are separate from setup.
49
- Use `aglit team --name "My team"` to associate this computer with a team on your account. The CLI derives its installed workspace; no server-side workspace record or private-key provisioning is required. Manage invitations at teams.aglit.ai.
50
- Use `aglit teams` to inspect memberships and `aglit pipelines --open` to open the
47
+ Use `aglit teams init --name "My team"` to associate this computer with a team on your account. The CLI derives its installed workspace; no server-side workspace record or private-key provisioning is required. Manage invitations at teams.aglit.ai.
48
+ Use `aglit teams list` to inspect memberships and `aglit pipelines list` to inspect the
51
49
  encrypted pipeline viewer. A listed device does not prove its pipelines are connected.
52
50
 
53
51
  For unattended setup, use `aglit init --json --name "Office Mac" --provider codex
@@ -142,17 +140,10 @@ detection is separate from browser-control connectivity.
142
140
 
143
141
  This release supports Apple Silicon Macs. Windows publication is deferred until its signed native runtime is available.
144
142
 
145
- On macOS, interactive `aglit init` also checks for the BlackHole 2ch audio cable
146
- and installs the official Homebrew cask when it is missing. Rerun `aglit init` to resume setup. Administrator
147
- approval happens in your terminal. Unattended setup reports the required action
148
- without waiting for a password. Audio setup preserves existing BlackHole variants,
149
- default audio devices and active CoreAudio sessions; restart macOS when the
150
- installer requires it. In the remote app, select BlackHole 2ch as the microphone
151
- for forwarded microphone audio. Browser speaker playback still requires a click
152
- when autoplay is blocked.
153
-
154
- When settings already exist, interactive `aglit init` shows the saved computer name,
155
- account and provider and offers to reuse them. Choosing reuse preserves the saved
156
- model connection without repeating discovery or selection. Explicit `--name` and
157
- `--provider` options override the corresponding saved values. Unattended init reuses
158
- saved settings by default. Permission and connection checks still run.
143
+ Run `aglit init` again to edit setup with the saved account, computer name, and AI provider preselected. Choose a different account in the account step to switch; cloud access is revoked before the previous account signs out.
144
+
145
+ Manage repositories with `aglit pipelines init --repo GIT_URL --branch master`, inspect local definitions with `aglit pipelines list`, and list accessible computers with `aglit pipelines devices`. Teams use `aglit teams list`, `aglit teams init`, and `aglit teams invite`. These commands do not open browser pages.
146
+
147
+ Use `aglit models setup` to choose a connection and enable model proxying for the root assistant. `aglit models choose` changes only the saved connection preference; `aglit models proxying on|off` changes the global proxy setting. `aglit models accounts` lists connected local accounts.
148
+
149
+ Connect an existing private Codex home explicitly with `aglit models import codex --home /absolute/path/to/codex-home --name Brian`. Importing another account preserves connected accounts and pools. The selected home remains the credential and refresh owner; passive discovery never scans homes or starts login. Create or update a pool with `aglit models pool POOL_ID --name NAME --connections local:ACCOUNT_A,local:ACCOUNT_B`. Native account setup and repository operations require a compatible updated runtime.
package/bin/aglit.cjs CHANGED
@@ -20,7 +20,6 @@ const help = `Aglit Computer Use
20
20
  [--name "Computer name"] [--provider codex|claude|auto]
21
21
  [--account EMAIL] [--channel latest|canary|embedded]
22
22
  aglit install Install the signed Desktop runtime
23
- aglit start
24
23
  aglit ui
25
24
  aglit status [--json]
26
25
  aglit rename --name "Computer name"
@@ -28,12 +27,16 @@ const help = `Aglit Computer Use
28
27
  aglit update [--check] [--channel latest|canary|embedded] [--json]
29
28
  aglit updates on|off|status [--json]
30
29
  aglit enable [--two-factor on|off] | disable
31
- aglit teams [--open]
32
- aglit team --name "Team name" [--team TEAM_ID] [--workspace WORKSPACE_ID]
33
- aglit pipelines [--open]
30
+ aglit teams list
31
+ aglit teams init --name "Team name" [--team TEAM_ID] [--workspace WORKSPACE_ID]
32
+ aglit teams invite EMAIL --team TEAM_ID
33
+ aglit pipelines list | devices
34
+ aglit pipelines init [--repo GIT_URL] [--branch BRANCH]
34
35
  aglit browser setup|status [--json]
35
- aglit providers
36
- aglit models list | choose
36
+ aglit models import codex --home ABSOLUTE_CODEX_HOME --name NAME
37
+ aglit models proxying on|off
38
+ aglit models accounts
39
+ aglit models list | setup | choose
37
40
  aglit models login copilot [--reauth] [--json]
38
41
  aglit models login-status copilot --attempt ATTEMPT_ID [--json]
39
42
  aglit models publish | unpublish --connection CONNECTION_ID --model MODEL_ID
@@ -43,11 +46,10 @@ const help = `Aglit Computer Use
43
46
  aglit models pool POOL_ID --name NAME --connections local:ACCOUNT_A,local:ACCOUNT_B
44
47
  aglit models test --model MODEL_ID [--connection CONNECTION_ID] [--harness]
45
48
  aglit models unserve
46
- aglit invite EMAIL --team TEAM_ID
47
49
  aglit uninstall
48
50
 
49
51
  Aglit runs in the background. Use aglit ui to open the Desktop window.
50
- Use --json with install, init, start or status for non-interactive JSON output.
52
+ Use --json with install, init, ui or status for non-interactive JSON output.
51
53
  Uninstall revokes access and purges only this installation. Mini Computer,
52
54
  VMs, shared browser profiles and original provider logins remain separate.
53
55
  Setup registers a Personal computer. Team invitations are separate commands.
@@ -72,20 +74,20 @@ async function main() {
72
74
  'two-factor': { type: 'string' }, connections: { type: 'string' }, attempt: { type: 'string' }, reauth: {type:'boolean'},
73
75
  harness: { type: 'boolean' },
74
76
  device: { type: 'string' }, fingerprint: { type: 'string' }, challenge: { type: 'string' }, code: { type: 'string' }, connection: { type: 'string' }, model: { type: 'string' }, name: { type: 'string' }, provider: { type: 'string' }, account: { type: 'string' },
75
- team: { type: 'string' }, workspace: { type: 'string' }, check: { type: 'boolean' }, channel: { type: 'string' }, open: { type: 'boolean' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
77
+ home: { type: 'string' }, repo: { type: 'string' }, branch: { type: 'string' }, team: { type: 'string' }, workspace: { type: 'string' }, check: { type: 'boolean' }, channel: { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
76
78
  }, allowPositionals: true, strict: true });
77
79
  const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !values.json && !process.env.CI);
78
80
  if (values.help || !positionals.length && !interactive) { process.stdout.write(help); return; }
79
- const [command = 'init', email, connectionId] = positionals;
81
+ const [command = 'init', email, connectionId] = require('../src/command-groups.cjs').commandGroups(positionals);
80
82
  const options = {
81
- models: ['device','fingerprint','name','json','challenge','code','connection','connections','model','harness','attempt','reauth'],
82
- install: ['json', 'channel'], init: ['name', 'provider', 'account', 'json', 'channel', 'headed', 'headless'],
83
- start: ['json', 'channel', 'headed', 'headless'], ui: ['json'], status: ['json'], update: ['check', 'channel', 'json'], updates: ['json'], enable: ['two-factor'], disable: [], rename: ['name', 'json'], console: ['json'], teams: ['json', 'open'], team: ['name', 'team', 'workspace', 'json'], pipelines: ['open', 'json'], browser: ['json'], providers: ['json'], invite: ['team'], uninstall: [],
83
+ models: ['home','device','fingerprint','name','json','challenge','code','connection','connections','model','harness','attempt','reauth'],
84
+ install: ['json', 'channel'], init: ['name', 'provider', 'account', 'json', 'channel'],
85
+ ui: ['json', 'channel'], status: ['json'], update: ['check', 'channel', 'json'], updates: ['json'], enable: ['two-factor'], disable: [], rename: ['name', 'json'], console: ['json'], teams: ['json'], team: ['name', 'team', 'workspace', 'json'], pipelines: ['json'], 'pipelines-list': ['json'], 'pipelines-init': ['repo','branch','json'], 'models-accounts': ['json'], browser: ['json'], invite: ['team'], uninstall: [],
84
86
  };
85
- if (!Object.hasOwn(options, command) || Object.keys(values).some(key => !options[command].includes(key)) || positionals.length > (command === 'models' ? 3 : ['invite', 'updates', 'browser'].includes(command) ? 2 : 1)) throw Error('Invalid command or options. Run aglit --help.');
87
+ if (!Object.hasOwn(options, command) || Object.keys(values).some(key => !options[command].includes(key)) || require('../src/command-groups.cjs').commandGroups(positionals).length > (command === 'models' ? 3 : ['invite', 'updates', 'browser'].includes(command) ? 2 : 1)) throw Error('Invalid command or options. Run aglit --help.');
86
88
  if (values['two-factor'] !== undefined && !['on', 'off'].includes(values['two-factor']))
87
89
  throw Error('Choose --two-factor on or off.');
88
- if (installation.development && !['models','status','init','providers','enable','disable','ui','start','browser'].includes(command)) throw Error('Source profile supports model/setup, access and window commands; installed runtime management is separate');
90
+ if (installation.development && !['models','status','init','models-accounts','enable','disable','ui','browser'].includes(command)) throw Error('Source profile supports model/setup, access and window commands; installed runtime management is separate');
89
91
  if (command === 'browser' && !['setup','status'].includes(email)) throw Error('Use aglit browser setup|status');
90
92
  if (command === 'updates' && !['on', 'off', 'status'].includes(email)) throw Error('Use aglit updates on|off|status');
91
93
  if (values.channel !== undefined && !['latest', 'canary', 'embedded'].includes(values.channel)) throw Error('Update channel must be latest, canary or embedded');
@@ -133,7 +135,7 @@ async function main() {
133
135
  }
134
136
  }
135
137
  let name = values.name;
136
- const mode = command === 'ui' ? 'headed' : 'headless';
138
+ const mode = ['init','ui'].includes(command) ? 'headed' : 'headless';
137
139
  if (command === 'init') {
138
140
  if (!installed()) {
139
141
  if (!interactive && !name) throw Error('Provide --name for unattended setup');
@@ -142,11 +144,11 @@ async function main() {
142
144
  }
143
145
  if (!installed()) throw Error('Run aglit init to install and register this computer first.');
144
146
  installation.readInstallation();
145
- if (!installation.development && ['start', 'ui', 'init'].includes(command)) {
147
+ if (!installation.development && ['ui', 'init'].includes(command)) {
146
148
  const recovered = await updates.recoverActivation();
147
149
  if (recovered.updating) { ui.result(recovered); ui.finish('Update in progress'); return; }
148
150
  }
149
- if (!installation.development && ['start', 'ui', 'init'].includes(command)) updates.initialize(values.channel);
151
+ if (!installation.development && ['ui', 'init'].includes(command)) updates.initialize(values.channel);
150
152
  const configPath = path.join(installation.profile, 'ipc/sdk-ipc.json');
151
153
  const control = (action, fields = {}) => require('../src/control.cjs').control(configPath, action, fields);
152
154
  let state;
@@ -158,7 +160,7 @@ async function main() {
158
160
  if (updates.state().automatic && updates.runtimeStopped(installation.readInstallation())) {
159
161
  try { await updates.update(); } catch {
160
162
  if (fs.existsSync(path.join(installation.root, 'update-transaction.json')) || fs.existsSync(path.join(installation.root, 'update-activation.json'))) {
161
- const recovered = await updates.recoverActivation(); if (recovered.updating) throw Error('An update is still active; retry aglit start after it finishes');
163
+ const recovered = await updates.recoverActivation(); if (recovered.updating) throw Error('An update is still active; retry aglit ui after it finishes');
162
164
  }
163
165
  ui.info('Automatic update deferred; starting the installed Desktop.');
164
166
  }
@@ -173,16 +175,17 @@ async function main() {
173
175
  throw Error('Computer Use did not become ready. See runtime.log in its installation directory.');
174
176
  });
175
177
  }
176
- if (!installation.development && state.running && ['status', 'start', 'ui', 'init'].includes(command)) updates.rememberStatus(state);
177
- if (!installation.development && ['start', 'ui', 'init'].includes(command) && state.running) updates.ensureWorker(state.pid);
178
+ if (!installation.development && state.running && ['status', 'ui', 'init'].includes(command)) updates.rememberStatus(state);
179
+ if (!installation.development && ['ui', 'init'].includes(command) && state.running) updates.ensureWorker(state.pid);
178
180
  const modelQuestion = interactive ? async text => {
179
181
  const prompt = createInterface({ input: process.stdin, output: process.stderr });
180
182
  try { return await prompt.question(text); } finally { prompt.close(); }
181
183
  } : undefined;
182
184
  if (command === 'models') {
183
- ui.result(await require('../src/model-connections.cjs').modelCommand(control,email,connectionId,values,modelQuestion));
185
+ ui.result(await require('../src/model-connections.cjs').modelCommand(control,email,connectionId,values,modelQuestion,interactive ? ui.modelConnection : undefined));
184
186
  ui.finish('Model connections'); return;
185
187
  }
188
+ if (command === 'models-accounts') { const view=await require('../src/model-connections.cjs').listModelConnections(control);ui.result({accounts:view.connections.filter(value=>value.kind==='local'&&!value.id.startsWith('local:pool:')),warnings:view.warnings||[]});ui.finish('Model accounts');return; }
186
189
  if (command === 'status') { ui.result(state); ui.finish('Computer status'); return; }
187
190
  if (command === 'browser') {
188
191
  ui.result(email === 'status' ? await control('browser-status') : await require('../src/browser-setup.cjs').setupBrowser(control, ui, interactive));
@@ -199,55 +202,40 @@ async function main() {
199
202
  name: values.name, ...(values.team ? { teamId: values.team } : {}),
200
203
  ...(values.workspace ? { workspaceId: values.workspace } : {}),
201
204
  })));
202
- ui.finish('Manage your team at https://teams.aglit.ai/'); return;
205
+ ui.finish('Manage your team in Console settings: https://console.aglit.ai/?settings=teams'); return;
206
+ }
207
+ if (command === 'pipelines-list' || command === 'pipelines-init') {
208
+ let fields = {};
209
+ if (command === 'pipelines-init') {
210
+ const repositoryUrl = values.repo || require('node:child_process').execFileSync('git', ['remote','get-url','origin'], {encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim();
211
+ fields = { repository_url: repositoryUrl, branch: values.branch || 'master' };
212
+ }
213
+ const result = await control(command, fields);
214
+ ui.result(result); ui.finish(command === 'pipelines-init' ? 'Repository polling configured' : 'Pipelines'); return;
203
215
  }
204
216
  if (command === 'teams' || command === 'pipelines') {
205
217
  const result = await ui.step(command === 'teams' ? 'Load your teams' : 'Load authorized pipeline devices', () => control(command));
206
218
  if (interactive) ui[command](result); else ui.result(result);
207
- if (command === 'teams' && values.open) await control('open-teams');
208
- if (command === 'pipelines' && values.open) await control('open-pipelines');
209
219
  ui.finish(command === 'teams' ? 'Your teams' : 'Your pipelines'); return;
210
220
  }
211
- if (command === 'start' || command === 'ui') {
221
+ if (command === 'ui') {
212
222
  const window = await control('window', { mode });
213
223
  state = { ...state, ...window };
214
224
  ui.result(state); ui.finish(mode === 'headed' ? 'Desktop window opened' : 'Running in the background'); return;
215
225
  }
216
226
  if (command === 'init') {
217
227
  assertSetupRuntime(state);
218
- if (!state.email) {
219
- state = await ui.step('Complete sign-in in your browser', async () => {
220
- await control('login');
221
- const deadline = Date.now() + 5 * 60000;
222
- while (Date.now() < deadline) {
223
- await delay(1500); const current = await control('status'); if (current.email) return current;
224
- }
225
- throw Error('Sign-in timed out. Run aglit init to resume.');
226
- });
227
- }
228
- if (values.account && state.email?.toLowerCase() !== values.account.toLowerCase())
229
- throw Error(`Signed in as ${state.email}; expected ${values.account}. Switch the browser sign-in account and rerun aglit init.`);
230
- const reuseSavedSetup = Boolean(state.name) && (!interactive || await ui.reuseSetup(state));
231
- if (interactive && !reuseSavedSetup && !values.account && !await ui.confirmAccount(state.email))
232
- throw Object.assign(Error('Setup cancelled before enabling remote access.'), { cancelled: true });
233
- ui.info(`Signed in as ${state.email}`);
234
- if (!values.name && interactive && !reuseSavedSetup) name = await ui.name(state.name || os.hostname());
228
+ state = await require('../src/setup-account.cjs').setupAccount(control, ui, state, { interactive, expectedEmail: values.account });
229
+ if (!values.name && interactive) name = await ui.name(state.name || os.hostname());
235
230
  name ||= state.name;
236
231
  if (!name) throw Error('Provide --name for unattended setup');
237
- const selected = reuseSavedSetup && !values.provider ? null :
238
- await require('../src/model-connections.cjs').chooseModelConnection(control, { provider: values.provider, question: modelQuestion });
232
+ const selected = !interactive && state.name && !values.provider ? null :
233
+ await require('../src/model-connections.cjs').chooseModelConnection(control, { provider: values.provider, question: modelQuestion, ...(interactive ? { selectChoice: ui.modelConnection } : {}) });
234
+ if(selected?.unavailable)throw Error('Update the Aglit runtime to restore model discovery, then rerun aglit init.');
239
235
  for (const warning of selected?.view.warnings || []) ui.info(warning);
240
236
  await ui.step('Save computer settings', () => control('configure', { name,
241
- ...(selected?.connectionId ? { provider: 'aglit', model_connection_id: selected.connectionId } : values.provider && values.provider !== 'auto' ? { provider: values.provider } : {}) }));
237
+ ...(selected?.connectionId ? { model_connection_id: selected.connectionId } : values.provider && values.provider !== 'auto' ? { provider: values.provider } : {}) }));
242
238
  if (process.platform === 'darwin') {
243
- try {
244
- const audio = require('../src/mac-audio.cjs').setup({ interactive });
245
- if (audio.actionRequired) ui.info(audio.actionRequired);
246
- if (audio.restartRequired) ui.info('BlackHole is installed. Restart this Mac when convenient to enable the audio cable.');
247
- if (!interactive) ui.result({ audio });
248
- } catch (error) {
249
- ui.info(`${error.message} Rerun aglit init to retry audio setup.`);
250
- }
251
239
  ui.permissions();
252
240
  }
253
241
  const permissions = await ui.step('Waiting for screen recording and input permissions', () => control('permissions'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglit-ai/cli",
3
- "version": "1.0.17300",
3
+ "version": "1.0.17343",
4
4
  "description": "Install and manage Aglit Computer Use",
5
5
  "license": "UNLICENSED",
6
6
  "type": "commonjs",
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ function commandGroups(positionals) {
3
+ const [group, action, argument] = positionals;
4
+ if (['start','providers','team','invite'].includes(group)) throw Error('Use aglit ui, aglit models, or aglit teams. Run aglit --help.');
5
+ if (group === 'teams') {
6
+ if (action === 'list' && positionals.length === 2) return ['teams'];
7
+ if (action === 'init' && positionals.length === 2) return ['team'];
8
+ if (action === 'invite' && argument && positionals.length === 3) return ['invite',argument];
9
+ throw Error('Use aglit teams list, teams init, or teams invite EMAIL.');
10
+ }
11
+ if (group === 'pipelines') {
12
+ if (positionals.length === 2 && ['list','init','devices'].includes(action)) return [action === 'devices' ? 'pipelines' : 'pipelines-'+action];
13
+ throw Error('Use aglit pipelines list, pipelines init, or pipelines devices.');
14
+ }
15
+ if (group === 'models' && action === 'accounts' && positionals.length === 2) return ['models-accounts'];
16
+ return positionals;
17
+ }
18
+ module.exports = { commandGroups };
@@ -11,9 +11,21 @@ async function listModelConnections(control) {
11
11
  warnings: ['Model endpoint discovery is unavailable. Existing provider settings are unchanged; retry aglit models list later.'] };
12
12
  }
13
13
  }
14
- async function chooseModelConnection(control, { provider, question } = {}) {
14
+ async function chooseModelConnection(control, { provider, question, selectChoice } = {}) {
15
15
  let view = await listModelConnections(control);
16
16
  if (view.selection.status === 'discovery-unavailable') return { view, connectionId: null, unavailable: true };
17
+ if (selectChoice) {
18
+ const choices = view.connections.filter(value => value.models.length && (value.available || value.kind === 'remote') &&
19
+ (!provider || ['auto','aglit'].includes(provider) || value.models.some(model=>model.provider===provider)));
20
+ if (!choices.length) throw Error('No model connection is ready. Connect an account with aglit models.');
21
+ const id = await selectChoice(choices, view.preferred_connection_id || view.selection.connection_id || choices[0].id);
22
+ const selected = choices.find(value=>value.id===id); if (!selected) throw Error('Choose a listed model connection');
23
+ if (selected.kind === 'remote' && !selected.available)
24
+ await modelCommand(control,'register',undefined,{device:selected.device_id,fingerprint:selected.identity_fingerprint,name:selected.name},question);
25
+ view = await control('models-prefer',{connection_id:id});
26
+ if (view.selection.status !== 'selected' || view.selection.connection_id !== id) throw Error('Selected model connection is unavailable');
27
+ return {view,connectionId:id};
28
+ }
17
29
  // Init/choose is an explicit connection action. Passive models-list never
18
30
  // prompts; reconnect only the saved/chosen endpoint, never every known host.
19
31
  if (['preferred-unavailable','setup-required'].includes(view.selection.status)) {
@@ -51,9 +63,11 @@ async function chooseModelConnection(control, { provider, question } = {}) {
51
63
  return { view, connectionId: selection.connection_id };
52
64
  }
53
65
  function validateModelCommand(command, id, values) {
54
- const allowed = {list:['json'],login:['json','reauth'],'login-status':['attempt','json'],test:['connection','model','harness'],choose:[],pool:['name','connections'],publish:['connection','model'],unpublish:['connection','model'],unserve:[],register:['device','fingerprint','name','challenge','code'],prefer:[],remove:[]};
66
+ const allowed = {list:['json'],import:['home','name','json'],proxying:[],login:['json','reauth'],'login-status':['attempt','json'],test:['connection','model','harness'],setup:[],choose:[],pool:['name','connections'],publish:['connection','model'],unpublish:['connection','model'],unserve:[],register:['device','fingerprint','name','challenge','code'],prefer:[],remove:[]};
55
67
  if (!Object.hasOwn(allowed,command) || Object.keys(values).some(key=>!allowed[command].includes(key)) ||
56
- (!['prefer','remove','pool','login','login-status'].includes(command) && id) || (['prefer','remove','pool','login','login-status'].includes(command) && !id)) throw Error('Invalid model command or options');
68
+ (!['prefer','remove','pool','login','login-status','import','proxying'].includes(command) && id) || (['prefer','remove','pool','login','login-status','import','proxying'].includes(command) && !id)) throw Error('Invalid model command or options');
69
+ if(command==='import'&&(id!=='codex'||typeof values.home!=='string'||!require('node:path').isAbsolute(values.home)||values.home.length>4096||/[\x00-\x1f\x7f]/.test(values.home)||typeof values.name!=='string'||!values.name.trim()||values.name.length>100||/[\x00-\x1f\x7f]/.test(values.name)))throw Error('Use aglit models import codex --home ABSOLUTE_CODEX_HOME --name NAME');
70
+ if(command==='proxying'&&!['on','off'].includes(id))throw Error('Use aglit models proxying on|off');
57
71
  if(['login','login-status'].includes(command)&&(id!=='copilot'||command==='login-status'&&!/^[a-f0-9-]{36}$/.test(values.attempt??'')))throw Error('Use aglit models login copilot or aglit models login-status copilot --attempt ATTEMPT_ID');
58
72
  if(values.reauth!==undefined&&values.reauth!==true)throw Error('Use --reauth only to explicitly replace the Copilot login');
59
73
  if (command === 'pool' && (!/^[A-Za-z0-9._-]{1,80}$/.test(id) || !values.name?.trim() || values.name.length > 100 ||
@@ -67,8 +81,10 @@ function validateModelCommand(command, id, values) {
67
81
  if (['publish','unpublish'].includes(command) && (!/^local:[A-Za-z0-9._:-]{1,180}$/.test(values.connection||'') || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(values.model||''))) throw Error('Choose a local --connection and --model');
68
82
  if (command === 'remove' && !id.startsWith('remote:')) throw Error('A registered remote connection ID is required');
69
83
  }
70
- async function modelCommand(control, command, id, values, question) {
84
+ async function modelCommand(control, command, id, values, question, selectChoice) {
71
85
  validateModelCommand(command,id,values);
86
+ if(command==='import')return control('models-import',{provider_id:id,home:values.home,name:values.name});
87
+ if(command==='proxying')return control('models-proxying',{enabled:String(id==='on')});
72
88
  if (command === 'list') return listModelConnections(control);
73
89
  if(command==='login'||command==='login-status')return control(command==='login'?'models-login':'models-login-status',{provider_id:id,...(command==='login-status'?{attempt_id:values.attempt}:values.reauth?{reauth:'true'}:{})});
74
90
  if (command === 'test') {
@@ -106,7 +122,9 @@ async function modelCommand(control, command, id, values, question) {
106
122
  }
107
123
  if (command === 'publish' || command === 'unpublish') return control('models-publish',{connection_id:values.connection,model_id:values.model,published:String(command==='publish')});
108
124
  if (command === 'unserve') return control('models-unpublish-all');
109
- if (command === 'choose') return (await chooseModelConnection(control,{question})).view;
125
+ if (command === 'setup' || command === 'choose') { const selected=await chooseModelConnection(control,{question,selectChoice});
126
+ if(selected.unavailable)throw Error('Update the Aglit runtime to restore model discovery before configuring model proxying');
127
+ return command==='setup'?control('models-setup',{connection_id:selected.connectionId}):selected.view; }
110
128
  throw Error('Use aglit models list|register|prefer|remove|publish|unpublish|unserve|choose');
111
129
  }
112
130
  module.exports={chooseModelConnection,listModelConnections,modelCommand,validateModelCommand};
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+ async function setupAccount(control, ui, state, { interactive, expectedEmail, delay = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
3
+ if (state.email && interactive) {
4
+ const choice = await ui.account(state.email);
5
+ if (choice === 'change') {
6
+ try { await control('account-sign-out'); }
7
+ catch (error) { if (/Unknown Computer Use command/.test(error.message)) throw Error('Update the Aglit runtime before switching accounts. The current account is unchanged.'); throw error; }
8
+ state = await control('status');
9
+ if (state.email) throw Error('The previous account has not signed out. Setup stopped.');
10
+ }
11
+ }
12
+ if (!state.email) {
13
+ state = await ui.step('Sign in to Aglit in your browser', async () => {
14
+ await control('login'); const deadline = Date.now() + 5 * 60000;
15
+ while (Date.now() < deadline) { await delay(1500); const current = await control('status'); if (current.email) return current; }
16
+ throw Error('Sign-in timed out. Run aglit init to resume.');
17
+ });
18
+ ui.selection('Aglit account', state.email);
19
+ }
20
+ if (expectedEmail && state.email.toLowerCase() !== expectedEmail.toLowerCase())
21
+ throw Error(`Signed in as ${state.email}; expected ${expectedEmail}. Rerun aglit init and choose a different account.`);
22
+ return state;
23
+ }
24
+ module.exports = { setupAccount };
package/src/ui.cjs CHANGED
@@ -40,15 +40,12 @@ async function createUi(interactive) {
40
40
  return teams;
41
41
  },
42
42
  pipelines(value) {
43
- p.note((value.devices.length ? value.devices.map(device => `${clean(device.name)}${device.platform ? ' · ' + clean(device.platform) : ''}`).join('\n') + '\n\n' : 'No authorized pipeline devices.\n\n') +
44
- 'https://pipelines.aglit.ai/\nOpen the encrypted viewer with aglit pipelines --open.', 'Pipeline devices');
43
+ p.note((value.devices.length ? value.devices.map(device => `${clean(device.name)}${device.platform ? ' · ' + clean(device.platform) : ''}`).join('\n') + '\n\n' : 'No authorized pipeline devices.\n\n'), 'Pipeline devices');
45
44
  },
46
- reuseSetup: state => {
47
- p.note([`Computer ${clean(state.name)}`, `Account ${clean(state.email)}`,
48
- state.provider && `Provider ${clean(state.provider)}`].filter(Boolean).join('\n'), 'Saved setup');
49
- return answer(p.confirm({ message: 'Reuse these settings?', initialValue: true }));
50
- },
51
- confirmAccount: email => answer(p.confirm({ message: `Register with ${clean(email)}?`, initialValue: true })),
45
+ account: email => answer(p.select({ message: 'Aglit account', initialValue: 'keep',
46
+ options: [{value:'keep',label:clean(email)},{value:'change',label:'Use a different account'}] })),
47
+ modelConnection: (connections, preferred) => answer(p.select({ message:'AI provider', initialValue:preferred,
48
+ options:connections.map(value=>({value:value.id,label:clean(value.name),hint:value.kind==='remote'?'Shared account':'This computer'})) })),
52
49
  async step(label, action) {
53
50
  const spinner = p.spinner(); spinner.start(label);
54
51
  try { const value = await action(); spinner.stop(label); return value; }