@ouro.bot/cli 0.1.0-alpha.200 → 0.1.0-alpha.202

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,22 @@
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.202",
6
+ "changes": [
7
+ "refactor: unify hatch + runtime auth into single credential path (PROVIDER_CREDENTIALS table)",
8
+ "fix: github-copilot credential discovery from stored secrets and env vars",
9
+ "feat: ping-verify credentials before entering serpent guide session",
10
+ "fix: clean up React tooltip timeouts on unmount to prevent state update warning",
11
+ "fix: guard against undefined child from spawn to prevent unhandled rejections"
12
+ ]
13
+ },
14
+ {
15
+ "version": "0.1.0-alpha.201",
16
+ "changes": [
17
+ "fix: don't launchctl bootstrap after daemon start \u2014 was starting competing daemon that killed the first"
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.200",
6
22
  "changes": [
@@ -41,6 +41,7 @@ exports.getMinimaxConfig = getMinimaxConfig;
41
41
  exports.getAnthropicConfig = getAnthropicConfig;
42
42
  exports.getOpenAICodexConfig = getOpenAICodexConfig;
43
43
  exports.getGithubCopilotConfig = getGithubCopilotConfig;
44
+ exports.getProviderConfig = getProviderConfig;
44
45
  exports.getTeamsConfig = getTeamsConfig;
45
46
  exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
46
47
  exports.getContextConfig = getContextConfig;
@@ -270,6 +271,10 @@ function getGithubCopilotConfig() {
270
271
  const config = loadConfig();
271
272
  return { ...config.providers["github-copilot"] };
272
273
  }
274
+ function getProviderConfig(provider) {
275
+ const config = loadConfig();
276
+ return { ...config.providers[provider] };
277
+ }
273
278
  function getTeamsConfig() {
274
279
  const config = loadConfig();
275
280
  return { ...config.teams };
@@ -42,31 +42,8 @@ function getProviderRuntimeFingerprint(facing) {
42
42
  const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
43
43
  const provider = facingConfig.provider;
44
44
  const model = facingConfig.model;
45
- /* v8 ignore next -- switch: not all provider branches exercised in CI @preserve */
46
- switch (provider) {
47
- case "azure": {
48
- const { apiKey, endpoint, deployment, apiVersion, managedIdentityClientId } = (0, config_1.getAzureConfig)();
49
- return JSON.stringify({ provider, model, apiKey, endpoint, deployment, apiVersion, managedIdentityClientId });
50
- }
51
- case "anthropic": {
52
- const { setupToken } = (0, config_1.getAnthropicConfig)();
53
- return JSON.stringify({ provider, model, setupToken });
54
- }
55
- case "minimax": {
56
- const { apiKey } = (0, config_1.getMinimaxConfig)();
57
- return JSON.stringify({ provider, model, apiKey });
58
- }
59
- case "openai-codex": {
60
- const { oauthAccessToken } = (0, config_1.getOpenAICodexConfig)();
61
- return JSON.stringify({ provider, model, oauthAccessToken });
62
- }
63
- /* v8 ignore start -- fingerprint: tested via provider init tests @preserve */
64
- case "github-copilot": {
65
- const { githubToken, baseUrl } = (0, config_1.getGithubCopilotConfig)();
66
- return JSON.stringify({ provider, model, githubToken, baseUrl });
67
- }
68
- /* v8 ignore stop */
69
- }
45
+ const providerConfig = (0, config_1.getProviderConfig)(provider);
46
+ return JSON.stringify({ provider, model, ...providerConfig });
70
47
  }
71
48
  function createProviderRegistry() {
72
49
  const factories = {
@@ -87,8 +87,8 @@ function checkAgentConfig(agentName, bundlesRoot, secretsRoot) {
87
87
  fix: `Add humanFacing.provider to ${agentJsonPath}. Valid providers: ${Object.keys(identity_1.PROVIDER_CREDENTIALS).join(", ")}`,
88
88
  };
89
89
  }
90
- const requiredFields = identity_1.PROVIDER_CREDENTIALS[provider];
91
- if (!requiredFields) {
90
+ const desc = identity_1.PROVIDER_CREDENTIALS[provider];
91
+ if (!desc) {
92
92
  return {
93
93
  ok: false,
94
94
  error: `Unknown provider '${provider}' in agent.json for '${agentName}'`,
@@ -128,7 +128,7 @@ function checkAgentConfig(agentName, bundlesRoot, secretsRoot) {
128
128
  return { ok: true };
129
129
  }
130
130
  }
131
- const missing = requiredFields.filter((field) => {
131
+ const missing = desc.required.filter((field) => {
132
132
  const val = providerSecrets[field];
133
133
  return typeof val !== "string" || val.length === 0;
134
134
  });
@@ -263,7 +263,7 @@ async function collectRuntimeAuthCredentials(input, deps) {
263
263
  const spawnSync = deps.spawnSync ?? child_process_1.spawnSync;
264
264
  const homeDir = deps.homeDir ?? os.homedir();
265
265
  if (input.provider === "github-copilot") {
266
- let token = process.env.GH_TOKEN?.trim() || "";
266
+ let token = process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || "";
267
267
  if (!token) {
268
268
  const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
269
269
  token = (result.status === 0 && result.stdout ? result.stdout.trim() : "");
@@ -361,95 +361,62 @@ async function collectRuntimeAuthCredentials(input, deps) {
361
361
  /* v8 ignore stop */
362
362
  return { setupToken };
363
363
  }
364
- if (input.provider === "minimax") {
365
- const prompt = ensurePromptInput(input.promptInput, input.provider);
366
- const apiKey = (await prompt("MiniMax API key: ")).trim();
367
- if (!apiKey)
368
- throw new Error("MiniMax API key is required.");
369
- return { apiKey };
370
- }
364
+ // Generic prompt-for-fields fallback (minimax, azure, any future simple providers)
371
365
  const prompt = ensurePromptInput(input.promptInput, input.provider);
372
- const apiKey = (await prompt("Azure API key: ")).trim();
373
- const endpoint = (await prompt("Azure endpoint: ")).trim();
374
- const deployment = (await prompt("Azure deployment: ")).trim();
375
- if (!apiKey || !endpoint || !deployment) {
376
- throw new Error("Azure API key, endpoint, and deployment are required.");
366
+ const desc = identity_1.PROVIDER_CREDENTIALS[input.provider];
367
+ const creds = {};
368
+ for (const field of desc.required) {
369
+ /* v8 ignore next -- fallback: all current providers define promptLabels for required fields @preserve */
370
+ const label = desc.promptLabels[field] ?? field;
371
+ const value = (await prompt(`${label}: `)).trim();
372
+ if (!value)
373
+ throw new Error(`${label} is required.`);
374
+ creds[field] = value;
377
375
  }
378
- return { apiKey, endpoint, deployment };
376
+ return creds;
379
377
  }
380
378
  async function resolveHatchCredentials(input) {
381
- const prompt = input.promptInput;
382
379
  const credentials = { ...(input.credentials ?? {}) };
383
- if (input.provider === "github-copilot" && !credentials.githubToken && input.runAuthFlow) {
384
- const result = await input.runAuthFlow({
385
- agentName: input.agentName,
386
- provider: "github-copilot",
387
- promptInput: prompt,
388
- });
389
- Object.assign(credentials, result.credentials);
390
- }
391
- if (input.provider === "anthropic" && !credentials.setupToken && input.runAuthFlow) {
380
+ // If all required fields are already provided, return as-is
381
+ const cred = credentials;
382
+ const missing = identity_1.PROVIDER_CREDENTIALS[input.provider].required.some((key) => !cred[key]);
383
+ if (!missing)
384
+ return credentials;
385
+ // Try the full auth flow (wraps collectRuntimeAuthCredentials + writes secrets)
386
+ if (input.runAuthFlow) {
392
387
  const result = await input.runAuthFlow({
393
388
  agentName: input.agentName,
394
- provider: "anthropic",
395
- promptInput: prompt,
389
+ provider: input.provider,
390
+ promptInput: input.promptInput,
396
391
  });
397
392
  Object.assign(credentials, result.credentials);
393
+ /* v8 ignore next 3 -- branch: auth flow always fills all required fields in production @preserve */
394
+ if (!identity_1.PROVIDER_CREDENTIALS[input.provider].required.some((key) => !credentials[key])) {
395
+ return credentials;
396
+ }
398
397
  }
399
- if (input.provider === "anthropic" && !credentials.setupToken && prompt) {
400
- credentials.setupToken = await prompt("Anthropic setup-token: ");
401
- }
402
- if (input.provider === "openai-codex" && !credentials.oauthAccessToken && input.runAuthFlow) {
403
- const result = await input.runAuthFlow({
404
- agentName: input.agentName,
405
- provider: "openai-codex",
406
- promptInput: prompt,
407
- });
408
- Object.assign(credentials, result.credentials);
409
- }
410
- if (input.provider === "openai-codex" && !credentials.oauthAccessToken && prompt) {
411
- credentials.oauthAccessToken = await prompt("OpenAI Codex OAuth token: ");
412
- }
413
- if (input.provider === "minimax" && !credentials.apiKey && prompt) {
414
- credentials.apiKey = await prompt("MiniMax API key: ");
415
- }
416
- if (input.provider === "azure") {
417
- if (!credentials.apiKey && prompt)
418
- credentials.apiKey = await prompt("Azure API key: ");
419
- if (!credentials.endpoint && prompt)
420
- credentials.endpoint = await prompt("Azure endpoint: ");
421
- if (!credentials.deployment && prompt)
422
- credentials.deployment = await prompt("Azure deployment: ");
398
+ // Prompt for any still-missing required fields
399
+ /* v8 ignore next -- guard: no promptInput means we can't collect remaining fields @preserve */
400
+ if (input.promptInput) {
401
+ const desc = identity_1.PROVIDER_CREDENTIALS[input.provider];
402
+ for (const field of desc.required) {
403
+ if (!cred[field]) {
404
+ const label = desc.promptLabels[field] ?? field;
405
+ cred[field] = await input.promptInput(`${label}: `);
406
+ }
407
+ }
423
408
  }
424
409
  return credentials;
425
410
  }
426
411
  function applyCredentials(secrets, provider, credentials) {
427
- if (provider === "anthropic") {
428
- secrets.providers.anthropic.setupToken = credentials.setupToken.trim();
429
- /* v8 ignore start -- token refresh fields: populated when OAuth exchange succeeds @preserve */
430
- if (credentials.refreshToken)
431
- secrets.providers.anthropic.refreshToken = credentials.refreshToken;
432
- if (credentials.expiresAt)
433
- secrets.providers.anthropic.expiresAt = credentials.expiresAt;
434
- /* v8 ignore stop */
435
- return;
436
- }
437
- if (provider === "github-copilot") {
438
- secrets.providers["github-copilot"].githubToken = credentials.githubToken.trim();
439
- secrets.providers["github-copilot"].baseUrl = credentials.baseUrl.trim();
440
- return;
441
- }
442
- if (provider === "openai-codex") {
443
- secrets.providers["openai-codex"].oauthAccessToken = credentials.oauthAccessToken.trim();
444
- return;
445
- }
446
- if (provider === "minimax") {
447
- secrets.providers.minimax.apiKey = credentials.apiKey.trim();
448
- return;
412
+ const target = secrets.providers[provider];
413
+ // Copy all non-empty credential fields to the provider's secrets block
414
+ for (const [key, value] of Object.entries(credentials)) {
415
+ /* v8 ignore next -- guard: skip null/empty fields from partial credential objects @preserve */
416
+ if (value != null && value !== "") {
417
+ target[key] = typeof value === "string" ? value.trim() : value;
418
+ }
449
419
  }
450
- secrets.providers.azure.apiKey = credentials.apiKey.trim();
451
- secrets.providers.azure.endpoint = credentials.endpoint.trim();
452
- secrets.providers.azure.deployment = credentials.deployment.trim();
453
420
  }
454
421
  async function runRuntimeAuthFlow(input, deps = {}) {
455
422
  (0, runtime_1.emitNervesEvent)({
@@ -561,20 +561,10 @@ function parseLinkCommand(args, kind = "friend.link") {
561
561
  function isAgentProvider(value) {
562
562
  return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
563
563
  }
564
- /* v8 ignore start -- hasStoredCredentials: per-provider branches tested via auth switch tests @preserve */
564
+ /* v8 ignore next 3 -- only called from auth.switch inside integration block @preserve */
565
565
  function hasStoredCredentials(provider, providerSecrets) {
566
- if (provider === "anthropic")
567
- return !!providerSecrets.setupToken;
568
- if (provider === "openai-codex")
569
- return !!providerSecrets.oauthAccessToken;
570
- if (provider === "github-copilot")
571
- return !!providerSecrets.githubToken;
572
- if (provider === "minimax")
573
- return !!providerSecrets.apiKey;
574
- // azure
575
- return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
566
+ return identity_1.PROVIDER_CREDENTIALS[provider].required.every((key) => !!providerSecrets[key]);
576
567
  }
577
- /* v8 ignore stop */
578
568
  /* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
579
569
  async function verifyProviderCredentials(provider, providers) {
580
570
  const config = providers[provider];
@@ -1298,14 +1288,10 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
1298
1288
  return;
1299
1289
  }
1300
1290
  const homeDir = os.homedir();
1301
- const launchdDeps = {
1302
- exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
1291
+ const writeDeps = {
1303
1292
  writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
1304
- removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
1305
- existsFile: (filePath) => fs.existsSync(filePath),
1306
1293
  mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
1307
1294
  homeDir,
1308
- userUid: process.getuid?.() ?? 0,
1309
1295
  };
1310
1296
  const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
1311
1297
  /* v8 ignore next -- covered via mock in daemon-cli-defaults.test.ts; v8 on CI attributes the real fs.existsSync branch to the non-mock load @preserve */
@@ -1319,7 +1305,12 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
1319
1305
  });
1320
1306
  }
1321
1307
  const logDir = (0, identity_1.getAgentDaemonLogsDir)();
1322
- (0, launchd_1.installLaunchAgent)(launchdDeps, {
1308
+ // Write plist only — do NOT launchctl bootstrap.
1309
+ // The daemon is already running (started by ouro up). Bootstrapping would
1310
+ // start a SECOND daemon via launchd's RunAtLoad, causing a race where
1311
+ // killOrphanProcesses kills the first daemon and both end up dead.
1312
+ // The plist on disk is sufficient: launchd picks it up on login.
1313
+ (0, launchd_1.writeLaunchAgentPlist)(writeDeps, {
1323
1314
  nodePath: process.execPath,
1324
1315
  entryPath,
1325
1316
  socketPath,
@@ -1378,17 +1369,16 @@ function discoverExistingCredentials(secretsRoot) {
1378
1369
  if (!parsed.providers)
1379
1370
  continue;
1380
1371
  for (const [provName, provConfig] of Object.entries(parsed.providers)) {
1381
- if (provName === "anthropic" && provConfig.setupToken) {
1382
- found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
1383
- }
1384
- else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
1385
- found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
1386
- }
1387
- else if (provName === "minimax" && provConfig.apiKey) {
1388
- found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
1389
- }
1390
- else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
1391
- found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
1372
+ const desc = identity_1.PROVIDER_CREDENTIALS[provName];
1373
+ /* v8 ignore next -- guard: unknown provider names in stored secrets @preserve */
1374
+ if (!desc)
1375
+ continue;
1376
+ const hasRequired = desc.required.every((key) => !!provConfig[key]);
1377
+ if (hasRequired) {
1378
+ const credentials = {};
1379
+ for (const key of Object.keys(provConfig))
1380
+ credentials[key] = provConfig[key];
1381
+ found.push({ agentName: entry.name, provider: provName, credentials, providerConfig: { ...provConfig } });
1392
1382
  }
1393
1383
  }
1394
1384
  }
@@ -1432,33 +1422,27 @@ async function defaultRunSerpentGuide() {
1432
1422
  "github-copilot": "claude-sonnet-4.6",
1433
1423
  azure: "",
1434
1424
  };
1435
- // Scan environment variables for API keys
1436
- const envKeys = [
1437
- { provider: "anthropic", envVar: "ANTHROPIC_API_KEY", credKey: "setupToken" },
1438
- { provider: "openai-codex", envVar: "OPENAI_API_KEY", credKey: "oauthAccessToken" },
1439
- { provider: "azure", envVar: "AZURE_OPENAI_API_KEY", credKey: "apiKey" },
1440
- { provider: "azure", envVar: "AZURE_OPENAI_KEY", credKey: "apiKey" },
1441
- { provider: "minimax", envVar: "MINIMAX_API_KEY", credKey: "apiKey" },
1442
- { provider: "github-copilot", envVar: "GITHUB_TOKEN", credKey: "token" },
1443
- ];
1425
+ // Scan environment variables for API keys using the canonical descriptor
1444
1426
  const envDiscovered = [];
1445
- for (const { provider, envVar, credKey } of envKeys) {
1446
- const value = process.env[envVar];
1447
- if (value) {
1448
- const envCred = { [credKey]: value };
1449
- // For Azure, also check for endpoint and deployment env vars
1450
- if (provider === "azure") {
1451
- const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
1452
- const deployment = process.env.AZURE_OPENAI_DEPLOYMENT;
1453
- if (endpoint)
1454
- envCred.endpoint = endpoint;
1455
- if (deployment)
1456
- envCred.deployment = deployment;
1427
+ for (const [provider, desc] of Object.entries(identity_1.PROVIDER_CREDENTIALS)) {
1428
+ const envCred = {};
1429
+ let firstEnvVar;
1430
+ for (const [envVar, credKey] of Object.entries(desc.envVars)) {
1431
+ const value = process.env[envVar];
1432
+ if (value) {
1433
+ ;
1434
+ envCred[credKey] = value;
1435
+ if (!firstEnvVar)
1436
+ firstEnvVar = envVar;
1457
1437
  }
1438
+ }
1439
+ // Only register if at least one required field was found
1440
+ const hasRequired = desc.required.some((key) => !!envCred[key]);
1441
+ if (hasRequired && firstEnvVar) {
1458
1442
  const provCfg = { model: defaultModels[provider] };
1459
1443
  if (provider === "azure" && envCred.deployment)
1460
1444
  provCfg.deployment = envCred.deployment;
1461
- envDiscovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg, envVar });
1445
+ envDiscovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg, envVar: firstEnvVar });
1462
1446
  discovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg });
1463
1447
  }
1464
1448
  }
@@ -1490,17 +1474,7 @@ async function defaultRunSerpentGuide() {
1490
1474
  }
1491
1475
  providerRaw = pRaw;
1492
1476
  providerConfig = { model: defaultModels[providerRaw] };
1493
- if (providerRaw === "anthropic")
1494
- credentials.setupToken = await coldPrompt("API key: ");
1495
- if (providerRaw === "openai-codex")
1496
- credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
1497
- if (providerRaw === "minimax")
1498
- credentials.apiKey = await coldPrompt("API key: ");
1499
- if (providerRaw === "azure") {
1500
- credentials.apiKey = await coldPrompt("API key: ");
1501
- credentials.endpoint = await coldPrompt("endpoint: ");
1502
- credentials.deployment = await coldPrompt("deployment: ");
1503
- }
1477
+ credentials = await (0, auth_flow_1.collectRuntimeAuthCredentials)({ agentName: "SerpentGuide", provider: providerRaw, promptInput: coldPrompt }, {});
1504
1478
  }
1505
1479
  }
1506
1480
  else {
@@ -1514,17 +1488,7 @@ async function defaultRunSerpentGuide() {
1514
1488
  }
1515
1489
  providerRaw = pRaw;
1516
1490
  providerConfig = { model: defaultModels[providerRaw] };
1517
- if (providerRaw === "anthropic")
1518
- credentials.setupToken = await coldPrompt("API key: ");
1519
- if (providerRaw === "openai-codex")
1520
- credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
1521
- if (providerRaw === "minimax")
1522
- credentials.apiKey = await coldPrompt("API key: ");
1523
- if (providerRaw === "azure") {
1524
- credentials.apiKey = await coldPrompt("API key: ");
1525
- credentials.endpoint = await coldPrompt("endpoint: ");
1526
- credentials.deployment = await coldPrompt("deployment: ");
1527
- }
1491
+ credentials = await (0, auth_flow_1.collectRuntimeAuthCredentials)({ agentName: "SerpentGuide", provider: providerRaw, promptInput: coldPrompt }, {});
1528
1492
  }
1529
1493
  coldRl.close();
1530
1494
  process.stdout.write("\n");
@@ -1560,6 +1524,13 @@ async function defaultRunSerpentGuide() {
1560
1524
  [providerRaw]: { ...providerConfig, ...credentials },
1561
1525
  },
1562
1526
  });
1527
+ // Ping-verify credentials before entering the serpent guide session
1528
+ const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../provider-ping")));
1529
+ const pingResult = await pingProvider(providerRaw, { ...providerConfig, ...credentials });
1530
+ if (!pingResult.ok) {
1531
+ process.stdout.write(`credentials didn't work (${pingResult.message}). run 'ouro hatch' to try again.\n`);
1532
+ return null;
1533
+ }
1563
1534
  const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
1564
1535
  const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
1565
1536
  tempDir,
@@ -45,16 +45,7 @@ const auth_flow_1 = require("./auth-flow");
45
45
  const habit_parser_1 = require("./habit-parser");
46
46
  const hatch_specialist_1 = require("./hatch-specialist");
47
47
  function requiredCredentialKeys(provider) {
48
- if (provider === "anthropic")
49
- return ["setupToken"];
50
- if (provider === "openai-codex")
51
- return ["oauthAccessToken"];
52
- /* v8 ignore next -- branch tested via requiredCredentialKeys unit test @preserve */
53
- if (provider === "github-copilot")
54
- return ["githubToken"];
55
- if (provider === "minimax")
56
- return ["apiKey"];
57
- return ["apiKey", "endpoint", "deployment"];
48
+ return identity_1.PROVIDER_CREDENTIALS[provider].required;
58
49
  }
59
50
  function validateCredentials(provider, credentials) {
60
51
  const missing = requiredCredentialKeys(provider).filter((key) => {
@@ -77,6 +77,7 @@ class DaemonProcessManager {
77
77
  stopRequested: false,
78
78
  cooldownTimer: null,
79
79
  cooldownRetryCount: 0,
80
+ fastCrashCount: 0,
80
81
  snapshot: {
81
82
  name: agent.name,
82
83
  channel: agent.channel,
@@ -141,6 +142,12 @@ class DaemonProcessManager {
141
142
  env: state.config.env ? { ...process.env, ...state.config.env } : process.env,
142
143
  stdio: ["ignore", "ignore", "ignore", "ipc"],
143
144
  });
145
+ /* v8 ignore next 7 -- defensive: spawn should always return a ChildProcess @preserve */
146
+ if (!child) {
147
+ state.snapshot.status = "crashed";
148
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.agent_spawn_failed", message: "spawn returned null", meta: { agent } });
149
+ return;
150
+ }
144
151
  state.process = child;
145
152
  state.snapshot.status = "running";
146
153
  state.snapshot.pid = child.pid ?? null;
@@ -251,6 +258,29 @@ class DaemonProcessManager {
251
258
  return;
252
259
  }
253
260
  state.snapshot.lastCrashAt = new Date(now).toISOString();
261
+ // Fast-crash detection: if the agent dies within 5 seconds of starting, it's likely
262
+ // a configuration issue (missing credentials, bad provider, etc.) not a transient failure.
263
+ // After 3 consecutive fast crashes, stop retrying and mark as config-failed.
264
+ const FAST_CRASH_THRESHOLD_MS = 5000;
265
+ const FAST_CRASH_MAX = 3;
266
+ if (runDuration < FAST_CRASH_THRESHOLD_MS) {
267
+ state.fastCrashCount = state.fastCrashCount + 1;
268
+ if (state.fastCrashCount >= FAST_CRASH_MAX) {
269
+ state.snapshot.status = "crashed";
270
+ (0, runtime_1.emitNervesEvent)({
271
+ level: "error",
272
+ component: "daemon",
273
+ event: "daemon.agent_config_failure",
274
+ message: `agent crashed ${FAST_CRASH_MAX} times within ${FAST_CRASH_THRESHOLD_MS}ms of startup — likely a configuration issue (missing credentials, bad provider). Not retrying. Fix the config and run \`ouro up\` to restart.`,
275
+ meta: { agent: state.config.name, fastCrashCount: state.fastCrashCount, avgRunDurationMs: runDuration },
276
+ });
277
+ return; // Don't schedule cooldown recovery — this needs human/agent intervention
278
+ }
279
+ }
280
+ else {
281
+ // Reset fast-crash counter on a stable run
282
+ state.fastCrashCount = 0;
283
+ }
254
284
  state.crashTimestamps = state.crashTimestamps.filter((crashTs) => crashTs >= startOfHour(now));
255
285
  state.crashTimestamps.push(now);
256
286
  if (state.crashTimestamps.length > this.maxRestartsPerHour) {
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.HARNESS_CANONICAL_REPO_URL = exports.PROVIDER_CREDENTIALS = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
36
+ exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = void 0;
37
37
  exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
38
38
  exports.getAgentName = getAgentName;
39
39
  exports.getRepoRoot = getRepoRoot;
@@ -57,6 +57,14 @@ const os = __importStar(require("os"));
57
57
  const path = __importStar(require("path"));
58
58
  const runtime_1 = require("../nerves/runtime");
59
59
  const migrate_config_1 = require("./migrate-config");
60
+ /** Single source of truth for per-provider credential field names, env var mappings, and prompt labels. */
61
+ exports.PROVIDER_CREDENTIALS = {
62
+ anthropic: { required: ["setupToken"], envVars: { ANTHROPIC_API_KEY: "setupToken" }, promptLabels: { setupToken: "Anthropic setup-token" } },
63
+ "openai-codex": { required: ["oauthAccessToken"], envVars: { OPENAI_API_KEY: "oauthAccessToken" }, promptLabels: { oauthAccessToken: "OpenAI Codex OAuth token" } },
64
+ azure: { required: ["apiKey", "endpoint", "deployment"], envVars: { AZURE_OPENAI_API_KEY: "apiKey", AZURE_OPENAI_KEY: "apiKey", AZURE_OPENAI_ENDPOINT: "endpoint", AZURE_OPENAI_DEPLOYMENT: "deployment" }, promptLabels: { apiKey: "Azure API key", endpoint: "Azure endpoint", deployment: "Azure deployment" } },
65
+ minimax: { required: ["apiKey"], envVars: { MINIMAX_API_KEY: "apiKey" }, promptLabels: { apiKey: "MiniMax API key" } },
66
+ "github-copilot": { required: ["githubToken", "baseUrl"], envVars: { GH_TOKEN: "githubToken", GITHUB_TOKEN: "githubToken" }, promptLabels: { githubToken: "GitHub token" } },
67
+ };
60
68
  exports.DEFAULT_AGENT_CONTEXT = {
61
69
  maxTokens: 80000,
62
70
  contextMargin: 20,
@@ -208,17 +216,6 @@ function resolveOptionalAgentName(agentName) {
208
216
  function getAgentStateRoot(agentName) {
209
217
  return path.join(getAgentRoot(resolveOptionalAgentName(agentName)), "state");
210
218
  }
211
- /**
212
- * Required credential fields per provider.
213
- * Used by daemon config-check to validate secrets before spawning an agent.
214
- */
215
- exports.PROVIDER_CREDENTIALS = {
216
- azure: ["apiKey", "endpoint", "deployment"],
217
- minimax: ["apiKey"],
218
- anthropic: ["setupToken"],
219
- "openai-codex": ["oauthAccessToken"],
220
- "github-copilot": ["githubToken", "baseUrl"],
221
- };
222
219
  exports.HARNESS_CANONICAL_REPO_URL = "https://github.com/ouroborosbot/ouroboros.git";
223
220
  function getAgentRepoWorkspacesRoot(agentName) {
224
221
  return path.join(getAgentStateRoot(resolveOptionalAgentName(agentName)), "workspaces");
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitizeErrorMessage = sanitizeErrorMessage;
4
4
  exports.pingProvider = pingProvider;
5
5
  exports.runHealthInventory = runHealthInventory;
6
+ const identity_1 = require("./identity");
6
7
  const anthropic_1 = require("./providers/anthropic");
7
8
  const azure_1 = require("./providers/azure");
8
9
  const minimax_1 = require("./providers/minimax");
@@ -42,25 +43,8 @@ function sanitizeErrorMessage(message) {
42
43
  return message;
43
44
  }
44
45
  function hasEmptyCredentials(provider, config) {
45
- switch (provider) {
46
- case "anthropic":
47
- return !config.setupToken;
48
- case "openai-codex":
49
- return !config.oauthAccessToken;
50
- case "minimax":
51
- return !config.apiKey;
52
- case "azure": {
53
- const azure = config;
54
- return !(azure.apiKey && azure.endpoint && azure.deployment);
55
- }
56
- case "github-copilot": {
57
- const copilot = config;
58
- return !(copilot.githubToken && copilot.baseUrl);
59
- }
60
- /* v8 ignore next 2 -- exhaustive: all providers handled above @preserve */
61
- default:
62
- return true;
63
- }
46
+ const record = config;
47
+ return identity_1.PROVIDER_CREDENTIALS[provider].required.some((key) => !record[key]);
64
48
  }
65
49
  function createRuntimeForPing(provider, _config) {
66
50
  // The provider constructors read credentials from the active config via getXxxConfig().
@@ -151,6 +151,11 @@ function InputArea({ onSubmit, onCtrlC, history, agentName, model }) {
151
151
  const cursorRef = (0, react_1.useRef)(0);
152
152
  const historyIdx = (0, react_1.useRef)(-1);
153
153
  const savedInput = (0, react_1.useRef)("");
154
+ const tooltipTimerRef = (0, react_1.useRef)(null);
155
+ (0, react_1.useEffect)(() => () => {
156
+ if (tooltipTimerRef.current)
157
+ clearTimeout(tooltipTimerRef.current);
158
+ }, []);
154
159
  // Helper: update input and cursor together
155
160
  const updateInput = (text, pos) => {
156
161
  inputRef.current = text;
@@ -174,7 +179,9 @@ function InputArea({ onSubmit, onCtrlC, history, agentName, model }) {
174
179
  else if (action === "warn") {
175
180
  // tooltip handled below
176
181
  setTooltip("Ctrl-C again to exit");
177
- setTimeout(() => setTooltip(""), 3000);
182
+ if (tooltipTimerRef.current)
183
+ clearTimeout(tooltipTimerRef.current);
184
+ tooltipTimerRef.current = setTimeout(() => setTooltip(""), 3000);
178
185
  }
179
186
  // "abort" and "exit" are handled by the parent (cli.ts)
180
187
  }, [onCtrlC]);
@@ -191,7 +198,9 @@ function InputArea({ onSubmit, onCtrlC, history, agentName, model }) {
191
198
  updateInput("");
192
199
  historyIdx.current = -1;
193
200
  setTooltip("Esc again to clear");
194
- setTimeout(() => setTooltip(""), 2000);
201
+ if (tooltipTimerRef.current)
202
+ clearTimeout(tooltipTimerRef.current);
203
+ tooltipTimerRef.current = setTimeout(() => setTooltip(""), 2000);
195
204
  }
196
205
  else {
197
206
  setTooltip("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.200",
3
+ "version": "0.1.0-alpha.202",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",