@aglit-ai/cli 1.0.17197 → 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
- name and AI provider, requests the required OS permissions, guides installation of
47
- the Aglit AI Chrome extension, and enables Anywhere Access. Team selection and
44
+ name and AI provider, requests the required OS permissions, enables Anywhere
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
@@ -130,27 +128,22 @@ Google currently rejects this proposed callback with `redirect_uri_mismatch`.
130
128
  No port change has shipped; provider registration and native acceptance remain
131
129
  required before qualifying coexistence.
132
130
 
133
- Anywhere Access requires a two-factor security code by default; an explicit off preference is preserved. In CLI headless mode, incoming connection requests display the code in a native window without opening the full app. This requires a logged-in graphical desktop session; an SSH-only/server environment cannot display the prompt. Codes are not printed to the terminal.
131
+ Anywhere Access defaults to no two-factor security code; an explicit on preference is preserved. When enabled, incoming connection requests display the code in a native window without opening the full app. This requires a logged-in graphical desktop session; an SSH-only/server environment cannot display the prompt. Codes are not printed to the terminal.
134
132
 
135
- After computer-use permissions, setup opens the published Aglit AI Chrome Web Store
136
- listing. Choose Add to Chrome and approve Chrome’s prompt. Setup waits until the
137
- Store extension is installed and enabled in Chrome’s selected profile; it never
138
- changes a disabled extension setting or browser policy. Installation detection is
139
- separate from browser-control connectivity.
133
+ After computer-use permissions, setup enables Anywhere Access before offering
134
+ the Aglit AI Chrome extension. Browser setup failures do not undo computer
135
+ registration. Run `aglit browser setup` to retry, or `aglit browser status --json`
136
+ to inspect it separately. Interactive browser setup waits for installation;
137
+ unattended setup returns the current browser status without waiting for a person.
138
+ It never changes a disabled extension setting or browser policy. Installation
139
+ detection is separate from browser-control connectivity.
140
140
 
141
141
  This release supports Apple Silicon Macs. Windows publication is deferred until its signed native runtime is available.
142
142
 
143
- On macOS, interactive `aglit init` also checks for the BlackHole 2ch audio cable
144
- and installs the official Homebrew cask when it is missing. Rerun `aglit init` to resume setup. Administrator
145
- approval happens in your terminal. Unattended setup reports the required action
146
- without waiting for a password. Audio setup preserves existing BlackHole variants,
147
- default audio devices and active CoreAudio sessions; restart macOS when the
148
- installer requires it. In the remote app, select BlackHole 2ch as the microphone
149
- for forwarded microphone audio. Browser speaker playback still requires a click
150
- when autoplay is blocked.
151
-
152
- When settings already exist, interactive `aglit init` shows the saved computer name,
153
- account and provider and offers to reuse them. Choosing reuse preserves the saved
154
- model connection without repeating discovery or selection. Explicit `--name` and
155
- `--provider` options override the corresponding saved values. Unattended init reuses
156
- 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,11 +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]
34
- aglit providers
35
- aglit models list | choose
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]
35
+ aglit browser setup|status [--json]
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
36
40
  aglit models login copilot [--reauth] [--json]
37
41
  aglit models login-status copilot --attempt ATTEMPT_ID [--json]
38
42
  aglit models publish | unpublish --connection CONNECTION_ID --model MODEL_ID
