agentsw 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,14 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import YAML from "yaml";
4
- import { backupFile, home, localAppDataDir, readTextIfExists, writeFileAtomic } from "../fsutil.js";
3
+ import { backupFile, readTextIfExists, writeFileAtomic } from "../fsutil.js";
4
+ import { hermesConfigFile, hermesDir, hermesEnvFile } from "../app-paths.js";
5
5
  import { envAssignments, removeEnvAssignments, upsertEnvAssignment } from "../envfile.js";
6
6
  import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
7
7
  import { transactionalTarget } from "../target-transaction.js";
8
8
  import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
9
9
  import { sdkBaseUrl } from "./wire.js";
10
10
  function hermesHome() {
11
- const env = process.env.HERMES_HOME?.trim();
12
- if (env)
13
- return env;
14
- if (process.platform === "win32") {
15
- return localAppDataDir("hermes");
16
- }
17
- return path.join(home, ".hermes");
11
+ return hermesDir();
18
12
  }
19
13
  function parseConfig(file, text) {
20
14
  const doc = parseYamlMapping(file, text);
@@ -58,12 +52,11 @@ export const hermes = transactionalTarget({
58
52
  id: "hermes",
59
53
  name: "Hermes",
60
54
  protocols: ["openai", "anthropic"],
61
- configPaths: [path.join(hermesHome(), "config.yaml"), path.join(hermesHome(), ".env")],
55
+ configPaths: [hermesConfigFile(), hermesEnvFile()],
62
56
  detect: () => fs.existsSync(hermesHome()),
63
57
  async apply(provider) {
64
- const home = hermesHome();
65
- const configFile = path.join(home, "config.yaml");
66
- const envFile = path.join(home, ".env");
58
+ const configFile = hermesConfigFile();
59
+ const envFile = hermesEnvFile();
67
60
  const notes = [];
68
61
  const text = readTextIfExists(configFile);
69
62
  const doc = parseConfig(configFile, text);
@@ -133,9 +126,8 @@ export const hermes = transactionalTarget({
133
126
  return { app: this.id, changed: [configFile, envFile], notes };
134
127
  },
135
128
  async prune(provider) {
136
- const home = hermesHome();
137
- const configFile = path.join(home, "config.yaml");
138
- const envFile = path.join(home, ".env");
129
+ const configFile = hermesConfigFile();
130
+ const envFile = hermesEnvFile();
139
131
  const doc = parseConfig(configFile, readTextIfExists(configFile));
140
132
  const at = ["providers", provider.id];
141
133
  const hasProvider = doc.hasIn(at);
@@ -176,7 +168,7 @@ export const hermes = transactionalTarget({
176
168
  return changed.length ? { app: this.id, changed, notes } : { app: this.id, changed, notes, skipped: `no providers.${provider.id} entry or managed credential` };
177
169
  },
178
170
  current() {
179
- const text = readTextIfExists(path.join(hermesHome(), "config.yaml"));
171
+ const text = readTextIfExists(hermesConfigFile());
180
172
  if (!text)
181
173
  return undefined;
182
174
  try {
@@ -190,8 +182,7 @@ export const hermes = transactionalTarget({
190
182
  }
191
183
  },
192
184
  candidates() {
193
- const hh = hermesHome();
194
- const text = readTextIfExists(path.join(hh, "config.yaml"));
185
+ const text = readTextIfExists(hermesConfigFile());
195
186
  if (!text)
196
187
  return [];
197
188
  let parsed;
@@ -203,7 +194,7 @@ export const hermes = transactionalTarget({
203
194
  }
204
195
  if (!parsed?.providers)
205
196
  return [];
206
- const envFile = path.join(hh, ".env");
197
+ const envFile = hermesEnvFile();
207
198
  const envText = readTextIfExists(envFile) ?? "";
208
199
  const values = new Map(envAssignments(envFile, envText).map((assignment) => [assignment.name, assignment.value]));
209
200
  const readEnv = (key) => process.env[key] ?? values.get(key);
@@ -1,16 +1,18 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import YAML from "yaml";
4
- import { backupFile, home, readTextIfExists, writeFileAtomic } from "../fsutil.js";
3
+ import { backupFile, readTextIfExists, writeFileAtomic } from "../fsutil.js";
4
+ import { ompAgentDir, ompModelsFiles } from "../app-paths.js";
5
5
  import { looksLikeEnvName } from "../slug.js";
6
6
  import { transactionalTarget } from "../target-transaction.js";
7
7
  import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
8
8
  import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
9
9
  /** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
10
10
  const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
11
- const agentDir = path.join(home, ".omp", "agent");
12
- const modelsYml = path.join(agentDir, "models.yml");
13
- const modelsYaml = path.join(agentDir, "models.yaml");
11
+ /** omp has no directory override; the file set is shared with remove/rename. */
12
+ function modelFiles() {
13
+ const [yml, yaml] = ompModelsFiles();
14
+ return [yml, yaml];
15
+ }
14
16
  function parseModelsDocument(file, text) {
15
17
  const doc = parseYamlMapping(file, text);
16
18
  if (doc.hasIn(["providers"]) && !YAML.isMap(doc.getIn(["providers"]))) {
@@ -26,11 +28,14 @@ export const omp = transactionalTarget({
26
28
  id: "omp",
27
29
  name: "Oh My Pi",
28
30
  protocols: ["openai", "anthropic"],
29
- configPaths: [modelsYml],
30
- detect: () => fs.existsSync(path.join(home, ".omp")),
31
+ get configPaths() {
32
+ return [modelFiles()[0]];
33
+ },
34
+ detect: () => fs.existsSync(ompAgentDir()),
31
35
  async apply(provider) {
32
36
  const notes = [];
33
37
  // Respect an existing models.yaml if models.yml is absent (omp precedence: yml then yaml).
38
+ const [modelsYml, modelsYaml] = modelFiles();
34
39
  const file = !fs.existsSync(modelsYml) && fs.existsSync(modelsYaml) ? modelsYaml : modelsYml;
35
40
  const text = readTextIfExists(file);
36
41
  const doc = parseModelsDocument(file, text);
@@ -92,6 +97,7 @@ export const omp = transactionalTarget({
92
97
  return { app: this.id, changed: [file], notes };
93
98
  },
94
99
  async prune(provider) {
100
+ const [modelsYml, modelsYaml] = modelFiles();
95
101
  const file = fs.existsSync(modelsYml) ? modelsYml : fs.existsSync(modelsYaml) ? modelsYaml : undefined;
96
102
  if (!file)
97
103
  return { app: this.id, changed: [], notes: [], skipped: "no models.yml" };
@@ -109,6 +115,7 @@ export const omp = transactionalTarget({
109
115
  return { app: this.id, changed: [file], notes };
110
116
  },
111
117
  current() {
118
+ const [modelsYml, modelsYaml] = modelFiles();
112
119
  const file = fs.existsSync(modelsYml) ? modelsYml : fs.existsSync(modelsYaml) ? modelsYaml : undefined;
113
120
  if (!file)
114
121
  return undefined;
@@ -122,6 +129,7 @@ export const omp = transactionalTarget({
122
129
  }
123
130
  },
124
131
  candidates() {
132
+ const [modelsYml, modelsYaml] = modelFiles();
125
133
  const file = fs.existsSync(modelsYml) ? modelsYml : fs.existsSync(modelsYaml) ? modelsYaml : undefined;
126
134
  if (!file)
127
135
  return [];
@@ -1,27 +1,34 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
- import { appDataDir, backupFile, expandHome, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
- import { isJsonObject } from "../jsonc.js";
2
+ import { backupFile, readTextIfExists, writeFileAtomic } from "../fsutil.js";
3
+ import { isJsonObject, readJsoncObject, editJsoncObject } from "../jsonc.js";
4
+ import { opencodeConfigDirs, opencodeConfigFiles, opencodePrimaryConfigFile } from "../app-paths.js";
5
5
  import { transactionalTarget } from "../target-transaction.js";
6
- const customDir = process.env.OPENCODE_CONFIG_DIR?.trim();
7
- const configFile = customDir
8
- ? path.join(expandHome(customDir), "opencode.json")
9
- : path.join(appDataDir("opencode"), "opencode.json");
10
6
  /**
11
- * opencode: custom providers in ~/.config/opencode/opencode.json "provider" map,
12
- * backed by @ai-sdk/openai-compatible or @ai-sdk/anthropic npm loaders.
7
+ * opencode: custom providers in the "provider" map of its config document
8
+ * (opencode.json / opencode.jsonc / config.json under $OPENCODE_CONFIG_DIR or the
9
+ * default directory; $OPENCODE_CONFIG names one file explicitly), backed by
10
+ * @ai-sdk/openai-compatible or @ai-sdk/anthropic npm loaders. A JSONC round-trip
11
+ * keeps comments and every key agentsw does not own.
13
12
  */
14
13
  export const opencode = transactionalTarget({
15
14
  id: "opencode",
16
15
  name: "opencode",
17
16
  protocols: ["openai", "anthropic"],
18
- configPaths: [configFile],
19
- detect: () => fs.existsSync(path.dirname(configFile)),
17
+ get configPaths() {
18
+ return [opencodePrimaryConfigFile()];
19
+ },
20
+ detect: () => opencodeConfigFiles().some((file) => fs.existsSync(file)) ||
21
+ opencodeConfigDirs().some((dir) => fs.existsSync(dir)),
20
22
  async apply(provider) {
23
+ const configFile = opencodePrimaryConfigFile();
21
24
  const notes = [];
22
- const configValue = readJsonIfExists(configFile);
23
- const config = configValue === undefined ? { $schema: "https://opencode.ai/config.json" } : configValue;
24
- if (!isJsonObject(config) || (config.provider !== undefined && !isJsonObject(config.provider))) {
25
+ const existed = readTextIfExists(configFile) !== undefined;
26
+ const document = readJsoncObject(configFile);
27
+ // Only a missing file is initialized with the schema hint; an existing {} stays as-is.
28
+ const config = existed
29
+ ? { ...document.value }
30
+ : { $schema: "https://opencode.ai/config.json" };
31
+ if (config.provider !== undefined && !isJsonObject(config.provider)) {
25
32
  throw new Error(`${configFile}: expected config and provider to be JSON objects`);
26
33
  }
27
34
  const anthropic = provider.protocol === "anthropic";
@@ -88,26 +95,36 @@ export const opencode = transactionalTarget({
88
95
  };
89
96
  config.provider = providerMap;
90
97
  config.model = `${provider.id}/${provider.defaultModel}`;
91
- if (provider.smallModel)
98
+ if (provider.smallModel) {
92
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;
105
+ }
93
106
  const backup = backupFile(configFile);
94
107
  if (backup)
95
108
  notes.push(`backup: ${backup}`);
96
- writeFileAtomic(configFile, JSON.stringify(config, null, 2) + "\n");
109
+ writeFileAtomic(configFile, editJsoncObject(document, config));
97
110
  return { app: this.id, changed: [configFile], notes };
98
111
  },
99
112
  async prune(provider) {
100
- const config = readJsonIfExists(configFile);
101
- if (config !== undefined && (!isJsonObject(config) || (config.provider !== undefined && !isJsonObject(config.provider)))) {
113
+ const configFile = opencodePrimaryConfigFile();
114
+ const document = readJsoncObject(configFile);
115
+ const config = { ...document.value };
116
+ if (config.provider !== undefined && !isJsonObject(config.provider)) {
102
117
  throw new Error(`${configFile}: expected config and provider to be JSON objects`);
103
118
  }
104
- const providerMap = config?.provider;
105
- if (!config || !providerMap?.[provider.id]) {
119
+ const providerMap = { ...config.provider };
120
+ if (!providerMap[provider.id]) {
106
121
  return { app: this.id, changed: [], notes: [], skipped: `no provider.${provider.id} entry` };
107
122
  }
108
123
  delete providerMap[provider.id];
109
124
  if (Object.keys(providerMap).length === 0)
110
125
  delete config.provider;
126
+ else
127
+ config.provider = providerMap;
111
128
  const notes = [];
112
129
  if (typeof config.model === "string" && config.model.startsWith(`${provider.id}/`)) {
113
130
  delete config.model;
@@ -119,16 +136,16 @@ export const opencode = transactionalTarget({
119
136
  const backup = backupFile(configFile);
120
137
  if (backup)
121
138
  notes.push(`backup: ${backup}`);
122
- writeFileAtomic(configFile, JSON.stringify(config, null, 2) + "\n");
139
+ writeFileAtomic(configFile, editJsoncObject(document, config));
123
140
  return { app: this.id, changed: [configFile], notes };
124
141
  },
125
142
  current() {
126
- const config = readJsonIfExists(configFile);
127
- return config?.model;
143
+ const config = readJsoncObject(opencodePrimaryConfigFile()).value;
144
+ return typeof config.model === "string" ? config.model : undefined;
128
145
  },
129
146
  candidates() {
130
- const config = readJsonIfExists(configFile);
131
- if (!config?.provider)
147
+ const config = readJsoncObject(opencodePrimaryConfigFile()).value;
148
+ if (!config.provider)
132
149
  return [];
133
150
  const activeProvider = config.model?.includes("/") ? config.model.split("/")[0] : undefined;
134
151
  const activeModel = activeProvider && config.model && config.model.includes("/") ? config.model.split("/").slice(1).join("/") : undefined;
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { backupFile, home, writeFileAtomic } from "../fsutil.js";
3
+ import { backupFile, writeFileAtomic } from "../fsutil.js";
4
+ import { piDir, piModelsFile, piSettingsFile } from "../app-paths.js";
4
5
  import { editJsoncObject, isJsonObject, readJsoncObject } from "../jsonc.js";
5
6
  import { looksLikeEnvName } from "../slug.js";
6
7
  import { transactionalTarget } from "../target-transaction.js";
@@ -57,17 +58,15 @@ function thinkingLevelMap(efforts) {
57
58
  return map;
58
59
  }
59
60
  export function piStyleTarget(opts) {
60
- const resolveDir = () => {
61
- const env = process.env[opts.dirEnvVar];
62
- if (env)
63
- return env.startsWith("~") ? path.join(home, env.slice(1)) : env;
64
- return path.join(home, opts.configDirName);
65
- };
61
+ const id = opts.id;
62
+ const resolveDir = () => piDir(id);
66
63
  return transactionalTarget({
67
64
  id: opts.id,
68
65
  name: opts.name,
69
66
  protocols: ["openai", "anthropic"],
70
- configPaths: [path.join(resolveDir(), "models.json"), path.join(resolveDir(), "settings.json")],
67
+ get configPaths() {
68
+ return [piModelsFile(id), piSettingsFile(id)];
69
+ },
71
70
  detect: () => fs.existsSync(path.dirname(resolveDir())),
72
71
  async apply(provider) {
73
72
  const dir = resolveDir();
@@ -232,15 +231,5 @@ export function piStyleTarget(opts) {
232
231
  },
233
232
  });
234
233
  }
235
- export const pi = piStyleTarget({
236
- id: "pi",
237
- name: "pi",
238
- configDirName: ".pi/agent",
239
- dirEnvVar: "PI_CODING_AGENT_DIR",
240
- });
241
- export const prime = piStyleTarget({
242
- id: "prime",
243
- name: "prime-agent",
244
- configDirName: ".prime/agent",
245
- dirEnvVar: "PRIME_AGENT_CODING_AGENT_DIR",
246
- });
234
+ export const pi = piStyleTarget({ id: "pi", name: "pi" });
235
+ export const prime = piStyleTarget({ id: "prime", name: "prime-agent" });
@@ -1,13 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { appDataDir, backupFile, home, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
3
+ import { backupFile, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
4
  import { isJsonObject } from "../jsonc.js";
5
+ import { workbuddyDir } from "../app-paths.js";
5
6
  import { localProviderId } from "../provider-identity.js";
6
7
  import { providerIdFromBaseUrl, providerNameFromBaseUrl } from "../slug.js";
7
8
  import { transactionalTarget } from "../target-transaction.js";
8
9
  function configDir() {
9
- return process.env.WORKBUDDY_CONFIG_DIR?.trim() || process.env.CODEBUDDY_CONFIG_DIR?.trim() ||
10
- (process.platform === "win32" ? appDataDir("workbuddy") : path.join(home, ".workbuddy"));
10
+ return workbuddyDir();
11
11
  }
12
12
  function isModel(row) {
13
13
  return isJsonObject(row) && typeof row.id === "string";
@@ -74,7 +74,9 @@ export const workbuddy = transactionalTarget({
74
74
  id: "workbuddy",
75
75
  name: "WorkBuddy",
76
76
  protocols: ["openai"],
77
- configPaths: [path.join(configDir(), "models.json"), path.join(configDir(), "settings.json")],
77
+ get configPaths() {
78
+ return [path.join(configDir(), "models.json"), path.join(configDir(), "settings.json")];
79
+ },
78
80
  detect: () => fs.existsSync(configDir()),
79
81
  async apply(provider) {
80
82
  const dir = configDir();
package/dist/ui.js ADDED
@@ -0,0 +1,157 @@
1
+ import pc from "picocolors";
2
+ import { t } from "./i18n.js";
3
+ /** Thrown by cancelInteractive() inside the menu so a Ctrl-C returns to the menu instead of exiting. */
4
+ export class ActionCancelled extends Error {
5
+ constructor() {
6
+ super("cancelled");
7
+ this.name = "ActionCancelled";
8
+ }
9
+ }
10
+ let json = false;
11
+ let quiet = false;
12
+ let version = "";
13
+ let command = "";
14
+ let inMenu = false;
15
+ export function configureOutput(config) {
16
+ if (config.json !== undefined)
17
+ json = config.json;
18
+ if (config.quiet !== undefined)
19
+ quiet = config.quiet;
20
+ if (config.version !== undefined)
21
+ version = config.version;
22
+ }
23
+ export function setCommandName(name) {
24
+ command = name;
25
+ }
26
+ export function setInMenu(value) {
27
+ inMenu = value;
28
+ }
29
+ export function isJson() {
30
+ return json;
31
+ }
32
+ export function isQuiet() {
33
+ return quiet;
34
+ }
35
+ /** Primary result channel. Machine mode suppresses it so stdout carries exactly one JSON document. */
36
+ export function out(line = "") {
37
+ if (json)
38
+ return;
39
+ console.log(line);
40
+ }
41
+ /** Dimmed guidance ("next: ...", "run agentsw sync"). Dropped by --quiet and --json. */
42
+ export function note(message) {
43
+ if (json || quiet)
44
+ return;
45
+ process.stderr.write(pc.dim(message) + "\n");
46
+ }
47
+ /** Non-fatal problem. Keeps the literal "warning: " prefix callers and tests rely on. */
48
+ export function warn(message) {
49
+ if (json || quiet)
50
+ return;
51
+ process.stderr.write(pc.yellow(`warning: ${message}`) + "\n");
52
+ }
53
+ function emitJsonError(message) {
54
+ process.stdout.write(JSON.stringify({ ok: false, command, version, error: { message } }) + "\n");
55
+ }
56
+ export function emitJson(data) {
57
+ process.stdout.write(JSON.stringify({ ok: true, command, version, data }) + "\n");
58
+ }
59
+ /** Fatal error. In machine mode the failure becomes the single stdout document, so pipes stay parseable. */
60
+ export function error(message) {
61
+ if (json)
62
+ emitJsonError(message);
63
+ else
64
+ process.stderr.write(pc.red(`error: ${message}`) + "\n");
65
+ process.exit(1);
66
+ }
67
+ /**
68
+ * Ctrl-C / Esc at an interactive prompt. Inside the menu this unwinds to the dispatch loop, which keeps the
69
+ * session alive; standalone it exits 130 (the SIGINT convention), so a cancelled run is never mistaken for
70
+ * a successful one. Prompts awaits onCancel inside a catch, so throwing here reliably rejects the prompt.
71
+ */
72
+ export function cancelInteractive() {
73
+ if (inMenu)
74
+ throw new ActionCancelled();
75
+ process.stderr.write(pc.dim(t("common.cancelled")) + "\n");
76
+ process.exit(130);
77
+ }
78
+ /** 0 means "no known width" (pipe, file, test runner), which disables truncation entirely. */
79
+ export function terminalWidth() {
80
+ const columns = process.stdout.columns;
81
+ return typeof columns === "number" && Number.isFinite(columns) && columns > 0 ? columns : 0;
82
+ }
83
+ const SGR = /\u001b\[[0-9;]*m/g;
84
+ export function visibleWidth(text) {
85
+ return text.replace(SGR, "").length;
86
+ }
87
+ /** Clip to a visible width without ever cutting inside an SGR sequence; closes color that was left open. */
88
+ export function truncateVisible(text, max) {
89
+ if (max <= 0)
90
+ return "";
91
+ if (visibleWidth(text) <= max)
92
+ return text;
93
+ const target = Math.max(0, max - 1);
94
+ let out = "";
95
+ let visible = 0;
96
+ let colored = false;
97
+ for (let i = 0; i < text.length && visible < target;) {
98
+ if (text.charAt(i) === "\u001b") {
99
+ const match = /^\u001b\[[0-9;]*m/.exec(text.slice(i));
100
+ if (match) {
101
+ out += match[0];
102
+ colored = true;
103
+ i += match[0].length;
104
+ continue;
105
+ }
106
+ }
107
+ out += text.charAt(i);
108
+ visible++;
109
+ i++;
110
+ }
111
+ return `${out}…${colored ? "\u001b[0m" : ""}`;
112
+ }
113
+ export function padCell(text, width, align = "left") {
114
+ const padding = " ".repeat(Math.max(0, width - visibleWidth(text)));
115
+ return align === "right" ? padding + text : text + padding;
116
+ }
117
+ export function table(rows, header, options = {}) {
118
+ const all = header ? [header, ...rows] : rows;
119
+ if (all.length === 0)
120
+ return "";
121
+ const widths = [];
122
+ for (const row of all) {
123
+ row.forEach((cell, i) => {
124
+ widths[i] = Math.max(widths[i] ?? 0, visibleWidth(cell));
125
+ });
126
+ }
127
+ const minWidth = options.minWidth ?? 8;
128
+ const width = options.width ?? terminalWidth();
129
+ const eligible = (options.truncate ?? []).filter((i) => i < widths.length);
130
+ if (width > 0 && eligible.length > 0) {
131
+ const total = () => widths.reduce((sum, w, i) => sum + w + (i > 0 ? 2 : 0), 0);
132
+ while (total() > width) {
133
+ let widest = -1;
134
+ for (const i of eligible) {
135
+ if ((widths[i] ?? 0) <= minWidth)
136
+ continue;
137
+ if (widest === -1 || (widths[i] ?? 0) > (widths[widest] ?? 0))
138
+ widest = i;
139
+ }
140
+ if (widest === -1)
141
+ break;
142
+ widths[widest] = (widths[widest] ?? 0) - 1;
143
+ }
144
+ }
145
+ const render = (row) => row
146
+ .map((cell, i) => {
147
+ const column = widths[i] ?? 0;
148
+ const clipped = visibleWidth(cell) > column ? truncateVisible(cell, column) : cell;
149
+ return padCell(clipped, column, options.align?.[i] ?? "left");
150
+ })
151
+ .join(" ")
152
+ .trimEnd();
153
+ const lines = all.map(render);
154
+ if (header)
155
+ lines.splice(1, 0, widths.map((w) => "-".repeat(w)).join(" "));
156
+ return lines.join("\n");
157
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsw",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
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": {
@@ -37,7 +37,7 @@
37
37
  "prepublishOnly": "npm run build",
38
38
  "dev": "tsx src/index.ts",
39
39
  "typecheck": "tsc -p tsconfig.json --noEmit",
40
- "test": "tsx --test test/*.test.ts"
40
+ "test": "tsx --import ./test/setup.ts --test test/*.test.ts"
41
41
  },
42
42
  "dependencies": {
43
43
  "commander": "^15.0.0",