@awak-app/simy-cli 0.7.19 → 0.7.20

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
@@ -248,15 +248,19 @@ Closing the window keeps the agent running. Open the app again or choose
248
248
  **Show SIMY** from its menu bar icon to return. **Quit SIMY** asks before
249
249
  interrupting active tasks. The normal `simy` command remains available.
250
250
 
251
- Signed patch/minor updates wait for idle and verify the new runtime before
252
- handoff; failed startup restores the previous installed version. After a runtime
253
- update, choose **Restart to update** to load the updated native interface. This
254
- action asks to stop running tasks before restarting. **Stop task…** in a task
251
+ The macOS **Automatic updates** switch saves this Mac's choice across restarts.
252
+ Compatible signed installations enable it by default. Patch/minor updates wait
253
+ for idle and verify the new runtime before handoff; failed startup restores the
254
+ previous installed version. The app then switches to the verified new interface
255
+ when no unfinished work or confirmation is pending. Turning the switch off keeps
256
+ manual checks and **Download DMG** available. An explicit `SIMY_AUTO_UPDATE=0`
257
+ remains authoritative, and major updates continue to use the DMG.
258
+ **Stop task…** in a task
255
259
  detail asks for confirmation and terminates its local Provider process, keeping
256
260
  existing output and history. An explicit update can likewise stop active work
257
261
  after confirmation; automatic updates still wait for idle. **⌘Q** exits SIMY and
258
- its background services directly, including when work is running. The window reports App and runtime versions separately while they
259
- differ, and normal app launches select the newer installed interface.
262
+ its background services directly, including when work is running. The window shows
263
+ the app version, and normal app launches select the newer installed interface.
260
264
 
261
265
  Older installers that predate native app relaunch support need a one-time