@@ -42,11 +46,10 @@ const help = `Aglit Computer Use
42
46
  aglit models pool POOL_ID --name NAME --connections local:ACCOUNT_A,local:ACCOUNT_B
43
47
  aglit models test --model MODEL_ID [--connection CONNECTION_ID] [--harness]
44
48
  aglit models unserve
45
- aglit invite EMAIL --team TEAM_ID
46
49
  aglit uninstall
47
50
 
48
51
  Aglit runs in the background. Use aglit ui to open the Desktop window.
49
- 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.
50
53
  Uninstall revokes access and purges only this installation. Mini Computer,
51
54
  VMs, shared browser profiles and original provider logins remain separate.
52
55
  Setup registers a Personal computer. Team invitations are separate commands.
@@ -71,20 +74,21 @@ async function main() {
71
74
  'two-factor': { type: 'string' }, connections: { type: 'string' }, attempt: { type: 'string' }, reauth: {type:'boolean'},
72
75
  harness: { type: 'boolean' },
73
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' },
74
- 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' },
75
78
  }, allowPositionals: true, strict: true });
76
79
  const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !values.json && !process.env.CI);
77
80
  if (values.help || !positionals.length && !interactive) { process.stdout.write(help); return; }
78
- const [command = 'init', email, connectionId] = positionals;
81
+ const [command = 'init', email, connectionId] = require('../src/command-groups.cjs').commandGroups(positionals);
79
82
  const options = {
80
- models: ['device','fingerprint','name','json','challenge','code','connection','connections','model','harness','attempt','reauth'],
81
- install: ['json', 'channel'], init: ['name', 'provider', 'account', 'json', 'channel', 'headed', 'headless'],
82
- 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'], 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: [],
83
86
  };
84
- if (!Object.hasOwn(options, command) || Object.keys(values).some(key => !options[command].includes(key)) || positionals.length > (command === 'models' ? 3 : ['invite', 'updates'].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.');
85
88
  if (values['two-factor'] !== undefined && !['on', 'off'].includes(values['two-factor']))
86
89
  throw Error('Choose --two-factor on or off.');
87
- if (installation.development && !['models','status','init','providers','enable','disable','ui','start'].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');
91
+ if (command === 'browser' && !['setup','status'].includes(email)) throw Error('Use aglit browser setup|status');
88
92
  if (command === 'updates' && !['on', 'off', 'status'].includes(email)) throw Error('Use aglit updates on|off|status');
89
93
  if (values.channel !== undefined && !['latest', 'canary', 'embedded'].includes(values.channel)) throw Error('Update channel must be latest, canary or embedded');
90
94
  if (values.team && !/^[A-Za-z0-9._:-]{1,128}$/.test(values.team)) throw Error('Invalid team ID');
@@ -131,7 +135,7 @@ async function main() {
131
135
  }
132
136
  }
133
137
  let name = values.name;
134
- const mode = command === 'ui' ? 'headed' : 'headless';
138
+ const mode = ['init','ui'].includes(command) ? 'headed' : 'headless';
135
139
  if (command === 'init') {
136
140
  if (!installed()) {
137
141
  if (!interactive && !name) throw Error('Provide --name for unattended setup');
@@ -140,11 +144,11 @@ async function main() {
140
144
  }
141
145
  if (!installed()) throw Error('Run aglit init to install and register this computer first.');
142
146
  installation.readInstallation();
143
- if (!installation.development && ['start', 'ui', 'init'].includes(command)) {
147
+ if (!installation.development && ['ui', 'init'].includes(command)) {
144
148
  const recovered = await updates.recoverActivation();
145
149
  if (recovered.updating) { ui.result(recovered); ui.finish('Update in progress'); return; }
146
150
  }
147
- if (!installation.development && ['start', 'ui', 'init'].includes(command)) updates.initialize(values.channel);
151
+ if (!installation.development && ['ui', 'init'].includes(command)) updates.initialize(values.channel);
148
152
  const configPath = path.join(installation.profile, 'ipc/sdk-ipc.json');
149
153
  const control = (action, fields = {}) => require('../src/control.cjs').control(configPath, action, fields);
150
154
  let state;
@@ -156,7 +160,7 @@ async function main() {
156
160
  if (updates.state().automatic && updates.runtimeStopped(installation.readInstallation())) {
157
161
  try { await updates.update(); } catch {
158
162
  if (fs.existsSync(path.join(installation.root, 'update-transaction.json')) || fs.existsSync(path.join(installation.root, 'update-activation.json'))) {
159
- 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');
160
164
  }
161
165
  ui.info('Automatic update deferred; starting the installed Desktop.');
162
166
  }
@@ -171,17 +175,22 @@ async function main() {
171
175
  throw Error('Computer Use did not become ready. See runtime.log in its installation directory.');
172
176
  });
173
177
  }
174
- if (!installation.development && state.running && ['status', 'start', 'ui', 'init'].includes(command)) updates.rememberStatus(state);
175
- 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);
176
180
  const modelQuestion = interactive ? async text => {
177
181
  const prompt = createInterface({ input: process.stdin, output: process.stderr });
178
182
  try { return await prompt.question(text); } finally { prompt.close(); }
179
183
  } : undefined;
180
184
  if (command === 'models') {
181
- 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));
182
186
  ui.finish('Model connections'); return;
183
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; }
184
189
  if (command === 'status') { ui.result(state); ui.finish('Computer status'); return; }
190
+ if (command === 'browser') {
191
+ ui.result(email === 'status' ? await control('browser-status') : await require('../src/browser-setup.cjs').setupBrowser(control, ui, interactive));
192
+ ui.finish('Browser extension'); return;
193
+ }
185
194
  if (command === 'console') { ui.result({ url: state.consoleUrl }); ui.finish('Console'); return; }
186
195
  if (command === 'rename') {
187
196
  if (!values.name) throw Error('Provide --name to rename this computer');
@@ -193,83 +202,55 @@ async function main() {
193
202
  name: values.name, ...(values.team ? { teamId: values.team } : {}),
194
203
  ...(values.workspace ? { workspaceId: values.workspace } : {}),
195
204
  })));
196
- 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;
197
215
  }
198
216
  if (command === 'teams' || command === 'pipelines') {
199
217
  const result = await ui.step(command === 'teams' ? 'Load your teams' : 'Load authorized pipeline devices', () => control(command));
200
218
  if (interactive) ui[command](result); else ui.result(result);
201
- if (command === 'teams' && values.open) await control('open-teams');
202
- if (command === 'pipelines' && values.open) await control('open-pipelines');
203
219
  ui.finish(command === 'teams' ? 'Your teams' : 'Your pipelines'); return;
204
220
  }
205
- if (command === 'start' || command === 'ui') {
221
+ if (command === 'ui') {
206
222
  const window = await control('window', { mode });
207
223
  state = { ...state, ...window };
208
224
  ui.result(state); ui.finish(mode === 'headed' ? 'Desktop window opened' : 'Running in the background'); return;
209
225
  }
210
226
  if (command === 'init') {
211
227
  assertSetupRuntime(state);
212
- if (!state.email) {
213
- state = await ui.step('Complete sign-in in your browser', async () => {
214
- await control('login');
215
- const deadline = Date.now() + 5 * 60000;
216
- while (Date.now() < deadline) {
217
- await delay(1500); const current = await control('status'); if (current.email) return current;
218
- }
219
- throw Error('Sign-in timed out. Run aglit init to resume.');
220
- });
221
- }
222
- if (values.account && state.email?.toLowerCase() !== values.account.toLowerCase())
223
- throw Error(`Signed in as ${state.email}; expected ${values.account}. Switch the browser sign-in account and rerun aglit init.`);
224
- const reuseSavedSetup = Boolean(state.name) && (!interactive || await ui.reuseSetup(state));
225
- if (interactive && !reuseSavedSetup && !values.account && !await ui.confirmAccount(state.email))
226
- throw Object.assign(Error('Setup cancelled before enabling remote access.'), { cancelled: true });
227
- ui.info(`Signed in as ${state.email}`);
228
- 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());
229
230
  name ||= state.name;
230
231
  if (!name) throw Error('Provide --name for unattended setup');
231
- const selected = reuseSavedSetup && !values.provider ? null :
232
- 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.');
233
235
  for (const warning of selected?.view.warnings || []) ui.info(warning);
234
236
  await ui.step('Save computer settings', () => control('configure', { name,
235
- ...(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 } : {}) }));
236
238
  if (process.platform === 'darwin') {
237
- try {
238
- const audio = require('../src/mac-audio.cjs').setup({ interactive });
239
- if (audio.actionRequired) ui.info(audio.actionRequired);
240
- if (audio.restartRequired) ui.info('BlackHole is installed. Restart this Mac when convenient to enable the audio cable.');
241
- if (!interactive) ui.result({ audio });
242
- } catch (error) {
243
- ui.info(`${error.message} Rerun aglit init to retry audio setup.`);
244
- }
245
239
  ui.permissions();
246
240
  }
247
241
  const permissions = await ui.step('Waiting for screen recording and input permissions', () => control('permissions'));
248
242
  if (!interactive) ui.result(permissions);
249
243
  if (permissions.permissions?.supported && (permissions.permissions.screenRecording !== 'granted' || permissions.permissions.accessibility !== 'granted'))
250
244
  throw Error('Grant Screen Recording and Accessibility in System Settings, then rerun aglit init. Anywhere Access has not been enabled.');
251
- let browser = await ui.step('Check Aglit AI browser extension', () => control('browser-status'));
252
- if (!browser.enabled) {
253
- const prepared = await ui.step('Set up Aglit AI browser extension', () => control('browser-setup'));
254
- ui.browserExtension(prepared);
255
- browser = await ui.step('Waiting for Chrome extension installation', async () => {
256
- const deadline = Date.now() + 180000;
257
- do {
258
- const current = await control('browser-status');
259
- if (current.installed && current.enabled) return current;
260
- await delay(1500);
261
- } while (Date.now() < deadline);
262
- throw Error('Add or enable the Aglit AI Chrome extension, then rerun aglit init.');
263
- });
264
- }
265
245
  state = await ui.step('Connect this computer to your account', async () => {
266
246
  await control('enable');
267
247
  return control('status');
268
248
  });
269
249
  if (!installation.development) updates.rememberStatus(state);
270
250
  if (typeof state.deviceId !== 'string' || !state.deviceId) throw Error('Registration returned no device ID. Check aglit status before retrying.');
251
+ const browser = await require('../src/browser-setup.cjs').setupOptionalBrowser(control, ui, interactive);
271
252
  state = { ...state, ...await control('window', { mode }) };
272
- state = { ...state, employeeUrl: new URL(`/employees/${encodeURIComponent(state.deviceId)}`, state.consoleUrl).href };
253
+ state = { ...state, browser, employeeUrl: new URL(`/employees/${encodeURIComponent(state.deviceId)}`, state.consoleUrl).href };
273
254
  ui.result(state); ui.finish('Anywhere Access enabled'); return;
274
255
  }
275
256
  if (command === 'invite') { ui.result(await control('invite', { teamId: values.team, email })); ui.finish('Invitation sent'); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglit-ai/cli",
3
- "version": "1.0.17197",
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,27 @@
1
+ 'use strict';
2
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
3
+ async function setupBrowser(control, ui, interactive) {
4
+ let browser = await ui.step('Check Aglit AI browser extension', () => control('browser-status'));
5
+ if (browser.enabled) return browser;
6
+ const prepared = await ui.step('Set up Aglit AI browser extension', () => control('browser-setup'));
7
+ ui.browserExtension(prepared);
8
+ if (!interactive) return control('browser-status');
9
+ return ui.step('Waiting for Chrome extension installation', async () => {
10
+ const deadline = Date.now() + 180000;
11
+ do {
12
+ browser = await control('browser-status');
13
+ if (browser.installed && browser.enabled) return browser;
14
+ await delay(1500);
15
+ } while (Date.now() < deadline);
16
+ throw Error('Add or enable the Aglit AI Chrome extension, then run aglit browser setup.');
17
+ });
18
+ }
19
+ async function setupOptionalBrowser(control, ui, interactive) {
20
+ try { return await setupBrowser(control, ui, interactive); }
21
+ catch (error) {
22
+ if (error.cancelled) throw error;
23
+ ui.info('Browser setup is incomplete: ' + error.message + '\nRetry with aglit browser setup. Anywhere Access remains enabled.');
24
+ return { installed: false, enabled: false, setupRequired: true };
25
+ }
26
+ }
27
+ module.exports = {setupBrowser, setupOptionalBrowser};
@@ -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 };
@@ -195,6 +195,21 @@ async function install(channel) {
195
195
  if (bootstrap) require('./updates.cjs').adoptBootstrap(bootstrap);
196
196
  return { installed: true, ...receipt };
197
197
  }
198
+ function runtimePorts() {
199
+ const file = path.join(root, 'runtime-ports.json');
200
+ if (fs.existsSync(file)) {
201
+ const stat = fs.lstatSync(file);
202
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 1024 ||
203
+ process.platform !== 'win32' && (stat.uid !== process.getuid() || stat.mode & 0o077)) throw Error('Private runtime ports required');
204
+ const ports = JSON.parse(fs.readFileSync(file, 'utf8'));
205
+ if (!Array.isArray(ports) || ports.length !== 5 || new Set(ports).size !== 5 || ports.some(port => !Number.isInteger(port) || port < 1024 || port > 65535)) throw Error('Runtime ports differ');
206
+ return ports;
207
+ }
208
+ const probe = `const net=require('node:net');(async()=>{for(let base=49117;base<50117;base+=10){const servers=[];try{for(let i=0;i<5;i++){const server=net.createServer();servers.push(server);await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(base+i,'127.0.0.1',resolve);});}console.log(JSON.stringify(servers.map(server=>server.address().port)));return;}catch{}finally{for(const server of servers)server.close();}}process.exitCode=1;})()`;
209
+ const ports = JSON.parse(run(process.execPath, ['-e', probe]));
210
+ fs.writeFileSync(file, JSON.stringify(ports) + '\n', {flag:'wx',mode:0o600});
211
+ return ports;
212
+ }
198
213
  function start() {
199
214
  if (development) throw Error('Start the source runtime with scripts/dev/run-computer-use.cjs before invoking this CLI');
200
215
  if (fs.existsSync(path.join(root, 'uninstalling.json'))) throw Error('Computer Use is uninstalling. Run aglit uninstall to finish cleanup.');
@@ -202,6 +217,7 @@ function start() {
202
217
  '$p=[Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()); if($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)){throw "Run aglit init from a non-elevated interactive user terminal"}']);
203
218
  const installed = readInstallation();
204
219
  verifyPublisher(installed.executable);
220
+ runtimePorts();
205
221
  const log = fs.openSync(path.join(root, 'runtime.log'), 'a', 0o600);
206
222
  const env = { ...process.env };
207
223
  // Do not let an operator's guest/development environment adopt another identity.
@@ -222,10 +238,10 @@ function start() {
222
238
  // Bind the result to the exact installed executable and current OS user.
223
239
  const deadline = Date.now() + 10000;
224
240
  do {
225
- const rows = execFileSync('/bin/ps', ['-axo', 'pid=,uid=,comm='], {encoding: 'utf8', timeout: 15000, maxBuffer: 8 * 1024 ** 2});
241
+ const rows = execFileSync('/bin/ps', ['-axo', 'pid=,uid=,args='], {encoding: 'utf8', timeout: 15000, maxBuffer: 8 * 1024 ** 2});
226
242
  const matches = rows.split('\n').flatMap(line => {
227
243
  const row = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
228
- return row && Number(row[2]) === process.getuid() && row[3].trim() === installed.executable ? [Number(row[1])] : [];
244
+ return row && Number(row[2]) === process.getuid() && row[3].trim() === installed.executable + ' --computer-use' ? [Number(row[1])] : [];
229
245
  });
230
246
  if (matches.length > 1) throw Error('Multiple installed Desktop processes; activation remains pending');
231
247
  if (matches.length === 1) return { pid: matches[0] };
@@ -278,4 +294,4 @@ function readUninstall() {
278
294
  !Number.isSafeInteger(receipt.pid) || receipt.pid <= 0) throw Error('Invalid uninstall receipt');
279
295
  return receipt;
280
296
  }
281
- module.exports = { development, root, profile, readInstallation, install, start, purge, purgeIncomplete, readUninstall, privateRoot, canonical, verifyPublisher, embeddedRelease, copyArchive, stageRuntime, verifyRuntime };
297
+ module.exports = { runtimePorts, development, root, profile, readInstallation, install, start, purge, purgeIncomplete, readUninstall, privateRoot, canonical, verifyPublisher, embeddedRelease, copyArchive, stageRuntime, verifyRuntime };
@@ -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; }