@ouro.bot/cli 0.1.0-alpha.547 → 0.1.0-alpha.549

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.549",
6
+ "changes": [
7
+ "`ouro up` now unloads any already-loaded daemon LaunchAgent before replacing a drifted daemon runtime, preventing launchd KeepAlive from racing the manual replacement and leaving two daemon processes split across the same socket.",
8
+ "`ouro up` now rewrites the daemon boot plist as soon as the daemon is answering, so a degraded sense no longer prevents restart auto-start metadata from being refreshed."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.548",
13
+ "changes": [
14
+ "OpenAI Codex auth now reuses an existing fresh local Codex login before starting browser login, letting `ouro auth --provider openai-codex` repair stale vault copies without a human OAuth round-trip when the local Codex CLI is already logged in.",
15
+ "Codex provider credentials now retain refresh metadata (`refreshToken` and `expiresAt`) alongside the access token, setting up proactive non-browser refresh handling for future hardening."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.547",
6
20
  "changes": [
@@ -52,6 +52,7 @@ const provider_credentials_1 = require("../provider-credentials");
52
52
  const vault_unlock_1 = require("../../repertoire/vault-unlock");
53
53
  const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
54
54
  const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
55
+ const CODEX_LOCAL_TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
55
56
  function assertPersistentProviderCredentialsAllowed(agentName) {
56
57
  if (agentName === "SerpentGuide") {
57
58
  throw new Error("SerpentGuide uses provider credentials in memory during hatch bootstrap; persistent SerpentGuide auth is not supported.");
@@ -171,18 +172,64 @@ function writeAgentModel(agentName, facing, modelName, deps = {}) {
171
172
  });
172
173
  return { configPath, provider, previousModel };
173
174
  }
174
- function readCodexAccessToken(homeDir) {
175
+ function decodeJwtPayload(token) {
176
+ const parts = token.split(".");
177
+ if (parts.length < 2 || !parts[1])
178
+ return null;
179
+ try {
180
+ const base64 = parts[1]
181
+ .replace(/-/g, "+")
182
+ .replace(/_/g, "/")
183
+ .padEnd(Math.ceil(parts[1].length / 4) * 4, "=");
184
+ const parsed = JSON.parse(Buffer.from(base64, "base64").toString("utf8"));
185
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
186
+ return null;
187
+ return parsed;
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ function readJwtExpiresAt(token) {
194
+ const payload = decodeJwtPayload(token);
195
+ const exp = payload?.exp;
196
+ if (typeof exp !== "number" || !Number.isFinite(exp) || exp <= 0)
197
+ return undefined;
198
+ return Math.floor(exp * 1000);
199
+ }
200
+ function isFreshCodexToken(credentials, now) {
201
+ if (!credentials.oauthAccessToken)
202
+ return false;
203
+ if (typeof credentials.expiresAt !== "number")
204
+ return false;
205
+ return credentials.expiresAt > now.getTime() + CODEX_LOCAL_TOKEN_REFRESH_MARGIN_MS;
206
+ }
207
+ function readCodexLocalAuthCredentials(homeDir) {
175
208
  const authPath = path.join(homeDir, ".codex", "auth.json");
176
209
  try {
177
210
  const raw = fs.readFileSync(authPath, "utf8");
178
211
  const parsed = JSON.parse(raw);
179
- const token = parsed?.tokens?.access_token;
180
- return typeof token === "string" ? token.trim() : /* v8 ignore next -- defensive: codex login always writes a string token @preserve */ "";
212
+ const accessToken = typeof parsed.tokens?.access_token === "string" ? parsed.tokens.access_token.trim() : "";
213
+ if (!accessToken)
214
+ return {};
215
+ const refreshToken = typeof parsed.tokens?.refresh_token === "string" ? parsed.tokens.refresh_token.trim() : "";
216
+ const expiresAt = readJwtExpiresAt(accessToken);
217
+ return {
218
+ oauthAccessToken: accessToken,
219
+ ...(refreshToken ? { refreshToken } : {}),
220
+ ...(expiresAt ? { expiresAt } : {}),
221
+ };
181
222
  }
182
223
  catch {
183
- return "";
224
+ return {};
184
225
  }
185
226
  }
227
+ function isCodexLoginStatusReady(result) {
228
+ if (result.error || result.status !== 0)
229
+ return false;
230
+ const output = `${typeof result.stdout === "string" ? result.stdout : ""}\n${typeof result.stderr === "string" ? result.stderr : ""}`;
231
+ return output.toLowerCase().includes("logged in");
232
+ }
186
233
  function ensurePromptInput(promptInput, provider) {
187
234
  if (promptInput)
188
235
  return promptInput;
@@ -224,6 +271,7 @@ function validateAnthropicToken(token) {
224
271
  async function collectRuntimeAuthCredentials(input, deps) {
225
272
  const spawnSync = deps.spawnSync ?? child_process_1.spawnSync;
226
273
  const homeDir = deps.homeDir ?? os.homedir();
274
+ const now = deps.now ?? (() => new Date());
227
275
  if (input.provider === "github-copilot") {
228
276
  let token = process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || "";
229
277
  if (!token) {
@@ -267,9 +315,13 @@ async function collectRuntimeAuthCredentials(input, deps) {
267
315
  return { githubToken: token, baseUrl };
268
316
  }
269
317
  if (input.provider === "openai-codex") {
270
- // Always run codex login when auth is explicitly requested — stale tokens
271
- // are indistinguishable from valid ones without an API call, and the user
272
- // is asking to re-authenticate.
318
+ writeAuthProgress(input, "checking local Codex login...");
319
+ const localStatus = spawnSync("codex", ["login", "status"], { encoding: "utf8" });
320
+ const localCredentials = readCodexLocalAuthCredentials(homeDir);
321
+ if (isCodexLoginStatusReady(localStatus) && isFreshCodexToken(localCredentials, now())) {
322
+ writeAuthProgress(input, "using existing openai-codex local login...");
323
+ return localCredentials;
324
+ }
273
325
  (0, runtime_1.emitNervesEvent)({
274
326
  component: "daemon",
275
327
  event: "daemon.auth_codex_login_start",
@@ -285,11 +337,11 @@ async function collectRuntimeAuthCredentials(input, deps) {
285
337
  throw new Error(`'codex login' exited with status ${result.status}.`);
286
338
  }
287
339
  writeAuthProgress(input, "openai-codex login complete; reading local Codex token...");
288
- const token = readCodexAccessToken(homeDir);
289
- if (!token) {
340
+ const credentials = readCodexLocalAuthCredentials(homeDir);
341
+ if (!credentials.oauthAccessToken) {
290
342
  throw new Error("Codex login completed but no token was found in ~/.codex/auth.json. Re-run `codex login` and try again.");
291
343
  }
292
- return { oauthAccessToken: token };
344
+ return credentials;
293
345
  }
294
346
  if (input.provider === "anthropic") {
295
347
  (0, runtime_1.emitNervesEvent)({
@@ -251,6 +251,15 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
251
251
  envPath: process.env.PATH,
252
252
  });
253
253
  }
254
+ function defaultPrepareDaemonRuntimeReplacement() {
255
+ if (process.platform !== "darwin") {
256
+ return;
257
+ }
258
+ (0, launchd_1.bootoutLaunchAgentByLabel)({
259
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
260
+ userUid: process.getuid?.() ?? 0,
261
+ });
262
+ }
254
263
  async function defaultPromptInput(question) {
255
264
  const readline = await Promise.resolve().then(() => __importStar(require("readline/promises")));
256
265
  const rl = readline.createInterface({
@@ -615,6 +624,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
615
624
  syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
616
625
  pruneDaemonLogs: logs_prune_1.pruneDaemonLogs,
617
626
  ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
627
+ prepareDaemonRuntimeReplacement: defaultPrepareDaemonRuntimeReplacement,
618
628
  ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
619
629
  /* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
620
630
  detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
@@ -943,6 +943,7 @@ async function ensureDaemonRunning(deps, options = {}) {
943
943
  stopDaemon: async () => {
944
944
  await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
945
945
  },
946
+ prepareDaemonRuntimeReplacement: deps.prepareDaemonRuntimeReplacement,
946
947
  cleanupStaleSocket: deps.cleanupStaleSocket,
947
948
  startDaemonProcess: deps.startDaemonProcess,
948
949
  checkSocketAlive: deps.checkSocketAlive,
@@ -5901,6 +5902,20 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5901
5902
  return returnCliFailure(deps, daemonResult.message);
5902
5903
  }
5903
5904
  progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
5905
+ if (deps.ensureDaemonBootPersistence) {
5906
+ try {
5907
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
5908
+ }
5909
+ catch (error) {
5910
+ (0, runtime_1.emitNervesEvent)({
5911
+ level: "warn",
5912
+ component: "daemon",
5913
+ event: "daemon.system_setup_launchd_error",
5914
+ message: "failed to persist daemon boot startup",
5915
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
5916
+ });
5917
+ }
5918
+ }
5904
5919
  progress.startPhase("provider checks");
5905
5920
  const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
5906
5921
  daemonResult.stability = mergeStartupStability(daemonResult.stability, providerDegraded);
@@ -6069,23 +6084,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6069
6084
  const driftAdvisories = await collectAgentDriftAdvisories(deps);
6070
6085
  writeDriftAdvisorySummary(deps, driftAdvisories);
6071
6086
  }
6072
- // Persist boot startup AFTER daemon is running — bootstrap is safe now
6073
- // because the daemon socket exists, so launchd's KeepAlive registers
6074
- // for crash recovery without starting a competing process.
6075
- if (deps.ensureDaemonBootPersistence) {
6076
- try {
6077
- await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
6078
- }
6079
- catch (error) {
6080
- (0, runtime_1.emitNervesEvent)({
6081
- level: "warn",
6082
- component: "daemon",
6083
- event: "daemon.system_setup_launchd_error",
6084
- message: "failed to persist daemon boot startup",
6085
- meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
6086
- });
6087
- }
6088
- }
6089
6087
  return daemonResult.message;
6090
6088
  }
6091
6089
  if (command.kind === "daemon.dev") {
@@ -107,6 +107,24 @@ async function ensureCurrentDaemonRuntime(deps) {
107
107
  try {
108
108
  deps.onProgress?.("stopping the older background service");
109
109
  await deps.stopDaemon();
110
+ if (deps.prepareDaemonRuntimeReplacement) {
111
+ deps.onProgress?.("disabling daemon auto-restart during replacement");
112
+ try {
113
+ await Promise.resolve(deps.prepareDaemonRuntimeReplacement());
114
+ }
115
+ catch (error) {
116
+ (0, runtime_1.emitNervesEvent)({
117
+ level: "warn",
118
+ component: "daemon",
119
+ event: "daemon.runtime_sync_replacement_prepare_error",
120
+ message: "daemon runtime replacement preparation failed",
121
+ meta: {
122
+ socketPath: deps.socketPath,
123
+ reason: formatErrorReason(error),
124
+ },
125
+ });
126
+ }
127
+ }
110
128
  }
111
129
  catch (error) {
112
130
  const reason = formatErrorReason(error);
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DAEMON_PLIST_LABEL = void 0;
37
+ exports.bootoutLaunchAgentByLabel = bootoutLaunchAgentByLabel;
37
38
  exports.generateDaemonPlist = generateDaemonPlist;
38
39
  exports.writeLaunchAgentPlist = writeLaunchAgentPlist;
39
40
  exports.installLaunchAgent = installLaunchAgent;
@@ -48,6 +49,26 @@ function plistFilePath(homeDir) {
48
49
  function userLaunchDomain(userUid) {
49
50
  return `gui/${userUid}`;
50
51
  }
52
+ function bootoutLaunchAgentByLabel(deps) {
53
+ const domain = userLaunchDomain(deps.userUid);
54
+ try {
55
+ deps.exec(`launchctl bootout ${domain}/${exports.DAEMON_PLIST_LABEL}`);
56
+ (0, runtime_1.emitNervesEvent)({
57
+ component: "daemon",
58
+ event: "daemon.launchd_label_bootout",
59
+ message: "booted out daemon launch agent by label",
60
+ meta: { label: exports.DAEMON_PLIST_LABEL, domain },
61
+ });
62
+ }
63
+ catch (error) {
64
+ (0, runtime_1.emitNervesEvent)({
65
+ component: "daemon",
66
+ event: "daemon.launchd_label_bootout_skipped",
67
+ message: "daemon launch agent label bootout skipped",
68
+ meta: { label: exports.DAEMON_PLIST_LABEL, domain, reason: error instanceof Error ? error.message : String(error) },
69
+ });
70
+ }
71
+ }
51
72
  function generateDaemonPlist(options) {
52
73
  (0, runtime_1.emitNervesEvent)({
53
74
  component: "daemon",
@@ -145,12 +166,8 @@ function uninstallLaunchAgent(deps) {
145
166
  meta: {},
146
167
  });
147
168
  const fullPath = plistFilePath(deps.homeDir);
148
- const domain = userLaunchDomain(deps.userUid);
169
+ bootoutLaunchAgentByLabel(deps);
149
170
  if (deps.existsFile(fullPath)) {
150
- try {
151
- deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
152
- }
153
- catch { /* best effort */ }
154
171
  deps.removeFile(fullPath);
155
172
  }
156
173
  (0, runtime_1.emitNervesEvent)({
@@ -63,7 +63,7 @@ const PROVIDER_FIELD_SPLITS = {
63
63
  config: [],
64
64
  },
65
65
  "openai-codex": {
66
- credentials: ["oauthAccessToken"],
66
+ credentials: ["oauthAccessToken", "refreshToken", "expiresAt"],
67
67
  config: [],
68
68
  },
69
69
  azure: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.547",
3
+ "version": "0.1.0-alpha.549",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",