@oxygen-agent/cli 1.1014.1600 → 1.1014.1763

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 (127) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +4 -0
  3. package/dist/command-manifest.js +18 -3
  4. package/dist/command-support.js +1 -1
  5. package/dist/commands/admin.js +82 -1
  6. package/dist/commands/billing.js +1 -1
  7. package/dist/commands/copilot.js +29 -7
  8. package/dist/commands/custom-integrations.js +13 -0
  9. package/dist/commands/domains.js +7 -0
  10. package/dist/commands/email-assets.d.ts +3 -0
  11. package/dist/commands/email-assets.js +78 -0
  12. package/dist/commands/email-templates.d.ts +3 -0
  13. package/dist/commands/email-templates.js +101 -0
  14. package/dist/commands/inbox.js +1 -1
  15. package/dist/commands/integrations.js +5 -1
  16. package/dist/commands/mailboxes.js +26 -6
  17. package/dist/commands/managed-inboxes.js +2 -2
  18. package/dist/commands/senders.js +4 -4
  19. package/dist/commands/sequences.js +47 -12
  20. package/dist/commands/subscriptions.d.ts +2 -4
  21. package/dist/commands/subscriptions.js +297 -51
  22. package/dist/credential-storage/store.d.ts +10 -0
  23. package/dist/credential-storage/store.js +282 -0
  24. package/dist/credentials.d.ts +1 -3
  25. package/dist/credentials.js +3 -275
  26. package/dist/mailbox-fleet-output.js +11 -2
  27. package/dist/sequence-start-at.d.ts +7 -0
  28. package/dist/sequence-start-at.js +21 -0
  29. package/dist/sequences-brand-kit-command.d.ts +28 -0
  30. package/dist/sequences-brand-kit-command.js +83 -0
  31. package/dist/sequences-content-commands.d.ts +28 -0
  32. package/dist/sequences-content-commands.js +176 -0
  33. package/dist/sequences-newsletter-commands.d.ts +6 -0
  34. package/dist/sequences-newsletter-commands.js +108 -0
  35. package/dist/sequences-test-send-command.d.ts +3 -0
  36. package/dist/sequences-test-send-command.js +47 -0
  37. package/dist/workspace-placement-commands.js +3 -2
  38. package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
  39. package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
  40. package/node_modules/@oxygen/formula/dist/expression.js +26 -4
  41. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
  42. package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
  43. package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
  44. package/node_modules/@oxygen/shared/dist/billing.js +5 -7
  45. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
  46. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
  47. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
  48. package/node_modules/@oxygen/shared/dist/capability-discovery.js +236 -18
  49. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
  50. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
  51. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
  52. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
  53. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
  54. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
  55. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
  56. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -1
  57. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
  58. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
  59. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
  60. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
  61. package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
  62. package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
  63. package/node_modules/@oxygen/shared/dist/email-classification-scope.d.ts +35 -0
  64. package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -0
  65. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
  66. package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
  67. package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
  68. package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
  69. package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
  70. package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
  71. package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
  72. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
  73. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
  74. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
  75. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
  76. package/node_modules/@oxygen/shared/dist/native-chat.d.ts +9 -0
  77. package/node_modules/@oxygen/shared/dist/native-chat.js +12 -1
  78. package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
  79. package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
  80. package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
  81. package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
  82. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
  83. package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
  84. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +9 -2
  85. package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
  86. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
  87. package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
  88. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
  89. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
  90. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
  91. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
  92. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
  93. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
  94. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
  95. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
  96. package/node_modules/@oxygen/shared/dist/recipes.d.ts +2 -0
  97. package/node_modules/@oxygen/shared/dist/recipes.js +13 -0
  98. package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
  99. package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
  100. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +30 -0
  101. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
  102. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
  103. package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
  104. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
  105. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
  106. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
  107. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  108. package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
  109. package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
  110. package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
  111. package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
  112. package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
  113. package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
  114. package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
  115. package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
  116. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
  117. package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
  118. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  119. package/node_modules/@oxygen/shared/dist/tool-call-arguments.d.ts +24 -0
  120. package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
  121. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
  122. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  123. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  124. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
  125. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
  126. package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
  127. package/package.json +2 -2
