@oxygen-agent/cli 1.1014.19 → 1.1014.69

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.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +1 -1
  3. package/dist/help.js +21 -1
  4. package/dist/index.js +50 -14
  5. package/dist/json-output.d.ts +3 -0
  6. package/dist/json-output.js +33 -0
  7. package/dist/workspace-skill-commands.d.ts +9 -0
  8. package/dist/workspace-skill-commands.js +134 -0
  9. package/node_modules/@oxygen/shared/dist/capability-discovery.js +9 -0
  10. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  11. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +1 -1
  12. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +7 -4
  13. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +14 -0
  14. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +18 -0
  15. package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.d.ts +6 -0
  16. package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.js +62 -0
  17. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +5 -1
  18. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +178 -63
  19. package/node_modules/@oxygen/shared/dist/email-health-status.d.ts +11 -0
  20. package/node_modules/@oxygen/shared/dist/email-health-status.js +16 -0
  21. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +3 -2
  22. package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -6
  23. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +4 -2
  24. package/node_modules/@oxygen/shared/dist/langfuse.js +123 -66
  25. package/node_modules/@oxygen/shared/dist/table-run-summary.d.ts +2 -0
  26. package/node_modules/@oxygen/shared/dist/table-run-summary.js +26 -0
  27. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  28. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  29. package/node_modules/@oxygen/shared/dist/workspace-skills.d.ts +26 -0
  30. package/node_modules/@oxygen/shared/dist/workspace-skills.js +102 -0
  31. package/package.json +1 -1
@@ -47,6 +47,10 @@ const WARN_THROTTLE_MS = 30_000;
47
47
  * meant every flush that ran long lost its spans instead of finishing late.
48
48
  */
49
49
  const LANGFUSE_EXPORT_TIMEOUT_SECONDS = 10;
50
+ // Keep retries below BSP's 30s batch deadline. Shutdown must outlive delivery;
51
+ // ordinary flush callers still return at their existing fail-open bound.
52
+ const LANGFUSE_EXPORT_BUDGET_MS = 25_000;
53
+ const LANGFUSE_SHUTDOWN_TIMEOUT_MS = 30_000;
50
54
  // Scores are API strings; unlike observation payloads they cannot use chunks.
51
55
  const MAX_JSON_FIELD_CHARS = 400_000;