262
266
  reinstallation from the official download page to update their Applications
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.7.19",
3
+ "version": "0.7.20",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -86,6 +86,7 @@ export class CliAutoUpdater extends EventEmitter {
86
86
  install_mode: options.installMode,
87
87
  automatic: Boolean(
88
88
  !options.disabled &&
89
+ options.autoUpdatesEnabled !== false &&
89
90
  options.daemon &&
90
91
  (options.installMode === "global_npm" ||
91
92
  (options.installMode === MACOS_INSTALLER_MODE && options.installerMarker?.teamId)) &&
@@ -101,7 +102,20 @@ export class CliAutoUpdater extends EventEmitter {
101
102
  }
102
103
 
103
104
  snapshot() {
104
- return structuredClone({ ...this.#snapshot, requires_task_stop: !this.#isIdle() });
105
+ const supported = !this.#options.disabled && this.#options.daemon && this.#options.requestedPort === 0 &&
106
+ (this.#options.installMode === 'global_npm' || this.#options.installMode === MACOS_INSTALLER_MODE && Boolean(this.#options.installerMarker?.teamId));
107
+ return structuredClone({ ...this.#snapshot, auto_update_supported: Boolean(supported),
108
+ auto_update_enabled: this.#snapshot.automatic, auto_update_busy: Boolean(this.#applying || this.#confirmedUpdate), requires_task_stop: !this.#isIdle() });
109
+ }
110
+
111
+ setAutomaticUpdates(enabled) {
112
+ if (typeof enabled !== 'boolean') throw new Error('Choose whether automatic updates are enabled.');
113
+ if (enabled && !this.snapshot().auto_update_supported) throw new Error('Automatic updates are unavailable for this installation.');
114
+ if (this.#applying || this.#confirmedUpdate) throw new Error('Wait for the current update to finish.');
115
+ this.#options.autoUpdatesEnabled = enabled;
116
+ if (!enabled) this.#clearIdleTimer();
117
+ this.#setState(!enabled && this.#snapshot.state === 'waiting_for_idle' ? 'update_available' : this.#snapshot.state,
118
+ { automatic: enabled, error: null });
105
119
  }
106
120
 
107
121
  // Close admission before cancellation begins. The same transaction owns the
@@ -435,6 +449,7 @@ export async function createCliAutoUpdater({
435
449
  interactive,
436
450
  requestedPort = 0,
437
451
  disabled = false,
452
+ autoUpdatesEnabled = true,
438
453
  packageRoot = PACKAGE_ROOT,
439
454
  webOrigin,
440
455
  dependencies = {},
@@ -458,6 +473,7 @@ export async function createCliAutoUpdater({
458
473
  interactive: Boolean(interactive),
459
474
  requestedPort,
460
475
  disabled,
476
+ autoUpdatesEnabled,
461
477
  currentVersion: packageInfo.version,
462
478
  installMode,
463
479
  installerMarker,
@@ -14,7 +14,7 @@ import {
14
14
  } from "./durable-local-task-steps-contract.js";
15
15
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
16
16
 
17
- export const CLI_VERSION = "0.7.19";
17
+ export const CLI_VERSION = "0.7.20";
18
18
  export const CLI_API_CONTRACT_VERSION = 10;
19
19
 
20
20
  export function narrativeImageCapabilityMetadata() {
@@ -8,7 +8,7 @@ export function validDesktopAccessPath(path, slug) {
8
8
  }
9
9
  export const desktopAccessReady = value => value?.state === 'ready';
10
10
  const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization_open', 'organization_select', 'organization_cancel', 'organizations_refresh',
11
- 'check_update', 'update', 'prepare_update_restart', 'quit', 'task_stop', 'delivery_stop', 'delivery_pause', 'delivery_reject',
11
+ 'check_update', 'auto_update_set_enabled', 'update', 'prepare_update_restart', 'quit', 'task_stop', 'delivery_stop', 'delivery_pause', 'delivery_reject',
12
12
  'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members']);
13
13
  export function desktopActionAllowed(access, command) {
14
14
  return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
@@ -1,4 +1,5 @@
1
1
  import { createRemoteHostManager } from './session-supervision/remote-hosts.js';
2
+ import { readDesktopAutoUpdate, writeDesktopAutoUpdate } from './desktop-update-settings.js';
2
3
  import { createUpdateRuntime } from './update-detachment/runtime.js';
3
4
  import { createCoordinatorReconciler } from "./update-detachment/reconciliation-adapter.js";
4
5
  import { createLedgerCoordinatorAdapters } from "./update-detachment/ledger-adapter.js";
@@ -169,6 +170,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
169
170
  let encodedStatus = null, lastStatus = null, updateProgress = null;
170
171
  let remoteHosts;
171
172
  const emitStatus = status => {
173
+ const updateSettings = updater?.snapshot?.() || {};
174
+ status = { ...status, update: { ...status.update,
175
+ auto_update_supported: updateSettings.auto_update_supported === true,
176
+ auto_update_enabled: updateSettings.auto_update_enabled === true,
177
+ auto_update_busy: updateSettings.auto_update_busy === true } };
172
178
  if (env.SIMY_REMOTE_CHILD === "1") status = { ...status, remoteSessionProtocol: 1 };
173
179
  if (remoteHosts) {
174
180
  void remoteHosts.update(providerLinks.codex.connected ? status : { ...status, access: { state: "unavailable" } }).catch(() => {});
@@ -644,8 +650,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
644
650
  const sampledAccess = structuredClone(status.access || { state: 'unavailable', organizationId: null, organizationName: null });
645
651
  if (!await acceptStatusAccess(sampledAccess)) { publishAgain = true; return; }
646
652
  status.access = sampledAccess;
647
- status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.());
648
- if(!desktopAccessReady(sampledAccess))status.updateDetachment={...status.updateDetachment,tasks:[],actions:status.updateDetachment.actions.filter(action=>action==='cancel_detach_update')};
653
+ status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(sampledAccess)});
649
654
  const requestedLinks = await decorateProviderSettings(status);
650
655
  const sameScope = desktopAccessReady(currentAccess) &&
651
656
  accessScope(lastStatus?.access) === accessScope(currentAccess);
@@ -694,8 +699,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
694
699
  const sampledAccess = structuredClone(status.access || { state: 'unavailable', organizationId: null, organizationName: null });
695
700
  if (!await acceptStatusAccess(sampledAccess)) { publishAgain = true; return; }
696
701
  status.access = sampledAccess;
697
- status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.());
698
- if(!desktopAccessReady(sampledAccess))status.updateDetachment={...status.updateDetachment,tasks:[],actions:status.updateDetachment.actions.filter(action=>action==='cancel_detach_update')};
702
+ status.updateDetachment = updateRuntime.snapshot(coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(sampledAccess)});
699
703
  const requestedScope = accessScope(currentAccess);
700
704
  const requestedLinks = await decorateProviderSettings(status);
701
705
  const allRecords = await recordReader.list();
@@ -841,6 +845,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
841
845
  updater = await (dependencies.createUpdater || (process.platform === "win32" ? (() => createWindowsDesktopUpdater({ version: packageInfo.version, disabled: env.SIMY_AUTO_UPDATE === "0" })) : createCliAutoUpdater))({
842
846
  daemon: true, interactive: true, webOrigin,
843
847
  disabled: env.SIMY_AUTO_UPDATE === "0",
848
+ autoUpdatesEnabled: readDesktopAutoUpdate(root),
844
849
  ...(updatePackageRoot ? { packageRoot: updatePackageRoot } : {}),
845
850
  dependencies: { packageInfo: updatePackageRoot ? await readCliPackageInfo(updatePackageRoot) : packageInfo,
846
851
  logger: message => diagnostics.write(`update: ${message}`) },
@@ -1055,7 +1060,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
1055
1060
  return;
1056
1061
  }
1057
1062
  if(command.action==='cancel_detach_update') {
1058
- try {updateRuntime.validate(command,coordinator.snapshot(),updater?.snapshot?.());coordinator.cancel();emit({type:'result',id:command.id,ok:true});}
1063
+ try {updateRuntime.validate(command,coordinator.snapshot(),updater?.snapshot?.(),{accessReady:desktopAccessReady(currentAccess)});coordinator.cancel();emit({type:'result',id:command.id,ok:true});}
1059
1064
  catch {emit({type:'result',id:command.id,ok:false,message:'This update operation changed. Refresh its status.'});}
1060
1065
  return;
1061
1066
  }
@@ -1124,7 +1129,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
1124
1129
  const access = await accessSnapshot();
1125
1130
  const commandScope = accessScope(access);
1126
1131
  if (!desktopActionAllowed(access, command)) throw Object.assign(new Error('Desktop access is required.'), { code: 'desktop_access_required' });
1127
- const recoveryControl = ['provider_disconnect', 'session_unmonitor'].includes(action) ||
1132
+ const recoveryControl = ['provider_disconnect', 'session_unmonitor'].includes(action) || action === 'auto_update_set_enabled' && command.enabled === false ||
1128
1133
  ['session_set_auto_continue', 'session_set_email_reports', 'team_thread_share'].includes(action) && command.enabled === false;
1129
1134
  if (['detach_update', 'update_reconnect'].includes(action)) requireDesktopAccess(access);
1130
1135
  else if (!recoveryControl && desktopAccessReady(access) && !desktopActionAllowed({ state: 'unavailable' }, command)) await authorizeWork();
@@ -1278,6 +1283,16 @@ export async function startDesktopHost({ input = process.stdin, output = process
1278
1283
  if (!result.opened) throw new Error('Browser unavailable.');
1279
1284
  } catch { throw Object.assign(new Error('Browser unavailable.'), { code: 'desktop_browser_unavailable' }); }
1280
1285
  }
1286
+ } else if (action === 'auto_update_set_enabled') {
1287
+ if (typeof command.enabled !== 'boolean' || typeof updater.setAutomaticUpdates !== 'function') throw new Error('Automatic updates are unavailable.');
1288
+ const previous = updater.snapshot().auto_update_enabled === true;
1289
+ updater.setAutomaticUpdates(command.enabled);
1290
+ try { writeDesktopAutoUpdate(root, command.enabled); }
1291
+ catch (error) { updater.setAutomaticUpdates(previous); throw error; }
1292
+ emit({ type: 'result', id: command.id, ok: true });
1293
+ await publish();
1294
+ if (command.enabled) setImmediate(() => { if (!closed && !relay) void updater.checkNow().catch(() => {}); });
1295
+ return;
1281
1296
  } else if (action === "check_update") await updater.checkNow();
1282
1297
  else if (action === "update") {
1283
1298
  const status = await agent.getDesktopStatus();
@@ -0,0 +1,15 @@
1
+ import { join } from 'node:path';
2
+ import { readState, atomicSave } from './update-detachment/state-store.js';
3
+
4
+ export function readDesktopAutoUpdate(root) {
5
+ try {
6
+ const settings = readState(join(root, 'desktop-update-settings.json'));
7
+ return settings === null ? true : settings.enabled === true;
8
+ }
9
+ catch { return false; }
10
+ }
11
+
12
+ export function writeDesktopAutoUpdate(root, enabled) {
13
+ if (typeof enabled !== 'boolean') throw new Error('Choose whether automatic updates are enabled.');
14
+ atomicSave(join(root, 'desktop-update-settings.json'), { schemaVersion: 1, enabled });
15
+ }
@@ -259,18 +259,22 @@ export function createUpdateRuntime({ root, origin, readContext, listRecords, ob
259
259
  };
260
260
  return {bind,boundary,reconcile,store,gate:adapters.gate,
261
261
  async initialize() {if(!transaction)write({connectionState:readDispatchGate(root).connection,phase:'startup',updateState:'pending'});},
262
- snapshot(base,update) {
262
+ snapshot(base,update,{accessReady=true}={}) {
263
263
  if(ownerTransferred)transaction=readState(file);
264
264
  const action=base.state==='connected' && update?.state==='waiting_for_idle'?'detach_update':base.state==='detaching'?'cancel_detach_update':!base.busy && ['detached','recovery_required'].includes(base.state)?'update_reconnect':null;
265
265
  const frame={...base,schemaVersion:1,connectionState:base.state,
266
- updateState:transaction?.updateState || 'pending',tasks:rows(),actions:action?[action]:[]};
266
+ updateState:transaction?.updateState || 'pending',updaterState:update?.state || 'unknown',tasks:rows(),actions:action?[action]:[]};
267
+ // Revision the exact frame the native client sees. Filtering after the
268
+ // fingerprint makes an access change look like conflicting data at the
269
+ // same revision, which correctly fences all native update actions.
270
+ if(!accessReady) {frame.tasks=[];frame.actions=frame.actions.filter(action=>action==='cancel_detach_update');}
267
271
  const fingerprint=digest(frame);
268
272
  if(!ownerTransferred && transaction && transaction.presentation!==fingerprint)write({presentation:fingerprint});
269
273
  presented={...frame,operationId:transaction?.operationId,revision:transaction?.revision || 0};
270
274
  return presented;
271
275
  },
272
- validate(command,base,update) {
273
- const frame=this.snapshot(base,update);
276
+ validate(command,base,update,options) {
277
+ const frame=this.snapshot(base,update,options);
274
278
  if(command.operationId!==frame.operationId || command.revision!==frame.revision || !frame.actions.includes(command.action) || transaction?.claimedCommands?.includes(command.id))throw failure('stale_operation');
275
279
  write({claimedCommands:[...(transaction?.claimedCommands || []).slice(-255),command.id]});
276
280
  },