agentsw 0.12.0 → 0.12.1

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
@@ -325,7 +325,9 @@ Import still skips already configured accounts.
325
325
  written once rather than once per provider: it gets the active provider when it can accept it, or the single
326
326
  provider in the run that it can (Claude Code takes anthropic endpoints only, so an openai active provider does not
327
327
  starve it). With several candidates and the active provider not among them the choice is yours, so the app keeps
328
- pointing where it already does: syncing is not switching. A provider whose endpoint cannot be reached is reported
328
+ pointing where it already does: syncing is not switching. The same holds for an app that keeps a pointer of its
329
+ own — only the store's active provider may move it, so `sync -p other` writes that provider's block and models and
330
+ leaves the app's current model alone (`use` and `add` are what switch). A provider whose endpoint cannot be reached is reported
329
331
  and skipped, the rest still sync, and the run exits non-zero.
330
332
  - **Automatic lookup is not an automatic push:** enrichment updates saved metadata during add/quick/discover/import/refresh
331
333
  and never writes agent configs by itself. `sync` is the push, and it refreshes first: it fetches the provider's model
package/dist/commands.js CHANGED
@@ -801,9 +801,10 @@ function resolveProviderIds(store, opts, fallback) {
801
801
  return ids;
802
802
  }
803
803
  /**
804
- * Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`); for the rest
805
- * the last writer owns whatever "current" pointer the app keeps, so a multi-provider sync finishes
806
- * on the active provider and leaves the agents where the store says they already are.
804
+ * Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`); every other
805
+ * app only lets the store's active provider move its "current" pointer (see `ApplyOptions.keepSelection`),
806
+ * so a multi-provider sync finishes on the active provider and a run that does not include it leaves
807
+ * the pointer where the app already had it.
807
808
  */
808
809
  function activeLast(store, ids) {
809
810
  const active = store.active;
@@ -895,7 +896,7 @@ async function syncProviders(opts, sourceStore) {
895
896
  }
896
897
  }
