primcli 1.37.0 → 1.38.1

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.
@@ -4,16 +4,16 @@ import { $ as putAgentContactPolicy, A as getRepeatingSend, B as listEmails, C a
4
4
  import { A as saveActiveChatState, C as resolveCliAuth, D as deleteChatState, E as chatStatePath, O as loadActiveChatState, T as saveSignupCredentials, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as hasStoredCliLogin, c as resolveConfigEnvironment, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as PrimitiveApiClient, k as loadChatConversationByLocalId, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as saveCliCredentials, x as loadCliCredentials, y as detectPrimitiveKeyEnvMisname } from "../cli-config-UOlrfcPz.js";
5
5
  import { S as resolveListenSubscription, _ as acquireListenLock, a as mailLockBusy, c as mailTime, d as readMailJson, f as removeMailFile, g as ListenStateError, h as writeMailJson, i as mailId, l as privateMailDirectory, m as withMailLock, o as mailObject, p as syncMailDirectory, r as mailAddress, s as mailString, u as privateMailPermissions, x as normalizeListenOrigin, y as listenIdentity } from "../shared-mail-files-CZqI1kj6.js";
6
6
  import { $ as extractErrorCode, A as reserveSharedMailSubscription, At as queryUsesAutomated, B as sentInThread, C as thrownErrorExitCode, Ct as AutomatedFilterUnsupportedError, D as ensureSharedMailSubscription, Dt as expectedAutomatedVerdicts, E as sharedMailScope, Et as ensureSearchReportsAutomated, F as describeWake, Ft as resolveCliApiRequestConfig, G as EventConnection, H as serverRelationship, I as displayAddress, J as pushFile, K as EventReceiverError, L as formatWakeContext, M as readConversationFollow, Mt as createAuthenticatedCliApiClient, N as otherParticipants, Nt as createCliApiClient, O as createSharedMailWaiter, Ot as hasAutomatedVerdict, P as bareAddress, Pt as refreshStoredCliCredentials, Q as createOperationCommand, R as latestOwnOutbound, S as successfulSendOutcome, St as AUTOMATED_VALUES, T as openSharedMailReceiver, Tt as automatedRejectedError, U as wakeRelationship, V as serverMuted, W as readSharedMailOwner, X as API_ERROR_CODES, Y as API_BASE_URL_FLAG_DESCRIPTION, Z as TIME_FLAG_DESCRIPTION, _ as reportSendCommandResult, _t as queryUsesAwaiting, a as assertValidIdempotencyKey, at as writeErrorWithHints, b as sentHistoryWindowStart, bt as AUTOMATED_FILTER_UNSUPPORTED_CODE, c as checkPriorReplies, ct as REPLY_STATE_UNSUPPORTED_CODE, d as formatDeletedEarlierSendNotice, dt as awaitingRejectedError, et as extractErrorPayload, f as formatPriorRepliesCheckSkipped, ft as ensureSearchReportsReplyState, g as priorRepliesThatWentOut, gt as isAwaitingRejectedError, h as formatSendRecordFailureSummary, ht as hasReplyState, i as apiResultHttpStatus, it as surfaceUnauthorizedHint, j as followEmailConversation, k as openSharedMailStore, kt as isAutomatedRejectedError, l as classifySendError, lt as ReplyStateUnsupportedError, m as formatSendFailureSummary, mt as formatRepliesCell, n as IDEMPOTENCY_KEY_FLAG_DESCRIPTION, nt as readTextFileFlag, o as buildFollowUpCommand, ot as AWAITING_FLAG_DESCRIPTION, p as formatPriorRepliesWarning, pt as formatAwaitingCell, q as pullFile, r as SEND_OUTCOME_HELP, rt as runWithTiming, s as buildThrownSendFailureEnvelope, st as AWAITING_VALUES, t as sendSignal, tt as formatErrorPayload, u as deriveSendIdempotencyKey, ut as assertAwaitingFilterRows, v as sendIdentityFields, vt as formatAlreadySentNotice, w as reconcileChatSend, wt as assertAutomatedVerdict, x as serializeErrorPayload, xt as AUTOMATED_FLAG_DESCRIPTION, y as sendOutcomeExitCode, yt as writeIdempotentReplayBannerIfReplay, z as readThreadContext } from "../signal-command-CKaEqXro.js";
7
- import { _ as NativeSessionNotLoadedError, a as notificationSenders, c as NotificationRetryError, d as readConversationStatusContent, f as SESSION_UUID$1, g as NativeSessionError, h as NativeSessionDisconnectedError, i as notificationScope, l as isRoutineNotificationContent, m as defaultSessionSocket, n as installConnectSkill, o as openSessionNotifications, p as connectNativeSession, r as readBundledConnectSkill, s as notificationReceiptPage, t as detectAgentRuntime, u as notificationPartReader, v as NotificationOutcomeUnknownError } from "../connect-skill-DednypYJ.js";
7
+ import { _ as NativeSessionDisconnectedError, a as readBundledConnectSkill, b as NotificationOutcomeUnknownError, c as openSessionNotifications, d as isRoutineNotificationContent, f as notificationPartReader, g as defaultSessionSocket, h as connectNativeSession, i as installedVersion, l as notificationReceiptPage, m as SESSION_UUID$1, n as detectAgentRuntime, o as notificationScope, p as readConversationStatusContent, r as installConnectSkill, s as notificationSenders, t as connectSkillTarget, u as NotificationRetryError, v as NativeSessionError, y as NativeSessionNotLoadedError } from "../connect-skill-HJNQC3A9.js";
8
8
  import { c as startBackgroundListen, i as backgroundListenToken, l as stopBackgroundListen, o as runBackgroundListen, r as backgroundListenStatus, u as verifyBackgroundListenTarget } from "../listen-background-Bsofqq_6.js";
9
9
  import { F as readBeforeDeadline, I as scopedChatSenderTrust, L as presenceDisposition, M as isConnectedChatCredential, N as isPlainChatReply, P as isScopedChatReply, R as readPresenceProjection, T as restoreAutoWorking, g as haltAutoWorking, h as dispatchAutoWorking, l as autoSignalEligible, m as dispatchAutoRead } from "../auto-signals-UPcJmV3j.js";
10
10
  import { t as guardJsonOutputCommands } from "../json-output-BVRgl6jf.js";
11
11
  import { Args, Command, Errors, Flags, ux } from "@oclif/core";
12
- import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import { accessSync, chmodSync, closeSync, constants, copyFileSync, cpSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
13
13
  import { createHash, randomUUID, scryptSync } from "node:crypto";
14
- import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
14
+ import path, { basename, delimiter, dirname, join, relative, resolve, sep } from "node:path";
15
15
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
16
- import { execFile, spawn } from "node:child_process";
16
+ import { execFile, execFileSync, spawn } from "node:child_process";
17
17
  import { homedir, hostname } from "node:os";
18
18
  import { promisify } from "node:util";
19
19
  import process$1 from "node:process";
@@ -10741,11 +10741,27 @@ const openapiDocument = {
10741
10741
  },
10742
10742
  "quota_mb": {
10743
10743
  "type": "number",
10744
- "description": "Storage quota in megabytes (based on plan)"
10744
+ "description": "The plan's included storage in megabytes, the same value as included_mb. It is an allowance, not a limit; see limit_mb."
10745
10745
  },
10746
10746
  "percentage": {
10747
10747
  "type": "number",
10748
- "description": "Percentage of quota used (1 decimal)"
10748
+ "description": "Storage used as a percentage of the included storage (1 decimal). Can exceed 100."
10749
+ },
10750
+ "included_mb": {
10751
+ "type": "number",
10752
+ "description": "Storage included with the plan, in megabytes."
10753
+ },
10754
+ "limit_mb": {
10755
+ "type": ["number", "null"],
10756
+ "description": "Storage in megabytes past which inbound email is refused. Null when inbound email is never refused for storage."
10757
+ },
10758
+ "overage_mb": {
10759
+ "type": "number",
10760
+ "description": "Storage past included_mb, in megabytes, counting stored email and the search index."
10761
+ },
10762
+ "search_index_bytes": {
10763
+ "type": "integer",
10764
+ "description": "Storage attributed to the semantic search index, in bytes. Counted toward included_mb and overage_mb, never toward limit_mb."
10749
10765
  },
10750
10766
  "emails_count": {
10751
10767
  "type": "integer",
@@ -16888,11 +16904,27 @@ const operationManifest = [
16888
16904
  },
16889
16905
  "quota_mb": {
16890
16906
  "type": "number",
16891
- "description": "Storage quota in megabytes (based on plan)"
16907
+ "description": "The plan's included storage in megabytes, the same value as included_mb. It is an allowance, not a limit; see limit_mb."
16892
16908
  },
16893
16909
  "percentage": {
16894
16910
  "type": "number",
16895
- "description": "Percentage of quota used (1 decimal)"
16911
+ "description": "Storage used as a percentage of the included storage (1 decimal). Can exceed 100."
16912
+ },
16913
+ "included_mb": {
16914
+ "type": "number",
16915
+ "description": "Storage included with the plan, in megabytes."
16916
+ },
16917
+ "limit_mb": {
16918
+ "type": ["number", "null"],
16919
+ "description": "Storage in megabytes past which inbound email is refused. Null when inbound email is never refused for storage."
16920
+ },
16921
+ "overage_mb": {
16922
+ "type": "number",
16923
+ "description": "Storage past included_mb, in megabytes, counting stored email and the search index."
16924
+ },
16925
+ "search_index_bytes": {
16926
+ "type": "integer",
16927
+ "description": "Storage attributed to the semantic search index, in bytes. Counted toward included_mb and overage_mb, never toward limit_mb."
16896
16928
  },
16897
16929
  "emails_count": {
16898
16930
  "type": "integer",
@@ -34954,10 +34986,10 @@ function isMemoryJsonValue(value, seen = /* @__PURE__ */ new Set()) {
34954
34986
  //#region src/oclif/claude-wake-install.ts
34955
34987
  const HOOK_MARKER = "primitive-agent-wake-v1";
34956
34988
  const WAKE_EVENTS = ["Stop", "SessionStart"];
34957
- const PENDING_MARKER = "primitive-pending-mail-v1";
34989
+ const PENDING_MARKER$1 = "primitive-pending-mail-v1";
34958
34990
  const PENDING_EVENT = "PostToolUse";
34959
- const LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
34960
- function record$2(value) {
34991
+ const LOCK_WAIT$1 = new Int32Array(new SharedArrayBuffer(4));
34992
+ function record$4(value) {
34961
34993
  return value !== null && typeof value === "object" && !Array.isArray(value);
34962
34994
  }
34963
34995
  function claudeWakeHookStatus(options) {
@@ -34973,21 +35005,21 @@ function claudeWakeHookStatus(options) {
34973
35005
  if (!SESSION_UUID$1.test(options.sessionId)) return unavailable;
34974
35006
  const sessionId = options.sessionId.toLowerCase();
34975
35007
  const settings = readMailJson(join(resolve(options.env?.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), "settings.json"));
34976
- if (!record$2(settings) || !record$2(settings.hooks)) return unavailable;
35008
+ if (!record$4(settings) || !record$4(settings.hooks)) return unavailable;
34977
35009
  const hooks = settings.hooks;
34978
35010
  const owns = (event, script, marker) => {
34979
35011
  const entries = hooks[event];
34980
- return Array.isArray(entries) && entries.some((entry) => record$2(entry) && Array.isArray(entry.hooks) && entry.hooks.some((candidate) => {
34981
- if (!record$2(candidate) || !Array.isArray(candidate.args)) return false;
35012
+ return Array.isArray(entries) && entries.some((entry) => record$4(entry) && Array.isArray(entry.hooks) && entry.hooks.some((candidate) => {
35013
+ if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
34982
35014
  const args = candidate.args;
34983
35015
  const offset = 2;
34984
35016
  return candidate.type === "command" && candidate.command === process.execPath && args.length === 7 && typeof args[0] === "string" && basename(args[0]) === script && args[offset] === configDir && args[offset + 1] === profileName && args[offset + 2] === agentAddress && args[offset + 3] === sessionId && args[offset + 4] === marker;
34985
35017
  }));
34986
35018
  };
34987
- const installed = owns("Stop", "claude-wake.mjs", HOOK_MARKER) && owns("SessionStart", "claude-wake.mjs", HOOK_MARKER) && owns(PENDING_EVENT, "claude-pending-mail.mjs", PENDING_MARKER);
35019
+ const installed = owns("Stop", "claude-wake.mjs", HOOK_MARKER) && owns("SessionStart", "claude-wake.mjs", HOOK_MARKER) && owns(PENDING_EVENT, "claude-pending-mail.mjs", PENDING_MARKER$1);
34988
35020
  let lastFiredAt = null;
34989
35021
  const fired = readMailJson(join(agentProfileDirectory(configDir, profileName), `pending-mail-${sessionId}.fired.json`));
34990
- if (record$2(fired) && fired.version === 1 && typeof fired.at === "string" && Number.isFinite(Date.parse(fired.at))) lastFiredAt = fired.at;
35022
+ if (record$4(fired) && fired.version === 1 && typeof fired.at === "string" && Number.isFinite(Date.parse(fired.at))) lastFiredAt = fired.at;
34991
35023
  return {
34992
35024
  installed,
34993
35025
  lastFiredAt,
@@ -35007,7 +35039,7 @@ function editClaudeSettings(claudeDir, edit) {
35007
35039
  release = acquireListenLock(claudeDir, "primitive-claude-settings");
35008
35040
  break;
35009
35041
  } catch {
35010
- Atomics.wait(LOCK_WAIT, 0, 0, 25);
35042
+ Atomics.wait(LOCK_WAIT$1, 0, 0, 25);
35011
35043
  }
35012
35044
  if (!release) return false;
35013
35045
  try {
@@ -35020,7 +35052,7 @@ function editClaudeSettings(claudeDir, edit) {
35020
35052
  if (!stat.isFile() || stat.size > 1048576) return false;
35021
35053
  original = readFileSync(settingsPath, "utf8");
35022
35054
  const parsed = JSON.parse(original);
35023
- if (!record$2(parsed)) return false;
35055
+ if (!record$4(parsed)) return false;
35024
35056
  settings = parsed;
35025
35057
  }
35026
35058
  const next = edit(settings);
@@ -35058,7 +35090,7 @@ function installClaudeWakeHook(options) {
35058
35090
  const sessionId = options.sessionId.toLowerCase();
35059
35091
  return editClaudeSettings(resolve((options.env ?? process.env).CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), (settings) => {
35060
35092
  const hooks = settings.hooks ?? {};
35061
- if (!record$2(hooks)) throw new Error("Invalid hook settings");
35093
+ if (!record$4(hooks)) throw new Error("Invalid hook settings");
35062
35094
  for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
35063
35095
  if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
35064
35096
  const hook = {
@@ -35086,26 +35118,26 @@ function installClaudeWakeHook(options) {
35086
35118
  profileName,
35087
35119
  agentAddress,
35088
35120
  sessionId,
35089
- PENDING_MARKER
35121
+ PENDING_MARKER$1
35090
35122
  ],
35091
35123
  timeout: 10
35092
35124
  };
35093
- const isOwnPendingHook = (candidate) => record$2(candidate) && candidate.type === "command" && candidate.command === process.execPath && Array.isArray(candidate.args) && candidate.args.length === 7 && candidate.args[0] === pendingPath && candidate.args[1] === cliPath && candidate.args[2] === configDir && candidate.args[5] === sessionId && candidate.args[6] === PENDING_MARKER;
35125
+ const isOwnPendingHook = (candidate) => record$4(candidate) && candidate.type === "command" && Array.isArray(candidate.args) && candidate.args.length === 7 && typeof candidate.args[0] === "string" && basename(candidate.args[0]) === "claude-pending-mail.mjs" && candidate.args[2] === configDir && candidate.args[5] === sessionId && candidate.args[6] === PENDING_MARKER$1;
35094
35126
  const isOwnHook = (candidate) => {
35095
- if (!record$2(candidate) || !Array.isArray(candidate.args)) return false;
35096
- if (candidate.command !== process.execPath || candidate.args[0] !== wrapperPath || candidate.args[1] !== cliPath || candidate.args[2] !== configDir) return false;
35127
+ if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
35128
+ if (typeof candidate.args[0] !== "string" || basename(candidate.args[0]) !== "claude-wake.mjs" || candidate.args[2] !== configDir) return false;
35097
35129
  if (candidate.args.length === 7 && candidate.args[5] === sessionId && candidate.args[6] === HOOK_MARKER) return true;
35098
35130
  if (candidate.args.length !== 6 || candidate.args[5] !== HOOK_MARKER) return false;
35099
35131
  try {
35100
35132
  const state = readMailJson(join(agentProfileDirectory(configDir, agentProfileName(String(candidate.args[3]))), "setup.json"));
35101
- return record$2(state) && state.session === sessionId;
35133
+ return record$4(state) && state.session === sessionId;
35102
35134
  } catch {
35103
35135
  return false;
35104
35136
  }
35105
35137
  };
35106
35138
  const nextHooks = { ...hooks };
35107
35139
  for (const event of WAKE_EVENTS) nextHooks[event] = [...(hooks[event] ?? []).flatMap((entry) => {
35108
- if (!record$2(entry) || !Array.isArray(entry.hooks)) return [entry];
35140
+ if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
35109
35141
  const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
35110
35142
  if (siblings.length === entry.hooks.length) return [entry];
35111
35143
  return siblings.length ? [{
@@ -35117,7 +35149,7 @@ function installClaudeWakeHook(options) {
35117
35149
  hooks: [hook]
35118
35150
  } : { hooks: [hook] }];
35119
35151
  nextHooks[PENDING_EVENT] = [...(hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
35120
- if (!record$2(entry) || !Array.isArray(entry.hooks)) return [entry];
35152
+ if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
35121
35153
  const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
35122
35154
  if (siblings.length === entry.hooks.length) return [entry];
35123
35155
  return siblings.length ? [{
@@ -35146,31 +35178,31 @@ function uninstallClaudeWakeHook(options) {
35146
35178
  if (!existsSync(join(claudeDir, "settings.json"))) return true;
35147
35179
  return editClaudeSettings(claudeDir, (settings) => {
35148
35180
  const hooks = settings.hooks ?? {};
35149
- if (!record$2(hooks)) throw new Error("Invalid hook settings");
35181
+ if (!record$4(hooks)) throw new Error("Invalid hook settings");
35150
35182
  for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
35151
35183
  if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
35152
35184
  const legacySessionMatches = () => {
35153
35185
  try {
35154
35186
  const setup = readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json"));
35155
- return record$2(setup) && setup.session === sessionId;
35187
+ return record$4(setup) && setup.session === sessionId;
35156
35188
  } catch {
35157
35189
  return false;
35158
35190
  }
35159
35191
  };
35160
35192
  const isOwnHook = (candidate) => {
35161
- if (!record$2(candidate) || !Array.isArray(candidate.args)) return false;
35193
+ if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
35162
35194
  const args = candidate.args;
35163
35195
  return candidate.type === "command" && typeof candidate.command === "string" && candidate.asyncRewake === true && typeof args[0] === "string" && basename(args[0]) === "claude-wake.mjs" && typeof args[1] === "string" && args[2] === configDir && args[3] === profileName && args[4] === agentAddress && (args.length === 7 && args[5] === sessionId && args[6] === HOOK_MARKER || args.length === 6 && args[5] === HOOK_MARKER && legacySessionMatches());
35164
35196
  };
35165
35197
  const isOwnPendingHook = (candidate) => {
35166
- if (!record$2(candidate) || !Array.isArray(candidate.args)) return false;
35198
+ if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
35167
35199
  const args = candidate.args;
35168
- return candidate.type === "command" && typeof candidate.command === "string" && typeof args[0] === "string" && basename(args[0]) === "claude-pending-mail.mjs" && args[2] === configDir && args[3] === profileName && args[4] === agentAddress && args[5] === sessionId && args[6] === PENDING_MARKER && args.length === 7;
35200
+ return candidate.type === "command" && typeof candidate.command === "string" && typeof args[0] === "string" && basename(args[0]) === "claude-pending-mail.mjs" && args[2] === configDir && args[3] === profileName && args[4] === agentAddress && args[5] === sessionId && args[6] === PENDING_MARKER$1 && args.length === 7;
35169
35201
  };
35170
35202
  let removed = false;
35171
35203
  const nextHooks = { ...hooks };
35172
35204
  for (const event of WAKE_EVENTS) nextHooks[event] = (hooks[event] ?? []).flatMap((entry) => {
35173
- if (!record$2(entry) || !Array.isArray(entry.hooks)) return [entry];
35205
+ if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
35174
35206
  const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
35175
35207
  if (siblings.length === entry.hooks.length) return [entry];
35176
35208
  removed = true;
@@ -35180,7 +35212,7 @@ function uninstallClaudeWakeHook(options) {
35180
35212
  }] : [];
35181
35213
  });
35182
35214
  nextHooks[PENDING_EVENT] = (hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
35183
- if (!record$2(entry) || !Array.isArray(entry.hooks)) return [entry];
35215
+ if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
35184
35216
  const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
35185
35217
  if (siblings.length === entry.hooks.length) return [entry];
35186
35218
  removed = true;
@@ -35853,7 +35885,7 @@ function matchesContactSelector(input, sender) {
35853
35885
  }
35854
35886
  //#endregion
35855
35887
  //#region src/oclif/contact-policy.ts
35856
- function record$1(value) {
35888
+ function record$3(value) {
35857
35889
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid contact policy.");
35858
35890
  return value;
35859
35891
  }
@@ -35862,11 +35894,11 @@ function fingerprint(value) {
35862
35894
  return value;
35863
35895
  }
35864
35896
  function document(value, organization) {
35865
- const row = record$1(value);
35897
+ const row = record$3(value);
35866
35898
  if (!Array.isArray(row.rules) || row.rules.length > 100 || !(typeof row.allow_contact_requests === "boolean" || !organization && row.allow_contact_requests === null)) throw new Error("Invalid contact policy document.");
35867
35899
  const seen = /* @__PURE__ */ new Set();
35868
35900
  const rules = row.rules.map((value) => {
35869
- const rule = record$1(value);
35901
+ const rule = record$3(value);
35870
35902
  if (typeof rule.pattern !== "string" || rule.effect !== "allow" && rule.effect !== "silence") throw new Error("Invalid contact policy rule.");
35871
35903
  const pattern = canonicalContactSelector({
35872
35904
  kind: "pattern",
@@ -35907,7 +35939,7 @@ function document(value, organization) {
35907
35939
  };
35908
35940
  }
35909
35941
  function parseAgentContactPolicy(value, recipient) {
35910
- const row = record$1(value);
35942
+ const row = record$3(value);
35911
35943
  if (row.agent_address !== recipient || typeof row.allow_contact_requests !== "boolean") throw new Error("Contact policy recipient mismatch.");
35912
35944
  const org = document(row.org_policy, true);
35913
35945
  const agent = document(row.agent_policy, false);
@@ -36858,28 +36890,7 @@ function defaults(options) {
36858
36890
  env: options.env
36859
36891
  });
36860
36892
  },
36861
- async seedAgentInfo(profileName, value) {
36862
- const profile = loadConnectedAgentProfile(options.configDir, profileName);
36863
- if (!profile) return "failed";
36864
- const client = new PrimitiveApiClient({
36865
- apiKey: profile.api_key,
36866
- apiBaseUrl: profile.api_base_url
36867
- });
36868
- try {
36869
- await runAddressNotesRequest(client.client, {
36870
- action: "set",
36871
- address: profile.agent_address,
36872
- name: AGENT_INFO_NOTE,
36873
- value,
36874
- visibility: "private",
36875
- ifAbsent: true
36876
- });
36877
- return "created";
36878
- } catch (error) {
36879
- if (error instanceof AddressNotesApiError && error.status === 409) return "already_present";
36880
- return "failed";
36881
- }
36882
- },
36893
+ seedAgentInfo: (profileName, value) => seedAgentInfoNote(options.configDir, profileName, value),
36883
36894
  refreshOwnerMemberAddress: (profileName) => refreshOwnerMemberAddress({
36884
36895
  configDir: options.configDir,
36885
36896
  profileName
@@ -36902,6 +36913,30 @@ function defaults(options) {
36902
36913
  }
36903
36914
  };
36904
36915
  }
36916
+ /** Create the private AGENT_INFO note only when the address has none. */
36917
+ async function seedAgentInfoNote(configDir, profileName, value, fetchImpl) {
36918
+ const profile = loadConnectedAgentProfile(configDir, profileName);
36919
+ if (!profile) return "failed";
36920
+ const client = new PrimitiveApiClient({
36921
+ apiKey: profile.api_key,
36922
+ apiBaseUrl: profile.api_base_url,
36923
+ ...fetchImpl ? { fetch: fetchImpl } : {}
36924
+ });
36925
+ try {
36926
+ await runAddressNotesRequest(client.client, {
36927
+ action: "set",
36928
+ address: profile.agent_address,
36929
+ name: AGENT_INFO_NOTE,
36930
+ value,
36931
+ visibility: "private",
36932
+ ifAbsent: true
36933
+ });
36934
+ return "created";
36935
+ } catch (error) {
36936
+ if (error instanceof AddressNotesApiError && error.status === 409) return "already_present";
36937
+ return "failed";
36938
+ }
36939
+ }
36905
36940
  /**
36906
36941
  * One call: self check, install the matching skill, claim and verify, start
36907
36942
  * receiving, seed AGENT_INFO, and report everything as one result. Every step
@@ -38210,6 +38245,853 @@ var AgentNotesDeleteCommand = class AgentNotesDeleteCommand extends Command {
38210
38245
  }
38211
38246
  };
38212
38247
  //#endregion
38248
+ //#region src/oclif/machine-session.ts
38249
+ const MACHINE_RUNTIMES = [
38250
+ "claude",
38251
+ "codex",
38252
+ "omp"
38253
+ ];
38254
+ function sessionRecordPath(configDir, session) {
38255
+ return join(configDir, "machine", "sessions", `${session}.json`);
38256
+ }
38257
+ function readSessionRecord(configDir, session) {
38258
+ try {
38259
+ const value = readMailJson(sessionRecordPath(configDir, session));
38260
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
38261
+ const row = value;
38262
+ if (row.version !== 1 || row.session !== session || typeof row.profile !== "string" || typeof row.name !== "string" || !MACHINE_RUNTIMES.includes(row.runtime) || row.createdBy !== "session-register" && row.createdBy !== "existing") return null;
38263
+ return {
38264
+ version: 1,
38265
+ runtime: row.runtime,
38266
+ session,
38267
+ profile: row.profile,
38268
+ name: row.name,
38269
+ createdBy: row.createdBy,
38270
+ address: typeof row.address === "string" ? row.address : null,
38271
+ agentInfo: row.agentInfo === "created" || row.agentInfo === "already_present" ? row.agentInfo : null,
38272
+ registeredAt: typeof row.registeredAt === "string" ? row.registeredAt : (/* @__PURE__ */ new Date(0)).toISOString(),
38273
+ endedAt: typeof row.endedAt === "string" ? row.endedAt : null,
38274
+ disconnect: row.disconnect === "done" || row.disconnect === "pending" || row.disconnect === "pending_enrollment" ? row.disconnect : typeof row.endedAt === "string" ? "done" : null
38275
+ };
38276
+ } catch {
38277
+ return null;
38278
+ }
38279
+ }
38280
+ function saveSessionRecord(configDir, row) {
38281
+ writeMailJson(sessionRecordPath(configDir, row.session), row);
38282
+ }
38283
+ /** The repository (or directory) a session works in, for its default name. */
38284
+ function workspaceName(cwd) {
38285
+ let current = resolve(cwd);
38286
+ for (let depth = 0; depth < 64; depth++) {
38287
+ if (existsSync(join(current, ".git"))) return basename(current);
38288
+ const parent = dirname(current);
38289
+ if (parent === current) break;
38290
+ current = parent;
38291
+ }
38292
+ return basename(resolve(cwd));
38293
+ }
38294
+ function defaultSessionName(runtime, cwd) {
38295
+ return Array.from(`${runtime}-${workspaceName(cwd)}`).filter((character) => {
38296
+ const code = character.charCodeAt(0);
38297
+ return code >= 32 && code !== 127;
38298
+ }).join("").replace(/\s+/g, " ").trim().slice(0, 80).trim() || runtime;
38299
+ }
38300
+ /** A short private role note. Never transcript content. */
38301
+ function agentInfoForSession(runtime, cwd) {
38302
+ return `${runtime === "claude" ? "Claude Code" : runtime === "codex" ? "Codex" : "omp"} coding session. Repository: ${workspaceName(cwd)}. Directory: ${basename(resolve(cwd))}.`.slice(0, 1e3);
38303
+ }
38304
+ function psProcessInfo(pid) {
38305
+ try {
38306
+ const line = execFileSync("ps", [
38307
+ "-o",
38308
+ "ppid=,lstart=,command=",
38309
+ "-p",
38310
+ String(pid)
38311
+ ], {
38312
+ encoding: "utf8",
38313
+ timeout: 2e3,
38314
+ env: {
38315
+ ...process.env,
38316
+ LC_ALL: "C"
38317
+ },
38318
+ stdio: [
38319
+ "ignore",
38320
+ "pipe",
38321
+ "ignore"
38322
+ ]
38323
+ }).trim();
38324
+ const match = /^(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d\d:\d\d:\d\d\s+\d{4})\s+(.*)$/.exec(line);
38325
+ if (!match) return null;
38326
+ return {
38327
+ ppid: Number(match[1]),
38328
+ started: match[2] ?? "",
38329
+ command: match[3] ?? ""
38330
+ };
38331
+ } catch {
38332
+ return null;
38333
+ }
38334
+ }
38335
+ const OMP_MAIN = /(?:^|[\s/\\])omp(?:\s|$)|pi-coding-agent[\\/]dist[\\/]cli\.js(?:\s|$)/;
38336
+ /**
38337
+ * omp does not expose a session ID to the commands it runs. Use one generated
38338
+ * ID per running omp process, keyed by its PID and start time so a reused PID
38339
+ * never inherits another process's identity. A command reached through one of
38340
+ * omp's shared worker processes gets no ID rather than a guessed one.
38341
+ */
38342
+ function ompProcessSession(configDir, options = {}) {
38343
+ if ((options.platform ?? process.platform) === "win32") return null;
38344
+ const info = options.processInfo ?? psProcessInfo;
38345
+ let pid = options.startPid ?? process.ppid;
38346
+ for (let hop = 0; hop < 16 && pid > 1; hop++) {
38347
+ const row = info(pid);
38348
+ if (!row) return null;
38349
+ if (row.command.includes("__omp_worker")) return null;
38350
+ if (OMP_MAIN.test(row.command)) {
38351
+ const key = createHash("sha256").update(`${pid}\0${row.started}`).digest("hex").slice(0, 32);
38352
+ const directory = join(configDir, "machine", "omp-processes");
38353
+ const path = join(directory, `${key}.json`);
38354
+ privateMailDirectory(directory, true);
38355
+ try {
38356
+ const fd = openSync(path, "wx", 384);
38357
+ try {
38358
+ writeFileSync(fd, `${JSON.stringify({
38359
+ version: 1,
38360
+ session: randomUUID()
38361
+ })}\n`);
38362
+ fsyncSync(fd);
38363
+ } finally {
38364
+ closeSync(fd);
38365
+ }
38366
+ } catch (error) {
38367
+ if (error.code !== "EEXIST") return null;
38368
+ }
38369
+ const saved = readMailJson(path);
38370
+ return typeof saved?.session === "string" && SESSION_UUID$1.test(saved.session) ? saved.session : null;
38371
+ }
38372
+ pid = row.ppid;
38373
+ }
38374
+ return null;
38375
+ }
38376
+ /** The session this runtime says the command is running in. */
38377
+ function runtimeSessionId(runtime, env, configDir, ompSession = ompProcessSession) {
38378
+ return (runtime === "claude" ? env.CLAUDE_CODE_SESSION_ID : runtime === "codex" ? env.CODEX_THREAD_ID?.trim() || env.CODEX_SESSION_ID : ompSession(configDir))?.trim() || null;
38379
+ }
38380
+ /**
38381
+ * Claude sets CLAUDE_CODE_ENTRYPOINT to "cli" for an interactive session and
38382
+ * to "sdk-cli" for `claude -p`; SDK hosts set "sdk-ts" or "sdk-py", and the
38383
+ * GitHub action and MCP server modes have their own values. Those runs are
38384
+ * not sessions a person works in, so they get no address. Any other or
38385
+ * missing value is treated as interactive.
38386
+ */
38387
+ const HEADLESS_CLAUDE_ENTRYPOINTS = new Set([
38388
+ "sdk-cli",
38389
+ "sdk-ts",
38390
+ "sdk-py",
38391
+ "claude-code-github-action",
38392
+ "mcp"
38393
+ ]);
38394
+ function headlessClaudeRun(env) {
38395
+ return HEADLESS_CLAUDE_ENTRYPOINTS.has(env.CLAUDE_CODE_ENTRYPOINT?.trim() ?? "");
38396
+ }
38397
+ const HEADLESS_DETAIL = "This is a non-interactive Claude run (CLAUDE_CODE_ENTRYPOINT), so no address is used for it.";
38398
+ /** Read the connection's own record with its credential. */
38399
+ async function agentConnectionState(profile, fetchImpl = fetch, timeoutMs = 5e3) {
38400
+ try {
38401
+ const response = await fetchImpl(`${profile.api_base_url}/agent-connections/me`, {
38402
+ method: "GET",
38403
+ redirect: "error",
38404
+ signal: AbortSignal.timeout(timeoutMs),
38405
+ headers: {
38406
+ authorization: `Bearer ${profile.api_key}`,
38407
+ accept: "application/json"
38408
+ }
38409
+ });
38410
+ if (response.status === 401 || response.status === 403) {
38411
+ await response.body?.cancel().catch(() => void 0);
38412
+ return "rejected";
38413
+ }
38414
+ if (response.status !== 200) {
38415
+ await response.body?.cancel().catch(() => void 0);
38416
+ return "unavailable";
38417
+ }
38418
+ const text = await response.text();
38419
+ if (text.length > 65536) return "unavailable";
38420
+ const body = JSON.parse(text);
38421
+ const connection = body?.data?.connection;
38422
+ if (body?.success !== true || typeof connection?.address !== "string" || connection.address.toLowerCase() !== profile.agent_address.toLowerCase()) return "unavailable";
38423
+ if (connection.status === "connected") return "connected";
38424
+ if (connection.status === "revoked") return "revoked";
38425
+ if (connection.status === "claimed" || connection.status === "pending") return "pending";
38426
+ return "unavailable";
38427
+ } catch {
38428
+ return "unavailable";
38429
+ }
38430
+ }
38431
+ function receiverMode(setupDirectory) {
38432
+ try {
38433
+ const saved = readMailJson(join(setupDirectory, "setup.json"));
38434
+ if (!saved) return null;
38435
+ return saved.receiverMode === "external" ? "external" : "native";
38436
+ } catch {
38437
+ return null;
38438
+ }
38439
+ }
38440
+ /** Serialize every read-modify-write of one session's record. */
38441
+ async function withRecordLock(configDir, session, action) {
38442
+ const directory = join(configDir, "machine", "sessions");
38443
+ privateMailDirectory(directory, true);
38444
+ let release;
38445
+ for (let attempt = 0; attempt < 200 && !release; attempt++) try {
38446
+ release = acquireListenLock(directory, `session-record-${session}`);
38447
+ } catch {
38448
+ await setTimeout$1(50);
38449
+ }
38450
+ if (!release) throw new ListenStateError("The session record is locked by another run.");
38451
+ try {
38452
+ return await action();
38453
+ } finally {
38454
+ release();
38455
+ }
38456
+ }
38457
+ /** Read, change and save one record under its lock. Returns the saved record. */
38458
+ function updateSessionRecord(configDir, session, change) {
38459
+ return withRecordLock(configDir, session, () => {
38460
+ const current = readSessionRecord(configDir, session);
38461
+ const next = change(current);
38462
+ if (next && next !== current) saveSessionRecord(configDir, next);
38463
+ return next ?? current;
38464
+ });
38465
+ }
38466
+ /** The address an interrupted enrollment saved, when there is one. */
38467
+ function enrollmentAddress(configDir, profileName) {
38468
+ try {
38469
+ const state = readMailJson(join(agentProfileDirectory(configDir, profileName), "enrollment", "state.json"), 65536);
38470
+ if (state === null) return {
38471
+ exists: false,
38472
+ address: null
38473
+ };
38474
+ return {
38475
+ exists: true,
38476
+ address: typeof state.address === "string" ? state.address : null
38477
+ };
38478
+ } catch {
38479
+ return {
38480
+ exists: true,
38481
+ address: null
38482
+ };
38483
+ }
38484
+ }
38485
+ /** `agent disconnect` already confirmed revocation of this profile's credential. */
38486
+ function disconnectConfirmedLocally(configDir, profileName) {
38487
+ try {
38488
+ return readdirSync(agentProfileDirectory(configDir, profileName)).some((name) => /^disconnected-[a-f0-9]{64}\.json$/.test(name));
38489
+ } catch {
38490
+ return false;
38491
+ }
38492
+ }
38493
+ /**
38494
+ * Revoke a connection by address with the saved member login, for a session
38495
+ * whose local credential is gone. True only when Primitive confirms it.
38496
+ */
38497
+ async function revokeWithMemberLogin(configDir, address, fetchImpl = fetch) {
38498
+ try {
38499
+ const saved = loadCliCredentials(configDir);
38500
+ if (!saved) return false;
38501
+ const credentials = await refreshStoredCliCredentials({
38502
+ apiBaseUrl: saved.api_base_url,
38503
+ configDir,
38504
+ credentials: saved,
38505
+ fetch: fetchImpl
38506
+ });
38507
+ const response = await fetchImpl(`${credentials.api_base_url}/agent-connections/${encodeURIComponent(address)}`, {
38508
+ method: "DELETE",
38509
+ redirect: "error",
38510
+ signal: AbortSignal.timeout(15e3),
38511
+ headers: {
38512
+ authorization: `Bearer ${credentials.access_token}`,
38513
+ accept: "application/json"
38514
+ }
38515
+ });
38516
+ if (response.status !== 200) {
38517
+ await response.body?.cancel().catch(() => void 0);
38518
+ return false;
38519
+ }
38520
+ const text = await response.text();
38521
+ if (text.length > 65536) return false;
38522
+ const body = JSON.parse(text);
38523
+ return body?.success === true && body.data?.connection?.address === address && body.data.connection.status === "revoked";
38524
+ } catch {
38525
+ return false;
38526
+ }
38527
+ }
38528
+ /** Enrollment saves its state before any request, so a quiet record this old created nothing. */
38529
+ const ENROLLMENT_SETTLED_MS = 10 * 6e4;
38530
+ async function disconnectSessionAgent(configDir, record, deps, env, fetchImpl, enrollmentSettled = false) {
38531
+ try {
38532
+ if (loadConnectedAgentProfile(configDir, record.profile)) {
38533
+ await deps.disconnect({
38534
+ configDir,
38535
+ profileName: record.profile,
38536
+ fetch: fetchImpl,
38537
+ env
38538
+ });
38539
+ return "done";
38540
+ }
38541
+ if (disconnectConfirmedLocally(configDir, record.profile)) return "done";
38542
+ const enrollment = enrollmentAddress(configDir, record.profile);
38543
+ const address = record.address ?? enrollment.address;
38544
+ if (!address) {
38545
+ if (enrollment.exists) return "pending";
38546
+ return enrollmentSettled || Date.now() - Date.parse(record.registeredAt) > ENROLLMENT_SETTLED_MS ? "done" : "pending_enrollment";
38547
+ }
38548
+ return await deps.revokeByAddress(configDir, address, fetchImpl) ? "done" : "pending";
38549
+ } catch {
38550
+ return "pending";
38551
+ }
38552
+ }
38553
+ async function finishSessionDisconnect(configDir, record, deps, env, fetchImpl, enrollmentSettled = false) {
38554
+ const outcome = await disconnectSessionAgent(configDir, record, deps, env, fetchImpl, enrollmentSettled);
38555
+ return (await updateSessionRecord(configDir, record.session, (current) => !current || current.disconnect === "done" || current.disconnect === outcome ? current : {
38556
+ ...current,
38557
+ disconnect: outcome
38558
+ }))?.disconnect === "done" ? "done" : outcome;
38559
+ }
38560
+ const RECEIVING = {
38561
+ claude: "external_hook",
38562
+ codex: "native",
38563
+ omp: "unsupported"
38564
+ };
38565
+ function withDefined(defaults, overrides) {
38566
+ return {
38567
+ ...defaults,
38568
+ ...Object.fromEntries(Object.entries(overrides ?? {}).filter(([, value]) => value !== void 0))
38569
+ };
38570
+ }
38571
+ const ENDED_DETAIL = "This session ended, so its agent was disconnected. A new address is never created for the same session.";
38572
+ const ENDED_PENDING_DETAIL = "This session ended, but disconnecting its agent has not been confirmed yet; `primitive machine doctor --fix` retries it.";
38573
+ /**
38574
+ * Give this session an address once. Safe to run on every start and resume:
38575
+ * a session already bound to a profile is only re-verified, and a session
38576
+ * whose agent was disconnected or removed is never given a second address.
38577
+ * Never throws; every failure is a status.
38578
+ */
38579
+ async function registerSession(options) {
38580
+ const env = options.env ?? process.env;
38581
+ const deps = withDefined({
38582
+ enroll: enrollAgent,
38583
+ installClaudeHook: installClaudeWakeHook,
38584
+ seedAgentInfo: (configDir, profileName, value) => seedAgentInfoNote(configDir, profileName, value, options.fetch),
38585
+ connectionState: (profile) => agentConnectionState(profile, options.fetch),
38586
+ ompSession: ompProcessSession,
38587
+ now: () => /* @__PURE__ */ new Date(),
38588
+ disconnect: disconnectAgent,
38589
+ revokeByAddress: revokeWithMemberLogin
38590
+ }, options.dependencies);
38591
+ const runtime = options.runtime;
38592
+ const base = {
38593
+ runtime,
38594
+ session: null,
38595
+ profile: null,
38596
+ address: null,
38597
+ receiving: null
38598
+ };
38599
+ if (runtime === "claude" && headlessClaudeRun(env)) return {
38600
+ ...base,
38601
+ status: "skipped_headless",
38602
+ detail: HEADLESS_DETAIL
38603
+ };
38604
+ let session;
38605
+ try {
38606
+ session = options.session?.trim() || runtimeSessionId(runtime, env, options.configDir, deps.ompSession);
38607
+ } catch {
38608
+ session = null;
38609
+ }
38610
+ if (!session) return {
38611
+ ...base,
38612
+ status: "no_session",
38613
+ detail: runtime === "omp" ? "omp does not expose a session ID to commands, and no running omp process was found. Pass --session." : `No ${runtime === "claude" ? "CLAUDE_CODE_SESSION_ID" : "CODEX_THREAD_ID"} in this environment. Pass --session.`
38614
+ };
38615
+ if (!SESSION_UUID$1.test(session)) return {
38616
+ ...base,
38617
+ status: "failed",
38618
+ detail: "The session ID is not a UUID. Nothing was changed."
38619
+ };
38620
+ const sessionId = session.toLowerCase();
38621
+ const profileName = `session-${sessionId}`;
38622
+ const known = {
38623
+ ...base,
38624
+ session: sessionId,
38625
+ profile: profileName
38626
+ };
38627
+ const cwd = resolve(options.cwd ?? process.cwd());
38628
+ const ended = async (current, enrolledNow = false) => {
38629
+ const outcome = current.disconnect === "done" && !enrolledNow ? "done" : await finishSessionDisconnect(options.configDir, current, deps, env, options.fetch, enrolledNow);
38630
+ return {
38631
+ ...known,
38632
+ address: current.address,
38633
+ status: "ended",
38634
+ detail: outcome === "done" ? ENDED_DETAIL : ENDED_PENDING_DETAIL
38635
+ };
38636
+ };
38637
+ const newRecord = (createdBy, address) => ({
38638
+ version: 1,
38639
+ runtime,
38640
+ session: sessionId,
38641
+ profile: profileName,
38642
+ name: defaultSessionName(runtime, cwd),
38643
+ createdBy,
38644
+ address,
38645
+ agentInfo: null,
38646
+ registeredAt: deps.now().toISOString(),
38647
+ endedAt: null,
38648
+ disconnect: null
38649
+ });
38650
+ try {
38651
+ let record = readSessionRecord(options.configDir, sessionId);
38652
+ if (record?.endedAt) return await ended(record);
38653
+ const finishAgentInfo = async () => {
38654
+ if (record?.agentInfo) return;
38655
+ const seeded = await deps.seedAgentInfo(options.configDir, profileName, agentInfoForSession(runtime, cwd));
38656
+ if (seeded !== "created" && seeded !== "already_present") return;
38657
+ record = await updateSessionRecord(options.configDir, sessionId, (current) => current ? {
38658
+ ...current,
38659
+ agentInfo: seeded
38660
+ } : current);
38661
+ };
38662
+ const installHook = (profile) => runtime !== "claude" || receiverMode(agentProfileDirectory(options.configDir, profileName)) !== "external" || deps.installClaudeHook({
38663
+ cliPath: options.cliPath,
38664
+ configDir: options.configDir,
38665
+ profileName,
38666
+ agentAddress: profile.agent_address,
38667
+ sessionId,
38668
+ env
38669
+ }) === "installed_unverified";
38670
+ const existing = loadConnectedAgentProfile(options.configDir, profileName);
38671
+ if (existing) {
38672
+ record = await updateSessionRecord(options.configDir, sessionId, (current) => current ?? newRecord("existing", existing.agent_address));
38673
+ if (!record) throw new Error("session record unavailable");
38674
+ if (record.endedAt) return await ended(record);
38675
+ const state = await deps.connectionState(existing);
38676
+ const withAddress = {
38677
+ ...known,
38678
+ address: existing.agent_address
38679
+ };
38680
+ if (state === "revoked" || state === "rejected") return {
38681
+ ...withAddress,
38682
+ status: "revoked",
38683
+ detail: "This session's agent was disconnected in Primitive. No new address was created. `primitive machine doctor --fix` removes the stale local profile."
38684
+ };
38685
+ if (state === "unavailable") return {
38686
+ ...withAddress,
38687
+ receiving: RECEIVING[runtime],
38688
+ status: "offline",
38689
+ detail: "This session already has an address; its status could not be checked right now."
38690
+ };
38691
+ if (state === "pending" && record.createdBy !== "session-register") return {
38692
+ ...withAddress,
38693
+ status: "pending",
38694
+ detail: "This session's agent is waiting for pairing confirmation. Finish it with the command that started it."
38695
+ };
38696
+ if (state === "connected") {
38697
+ const hooked = installHook(existing);
38698
+ await finishAgentInfo();
38699
+ return {
38700
+ ...withAddress,
38701
+ receiving: RECEIVING[runtime],
38702
+ status: "already_registered",
38703
+ detail: hooked ? "This session is already connected." : "This session is already connected, but its receive hook could not be installed. Run `primitive machine doctor`."
38704
+ };
38705
+ }
38706
+ } else if (record?.address && record.createdBy === "session-register" && !enrollmentAddress(options.configDir, profileName).exists || record?.createdBy === "existing") return {
38707
+ ...known,
38708
+ address: record.address,
38709
+ status: "removed",
38710
+ detail: "This session's local profile was removed. No new address was created for the same session."
38711
+ };
38712
+ let credentials;
38713
+ try {
38714
+ credentials = loadCliCredentials(options.configDir);
38715
+ } catch {
38716
+ credentials = null;
38717
+ }
38718
+ if (!credentials) return {
38719
+ ...known,
38720
+ status: "not_logged_in",
38721
+ detail: "No saved member login on this machine. Run `primitive signin`, then start the session again."
38722
+ };
38723
+ record = await updateSessionRecord(options.configDir, sessionId, (current) => current ?? newRecord("session-register", null));
38724
+ if (!record) throw new Error("session record unavailable");
38725
+ if (record.endedAt) return await ended(record);
38726
+ const enrollEnv = { ...env };
38727
+ delete enrollEnv.PRIMITIVE_AGENT_PROFILE;
38728
+ delete enrollEnv.PRIMITIVE_API_KEY;
38729
+ delete enrollEnv.PRIMITIVE_KEY;
38730
+ if (runtime === "claude" && options.trustedSession) enrollEnv.CLAUDE_CODE_SESSION_ID = sessionId;
38731
+ let result = null;
38732
+ let enrollError = null;
38733
+ try {
38734
+ result = await deps.enroll({
38735
+ configDir: options.configDir,
38736
+ session: sessionId,
38737
+ name: record.name,
38738
+ receiverMode: runtime === "codex" ? "native" : "external",
38739
+ contactRequests: false,
38740
+ env: enrollEnv,
38741
+ fetch: options.fetch,
38742
+ ...runtime === "omp" ? { preflight: async () => void 0 } : {}
38743
+ });
38744
+ } catch (error) {
38745
+ if (error instanceof ListenStateError) throw error;
38746
+ enrollError = error;
38747
+ }
38748
+ let created = result?.identity.agentAddress ?? null;
38749
+ if (!created) try {
38750
+ created = loadConnectedAgentProfile(options.configDir, profileName)?.agent_address ?? enrollmentAddress(options.configDir, profileName).address;
38751
+ } catch {
38752
+ created = enrollmentAddress(options.configDir, profileName).address;
38753
+ }
38754
+ record = await updateSessionRecord(options.configDir, sessionId, (current) => current && created && current.address !== created ? {
38755
+ ...current,
38756
+ address: created
38757
+ } : current);
38758
+ if (!record) throw new Error("session record unavailable");
38759
+ if (record.endedAt) return await ended(record, true);
38760
+ if (enrollError || !result) throw enrollError;
38761
+ const address = result.identity.agentAddress;
38762
+ const submitted = verificationReplySubmitted(result.verification.state);
38763
+ const status = result.connection.status;
38764
+ let hooked = true;
38765
+ if (runtime === "claude" && submitted && status !== "owner_inactive") hooked = deps.installClaudeHook({
38766
+ cliPath: options.cliPath,
38767
+ configDir: options.configDir,
38768
+ profileName,
38769
+ agentAddress: address,
38770
+ sessionId,
38771
+ env
38772
+ }) === "installed_unverified";
38773
+ if (status === "connected") await finishAgentInfo();
38774
+ const withAddress = {
38775
+ ...known,
38776
+ address,
38777
+ receiving: RECEIVING[runtime]
38778
+ };
38779
+ if (status === "connected") return {
38780
+ ...withAddress,
38781
+ status: "registered",
38782
+ detail: hooked ? `Connected as ${address}.` : `Connected as ${address}, but the receive hook could not be installed. Run \`primitive machine doctor\`.`
38783
+ };
38784
+ if (status === "revoked" || status === "owner_inactive") return {
38785
+ ...withAddress,
38786
+ status,
38787
+ detail: status === "revoked" ? "The new agent was disconnected in Primitive before setup finished." : "The member who owns this login is no longer active in the organization."
38788
+ };
38789
+ return {
38790
+ ...withAddress,
38791
+ status: "pending",
38792
+ detail: "The address exists and pairing is not confirmed yet. The next session start resumes it; no second address is created."
38793
+ };
38794
+ } catch (error) {
38795
+ if (error instanceof ListenStateError) return {
38796
+ ...known,
38797
+ status: "busy",
38798
+ detail: "Another registration for this session is already running."
38799
+ };
38800
+ return {
38801
+ ...known,
38802
+ status: "failed",
38803
+ detail: error instanceof AgentConnectionSetupError ? error.message : "Registration stopped before finishing. The session continues normally; the next start resumes it."
38804
+ };
38805
+ }
38806
+ }
38807
+ /**
38808
+ * Disconnect the agent a session got from `session-register`. Agents
38809
+ * connected any other way are left alone. The end is recorded first, under
38810
+ * the record's lock, so a registration still enrolling sees it and
38811
+ * disconnects the agent it creates. Never throws.
38812
+ */
38813
+ async function endSession(options) {
38814
+ const env = options.env ?? process.env;
38815
+ const deps = withDefined({
38816
+ disconnect: disconnectAgent,
38817
+ revokeByAddress: revokeWithMemberLogin,
38818
+ now: () => /* @__PURE__ */ new Date()
38819
+ }, options.dependencies);
38820
+ const runtime = options.runtime;
38821
+ const base = {
38822
+ runtime,
38823
+ session: null,
38824
+ address: null
38825
+ };
38826
+ if (runtime === "claude" && headlessClaudeRun(env)) return {
38827
+ ...base,
38828
+ status: "skipped_headless",
38829
+ detail: HEADLESS_DETAIL
38830
+ };
38831
+ let session;
38832
+ try {
38833
+ session = options.session?.trim() || runtimeSessionId(runtime, env, options.configDir, () => null);
38834
+ } catch {
38835
+ session = null;
38836
+ }
38837
+ if (!session || !SESSION_UUID$1.test(session)) return {
38838
+ ...base,
38839
+ status: "no_session",
38840
+ detail: "No session ID was given or found. Nothing was changed."
38841
+ };
38842
+ const sessionId = session.toLowerCase();
38843
+ try {
38844
+ let managed = false;
38845
+ let alreadyDone = false;
38846
+ const record = await updateSessionRecord(options.configDir, sessionId, (current) => {
38847
+ if (!current || current.createdBy !== "session-register") return current;
38848
+ managed = true;
38849
+ if (current.endedAt && current.disconnect === "done") {
38850
+ alreadyDone = true;
38851
+ return current;
38852
+ }
38853
+ return {
38854
+ ...current,
38855
+ endedAt: current.endedAt ?? deps.now().toISOString(),
38856
+ disconnect: "pending"
38857
+ };
38858
+ });
38859
+ const known = {
38860
+ ...base,
38861
+ session: sessionId,
38862
+ address: record?.address ?? null
38863
+ };
38864
+ if (!record || !managed) return {
38865
+ ...known,
38866
+ status: "not_managed",
38867
+ detail: "This session's agent was not created by `agent session-register`, so it was left connected."
38868
+ };
38869
+ if (alreadyDone) return {
38870
+ ...known,
38871
+ status: "already_ended",
38872
+ detail: "This session's agent was already disconnected."
38873
+ };
38874
+ return await finishSessionDisconnect(options.configDir, record, deps, env, options.fetch) === "done" ? {
38875
+ ...known,
38876
+ status: "disconnected",
38877
+ detail: "This session's agent was disconnected."
38878
+ } : {
38879
+ ...known,
38880
+ status: "disconnect_pending",
38881
+ detail: "The session is marked ended, but disconnecting its agent is not confirmed yet. The next `primitive machine doctor --fix` (or a registration still finishing) retries it."
38882
+ };
38883
+ } catch (error) {
38884
+ return {
38885
+ ...base,
38886
+ session: sessionId,
38887
+ status: "failed",
38888
+ detail: error instanceof AgentDisconnectError ? error.message : "Disconnect did not finish. The agent was left as it was."
38889
+ };
38890
+ }
38891
+ }
38892
+ /** Ended sessions whose agent disconnect still needs confirming. */
38893
+ function pendingSessionDisconnects(configDir) {
38894
+ const directory = join(configDir, "machine", "sessions");
38895
+ let names;
38896
+ try {
38897
+ names = readdirSync(directory);
38898
+ } catch {
38899
+ return [];
38900
+ }
38901
+ return names.flatMap((name) => {
38902
+ const match = /^([0-9a-f-]{36})\.json$/.exec(name);
38903
+ if (!match?.[1]) return [];
38904
+ const record = readSessionRecord(configDir, match[1]);
38905
+ return record?.endedAt && (record.disconnect === "pending" || record.disconnect === "pending_enrollment") ? [record] : [];
38906
+ });
38907
+ }
38908
+ /** Retry every pending disconnect. Returns how many are now confirmed. */
38909
+ async function retryPendingDisconnects(configDir, options = {}) {
38910
+ const deps = withDefined({
38911
+ disconnect: disconnectAgent,
38912
+ revokeByAddress: revokeWithMemberLogin
38913
+ }, options.dependencies);
38914
+ let done = 0;
38915
+ for (const record of pendingSessionDisconnects(configDir)) if (await finishSessionDisconnect(configDir, record, deps, options.env ?? process.env, options.fetch) === "done") done++;
38916
+ return done;
38917
+ }
38918
+ /** Read Claude's hook JSON from stdin within a short budget. */
38919
+ async function readClaudeHookInput(stdin, timeoutMs = 2e3) {
38920
+ let input = "";
38921
+ const read = (async () => {
38922
+ for await (const chunk of stdin) {
38923
+ input += String(chunk);
38924
+ if (input.length > 65536) return false;
38925
+ }
38926
+ return true;
38927
+ })().catch(() => false);
38928
+ let timer;
38929
+ const complete = await Promise.race([read, new Promise((done) => {
38930
+ timer = setTimeout(() => done(false), timeoutMs);
38931
+ timer.unref();
38932
+ })]);
38933
+ if (timer) clearTimeout(timer);
38934
+ if (!complete) return null;
38935
+ try {
38936
+ const value = JSON.parse(input);
38937
+ if (typeof value?.session_id !== "string" || !SESSION_UUID$1.test(value.session_id)) return null;
38938
+ return {
38939
+ sessionId: value.session_id.toLowerCase(),
38940
+ cwd: typeof value.cwd === "string" && value.cwd ? value.cwd : null
38941
+ };
38942
+ } catch {
38943
+ return null;
38944
+ }
38945
+ }
38946
+ /**
38947
+ * Run the real work in a detached process so a hook never holds up the
38948
+ * session; wait for it only up to the budget.
38949
+ */
38950
+ async function runDetached(params) {
38951
+ try {
38952
+ const child = spawn(params.node, [params.entry, ...params.args], {
38953
+ detached: true,
38954
+ stdio: "ignore",
38955
+ env: params.env,
38956
+ ...params.cwd && existsSync(params.cwd) ? { cwd: params.cwd } : {}
38957
+ });
38958
+ child.unref();
38959
+ return await new Promise((done) => {
38960
+ const timer = setTimeout(() => done("detached"), params.waitMs);
38961
+ child.once("error", () => {
38962
+ clearTimeout(timer);
38963
+ done("failed");
38964
+ });
38965
+ child.once("exit", () => {
38966
+ clearTimeout(timer);
38967
+ done("finished");
38968
+ });
38969
+ });
38970
+ } catch {
38971
+ return "failed";
38972
+ }
38973
+ }
38974
+ //#endregion
38975
+ //#region src/oclif/commands/agent-session-end.ts
38976
+ var AgentSessionEndCommand = class AgentSessionEndCommand extends Command {
38977
+ static summary = "Disconnect the agent a session registered when it ends";
38978
+ static description = "Disconnects the agent that `primitive agent session-register` created for this session, and removes its exact-session receive hooks. Agents connected any other way are left connected. Never blocks or fails a session: every outcome exits 0 with a status. --hook reads Claude's SessionEnd hook input from stdin and disconnects in the background.";
38979
+ static examples = ["<%= config.bin %> agent session-end --runtime claude --json", "<%= config.bin %> agent session-end --runtime codex --session 11111111-1111-4111-8111-111111111111"];
38980
+ static flags = {
38981
+ runtime: Flags.string({
38982
+ required: true,
38983
+ options: [...MACHINE_RUNTIMES],
38984
+ description: "The coding agent runtime this session runs in"
38985
+ }),
38986
+ session: Flags.string({ description: "Exact session UUID; defaults to the runtime's own session ID" }),
38987
+ hook: Flags.boolean({ description: "Read Claude hook JSON from stdin and disconnect in the background" }),
38988
+ json: Flags.boolean({ description: "Print the result as JSON" }),
38989
+ quiet: Flags.boolean({ description: "Print nothing" })
38990
+ };
38991
+ async run() {
38992
+ const { flags } = await this.parse(AgentSessionEndCommand);
38993
+ const runtime = flags.runtime;
38994
+ if (flags.hook) {
38995
+ const input = await readClaudeHookInput(process.stdin);
38996
+ if (!input || runtime === "claude" && headlessClaudeRun(process.env)) return;
38997
+ await runDetached({
38998
+ node: process.execPath,
38999
+ entry: process.argv[1] ?? "",
39000
+ args: [
39001
+ "agent",
39002
+ "session-end",
39003
+ "--runtime",
39004
+ runtime,
39005
+ "--session",
39006
+ input.sessionId,
39007
+ "--quiet"
39008
+ ],
39009
+ env: process.env,
39010
+ waitMs: 0
39011
+ });
39012
+ return;
39013
+ }
39014
+ const result = await endSession({
39015
+ configDir: this.config.configDir,
39016
+ runtime,
39017
+ session: flags.session
39018
+ });
39019
+ const { status, ...rest } = result;
39020
+ if (flags.json) this.log(JSON.stringify({
39021
+ status,
39022
+ ...rest
39023
+ }));
39024
+ else if (!flags.quiet) this.log(`${result.status}: ${result.detail}`);
39025
+ }
39026
+ };
39027
+ //#endregion
39028
+ //#region src/oclif/commands/agent-session-register.ts
39029
+ /** A hook waits this long for registration before letting the session start. */
39030
+ const HOOK_BUDGET_MS = 3e3;
39031
+ var AgentSessionRegisterCommand = class AgentSessionRegisterCommand extends Command {
39032
+ static summary = "Register this coding session with Primitive, once";
39033
+ static description = "Gives the current Claude Code, Codex or omp session an address in your organization using the saved member login, without a browser. Safe to run on every start and resume: a session that already has a profile is only re-verified, and a session whose agent was disconnected or removed never gets a second address. The session ID comes from CLAUDE_CODE_SESSION_ID (Claude), CODEX_THREAD_ID or CODEX_SESSION_ID (Codex), or --session. omp does not expose a session ID to commands, so omp sessions use one generated ID per running omp process, and receiving mail is not supported for them. The agent is named <runtime>-<repository> and gets a private AGENT_INFO note naming the runtime and repository when it has none. Claude sessions receive mail through exact-session hooks; Codex through the native receiver. Non-interactive Claude runs (claude -p and SDK hosts, by CLAUDE_CODE_ENTRYPOINT) are skipped with status skipped_headless. Never blocks a session: every outcome, including no login or no network, exits 0 with a status. --hook reads Claude's or Codex's SessionStart hook input from stdin, returns within a few seconds and finishes slower work in the background.";
39034
+ static examples = [
39035
+ "<%= config.bin %> agent session-register --runtime codex --quiet",
39036
+ "<%= config.bin %> agent session-register --runtime claude --json",
39037
+ "<%= config.bin %> agent session-register --runtime omp --session 11111111-1111-4111-8111-111111111111 --json"
39038
+ ];
39039
+ static flags = {
39040
+ runtime: Flags.string({
39041
+ required: true,
39042
+ options: [...MACHINE_RUNTIMES],
39043
+ description: "The coding agent runtime this session runs in"
39044
+ }),
39045
+ session: Flags.string({ description: "Exact session UUID; defaults to the runtime's own session ID" }),
39046
+ cwd: Flags.string({ description: "Working directory used for the agent's name and AGENT_INFO; defaults to the current directory" }),
39047
+ hook: Flags.boolean({ description: "Read Claude or Codex SessionStart hook JSON from stdin and finish in the background if registration is slow" }),
39048
+ json: Flags.boolean({ description: "Print the result as JSON" }),
39049
+ quiet: Flags.boolean({ description: "Print nothing" })
39050
+ };
39051
+ async run() {
39052
+ const { flags } = await this.parse(AgentSessionRegisterCommand);
39053
+ const runtime = flags.runtime;
39054
+ if (flags.hook) {
39055
+ const input = await readClaudeHookInput(process.stdin);
39056
+ if (!input || runtime !== "claude" && runtime !== "codex" || runtime === "claude" && headlessClaudeRun(process.env)) return;
39057
+ await runDetached({
39058
+ node: process.execPath,
39059
+ entry: process.argv[1] ?? "",
39060
+ args: [
39061
+ "agent",
39062
+ "session-register",
39063
+ "--runtime",
39064
+ runtime,
39065
+ "--session",
39066
+ input.sessionId,
39067
+ ...input.cwd ? ["--cwd", input.cwd] : [],
39068
+ "--quiet"
39069
+ ],
39070
+ env: {
39071
+ ...process.env,
39072
+ ...runtime === "claude" ? { CLAUDE_CODE_SESSION_ID: input.sessionId } : { CODEX_THREAD_ID: input.sessionId }
39073
+ },
39074
+ cwd: input.cwd ?? void 0,
39075
+ waitMs: HOOK_BUDGET_MS
39076
+ });
39077
+ return;
39078
+ }
39079
+ const result = await registerSession({
39080
+ configDir: this.config.configDir,
39081
+ runtime,
39082
+ session: flags.session,
39083
+ cwd: flags.cwd,
39084
+ cliPath: process.argv[1] ?? ""
39085
+ });
39086
+ const { status, ...rest } = result;
39087
+ if (flags.json) this.log(JSON.stringify({
39088
+ status,
39089
+ ...rest
39090
+ }));
39091
+ else if (!flags.quiet) this.log(`${result.status}: ${result.detail}`);
39092
+ }
39093
+ };
39094
+ //#endregion
38213
39095
  //#region src/oclif/commands/agent-upgrade.ts
38214
39096
  /**
38215
39097
  * Interactive upgrade of an emailless agent account to a full developer
@@ -45000,7 +45882,7 @@ const PRIMITIVE_TEAM_AUTHOR = {
45000
45882
  url: "https://primitive.dev"
45001
45883
  };
45002
45884
  const SDK_VERSION_RANGE = "^1.34.0";
45003
- const CLI_VERSION_RANGE = "^1.37.0";
45885
+ const CLI_VERSION_RANGE = "^1.38.1";
45004
45886
  const ESBUILD_VERSION_RANGE = "^0.27.0";
45005
45887
  function renderHandler() {
45006
45888
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -50231,6 +51113,52 @@ function retryAfterSeconds$1(result) {
50231
51113
  const parsed = Number.parseInt(raw, 10);
50232
51114
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
50233
51115
  }
51116
+ const SIGN_IN_EMAIL_CHANGE_REQUIRED = "sign_in_email_change_required";
51117
+ const SIGN_IN_EMAIL_CHANGE_REQUIRED_FALLBACK = "Your Primitive sign-in email is on a domain Primitive receives mail for. Change it to an external address in the browser, then log in again.";
51118
+ function errorMessage$1(payload) {
51119
+ if (!payload || typeof payload !== "object") return null;
51120
+ const inner = payload.error;
51121
+ const message = (inner && typeof inner === "object" ? inner : payload).message;
51122
+ return typeof message === "string" && message.trim() !== "" ? message.trim() : null;
51123
+ }
51124
+ async function pollCliLoginUntilApproved(params) {
51125
+ const wait = params.sleep ?? sleep;
51126
+ const { retryCommand, start } = params;
51127
+ const deadline = Date.now() + start.expires_in * 1e3;
51128
+ let interval = Math.min(Math.max(1, start.interval), MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
51129
+ let nextPollDelay = 1;
51130
+ while (Date.now() < deadline) {
51131
+ await wait(nextPollDelay * 1e3);
51132
+ nextPollDelay = interval;
51133
+ const polled = await params.poll(start.device_code);
51134
+ if (polled.data) {
51135
+ const login = unwrapData$2(polled.data);
51136
+ if (!login) throw cliError$3("Primitive API returned an empty CLI poll response.");
51137
+ return login;
51138
+ }
51139
+ const payload = extractErrorPayload(polled.error);
51140
+ const code = extractErrorCode(payload);
51141
+ if (code === API_ERROR_CODES.authorizationPending) {
51142
+ nextPollDelay = interval;
51143
+ continue;
51144
+ }
51145
+ if (code === API_ERROR_CODES.slowDown) {
51146
+ interval = Math.min(retryAfterSeconds$1(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
51147
+ nextPollDelay = interval;
51148
+ continue;
51149
+ }
51150
+ if (code === "sign_in_email_change_required") throw new Errors.CLIError(errorMessage$1(payload) ?? SIGN_IN_EMAIL_CHANGE_REQUIRED_FALLBACK, {
51151
+ code: SIGN_IN_EMAIL_CHANGE_REQUIRED,
51152
+ exit: 1
51153
+ });
51154
+ if (code === API_ERROR_CODES.accessDenied) throw cliError$3("Primitive CLI login was denied in the browser.");
51155
+ if (code === API_ERROR_CODES.expiredToken) throw cliError$3(`Primitive CLI login expired. Run \`primitive ${retryCommand}\` again.`);
51156
+ if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$3(`Primitive CLI login device code is invalid. Run \`primitive ${retryCommand}\` again.`);
51157
+ writeErrorWithHints(payload);
51158
+ throw cliError$3("Primitive CLI login failed while polling for approval.");
51159
+ }
51160
+ throw cliError$3(`Primitive CLI login expired. Run \`primitive ${retryCommand}\` again.`);
51161
+ }
50234
51162
  async function checkExistingLogin(params) {
50235
51163
  const requestConfig = resolveCliApiRequestConfig({
50236
51164
  apiBaseUrl: params.apiBaseUrl,
@@ -50367,57 +51295,32 @@ var LoginCommand$1 = class extends Command {
50367
51295
  }
50368
51296
  process.stderr.write(`If the browser did not open, visit: ${start.verification_uri_complete}\n`);
50369
51297
  process.stderr.write("Waiting for browser approval...\n");
50370
- const deadline = Date.now() + start.expires_in * 1e3;
50371
- let interval = Math.min(Math.max(1, start.interval), MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
50372
- let nextPollDelay = 1;
50373
- while (Date.now() < deadline) {
50374
- await sleep(nextPollDelay * 1e3);
50375
- nextPollDelay = interval;
50376
- const polled = await pollCliLogin({
50377
- body: { device_code: start.device_code },
51298
+ const login = await pollCliLoginUntilApproved({
51299
+ poll: (deviceCode) => pollCliLogin({
51300
+ body: { device_code: deviceCode },
50378
51301
  client: apiClient.client,
50379
51302
  responseStyle: "fields"
50380
- });
50381
- if (polled.data) {
50382
- const login = unwrapData$2(polled.data);
50383
- if (!login) throw cliError$3("Primitive API returned an empty CLI poll response.");
50384
- deleteChatState(this.config.configDir);
50385
- saveCliCredentials(this.config.configDir, {
50386
- access_token: login.access_token,
50387
- api_base_url: apiBaseUrl,
50388
- auth_method: "oauth",
50389
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
50390
- expires_at: cliAccessTokenExpiresAt(login.expires_in),
50391
- oauth_client_id: login.oauth_client_id,
50392
- oauth_grant_id: login.oauth_grant_id,
50393
- org_id: login.org_id,
50394
- org_name: login.org_name,
50395
- refresh_token: login.refresh_token,
50396
- token_type: login.token_type
50397
- });
50398
- const org = login.org_name ? ` (${login.org_name})` : "";
50399
- process.stderr.write(`Logged in to org ${login.org_id}${org}.\n`);
50400
- process.stderr.write(`Saved credentials to ${credentialsPath(this.config.configDir)}.\n`);
50401
- return;
50402
- }
50403
- const payload = extractErrorPayload(polled.error);
50404
- const code = extractErrorCode(payload);
50405
- if (code === API_ERROR_CODES.authorizationPending) {
50406
- nextPollDelay = interval;
50407
- continue;
50408
- }
50409
- if (code === API_ERROR_CODES.slowDown) {
50410
- interval = Math.min(retryAfterSeconds$1(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
50411
- nextPollDelay = interval;
50412
- continue;
50413
- }
50414
- if (code === API_ERROR_CODES.accessDenied) throw cliError$3("Primitive CLI login was denied in the browser.");
50415
- if (code === API_ERROR_CODES.expiredToken) throw cliError$3(`Primitive CLI login expired. Run \`primitive ${retryCommand}\` again.`);
50416
- if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$3(`Primitive CLI login device code is invalid. Run \`primitive ${retryCommand}\` again.`);
50417
- writeErrorWithHints(payload);
50418
- throw cliError$3("Primitive CLI login failed while polling for approval.");
50419
- }
50420
- throw cliError$3(`Primitive CLI login expired. Run \`primitive ${retryCommand}\` again.`);
51303
+ }),
51304
+ retryCommand,
51305
+ start
51306
+ });
51307
+ deleteChatState(this.config.configDir);
51308
+ saveCliCredentials(this.config.configDir, {
51309
+ access_token: login.access_token,
51310
+ api_base_url: apiBaseUrl,
51311
+ auth_method: "oauth",
51312
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
51313
+ expires_at: cliAccessTokenExpiresAt(login.expires_in),
51314
+ oauth_client_id: login.oauth_client_id,
51315
+ oauth_grant_id: login.oauth_grant_id,
51316
+ org_id: login.org_id,
51317
+ org_name: login.org_name,
51318
+ refresh_token: login.refresh_token,
51319
+ token_type: login.token_type
51320
+ });
51321
+ const org = login.org_name ? ` (${login.org_name})` : "";
51322
+ process.stderr.write(`Logged in to org ${login.org_id}${org}.\n`);
51323
+ process.stderr.write(`Saved credentials to ${credentialsPath(this.config.configDir)}.\n`);
50421
51324
  }
50422
51325
  };
50423
51326
  //#endregion
@@ -51336,6 +52239,1627 @@ var LogoutCommand = class LogoutCommand extends Command {
51336
52239
  }
51337
52240
  };
51338
52241
  //#endregion
52242
+ //#region src/oclif/machine-files.ts
52243
+ /** Hand-edited files larger than this are reported, never rewritten. */
52244
+ const MAX_MANAGED_FILE_BYTES = 1048576;
52245
+ /** Backups kept per file; older Primitive backups of the same file are pruned. */
52246
+ const BACKUPS_KEPT = 10;
52247
+ const BACKUP_INFIX = ".primitive-bak-";
52248
+ /** A safe, user-facing reason a file was not changed. */
52249
+ var MachineFileError = class extends Error {};
52250
+ /**
52251
+ * Read a user-owned text file without following it anywhere surprising. A
52252
+ * symlink is followed to its real file so a later write updates the file the
52253
+ * user linked instead of replacing the link.
52254
+ */
52255
+ function readManagedFile(path) {
52256
+ let info;
52257
+ try {
52258
+ info = lstatSync(path);
52259
+ } catch (error) {
52260
+ if (error.code === "ENOENT") return {
52261
+ state: "absent",
52262
+ path,
52263
+ target: path
52264
+ };
52265
+ return {
52266
+ state: "invalid",
52267
+ path,
52268
+ detail: `${path} cannot be read (${error.code ?? "error"}).`
52269
+ };
52270
+ }
52271
+ let target = path;
52272
+ if (info.isSymbolicLink()) try {
52273
+ target = realpathSync(path);
52274
+ info = statSync(target);
52275
+ } catch {
52276
+ return {
52277
+ state: "invalid",
52278
+ path,
52279
+ detail: `${path} is a symlink to a missing file; it was left unchanged.`
52280
+ };
52281
+ }
52282
+ if (!info.isFile()) return {
52283
+ state: "invalid",
52284
+ path,
52285
+ detail: `${path} is not a regular file; it was left unchanged.`
52286
+ };
52287
+ if (info.size > MAX_MANAGED_FILE_BYTES) return {
52288
+ state: "invalid",
52289
+ path,
52290
+ detail: `${path} is larger than 1 MiB; it was left unchanged.`
52291
+ };
52292
+ let raw;
52293
+ try {
52294
+ raw = readFileSync(target);
52295
+ } catch (error) {
52296
+ return {
52297
+ state: "invalid",
52298
+ path,
52299
+ detail: `${path} cannot be read (${error.code ?? "error"}).`
52300
+ };
52301
+ }
52302
+ const text = raw.toString("utf8");
52303
+ if (text.includes("\0") || !Buffer.from(text, "utf8").equals(raw)) return {
52304
+ state: "invalid",
52305
+ path,
52306
+ detail: `${path} is not UTF-8 text; it was left unchanged.`
52307
+ };
52308
+ return {
52309
+ state: "present",
52310
+ path,
52311
+ target,
52312
+ text,
52313
+ mode: info.mode & 511
52314
+ };
52315
+ }
52316
+ function backupStamp$1(now) {
52317
+ return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
52318
+ }
52319
+ function syncDirectory(directory) {
52320
+ if (process.platform === "win32") return;
52321
+ try {
52322
+ const fd = openSync(directory, "r");
52323
+ try {
52324
+ fsyncSync(fd);
52325
+ } finally {
52326
+ closeSync(fd);
52327
+ }
52328
+ } catch {}
52329
+ }
52330
+ function pruneBackups(target) {
52331
+ const directory = dirname(target);
52332
+ const prefix = `${basename(target)}${BACKUP_INFIX}`;
52333
+ try {
52334
+ const backups = readdirSync(directory).filter((name) => name.startsWith(prefix)).sort();
52335
+ for (const name of backups.slice(0, Math.max(0, backups.length - BACKUPS_KEPT))) unlinkSync(join(directory, name));
52336
+ } catch {}
52337
+ }
52338
+ /** Copy the current file beside itself before any change. Returns the copy's path. */
52339
+ function backupManagedFile(target, mode, now) {
52340
+ const base = `${target}${BACKUP_INFIX}${backupStamp$1(now)}`;
52341
+ for (let attempt = 0; attempt < 100; attempt++) {
52342
+ const candidate = attempt === 0 ? base : `${base}-${attempt}`;
52343
+ try {
52344
+ copyFileSync(target, candidate, constants.COPYFILE_EXCL);
52345
+ chmodSync(candidate, mode);
52346
+ return candidate;
52347
+ } catch (error) {
52348
+ if (error.code !== "EEXIST") throw error;
52349
+ }
52350
+ }
52351
+ throw new MachineFileError(`Could not create a backup of ${target}.`);
52352
+ }
52353
+ /**
52354
+ * Replace a file atomically (temporary file plus rename in the same
52355
+ * directory), after backing up the existing content. The write is refused when
52356
+ * the file changed since it was read, so a concurrent edit is never lost.
52357
+ */
52358
+ function writeManagedFile(params) {
52359
+ const { read } = params;
52360
+ const target = read.target;
52361
+ const expected = read.state === "present" ? read.text : null;
52362
+ const directory = dirname(target);
52363
+ mkdirSync(directory, {
52364
+ recursive: true,
52365
+ mode: 448
52366
+ });
52367
+ const current = () => {
52368
+ try {
52369
+ return readFileSync(target, "utf8");
52370
+ } catch (error) {
52371
+ if (error.code === "ENOENT") return null;
52372
+ throw error;
52373
+ }
52374
+ };
52375
+ if (current() !== expected) throw new MachineFileError(`${read.path} changed while it was being repaired; it was left unchanged.`);
52376
+ const mode = read.state === "present" ? read.mode : params.newFileMode ?? 420;
52377
+ const backup = read.state === "present" ? backupManagedFile(target, mode, (params.now ?? (() => /* @__PURE__ */ new Date()))()) : null;
52378
+ const temporary = join(directory, `.${basename(target)}.${randomUUID()}.tmp`);
52379
+ try {
52380
+ const fd = openSync(temporary, "wx", mode);
52381
+ try {
52382
+ writeFileSync(fd, params.content, "utf8");
52383
+ fsyncSync(fd);
52384
+ } finally {
52385
+ closeSync(fd);
52386
+ }
52387
+ chmodSync(temporary, mode);
52388
+ if (current() !== expected) throw new MachineFileError(`${read.path} changed while it was being repaired; it was left unchanged.`);
52389
+ renameSync(temporary, target);
52390
+ syncDirectory(directory);
52391
+ } finally {
52392
+ if (existsSync(temporary)) unlinkSync(temporary);
52393
+ }
52394
+ if (backup) pruneBackups(target);
52395
+ return { backup };
52396
+ }
52397
+ const START = /^<!-- primitive:managed-block v=(\d+) START -->\r?$/;
52398
+ const END = /^<!-- primitive:managed-block END -->\r?$/;
52399
+ /** Character offsets of the single managed block, or why the markers are unusable. */
52400
+ function locateBlock(text) {
52401
+ const starts = [];
52402
+ const ends = [];
52403
+ let offset = 0;
52404
+ for (const line of text.split("\n")) {
52405
+ const start = START.exec(line);
52406
+ if (start) starts.push({
52407
+ offset,
52408
+ version: Number(start[1])
52409
+ });
52410
+ else if (END.test(line)) ends.push({
52411
+ offset,
52412
+ length: line.endsWith("\r") ? line.length - 1 : line.length
52413
+ });
52414
+ else if (line.includes("primitive:managed-block")) return { malformed: "a primitive:managed-block marker line was edited; fix or remove it by hand" };
52415
+ offset += line.length + 1;
52416
+ }
52417
+ if (starts.length === 0 && ends.length === 0) return null;
52418
+ if (starts.length !== 1 || ends.length !== 1) return { malformed: `found ${starts.length} START and ${ends.length} END markers; expected one of each` };
52419
+ const [start] = starts;
52420
+ const [end] = ends;
52421
+ if (!start || !end || end.offset < start.offset) return { malformed: "the END marker comes before the START marker" };
52422
+ return {
52423
+ start: start.offset,
52424
+ end: end.offset + end.length,
52425
+ version: start.version
52426
+ };
52427
+ }
52428
+ /** The exact block for a body, using the file's own line ending. */
52429
+ function renderManagedBlock(body, eol = "\n") {
52430
+ return [
52431
+ `<!-- primitive:managed-block v=1 START -->`,
52432
+ ...body.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n"),
52433
+ "<!-- primitive:managed-block END -->"
52434
+ ].join(eol);
52435
+ }
52436
+ function lineEnding(text) {
52437
+ return text.includes("\r\n") ? "\r\n" : "\n";
52438
+ }
52439
+ function inspectManagedBlock(text, body) {
52440
+ const found = locateBlock(text);
52441
+ if (found === null) return { state: "missing" };
52442
+ if ("malformed" in found) return {
52443
+ state: "malformed",
52444
+ detail: found.malformed
52445
+ };
52446
+ return text.slice(found.start, found.end).replace(/\r\n/g, "\n") === renderManagedBlock(body) ? { state: "ok" } : {
52447
+ state: "outdated",
52448
+ version: found.version
52449
+ };
52450
+ }
52451
+ /**
52452
+ * Replace only the managed block, or append it when absent. Text outside the
52453
+ * markers is preserved byte for byte. Throws for malformed markers.
52454
+ */
52455
+ function upsertManagedBlock(text, body) {
52456
+ const found = locateBlock(text);
52457
+ if (found !== null && "malformed" in found) throw new MachineFileError(found.malformed);
52458
+ const eol = lineEnding(text);
52459
+ const block = renderManagedBlock(body, eol);
52460
+ if (found) return `${text.slice(0, found.start)}${block}${text.slice(found.end)}`;
52461
+ if (text.length === 0) return `${block}${eol}`;
52462
+ return `${text}${text.endsWith(`${eol}${eol}`) ? "" : text.endsWith(eol) ? eol : `${eol}${eol}`}${block}${eol}`;
52463
+ }
52464
+ /** Indentation the file already uses, so a rewrite keeps its style. */
52465
+ function jsonIndentation(text) {
52466
+ return /^[{[][^\n]*\n([ \t]+)\S/.exec(text)?.[1] ?? 2;
52467
+ }
52468
+ //#endregion
52469
+ //#region src/oclif/claude-machine-hooks.ts
52470
+ function record$2(value) {
52471
+ return value !== null && typeof value === "object" && !Array.isArray(value);
52472
+ }
52473
+ function realpathOrNull(path) {
52474
+ try {
52475
+ return realpathSync(path);
52476
+ } catch {
52477
+ return null;
52478
+ }
52479
+ }
52480
+ /** Two paths name the same existing file. */
52481
+ function samePath(a, b) {
52482
+ if (typeof a !== "string" || !a) return false;
52483
+ if (a === b) return existsSync(a);
52484
+ const left = realpathOrNull(a);
52485
+ return left !== null && left === realpathOrNull(b);
52486
+ }
52487
+ function executable(path) {
52488
+ try {
52489
+ accessSync(path, constants.X_OK);
52490
+ return true;
52491
+ } catch {
52492
+ return false;
52493
+ }
52494
+ }
52495
+ /** The first PATH entry for a command, without running a shell. */
52496
+ function findOnPath(name, env) {
52497
+ for (const directory of (env.PATH ?? "").split(delimiter)) {
52498
+ if (!directory) continue;
52499
+ const candidate = join(directory, name);
52500
+ if (existsSync(candidate) && executable(candidate)) return candidate;
52501
+ }
52502
+ return null;
52503
+ }
52504
+ /**
52505
+ * Prefer a PATH link (for example a package manager's `bin/node`) over a
52506
+ * versioned install directory when both are the same binary, so a hook keeps
52507
+ * working after the versioned directory is replaced by an upgrade.
52508
+ */
52509
+ function stableNodePath(env, execPath = process.execPath) {
52510
+ const onPath = findOnPath("node", env);
52511
+ if (onPath && onPath !== execPath && samePath(onPath, execPath)) return onPath;
52512
+ return execPath;
52513
+ }
52514
+ /** Locate the running CLI. Null when its files are not where a package install puts them. */
52515
+ function currentCliLocation(entryArg, env, execPath = process.execPath) {
52516
+ const entry = realpathOrNull(entryArg);
52517
+ if (!entry) return null;
52518
+ const wake = realpathOrNull(join(dirname(entry), "claude-wake.mjs"));
52519
+ const pending = realpathOrNull(join(dirname(entry), "claude-pending-mail.mjs"));
52520
+ if (!wake || !pending) return null;
52521
+ return {
52522
+ node: stableNodePath(env, execPath),
52523
+ entry,
52524
+ wake,
52525
+ pending
52526
+ };
52527
+ }
52528
+ /** Package-runner caches are cleaned up behind the user's back. */
52529
+ function ephemeralCliReason(entry) {
52530
+ if (/[\\/]_npx[\\/]/.test(entry)) return "an npx cache";
52531
+ if (/[\\/]dlx[-\\/]/.test(entry)) return "a pnpm dlx cache";
52532
+ if (/[\\/]bunx-/.test(entry)) return "a bunx cache";
52533
+ return null;
52534
+ }
52535
+ function claudeConfigDir(env, home) {
52536
+ return resolve(env.CLAUDE_CONFIG_DIR || join(home, ".claude"));
52537
+ }
52538
+ function jsonErrorDetail(error) {
52539
+ const message = error instanceof Error ? error.message : "";
52540
+ const position = /position (\d+)/.exec(message)?.[1];
52541
+ const line = /line (\d+)/.exec(message)?.[1];
52542
+ return line ? ` near line ${line}` : position ? ` near character ${position}` : "";
52543
+ }
52544
+ /** Parse settings.json. Anything unexpected is reported, never repaired. */
52545
+ function readClaudeSettings(claudeDir) {
52546
+ const path = join(claudeDir, "settings.json");
52547
+ const read = readManagedFile(path);
52548
+ if (read.state === "invalid") return {
52549
+ ok: false,
52550
+ detail: read.detail
52551
+ };
52552
+ if (read.state === "absent") return {
52553
+ ok: true,
52554
+ read,
52555
+ settings: {}
52556
+ };
52557
+ let parsed;
52558
+ try {
52559
+ parsed = JSON.parse(read.text);
52560
+ } catch (error) {
52561
+ return {
52562
+ ok: false,
52563
+ detail: `${path} is not valid JSON${jsonErrorDetail(error)}. Fix it by hand; it was not modified.`
52564
+ };
52565
+ }
52566
+ if (!record$2(parsed)) return {
52567
+ ok: false,
52568
+ detail: `${path} must contain a JSON object. Fix it by hand; it was not modified.`
52569
+ };
52570
+ if (parsed.hooks !== void 0 && !record$2(parsed.hooks)) return {
52571
+ ok: false,
52572
+ detail: `${path} has a "hooks" value that is not an object. Fix it by hand; it was not modified.`
52573
+ };
52574
+ return {
52575
+ ok: true,
52576
+ read,
52577
+ settings: parsed
52578
+ };
52579
+ }
52580
+ const LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
52581
+ /**
52582
+ * Write settings under the same lock every Primitive hook installer uses,
52583
+ * keeping the file's indentation, after a timestamped backup.
52584
+ */
52585
+ function writeClaudeSettings(params) {
52586
+ let release;
52587
+ for (let attempt = 0; attempt < 80; attempt++) try {
52588
+ release = acquireListenLock(params.claudeDir, "primitive-claude-settings");
52589
+ break;
52590
+ } catch {
52591
+ Atomics.wait(LOCK_WAIT, 0, 0, 25);
52592
+ }
52593
+ if (!release) throw new MachineFileError("Another Primitive command is editing Claude settings; try again shortly.");
52594
+ try {
52595
+ const original = params.read.state === "present" ? params.read.text : null;
52596
+ const indent = original === null ? 2 : jsonIndentation(original);
52597
+ const trailing = original === null || original.endsWith("\n") ? "\n" : "";
52598
+ return writeManagedFile({
52599
+ read: params.read,
52600
+ content: `${JSON.stringify(params.settings, null, indent)}${trailing}`,
52601
+ now: params.now,
52602
+ newFileMode: 384
52603
+ });
52604
+ } finally {
52605
+ release();
52606
+ }
52607
+ }
52608
+ const GLOBAL_HOOKS = {
52609
+ "claude.hook.session_start": {
52610
+ event: "SessionStart",
52611
+ subcommand: "session-register",
52612
+ timeout: 15
52613
+ },
52614
+ "claude.hook.session_end": {
52615
+ event: "SessionEnd",
52616
+ subcommand: "session-end",
52617
+ timeout: 10
52618
+ }
52619
+ };
52620
+ /** A hook this CLI installed for every Claude session, from any CLI path or version. */
52621
+ function isGlobalManaged(hook, spec) {
52622
+ if (!record$2(hook) || hook.type !== "command" || !Array.isArray(hook.args)) return false;
52623
+ const args = hook.args;
52624
+ return args.length === 6 && typeof args[0] === "string" && args[1] === "agent" && args[2] === spec.subcommand && args[3] === "--runtime" && args[4] === "claude" && args[5] === "--hook";
52625
+ }
52626
+ function globalHookArgs(cli, spec) {
52627
+ return [
52628
+ cli.entry,
52629
+ "agent",
52630
+ spec.subcommand,
52631
+ "--runtime",
52632
+ "claude",
52633
+ "--hook"
52634
+ ];
52635
+ }
52636
+ function globalHookCurrent(hook, cli) {
52637
+ const args = hook.args;
52638
+ return samePath(hook.command, cli.node) && samePath(args[0], cli.entry);
52639
+ }
52640
+ const WAKE_MARKER = "primitive-agent-wake-v1";
52641
+ const PENDING_MARKER = "primitive-pending-mail-v1";
52642
+ const SESSION_EVENTS = [
52643
+ "Stop",
52644
+ "SessionStart",
52645
+ "PostToolUse"
52646
+ ];
52647
+ /** A per-session receive hook written by `agent connect` or `agent enroll`, any version. */
52648
+ function parseSessionHook(hook) {
52649
+ if (!record$2(hook) || hook.type !== "command" || !Array.isArray(hook.args)) return null;
52650
+ const args = hook.args;
52651
+ if (!args.every((value) => typeof value === "string")) return null;
52652
+ const strings = args;
52653
+ const script = basename(strings[0] ?? "");
52654
+ if (script === "claude-pending-mail.mjs") return strings.length === 7 && strings[6] === PENDING_MARKER ? {
52655
+ kind: "pending",
52656
+ configDir: strings[2] ?? "",
52657
+ profile: strings[3] ?? null,
52658
+ session: strings[5] ?? null
52659
+ } : null;
52660
+ if (script !== "claude-wake.mjs") return null;
52661
+ if (strings.length === 7 && strings[6] === WAKE_MARKER) return {
52662
+ kind: "wake",
52663
+ configDir: strings[2] ?? "",
52664
+ profile: strings[3] ?? null,
52665
+ session: strings[5] ?? null
52666
+ };
52667
+ if (strings.length === 6 && strings[5] === WAKE_MARKER) return {
52668
+ kind: "wake",
52669
+ configDir: strings[2] ?? "",
52670
+ profile: strings[3] ?? null,
52671
+ session: null
52672
+ };
52673
+ if (strings.length === 4 && strings[3] === WAKE_MARKER) return {
52674
+ kind: "wake",
52675
+ configDir: strings[2] ?? "",
52676
+ profile: null,
52677
+ session: null
52678
+ };
52679
+ return null;
52680
+ }
52681
+ /** A saved connected profile still holds a credential. Unreadable counts as live. */
52682
+ function profileStillConnected(configDir, profile) {
52683
+ try {
52684
+ return loadConnectedAgentProfile(configDir, profile) !== null;
52685
+ } catch {
52686
+ return true;
52687
+ }
52688
+ }
52689
+ function sessionHookPathsCurrent(hook, parsed, cli) {
52690
+ const args = hook.args;
52691
+ if (!cli) return existsSync(String(hook.command)) && existsSync(args[0] ?? "") && existsSync(args[1] ?? "");
52692
+ return samePath(hook.command, cli.node) && samePath(args[0], parsed.kind === "wake" ? cli.wake : cli.pending) && samePath(args[1], cli.entry);
52693
+ }
52694
+ function judgeSessionHooks(hooks, cli) {
52695
+ const verdicts = /* @__PURE__ */ new Map();
52696
+ const seen = /* @__PURE__ */ new Set();
52697
+ for (const event of SESSION_EVENTS) {
52698
+ const entries = hooks[event];
52699
+ if (!Array.isArray(entries)) continue;
52700
+ for (const entry of entries) {
52701
+ if (!record$2(entry) || !Array.isArray(entry.hooks)) continue;
52702
+ for (const hook of entry.hooks) {
52703
+ const parsed = parseSessionHook(hook);
52704
+ if (!parsed || !record$2(hook)) continue;
52705
+ const pathsExist = existsSync(String(hook.command)) && existsSync(String(hook.args[0])) && existsSync(String(hook.args[1]));
52706
+ if (!(parsed.profile ? existsSync(parsed.configDir) && profileStillConnected(parsed.configDir, parsed.profile) : existsSync(parsed.configDir) && pathsExist)) {
52707
+ verdicts.set(hook, "stale");
52708
+ continue;
52709
+ }
52710
+ const key = [
52711
+ event,
52712
+ parsed.kind,
52713
+ resolve(parsed.configDir),
52714
+ parsed.profile ?? "",
52715
+ parsed.session?.toLowerCase() ?? ""
52716
+ ].join("\0");
52717
+ if (seen.has(key)) {
52718
+ verdicts.set(hook, "duplicate");
52719
+ continue;
52720
+ }
52721
+ seen.add(key);
52722
+ verdicts.set(hook, sessionHookPathsCurrent(hook, parsed, cli) ? "keep" : "outdated");
52723
+ }
52724
+ }
52725
+ }
52726
+ return verdicts;
52727
+ }
52728
+ function eventEntries(hooks, event) {
52729
+ const entries = hooks[event];
52730
+ if (entries === void 0) return [];
52731
+ return Array.isArray(entries) ? entries : "invalid";
52732
+ }
52733
+ /** Inspect every Primitive-owned Claude hook without changing anything. */
52734
+ function inspectClaudeHooks(settings, context) {
52735
+ const hooks = record$2(settings.hooks) ? settings.hooks : {};
52736
+ const canFix = context.cli !== null && context.blocked === null;
52737
+ const blockedNote = context.blocked ? ` ${context.blocked}` : "";
52738
+ const findings = {};
52739
+ for (const [id, spec] of Object.entries(GLOBAL_HOOKS)) {
52740
+ const entries = eventEntries(hooks, spec.event);
52741
+ if (entries === "invalid") {
52742
+ findings[id] = {
52743
+ status: "fail",
52744
+ detail: `hooks.${spec.event} is not a list. Fix settings.json by hand; it was not modified.`,
52745
+ fixable: false
52746
+ };
52747
+ continue;
52748
+ }
52749
+ const managed = entries.flatMap((entry) => record$2(entry) && Array.isArray(entry.hooks) ? entry.hooks.filter((hook) => isGlobalManaged(hook, spec)) : []);
52750
+ const [first] = managed;
52751
+ if (!first) findings[id] = {
52752
+ status: "fail",
52753
+ detail: `No ${spec.event} hook runs \`primitive agent ${spec.subcommand}\`.${blockedNote}`,
52754
+ fixable: canFix
52755
+ };
52756
+ else if (managed.length > 1) findings[id] = {
52757
+ status: "fail",
52758
+ detail: `${managed.length} ${spec.event} hooks run \`primitive agent ${spec.subcommand}\`; expected exactly one.${blockedNote}`,
52759
+ fixable: canFix
52760
+ };
52761
+ else if (!context.cli || !globalHookCurrent(first, context.cli)) findings[id] = {
52762
+ status: "fail",
52763
+ detail: `The ${spec.event} hook runs ${String(first.args[0])}, which ${existsSync(String(first.args[0])) ? "is not this CLI" : "no longer exists"}.${blockedNote}`,
52764
+ fixable: canFix
52765
+ };
52766
+ else findings[id] = {
52767
+ status: "ok",
52768
+ detail: `${spec.event} runs \`primitive agent ${spec.subcommand}\` from ${context.cli.entry}.`,
52769
+ fixable: false
52770
+ };
52771
+ }
52772
+ const invalid = SESSION_EVENTS.find((event) => eventEntries(hooks, event) === "invalid");
52773
+ if (invalid) {
52774
+ findings["claude.hook.stop"] = {
52775
+ status: "fail",
52776
+ detail: `hooks.${invalid} is not a list. Fix settings.json by hand; it was not modified.`,
52777
+ fixable: false
52778
+ };
52779
+ return findings;
52780
+ }
52781
+ const counts = {
52782
+ keep: 0,
52783
+ stale: 0,
52784
+ duplicate: 0,
52785
+ outdated: 0
52786
+ };
52787
+ for (const verdict of judgeSessionHooks(hooks, context.cli).values()) counts[verdict]++;
52788
+ const problems = [
52789
+ counts.stale ? `${counts.stale} for disconnected or removed sessions` : "",
52790
+ counts.duplicate ? `${counts.duplicate} duplicates` : "",
52791
+ counts.outdated ? `${counts.outdated} pointing at an old CLI path` : ""
52792
+ ].filter(Boolean);
52793
+ findings["claude.hook.stop"] = problems.length ? {
52794
+ status: "fail",
52795
+ detail: `Per-session receive hooks need cleanup: ${problems.join(", ")}.${blockedNote}`,
52796
+ fixable: canFix || counts.outdated === 0 && context.blocked === null
52797
+ } : {
52798
+ status: "ok",
52799
+ detail: counts.keep ? `${counts.keep} per-session receive hooks, each present once and current.` : "No per-session receive hooks; each registered session adds its own exact-session hooks.",
52800
+ fixable: false
52801
+ };
52802
+ return findings;
52803
+ }
52804
+ function rewriteEntries(entries, transform) {
52805
+ let changed = false;
52806
+ return {
52807
+ entries: entries.flatMap((entry) => {
52808
+ if (!record$2(entry) || !Array.isArray(entry.hooks)) return [entry];
52809
+ let touched = false;
52810
+ const kept = entry.hooks.flatMap((hook) => {
52811
+ const replacement = transform(hook);
52812
+ if (replacement === hook) return [hook];
52813
+ touched = true;
52814
+ return replacement === void 0 ? [] : [replacement];
52815
+ });
52816
+ if (!touched) return [entry];
52817
+ changed = true;
52818
+ return kept.length ? [{
52819
+ ...entry,
52820
+ hooks: kept
52821
+ }] : [];
52822
+ }),
52823
+ changed
52824
+ };
52825
+ }
52826
+ /**
52827
+ * Apply fixes for the requested checks to a copy of the settings. Only
52828
+ * Primitive-owned hooks are added, rewritten or removed; every other key,
52829
+ * entry and sibling hook is carried over unchanged.
52830
+ */
52831
+ function repairClaudeHooks(settings, context, checks) {
52832
+ const changed = /* @__PURE__ */ new Set();
52833
+ const hooks = record$2(settings.hooks) ? { ...settings.hooks } : {};
52834
+ const cli = context.blocked === null ? context.cli : null;
52835
+ for (const [id, spec] of Object.entries(GLOBAL_HOOKS)) {
52836
+ if (!checks.has(id) || !cli) continue;
52837
+ const entries = eventEntries(hooks, spec.event);
52838
+ if (entries === "invalid") continue;
52839
+ let placed = false;
52840
+ const result = rewriteEntries(entries, (hook) => {
52841
+ if (!isGlobalManaged(hook, spec)) return hook;
52842
+ if (placed) return void 0;
52843
+ placed = true;
52844
+ if (globalHookCurrent(hook, cli)) return hook;
52845
+ return {
52846
+ ...hook,
52847
+ command: cli.node,
52848
+ args: globalHookArgs(cli, spec)
52849
+ };
52850
+ });
52851
+ let next = result.entries;
52852
+ if (!placed) next = [...next, { hooks: [{
52853
+ type: "command",
52854
+ command: cli.node,
52855
+ args: globalHookArgs(cli, spec),
52856
+ timeout: spec.timeout
52857
+ }] }];
52858
+ if (result.changed || !placed) {
52859
+ hooks[spec.event] = next;
52860
+ changed.add(id);
52861
+ }
52862
+ }
52863
+ if (checks.has("claude.hook.stop") && context.blocked === null) {
52864
+ if (!SESSION_EVENTS.some((event) => eventEntries(hooks, event) === "invalid")) {
52865
+ const verdicts = judgeSessionHooks(hooks, context.cli);
52866
+ for (const event of SESSION_EVENTS) {
52867
+ const entries = eventEntries(hooks, event);
52868
+ if (entries === "invalid" || entries.length === 0) continue;
52869
+ const result = rewriteEntries(entries, (hook) => {
52870
+ const verdict = verdicts.get(hook);
52871
+ if (verdict === "stale" || verdict === "duplicate") return void 0;
52872
+ if (verdict !== "outdated" || !cli || !record$2(hook)) return hook;
52873
+ const parsed = parseSessionHook(hook);
52874
+ const args = [...hook.args];
52875
+ args[0] = parsed?.kind === "pending" ? cli.pending : cli.wake;
52876
+ args[1] = cli.entry;
52877
+ return {
52878
+ ...hook,
52879
+ command: cli.node,
52880
+ args
52881
+ };
52882
+ });
52883
+ if (result.changed) {
52884
+ hooks[event] = result.entries;
52885
+ changed.add("claude.hook.stop");
52886
+ }
52887
+ }
52888
+ }
52889
+ }
52890
+ if (!changed.size) return {
52891
+ settings,
52892
+ changed
52893
+ };
52894
+ return {
52895
+ settings: {
52896
+ ...settings,
52897
+ hooks
52898
+ },
52899
+ changed
52900
+ };
52901
+ }
52902
+ //#endregion
52903
+ //#region src/oclif/codex-machine-hooks.ts
52904
+ function record$1(value) {
52905
+ return value !== null && typeof value === "object" && !Array.isArray(value);
52906
+ }
52907
+ /** Codex hook commands are shell strings, so every path is single-quoted. */
52908
+ function shellQuote(value) {
52909
+ return `'${value.replace(/'/g, `'\\''`)}'`;
52910
+ }
52911
+ const MANAGED_SUFFIX = /\bagent session-register --runtime codex --hook\s*$/;
52912
+ function codexHookCommand(cli) {
52913
+ return `${shellQuote(cli.node)} ${shellQuote(cli.entry)} agent session-register --runtime codex --hook`;
52914
+ }
52915
+ /** A SessionStart hook this CLI installed, from any CLI path or version. */
52916
+ function isManaged(hook) {
52917
+ return record$1(hook) && hook.type === "command" && typeof hook.command === "string" && MANAGED_SUFFIX.test(hook.command);
52918
+ }
52919
+ /** Parse hooks.json. Anything unexpected is reported, never repaired. */
52920
+ function readCodexHooks(codexHome) {
52921
+ const path = join(codexHome, "hooks.json");
52922
+ const read = readManagedFile(path);
52923
+ if (read.state === "invalid") return {
52924
+ ok: false,
52925
+ path,
52926
+ detail: read.detail
52927
+ };
52928
+ if (read.state === "absent") return {
52929
+ ok: true,
52930
+ path,
52931
+ read,
52932
+ settings: {}
52933
+ };
52934
+ let parsed;
52935
+ try {
52936
+ parsed = JSON.parse(read.text);
52937
+ } catch {
52938
+ return {
52939
+ ok: false,
52940
+ path,
52941
+ detail: `${path} is not valid JSON. Fix it by hand; it was not modified.`
52942
+ };
52943
+ }
52944
+ if (!record$1(parsed) || parsed.hooks !== void 0 && !record$1(parsed.hooks)) return {
52945
+ ok: false,
52946
+ path,
52947
+ detail: `${path} must be a JSON object with a "hooks" object. Fix it by hand; it was not modified.`
52948
+ };
52949
+ const entries = record$1(parsed.hooks) ? parsed.hooks.SessionStart : void 0;
52950
+ if (entries !== void 0 && !Array.isArray(entries)) return {
52951
+ ok: false,
52952
+ path,
52953
+ detail: `hooks.SessionStart in ${path} is not a list. Fix it by hand; it was not modified.`
52954
+ };
52955
+ return {
52956
+ ok: true,
52957
+ path,
52958
+ read,
52959
+ settings: parsed
52960
+ };
52961
+ }
52962
+ function sessionStartEntries(settings) {
52963
+ const hooks = record$1(settings.hooks) ? settings.hooks : {};
52964
+ return Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
52965
+ }
52966
+ /** Where each managed hook sits, as Codex numbers hooks for trust records. */
52967
+ function managedPositions(settings) {
52968
+ const found = [];
52969
+ sessionStartEntries(settings).forEach((entry, entryIndex) => {
52970
+ if (!record$1(entry) || !Array.isArray(entry.hooks)) return;
52971
+ const hooks = entry.hooks;
52972
+ hooks.forEach((hook, hookIndex) => {
52973
+ if (isManaged(hook)) found.push({
52974
+ entry: entryIndex,
52975
+ hook: hookIndex,
52976
+ value: hook,
52977
+ last: hookIndex === hooks.length - 1,
52978
+ matcher: typeof entry.matcher === "string" ? entry.matcher : null
52979
+ });
52980
+ });
52981
+ });
52982
+ return found;
52983
+ }
52984
+ /** Codex's default spill threshold; a hook set to it hashes as if unset. */
52985
+ const DEFAULT_ADDITIONAL_CONTEXT_LIMIT = 2500;
52986
+ function sortKeys(value) {
52987
+ if (Array.isArray(value)) return value.map(sortKeys);
52988
+ if (record$1(value)) return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]));
52989
+ return value;
52990
+ }
52991
+ /**
52992
+ * The trust hash Codex computes for a hooks.json command hook: SHA-256 of the
52993
+ * key-sorted JSON of its normalized identity (event, matcher, and the
52994
+ * handler with its default timeout filled in and unset options omitted).
52995
+ * Checked against the trust records Codex wrote on a real machine.
52996
+ */
52997
+ function codexHookTrustHash(event, matcher, hook) {
52998
+ const timeout = typeof hook.timeout === "number" && Number.isInteger(hook.timeout) ? Math.max(1, hook.timeout) : 600;
52999
+ const handler = {
53000
+ type: "command",
53001
+ command: hook.command,
53002
+ timeout,
53003
+ async: hook.async === true
53004
+ };
53005
+ if (typeof hook.statusMessage === "string") handler.statusMessage = hook.statusMessage;
53006
+ if (typeof hook.additionalContextLimit === "number" && hook.additionalContextLimit !== DEFAULT_ADDITIONAL_CONTEXT_LIMIT) handler.additionalContextLimit = hook.additionalContextLimit;
53007
+ const identity = {
53008
+ event_name: event,
53009
+ hooks: [handler]
53010
+ };
53011
+ if (matcher !== null) identity.matcher = matcher;
53012
+ return `sha256:${createHash("sha256").update(JSON.stringify(sortKeys(identity))).digest("hex")}`;
53013
+ }
53014
+ /**
53015
+ * Codex runs a hooks.json hook only once the person approves it. It records
53016
+ * the approval under `[hooks.state."<file>:session_start:<entry>:<hook>"]` in
53017
+ * config.toml with the hash of the hook as approved; a different current hash
53018
+ * means the approval no longer applies.
53019
+ */
53020
+ function trustState(configToml, key, currentHash) {
53021
+ if (configToml === null) return "untrusted";
53022
+ const lines = configToml.split(/\r?\n/);
53023
+ const header = `[hooks.state."${key}"]`;
53024
+ const start = lines.findIndex((line) => line.trim() === header);
53025
+ if (start < 0) return "untrusted";
53026
+ let trusted = null;
53027
+ let disabled = false;
53028
+ for (const line of lines.slice(start + 1)) {
53029
+ if (line.trim().startsWith("[")) break;
53030
+ const hash = /^\s*trusted_hash\s*=\s*"([^"]*)"/.exec(line);
53031
+ if (hash) trusted = hash[1] ?? null;
53032
+ if (/^\s*enabled\s*=\s*false\b/.test(line)) disabled = true;
53033
+ }
53034
+ if (disabled) return "disabled";
53035
+ if (trusted === null) return "untrusted";
53036
+ return trusted === currentHash ? "trusted" : "modified";
53037
+ }
53038
+ function inspectCodexHook(read, context, configToml) {
53039
+ const canFix = context.cli !== null && context.blocked === null;
53040
+ const blockedNote = context.blocked ? ` ${context.blocked}` : "";
53041
+ const managed = managedPositions(read.settings);
53042
+ const [first] = managed;
53043
+ if (!first) return {
53044
+ status: "fail",
53045
+ detail: `No SessionStart hook in ${read.path} runs \`primitive agent session-register --runtime codex --hook\`.${blockedNote}`,
53046
+ fixable: canFix
53047
+ };
53048
+ const desired = context.cli ? codexHookCommand(context.cli) : null;
53049
+ const extras = managed.slice(1);
53050
+ const removable = extras.filter((copy) => copy.last).length;
53051
+ const stale = managed.filter((copy) => copy.value.command !== desired).length;
53052
+ if (removable || stale) return {
53053
+ status: "fail",
53054
+ detail: [stale ? `${stale} SessionStart hooks run \`primitive agent session-register\` from another CLI path.` : "", removable ? `${removable} extra copies can be removed without moving other hooks.` : ""].filter(Boolean).join(" ").concat(blockedNote),
53055
+ fixable: canFix
53056
+ };
53057
+ if (extras.length) return {
53058
+ status: "warn",
53059
+ detail: `${extras.length} extra copies of the hook sit before other hooks in their entries; they were left in place so those hooks keep their Codex approval. Remove them by hand if you like.`,
53060
+ fixable: false
53061
+ };
53062
+ const trust = trustState(configToml, `${read.path}:session_start:${first.entry}:${first.hook}`, codexHookTrustHash("session_start", first.matcher, first.value));
53063
+ if (trust === "disabled") return {
53064
+ status: "warn",
53065
+ detail: "The Codex SessionStart hook is installed but disabled in Codex.",
53066
+ fixable: false
53067
+ };
53068
+ if (trust !== "trusted") return {
53069
+ status: "warn",
53070
+ detail: trust === "modified" ? "The Codex SessionStart hook changed since it was approved; approval needs to be confirmed in Codex." : "The Codex SessionStart hook is installed; approval needs to be confirmed in Codex before it runs.",
53071
+ fixable: false
53072
+ };
53073
+ return {
53074
+ status: "ok",
53075
+ detail: `SessionStart runs \`primitive agent session-register --runtime codex --hook\` from ${context.cli?.entry ?? "this CLI"}, approved in Codex.`,
53076
+ fixable: false
53077
+ };
53078
+ }
53079
+ /**
53080
+ * Codex keys approvals by entry and hook position, so this never removes or
53081
+ * reorders an entry, and never moves a hook that is not Primitive's. Managed
53082
+ * hooks are rewritten in place; an extra copy is removed only when it is the
53083
+ * last hook in its entry (its entry stays, even if empty); a hook is appended
53084
+ * as a new last entry when none exists.
53085
+ */
53086
+ function repairCodexHook(settings, cli) {
53087
+ const desired = codexHookCommand(cli);
53088
+ let placed = false;
53089
+ let changed = false;
53090
+ const entries = sessionStartEntries(settings).map((entry) => {
53091
+ if (!record$1(entry) || !Array.isArray(entry.hooks)) return entry;
53092
+ const hooks = entry.hooks;
53093
+ let touched = false;
53094
+ const next = hooks.flatMap((hook, index) => {
53095
+ if (!isManaged(hook)) return [hook];
53096
+ const isLast = index === hooks.length - 1;
53097
+ if (placed && isLast) {
53098
+ touched = true;
53099
+ return [];
53100
+ }
53101
+ placed = true;
53102
+ if (hook.command === desired) return [hook];
53103
+ touched = true;
53104
+ return [{
53105
+ ...hook,
53106
+ command: desired
53107
+ }];
53108
+ });
53109
+ if (!touched) return entry;
53110
+ changed = true;
53111
+ return {
53112
+ ...entry,
53113
+ hooks: next
53114
+ };
53115
+ });
53116
+ if (!placed) {
53117
+ entries.push({ hooks: [{
53118
+ type: "command",
53119
+ command: desired
53120
+ }] });
53121
+ changed = true;
53122
+ }
53123
+ if (!changed) return {
53124
+ settings,
53125
+ changed
53126
+ };
53127
+ const hooks = record$1(settings.hooks) ? settings.hooks : {};
53128
+ return {
53129
+ settings: {
53130
+ ...settings,
53131
+ hooks: {
53132
+ ...hooks,
53133
+ SessionStart: entries
53134
+ }
53135
+ },
53136
+ changed
53137
+ };
53138
+ }
53139
+ function writeCodexHooks(params) {
53140
+ const original = params.read.state === "present" ? params.read.text : null;
53141
+ const indent = original === null ? 2 : jsonIndentation(original);
53142
+ const trailing = original === null || original.endsWith("\n") ? "\n" : "";
53143
+ writeManagedFile({
53144
+ read: params.read,
53145
+ content: `${JSON.stringify(params.settings, null, indent)}${trailing}`,
53146
+ now: params.now,
53147
+ newFileMode: 384
53148
+ });
53149
+ }
53150
+ const DOCTOR_CHECK_IDS = [
53151
+ "cli.installed",
53152
+ "cli.version",
53153
+ "cli.path_stable",
53154
+ "auth.member_login",
53155
+ "claude.settings_valid",
53156
+ "claude.hook.session_start",
53157
+ "claude.hook.session_end",
53158
+ "claude.hook.stop",
53159
+ "claude.instructions",
53160
+ "codex.hook.session_start",
53161
+ "codex.instructions",
53162
+ "omp.instructions",
53163
+ "skill.claude",
53164
+ "skill.codex",
53165
+ "profiles.orphaned"
53166
+ ];
53167
+ const TITLES = {
53168
+ "cli.installed": "Primitive CLI installed",
53169
+ "cli.version": "Primitive CLI version",
53170
+ "cli.path_stable": "CLI location is stable",
53171
+ "auth.member_login": "Member login",
53172
+ "claude.settings_valid": "Claude Code settings file",
53173
+ "claude.hook.session_start": "Claude SessionStart hook",
53174
+ "claude.hook.session_end": "Claude SessionEnd hook",
53175
+ "claude.hook.stop": "Claude per-session receive hooks",
53176
+ "claude.instructions": "Claude Code instructions",
53177
+ "codex.hook.session_start": "Codex SessionStart hook",
53178
+ "codex.instructions": "Codex instructions",
53179
+ "omp.instructions": "omp instructions",
53180
+ "skill.claude": "primitive-connect skill for Claude Code",
53181
+ "skill.codex": "primitive-connect skill for Codex",
53182
+ "profiles.orphaned": "Saved agent profiles"
53183
+ };
53184
+ const WORKING_LINE = "Keep AGENT_WORKING current: `primitive agent working set \"<task>: <files>\"` when work starts and `primitive agent working clear` when it ends.";
53185
+ const REPLY_LINE = "Answer Primitive mail on Primitive with `primitive reply`; read receipts are automatic.";
53186
+ const SECRET_LINE = "Never print invitation tokens or credentials.";
53187
+ /** The managed instruction block for one runtime. Kept short on purpose. */
53188
+ function managedInstructions(runtime) {
53189
+ return [
53190
+ "## Primitive",
53191
+ "",
53192
+ `- ${runtime === "claude" || runtime === "codex" ? `A Primitive SessionStart hook registers this session with \`primitive agent session-register --runtime ${runtime} --quiet\`; if it has not run, run that command once.` : `At session start, if this session is not registered yet, run \`primitive agent session-register --runtime ${runtime} --quiet\`.`}`,
53193
+ `- ${REPLY_LINE}`,
53194
+ `- ${WORKING_LINE}`,
53195
+ `- ${SECRET_LINE}`
53196
+ ].join("\n");
53197
+ }
53198
+ /** Compare dotted numeric versions; pre-release suffixes are ignored. */
53199
+ function compareVersions(a, b) {
53200
+ const parse = (value) => {
53201
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
53202
+ return match ? match.slice(1, 4).map(Number) : null;
53203
+ };
53204
+ const left = parse(a);
53205
+ const right = parse(b);
53206
+ if (!left || !right) return null;
53207
+ for (let index = 0; index < 3; index++) {
53208
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
53209
+ if (difference) return Math.sign(difference);
53210
+ }
53211
+ return 0;
53212
+ }
53213
+ async function npmLatestVersion(fetchImpl) {
53214
+ try {
53215
+ const response = await fetchImpl("https://registry.npmjs.org/primitive/latest", {
53216
+ headers: { accept: "application/json" },
53217
+ redirect: "follow",
53218
+ signal: AbortSignal.timeout(3e3)
53219
+ });
53220
+ if (!response.ok) return null;
53221
+ const text = await response.text();
53222
+ if (text.length > 1048576) return null;
53223
+ const version = JSON.parse(text).version;
53224
+ return typeof version === "string" ? version : null;
53225
+ } catch {
53226
+ return null;
53227
+ }
53228
+ }
53229
+ const SKILL_BACKUPS_KEPT = 2;
53230
+ /**
53231
+ * Skill backups are named <runtime>-<sequence>-<timestamp>. The sequence is
53232
+ * one past the highest existing one for the runtime, so names order strictly
53233
+ * by creation even when several are made within the same second. Backups
53234
+ * from the earlier <runtime>-<timestamp>[-<n>] naming have no sequence; they
53235
+ * sort before every sequenced backup, oldest first, so pruning removes them
53236
+ * first.
53237
+ */
53238
+ function skillBackups(directory, runtime) {
53239
+ const sequenced = new RegExp(`^${runtime}-(\\d{6,})-\\d{8}T\\d{6}Z$`);
53240
+ const legacy = new RegExp(`^${runtime}-(\\d{8}T\\d{6}Z)(?:-(\\d+))?$`);
53241
+ let names;
53242
+ try {
53243
+ names = readdirSync(directory);
53244
+ } catch {
53245
+ return [];
53246
+ }
53247
+ const legacyNames = names.flatMap((name) => {
53248
+ const match = legacy.exec(name);
53249
+ return match ? [{
53250
+ name,
53251
+ stamp: match[1] ?? "",
53252
+ suffix: Number(match[2] ?? 0)
53253
+ }] : [];
53254
+ }).sort((left, right) => left.stamp === right.stamp ? left.suffix - right.suffix : left.stamp < right.stamp ? -1 : 1).map(({ name }, index, all) => ({
53255
+ name,
53256
+ sequence: index - all.length
53257
+ }));
53258
+ const sequencedNames = names.flatMap((name) => {
53259
+ const match = sequenced.exec(name);
53260
+ return match ? [{
53261
+ name,
53262
+ sequence: Number(match[1])
53263
+ }] : [];
53264
+ });
53265
+ return [...legacyNames, ...sequencedNames].sort((left, right) => left.sequence - right.sequence);
53266
+ }
53267
+ function nextSkillBackupName(directory, runtime, now) {
53268
+ const last = Math.max(0, skillBackups(directory, runtime).at(-1)?.sequence ?? 0);
53269
+ return `${runtime}-${String(last + 1).padStart(6, "0")}-${backupStamp(now)}`;
53270
+ }
53271
+ function pruneSkillBackups(directory, runtime) {
53272
+ const backups = skillBackups(directory, runtime);
53273
+ for (const { name } of backups.slice(0, Math.max(0, backups.length - SKILL_BACKUPS_KEPT))) try {
53274
+ rmSync(join(directory, name), {
53275
+ recursive: true,
53276
+ force: true
53277
+ });
53278
+ } catch {}
53279
+ }
53280
+ function backupStamp(now) {
53281
+ return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
53282
+ }
53283
+ function runtimePresent(runtime, paths, env) {
53284
+ return existsSync(runtime === "claude" ? paths.claudeDir : runtime === "codex" ? paths.codexHome : paths.ompRoot) || findOnPath(runtime, env) !== null;
53285
+ }
53286
+ function runtimePaths(env, home) {
53287
+ const ompRoot = resolve(home, env.PI_CONFIG_DIR || ".omp");
53288
+ return {
53289
+ claudeDir: claudeConfigDir(env, home),
53290
+ codexHome: resolve(env.CODEX_HOME || join(home, ".codex")),
53291
+ ompRoot,
53292
+ ompAgentDir: resolve(env.PI_CODING_AGENT_DIR || join(ompRoot, "agent"))
53293
+ };
53294
+ }
53295
+ /** Codex reads AGENTS.override.md instead of AGENTS.md when it has content. */
53296
+ function codexInstructionsPath(codexHome) {
53297
+ const override = join(codexHome, "AGENTS.override.md");
53298
+ const read = readManagedFile(override);
53299
+ if (read.state === "present" && read.text.trim()) return override;
53300
+ return join(codexHome, "AGENTS.md");
53301
+ }
53302
+ async function memberCredentialsFor(options) {
53303
+ try {
53304
+ const saved = loadCliCredentials(options.configDir);
53305
+ if (!saved) return null;
53306
+ return await refreshStoredCliCredentials({
53307
+ apiBaseUrl: saved.api_base_url,
53308
+ configDir: options.configDir,
53309
+ credentials: saved,
53310
+ fetch: options.fetch
53311
+ });
53312
+ } catch {
53313
+ return null;
53314
+ }
53315
+ }
53316
+ /** Find an address in the member's connection list; null when it cannot be confirmed. */
53317
+ async function ownerListedStatus(credentials, address, fetchImpl) {
53318
+ let cursor;
53319
+ for (let page = 0; page < 10; page++) {
53320
+ const url = new URL(`${credentials.api_base_url}/agent-connections`);
53321
+ url.searchParams.set("limit", "50");
53322
+ if (cursor) url.searchParams.set("cursor", cursor);
53323
+ try {
53324
+ const response = await fetchImpl(url, {
53325
+ method: "GET",
53326
+ redirect: "error",
53327
+ signal: AbortSignal.timeout(5e3),
53328
+ headers: {
53329
+ authorization: `Bearer ${credentials.access_token}`,
53330
+ accept: "application/json"
53331
+ }
53332
+ });
53333
+ if (response.status !== 200) {
53334
+ await response.body?.cancel().catch(() => void 0);
53335
+ return null;
53336
+ }
53337
+ const text = await response.text();
53338
+ if (text.length > 1048576) return null;
53339
+ const body = JSON.parse(text);
53340
+ if (body.success !== true || !Array.isArray(body.data)) return null;
53341
+ for (const row of body.data) if (row?.address === address && typeof row.status === "string") return row.status;
53342
+ const next = body.meta?.cursor;
53343
+ if (typeof next !== "string" || !next || next === cursor) return null;
53344
+ cursor = next;
53345
+ } catch {
53346
+ return null;
53347
+ }
53348
+ }
53349
+ return null;
53350
+ }
53351
+ function check(id, status, detail, extra = {}) {
53352
+ return {
53353
+ id,
53354
+ title: TITLES[id],
53355
+ status,
53356
+ detail,
53357
+ fixable: false,
53358
+ ...extra
53359
+ };
53360
+ }
53361
+ /**
53362
+ * Inspect this machine's Primitive setup for every agent runtime, and with
53363
+ * `fix`, repair what can be repaired without the person. Repairs only touch
53364
+ * Primitive-owned content, back up any file before changing it, write
53365
+ * atomically, and are no-ops when nothing drifted.
53366
+ */
53367
+ async function runMachineDoctor(options) {
53368
+ const env = options.env;
53369
+ const fetchImpl = options.fetch ?? fetch;
53370
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
53371
+ const paths = runtimePaths(env, options.home);
53372
+ const selected = (runtime) => !options.runtimes || options.runtimes.has(runtime);
53373
+ const present = Object.fromEntries(MACHINE_RUNTIMES.map((runtime) => [runtime, runtimePresent(runtime, paths, env)]));
53374
+ const skipRuntime = (runtime) => !selected(runtime) ? "Not selected with --runtime." : !present[runtime] ? `${runtime === "claude" ? "Claude Code" : runtime === "codex" ? "Codex" : "omp"} was not found on this machine.` : null;
53375
+ const cli = currentCliLocation(options.cliEntry, env, options.execPath);
53376
+ const ephemeral = cli ? ephemeralCliReason(cli.entry) : null;
53377
+ const hookBlocked = !cli ? "This CLI's hook scripts were not found next to it." : ephemeral ? `This CLI runs from ${ephemeral}, which can be deleted; install it with \`npm install -g primitive\` before repairing hooks.` : null;
53378
+ const skillEnv = {
53379
+ ...env,
53380
+ CLAUDE_CONFIG_DIR: paths.claudeDir,
53381
+ CODEX_HOME: paths.codexHome
53382
+ };
53383
+ let bundle;
53384
+ const loadBundle = () => {
53385
+ if (bundle !== void 0) return bundle;
53386
+ try {
53387
+ bundle = (options.bundle ?? (() => readBundledConnectSkill(options.packageRoot)))();
53388
+ } catch {
53389
+ bundle = null;
53390
+ }
53391
+ return bundle;
53392
+ };
53393
+ const instruction = (id, runtime, path) => ({
53394
+ inspect: () => {
53395
+ const skip = skipRuntime(runtime);
53396
+ if (skip) return check(id, "skip", skip);
53397
+ const target = path();
53398
+ const body = managedInstructions(runtime);
53399
+ const read = readManagedFile(target);
53400
+ if (read.state === "invalid") return check(id, "fail", read.detail, {
53401
+ path: target,
53402
+ action: "edit_file"
53403
+ });
53404
+ if (read.state === "absent") return check(id, "fail", `${target} does not exist yet.`, {
53405
+ path: target,
53406
+ fixable: true
53407
+ });
53408
+ const state = inspectManagedBlock(read.text, body);
53409
+ if (state.state === "ok") return check(id, "ok", `The Primitive block in ${target} is current.`, { path: target });
53410
+ if (state.state === "malformed") return check(id, "fail", `${target}: ${state.detail}. It was left unchanged.`, {
53411
+ path: target,
53412
+ action: "edit_file"
53413
+ });
53414
+ return check(id, "fail", state.state === "missing" ? `${target} has no Primitive block.` : `The Primitive block in ${target} is out of date (v${state.version}).`, {
53415
+ path: target,
53416
+ fixable: true
53417
+ });
53418
+ },
53419
+ repair: () => {
53420
+ const read = readManagedFile(path());
53421
+ if (read.state === "invalid") return false;
53422
+ const next = upsertManagedBlock(read.state === "present" ? read.text : "", managedInstructions(runtime));
53423
+ if (read.state === "present" && next === read.text) return false;
53424
+ writeManagedFile({
53425
+ read,
53426
+ content: next,
53427
+ now
53428
+ });
53429
+ return true;
53430
+ }
53431
+ });
53432
+ const skill = (id, runtime) => ({
53433
+ inspect: () => {
53434
+ const skip = skipRuntime(runtime);
53435
+ if (skip) return check(id, "skip", skip);
53436
+ const current = loadBundle();
53437
+ if (!current) return check(id, "skip", "This CLI build does not include the bundled skill.");
53438
+ const target = connectSkillTarget({
53439
+ runtime,
53440
+ env: skillEnv
53441
+ });
53442
+ let info = null;
53443
+ try {
53444
+ info = lstatSync(target);
53445
+ } catch {
53446
+ info = null;
53447
+ }
53448
+ if (!info) return check(id, "fail", `Not installed at ${target}.`, {
53449
+ path: target,
53450
+ fixable: true
53451
+ });
53452
+ if (info.isSymbolicLink()) return check(id, "warn", `${target} is a symlink you manage; it was left unchanged.`, { path: target });
53453
+ const version = installedVersion(target);
53454
+ if (version === current.version) return check(id, "ok", `Version ${version} matches this CLI.`, { path: target });
53455
+ return check(id, "fail", version ? `Installed version ${version} differs from this CLI's ${current.version}.` : `${target} is not a readable skill directory.`, {
53456
+ path: target,
53457
+ fixable: version !== null
53458
+ });
53459
+ },
53460
+ repair: () => {
53461
+ const current = loadBundle();
53462
+ if (!current) return false;
53463
+ const target = connectSkillTarget({
53464
+ runtime,
53465
+ env: skillEnv
53466
+ });
53467
+ const backups = join(options.configDir, "machine", "backups", "skills");
53468
+ if (existsSync(target)) {
53469
+ mkdirSync(backups, {
53470
+ recursive: true,
53471
+ mode: 448
53472
+ });
53473
+ const backup = join(backups, nextSkillBackupName(backups, runtime, now()));
53474
+ mkdirSync(backup, { mode: 448 });
53475
+ cpSync(target, backup, {
53476
+ recursive: true,
53477
+ verbatimSymlinks: true
53478
+ });
53479
+ }
53480
+ const result = installConnectSkill({
53481
+ bundle: current,
53482
+ runtime,
53483
+ env: skillEnv
53484
+ });
53485
+ if (result.state === "failed") throw new MachineFileError(`The skill could not be installed (${result.reason ?? "unknown"}).`);
53486
+ pruneSkillBackups(backups, runtime);
53487
+ return result.state === "installed" || result.state === "updated";
53488
+ }
53489
+ });
53490
+ const HOOK_CHECK_IDS = [
53491
+ "claude.hook.session_start",
53492
+ "claude.hook.session_end",
53493
+ "claude.hook.stop"
53494
+ ];
53495
+ const repairedAhead = /* @__PURE__ */ new Set();
53496
+ let settingsRead = readClaudeSettings(paths.claudeDir);
53497
+ const hookContext = {
53498
+ cli,
53499
+ blocked: hookBlocked
53500
+ };
53501
+ const hookCheck = (id) => ({
53502
+ inspect: () => {
53503
+ const skip = skipRuntime("claude");
53504
+ if (skip) return check(id, "skip", skip);
53505
+ if (!settingsRead.ok) return check(id, "fail", "Claude settings.json is invalid; repair it first.", { path: join(paths.claudeDir, "settings.json") });
53506
+ const finding = inspectClaudeHooks(settingsRead.settings, hookContext)[id];
53507
+ return check(id, finding.status, finding.detail, {
53508
+ fixable: finding.fixable,
53509
+ path: join(paths.claudeDir, "settings.json"),
53510
+ ...finding.status === "fail" && !finding.fixable && hookBlocked ? { action: "install_cli" } : {}
53511
+ });
53512
+ },
53513
+ repair: () => {
53514
+ if (!settingsRead.ok) return false;
53515
+ const findings = inspectClaudeHooks(settingsRead.settings, hookContext);
53516
+ const ids = HOOK_CHECK_IDS.filter((hook) => (hook === id || !options.only || options.only.has(hook)) && findings[hook].status === "fail" && findings[hook].fixable);
53517
+ const repaired = repairClaudeHooks(settingsRead.settings, hookContext, new Set(ids));
53518
+ if (!repaired.changed.size) return false;
53519
+ for (const hook of repaired.changed) repairedAhead.add(hook);
53520
+ writeClaudeSettings({
53521
+ claudeDir: paths.claudeDir,
53522
+ read: settingsRead.read,
53523
+ settings: repaired.settings,
53524
+ now
53525
+ });
53526
+ settingsRead = readClaudeSettings(paths.claudeDir);
53527
+ return repaired.changed.has(id);
53528
+ }
53529
+ });
53530
+ const codexHook = {
53531
+ inspect: () => {
53532
+ const id = "codex.hook.session_start";
53533
+ const skip = skipRuntime("codex");
53534
+ if (skip) return check(id, "skip", skip);
53535
+ const read = readCodexHooks(paths.codexHome);
53536
+ if (!read.ok) return check(id, "fail", read.detail, {
53537
+ path: read.path,
53538
+ action: "edit_file"
53539
+ });
53540
+ const configToml = readManagedFile(join(paths.codexHome, "config.toml"));
53541
+ const finding = inspectCodexHook(read, hookContext, configToml.state === "present" ? configToml.text : null);
53542
+ return check(id, finding.status, finding.detail, {
53543
+ fixable: finding.fixable,
53544
+ path: read.path,
53545
+ ...finding.status === "fail" && !finding.fixable && hookBlocked ? { action: "install_cli" } : {}
53546
+ });
53547
+ },
53548
+ repair: () => {
53549
+ const read = readCodexHooks(paths.codexHome);
53550
+ if (!read.ok || !cli || hookBlocked) return false;
53551
+ const repaired = repairCodexHook(read.settings, cli);
53552
+ if (!repaired.changed) return false;
53553
+ writeCodexHooks({
53554
+ read: read.read,
53555
+ settings: repaired.settings,
53556
+ now
53557
+ });
53558
+ return true;
53559
+ }
53560
+ };
53561
+ const orphans = [];
53562
+ const inspectProfiles = async () => {
53563
+ orphans.length = 0;
53564
+ const directory = join(agentProfilesDirectory(options.configDir), "profiles");
53565
+ let names = [];
53566
+ try {
53567
+ names = readdirSync(directory).filter((name) => {
53568
+ try {
53569
+ agentProfileName(name);
53570
+ return true;
53571
+ } catch {
53572
+ return false;
53573
+ }
53574
+ });
53575
+ } catch {
53576
+ names = [];
53577
+ }
53578
+ const profiles = [];
53579
+ let unreadable = 0;
53580
+ for (const name of names) try {
53581
+ const profile = loadConnectedAgentProfile(options.configDir, name);
53582
+ if (profile) profiles.push({
53583
+ name,
53584
+ profile
53585
+ });
53586
+ } catch {
53587
+ unreadable++;
53588
+ }
53589
+ if (!profiles.length) return unreadable ? check("profiles.orphaned", "warn", `${unreadable} saved profiles could not be read; they were left unchanged.`) : check("profiles.orphaned", "ok", "No saved agent profiles.");
53590
+ let member;
53591
+ let unconfirmed = 0;
53592
+ let unknown = 0;
53593
+ let connected = 0;
53594
+ const states = [];
53595
+ for (let start = 0; start < profiles.length; start += 8) states.push(...await Promise.all(profiles.slice(start, start + 8).map(async (entry) => ({
53596
+ ...entry,
53597
+ state: await agentConnectionState(entry.profile, fetchImpl)
53598
+ }))));
53599
+ for (const entry of states) if (entry.state === "revoked") orphans.push(entry);
53600
+ else if (entry.state === "rejected") {
53601
+ member ??= await memberCredentialsFor(options);
53602
+ if ((member && member.org_id === entry.profile.org_id && member.api_base_url === entry.profile.api_base_url ? await ownerListedStatus(member, entry.profile.agent_address, fetchImpl) : null) === "revoked") orphans.push(entry);
53603
+ else unconfirmed++;
53604
+ } else if (entry.state === "unavailable") unknown++;
53605
+ else connected++;
53606
+ if (orphans.length) return check("profiles.orphaned", "fail", `${orphans.length} saved profiles were disconnected in Primitive: ${orphans.map((entry) => entry.profile.agent_address).join(", ")}. A repair moves them aside locally; nothing changes in Primitive.`, { fixable: true });
53607
+ if (unconfirmed || unreadable) return check("profiles.orphaned", "warn", [unconfirmed ? `${unconfirmed} profiles' credentials were rejected but disconnection could not be confirmed` : "", unreadable ? `${unreadable} profiles could not be read` : ""].filter(Boolean).join("; ").concat("; they were left unchanged."));
53608
+ if (unknown && !connected) return check("profiles.orphaned", "skip", "Primitive could not be reached to check saved profiles.");
53609
+ return check("profiles.orphaned", "ok", `${connected} saved profiles are connected${unknown ? `; ${unknown} could not be checked` : ""}.`);
53610
+ };
53611
+ const moveOrphans = async () => {
53612
+ let moved = 0;
53613
+ for (const entry of orphans) {
53614
+ const directory = agentProfileDirectory(options.configDir, entry.name);
53615
+ let session = null;
53616
+ try {
53617
+ const setup = readMailJson(join(directory, "setup.json"));
53618
+ if (typeof setup?.session === "string" && SESSION_UUID$1.test(setup.session)) session = setup.session.toLowerCase();
53619
+ } catch {
53620
+ session = null;
53621
+ }
53622
+ if (session) {
53623
+ const stopped = await (options.stopReceiver ?? stopBackgroundListen)({
53624
+ configDir: options.configDir,
53625
+ scope: notificationScope(entry.profile.api_base_url, entry.profile.api_key),
53626
+ threadId: session
53627
+ }).catch(() => null);
53628
+ if (!stopped || !(stopped.phase === null || stopped.phase === "stopped" || stopped.phase === "failed" || stopped.reason === "exited")) continue;
53629
+ }
53630
+ const aside = join(agentProfilesDirectory(options.configDir), "orphaned");
53631
+ mkdirSync(aside, {
53632
+ recursive: true,
53633
+ mode: 448
53634
+ });
53635
+ renameSync(directory, join(aside, `${entry.name}-${backupStamp(now())}`));
53636
+ moved++;
53637
+ }
53638
+ return moved > 0;
53639
+ };
53640
+ const runners = {
53641
+ "cli.installed": { inspect: () => cli ? check("cli.installed", "ok", `primitive ${options.cliVersion} at ${cli.entry}.`, { path: cli.entry }) : check("cli.installed", "fail", "The running CLI's files could not be located.", { action: "install_cli" }) },
53642
+ "cli.version": { inspect: async () => {
53643
+ const version = options.cliVersion;
53644
+ if (options.minCliVersion) {
53645
+ const compared = compareVersions(version, options.minCliVersion);
53646
+ if (compared === null) return check("cli.version", "warn", `Cannot compare ${version} with the required ${options.minCliVersion}.`);
53647
+ if (compared < 0) return check("cli.version", "fail", `${version} is older than the required ${options.minCliVersion}.`, { action: "update_cli" });
53648
+ }
53649
+ const latest = await (options.latestVersion ?? (() => npmLatestVersion(fetchImpl)))();
53650
+ if (!latest) return options.minCliVersion ? check("cli.version", "ok", `${version} meets the required ${options.minCliVersion}; the latest release could not be checked.`) : check("cli.version", "skip", `${version}; the latest release could not be checked.`);
53651
+ const behind = compareVersions(version, latest);
53652
+ if (behind !== null && behind < 0) return check("cli.version", "warn", `${version} is older than the latest release ${latest}.`, { action: "update_cli" });
53653
+ return check("cli.version", "ok", `${version} is current.`);
53654
+ } },
53655
+ "cli.path_stable": { inspect: () => {
53656
+ if (!cli) return check("cli.path_stable", "fail", "The running CLI's files could not be located.", { action: "install_cli" });
53657
+ if (ephemeral) return check("cli.path_stable", "fail", `This CLI runs from ${ephemeral}; hooks pointing at it would break. Install it globally with \`npm install -g primitive\`.`, {
53658
+ action: "install_cli",
53659
+ path: cli.entry
53660
+ });
53661
+ const onPath = findOnPath("primitive", env);
53662
+ if (onPath && !samePath(onPath, cli.entry)) return check("cli.path_stable", "warn", `\`primitive\` on PATH is ${onPath}, but this run is ${cli.entry}; hooks follow the copy that runs the doctor.`, { path: cli.entry });
53663
+ return check("cli.path_stable", "ok", `Hooks use ${cli.node} and ${cli.entry}.`, { path: cli.entry });
53664
+ } },
53665
+ "auth.member_login": { inspect: async () => {
53666
+ let saved;
53667
+ try {
53668
+ saved = loadCliCredentials(options.configDir);
53669
+ } catch {
53670
+ return check("auth.member_login", "fail", "The saved login file is unreadable. Sign in again.", { action: "login" });
53671
+ }
53672
+ if (!saved) return check("auth.member_login", "fail", "No saved member login on this machine.", { action: "login" });
53673
+ let credentials;
53674
+ try {
53675
+ credentials = await refreshStoredCliCredentials({
53676
+ apiBaseUrl: saved.api_base_url,
53677
+ configDir: options.configDir,
53678
+ credentials: saved,
53679
+ fetch: options.fetch
53680
+ });
53681
+ } catch (error) {
53682
+ return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again." ? check("auth.member_login", "fail", "The saved login expired or was revoked.", { action: "login" }) : check("auth.member_login", "warn", "The saved login could not be refreshed right now (offline?).");
53683
+ }
53684
+ try {
53685
+ const response = await fetchImpl(`${credentials.api_base_url}/account`, {
53686
+ method: "GET",
53687
+ redirect: "error",
53688
+ signal: AbortSignal.timeout(5e3),
53689
+ headers: {
53690
+ authorization: `Bearer ${credentials.access_token}`,
53691
+ accept: "application/json"
53692
+ }
53693
+ });
53694
+ await response.body?.cancel().catch(() => void 0);
53695
+ if (response.status === 401 || response.status === 403) return check("auth.member_login", "fail", "The saved login was rejected.", { action: "login" });
53696
+ if (!response.ok) return check("auth.member_login", "warn", `The saved login could not be verified (HTTP ${response.status}).`);
53697
+ } catch {
53698
+ return check("auth.member_login", "warn", "The saved login could not be verified right now (offline?).");
53699
+ }
53700
+ return check("auth.member_login", "ok", `Signed in to ${credentials.org_name ?? credentials.org_id}.`);
53701
+ } },
53702
+ "claude.settings_valid": { inspect: () => {
53703
+ const skip = skipRuntime("claude");
53704
+ if (skip) return check("claude.settings_valid", "skip", skip);
53705
+ const path = join(paths.claudeDir, "settings.json");
53706
+ if (!settingsRead.ok) return check("claude.settings_valid", "fail", settingsRead.detail, {
53707
+ path,
53708
+ action: "edit_file"
53709
+ });
53710
+ return check("claude.settings_valid", "ok", settingsRead.read.state === "absent" ? `${path} does not exist yet; a repair creates it.` : `${path} is valid JSON.`, { path });
53711
+ } },
53712
+ "claude.hook.session_start": hookCheck("claude.hook.session_start"),
53713
+ "claude.hook.session_end": hookCheck("claude.hook.session_end"),
53714
+ "claude.hook.stop": hookCheck("claude.hook.stop"),
53715
+ "codex.hook.session_start": codexHook,
53716
+ "claude.instructions": instruction("claude.instructions", "claude", () => join(paths.claudeDir, "CLAUDE.md")),
53717
+ "codex.instructions": instruction("codex.instructions", "codex", () => codexInstructionsPath(paths.codexHome)),
53718
+ "omp.instructions": instruction("omp.instructions", "omp", () => join(paths.ompAgentDir, "AGENTS.md")),
53719
+ "skill.claude": skill("skill.claude", "claude"),
53720
+ "skill.codex": skill("skill.codex", "codex"),
53721
+ "profiles.orphaned": {
53722
+ inspect: async () => {
53723
+ const result = await inspectProfiles();
53724
+ const pending = pendingSessionDisconnects(options.configDir);
53725
+ if (!pending.length) return result;
53726
+ const addresses = pending.flatMap((record) => record.address ? [record.address] : []);
53727
+ return check("profiles.orphaned", "fail", `${pending.length} ended sessions still need their agent disconnected${addresses.length ? ` (${addresses.join(", ")})` : ""}; a repair retries it.${result.status === "ok" ? "" : ` ${result.detail}`}`, { fixable: true });
53728
+ },
53729
+ repair: async () => {
53730
+ const retried = await retryPendingDisconnects(options.configDir, {
53731
+ env,
53732
+ fetch: options.fetch,
53733
+ ...options.sessionDisconnect ? { dependencies: options.sessionDisconnect } : {}
53734
+ });
53735
+ return await moveOrphans() || retried > 0;
53736
+ }
53737
+ }
53738
+ };
53739
+ let release;
53740
+ let busy = false;
53741
+ if (options.fix) {
53742
+ for (let attempt = 0; attempt < 40 && !release; attempt++) try {
53743
+ release = acquireListenLock(join(options.configDir, "machine"), "machine-doctor");
53744
+ } catch {
53745
+ await new Promise((done) => setTimeout(done, 250));
53746
+ }
53747
+ busy = !release;
53748
+ }
53749
+ const results = /* @__PURE__ */ new Map();
53750
+ try {
53751
+ const order = ["profiles.orphaned", ...DOCTOR_CHECK_IDS.filter((id) => id !== "profiles.orphaned")];
53752
+ for (const id of order) {
53753
+ const runner = runners[id];
53754
+ let result;
53755
+ try {
53756
+ result = await runner.inspect();
53757
+ } catch {
53758
+ result = check(id, "fail", "This check could not run.");
53759
+ }
53760
+ if (result.status === "ok" && repairedAhead.has(id)) result = {
53761
+ ...result,
53762
+ fixed: true
53763
+ };
53764
+ const wanted = !options.only || options.only.has(id);
53765
+ if (options.fix && !busy && wanted && result.fixable && result.status !== "ok" && runner.repair) try {
53766
+ const changed = await runner.repair(result);
53767
+ const after = await runner.inspect();
53768
+ result = {
53769
+ ...after,
53770
+ fixed: changed && after.status !== "fail"
53771
+ };
53772
+ if (changed && after.status === "fail") result.detail = `${after.detail} (A repair was applied but did not resolve this.)`;
53773
+ } catch (error) {
53774
+ result = {
53775
+ ...result,
53776
+ fixed: false,
53777
+ detail: `${result.detail} Repair failed: ${error instanceof MachineFileError ? error.message : "an unexpected error; nothing further was changed."}`
53778
+ };
53779
+ }
53780
+ results.set(id, result);
53781
+ }
53782
+ } finally {
53783
+ release?.();
53784
+ }
53785
+ const checks = DOCTOR_CHECK_IDS.map((id) => results.get(id) ?? check(id, "fail", "This check did not run."));
53786
+ const summary = {
53787
+ ok: 0,
53788
+ warn: 0,
53789
+ fail: 0,
53790
+ skip: 0
53791
+ };
53792
+ for (const item of checks) summary[item.status]++;
53793
+ return {
53794
+ version: 1,
53795
+ cliVersion: options.cliVersion,
53796
+ checks,
53797
+ summary,
53798
+ fixedCount: checks.filter((item) => item.fixed).length,
53799
+ ...busy ? { repair: "skipped_busy" } : {}
53800
+ };
53801
+ }
53802
+ //#endregion
53803
+ //#region src/oclif/commands/machine-doctor.ts
53804
+ function renderCheck(check) {
53805
+ const tag = check.status === "ok" ? "[OK] " : check.status === "warn" ? "[WARN]" : check.status === "skip" ? "[SKIP]" : "[FAIL]";
53806
+ const fixed = check.fixed ? " (fixed)" : "";
53807
+ return `${tag} ${check.id}${fixed}: ${check.detail}`;
53808
+ }
53809
+ var MachineDoctorCommand = class MachineDoctorCommand extends Command {
53810
+ static summary = "Check, and optionally repair, this machine's Primitive setup for coding agents";
53811
+ static description = "Checks everything that lets Claude Code, Codex and omp sessions on this machine register with Primitive and answer mail: the CLI install and version, the saved member login, Claude settings and Primitive's SessionStart, SessionEnd and per-session receive hooks, the managed instruction block in each runtime's global instructions file, the bundled primitive-connect skill, and saved agent profiles disconnected in Primitive. Each check reports ok, warn, fail or skip, whether it can be repaired automatically, and an action (login, install_cli, update_cli, edit_file) when only a person can fix it. --fix repairs what it safely can: it only adds, rewrites or removes Primitive-owned hooks, blocks and skill copies, backs up any existing file beside it as <file>.primitive-bak-<timestamp> before changing it, writes atomically, and changes nothing when nothing drifted, so it is safe to run on a timer. Malformed or hand-edited files it cannot parse are reported and left unchanged. Exits 0 when every check is ok or skipped, 1 otherwise.";
53812
+ static examples = [
53813
+ "<%= config.bin %> machine doctor",
53814
+ "<%= config.bin %> machine doctor --json",
53815
+ "<%= config.bin %> machine doctor --fix --json",
53816
+ "<%= config.bin %> machine doctor --fix --check claude.hook.stop --json",
53817
+ "<%= config.bin %> machine doctor --runtime claude,codex --min-cli-version 1.38.0 --json"
53818
+ ];
53819
+ static flags = {
53820
+ fix: Flags.boolean({ description: "Repair every fixable check (or only those named with --check)" }),
53821
+ check: Flags.string({
53822
+ description: "With --fix, repair only this check; repeatable. Every check is still reported",
53823
+ multiple: true,
53824
+ options: [...DOCTOR_CHECK_IDS]
53825
+ }),
53826
+ runtime: Flags.string({ description: "Comma-separated runtimes to check: claude, codex, omp. Defaults to every runtime found on this machine" }),
53827
+ "min-cli-version": Flags.string({ description: "Report cli.version as failing when this CLI is older than the given version" }),
53828
+ json: Flags.boolean({ description: "Print the report as JSON" })
53829
+ };
53830
+ async run() {
53831
+ const { flags } = await this.parse(MachineDoctorCommand);
53832
+ let runtimes;
53833
+ if (flags.runtime) {
53834
+ const names = flags.runtime.split(",").map((name) => name.trim().toLowerCase()).filter(Boolean);
53835
+ const unknown = names.filter((name) => !MACHINE_RUNTIMES.includes(name));
53836
+ if (unknown.length) this.error(`Unknown runtime ${unknown.join(", ")}. Use claude, codex or omp.`, { exit: 2 });
53837
+ runtimes = new Set(names);
53838
+ }
53839
+ if (flags.check?.length && !flags.fix) this.error("--check selects which checks --fix repairs; add --fix.", { exit: 2 });
53840
+ const report = await runMachineDoctor({
53841
+ fix: flags.fix,
53842
+ only: flags.check?.length ? new Set(flags.check) : void 0,
53843
+ runtimes,
53844
+ configDir: this.config.configDir,
53845
+ home: homedir(),
53846
+ env: process.env,
53847
+ packageRoot: this.config.root,
53848
+ cliVersion: this.config.version,
53849
+ cliEntry: process.argv[1] ?? "",
53850
+ minCliVersion: flags["min-cli-version"]
53851
+ });
53852
+ if (flags.json) this.log(JSON.stringify(report));
53853
+ else {
53854
+ for (const check of report.checks) this.log(renderCheck(check));
53855
+ this.log(`${report.summary.ok} ok, ${report.summary.warn} warn, ${report.summary.fail} fail, ${report.summary.skip} skipped${report.fixedCount ? `; ${report.fixedCount} fixed` : ""}.`);
53856
+ if (report.repair === "skipped_busy") this.log("Another repair was running, so nothing was changed.");
53857
+ else if (!flags.fix && report.checks.some((check) => check.fixable)) this.log("Run `primitive machine doctor --fix` to repair fixable checks.");
53858
+ }
53859
+ if (report.summary.fail || report.summary.warn) process.exitCode = 1;
53860
+ }
53861
+ };
53862
+ //#endregion
51339
53863
  //#region src/oclif/commands/memories.ts
51340
53864
  var MemoryCliInputError = class extends Error {};
51341
53865
  const API_KEY_FLAG = Flags.string({
@@ -57900,6 +60424,8 @@ const COMMANDS = {
57900
60424
  "agent:connect": AgentConnectCommand,
57901
60425
  "agent:disconnect": AgentDisconnectCommand,
57902
60426
  "agent:enroll": AgentEnrollCommand,
60427
+ "agent:session-register": AgentSessionRegisterCommand,
60428
+ "agent:session-end": AgentSessionEndCommand,
57903
60429
  "contacts:request": ContactsRequestCommand,
57904
60430
  "contacts:accept": ContactsAcceptCommand,
57905
60431
  "contacts:wait": ContactsWaitCommand,
@@ -57965,6 +60491,7 @@ const COMMANDS = {
57965
60491
  whoami: WhoamiCommand,
57966
60492
  signal: SignalCommand,
57967
60493
  doctor: DoctorCommand,
60494
+ "machine:doctor": MachineDoctorCommand,
57968
60495
  "emails:latest": EmailsLatestCommand,
57969
60496
  "emails:watch": EmailsWatchCommand,
57970
60497
  "emails:wait": EmailsWaitCommand,