52
56
  /**
@@ -183,10 +187,12 @@ function compact(body) {
183
187
  // many spans Langfuse acknowledged, how many batches it refused, and how many
184
188
  // observations never reached the exporter at all. Counts only; never content.
185
189
  const EXPORT_SUMMARY_INTERVAL_MS = 60_000;
186
- export function createLlmExportSummary(now = Date.now) {
190
+ export function createLlmExportSummary(now = Date.now, context = {}) {
187
191
  let exported = 0;
188
192
  let failed = 0;
189
193
  let dropped = 0;
194
+ let retried = 0;
195
+ let recovered = 0;
190
196
  let windowStartedAt = now();
191
197
  const report = () => {
192
198
  const at = now();
@@ -196,12 +202,17 @@ export function createLlmExportSummary(now = Date.now) {
196
202
  exported,
197
203
  failed,
198
204
  dropped,
205
+ retried,
206
+ recovered,
207
+ ...context,
199
208
  window_seconds: Math.max(0, Math.round((at - windowStartedAt) / 1000)),
200
209
  });
201
210
  }
202
211
  exported = 0;
203
212
  failed = 0;
204
213
  dropped = 0;
214
+ retried = 0;
215
+ recovered = 0;
205
216
  windowStartedAt = at;
206
217
  };
207
218
  const add = (count) => (Number.isFinite(count) && count > 0 ? Math.floor(count) : 0);
@@ -209,6 +220,8 @@ export function createLlmExportSummary(now = Date.now) {
209
220
  exported: (count) => { exported += add(count); },
210
221
  failed: (count) => { failed += add(count); },
211
222
  dropped: (count) => { dropped += add(count); },
223
+ retried: () => { retried++; },
224
+ recovered: (count) => { recovered += add(count); },
212
225
  maybeReport: () => { if (now() - windowStartedAt >= EXPORT_SUMMARY_INTERVAL_MS)
213
226
  report(); },
214
227
  reportNow: report,
@@ -270,75 +283,95 @@ function createOtelEmitter(env, warn, summary) {
270
283
  if (!encoded)
271
284
  throw new Error("serialization_failed");
272
285
  const body = new TextDecoder().decode(encoded);
273
- const signal = AbortSignal.timeout(LANGFUSE_EXPORT_TIMEOUT_SECONDS * 1000);
274
- let response;
286
+ const startedAt = Date.now();
287
+ let status;
288
+ let failureKind = "transport_failed";
289
+ let attempts = 0;
275
290
  for (let attempt = 0; attempt < 3; attempt++) {
276
- try {
277
- response = await fetch(exportUrl, { method: "POST", headers: exportHeaders, body, signal, redirect: "error" });
278
- }
279
- catch {
280
- if (signal.aborted)
281
- throw new Error("deadline_exceeded");
282
- if (attempt === 2)
283
- throw new Error("network_failed");
284
- await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
285
- continue;
286
- }
287
- if (![429, 502, 503, 504].includes(response.status) || attempt === 2)
291
+ const remaining = LANGFUSE_EXPORT_BUDGET_MS - (Date.now() - startedAt);
292
+ if (remaining <= 0)
288
293
  break;
289
- // A retryable rejection has no useful private response to retain.
290
- await response.body?.cancel().catch(() => { });
291
- await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
292
- }
293
- if (!response?.ok) {
294
- await response?.body?.cancel().catch(() => { });
295
- throw new Error("http_rejected");
296
- }
297
- const chunks = [];
298
- let bytes = 0;
299
- const reader = response.body?.getReader();
300
- if (reader) {
294
+ attempts = attempt + 1;
295
+ status = undefined;
296
+ const signal = AbortSignal.timeout(Math.min(LANGFUSE_EXPORT_TIMEOUT_SECONDS * 1000, remaining));
297
+ let response;
301
298
  try {
302
- for (;;) {
303
- const next = await reader.read();
304
- if (next.done)
305
- break;
306
- bytes += next.value.byteLength;
307
- // OTLP acknowledgements are tiny; an echoed payload must never
308
- // become an unbounded allocation or reach any diagnostic logger.
309
- if (bytes > 64 * 1024)
310
- throw new Error("response_too_large");
311
- chunks.push(next.value);
299
+ try {
300
+ response = await fetch(exportUrl, { method: "POST", headers: exportHeaders, body, signal, redirect: "error" });
301
+ }
302
+ catch {
303
+ throw new Error(signal.aborted ? "deadline_exceeded" : "network_failed");
312
304
  }
305
+ status = response.status;
306
+ if (!response.ok) {
307
+ await response.body?.cancel().catch(() => { });
308
+ throw new Error("http_rejected");
309
+ }
310
+ const chunks = [];
311
+ let bytes = 0;
312
+ const reader = response.body?.getReader();
313
+ if (reader) {
314
+ try {
315
+ for (;;) {
316
+ const next = await reader.read();
317
+ if (next.done)
318
+ break;
319
+ bytes += next.value.byteLength;
320
+ // Never allocate or log an unbounded echoed private payload.
321
+ if (bytes > 64 * 1024)
322
+ throw new Error("response_too_large");
323
+ chunks.push(next.value);
324
+ }
325
+ }
326
+ catch (error) {
327
+ await reader.cancel().catch(() => { });
328
+ throw new Error(signal.aborted ? "deadline_exceeded" : error instanceof Error && error.message === "response_too_large" ? "response_too_large" : "response_read_failed");
329
+ }
330
+ finally {
331
+ reader.releaseLock();
332
+ }
333
+ }
334
+ let acknowledgement;
335
+ try {
336
+ acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
337
+ }
338
+ catch {
339
+ throw new Error("response_invalid");
340
+ }
341
+ if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
342
+ throw new Error("response_invalid");
343
+ const envelope = acknowledgement;
344
+ const partial = envelope.partialSuccess ?? envelope.partial_success;
345
+ if (partial !== undefined && partial !== null) {
346
+ if (typeof partial !== "object" || Array.isArray(partial))
347
+ throw new Error("response_invalid");
348
+ const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
349
+ if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
350
+ throw new Error("response_invalid");
351
+ if (Number(rawRejected) > 0)
352
+ throw new Error("partial_rejection");
353
+ }
354
+ if (attempt > 0)
355
+ summary.recovered(spans.length);
356
+ return;
313
357
  }
314
358
  catch (error) {
315
- await reader.cancel().catch(() => { });
316
- throw new Error(error instanceof Error && error.message === "response_too_large" ? "response_too_large" : "response_read_failed");
317
- }
318
- finally {
319
- reader.releaseLock();
359
+ failureKind = error instanceof Error ? error.message : "transport_failed";
360
+ const retryable = ["deadline_exceeded", "network_failed", "response_read_failed"].includes(failureKind)
361
+ || (failureKind === "http_rejected" && [429, 502, 503, 504].includes(status ?? 0));
362
+ if (!retryable || attempt === 2) {
363
+ // Carry fixed classifications and numbers only, never an SDK or
364
+ // receiver error that could quote the request/response payload.
365
+ throw Object.assign(new Error(failureKind), { attempts: attempt + 1, status });
366
+ }
367
+ const pause = 100 * (attempt + 1);
368
+ if (LANGFUSE_EXPORT_BUDGET_MS - (Date.now() - startedAt) <= pause)
369
+ break;
370
+ summary.retried();
371
+ await new Promise((resolve) => setTimeout(resolve, pause));
320
372
  }
321
373
  }
322
- let acknowledgement;
323
- try {
324
- acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
325
- }
326
- catch {
327
- throw new Error("response_invalid");
328
- }
329
- if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
330
- throw new Error("response_invalid");
331
- const envelope = acknowledgement;
332
- const partial = envelope.partialSuccess ?? envelope.partial_success;
333
- if (partial !== undefined && partial !== null) {
334
- if (typeof partial !== "object" || Array.isArray(partial))
335
- throw new Error("response_invalid");
336
- const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
337
- if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
338
- throw new Error("response_invalid");
339
- if (Number(rawRejected) > 0)
340
- throw new Error("partial_rejection");
341
- }
374
+ throw Object.assign(new Error(failureKind), { attempts, status });
342
375
  };
343
376
  const privateExporter = {
344
377
  export: (spans, done) => {
@@ -356,7 +389,9 @@ function createOtelEmitter(env, warn, summary) {
356
389
  done({ code: 1, error: new Error("langfuse_delivery_failed") });
357
390
  summary.failed(spans.length);
358
391
  summary.maybeReport();
359
- warn(error, { stage: "export", observation_count: spans.length, failure_kind: failureKind });
392
+ const details = error;
393
+ warn(error, { stage: "export", observation_count: spans.length, failure_kind: failureKind,
394
+ attempts: details.attempts, http_status: details.status });
360
395
  });
361
396
  pendingExports.add(pending);
362
397
  // Even an unexpected SDK callback failure must not create a rejected
@@ -453,7 +488,23 @@ function createOtelEmitter(env, warn, summary) {
453
488
  identity = undefined;
454
489
  }
455
490
  };
456
- return { processor, emit };
491
+ return { processor: {
492
+ // BSP forceFlush drains its queue, but does not wait for a batch its
493
+ // interval already removed. Always drain our active exporter too,
494
+ // including when the queued batch rejects.
495
+ forceFlush: async () => { try {
496
+ await processor.forceFlush();
497
+ }
498
+ finally {
499
+ await privateExporter.forceFlush();
500
+ } },
501
+ shutdown: async () => { try {
502
+ await processor.shutdown();
503
+ }
504
+ finally {
505
+ await privateExporter.shutdown();
506
+ } },
507
+ }, emit };
457
508
  }
458
509
  catch (error) {
459
510
  warn(error, { stage: "init" });
@@ -693,6 +744,11 @@ export function createLlmTracingClient(env = process.env, options) {
693
744
  }
694
745
  return null;
695
746
  }
747
+ const deliveryContext = {
748
+ tracing_environment: resolveLlmTracingEnvironment(env),
749
+ worker_role: ["relay", "bulk", "interactive", "provider", "outbound", "maintenance"].includes(env.OXYGEN_WORKER_QUEUE_ROLE ?? "")
750
+ ? env.OXYGEN_WORKER_QUEUE_ROLE : undefined,
751
+ };
696
752
  const lastWarnAtMs = new Map();
697
753
  const warn = (error, context) => {
698
754
  const nowMs = Date.now();
@@ -708,6 +764,7 @@ export function createLlmTracingClient(env = process.env, options) {
708
764
  // Exporter/API error messages can contain the rejected request or server
709
765
  // response. Only this fixed classification may leave the private store.
710
766
  error_code: errorCode,
767
+ ...deliveryContext,
711
768
  ...context,
712
769
  });
713
770
  };
@@ -738,7 +795,7 @@ export function createLlmTracingClient(env = process.env, options) {
738
795
  return observationSpanId(["langfuse-observation-v1", body.traceId, kind, body.name, body.id,
739
796
  body.startTime.toISOString(), body.metadata?.attempt_id ?? null, body.metadata?.call_id ?? null]);
740
797
  };
741
- const summary = createLlmExportSummary();
798
+ const summary = createLlmExportSummary(Date.now, deliveryContext);
742
799
  const transport = options?.emitterImpl ?? createOtelEmitter(env, warn, summary);
743
800
  const emitter = {
744
801
  ...transport,
@@ -970,7 +1027,7 @@ export function createLlmTracingClient(env = process.env, options) {
970
1027
  }
971
1028
  finally {
972
1029
  summary.reportNow();
973
- } })(), (error) => warn(error, { stage: "shutdown" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
1030
+ } })(), (error) => warn(error, { stage: "shutdown" }), LANGFUSE_SHUTDOWN_TIMEOUT_MS),
974
1031
  };
975
1032
  }
976
1033
  // --- Process-wide singleton (both runtimes construct at most one client) ------
@@ -0,0 +1,2 @@
1
+ /** Project the already-authorized, customer-safe native run; no provider or retry effects. */
2
+ export declare function tableRunSummary(run: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,26 @@
1
+ const SUMMARY_FIELDS = new Set([
2
+ "id", "runId", "tableId", "tableSlug", "status", "counts", "countsAvailable", "summaryLevel", "progress_counts_available", "creditsUsed", "creditsReserved",
3
+ "createdAt", "updatedAt", "startedAt", "completedAt", "lastError", "action_run_id",
4
+ "workspace_run_id", "table_id", "table_slug", "web_url", "deepLink", "plan",
5
+ "execution_status", "health_status", "user_status", "user_message", "progress_label",
6
+ "next_best_action", "recommended_action", "next_actions", "queue_age_seconds",
7
+ "deferred_count", "retrying_count", "credit_limit_skipped", "throughput", "bottlenecks",
8
+ "provider_errors", "provider_health", "provider_timeout_breaker", "classification_review",
9
+ "worker_health", "queue_backend", "execution_backend", "max_concurrency_requested",
10
+ "max_concurrency_effective", "enrichment_rollup", "idempotent_replay",
11
+ ]);
12
+ /** Project the already-authorized, customer-safe native run; no provider or retry effects. */
13
+ export function tableRunSummary(run) {
14
+ if (run.view === "summary")
15
+ return run;
16
+ const included = (key) => SUMMARY_FIELDS.has(key) || /credit|cost|approval|error|failure/i.test(key);
17
+ const id = run.action_run_id ?? run.id;
18
+ return {
19
+ ...Object.fromEntries(Object.entries(run).filter(([key]) => included(key))),
20
+ view: "summary",
21
+ omitted_fields: Object.keys(run).filter((key) => !included(key)).sort(),
22
+ full_read: { tool: "oxygen_table_runs_get", arguments: { action_run_id: id, view: "full" },
23
+ gateway: { tool: "oxygen_capabilities_read", arguments: { tool: "oxygen_table_runs_get", arguments: { action_run_id: id, view: "full" } } },
24
+ cli: `oxygen table-runs get ${String(id)} --full --json` },
25
+ };
26
+ }
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1014.19";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1014.69";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1014.19";
2
+ export const OXYGEN_BUILD_VERSION = "1.1014.69";
@@ -0,0 +1,26 @@
1
+ export declare const WORKSPACE_SKILL_MAX_FILES = 50;
2
+ export declare const WORKSPACE_SKILL_MAX_FILE_BYTES = 120000;
3
+ export declare const WORKSPACE_SKILL_MAX_BYTES = 1000000;
4
+ export type WorkspaceSkillFile = {
5
+ path: string;
6
+ content: string;
7
+ };
8
+ export type WorkspaceSkillBundle = {
9
+ name: string;
10
+ description: string;
11
+ files: WorkspaceSkillFile[];
12
+ source_url?: string;
13
+ };
14
+ export declare function workspaceSkillPath(path: unknown): string;
15
+ export declare function validateWorkspaceSkillFile(file: WorkspaceSkillFile): number;
16
+ /** Content stays verbatim. Names and paths are metadata, never tool authority. */
17
+ export declare function parseWorkspaceSkillBundle(input: {
18
+ files: readonly WorkspaceSkillFile[];
19
+ name?: string;
20
+ source_url?: string;
21
+ }): WorkspaceSkillBundle;
22
+ export declare function authorWorkspaceSkill(input: {
23
+ name: string;
24
+ description: string;
25
+ instructions: string;
26
+ }): WorkspaceSkillBundle;
@@ -0,0 +1,102 @@
1
+ import { OxygenError } from "./cli-result.js";
2
+ import { parseKnowledgeVaultMarkdown } from "./knowledge-vault-markdown.js";
3
+ export const WORKSPACE_SKILL_MAX_FILES = 50;
4
+ export const WORKSPACE_SKILL_MAX_FILE_BYTES = 120_000;
5
+ export const WORKSPACE_SKILL_MAX_BYTES = 1_000_000;
6
+ function invalid(message) {
7
+ throw new OxygenError("invalid_workspace_skill", message, { exitCode: 2 });
8
+ }
9
+ export function workspaceSkillPath(path) {
10
+ if (typeof path !== "string" ||
11
+ path.length > 240 ||
12
+ !path ||
13
+ /[\\\u0000-\u001f\u007f]/u.test(path) ||
14
+ path
15
+ .split("/")
16
+ .some((part) => !part || part === "." || part === ".." || part.length > 120) ||
17
+ path.split("/").length > 15)
18
+ invalid("File paths must be relative, without empty segments, backslashes or traversal.");
19
+ return path;
20
+ }
21
+ export function validateWorkspaceSkillFile(file) {
22
+ const path = workspaceSkillPath(file?.path);
23
+ if (typeof file.content !== "string" || file.content.includes("\0"))
24
+ invalid(`File ${path} must contain text.`);
25
+ const bytes = new TextEncoder().encode(file.content).length;
26
+ if (bytes > WORKSPACE_SKILL_MAX_FILE_BYTES)
27
+ invalid(`File ${path} exceeds ${WORKSPACE_SKILL_MAX_FILE_BYTES} bytes.`);
28
+ return bytes;
29
+ }
30
+ /** Content stays verbatim. Names and paths are metadata, never tool authority. */
31
+ export function parseWorkspaceSkillBundle(input) {
32
+ if (!Array.isArray(input.files) ||
33
+ input.files.length < 1 ||
34
+ input.files.length > WORKSPACE_SKILL_MAX_FILES)
35
+ invalid(`Provide 1–${WORKSPACE_SKILL_MAX_FILES} text files, including SKILL.md.`);
36
+ const paths = new Set();
37
+ let total = 0;
38
+ for (const file of input.files) {
39
+ const path = workspaceSkillPath(file?.path);
40
+ if (paths.has(path.toLowerCase()))
41
+ invalid(`Duplicate file path: ${path}`);
42
+ paths.add(path.toLowerCase());
43
+ total += validateWorkspaceSkillFile(file);
44
+ }
45
+ for (const path of paths) {
46
+ const parts = path.split("/");
47
+ for (let i = 1; i < parts.length; i++)
48
+ if (paths.has(parts.slice(0, i).join("/")))
49
+ invalid("A skill path cannot be both a file and a directory.");
50
+ }
51
+ if (total > WORKSPACE_SKILL_MAX_BYTES)
52
+ invalid(`Skill exceeds ${WORKSPACE_SKILL_MAX_BYTES} bytes.`);
53
+ const entry = input.files.find((file) => file.path === "SKILL.md");
54
+ if (!entry)
55
+ invalid("The skill must include a root SKILL.md file.");
56
+ const parsed = parseKnowledgeVaultMarkdown(entry.content);
57
+ if (parsed.frontmatterError)
58
+ invalid(`Invalid SKILL.md frontmatter: ${parsed.frontmatterError}`);
59
+ const name = input.name ?? parsed.properties.name;
60
+ const description = parsed.properties.description;
61
+ if (typeof name !== "string" ||
62
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) ||
63
+ name.length > 64)
64
+ invalid("A skill name must contain 1–64 lowercase letters, digits or single hyphens.");
65
+ if (typeof description !== "string" ||
66
+ !description.trim() ||
67
+ description.length > 1_024)
68
+ invalid("SKILL.md needs a description in its YAML frontmatter (1–1024 characters).");
69
+ if (input.source_url !== undefined) {
70
+ let url;
71
+ try {
72
+ url = new URL(input.source_url);
73
+ }
74
+ catch {
75
+ invalid("source_url must be an HTTPS source URL.");
76
+ }
77
+ if (url.protocol !== "https:" ||
78
+ url.username ||
79
+ url.password ||
80
+ input.source_url.length > 2_000)
81
+ invalid("source_url must be an HTTPS source URL without credentials.");
82
+ }
83
+ return {
84
+ name,
85
+ description: description.trim(),
86
+ files: input.files.map((file) => ({ ...file })),
87
+ ...(input.source_url ? { source_url: input.source_url } : {}),
88
+ };
89
+ }
90
+ export function authorWorkspaceSkill(input) {
91
+ if (typeof input.instructions !== "string" || !input.instructions.trim())
92
+ invalid("Skill instructions are required.");
93
+ return parseWorkspaceSkillBundle({
94
+ name: input.name,
95
+ files: [
96
+ {
97
+ path: "SKILL.md",
98
+ content: `---\nname: ${JSON.stringify(input.name)}\ndescription: ${JSON.stringify(input.description)}\n---\n\n${input.instructions}\n`,
99
+ },
100
+ ],
101
+ });
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1014.19",
3
+ "version": "1.1014.69",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",