897
898
  out(`${t(opts.keepDefault ? "sync.syncingKeepDefault" : "sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
898
- const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot, { keepDefault: opts.keepDefault });
899
+ const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot, { keepDefault: opts.keepDefault, keepSelection: provider.id !== store.active });
899
900
  if (outcome.failed?.length)
900
901
  failed.push(id);
901
902
  if (refresh?.missing.length) {
@@ -29,7 +29,7 @@ export const codex = transactionalTarget({
29
29
  return [codexConfigFile(), codexAuthFile()];
30
30
  },
31
31
  detect: () => fs.existsSync(codexDir()),
32
- async apply(provider) {
32
+ async apply(provider, options = {}) {
33
33
  const configFile = codexConfigFile();
34
34
  const authFile = codexAuthFile();
35
35
  const notes = [];
@@ -42,16 +42,18 @@ export const codex = transactionalTarget({
42
42
  if (config.model_providers !== undefined && !isJsonObject(config.model_providers)) {
43
43
  throw new Error(`${configFile}: expected model_providers to be a table`);
44
44
  }
45
- config.model_provider = provider.id;
46
- config.model = provider.defaultModel;
47
- const model = provider.models.find((m) => m.id === provider.defaultModel);
48
- if (provider.reasoningEffort && model?.reasoning !== false) {
49
- config.model_reasoning_effort = provider.reasoningEffort;
50
- }
51
- else {
52
- // The effort belongs to the active model: switching providers must not
53
- // inherit the previous provider's level (prune treats it as owned too).
54
- delete config.model_reasoning_effort;
45
+ if (!options.keepSelection) {
46
+ config.model_provider = provider.id;
47
+ config.model = provider.defaultModel;
48
+ const model = provider.models.find((m) => m.id === provider.defaultModel);
49
+ if (provider.reasoningEffort && model?.reasoning !== false) {
50
+ config.model_reasoning_effort = provider.reasoningEffort;
51
+ }
52
+ else {
53
+ // The effort belongs to the active model: switching providers must not
54
+ // inherit the previous provider's level (prune treats it as owned too).
55
+ delete config.model_reasoning_effort;
56
+ }
55
57
  }
56
58
  // Codex only speaks the OpenAI Responses API (wire_api = "chat" was removed Feb 2026).
57
59
  // requires_openai_auth = true makes Codex take the key from auth.json instead of an env var.
@@ -68,7 +68,7 @@ export const dsh = transactionalTarget({
68
68
  return [dshSettingsFile(), dshCredentialsFile()];
69
69
  },
70
70
  detect: () => fs.existsSync(dshDir()),
71
- async apply(provider) {
71
+ async apply(provider, options = {}) {
72
72
  const notes = [];
73
73
  const file = dshSettingsFile();
74
74
  const doc = parseSettings(file);
@@ -117,7 +117,9 @@ export const dsh = transactionalTarget({
117
117
  notes.push("dropped modelOverrides (llm-pi-ai refuses it beside an explicit models list)");
118
118
  }
119
119
  // The picker writes this section whole; a partial write would keep a stale reasoningEffort.
120
- doc.set("agent-default-model", doc.createNode({ provider: provider.id, model: provider.defaultModel }));
120
+ if (!options.keepSelection) {
121
+ doc.set("agent-default-model", doc.createNode({ provider: provider.id, model: provider.defaultModel }));
122
+ }
121
123
  const referenced = credentialReferences(doc, provider.id);
122
124
  const oldRef = typeof prev?.apiKeyEnv === "string" && isManagedCredentialRef(prev.apiKeyEnv, provider.id) && !referenced.has(prev.apiKeyEnv)
123
125
  ? prev.apiKeyEnv : undefined;
@@ -54,7 +54,7 @@ export const hermes = transactionalTarget({
54
54
  protocols: ["openai", "anthropic"],
55
55
  configPaths: [hermesConfigFile(), hermesEnvFile()],
56
56
  detect: () => fs.existsSync(hermesHome()),
57
- async apply(provider) {
57
+ async apply(provider, options = {}) {
58
58
  const configFile = hermesConfigFile();
59
59
  const envFile = hermesEnvFile();
60
60
  const notes = [];
@@ -101,8 +101,10 @@ export const hermes = transactionalTarget({
101
101
  else
102
102
  doc.deleteIn([...modelAt, "context_length"]);
103
103
  }
104
- doc.setIn(["model", "provider"], provider.id);
105
- doc.setIn(["model", "default"], provider.defaultModel);
104
+ if (!options.keepSelection) {
105
+ doc.setIn(["model", "provider"], provider.id);
106
+ doc.setIn(["model", "default"], provider.defaultModel);
107
+ }
106
108
  const envText = readTextIfExists(envFile) ?? "";
107
109
  const assignments = envAssignments(envFile, envText);
108
110
  const referenced = credentialReferences(doc, provider.id);
@@ -19,7 +19,7 @@ export const opencode = transactionalTarget({
19
19
  },
20
20
  detect: () => opencodeConfigFiles().some((file) => fs.existsSync(file)) ||
21
21
  opencodeConfigDirs().some((dir) => fs.existsSync(dir)),
22
- async apply(provider) {
22
+ async apply(provider, options = {}) {
23
23
  const configFile = opencodePrimaryConfigFile();
24
24
  const notes = [];
25
25
  const existed = readTextIfExists(configFile) !== undefined;
@@ -94,14 +94,16 @@ export const opencode = transactionalTarget({
94
94
  models,
95
95
  };
96
96
  config.provider = providerMap;
97
- config.model = `${provider.id}/${provider.defaultModel}`;
98
- if (provider.smallModel) {
99
- config.small_model = `${provider.id}/${provider.smallModel}`;
100
- }
101
- else if (typeof config.small_model === "string" && config.small_model.startsWith(`${provider.id}/`)) {
102
- // This provider's own previous small model is obsolete; another provider's
103
- // entry stays untouched, exactly as prune scopes its cleanup.
104
- delete config.small_model;
97
+ if (!options.keepSelection) {
98
+ config.model = `${provider.id}/${provider.defaultModel}`;
99
+ if (provider.smallModel) {
100
+ config.small_model = `${provider.id}/${provider.smallModel}`;
101
+ }
102
+ else if (typeof config.small_model === "string" && config.small_model.startsWith(`${provider.id}/`)) {
103
+ // This provider's own previous small model is obsolete; another provider's
104
+ // entry stays untouched, exactly as prune scopes its cleanup.
105
+ delete config.small_model;
106
+ }
105
107
  }
106
108
  const backup = backupFile(configFile);
107
109
  if (backup)
@@ -106,7 +106,7 @@ export function piStyleTarget(opts) {
106
106
  };
107
107
  modelsConfig.providers = providers;
108
108
  const modelsText = editJsoncObject(modelsDocument, modelsConfig);
109
- const settingsText = options.keepDefault ? undefined : editJsoncObject(settingsDocument, {
109
+ const settingsText = options.keepDefault || options.keepSelection ? undefined : editJsoncObject(settingsDocument, {
110
110
  ...settingsDocument.value,
111
111
  defaultProvider: provider.id,
112
112
  defaultModel: provider.defaultModel,
@@ -79,7 +79,7 @@ export const workbuddy = transactionalTarget({
79
79
  return [path.join(configDir(), "models.json"), path.join(configDir(), "settings.json")];
80
80
  },
81
81
  detect: () => fs.existsSync(configDir()),
82
- async apply(provider) {
82
+ async apply(provider, options = {}) {
83
83
  const dir = configDir();
84
84
  const modelsFile = path.join(dir, "models.json");
85
85
  const settingsFile = path.join(dir, "settings.json");
@@ -127,7 +127,8 @@ export const workbuddy = transactionalTarget({
127
127
  const availableModels = [...new Set([
128
128
  ...existingAvailable.filter((id) => !removedIds.has(id) || keptIds.has(id)), ...ourIds,
129
129
  ])];
130
- settings.model = provider.defaultModel;
130
+ if (!options.keepSelection)
131
+ settings.model = provider.defaultModel;
131
132
  const modelsBackup = backupFile(modelsFile);
132
133
  if (modelsBackup)
133
134
  notes.push(`backup: ${modelsBackup}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsw",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "CLI provider switcher for coding agents: manage OpenAI/Anthropic-protocol providers in one place and sync them, with models.dev metadata, into Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy and DeepSeek Harness",
5
5
  "type": "module",
6
6
  "repository": {