@oxygen-agent/cli 1.46.0 → 1.64.5

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.
@@ -387,7 +387,7 @@ async function readLocalHttpResponseBody(response) {
387
387
  if (!text)
388
388
  return null;
389
389
  const contentType = response.headers.get("content-type") ?? "";
390
- const looksJson = contentType.toLowerCase().includes("json") || /^[\s\n\r]*[\[{]/.test(text);
390
+ const looksJson = contentType.toLowerCase().includes("json") || /^[\s\n\r]*[\[{]/.test(text); // skipcq: JS-0097
391
391
  if (!looksJson)
392
392
  return text;
393
393
  try {
@@ -0,0 +1,41 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export type ExecFileSyncLike = typeof execFileSync;
3
+ export type SkillsInstallOptions = {
4
+ json?: boolean;
5
+ apiUrl?: string;
6
+ agents?: string | string[];
7
+ skill?: string;
8
+ project?: boolean;
9
+ copy?: boolean;
10
+ };
11
+ export type SkillsListOptions = {
12
+ json?: boolean;
13
+ apiUrl?: string;
14
+ };
15
+ export type SkillsDoctorOptions = {
16
+ json?: boolean;
17
+ apiUrl?: string;
18
+ };
19
+ export type AutomaticSkillsInstallResult = {
20
+ attempted: boolean;
21
+ ok: boolean;
22
+ command: string;
23
+ reason?: string;
24
+ error?: {
25
+ code: string;
26
+ message: string;
27
+ details?: unknown;
28
+ };
29
+ };
30
+ export declare function listAgentSkills(options: SkillsListOptions): Promise<Record<string, unknown>>;
31
+ export declare function doctorAgentSkills(options: SkillsDoctorOptions): Promise<Record<string, unknown>>;
32
+ export declare function installAgentSkills(options: SkillsInstallOptions, runtime?: {
33
+ env?: NodeJS.ProcessEnv;
34
+ execFileSync?: ExecFileSyncLike;
35
+ }): Record<string, unknown>;
36
+ export declare function runAutomaticSkillsInstall(options?: {
37
+ apiUrl?: string;
38
+ env?: NodeJS.ProcessEnv;
39
+ execFileSync?: ExecFileSyncLike;
40
+ }): AutomaticSkillsInstallResult;
41
+ export declare function skippedAutomaticSkillsInstall(reason: string, apiUrl?: string): AutomaticSkillsInstallResult;
package/dist/skills.js ADDED
@@ -0,0 +1,325 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { OxygenError, toFailure } from "@oxygen/shared";
6
+ import { defaultApiUrl, normalizeApiUrl } from "./credentials.js";
7
+ const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
8
+ const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
9
+ export async function listAgentSkills(options) {
10
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
11
+ const indexUrl = skillIndexUrl(apiUrl);
12
+ const index = await inspectSkillIndex(indexUrl);
13
+ const installed = detectInstalledAgentSkills();
14
+ return {
15
+ index_url: indexUrl,
16
+ available: {
17
+ detected: index.reachable,
18
+ names: index.skill_names,
19
+ skills: index.skills,
20
+ ...(index.error ? { error: index.error } : {}),
21
+ },
22
+ installed,
23
+ recommended_install_command: recommendedSkillsInstallCommand(apiUrl),
24
+ install_command_args: recommendedSkillsInstallArgs(apiUrl),
25
+ };
26
+ }
27
+ export async function doctorAgentSkills(options) {
28
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
29
+ const indexUrl = skillIndexUrl(apiUrl);
30
+ const index = await inspectSkillIndex(indexUrl);
31
+ const installer = inspectSkillsInstaller();
32
+ const installed = detectInstalledAgentSkills();
33
+ const data = {
34
+ index_url: indexUrl,
35
+ index,
36
+ installer,
37
+ installed,
38
+ recommended_install_command: recommendedSkillsInstallCommand(apiUrl),
39
+ install_command_args: recommendedSkillsInstallArgs(apiUrl),
40
+ };
41
+ if (!index.reachable) {
42
+ throw new OxygenError("skills_index_unreachable", "Oxygen skill index is not reachable.", {
43
+ details: data,
44
+ exitCode: 1,
45
+ });
46
+ }
47
+ if (!installer.available) {
48
+ throw new OxygenError("skills_installer_unavailable", "The local npx installer command is not available.", {
49
+ details: data,
50
+ exitCode: 1,
51
+ });
52
+ }
53
+ return data;
54
+ }
55
+ export function installAgentSkills(options, runtime = {}) {
56
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
57
+ const indexUrl = skillIndexUrl(apiUrl);
58
+ const agents = readWords(options.agents ?? DEFAULT_SKILL_AGENTS);
59
+ const skill = readOption(options.skill) ?? "*";
60
+ const args = installerAddArgs(indexUrl, agents, skill);
61
+ if (!options.project)
62
+ args.push("--global");
63
+ if (options.copy)
64
+ args.push("--copy");
65
+ let output = "";
66
+ try {
67
+ const exec = runtime.execFileSync ?? execFileSync;
68
+ output = String(exec("npx", args, {
69
+ encoding: "utf8",
70
+ env: runtime.env ?? process.env,
71
+ stdio: ["ignore", "pipe", "pipe"],
72
+ }));
73
+ }
74
+ catch (error) {
75
+ const failure = error;
76
+ throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
77
+ details: {
78
+ index_url: indexUrl,
79
+ stdout: failure.stdout?.slice(0, 2000) ?? "",
80
+ stderr: failure.stderr?.slice(0, 2000) ?? failure.message ?? "unknown",
81
+ },
82
+ exitCode: 1,
83
+ });
84
+ }
85
+ return {
86
+ indexUrl,
87
+ index_url: indexUrl,
88
+ agents,
89
+ skill,
90
+ scope: options.project ? "project" : "global",
91
+ output,
92
+ };
93
+ }
94
+ export function runAutomaticSkillsInstall(options = {}) {
95
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
96
+ const env = options.env ?? process.env;
97
+ if (env.OXYGEN_SKIP_SKILLS === "1") {
98
+ return skippedAutomaticSkillsInstall("OXYGEN_SKIP_SKILLS=1", apiUrl);
99
+ }
100
+ const command = recommendedSkillsInstallCommand(apiUrl);
101
+ try {
102
+ installAgentSkills({
103
+ apiUrl,
104
+ agents: DEFAULT_SKILL_AGENTS,
105
+ skill: "*",
106
+ json: true,
107
+ }, {
108
+ env,
109
+ ...(options.execFileSync ? { execFileSync: options.execFileSync } : {}),
110
+ });
111
+ return {
112
+ attempted: true,
113
+ ok: true,
114
+ command,
115
+ };
116
+ }
117
+ catch (error) {
118
+ const failure = toFailure("skills install", error);
119
+ return {
120
+ attempted: true,
121
+ ok: false,
122
+ command,
123
+ error: failure.error,
124
+ };
125
+ }
126
+ }
127
+ export function skippedAutomaticSkillsInstall(reason, apiUrl = normalizeApiUrl(defaultApiUrl())) {
128
+ return {
129
+ attempted: false,
130
+ ok: true,
131
+ command: recommendedSkillsInstallCommand(apiUrl),
132
+ reason,
133
+ };
134
+ }
135
+ async function inspectSkillIndex(indexUrl) {
136
+ const controller = new AbortController();
137
+ const timeout = setTimeout(() => controller.abort(), 10_000);
138
+ timeout.unref?.();
139
+ try {
140
+ const response = await fetch(indexUrl, {
141
+ headers: { Accept: "application/json" },
142
+ signal: controller.signal,
143
+ });
144
+ const text = await response.text();
145
+ if (!response.ok) {
146
+ return emptySkillIndexInspection({
147
+ status: response.status,
148
+ error: { code: "http_error", message: `HTTP ${response.status}` },
149
+ });
150
+ }
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(text);
154
+ }
155
+ catch (error) {
156
+ return emptySkillIndexInspection({
157
+ status: response.status,
158
+ error: {
159
+ code: "invalid_json",
160
+ message: error instanceof Error ? error.message : "Invalid JSON.",
161
+ },
162
+ });
163
+ }
164
+ let index;
165
+ try {
166
+ index = parseSkillIndex(parsed);
167
+ }
168
+ catch (error) {
169
+ return emptySkillIndexInspection({
170
+ status: response.status,
171
+ error: {
172
+ code: error instanceof OxygenError ? error.code : "invalid_skill_index",
173
+ message: error instanceof Error ? error.message : "Skill index shape is invalid.",
174
+ },
175
+ });
176
+ }
177
+ return {
178
+ reachable: true,
179
+ status: response.status,
180
+ version: index.version,
181
+ base_url: index.base_url,
182
+ skill_names: index.skills.map((skill) => skill.name),
183
+ skills: index.skills,
184
+ };
185
+ }
186
+ catch (error) {
187
+ const aborted = error instanceof Error && error.name === "AbortError";
188
+ return emptySkillIndexInspection({
189
+ status: null,
190
+ error: {
191
+ code: aborted ? "network_timeout" : "network_error",
192
+ message: error instanceof Error ? error.message : "Unable to reach skill index.",
193
+ },
194
+ });
195
+ }
196
+ finally {
197
+ clearTimeout(timeout);
198
+ }
199
+ }
200
+ function emptySkillIndexInspection(input) {
201
+ return {
202
+ reachable: false,
203
+ status: input.status,
204
+ version: null,
205
+ base_url: null,
206
+ skill_names: [],
207
+ skills: [],
208
+ error: input.error,
209
+ };
210
+ }
211
+ function parseSkillIndex(value) {
212
+ if (!isRecord(value) || !Array.isArray(value.skills)) {
213
+ throw new OxygenError("invalid_skill_index", "Skill index must include a skills array.", {
214
+ exitCode: 1,
215
+ });
216
+ }
217
+ const skills = value.skills
218
+ .map((entry) => {
219
+ if (!isRecord(entry) || typeof entry.name !== "string" || !entry.name.trim())
220
+ return null;
221
+ return {
222
+ name: entry.name.trim(),
223
+ title: typeof entry.title === "string" ? entry.title : null,
224
+ description: typeof entry.description === "string" ? entry.description : null,
225
+ entrypoint: typeof entry.entrypoint === "string" ? entry.entrypoint : null,
226
+ };
227
+ })
228
+ .filter((entry) => entry !== null);
229
+ return {
230
+ version: typeof value.version === "string" ? value.version : null,
231
+ base_url: typeof value.base_url === "string" ? value.base_url : null,
232
+ skills,
233
+ };
234
+ }
235
+ function inspectSkillsInstaller() {
236
+ const result = spawnSync("npx", ["--version"], {
237
+ encoding: "utf8",
238
+ stdio: ["ignore", "pipe", "pipe"],
239
+ timeout: 5000,
240
+ });
241
+ const error = result.error instanceof Error ? result.error.message : null;
242
+ const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
243
+ const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
244
+ return {
245
+ command_path: "npx",
246
+ check_args: ["--version"],
247
+ available: result.status === 0 && !error,
248
+ version: stdout || null,
249
+ ...(error ? { error } : {}),
250
+ ...(stderr ? { stderr: stderr.slice(0, 1000) } : {}),
251
+ };
252
+ }
253
+ function detectInstalledAgentSkills() {
254
+ const roots = [
255
+ { agent: "codex", path: join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "skills") },
256
+ { agent: "claude-code", path: join(process.env.CLAUDE_HOME ?? join(homedir(), ".claude"), "skills") },
257
+ { agent: "cursor", path: join(process.env.CURSOR_HOME ?? join(homedir(), ".cursor"), "skills") },
258
+ { agent: "project", path: resolve(".agents", "skills") },
259
+ ];
260
+ const names = new Set();
261
+ const locations = [];
262
+ for (const root of roots) {
263
+ if (!existsSync(root.path))
264
+ continue;
265
+ try {
266
+ const rootNames = readdirSync(root.path, { withFileTypes: true })
267
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("oxygen-"))
268
+ .map((entry) => entry.name)
269
+ .sort();
270
+ for (const name of rootNames)
271
+ names.add(name);
272
+ locations.push({ ...root, names: rootNames });
273
+ }
274
+ catch {
275
+ locations.push({ ...root, names: [] });
276
+ }
277
+ }
278
+ return {
279
+ detected: locations.length > 0,
280
+ names: [...names].sort(),
281
+ locations,
282
+ };
283
+ }
284
+ function skillIndexUrl(apiUrl) {
285
+ return `${apiUrl}${SKILL_INDEX_PATH}`;
286
+ }
287
+ function recommendedSkillsInstallCommand(apiUrl) {
288
+ return recommendedSkillsInstallArgs(apiUrl)
289
+ .map((entry) => entry === "*" ? "'*'" : entry)
290
+ .join(" ");
291
+ }
292
+ function recommendedSkillsInstallArgs(apiUrl) {
293
+ const args = ["oxygen", "skills", "install"];
294
+ if (apiUrl !== normalizeApiUrl(defaultApiUrl())) {
295
+ args.push("--api-url", apiUrl);
296
+ }
297
+ args.push("--agents", ...DEFAULT_SKILL_AGENTS, "--skill", "*", "--json");
298
+ return args;
299
+ }
300
+ function installerAddArgs(indexUrl, agents, skill) {
301
+ return [
302
+ "skills",
303
+ "add",
304
+ indexUrl,
305
+ "--agents",
306
+ ...agents,
307
+ "--yes",
308
+ "--skill",
309
+ skill,
310
+ "--full-depth",
311
+ ];
312
+ }
313
+ function readWords(value) {
314
+ const text = Array.isArray(value) ? value.join(" ") : value;
315
+ return text
316
+ .split(/[,\s]+/)
317
+ .map((entry) => entry.trim())
318
+ .filter(Boolean);
319
+ }
320
+ function readOption(value) {
321
+ return value?.trim() ? value.trim() : null;
322
+ }
323
+ function isRecord(value) {
324
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
325
+ }
@@ -0,0 +1,34 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { type AutomaticSkillsInstallResult, type ExecFileSyncLike } from "./skills.js";
3
+ type SpawnResult = ReturnType<typeof spawnSync>;
4
+ type SpawnSyncLike = (command: string, args: string[], options: {
5
+ encoding: BufferEncoding;
6
+ env: NodeJS.ProcessEnv;
7
+ stdio: "inherit" | ["ignore", "pipe", "pipe"];
8
+ windowsHide: boolean;
9
+ windowsVerbatimArguments?: boolean;
10
+ }) => SpawnResult;
11
+ export type UpdateOptions = {
12
+ json?: boolean;
13
+ package?: string;
14
+ dryRun?: boolean;
15
+ };
16
+ export type UpdateResult = {
17
+ current_version: string;
18
+ package: string;
19
+ command: string;
20
+ dry_run: boolean;
21
+ updated: boolean;
22
+ skills_install: AutomaticSkillsInstallResult;
23
+ };
24
+ export declare function updateCli(options: UpdateOptions, runtime?: {
25
+ env?: NodeJS.ProcessEnv;
26
+ platform?: NodeJS.Platform;
27
+ modulePath?: string;
28
+ spawnSync?: SpawnSyncLike;
29
+ execFileSync?: ExecFileSyncLike;
30
+ }): UpdateResult;
31
+ export declare function detectCliInstallPrefix(modulePath?: string): string | null;
32
+ export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
33
+ export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
34
+ export {};
package/dist/update.js ADDED
@@ -0,0 +1,123 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
4
+ import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
5
+ const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
6
+ // skipcq: JS-R1005 — handles platform/install-mode/dry-run combinations end-to-end
7
+ export function updateCli(options, runtime = {}) {
8
+ const env = runtime.env ?? process.env;
9
+ const platform = runtime.platform ?? process.platform;
10
+ const spawn = runtime.spawnSync ?? spawnSync;
11
+ const packageSpec = readOption(options.package) ?? DEFAULT_CLI_PACKAGE_SPEC;
12
+ const prefix = detectCliInstallPrefix(runtime.modulePath);
13
+ const args = prefix
14
+ ? ["install", "-g", "--prefix", prefix, packageSpec]
15
+ : ["install", "-g", packageSpec];
16
+ // The command string is for human display (printed in dry-run / success
17
+ // output). Quote args that contain shell-significant characters so a
18
+ // copy-paste of the line works on install prefixes with spaces.
19
+ const command = ["npm", ...args].map(quoteForDisplay).join(" ");
20
+ if (options.dryRun) {
21
+ return {
22
+ current_version: OXYGEN_VERSION,
23
+ package: packageSpec,
24
+ command,
25
+ dry_run: true,
26
+ updated: false,
27
+ skills_install: skippedAutomaticSkillsInstall("dry_run"),
28
+ };
29
+ }
30
+ const attemptedExecutables = [];
31
+ let lastResult = null;
32
+ for (const invocation of resolveNpmInvocations(args, platform, env)) {
33
+ attemptedExecutables.push(invocation.label);
34
+ const result = spawn(invocation.executable, invocation.args, {
35
+ encoding: "utf8",
36
+ env,
37
+ stdio: options.json ? ["ignore", "pipe", "pipe"] : "inherit",
38
+ windowsHide: true,
39
+ // We pre-quote args for `cmd /d /s /c`; Node must not re-escape them
40
+ // or the install prefix path ends up with literal `"` characters
41
+ // baked in (see scripts/ci/cli-package-smoke.mjs for the same fix).
42
+ ...(platform === "win32" ? { windowsVerbatimArguments: true } : {}),
43
+ });
44
+ lastResult = result;
45
+ if (result.error && isMissingExecutableError(result.error))
46
+ continue;
47
+ if (!result.error && result.status === 0) {
48
+ return {
49
+ current_version: OXYGEN_VERSION,
50
+ package: packageSpec,
51
+ command,
52
+ dry_run: false,
53
+ updated: true,
54
+ skills_install: runAutomaticSkillsInstall({
55
+ env,
56
+ ...(runtime.execFileSync ? { execFileSync: runtime.execFileSync } : {}),
57
+ }),
58
+ };
59
+ }
60
+ break;
61
+ }
62
+ const result = lastResult;
63
+ throw new OxygenError("cli_update_failed", "Unable to update the Oxygen CLI.", {
64
+ details: {
65
+ command,
66
+ package: packageSpec,
67
+ exit_code: result?.status ?? null,
68
+ reason: result?.error instanceof Error ? result.error.message : null,
69
+ stderr: typeof result?.stderr === "string" && result.stderr.trim() ? result.stderr.trim().slice(0, 4000) : null,
70
+ attempted_executables: attemptedExecutables,
71
+ },
72
+ exitCode: 1,
73
+ });
74
+ }
75
+ export function detectCliInstallPrefix(modulePath = fileURLToPath(import.meta.url)) {
76
+ return detectCliInstallPrefixFromPath(modulePath);
77
+ }
78
+ export function detectCliInstallPrefixFromPath(modulePath) {
79
+ const normalizedPath = modulePath.replace(/\\/g, "/");
80
+ const markers = [
81
+ "/node_modules/@oxygen-agent/cli/dist/",
82
+ "/node_modules/@oxygen/cli/dist/",
83
+ ];
84
+ for (const marker of markers) {
85
+ const markerIndex = normalizedPath.lastIndexOf(marker);
86
+ if (markerIndex === -1)
87
+ continue;
88
+ const packageParent = normalizedPath.slice(0, markerIndex);
89
+ if (packageParent.endsWith("/lib"))
90
+ return packageParent.slice(0, -"/lib".length);
91
+ return packageParent;
92
+ }
93
+ return null;
94
+ }
95
+ export function resolveNpmExecutables(platform = process.platform) {
96
+ return platform === "win32" ? ["cmd.exe"] : ["npm"];
97
+ }
98
+ function readOption(value) {
99
+ return typeof value === "string" && value.trim() ? value.trim() : null;
100
+ }
101
+ function isMissingExecutableError(error) {
102
+ return error.code === "ENOENT";
103
+ }
104
+ function resolveNpmInvocations(args, platform, env) {
105
+ if (platform !== "win32")
106
+ return [{ executable: "npm", args, label: "npm" }];
107
+ const shell = env.ComSpec?.trim() || "cmd.exe";
108
+ return [{
109
+ executable: shell,
110
+ args: ["/d", "/s", "/c", ["npm", ...args].map(quoteWindowsCmdArg).join(" ")],
111
+ label: shell,
112
+ }];
113
+ }
114
+ function quoteWindowsCmdArg(value) {
115
+ if (/^[A-Za-z0-9_@+=:,./\\-]+$/.test(value))
116
+ return value;
117
+ return `"${value.replace(/(["^&|<>()%])/g, "^$1")}"`;
118
+ }
119
+ function quoteForDisplay(value) {
120
+ if (/^[A-Za-z0-9_@+=:,./-]+$/.test(value))
121
+ return value;
122
+ return `'${value.replace(/'/g, "'\\''")}'`;
123
+ }
@@ -108,7 +108,8 @@ export declare const ACTIVE_SUBSCRIPTION_STATUSES: readonly ["active", "trialing
108
108
  export type ActiveSubscriptionStatus = (typeof ACTIVE_SUBSCRIPTION_STATUSES)[number];
109
109
  export type ResolvedPricingPlan = PricingPlanDefinition;
110
110
  export declare function resolveBasePricingPlan(tier: string | null | undefined): ResolvedPricingPlan | null;
111
- export declare function applyPricingPlanMetadataOverrides(plan: PricingPlanDefinition, metadata: PricingPlanMetadata): PricingPlanDefinition;
111
+ export declare function applyPricingPlanMetadataOverrides(// skipcq: JS-R1005
112
+ plan: PricingPlanDefinition, metadata: PricingPlanMetadata): PricingPlanDefinition;
112
113
  export declare function isSelfServePlanTier(value: string): value is SelfServePlanTier;
113
114
  export declare function isBillingCurrency(value: string): value is BillingCurrency;
114
115
  export declare function normalizeBillingCurrency(value: string | null | undefined): BillingCurrency;
@@ -208,7 +208,8 @@ export function resolveBasePricingPlan(tier) {
208
208
  const legacy = LEGACY_PRICING_PLAN_OVERRIDES[tier];
209
209
  return legacy ?? null;
210
210
  }
211
- export function applyPricingPlanMetadataOverrides(plan, metadata) {
211
+ export function applyPricingPlanMetadataOverrides(// skipcq: JS-R1005
212
+ plan, metadata) {
212
213
  const record = readRecord(metadata);
213
214
  if (!record)
214
215
  return plan;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared cell display formatter for CLI, MCP widgets, and the web app.
3
+ *
4
+ * One source of truth for how a stored cell value should look to a human.
5
+ * Storage stays untouched — this only decides what to render.
6
+ *
7
+ * Three responsibilities:
8
+ * 1. Format known-typed values consistently (numeric → 1,154; timestamp → ISO/short).
9
+ * 2. Rescue numeric-looking values that landed in a `text` column. Headcount
10
+ * and similar fields routinely arrive as text from CSV imports, AI columns,
11
+ * and provider adapters, so a `text` column whose values parse cleanly as
12
+ * numbers still gets thousands grouping at display time.
13
+ * 3. Refuse to "rescue" things that aren't actually numbers (IP addresses,
14
+ * version strings, phone numbers, URLs), via {@link looksLikeNumericText}.
15
+ */
16
+ /** Surfaces have different escaping/length budgets, but the canonical string is shared. */
17
+ export type CellFormatSurface = "cli" | "mcp" | "web";
18
+ export type CellColumnLike = {
19
+ dataType?: string | null;
20
+ data_type?: string | null;
21
+ kind?: string | null;
22
+ label?: string | null;
23
+ key?: string | null;
24
+ semanticType?: string | null;
25
+ semantic_type?: string | null;
26
+ };
27
+ export type CellFormatOptions = {
28
+ surface: CellFormatSurface;
29
+ locale?: string;
30
+ /** When the value is rescued from a text column, callers can show a hint. */
31
+ onRescued?: (parsed: number) => void;
32
+ };
33
+ /**
34
+ * Canonical display string for a single cell.
35
+ *
36
+ * Returns `""` for null/undefined/empty so callers can treat the absence of
37
+ * a value as the absence of a string. Surface-specific concerns (HTML
38
+ * escaping, ellipsis budgets) belong outside this function.
39
+ */
40
+ export declare function formatCellForDisplay(value: unknown, column: CellColumnLike | null | undefined, options: CellFormatOptions): string;
41
+ /**
42
+ * Best-effort parse of a string that looks numeric (with or without thousands
43
+ * grouping, with or without dotted IP-style separators) into a finite number.
44
+ * Returns `null` when the string is clearly *not* a single number — IP
45
+ * addresses, version strings, phone numbers, anything alphabetic.
46
+ */
47
+ export declare function rescueNumericText(raw: string): number | null;
48
+ /**
49
+ * True when a string is unambiguously numeric (integer, decimal, grouped, or
50
+ * a mangled dotted form we can recover). Exported for callers that want to
51
+ * highlight rescue cases without re-running the formatter.
52
+ */
53
+ export declare function looksLikeNumericText(value: string): boolean;
54
+ /**
55
+ * Decide whether the same value-level + column-level guards used inside
56
+ * `formatCellForDisplay`'s text path should rescue this string. Exposed so
57
+ * non-formatter callers (web grid, etc.) can stay in lockstep without
58
+ * duplicating the heuristic.
59
+ */
60
+ export declare function tryRescueTextCell(value: string, column: CellColumnLike | null | undefined): number | null;