@@ -0,0 +1,282 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { OxygenError } from "@oxygen/shared/cli-result";
7
+ const SERVICE_NAME = "oxygen-cli";
8
+ const ACCOUNT_NAME = "default";
9
+ const WINDOWS_CREDENTIAL_PAYLOAD_ENV = "OXYGEN_CLI_WINDOWS_CREDENTIAL_PAYLOAD";
10
+ /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
11
+ export function resolveConfigDir(env = process.env) {
12
+ return env.OXYGEN_CONFIG_DIR?.trim()
13
+ || (process.platform === "win32"
14
+ ? join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
15
+ : join(homedir(), ".config", "oxygen"));
16
+ }
17
+ /** Own raw credential IO. The same live environment is read at every original
18
+ * operation boundary; the application still owns documents and profiles.
19
+ * Construct once at the application's original promisify initialization point. */
20
+ export function createCredentialStorage() {
21
+ const execFileAsync = promisify(execFile);
22
+ async function readSystemCredentialPayload(env) {
23
+ const store = credentialStore(env);
24
+ if (store === "keychain")
25
+ return readMacOSKeychainPayload(env);
26
+ if (store === "credential-manager")
27
+ return readWindowsCredentialManagerPayload(env);
28
+ return readFileCredentialPayload(env);
29
+ }
30
+ async function writeSystemCredentialPayload(payload, env) {
31
+ const store = credentialStore(env);
32
+ if (store === "keychain") {
33
+ await writeMacOSKeychainPayload(payload, env);
34
+ return;
35
+ }
36
+ if (store === "credential-manager") {
37
+ await writeWindowsCredentialManagerPayload(payload, env);
38
+ return;
39
+ }
40
+ writeFileCredentialPayload(payload, env);
41
+ }
42
+ async function deleteSystemCredentialPayload(env) {
43
+ const store = credentialStore(env);
44
+ if (store === "keychain") {
45
+ await deleteMacOSKeychain(env);
46
+ return;
47
+ }
48
+ if (store === "credential-manager") {
49
+ await deleteWindowsCredentialManager(env);
50
+ return;
51
+ }
52
+ deleteFileCredentials(env);
53
+ }
54
+ function credentialStore(env) {
55
+ const forced = env.OXYGEN_CREDENTIAL_STORE?.trim().toLowerCase();
56
+ if (forced === "file")
57
+ return "file";
58
+ if (forced === "keychain")
59
+ return "keychain";
60
+ if (forced === "credential-manager")
61
+ return "credential-manager";
62
+ if (process.platform === "darwin")
63
+ return "keychain";
64
+ if (process.platform === "win32")
65
+ return "credential-manager";
66
+ return "file";
67
+ }
68
+ function credentialsPath(env) {
69
+ return join(resolveConfigDir(env), "credentials.json");
70
+ }
71
+ function readFileCredentialPayload(env) {
72
+ const path = credentialsPath(env);
73
+ if (!existsSync(path))
74
+ return null;
75
+ return readFileSync(path, "utf8");
76
+ }
77
+ function writeFileCredentialPayload(payload, env) {
78
+ const path = credentialsPath(env);
79
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
80
+ const tempPath = `${path}.${process.pid}.tmp`;
81
+ writeFileSync(tempPath, payload, { mode: 0o600 });
82
+ chmodSync(tempPath, 0o600);
83
+ renameSync(tempPath, path);
84
+ chmodSync(path, 0o600);
85
+ }
86
+ function deleteFileCredentials(env) {
87
+ const path = credentialsPath(env);
88
+ rmSync(path, { force: true });
89
+ }
90
+ async function readMacOSKeychainPayload(env) {
91
+ try {
92
+ const { stdout } = await execFileAsync(macOSSecurityCommand(env), [
93
+ "find-generic-password",
94
+ "-a",
95
+ ACCOUNT_NAME,
96
+ "-s",
97
+ SERVICE_NAME,
98
+ "-w",
99
+ ], { env });
100
+ return stdout;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ // `security -i` reads commands with a line buffer that traditionally caps at
107
+ // 4096 bytes. Beyond that, the input is silently truncated, which would
108
+ // corrupt the stored credential and break re-auth. Reject early with a clear
109
+ // error so a user with too many profiles gets actionable feedback instead of
110
+ // a partial-write that only surfaces on the next CLI invocation.
111
+ const MACOS_SECURITY_LINE_LIMIT_BYTES = 4096;
112
+ async function writeMacOSKeychainPayload(payload, env) {
113
+ // `security add-generic-password -w PAYLOAD` would put the token in argv
114
+ // (visible to any same-UID `ps`). Use `security -i` instead: the subcommand
115
+ // is written on stdin, so the only argv `ps` ever sees is `security -i`.
116
+ const command = macOSSecurityCommand(env);
117
+ const escaped = payload.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
118
+ const line = `add-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w "${escaped}" -U\n`;
119
+ const lineBytes = Buffer.byteLength(line, "utf8");
120
+ if (lineBytes >= MACOS_SECURITY_LINE_LIMIT_BYTES) {
121
+ throw credentialStoreError("write", "macOS Keychain", {
122
+ code: "payload_too_large",
123
+ stderr: `Credential document is ${lineBytes} bytes; \`security -i\` truncates lines at ${MACOS_SECURITY_LINE_LIMIT_BYTES} bytes. Reduce stored profiles (oxygen logout <profile>) and retry.`,
124
+ });
125
+ }
126
+ await new Promise((resolve, reject) => {
127
+ const child = spawn(command, ["-i"], { env, stdio: ["pipe", "pipe", "pipe"] });
128
+ let stderr = "";
129
+ child.stderr.on("data", (chunk) => {
130
+ stderr += chunk.toString("utf8");
131
+ });
132
+ child.once("error", (error) => {
133
+ reject(credentialStoreError("write", "macOS Keychain", error));
134
+ });
135
+ child.once("close", (code, signal) => {
136
+ const trimmedStderr = stderr.trim();
137
+ if (code === 0 && !trimmedStderr) {
138
+ resolve();
139
+ return;
140
+ }
141
+ reject(credentialStoreError("write", "macOS Keychain", { code, signal, stderr: trimmedStderr }));
142
+ });
143
+ child.stdin.end(line);
144
+ });
145
+ }
146
+ async function deleteMacOSKeychain(env) {
147
+ await execFileAsync(macOSSecurityCommand(env), [
148
+ "delete-generic-password",
149
+ "-a",
150
+ ACCOUNT_NAME,
151
+ "-s",
152
+ SERVICE_NAME,
153
+ ], { env }).catch(() => undefined);
154
+ }
155
+ async function readWindowsCredentialManagerPayload(env) {
156
+ try {
157
+ const { stdout } = await runPowerShell(`${windowsCredentialManagerScript()}
158
+ $ptr = [IntPtr]::Zero
159
+ try {
160
+ if (-not [OxygenCredMan]::CredRead("${SERVICE_NAME}", 1, 0, [ref]$ptr)) { exit 2 }
161
+ $cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][OxygenCredMan+CREDENTIAL])
162
+ $secret = [Runtime.InteropServices.Marshal]::PtrToStringUni($cred.CredentialBlob, [int]($cred.CredentialBlobSize / 2))
163
+ [Console]::Out.Write($secret)
164
+ } finally {
165
+ if ($ptr -ne [IntPtr]::Zero) { [OxygenCredMan]::CredFree($ptr) }
166
+ }
167
+ `, env);
168
+ return stdout;
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ async function writeWindowsCredentialManagerPayload(value, env) {
175
+ const payload = Buffer.from(value, "utf16le").toString("base64");
176
+ try {
177
+ await runPowerShell(`${windowsCredentialManagerScript()}
178
+ $payload = $env:${WINDOWS_CREDENTIAL_PAYLOAD_ENV}
179
+ if ([string]::IsNullOrWhiteSpace($payload)) { exit 1 }
180
+ $secret = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($payload))
181
+ $bytes = [Text.Encoding]::Unicode.GetBytes($secret)
182
+ $blob = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($secret)
183
+ try {
184
+ $cred = New-Object OxygenCredMan+CREDENTIAL
185
+ $cred.Type = 1
186
+ $cred.TargetName = "${SERVICE_NAME}"
187
+ $cred.UserName = "${ACCOUNT_NAME}"
188
+ $cred.CredentialBlob = $blob
189
+ $cred.CredentialBlobSize = $bytes.Length
190
+ $cred.Persist = 2
191
+ if (-not [OxygenCredMan]::CredWrite([ref]$cred, 0)) { exit 1 }
192
+ } finally {
193
+ [Runtime.InteropServices.Marshal]::FreeCoTaskMem($blob)
194
+ }
195
+ `, {
196
+ ...env,
197
+ [WINDOWS_CREDENTIAL_PAYLOAD_ENV]: payload,
198
+ });
199
+ }
200
+ catch (error) {
201
+ throw credentialStoreError("write", "Windows Credential Manager", error);
202
+ }
203
+ }
204
+ async function deleteWindowsCredentialManager(env) {
205
+ await runPowerShell(`${windowsCredentialManagerScript()}
206
+ [void][OxygenCredMan]::CredDelete("${SERVICE_NAME}", 1, 0)
207
+ `, env).catch(() => undefined);
208
+ }
209
+ function runPowerShell(script, env) {
210
+ const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
211
+ return execFileAsync(windowsPowerShellCommand(env), [
212
+ "-NoProfile",
213
+ "-NonInteractive",
214
+ "-ExecutionPolicy",
215
+ "Bypass",
216
+ "-EncodedCommand",
217
+ encodedCommand,
218
+ ], { env });
219
+ }
220
+ function credentialStoreError(operation, store, error) {
221
+ const child = error;
222
+ const details = {
223
+ operation,
224
+ store,
225
+ };
226
+ if (typeof child.code === "number") {
227
+ details.exit_code = child.code;
228
+ }
229
+ else if (typeof child.code === "string") {
230
+ details.reason = child.code;
231
+ }
232
+ if (typeof child.signal === "string")
233
+ details.signal = child.signal;
234
+ if (typeof child.stderr === "string" && child.stderr.trim()) {
235
+ details.stderr = sanitizeCredentialErrorText(child.stderr.trim()).slice(0, 4000);
236
+ }
237
+ return new OxygenError("credential_store_failed", `Unable to ${operation} Oxygen CLI credentials in ${store}.`, { details, exitCode: 1 });
238
+ }
239
+ function sanitizeCredentialErrorText(value) {
240
+ return value.replace(/\boxy_(?:live|user)_[A-Za-z0-9_-]+\b/g, "[redacted_token]");
241
+ }
242
+ function macOSSecurityCommand(env) {
243
+ return env.OXYGEN_SECURITY_COMMAND?.trim() || "security";
244
+ }
245
+ function windowsPowerShellCommand(env) {
246
+ return env.OXYGEN_POWERSHELL_COMMAND?.trim() || "powershell.exe";
247
+ }
248
+ function windowsCredentialManagerScript() {
249
+ return `
250
+ Add-Type @"
251
+ using System;
252
+ using System.Runtime.InteropServices;
253
+ public static class OxygenCredMan {
254
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
255
+ public struct CREDENTIAL {
256
+ public UInt32 Flags;
257
+ public UInt32 Type;
258
+ public string TargetName;
259
+ public string Comment;
260
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
261
+ public UInt32 CredentialBlobSize;
262
+ public IntPtr CredentialBlob;
263
+ public UInt32 Persist;
264
+ public UInt32 AttributeCount;
265
+ public IntPtr Attributes;
266
+ public string TargetAlias;
267
+ public string UserName;
268
+ }
269
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
270
+ public static extern bool CredWrite([In] ref CREDENTIAL userCredential, UInt32 flags);
271
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
272
+ public static extern bool CredRead(string target, UInt32 type, UInt32 reservedFlag, out IntPtr credentialPtr);
273
+ [DllImport("advapi32.dll", SetLastError = true)]
274
+ public static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
275
+ [DllImport("advapi32.dll", SetLastError = true)]
276
+ public static extern void CredFree([In] IntPtr cred);
277
+ }
278
+ "@
279
+ `;
280
+ }
281
+ return { readSystemCredentialPayload, writeSystemCredentialPayload, deleteSystemCredentialPayload };
282
+ }
@@ -1,3 +1,4 @@
1
+ export { resolveConfigDir } from "./credential-storage/store.js";
1
2
  export type StoredProfileIdentity = {
2
3
  organization: StoredOrganization | null;
3
4
  user: {
@@ -77,7 +78,4 @@ export declare function pickProfileNameForUserSession(userId: string, candidateS
77
78
  export declare function switchCredentialProfile(profile: string, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
78
79
  export declare function updateActiveOrganizationForProfile(profile: string, organization: StoredOrganization, env?: NodeJS.ProcessEnv): Promise<CredentialProfile>;
79
80
  export declare function normalizeApiUrl(value: string): string;
80
- /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
81
- export declare function resolveConfigDir(env?: NodeJS.ProcessEnv): string;
82
81
  export declare function readEnvToken(env?: NodeJS.ProcessEnv): string | null;
83
- export {};
@@ -1,16 +1,10 @@
1
- import { execFile, spawn } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { promisify } from "node:util";
1
+ import { createCredentialStorage } from "./credential-storage/store.js";
6
2
  import { OxygenError } from "@oxygen/shared/cli-result";
7
3
  import { sameApiOrigin } from "./runtime.js";
8
- const execFileAsync = promisify(execFile);
9
- const SERVICE_NAME = "oxygen-cli";
10
- const ACCOUNT_NAME = "default";
4
+ export { resolveConfigDir } from "./credential-storage/store.js";
5
+ const { readSystemCredentialPayload, writeSystemCredentialPayload, deleteSystemCredentialPayload } = createCredentialStorage();
11
6
  const DEFAULT_API_URL = "https://oxygen-agent.com";
12
7
  const DEFAULT_PROFILE_NAME = "default";
13
- const WINDOWS_CREDENTIAL_PAYLOAD_ENV = "OXYGEN_CLI_WINDOWS_CREDENTIAL_PAYLOAD";
14
8
  function readEnvApiUrl(env) {
15
9
  const apiUrl = env.OXYGEN_API_URL?.trim();
16
10
  return apiUrl ? normalizeApiUrl(apiUrl) : null;
@@ -347,38 +341,6 @@ async function readSystemCredentialDocument(env) {
347
341
  return null;
348
342
  return parseCredentialDocument(payload);
349
343
  }
350
- async function readSystemCredentialPayload(env) {
351
- const store = credentialStore(env);
352
- if (store === "keychain")
353
- return readMacOSKeychainPayload(env);
354
- if (store === "credential-manager")
355
- return readWindowsCredentialManagerPayload(env);
356
- return readFileCredentialPayload(env);
357
- }
358
- async function writeSystemCredentialPayload(payload, env) {
359
- const store = credentialStore(env);
360
- if (store === "keychain") {
361
- await writeMacOSKeychainPayload(payload, env);
362
- return;
363
- }
364
- if (store === "credential-manager") {
365
- await writeWindowsCredentialManagerPayload(payload, env);
366
- return;
367
- }
368
- writeFileCredentialPayload(payload, env);
369
- }
370
- async function deleteSystemCredentialPayload(env) {
371
- const store = credentialStore(env);
372
- if (store === "keychain") {
373
- await deleteMacOSKeychain(env);
374
- return;
375
- }
376
- if (store === "credential-manager") {
377
- await deleteWindowsCredentialManager(env);
378
- return;
379
- }
380
- deleteFileCredentials(env);
381
- }
382
344
  async function writeCredentialDocument(input, env) {
383
345
  const activeCredentials = input.profiles[input.activeProfile];
384
346
  if (!activeCredentials) {
@@ -397,240 +359,6 @@ async function writeCredentialDocument(input, env) {
397
359
  document.identity = activeCredentials.identity;
398
360
  await writeSystemCredentialPayload(serializeCredentialDocument(document), env);
399
361
  }
400
- function credentialStore(env) {
401
- const forced = env.OXYGEN_CREDENTIAL_STORE?.trim().toLowerCase();
402
- if (forced === "file")
403
- return "file";
404
- if (forced === "keychain")
405
- return "keychain";
406
- if (forced === "credential-manager")
407
- return "credential-manager";
408
- if (process.platform === "darwin")
409
- return "keychain";
410
- if (process.platform === "win32")
411
- return "credential-manager";
412
- return "file";
413
- }
414
- /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
415
- export function resolveConfigDir(env = process.env) {
416
- return env.OXYGEN_CONFIG_DIR?.trim()
417
- || (process.platform === "win32"
418
- ? join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
419
- : join(homedir(), ".config", "oxygen"));
420
- }
421
- function credentialsPath(env) {
422
- return join(resolveConfigDir(env), "credentials.json");
423
- }
424
- function readFileCredentialPayload(env) {
425
- const path = credentialsPath(env);
426
- if (!existsSync(path))
427
- return null;
428
- return readFileSync(path, "utf8");
429
- }
430
- function writeFileCredentialPayload(payload, env) {
431
- const path = credentialsPath(env);
432
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
433
- const tempPath = `${path}.${process.pid}.tmp`;
434
- writeFileSync(tempPath, payload, { mode: 0o600 });
435
- chmodSync(tempPath, 0o600);
436
- renameSync(tempPath, path);
437
- chmodSync(path, 0o600);
438
- }
439
- function deleteFileCredentials(env) {
440
- const path = credentialsPath(env);
441
- rmSync(path, { force: true });
442
- }
443
- async function readMacOSKeychainPayload(env) {
444
- try {
445
- const { stdout } = await execFileAsync(macOSSecurityCommand(env), [
446
- "find-generic-password",
447
- "-a",
448
- ACCOUNT_NAME,
449
- "-s",
450
- SERVICE_NAME,
451
- "-w",
452
- ], { env });
453
- return stdout;
454
- }
455
- catch {
456
- return null;
457
- }
458
- }
459
- // `security -i` reads commands with a line buffer that traditionally caps at
460
- // 4096 bytes. Beyond that, the input is silently truncated, which would
461
- // corrupt the stored credential and break re-auth. Reject early with a clear
462
- // error so a user with too many profiles gets actionable feedback instead of
463
- // a partial-write that only surfaces on the next CLI invocation.
464
- const MACOS_SECURITY_LINE_LIMIT_BYTES = 4096;
465
- async function writeMacOSKeychainPayload(payload, env) {
466
- // `security add-generic-password -w PAYLOAD` would put the token in argv
467
- // (visible to any same-UID `ps`). Use `security -i` instead: the subcommand
468
- // is written on stdin, so the only argv `ps` ever sees is `security -i`.
469
- const command = macOSSecurityCommand(env);
470
- const escaped = payload.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
471
- const line = `add-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w "${escaped}" -U\n`;
472
- const lineBytes = Buffer.byteLength(line, "utf8");
473
- if (lineBytes >= MACOS_SECURITY_LINE_LIMIT_BYTES) {
474
- throw credentialStoreError("write", "macOS Keychain", {
475
- code: "payload_too_large",
476
- stderr: `Credential document is ${lineBytes} bytes; \`security -i\` truncates lines at ${MACOS_SECURITY_LINE_LIMIT_BYTES} bytes. Reduce stored profiles (oxygen logout <profile>) and retry.`,
477
- });
478
- }
479
- await new Promise((resolve, reject) => {
480
- const child = spawn(command, ["-i"], { env, stdio: ["pipe", "pipe", "pipe"] });
481
- let stderr = "";
482
- child.stderr.on("data", (chunk) => {
483
- stderr += chunk.toString("utf8");
484
- });
485
- child.once("error", (error) => {
486
- reject(credentialStoreError("write", "macOS Keychain", error));
487
- });
488
- child.once("close", (code, signal) => {
489
- const trimmedStderr = stderr.trim();
490
- if (code === 0 && !trimmedStderr) {
491
- resolve();
492
- return;
493
- }
494
- reject(credentialStoreError("write", "macOS Keychain", { code, signal, stderr: trimmedStderr }));
495
- });
496
- child.stdin.end(line);
497
- });
498
- }
499
- async function deleteMacOSKeychain(env) {
500
- await execFileAsync(macOSSecurityCommand(env), [
501
- "delete-generic-password",
502
- "-a",
503
- ACCOUNT_NAME,
504
- "-s",
505
- SERVICE_NAME,
506
- ], { env }).catch(() => undefined);
507
- }
508
- async function readWindowsCredentialManagerPayload(env) {
509
- try {
510
- const { stdout } = await runPowerShell(`${windowsCredentialManagerScript()}
511
- $ptr = [IntPtr]::Zero
512
- try {
513
- if (-not [OxygenCredMan]::CredRead("${SERVICE_NAME}", 1, 0, [ref]$ptr)) { exit 2 }
514
- $cred = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][OxygenCredMan+CREDENTIAL])
515
- $secret = [Runtime.InteropServices.Marshal]::PtrToStringUni($cred.CredentialBlob, [int]($cred.CredentialBlobSize / 2))
516
- [Console]::Out.Write($secret)
517
- } finally {
518
- if ($ptr -ne [IntPtr]::Zero) { [OxygenCredMan]::CredFree($ptr) }
519
- }
520
- `, env);
521
- return stdout;
522
- }
523
- catch {
524
- return null;
525
- }
526
- }
527
- async function writeWindowsCredentialManagerPayload(value, env) {
528
- const payload = Buffer.from(value, "utf16le").toString("base64");
529
- try {
530
- await runPowerShell(`${windowsCredentialManagerScript()}
531
- $payload = $env:${WINDOWS_CREDENTIAL_PAYLOAD_ENV}
532
- if ([string]::IsNullOrWhiteSpace($payload)) { exit 1 }
533
- $secret = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($payload))
534
- $bytes = [Text.Encoding]::Unicode.GetBytes($secret)
535
- $blob = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($secret)
536
- try {
537
- $cred = New-Object OxygenCredMan+CREDENTIAL
538
- $cred.Type = 1
539
- $cred.TargetName = "${SERVICE_NAME}"
540
- $cred.UserName = "${ACCOUNT_NAME}"
541
- $cred.CredentialBlob = $blob
542
- $cred.CredentialBlobSize = $bytes.Length
543
- $cred.Persist = 2
544
- if (-not [OxygenCredMan]::CredWrite([ref]$cred, 0)) { exit 1 }
545
- } finally {
546
- [Runtime.InteropServices.Marshal]::FreeCoTaskMem($blob)
547
- }
548
- `, {
549
- ...env,
550
- [WINDOWS_CREDENTIAL_PAYLOAD_ENV]: payload,
551
- });
552
- }
553
- catch (error) {
554
- throw credentialStoreError("write", "Windows Credential Manager", error);
555
- }
556
- }
557
- async function deleteWindowsCredentialManager(env) {
558
- await runPowerShell(`${windowsCredentialManagerScript()}
559
- [void][OxygenCredMan]::CredDelete("${SERVICE_NAME}", 1, 0)
560
- `, env).catch(() => undefined);
561
- }
562
- function runPowerShell(script, env) {
563
- const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
564
- return execFileAsync(windowsPowerShellCommand(env), [
565
- "-NoProfile",
566
- "-NonInteractive",
567
- "-ExecutionPolicy",
568
- "Bypass",
569
- "-EncodedCommand",
570
- encodedCommand,
571
- ], { env });
572
- }
573
- function credentialStoreError(operation, store, error) {
574
- const child = error;
575
- const details = {
576
- operation,
577
- store,
578
- };
579
- if (typeof child.code === "number") {
580
- details.exit_code = child.code;
581
- }
582
- else if (typeof child.code === "string") {
583
- details.reason = child.code;
584
- }
585
- if (typeof child.signal === "string")
586
- details.signal = child.signal;
587
- if (typeof child.stderr === "string" && child.stderr.trim()) {
588
- details.stderr = sanitizeCredentialErrorText(child.stderr.trim()).slice(0, 4000);
589
- }
590
- return new OxygenError("credential_store_failed", `Unable to ${operation} Oxygen CLI credentials in ${store}.`, { details, exitCode: 1 });
591
- }
592
- function sanitizeCredentialErrorText(value) {
593
- return value.replace(/\boxy_(?:live|user)_[A-Za-z0-9_-]+\b/g, "[redacted_token]");
594
- }
595
- function macOSSecurityCommand(env) {
596
- return env.OXYGEN_SECURITY_COMMAND?.trim() || "security";
597
- }
598
- function windowsPowerShellCommand(env) {
599
- return env.OXYGEN_POWERSHELL_COMMAND?.trim() || "powershell.exe";
600
- }
601
- function windowsCredentialManagerScript() {
602
- return `
603
- Add-Type @"
604
- using System;
605
- using System.Runtime.InteropServices;
606
- public static class OxygenCredMan {
607
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
608
- public struct CREDENTIAL {
609
- public UInt32 Flags;
610
- public UInt32 Type;
611
- public string TargetName;
612
- public string Comment;
613
- public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
614
- public UInt32 CredentialBlobSize;
615
- public IntPtr CredentialBlob;
616
- public UInt32 Persist;
617
- public UInt32 AttributeCount;
618
- public IntPtr Attributes;
619
- public string TargetAlias;
620
- public string UserName;
621
- }
622
- [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
623
- public static extern bool CredWrite([In] ref CREDENTIAL userCredential, UInt32 flags);
624
- [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
625
- public static extern bool CredRead(string target, UInt32 type, UInt32 reservedFlag, out IntPtr credentialPtr);
626
- [DllImport("advapi32.dll", SetLastError = true)]
627
- public static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
628
- [DllImport("advapi32.dll", SetLastError = true)]
629
- public static extern void CredFree([In] IntPtr cred);
630
- }
631
- "@
632
- `;
633
- }
634
362
  function serializeCredentialDocument(document) {
635
363
  return JSON.stringify(document);
636
364
  }
@@ -29,9 +29,9 @@ export function formatMailboxFleetHealth(data) {
29
29
  `Warm-up stop causes: ${counts(warmup.byCause)}`,
30
30
  "Enrollment and next actions: oxygen mailboxes list --json (warmupTruth).",
31
31
  "",
32
- "Domains (worst first):",
32
+ ...(Array.isArray(data.domain_context) ? ["Selected inbox domains (whole-domain totals):"] : ["Domains (worst first):"]),
33
33
  ];
34
- const domains = Array.isArray(data.domains) ? data.domains.map(record) : [];
34
+ const domains = Array.isArray(data.domain_context) ? data.domain_context.map(record) : Array.isArray(data.domains) ? data.domains.map(record) : [];
35
35
  for (const domain of domains)
36
36
  lines.push(` ${text(domain.domain)} · ${count(domain.mailboxes)} inboxes · provider rejections ${count(domain.providerRejections7d)} (Google ${count(domain.googleRejections7d)}) · hard bounces ${count(domain.hardBounces7d)} / ${count(domain.coldSends7d)} sequence sends · youngest warm-up day ${count(domain.warmupDayMin)}`);
37
37
  if (!domains.length)
@@ -50,11 +50,20 @@ export function formatMailboxFleetHealth(data) {
50
50
  }
51
51
  const mailboxes = Array.isArray(data.mailboxes) ? data.mailboxes.map(record) : null;
52
52
  if (mailboxes) {
53
+ const selection = record(data.mailbox_selection);
54
+ if (Object.keys(selection).length)
55
+ lines.push("", `Selected inbox details: ${count(selection.matched)} of ${count(selection.fleet_total)}; fleet totals remain unfiltered.`);
53
56
  lines.push("", `Inbox roster (${mailboxes.length}):`);
54
57
  for (const mailbox of mailboxes) {
55
58
  const mailboxWarmup = record(mailbox.warmup);
56
59
  const warnings = Array.isArray(mailbox.warnings) ? mailbox.warnings.join(", ") : "";
57
60
  lines.push(` ${text(mailbox.emailAddress)} · ${text(mailbox.status)} · health ${text(mailbox.healthState)} (${text(mailbox.healthFreshness)}) · stored warm-up rail ${text(mailboxWarmup.state)}${typeof mailboxWarmup.pauseCode === "string" ? ` / ${mailboxWarmup.pauseCode}` : ""}${warnings ? ` · ${warnings}` : ""}`);
61
+ if (data.view === "full") {
62
+ const evidence = record(mailbox.signals);
63
+ lines.push(` Last-known external score: ${count(mailbox.healthScore)} · ${text(mailbox.healthProvider)} · checked ${text(mailbox.healthCheckedAt)}`);
64
+ lines.push(` ${text(evidence.window)} evidence: ${count(evidence.coldSends7d)} sequence sends · ${count(evidence.hardBounces7d)} hard bounces · ${count(evidence.googleRejections7d)} Google rejections (all send sources)`);
65
+ lines.push(` Delivery notifications: ${counts(evidence.dsn7d)} · diagnostic codes: ${counts(evidence.dsnCodes7d)}`);
66
+ }
58
67
  }
59
68
  }
60
69
  else if (data.view === "fleet")
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `sequences start --at`: an ISO 8601 timestamp with a timezone, or a relative
3
+ * "in 30m" / "in 2h" / "in 3d". Returns the canonical UTC timestamp the start
4
+ * route stores; the same future and 30-day bounds apply as on the server, so a
5
+ * bad value fails before any request.
6
+ */
7
+ export declare function parseStartAtFlag(value: string, now?: Date): string;
@@ -0,0 +1,21 @@
1
+ import { OxygenError } from "@oxygen/shared/cli-result";
2
+ import { parseSequenceStartAt } from "@oxygen/shared/sequence-lifecycle";
3
+ const RELATIVE = /^in\s+(\d+)\s*(m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)$/i;
4
+ const UNIT_MS = { m: 60_000, h: 3_600_000, d: 86_400_000 };
5
+ /**
6
+ * `sequences start --at`: an ISO 8601 timestamp with a timezone, or a relative
7
+ * "in 30m" / "in 2h" / "in 3d". Returns the canonical UTC timestamp the start
8
+ * route stores; the same future and 30-day bounds apply as on the server, so a
9
+ * bad value fails before any request.
10
+ */
11
+ export function parseStartAtFlag(value, now = new Date()) {
12
+ const relative = RELATIVE.exec(value.trim());
13
+ const candidate = relative
14
+ ? new Date(now.getTime() + Number(relative[1]) * UNIT_MS[relative[2].charAt(0).toLowerCase()]).toISOString()
15
+ : value;
16
+ const parsed = parseSequenceStartAt(candidate, now);
17
+ if (!parsed.ok) {
18
+ throw new OxygenError("invalid_request", `--at: ${parsed.message} Relative times like "in 2h", "in 30m" or "in 3d" also work.`, { exitCode: 2 });
19
+ }
20
+ return parsed.startAt;
21
+ }