primcli 1.40.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{connect-skill-DjKt8NC8.js → connect-skill-DuanRI34.js} +1 -1
- package/dist/oclif/auto-signal-worker.js +2 -2
- package/dist/oclif/index.js +2099 -1872
- package/dist/oclif/receiver-heal.js +1 -1
- package/dist/{sdk.gen-CgKZwHs3.js → sdk.gen-I4mhUuUW.js} +7 -0
- package/dist/{signal-command-C8Ph0ylh.js → signal-command-BFPKbatO.js} +2 -2
- package/package.json +1 -1
package/dist/oclif/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { a as REPEAT_IDLE_MAX_MINUTES, d as parseRepeatTick, f as preparePresenceAliveEmail, g as isEmailReceivedEvent, i as REPEAT_EVERY_MAX_MINUTES, l as parseInteractionEnvelope$1, m as repeatStopCommand, o as REPEAT_MAX_SENDS_MAX, p as prepareSignalEmail, s as buildRepeatStopBody, u as parsePresenceEnvelope, v as parseWebhookEvent, y as parseFromHeader } from "../repeats-CoK0tpCK-Bx7nmyRi.js";
|
|
2
2
|
import { a as agentProfilesDirectory, c as loadConnectedAgentProfile, d as requireDefaultLoginProfile, f as saveConnectedAgentProfile, i as agentProfileName, l as parseConnectedAgentProfile, n as AgentConnectionSetupError, o as connectedAgentIdentity, r as agentProfileDirectory, s as connectedApiBaseUrl, t as AGENT_PROFILE_ENV, u as parseOwnerMemberAddress } from "../connected-agent-profile-8tpOMdq3.js";
|
|
3
|
-
import { $ as putAgentContactPolicy, A as getRepeatingSend, B as listEmails, C as getEmail, Ct as updateFunction, D as getInboxStatus, Dt as updateWakeSchedule, E as getFunctionTestRunTrace, Et as updateWakeAuthorization, F as listAgentNetworks, G as listRoutes, H as listFunctionLogs, I as listContacts, J as listWakeDispatches, K as listSentEmails, L as listDefaultNetworkAgents, M as getThread, N as getWakeSchedule, O as getMemory, Ot as verifyAgentClaim, P as listAgentContacts, Q as putAgentContact, R as listDefaultNetworkMembers, S as getDefaultNetworkAgent, St as updateDefaultNetworkMember, T as getFunctionRouting, Tt as updateRoute, U as listFunctions, V as listEndpoints, W as listRepeatingSends, X as pollCliLogin, Y as listWakeSchedules, Z as pullWebhookEvent, _ as getAccount, _t as startAgentSignup, a as createEndpoint, at as resendAgentSignupVerification, b as getConversation, bt as testFunction, c as createWakeAuthorization, ct as searchEmails, d as deleteContact, dt as sendEmail, et as putContact, f as deleteMemory, ft as setFunctionRoute, gt as startAgentClaim, h as deleteWakeSchedule, ht as simulateRoute, i as completeWebhookEvent, it as replyToEmail, j as getSentEmail, k as getOrgRoutingTopology, kt as verifyAgentSignup, l as createWakeSchedule, lt as searchMemories, m as deleteWakeAuthorization, mt as setMemory, n as checkDefaultNetworkContactAdmission, nt as removeDefaultNetworkMember, o as createFunction, ot as runWakeSchedule, p as deleteRoute, pt as setFunctionSecret, q as listWakeAuthorizations, r as cliLogout, rt as reorderRoutes, s as createRoute, t as addDefaultNetworkMember, tt as redeemCreditCode, u as deleteAgentContact, ut as semanticSearch, v as getAgentContactPolicy, vt as startCliLogin, w as getFunction, wt as updateRepeatingSend, x as getCreditBalance, xt as unsetFunctionRoute, y as getContact, yt as stopRepeatFromEmail, z as listDomains } from "../sdk.gen-
|
|
3
|
+
import { $ as putAgentContactPolicy, A as getRepeatingSend, B as listEmails, C as getEmail, Ct as updateFunction, D as getInboxStatus, Dt as updateWakeSchedule, E as getFunctionTestRunTrace, Et as updateWakeAuthorization, F as listAgentNetworks, G as listRoutes, H as listFunctionLogs, I as listContacts, J as listWakeDispatches, K as listSentEmails, L as listDefaultNetworkAgents, M as getThread, N as getWakeSchedule, O as getMemory, Ot as verifyAgentClaim, P as listAgentContacts, Q as putAgentContact, R as listDefaultNetworkMembers, S as getDefaultNetworkAgent, St as updateDefaultNetworkMember, T as getFunctionRouting, Tt as updateRoute, U as listFunctions, V as listEndpoints, W as listRepeatingSends, X as pollCliLogin, Y as listWakeSchedules, Z as pullWebhookEvent, _ as getAccount, _t as startAgentSignup, a as createEndpoint, at as resendAgentSignupVerification, b as getConversation, bt as testFunction, c as createWakeAuthorization, ct as searchEmails, d as deleteContact, dt as sendEmail, et as putContact, f as deleteMemory, ft as setFunctionRoute, gt as startAgentClaim, h as deleteWakeSchedule, ht as simulateRoute, i as completeWebhookEvent, it as replyToEmail, j as getSentEmail, k as getOrgRoutingTopology, kt as verifyAgentSignup, l as createWakeSchedule, lt as searchMemories, m as deleteWakeAuthorization, mt as setMemory, n as checkDefaultNetworkContactAdmission, nt as removeDefaultNetworkMember, o as createFunction, ot as runWakeSchedule, p as deleteRoute, pt as setFunctionSecret, q as listWakeAuthorizations, r as cliLogout, rt as reorderRoutes, s as createRoute, t as addDefaultNetworkMember, tt as redeemCreditCode, u as deleteAgentContact, ut as semanticSearch, v as getAgentContactPolicy, vt as startCliLogin, w as getFunction, wt as updateRepeatingSend, x as getCreditBalance, xt as unsetFunctionRoute, y as getContact, yt as stopRepeatFromEmail, z as listDomains } from "../sdk.gen-I4mhUuUW.js";
|
|
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
|
-
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-
|
|
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-
|
|
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-BFPKbatO.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-DuanRI34.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-C8nMME8J.js";
|
|
10
10
|
import { t as guardJsonOutputCommands } from "../json-output-BVRgl6jf.js";
|
|
@@ -1695,8 +1695,15 @@ const openapiDocument = {
|
|
|
1695
1695
|
"get": {
|
|
1696
1696
|
"operationId": "getConversation",
|
|
1697
1697
|
"summary": "Get the conversation an email belongs to",
|
|
1698
|
-
"description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
|
|
1698
|
+
"description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n\nPass `since` for an incremental read. `since=start` returns the\nfull conversation plus a `cursor`. Sending that `cursor` back as\n`since` returns only the messages created or changed after it,\noldest first, with the same per-message shape, and a new\n`cursor`. Without `since` the response carries no `cursor` and\nno per-message `status`.\n",
|
|
1699
1699
|
"tags": ["Emails"],
|
|
1700
|
+
"parameters": [{
|
|
1701
|
+
"name": "since",
|
|
1702
|
+
"in": "query",
|
|
1703
|
+
"required": false,
|
|
1704
|
+
"schema": { "type": "string" },
|
|
1705
|
+
"description": "Incremental read position. Either the literal `start` or a\n`cursor` returned by a previous read of this endpoint. Any\nother value returns 400 `validation_error`.\n\nWith `start`, the response holds every message the read\nwithout `since` would return, plus `cursor`. With a cursor,\n`messages` holds only the messages created or changed after\nit, oldest first. A change includes an inbound message\nbecoming visible or its body being filled, withheld or\ndiscarded, and a delivery status change on an outbound\nmessage. `thread_id`, `subject`, `message_count` and\n`truncated` always describe the whole conversation, not the\nreturned messages.\n\nThe cursor is an opaque string. Send it back verbatim and do\nnot parse it or build one; its format can change. A message\ncan be returned again by a later read (for example while\nolder writes are still committing), so upsert messages by\n`id`. Deleted messages are not reported; a periodic\n`since=start` read (or a read without `since`) reconciles.\n"
|
|
1706
|
+
}],
|
|
1700
1707
|
"responses": {
|
|
1701
1708
|
"200": {
|
|
1702
1709
|
"description": "Conversation",
|
|
@@ -12247,6 +12254,10 @@ const openapiDocument = {
|
|
|
12247
12254
|
"messages": {
|
|
12248
12255
|
"type": "array",
|
|
12249
12256
|
"items": { "$ref": "#/components/schemas/ConversationMessage" }
|
|
12257
|
+
},
|
|
12258
|
+
"cursor": {
|
|
12259
|
+
"type": "string",
|
|
12260
|
+
"description": "Present only on a `since` read: the position to send as\n`since` on the next read. An opaque string: send it back\nverbatim and do not parse it or build one.\n"
|
|
12250
12261
|
}
|
|
12251
12262
|
},
|
|
12252
12263
|
"required": [
|
|
@@ -12258,7 +12269,7 @@ const openapiDocument = {
|
|
|
12258
12269
|
},
|
|
12259
12270
|
"ConversationMessage": {
|
|
12260
12271
|
"type": "object",
|
|
12261
|
-
"description": "One message in the conversation, with its body and a chat role
|
|
12272
|
+
"description": "One message in the conversation, with its body and a chat role.\n`status` is the delivery status of an outbound message, as on\n`/sent-emails/{id}`, and is present only on a `since` read and\nonly on outbound messages.\n",
|
|
12262
12273
|
"properties": {
|
|
12263
12274
|
"role": {
|
|
12264
12275
|
"type": "string",
|
|
@@ -12287,397 +12298,398 @@ const openapiDocument = {
|
|
|
12287
12298
|
"format": "date-time",
|
|
12288
12299
|
"description": "received_at for inbound, created_at for outbound."
|
|
12289
12300
|
},
|
|
12290
|
-
"presence_control": { "$ref": "#/components/schemas/PresenceControl" },
|
|
12291
|
-
"repeat": {
|
|
12292
|
-
"type": ["object", "null"],
|
|
12293
|
-
"description": "Set when Primitive sent this message as part of a repeating send. Resolved from Primitive's own records, never from the message content. Null on every other message and on servers that predate repeating sends.",
|
|
12294
|
-
"properties": {
|
|
12295
|
-
"repeat_id": {
|
|
12296
|
-
"type": "string",
|
|
12297
|
-
"format": "uuid"
|
|
12298
|
-
},
|
|
12299
|
-
"sequence": {
|
|
12300
|
-
"type": "integer",
|
|
12301
|
-
"minimum": 1
|
|
12302
|
-
}
|
|
12303
|
-
},
|
|
12304
|
-
"required": ["repeat_id", "sequence"]
|
|
12305
|
-
},
|
|
12306
|
-
"sender_member": {
|
|
12307
|
-
"type": ["object", "null"],
|
|
12308
|
-
"description": "Verified human authorship, projected only within the member organization. Historical attribution is not current sending or owner authority.",
|
|
12309
|
-
"properties": {
|
|
12310
|
-
"address": {
|
|
12311
|
-
"type": "string",
|
|
12312
|
-
"format": "email"
|
|
12313
|
-
},
|
|
12314
|
-
"user_id": { "type": "string" },
|
|
12315
|
-
"name": { "type": ["string", "null"] }
|
|
12316
|
-
},
|
|
12317
|
-
"required": [
|
|
12318
|
-
"address",
|
|
12319
|
-
"user_id",
|
|
12320
|
-
"name"
|
|
12321
|
-
]
|
|
12322
|
-
}
|
|
12323
|
-
},
|
|
12324
|
-
"required": [
|
|
12325
|
-
"role",
|
|
12326
|
-
"direction",
|
|
12327
|
-
"id",
|
|
12328
|
-
"text"
|
|
12329
|
-
]
|
|
12330
|
-
},
|
|
12331
|
-
"SendMailAttachment": {
|
|
12332
|
-
"type": "object",
|
|
12333
|
-
"additionalProperties": false,
|
|
12334
|
-
"properties": {
|
|
12335
|
-
"filename": {
|
|
12336
|
-
"type": "string",
|
|
12337
|
-
"minLength": 1,
|
|
12338
|
-
"maxLength": 255,
|
|
12339
|
-
"description": "Attachment filename. Control characters are rejected."
|
|
12340
|
-
},
|
|
12341
|
-
"content_type": {
|
|
12342
|
-
"type": "string",
|
|
12343
|
-
"minLength": 1,
|
|
12344
|
-
"maxLength": 255,
|
|
12345
|
-
"description": "Optional MIME content type. Control characters are rejected."
|
|
12346
|
-
},
|
|
12347
|
-
"content_base64": {
|
|
12348
|
-
"type": "string",
|
|
12349
|
-
"minLength": 1,
|
|
12350
|
-
"maxLength": 44040192,
|
|
12351
|
-
"description": "Base64-encoded attachment bytes."
|
|
12352
|
-
}
|
|
12353
|
-
},
|
|
12354
|
-
"required": ["filename", "content_base64"]
|
|
12355
|
-
},
|
|
12356
|
-
"SendMailPayloadRef": {
|
|
12357
|
-
"type": "object",
|
|
12358
|
-
"additionalProperties": false,
|
|
12359
|
-
"description": "A reference to an already-uploaded Primitive Payloads object, delivered as an attachment without inlining the bytes - the way to send an attachment larger than the inline cap. Upload the object via /v1/payloads (with a client-held CEK the server never sees), then reference it here.",
|
|
12360
|
-
"properties": {
|
|
12361
|
-
"root": {
|
|
12362
|
-
"type": "string",
|
|
12363
|
-
"pattern": "^[0-9a-f]{64}$",
|
|
12364
|
-
"description": "The 64-char lowercase-hex Merkle root of a finalized payloads object."
|
|
12365
|
-
},
|
|
12366
|
-
"filename": {
|
|
12367
|
-
"type": "string",
|
|
12368
|
-
"minLength": 1,
|
|
12369
|
-
"maxLength": 255,
|
|
12370
|
-
"description": "Attachment filename presented to the recipient."
|
|
12371
|
-
},
|
|
12372
|
-
"content_type": {
|
|
12373
|
-
"type": "string",
|
|
12374
|
-
"minLength": 1,
|
|
12375
|
-
"maxLength": 255,
|
|
12376
|
-
"description": "Optional MIME content type."
|
|
12377
|
-
},
|
|
12378
|
-
"cek": {
|
|
12379
|
-
"type": "string",
|
|
12380
|
-
"pattern": "^[A-Za-z0-9_-]{1,128}$",
|
|
12381
|
-
"description": "Base64url-encoded (unpadded) content-encryption key the recipient uses to decrypt. Travels with the email; the object store only ever holds ciphertext."
|
|
12382
|
-
}
|
|
12383
|
-
},
|
|
12384
|
-
"required": [
|
|
12385
|
-
"root",
|
|
12386
|
-
"filename",
|
|
12387
|
-
"cek"
|
|
12388
|
-
]
|
|
12389
|
-
},
|
|
12390
|
-
"SendMailInput": {
|
|
12391
|
-
"type": "object",
|
|
12392
|
-
"additionalProperties": false,
|
|
12393
|
-
"properties": {
|
|
12394
|
-
"repeat": { "$ref": "#/components/schemas/RepeatInput" },
|
|
12395
|
-
"from": {
|
|
12396
|
-
"type": "string",
|
|
12397
|
-
"minLength": 3,
|
|
12398
|
-
"maxLength": 998,
|
|
12399
|
-
"description": "RFC 5322 From header. The sender domain must be a verified outbound domain for your organization."
|
|
12400
|
-
},
|
|
12401
|
-
"to": {
|
|
12402
|
-
"type": "string",
|
|
12403
|
-
"minLength": 3,
|
|
12404
|
-
"maxLength": 320,
|
|
12405
|
-
"description": "Recipient address. Recipient eligibility depends on your account's outbound entitlements."
|
|
12406
|
-
},
|
|
12407
|
-
"cc": {
|
|
12408
|
-
"description": "Carbon-copy recipients. Either a single address or an array of addresses. Each entry must be a single address; use the array form for multiple recipients. Cc recipients are visible to everyone who receives the message. The combined number of to, cc, and bcc recipients must not exceed 100.",
|
|
12409
|
-
"oneOf": [{
|
|
12410
|
-
"type": "string",
|
|
12411
|
-
"minLength": 1
|
|
12412
|
-
}, {
|
|
12413
|
-
"type": "array",
|
|
12414
|
-
"minItems": 1,
|
|
12415
|
-
"maxItems": 100,
|
|
12416
|
-
"items": {
|
|
12417
|
-
"type": "string",
|
|
12418
|
-
"minLength": 1
|
|
12419
|
-
}
|
|
12420
|
-
}]
|
|
12421
|
-
},
|
|
12422
|
-
"bcc": {
|
|
12423
|
-
"description": "Blind-carbon-copy recipients. Either a single address or an array of addresses. Each entry must be a single address; use the array form for multiple recipients. Bcc recipients receive the message but are not disclosed to the other recipients: no Bcc header is sent. The combined number of to, cc, and bcc recipients must not exceed 100.",
|
|
12424
|
-
"oneOf": [{
|
|
12425
|
-
"type": "string",
|
|
12426
|
-
"minLength": 1
|
|
12427
|
-
}, {
|
|
12428
|
-
"type": "array",
|
|
12429
|
-
"minItems": 1,
|
|
12430
|
-
"maxItems": 100,
|
|
12431
|
-
"items": {
|
|
12432
|
-
"type": "string",
|
|
12433
|
-
"minLength": 1
|
|
12434
|
-
}
|
|
12435
|
-
}]
|
|
12436
|
-
},
|
|
12437
|
-
"subject": {
|
|
12438
|
-
"type": "string",
|
|
12439
|
-
"minLength": 1,
|
|
12440
|
-
"maxLength": 998,
|
|
12441
|
-
"description": "Subject line for the outbound message"
|
|
12442
|
-
},
|
|
12443
|
-
"body_text": {
|
|
12444
|
-
"type": "string",
|
|
12445
|
-
"description": "Plain-text message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes."
|
|
12446
|
-
},
|
|
12447
|
-
"body_html": {
|
|
12448
|
-
"type": "string",
|
|
12449
|
-
"description": "HTML message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes."
|
|
12450
|
-
},
|
|
12451
|
-
"in_reply_to": {
|
|
12452
|
-
"type": "string",
|
|
12453
|
-
"minLength": 1,
|
|
12454
|
-
"maxLength": 998,
|
|
12455
|
-
"pattern": "^[^\\x00-\\x1F\\x7F]+$",
|
|
12456
|
-
"description": "Message-ID of the direct parent email when sending a threaded reply."
|
|
12457
|
-
},
|
|
12458
|
-
"references": {
|
|
12459
|
-
"type": "array",
|
|
12460
|
-
"maxItems": 100,
|
|
12461
|
-
"description": "Full ordered message-id chain for the thread.",
|
|
12462
|
-
"items": {
|
|
12463
|
-
"type": "string",
|
|
12464
|
-
"minLength": 1,
|
|
12465
|
-
"maxLength": 998,
|
|
12466
|
-
"pattern": "^[^\\x00-\\x1F\\x7F]+$"
|
|
12467
|
-
}
|
|
12468
|
-
},
|
|
12469
|
-
"attachments": {
|
|
12470
|
-
"type": "array",
|
|
12471
|
-
"maxItems": 100,
|
|
12472
|
-
"description": "Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.",
|
|
12473
|
-
"items": { "$ref": "#/components/schemas/SendMailAttachment" }
|
|
12474
|
-
},
|
|
12475
|
-
"payload_attachments": {
|
|
12476
|
-
"type": "array",
|
|
12477
|
-
"maxItems": 1,
|
|
12478
|
-
"description": "Deliver an already-uploaded Primitive Payloads object as an attachment by reference, without inlining the bytes - the way to send attachments larger than the inline cap. Upload the object via /v1/payloads (client-held CEK), then reference it here. v1 supports at most one.",
|
|
12479
|
-
"items": { "$ref": "#/components/schemas/SendMailPayloadRef" }
|
|
12480
|
-
},
|
|
12481
|
-
"wait": {
|
|
12482
|
-
"type": "boolean",
|
|
12483
|
-
"description": "When true, wait for the first downstream SMTP delivery outcome before returning."
|
|
12484
|
-
},
|
|
12485
|
-
"wait_timeout_ms": {
|
|
12486
|
-
"type": "integer",
|
|
12487
|
-
"minimum": 1e3,
|
|
12488
|
-
"maximum": 3e4,
|
|
12489
|
-
"description": "Maximum time to wait for a delivery outcome when wait is true. Defaults to 30000."
|
|
12490
|
-
},
|
|
12491
|
-
"scheduled_at": {
|
|
12492
|
-
"type": "string",
|
|
12493
|
-
"format": "date-time",
|
|
12494
|
-
"description": "Optional future execution time (ISO 8601). When set, the\nsend is recorded with status `scheduled` and executed at\nthe requested time instead of immediately. Must be in the\nfuture and at most 30 days out. Incompatible with `wait`\n(a scheduled send resolves after this request completes)\nand with `attachments` / `payload_attachments` (not yet\nsupported on scheduled sends). Reschedule via PATCH\n/sent-emails/{id}; cancel via /sent-emails/{id}/cancel.\n"
|
|
12495
|
-
}
|
|
12496
|
-
},
|
|
12497
|
-
"required": [
|
|
12498
|
-
"from",
|
|
12499
|
-
"to",
|
|
12500
|
-
"subject"
|
|
12501
|
-
]
|
|
12502
|
-
},
|
|
12503
|
-
"EmailStatus": {
|
|
12504
|
-
"type": "string",
|
|
12505
|
-
"description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
|
|
12506
|
-
"enum": [
|
|
12507
|
-
"pending",
|
|
12508
|
-
"accepted",
|
|
12509
|
-
"completed",
|
|
12510
|
-
"rejected"
|
|
12511
|
-
]
|
|
12512
|
-
},
|
|
12513
|
-
"EmailWebhookStatus": {
|
|
12514
|
-
"type": ["string", "null"],
|
|
12515
|
-
"description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
|
|
12516
|
-
"enum": [
|
|
12517
|
-
"pending",
|
|
12518
|
-
"in_flight",
|
|
12519
|
-
"fired",
|
|
12520
|
-
"failed",
|
|
12521
|
-
"exhausted",
|
|
12522
|
-
null
|
|
12523
|
-
]
|
|
12524
|
-
},
|
|
12525
|
-
"SentEmailStatus": {
|
|
12526
|
-
"type": "string",
|
|
12527
|
-
"description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n - `scheduled`: created with a future `scheduled_at` and\n not yet executed; `scheduled_at` carries the pending\n execution time. Reschedulable via PATCH\n /sent-emails/{id} and cancelable via\n /sent-emails/{id}/cancel while in this status.\n - `canceled`: terminal; a scheduled send canceled before\n execution. `canceled_at` carries the cancellation time\n and nothing was dispatched.\n",
|
|
12528
|
-
"enum": [
|
|
12529
|
-
"queued",
|
|
12530
|
-
"submitted_to_agent",
|
|
12531
|
-
"agent_failed",
|
|
12532
|
-
"gate_denied",
|
|
12533
|
-
"unknown",
|
|
12534
|
-
"delivered",
|
|
12535
|
-
"bounced",
|
|
12536
|
-
"deferred",
|
|
12537
|
-
"wait_timeout",
|
|
12538
|
-
"scheduled",
|
|
12539
|
-
"canceled"
|
|
12540
|
-
]
|
|
12541
|
-
},
|
|
12542
|
-
"DeliveryStatus": {
|
|
12543
|
-
"type": "string",
|
|
12544
|
-
"description": "Narrower enum covering only the four terminal delivery\noutcomes returned to a synchronous `wait: true` send.\n\nOn the SendMailResult shape, `delivery_status` is always\nequal to `status` whenever both are present (i.e. on\nterminal-state replays and live wait=true responses).\nThe two fields exist so callers that want to type-narrow\non \"this is a delivery outcome\" can pattern-match against\nthe four-value enum without handling the broader\nSentEmailStatus value set (which also covers `queued`,\n`submitted_to_agent`, `agent_failed`, `gate_denied`,\n`unknown`).\n\nOn async-mode and pre-terminal responses, `delivery_status`\nis absent and only `status` is populated. Use `status` if\nyou want a single field that's always present.\n",
|
|
12545
|
-
"enum": [
|
|
12546
|
-
"delivered",
|
|
12547
|
-
"bounced",
|
|
12548
|
-
"deferred",
|
|
12549
|
-
"wait_timeout"
|
|
12550
|
-
]
|
|
12551
|
-
},
|
|
12552
|
-
"SentEmailSummary": {
|
|
12553
|
-
"type": "object",
|
|
12554
|
-
"description": "List-row projection of a sent-email record. Drops\n`body_text` and `body_html` to keep paginated responses\nsmall; fetch /sent-emails/{id} for the full record with\nbodies.\n",
|
|
12555
|
-
"properties": {
|
|
12556
|
-
"id": {
|
|
12557
|
-
"type": "string",
|
|
12558
|
-
"format": "uuid"
|
|
12559
|
-
},
|
|
12560
12301
|
"status": { "$ref": "#/components/schemas/SentEmailStatus" },
|
|
12561
|
-
"
|
|
12562
|
-
|
|
12563
|
-
"
|
|
12564
|
-
"description": "
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
"
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
"
|
|
12580
|
-
"
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
"
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
}
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
"
|
|
12599
|
-
"
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
"
|
|
12610
|
-
"description": "
|
|
12611
|
-
},
|
|
12612
|
-
"
|
|
12613
|
-
"type":
|
|
12614
|
-
"
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
"
|
|
12622
|
-
"
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
"
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
"
|
|
12640
|
-
"
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
"
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
"
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
"
|
|
12652
|
-
"description": "
|
|
12653
|
-
}
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
"
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
"
|
|
12669
|
-
"
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
"
|
|
12674
|
-
"
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
"description": "
|
|
12680
|
-
|
|
12302
|
+
"presence_control": { "$ref": "#/components/schemas/PresenceControl" },
|
|
12303
|
+
"repeat": {
|
|
12304
|
+
"type": ["object", "null"],
|
|
12305
|
+
"description": "Set when Primitive sent this message as part of a repeating send. Resolved from Primitive's own records, never from the message content. Null on every other message and on servers that predate repeating sends.",
|
|
12306
|
+
"properties": {
|
|
12307
|
+
"repeat_id": {
|
|
12308
|
+
"type": "string",
|
|
12309
|
+
"format": "uuid"
|
|
12310
|
+
},
|
|
12311
|
+
"sequence": {
|
|
12312
|
+
"type": "integer",
|
|
12313
|
+
"minimum": 1
|
|
12314
|
+
}
|
|
12315
|
+
},
|
|
12316
|
+
"required": ["repeat_id", "sequence"]
|
|
12317
|
+
},
|
|
12318
|
+
"sender_member": {
|
|
12319
|
+
"type": ["object", "null"],
|
|
12320
|
+
"description": "Verified human authorship, projected only within the member organization. Historical attribution is not current sending or owner authority.",
|
|
12321
|
+
"properties": {
|
|
12322
|
+
"address": {
|
|
12323
|
+
"type": "string",
|
|
12324
|
+
"format": "email"
|
|
12325
|
+
},
|
|
12326
|
+
"user_id": { "type": "string" },
|
|
12327
|
+
"name": { "type": ["string", "null"] }
|
|
12328
|
+
},
|
|
12329
|
+
"required": [
|
|
12330
|
+
"address",
|
|
12331
|
+
"user_id",
|
|
12332
|
+
"name"
|
|
12333
|
+
]
|
|
12334
|
+
}
|
|
12335
|
+
},
|
|
12336
|
+
"required": [
|
|
12337
|
+
"role",
|
|
12338
|
+
"direction",
|
|
12339
|
+
"id",
|
|
12340
|
+
"text"
|
|
12341
|
+
]
|
|
12342
|
+
},
|
|
12343
|
+
"SendMailAttachment": {
|
|
12344
|
+
"type": "object",
|
|
12345
|
+
"additionalProperties": false,
|
|
12346
|
+
"properties": {
|
|
12347
|
+
"filename": {
|
|
12348
|
+
"type": "string",
|
|
12349
|
+
"minLength": 1,
|
|
12350
|
+
"maxLength": 255,
|
|
12351
|
+
"description": "Attachment filename. Control characters are rejected."
|
|
12352
|
+
},
|
|
12353
|
+
"content_type": {
|
|
12354
|
+
"type": "string",
|
|
12355
|
+
"minLength": 1,
|
|
12356
|
+
"maxLength": 255,
|
|
12357
|
+
"description": "Optional MIME content type. Control characters are rejected."
|
|
12358
|
+
},
|
|
12359
|
+
"content_base64": {
|
|
12360
|
+
"type": "string",
|
|
12361
|
+
"minLength": 1,
|
|
12362
|
+
"maxLength": 44040192,
|
|
12363
|
+
"description": "Base64-encoded attachment bytes."
|
|
12364
|
+
}
|
|
12365
|
+
},
|
|
12366
|
+
"required": ["filename", "content_base64"]
|
|
12367
|
+
},
|
|
12368
|
+
"SendMailPayloadRef": {
|
|
12369
|
+
"type": "object",
|
|
12370
|
+
"additionalProperties": false,
|
|
12371
|
+
"description": "A reference to an already-uploaded Primitive Payloads object, delivered as an attachment without inlining the bytes - the way to send an attachment larger than the inline cap. Upload the object via /v1/payloads (with a client-held CEK the server never sees), then reference it here.",
|
|
12372
|
+
"properties": {
|
|
12373
|
+
"root": {
|
|
12374
|
+
"type": "string",
|
|
12375
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
12376
|
+
"description": "The 64-char lowercase-hex Merkle root of a finalized payloads object."
|
|
12377
|
+
},
|
|
12378
|
+
"filename": {
|
|
12379
|
+
"type": "string",
|
|
12380
|
+
"minLength": 1,
|
|
12381
|
+
"maxLength": 255,
|
|
12382
|
+
"description": "Attachment filename presented to the recipient."
|
|
12383
|
+
},
|
|
12384
|
+
"content_type": {
|
|
12385
|
+
"type": "string",
|
|
12386
|
+
"minLength": 1,
|
|
12387
|
+
"maxLength": 255,
|
|
12388
|
+
"description": "Optional MIME content type."
|
|
12389
|
+
},
|
|
12390
|
+
"cek": {
|
|
12391
|
+
"type": "string",
|
|
12392
|
+
"pattern": "^[A-Za-z0-9_-]{1,128}$",
|
|
12393
|
+
"description": "Base64url-encoded (unpadded) content-encryption key the recipient uses to decrypt. Travels with the email; the object store only ever holds ciphertext."
|
|
12394
|
+
}
|
|
12395
|
+
},
|
|
12396
|
+
"required": [
|
|
12397
|
+
"root",
|
|
12398
|
+
"filename",
|
|
12399
|
+
"cek"
|
|
12400
|
+
]
|
|
12401
|
+
},
|
|
12402
|
+
"SendMailInput": {
|
|
12403
|
+
"type": "object",
|
|
12404
|
+
"additionalProperties": false,
|
|
12405
|
+
"properties": {
|
|
12406
|
+
"repeat": { "$ref": "#/components/schemas/RepeatInput" },
|
|
12407
|
+
"from": {
|
|
12408
|
+
"type": "string",
|
|
12409
|
+
"minLength": 3,
|
|
12410
|
+
"maxLength": 998,
|
|
12411
|
+
"description": "RFC 5322 From header. The sender domain must be a verified outbound domain for your organization."
|
|
12412
|
+
},
|
|
12413
|
+
"to": {
|
|
12414
|
+
"type": "string",
|
|
12415
|
+
"minLength": 3,
|
|
12416
|
+
"maxLength": 320,
|
|
12417
|
+
"description": "Recipient address. Recipient eligibility depends on your account's outbound entitlements."
|
|
12418
|
+
},
|
|
12419
|
+
"cc": {
|
|
12420
|
+
"description": "Carbon-copy recipients. Either a single address or an array of addresses. Each entry must be a single address; use the array form for multiple recipients. Cc recipients are visible to everyone who receives the message. The combined number of to, cc, and bcc recipients must not exceed 100.",
|
|
12421
|
+
"oneOf": [{
|
|
12422
|
+
"type": "string",
|
|
12423
|
+
"minLength": 1
|
|
12424
|
+
}, {
|
|
12425
|
+
"type": "array",
|
|
12426
|
+
"minItems": 1,
|
|
12427
|
+
"maxItems": 100,
|
|
12428
|
+
"items": {
|
|
12429
|
+
"type": "string",
|
|
12430
|
+
"minLength": 1
|
|
12431
|
+
}
|
|
12432
|
+
}]
|
|
12433
|
+
},
|
|
12434
|
+
"bcc": {
|
|
12435
|
+
"description": "Blind-carbon-copy recipients. Either a single address or an array of addresses. Each entry must be a single address; use the array form for multiple recipients. Bcc recipients receive the message but are not disclosed to the other recipients: no Bcc header is sent. The combined number of to, cc, and bcc recipients must not exceed 100.",
|
|
12436
|
+
"oneOf": [{
|
|
12437
|
+
"type": "string",
|
|
12438
|
+
"minLength": 1
|
|
12439
|
+
}, {
|
|
12440
|
+
"type": "array",
|
|
12441
|
+
"minItems": 1,
|
|
12442
|
+
"maxItems": 100,
|
|
12443
|
+
"items": {
|
|
12444
|
+
"type": "string",
|
|
12445
|
+
"minLength": 1
|
|
12446
|
+
}
|
|
12447
|
+
}]
|
|
12448
|
+
},
|
|
12449
|
+
"subject": {
|
|
12450
|
+
"type": "string",
|
|
12451
|
+
"minLength": 1,
|
|
12452
|
+
"maxLength": 998,
|
|
12453
|
+
"description": "Subject line for the outbound message"
|
|
12454
|
+
},
|
|
12455
|
+
"body_text": {
|
|
12456
|
+
"type": "string",
|
|
12457
|
+
"description": "Plain-text message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes."
|
|
12458
|
+
},
|
|
12459
|
+
"body_html": {
|
|
12460
|
+
"type": "string",
|
|
12461
|
+
"description": "HTML message body. At least one of body_text or body_html is required. The combined UTF-8 byte length of body_text and body_html must be at most 262144 bytes."
|
|
12462
|
+
},
|
|
12463
|
+
"in_reply_to": {
|
|
12464
|
+
"type": "string",
|
|
12465
|
+
"minLength": 1,
|
|
12466
|
+
"maxLength": 998,
|
|
12467
|
+
"pattern": "^[^\\x00-\\x1F\\x7F]+$",
|
|
12468
|
+
"description": "Message-ID of the direct parent email when sending a threaded reply."
|
|
12469
|
+
},
|
|
12470
|
+
"references": {
|
|
12471
|
+
"type": "array",
|
|
12472
|
+
"maxItems": 100,
|
|
12473
|
+
"description": "Full ordered message-id chain for the thread.",
|
|
12474
|
+
"items": {
|
|
12475
|
+
"type": "string",
|
|
12476
|
+
"minLength": 1,
|
|
12477
|
+
"maxLength": 998,
|
|
12478
|
+
"pattern": "^[^\\x00-\\x1F\\x7F]+$"
|
|
12479
|
+
}
|
|
12480
|
+
},
|
|
12481
|
+
"attachments": {
|
|
12482
|
+
"type": "array",
|
|
12483
|
+
"maxItems": 100,
|
|
12484
|
+
"description": "Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.",
|
|
12485
|
+
"items": { "$ref": "#/components/schemas/SendMailAttachment" }
|
|
12486
|
+
},
|
|
12487
|
+
"payload_attachments": {
|
|
12488
|
+
"type": "array",
|
|
12489
|
+
"maxItems": 1,
|
|
12490
|
+
"description": "Deliver an already-uploaded Primitive Payloads object as an attachment by reference, without inlining the bytes - the way to send attachments larger than the inline cap. Upload the object via /v1/payloads (client-held CEK), then reference it here. v1 supports at most one.",
|
|
12491
|
+
"items": { "$ref": "#/components/schemas/SendMailPayloadRef" }
|
|
12492
|
+
},
|
|
12493
|
+
"wait": {
|
|
12494
|
+
"type": "boolean",
|
|
12495
|
+
"description": "When true, wait for the first downstream SMTP delivery outcome before returning."
|
|
12496
|
+
},
|
|
12497
|
+
"wait_timeout_ms": {
|
|
12498
|
+
"type": "integer",
|
|
12499
|
+
"minimum": 1e3,
|
|
12500
|
+
"maximum": 3e4,
|
|
12501
|
+
"description": "Maximum time to wait for a delivery outcome when wait is true. Defaults to 30000."
|
|
12502
|
+
},
|
|
12503
|
+
"scheduled_at": {
|
|
12504
|
+
"type": "string",
|
|
12505
|
+
"format": "date-time",
|
|
12506
|
+
"description": "Optional future execution time (ISO 8601). When set, the\nsend is recorded with status `scheduled` and executed at\nthe requested time instead of immediately. Must be in the\nfuture and at most 30 days out. Incompatible with `wait`\n(a scheduled send resolves after this request completes)\nand with `attachments` / `payload_attachments` (not yet\nsupported on scheduled sends). Reschedule via PATCH\n/sent-emails/{id}; cancel via /sent-emails/{id}/cancel.\n"
|
|
12507
|
+
}
|
|
12508
|
+
},
|
|
12509
|
+
"required": [
|
|
12510
|
+
"from",
|
|
12511
|
+
"to",
|
|
12512
|
+
"subject"
|
|
12513
|
+
]
|
|
12514
|
+
},
|
|
12515
|
+
"EmailStatus": {
|
|
12516
|
+
"type": "string",
|
|
12517
|
+
"description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
|
|
12518
|
+
"enum": [
|
|
12519
|
+
"pending",
|
|
12520
|
+
"accepted",
|
|
12521
|
+
"completed",
|
|
12522
|
+
"rejected"
|
|
12523
|
+
]
|
|
12524
|
+
},
|
|
12525
|
+
"EmailWebhookStatus": {
|
|
12526
|
+
"type": ["string", "null"],
|
|
12527
|
+
"description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
|
|
12528
|
+
"enum": [
|
|
12529
|
+
"pending",
|
|
12530
|
+
"in_flight",
|
|
12531
|
+
"fired",
|
|
12532
|
+
"failed",
|
|
12533
|
+
"exhausted",
|
|
12534
|
+
null
|
|
12535
|
+
]
|
|
12536
|
+
},
|
|
12537
|
+
"SentEmailStatus": {
|
|
12538
|
+
"type": "string",
|
|
12539
|
+
"description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n - `scheduled`: created with a future `scheduled_at` and\n not yet executed; `scheduled_at` carries the pending\n execution time. Reschedulable via PATCH\n /sent-emails/{id} and cancelable via\n /sent-emails/{id}/cancel while in this status.\n - `canceled`: terminal; a scheduled send canceled before\n execution. `canceled_at` carries the cancellation time\n and nothing was dispatched.\n",
|
|
12540
|
+
"enum": [
|
|
12541
|
+
"queued",
|
|
12542
|
+
"submitted_to_agent",
|
|
12543
|
+
"agent_failed",
|
|
12544
|
+
"gate_denied",
|
|
12545
|
+
"unknown",
|
|
12546
|
+
"delivered",
|
|
12547
|
+
"bounced",
|
|
12548
|
+
"deferred",
|
|
12549
|
+
"wait_timeout",
|
|
12550
|
+
"scheduled",
|
|
12551
|
+
"canceled"
|
|
12552
|
+
]
|
|
12553
|
+
},
|
|
12554
|
+
"DeliveryStatus": {
|
|
12555
|
+
"type": "string",
|
|
12556
|
+
"description": "Narrower enum covering only the four terminal delivery\noutcomes returned to a synchronous `wait: true` send.\n\nOn the SendMailResult shape, `delivery_status` is always\nequal to `status` whenever both are present (i.e. on\nterminal-state replays and live wait=true responses).\nThe two fields exist so callers that want to type-narrow\non \"this is a delivery outcome\" can pattern-match against\nthe four-value enum without handling the broader\nSentEmailStatus value set (which also covers `queued`,\n`submitted_to_agent`, `agent_failed`, `gate_denied`,\n`unknown`).\n\nOn async-mode and pre-terminal responses, `delivery_status`\nis absent and only `status` is populated. Use `status` if\nyou want a single field that's always present.\n",
|
|
12557
|
+
"enum": [
|
|
12558
|
+
"delivered",
|
|
12559
|
+
"bounced",
|
|
12560
|
+
"deferred",
|
|
12561
|
+
"wait_timeout"
|
|
12562
|
+
]
|
|
12563
|
+
},
|
|
12564
|
+
"SentEmailSummary": {
|
|
12565
|
+
"type": "object",
|
|
12566
|
+
"description": "List-row projection of a sent-email record. Drops\n`body_text` and `body_html` to keep paginated responses\nsmall; fetch /sent-emails/{id} for the full record with\nbodies.\n",
|
|
12567
|
+
"properties": {
|
|
12568
|
+
"id": {
|
|
12569
|
+
"type": "string",
|
|
12570
|
+
"format": "uuid"
|
|
12571
|
+
},
|
|
12572
|
+
"status": { "$ref": "#/components/schemas/SentEmailStatus" },
|
|
12573
|
+
"status_changed_at": {
|
|
12574
|
+
"type": "string",
|
|
12575
|
+
"format": "date-time",
|
|
12576
|
+
"description": "Timestamp of the most recent status transition.\nPolling clients should treat `status='queued'` AND\n`status_changed_at` older than 5 minutes as\n\"stuck-queued\" (the post-tx UPDATE failed and the\nactual delivery state is recoverable from on-box logs\nvia `queue_id` when populated, or `request_id`).\n"
|
|
12577
|
+
},
|
|
12578
|
+
"created_at": {
|
|
12579
|
+
"type": "string",
|
|
12580
|
+
"format": "date-time"
|
|
12581
|
+
},
|
|
12582
|
+
"updated_at": {
|
|
12583
|
+
"type": "string",
|
|
12584
|
+
"format": "date-time"
|
|
12585
|
+
},
|
|
12586
|
+
"client_idempotency_key": {
|
|
12587
|
+
"type": ["string", "null"],
|
|
12588
|
+
"description": "Effective idempotency key used for this send. If the\ncaller passed the `Idempotency-Key` header, this is\nthat value; otherwise it's a server-derived hash of\nthe canonical request payload.\n"
|
|
12589
|
+
},
|
|
12590
|
+
"content_hash": {
|
|
12591
|
+
"type": "string",
|
|
12592
|
+
"description": "Stable hash of the canonical send payload."
|
|
12593
|
+
},
|
|
12594
|
+
"from_header": {
|
|
12595
|
+
"type": "string",
|
|
12596
|
+
"description": "Raw `From:` header as sent on the wire, including any\ndisplay name (e.g. `\"Acme Support\" <agent@acme.test>`).\n"
|
|
12597
|
+
},
|
|
12598
|
+
"from_address": {
|
|
12599
|
+
"type": "string",
|
|
12600
|
+
"description": "Bare email address parsed from `from_header`."
|
|
12601
|
+
},
|
|
12602
|
+
"to_header": {
|
|
12603
|
+
"type": "string",
|
|
12604
|
+
"description": "Raw `To:` header as sent on the wire, including any\ndisplay name.\n"
|
|
12605
|
+
},
|
|
12606
|
+
"to_address": {
|
|
12607
|
+
"type": "string",
|
|
12608
|
+
"description": "Bare email address parsed from `to_header`."
|
|
12609
|
+
},
|
|
12610
|
+
"subject": { "type": "string" },
|
|
12611
|
+
"body_size_bytes": {
|
|
12612
|
+
"type": "integer",
|
|
12613
|
+
"description": "Total UTF-8 byte length of `body_text` + `body_html`.\nSurfaced on the list endpoint so callers can see \"this\nrow has a 4MB body\" without fetching it.\n"
|
|
12614
|
+
},
|
|
12615
|
+
"content_discarded_at": {
|
|
12616
|
+
"type": ["string", "null"],
|
|
12617
|
+
"format": "date-time",
|
|
12618
|
+
"description": "Timestamp at which the bodies were discarded by an\nentitlement-driven retention policy. Null when bodies\nare still present. The detail endpoint returns\nnull-valued `body_text`/`body_html` for discarded rows.\n"
|
|
12619
|
+
},
|
|
12620
|
+
"message_id": {
|
|
12621
|
+
"type": ["string", "null"],
|
|
12622
|
+
"description": "Wire-level Message-ID assigned to the outbound message\n(RFC 5322). Null on rows that never reached signing\n(queued, gate_denied, agent_failed before signing).\n"
|
|
12623
|
+
},
|
|
12624
|
+
"in_reply_to": {
|
|
12625
|
+
"type": ["string", "null"],
|
|
12626
|
+
"description": "Wire-level In-Reply-To header value, when this send\nwas a reply.\n"
|
|
12627
|
+
},
|
|
12628
|
+
"email_references": {
|
|
12629
|
+
"type": ["string", "null"],
|
|
12630
|
+
"description": "Wire-level References header value, when this send\nwas a reply.\n"
|
|
12631
|
+
},
|
|
12632
|
+
"in_reply_to_email_id": {
|
|
12633
|
+
"type": ["string", "null"],
|
|
12634
|
+
"format": "uuid",
|
|
12635
|
+
"description": "Reference to the inbound `emails.id` that this send\nreplied to, when known. Populated when the caller used\n/emails/{id}/reply or when /send-mail's `in_reply_to`\nmatched a stored inbound message_id in the same org.\n"
|
|
12636
|
+
},
|
|
12637
|
+
"thread_id": {
|
|
12638
|
+
"type": ["string", "null"],
|
|
12639
|
+
"format": "uuid",
|
|
12640
|
+
"description": "Conversation thread this send belongs to. A reply inherits\nthe thread of the inbound it answers; a fresh send starts a\nnew thread. Fetch `/threads/{thread_id}` for the full\nordered thread (inbound + outbound interleaved). NULL on\ngate-denied sends and on sends created before threading was\nenabled.\n"
|
|
12641
|
+
},
|
|
12642
|
+
"queue_id": {
|
|
12643
|
+
"type": ["string", "null"],
|
|
12644
|
+
"description": "Message identifier assigned by Primitive's outbound\nrelay once the agent accepts the message. Null on\nqueued, gate_denied, and agent_failed rows.\n"
|
|
12645
|
+
},
|
|
12646
|
+
"smtp_response_code": {
|
|
12647
|
+
"type": ["integer", "null"],
|
|
12648
|
+
"description": "Receiver's 3-digit SMTP code (e.g. 250, 550, 451).\nPopulated on terminal delivery statuses; may be null\non a deferred where the agent never got an SMTP-level\nresponse (TCP refused, DNS failed, TLS handshake\nfailed). `smtp_response_text` still carries Postfix's\ndescriptive text in those cases.\n"
|
|
12649
|
+
},
|
|
12650
|
+
"smtp_response_text": {
|
|
12651
|
+
"type": ["string", "null"],
|
|
12652
|
+
"description": "Free-form text portion of the receiver's SMTP\nresponse. The most useful debugging signal on a\n`bounced` or `deferred` row.\n"
|
|
12653
|
+
},
|
|
12654
|
+
"smtp_enhanced_status_code": {
|
|
12655
|
+
"type": ["string", "null"],
|
|
12656
|
+
"description": "RFC 3463 enhanced status code (e.g. `5.1.1` for \"Bad\ndestination mailbox address\"). Distinct from\n`smtp_response_code`: the basic 3-digit code is coarse\n(550 = \"permanent failure\"), the enhanced code is\nfiner-grained.\n"
|
|
12657
|
+
},
|
|
12658
|
+
"dkim_selector": {
|
|
12659
|
+
"type": ["string", "null"],
|
|
12660
|
+
"description": "DKIM selector used to sign the outbound message.\nPublic DNS data; useful for diagnosing why a downstream\nverifier rejected the signature.\n"
|
|
12661
|
+
},
|
|
12662
|
+
"dkim_domain": {
|
|
12663
|
+
"type": ["string", "null"],
|
|
12664
|
+
"description": "DKIM signing domain."
|
|
12665
|
+
},
|
|
12666
|
+
"error_code": {
|
|
12667
|
+
"type": ["string", "null"],
|
|
12668
|
+
"description": "Stable public error code on `agent_failed` rows. The\nagent's internal codes are remapped to a stable public\ntaxonomy (see `publicAgentError` in the server) so this\nfield is safe to branch on across agent versions.\n"
|
|
12669
|
+
},
|
|
12670
|
+
"error_message": {
|
|
12671
|
+
"type": ["string", "null"],
|
|
12672
|
+
"description": "Free-form error message accompanying `error_code`."
|
|
12673
|
+
},
|
|
12674
|
+
"gates": {
|
|
12675
|
+
"type": ["array", "null"],
|
|
12676
|
+
"items": { "$ref": "#/components/schemas/GateDenial" },
|
|
12677
|
+
"description": "Gate-denial detail on `gate_denied` rows. Mirrors the\nsynchronous /send-mail 403 contract so a caller's\nGateDenial handler is the same across live denies and\nhistorical lookups. Null on every other status.\n"
|
|
12678
|
+
},
|
|
12679
|
+
"request_id": {
|
|
12680
|
+
"type": ["string", "null"],
|
|
12681
|
+
"description": "Server-issued request identifier from the original\n/send-mail call. Surfaced as the `X-Request-Id`\nresponse header on the live send and recorded here\nfor support escalation.\n"
|
|
12682
|
+
},
|
|
12683
|
+
"scheduled_at": {
|
|
12684
|
+
"type": ["string", "null"],
|
|
12685
|
+
"format": "date-time",
|
|
12686
|
+
"description": "Requested execution time for a scheduled send. Kept\nafter execution as the historical schedule; null on\nordinary immediate sends.\n"
|
|
12687
|
+
},
|
|
12688
|
+
"canceled_at": {
|
|
12689
|
+
"type": ["string", "null"],
|
|
12690
|
+
"format": "date-time",
|
|
12691
|
+
"description": "When a scheduled send was canceled. Null unless the row\nreached the `canceled` status.\n"
|
|
12692
|
+
},
|
|
12681
12693
|
"presence_control": { "$ref": "#/components/schemas/PresenceControl" },
|
|
12682
12694
|
"repeat": {
|
|
12683
12695
|
"type": ["object", "null"],
|
|
@@ -22645,7 +22657,7 @@ const operationManifest = [
|
|
|
22645
22657
|
"binaryResponse": false,
|
|
22646
22658
|
"bodyRequired": false,
|
|
22647
22659
|
"command": "get-conversation",
|
|
22648
|
-
"description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
|
|
22660
|
+
"description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n\nPass `since` for an incremental read. `since=start` returns the\nfull conversation plus a `cursor`. Sending that `cursor` back as\n`since` returns only the messages created or changed after it,\noldest first, with the same per-message shape, and a new\n`cursor`. Without `since` the response carries no `cursor` and\nno per-message `status`.\n",
|
|
22649
22661
|
"hasJsonBody": false,
|
|
22650
22662
|
"method": "GET",
|
|
22651
22663
|
"operationId": "getConversation",
|
|
@@ -22657,7 +22669,13 @@ const operationManifest = [
|
|
|
22657
22669
|
"required": true,
|
|
22658
22670
|
"type": "string"
|
|
22659
22671
|
}],
|
|
22660
|
-
"queryParams": [
|
|
22672
|
+
"queryParams": [{
|
|
22673
|
+
"description": "Incremental read position. Either the literal `start` or a\n`cursor` returned by a previous read of this endpoint. Any\nother value returns 400 `validation_error`.\n\nWith `start`, the response holds every message the read\nwithout `since` would return, plus `cursor`. With a cursor,\n`messages` holds only the messages created or changed after\nit, oldest first. A change includes an inbound message\nbecoming visible or its body being filled, withheld or\ndiscarded, and a delivery status change on an outbound\nmessage. `thread_id`, `subject`, `message_count` and\n`truncated` always describe the whole conversation, not the\nreturned messages.\n\nThe cursor is an opaque string. Send it back verbatim and do\nnot parse it or build one; its format can change. A message\ncan be returned again by a later read (for example while\nolder writes are still committing), so upsert messages by\n`id`. Deleted messages are not reported; a periodic\n`since=start` read (or a read without `since`) reconciles.\n",
|
|
22674
|
+
"enum": null,
|
|
22675
|
+
"name": "since",
|
|
22676
|
+
"required": false,
|
|
22677
|
+
"type": "string"
|
|
22678
|
+
}],
|
|
22661
22679
|
"requestSchema": null,
|
|
22662
22680
|
"responseSchema": {
|
|
22663
22681
|
"type": "object",
|
|
@@ -22684,7 +22702,7 @@ const operationManifest = [
|
|
|
22684
22702
|
"type": "array",
|
|
22685
22703
|
"items": {
|
|
22686
22704
|
"type": "object",
|
|
22687
|
-
"description": "One message in the conversation, with its body and a chat role
|
|
22705
|
+
"description": "One message in the conversation, with its body and a chat role.\n`status` is the delivery status of an outbound message, as on\n`/sent-emails/{id}`, and is present only on a `since` read and\nonly on outbound messages.\n",
|
|
22688
22706
|
"properties": {
|
|
22689
22707
|
"role": {
|
|
22690
22708
|
"type": "string",
|
|
@@ -22713,6 +22731,23 @@ const operationManifest = [
|
|
|
22713
22731
|
"format": "date-time",
|
|
22714
22732
|
"description": "received_at for inbound, created_at for outbound."
|
|
22715
22733
|
},
|
|
22734
|
+
"status": {
|
|
22735
|
+
"type": "string",
|
|
22736
|
+
"description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n - `scheduled`: created with a future `scheduled_at` and\n not yet executed; `scheduled_at` carries the pending\n execution time. Reschedulable via PATCH\n /sent-emails/{id} and cancelable via\n /sent-emails/{id}/cancel while in this status.\n - `canceled`: terminal; a scheduled send canceled before\n execution. `canceled_at` carries the cancellation time\n and nothing was dispatched.\n",
|
|
22737
|
+
"enum": [
|
|
22738
|
+
"queued",
|
|
22739
|
+
"submitted_to_agent",
|
|
22740
|
+
"agent_failed",
|
|
22741
|
+
"gate_denied",
|
|
22742
|
+
"unknown",
|
|
22743
|
+
"delivered",
|
|
22744
|
+
"bounced",
|
|
22745
|
+
"deferred",
|
|
22746
|
+
"wait_timeout",
|
|
22747
|
+
"scheduled",
|
|
22748
|
+
"canceled"
|
|
22749
|
+
]
|
|
22750
|
+
},
|
|
22716
22751
|
"presence_control": { "anyOf": [{
|
|
22717
22752
|
"type": "object",
|
|
22718
22753
|
"properties": {
|
|
@@ -22773,6 +22808,10 @@ const operationManifest = [
|
|
|
22773
22808
|
"text"
|
|
22774
22809
|
]
|
|
22775
22810
|
}
|
|
22811
|
+
},
|
|
22812
|
+
"cursor": {
|
|
22813
|
+
"type": "string",
|
|
22814
|
+
"description": "Present only on a `since` read: the position to send as\n`since` on the next read. An opaque string: send it back\nverbatim and do not parse it or build one.\n"
|
|
22776
22815
|
}
|
|
22777
22816
|
},
|
|
22778
22817
|
"required": [
|
|
@@ -35116,477 +35155,1252 @@ Requires a connected agent profile selected with ${AGENT_PROFILE_ENV}. Output is
|
|
|
35116
35155
|
}
|
|
35117
35156
|
};
|
|
35118
35157
|
//#endregion
|
|
35119
|
-
//#region src/oclif/
|
|
35120
|
-
|
|
35121
|
-
const
|
|
35122
|
-
|
|
35123
|
-
const
|
|
35124
|
-
const
|
|
35125
|
-
|
|
35126
|
-
|
|
35127
|
-
|
|
35128
|
-
|
|
35129
|
-
|
|
35130
|
-
|
|
35131
|
-
|
|
35132
|
-
|
|
35133
|
-
|
|
35158
|
+
//#region src/oclif/machine-files.ts
|
|
35159
|
+
/** Hand-edited files larger than this are reported, never rewritten. */
|
|
35160
|
+
const MAX_MANAGED_FILE_BYTES = 1048576;
|
|
35161
|
+
/** Backups kept per file; older Primitive backups of the same file are pruned. */
|
|
35162
|
+
const BACKUPS_KEPT = 10;
|
|
35163
|
+
const BACKUP_INFIX = ".primitive-bak-";
|
|
35164
|
+
/** A safe, user-facing reason a file was not changed. */
|
|
35165
|
+
var MachineFileError = class extends Error {};
|
|
35166
|
+
/**
|
|
35167
|
+
* Read a user-owned text file without following it anywhere surprising. A
|
|
35168
|
+
* symlink is followed to its real file so a later write updates the file the
|
|
35169
|
+
* user linked instead of replacing the link.
|
|
35170
|
+
*/
|
|
35171
|
+
function readManagedFile(path) {
|
|
35172
|
+
let info;
|
|
35134
35173
|
try {
|
|
35135
|
-
|
|
35136
|
-
|
|
35137
|
-
|
|
35138
|
-
|
|
35139
|
-
|
|
35140
|
-
|
|
35141
|
-
if (!record$4(settings) || !record$4(settings.hooks)) return unavailable;
|
|
35142
|
-
const hooks = settings.hooks;
|
|
35143
|
-
const owns = (event, script, marker) => {
|
|
35144
|
-
const entries = hooks[event];
|
|
35145
|
-
return Array.isArray(entries) && entries.some((entry) => record$4(entry) && Array.isArray(entry.hooks) && entry.hooks.some((candidate) => {
|
|
35146
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35147
|
-
const args = candidate.args;
|
|
35148
|
-
const offset = 2;
|
|
35149
|
-
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;
|
|
35150
|
-
}));
|
|
35174
|
+
info = lstatSync(path);
|
|
35175
|
+
} catch (error) {
|
|
35176
|
+
if (error.code === "ENOENT") return {
|
|
35177
|
+
state: "absent",
|
|
35178
|
+
path,
|
|
35179
|
+
target: path
|
|
35151
35180
|
};
|
|
35152
|
-
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);
|
|
35153
|
-
let lastFiredAt = null;
|
|
35154
|
-
const fired = readMailJson(join(agentProfileDirectory(configDir, profileName), `pending-mail-${sessionId}.fired.json`));
|
|
35155
|
-
if (record$4(fired) && fired.version === 1 && typeof fired.at === "string" && Number.isFinite(Date.parse(fired.at))) lastFiredAt = fired.at;
|
|
35156
35181
|
return {
|
|
35157
|
-
|
|
35158
|
-
|
|
35159
|
-
|
|
35182
|
+
state: "invalid",
|
|
35183
|
+
path,
|
|
35184
|
+
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
35160
35185
|
};
|
|
35161
|
-
} catch {
|
|
35162
|
-
return unavailable;
|
|
35163
|
-
}
|
|
35164
|
-
}
|
|
35165
|
-
function editClaudeSettings(claudeDir, edit) {
|
|
35166
|
-
mkdirSync(claudeDir, {
|
|
35167
|
-
recursive: true,
|
|
35168
|
-
mode: 448
|
|
35169
|
-
});
|
|
35170
|
-
let release;
|
|
35171
|
-
for (let attempt = 0; attempt < 40; attempt++) try {
|
|
35172
|
-
release = acquireListenLock(claudeDir, "primitive-claude-settings");
|
|
35173
|
-
break;
|
|
35174
|
-
} catch {
|
|
35175
|
-
Atomics.wait(LOCK_WAIT$1, 0, 0, 25);
|
|
35176
|
-
}
|
|
35177
|
-
if (!release) return false;
|
|
35178
|
-
try {
|
|
35179
|
-
const settingsPath = join(claudeDir, "settings.json");
|
|
35180
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
35181
|
-
let original = null;
|
|
35182
|
-
let settings = {};
|
|
35183
|
-
if (existsSync(settingsPath)) {
|
|
35184
|
-
const stat = lstatSync(settingsPath);
|
|
35185
|
-
if (!stat.isFile() || stat.size > 1048576) return false;
|
|
35186
|
-
original = readFileSync(settingsPath, "utf8");
|
|
35187
|
-
const parsed = JSON.parse(original);
|
|
35188
|
-
if (!record$4(parsed)) return false;
|
|
35189
|
-
settings = parsed;
|
|
35190
|
-
}
|
|
35191
|
-
const next = edit(settings);
|
|
35192
|
-
if (next === null) return true;
|
|
35193
|
-
const temporary = join(claudeDir, `.settings.json.${randomUUID()}.tmp`);
|
|
35194
|
-
const fd = openSync(temporary, "wx", 384);
|
|
35195
|
-
try {
|
|
35196
|
-
writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
35197
|
-
} finally {
|
|
35198
|
-
closeSync(fd);
|
|
35199
|
-
}
|
|
35200
|
-
try {
|
|
35201
|
-
if ((existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null) !== original) continue;
|
|
35202
|
-
renameSync(temporary, settingsPath);
|
|
35203
|
-
return true;
|
|
35204
|
-
} finally {
|
|
35205
|
-
if (existsSync(temporary)) unlinkSync(temporary);
|
|
35206
|
-
}
|
|
35207
|
-
}
|
|
35208
|
-
return false;
|
|
35209
|
-
} finally {
|
|
35210
|
-
release();
|
|
35211
|
-
}
|
|
35212
|
-
}
|
|
35213
|
-
/** Add exact-session fail-open receiving hooks while preserving unrelated settings. */
|
|
35214
|
-
function installClaudeWakeHook(options) {
|
|
35215
|
-
try {
|
|
35216
|
-
const cliPath = realpathSync(options.cliPath);
|
|
35217
|
-
const wrapperPath = realpathSync(join(dirname(cliPath), "claude-wake.mjs"));
|
|
35218
|
-
const pendingPath = realpathSync(join(dirname(cliPath), "claude-pending-mail.mjs"));
|
|
35219
|
-
const configDir = resolve(options.configDir);
|
|
35220
|
-
const profileName = agentProfileName(options.profileName);
|
|
35221
|
-
const agentAddress = mailAddress(options.agentAddress);
|
|
35222
|
-
if (!SESSION_UUID$1.test(options.sessionId)) return "unavailable";
|
|
35223
|
-
const sessionId = options.sessionId.toLowerCase();
|
|
35224
|
-
return editClaudeSettings(resolve((options.env ?? process.env).CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), (settings) => {
|
|
35225
|
-
const hooks = settings.hooks ?? {};
|
|
35226
|
-
if (!record$4(hooks)) throw new Error("Invalid hook settings");
|
|
35227
|
-
for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
|
|
35228
|
-
if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
|
|
35229
|
-
const hook = {
|
|
35230
|
-
type: "command",
|
|
35231
|
-
command: process.execPath,
|
|
35232
|
-
args: [
|
|
35233
|
-
wrapperPath,
|
|
35234
|
-
cliPath,
|
|
35235
|
-
configDir,
|
|
35236
|
-
profileName,
|
|
35237
|
-
agentAddress,
|
|
35238
|
-
sessionId,
|
|
35239
|
-
HOOK_MARKER
|
|
35240
|
-
],
|
|
35241
|
-
asyncRewake: true,
|
|
35242
|
-
timeout: 604800
|
|
35243
|
-
};
|
|
35244
|
-
const pendingHook = {
|
|
35245
|
-
type: "command",
|
|
35246
|
-
command: process.execPath,
|
|
35247
|
-
args: [
|
|
35248
|
-
pendingPath,
|
|
35249
|
-
cliPath,
|
|
35250
|
-
configDir,
|
|
35251
|
-
profileName,
|
|
35252
|
-
agentAddress,
|
|
35253
|
-
sessionId,
|
|
35254
|
-
PENDING_MARKER$1
|
|
35255
|
-
],
|
|
35256
|
-
timeout: 10
|
|
35257
|
-
};
|
|
35258
|
-
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;
|
|
35259
|
-
const isOwnHook = (candidate) => {
|
|
35260
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35261
|
-
if (typeof candidate.args[0] !== "string" || basename(candidate.args[0]) !== "claude-wake.mjs" || candidate.args[2] !== configDir) return false;
|
|
35262
|
-
if (candidate.args.length === 7 && candidate.args[5] === sessionId && candidate.args[6] === HOOK_MARKER) return true;
|
|
35263
|
-
if (candidate.args.length !== 6 || candidate.args[5] !== HOOK_MARKER) return false;
|
|
35264
|
-
try {
|
|
35265
|
-
const state = readMailJson(join(agentProfileDirectory(configDir, agentProfileName(String(candidate.args[3]))), "setup.json"));
|
|
35266
|
-
return record$4(state) && state.session === sessionId;
|
|
35267
|
-
} catch {
|
|
35268
|
-
return false;
|
|
35269
|
-
}
|
|
35270
|
-
};
|
|
35271
|
-
const nextHooks = { ...hooks };
|
|
35272
|
-
for (const event of WAKE_EVENTS) nextHooks[event] = [...(hooks[event] ?? []).flatMap((entry) => {
|
|
35273
|
-
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35274
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
|
|
35275
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35276
|
-
return siblings.length ? [{
|
|
35277
|
-
...entry,
|
|
35278
|
-
hooks: siblings
|
|
35279
|
-
}] : [];
|
|
35280
|
-
}), event === "SessionStart" ? {
|
|
35281
|
-
matcher: "resume",
|
|
35282
|
-
hooks: [hook]
|
|
35283
|
-
} : { hooks: [hook] }];
|
|
35284
|
-
nextHooks[PENDING_EVENT] = [...(hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
|
|
35285
|
-
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35286
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
|
|
35287
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35288
|
-
return siblings.length ? [{
|
|
35289
|
-
...entry,
|
|
35290
|
-
hooks: siblings
|
|
35291
|
-
}] : [];
|
|
35292
|
-
}), { hooks: [pendingHook] }];
|
|
35293
|
-
return {
|
|
35294
|
-
...settings,
|
|
35295
|
-
hooks: nextHooks
|
|
35296
|
-
};
|
|
35297
|
-
}) ? "installed_unverified" : "unavailable";
|
|
35298
|
-
} catch {
|
|
35299
|
-
return "unavailable";
|
|
35300
35186
|
}
|
|
35301
|
-
|
|
35302
|
-
|
|
35303
|
-
|
|
35304
|
-
|
|
35305
|
-
const configDir = resolve(options.configDir);
|
|
35306
|
-
const profileName = agentProfileName(options.profileName);
|
|
35307
|
-
const agentAddress = mailAddress(options.agentAddress);
|
|
35308
|
-
if (!SESSION_UUID$1.test(options.sessionId)) return false;
|
|
35309
|
-
const sessionId = options.sessionId.toLowerCase();
|
|
35310
|
-
const claudeDir = resolve((options.env ?? process.env).CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
|
|
35311
|
-
if (!existsSync(join(claudeDir, "settings.json"))) return true;
|
|
35312
|
-
return editClaudeSettings(claudeDir, (settings) => {
|
|
35313
|
-
const hooks = settings.hooks ?? {};
|
|
35314
|
-
if (!record$4(hooks)) throw new Error("Invalid hook settings");
|
|
35315
|
-
for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
|
|
35316
|
-
if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
|
|
35317
|
-
const legacySessionMatches = () => {
|
|
35318
|
-
try {
|
|
35319
|
-
const setup = readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json"));
|
|
35320
|
-
return record$4(setup) && setup.session === sessionId;
|
|
35321
|
-
} catch {
|
|
35322
|
-
return false;
|
|
35323
|
-
}
|
|
35324
|
-
};
|
|
35325
|
-
const isOwnHook = (candidate) => {
|
|
35326
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35327
|
-
const args = candidate.args;
|
|
35328
|
-
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());
|
|
35329
|
-
};
|
|
35330
|
-
const isOwnPendingHook = (candidate) => {
|
|
35331
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35332
|
-
const args = candidate.args;
|
|
35333
|
-
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;
|
|
35334
|
-
};
|
|
35335
|
-
let removed = false;
|
|
35336
|
-
const nextHooks = { ...hooks };
|
|
35337
|
-
for (const event of WAKE_EVENTS) nextHooks[event] = (hooks[event] ?? []).flatMap((entry) => {
|
|
35338
|
-
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35339
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
|
|
35340
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35341
|
-
removed = true;
|
|
35342
|
-
return siblings.length ? [{
|
|
35343
|
-
...entry,
|
|
35344
|
-
hooks: siblings
|
|
35345
|
-
}] : [];
|
|
35346
|
-
});
|
|
35347
|
-
nextHooks[PENDING_EVENT] = (hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
|
|
35348
|
-
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35349
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
|
|
35350
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35351
|
-
removed = true;
|
|
35352
|
-
return siblings.length ? [{
|
|
35353
|
-
...entry,
|
|
35354
|
-
hooks: siblings
|
|
35355
|
-
}] : [];
|
|
35356
|
-
});
|
|
35357
|
-
return removed ? {
|
|
35358
|
-
...settings,
|
|
35359
|
-
hooks: nextHooks
|
|
35360
|
-
} : null;
|
|
35361
|
-
});
|
|
35187
|
+
let target = path;
|
|
35188
|
+
if (info.isSymbolicLink()) try {
|
|
35189
|
+
target = realpathSync(path);
|
|
35190
|
+
info = statSync(target);
|
|
35362
35191
|
} catch {
|
|
35363
|
-
return false;
|
|
35364
|
-
}
|
|
35365
|
-
}
|
|
35366
|
-
//#endregion
|
|
35367
|
-
//#region src/oclif/agent-connect.ts
|
|
35368
|
-
const MAX_INVITATION_BYTES = 4096;
|
|
35369
|
-
const MAX_RESPONSE_BYTES$2 = 32768;
|
|
35370
|
-
const UNCERTAIN_CLAIM = "Setup did not complete safely. The invitation may have been consumed. Request a fresh invitation from the owner; do not retry this invitation.";
|
|
35371
|
-
/** Offline metadata only; a saved profile is not proof of current API or listener readiness. */
|
|
35372
|
-
function agentConnectionStatus(configDir, profileName) {
|
|
35373
|
-
agentProfileName(profileName);
|
|
35374
|
-
const profile = loadConnectedAgentProfile(configDir, profileName);
|
|
35375
|
-
const setup = profile ? readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json")) : null;
|
|
35376
|
-
const saved = setup && typeof setup === "object" && !Array.isArray(setup) ? setup : null;
|
|
35377
|
-
const session = saved && typeof saved.session === "string" && SESSION_UUID$1.test(saved.session) ? saved.session : null;
|
|
35378
|
-
const mode = saved?.receiverMode === "external" || saved?.receiverMode === "poll" ? saved.receiverMode : "native";
|
|
35379
|
-
const receiving = profile && saved && mode === "poll" ? {
|
|
35380
|
-
mode,
|
|
35381
|
-
sessionId: session,
|
|
35382
|
-
state: "poll",
|
|
35383
|
-
reason: "agent_checks_mail",
|
|
35384
|
-
lastSuccessfulMailCheckAt: null,
|
|
35385
|
-
liveness: "unknown"
|
|
35386
|
-
} : profile && session && mode === "native" ? (() => {
|
|
35387
|
-
const scope = notificationScope(profile.api_base_url, profile.api_key);
|
|
35388
|
-
const listener = backgroundListenStatus({
|
|
35389
|
-
configDir,
|
|
35390
|
-
scope,
|
|
35391
|
-
threadId: session
|
|
35392
|
-
});
|
|
35393
|
-
const mailOwner = readSharedMailOwner({ directory: join(configDir, "shared-mail", createHash("sha256").update(scope).digest("hex")) });
|
|
35394
35192
|
return {
|
|
35395
|
-
|
|
35396
|
-
|
|
35397
|
-
|
|
35398
|
-
reason: listener.reason ?? listener.failureCode,
|
|
35399
|
-
failureCode: listener.failureCode,
|
|
35400
|
-
detail: listener.detail ?? null,
|
|
35401
|
-
lastSuccessfulMailCheckAt: mailOwner?.lastMailCheckAt ?? null,
|
|
35402
|
-
liveness: listener.healthy && listener.phase === "receiving" ? "live" : "unknown",
|
|
35403
|
-
listener
|
|
35193
|
+
state: "invalid",
|
|
35194
|
+
path,
|
|
35195
|
+
detail: `${path} is a symlink to a missing file; it was left unchanged.`
|
|
35404
35196
|
};
|
|
35405
|
-
}
|
|
35406
|
-
|
|
35407
|
-
|
|
35408
|
-
|
|
35409
|
-
|
|
35410
|
-
lastSuccessfulMailCheckAt: null,
|
|
35411
|
-
liveness: "unknown",
|
|
35412
|
-
hook: claudeWakeHookStatus({
|
|
35413
|
-
configDir,
|
|
35414
|
-
profileName,
|
|
35415
|
-
agentAddress: profile.agent_address,
|
|
35416
|
-
sessionId: session
|
|
35417
|
-
})
|
|
35418
|
-
} : {
|
|
35419
|
-
mode: "unknown",
|
|
35420
|
-
sessionId: null,
|
|
35421
|
-
state: "unknown",
|
|
35422
|
-
reason: "session_not_configured",
|
|
35423
|
-
lastSuccessfulMailCheckAt: null,
|
|
35424
|
-
liveness: "unknown"
|
|
35197
|
+
}
|
|
35198
|
+
if (!info.isFile()) return {
|
|
35199
|
+
state: "invalid",
|
|
35200
|
+
path,
|
|
35201
|
+
detail: `${path} is not a regular file; it was left unchanged.`
|
|
35425
35202
|
};
|
|
35426
|
-
|
|
35427
|
-
|
|
35428
|
-
|
|
35429
|
-
|
|
35430
|
-
} : {
|
|
35431
|
-
status: "not_configured",
|
|
35432
|
-
profileName
|
|
35203
|
+
if (info.size > MAX_MANAGED_FILE_BYTES) return {
|
|
35204
|
+
state: "invalid",
|
|
35205
|
+
path,
|
|
35206
|
+
detail: `${path} is larger than 1 MiB; it was left unchanged.`
|
|
35433
35207
|
};
|
|
35434
|
-
|
|
35435
|
-
function parseAgentInvitation(input) {
|
|
35208
|
+
let raw;
|
|
35436
35209
|
try {
|
|
35437
|
-
|
|
35438
|
-
|
|
35439
|
-
let token;
|
|
35440
|
-
let base;
|
|
35441
|
-
if (text.startsWith("{")) {
|
|
35442
|
-
const value = JSON.parse(text);
|
|
35443
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
35444
|
-
const row = value;
|
|
35445
|
-
if (Object.keys(row).some((key) => key !== "token" && key !== "api_base_url")) throw new Error();
|
|
35446
|
-
token = row.token;
|
|
35447
|
-
base = row.api_base_url ?? "https://api.primitive.dev/v1";
|
|
35448
|
-
} else {
|
|
35449
|
-
const url = new URL(text);
|
|
35450
|
-
if (url.username || url.password || url.search || url.pathname !== "/v1/agent-connections/setup") throw new Error();
|
|
35451
|
-
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
35452
|
-
if (Array.from(fragment.keys()).length !== 1 || fragment.getAll("token").length !== 1) throw new Error();
|
|
35453
|
-
token = fragment.get("token");
|
|
35454
|
-
base = `${url.origin}/v1`;
|
|
35455
|
-
}
|
|
35456
|
-
if (typeof token !== "string" || token.length < 32 || token.length > 256 || /\s/.test(token) || Array.from(token).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw new Error();
|
|
35210
|
+
raw = readFileSync(target);
|
|
35211
|
+
} catch (error) {
|
|
35457
35212
|
return {
|
|
35458
|
-
|
|
35459
|
-
|
|
35213
|
+
state: "invalid",
|
|
35214
|
+
path,
|
|
35215
|
+
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
35460
35216
|
};
|
|
35461
|
-
} catch {
|
|
35462
|
-
throw new AgentConnectionSetupError("Pipe the private Primitive setup URL or a JSON object containing token and optional api_base_url to stdin. Invitation values are never accepted as command arguments.");
|
|
35463
35217
|
}
|
|
35218
|
+
const text = raw.toString("utf8");
|
|
35219
|
+
if (text.includes("\0") || !Buffer.from(text, "utf8").equals(raw)) return {
|
|
35220
|
+
state: "invalid",
|
|
35221
|
+
path,
|
|
35222
|
+
detail: `${path} is not UTF-8 text; it was left unchanged.`
|
|
35223
|
+
};
|
|
35224
|
+
return {
|
|
35225
|
+
state: "present",
|
|
35226
|
+
path,
|
|
35227
|
+
target,
|
|
35228
|
+
text,
|
|
35229
|
+
mode: info.mode & 511
|
|
35230
|
+
};
|
|
35464
35231
|
}
|
|
35465
|
-
|
|
35466
|
-
|
|
35467
|
-
const chunks = [];
|
|
35468
|
-
let size = 0;
|
|
35469
|
-
for await (const chunk of input) {
|
|
35470
|
-
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
35471
|
-
size += bytes.length;
|
|
35472
|
-
if (size > MAX_INVITATION_BYTES) throw new AgentConnectionSetupError("Setup invitation exceeds the input limit.");
|
|
35473
|
-
chunks.push(bytes);
|
|
35474
|
-
}
|
|
35475
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
35232
|
+
function backupStamp$1(now) {
|
|
35233
|
+
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
35476
35234
|
}
|
|
35477
|
-
|
|
35478
|
-
|
|
35479
|
-
if (!reader) throw new Error();
|
|
35480
|
-
const chunks = [];
|
|
35481
|
-
let size = 0;
|
|
35235
|
+
function syncDirectory(directory) {
|
|
35236
|
+
if (process.platform === "win32") return;
|
|
35482
35237
|
try {
|
|
35483
|
-
|
|
35484
|
-
|
|
35485
|
-
|
|
35486
|
-
|
|
35487
|
-
|
|
35488
|
-
chunks.push(part.value);
|
|
35238
|
+
const fd = openSync(directory, "r");
|
|
35239
|
+
try {
|
|
35240
|
+
fsyncSync(fd);
|
|
35241
|
+
} finally {
|
|
35242
|
+
closeSync(fd);
|
|
35489
35243
|
}
|
|
35490
|
-
|
|
35491
|
-
} finally {
|
|
35492
|
-
await reader.cancel().catch(() => void 0);
|
|
35493
|
-
}
|
|
35244
|
+
} catch {}
|
|
35494
35245
|
}
|
|
35495
|
-
function
|
|
35496
|
-
const
|
|
35497
|
-
const
|
|
35498
|
-
const connection = data?.connection;
|
|
35499
|
-
if (response?.success !== true || !data || !connection || data.api_base_url !== invitation.apiBaseUrl || data.owner_address !== connection.owner_address || !["claimed", "connected"].includes(String(connection.status))) throw new Error();
|
|
35500
|
-
let ownerMember;
|
|
35246
|
+
function pruneBackups(target) {
|
|
35247
|
+
const directory = dirname(target);
|
|
35248
|
+
const prefix = `${basename(target)}${BACKUP_INFIX}`;
|
|
35501
35249
|
try {
|
|
35502
|
-
|
|
35503
|
-
|
|
35504
|
-
|
|
35505
|
-
}
|
|
35506
|
-
return parseConnectedAgentProfile({
|
|
35507
|
-
version: 1,
|
|
35508
|
-
auth_method: "agent_connection",
|
|
35509
|
-
api_key: data.api_key,
|
|
35510
|
-
api_base_url: data.api_base_url,
|
|
35511
|
-
org_id: data.org_id,
|
|
35512
|
-
agent_address: connection.address,
|
|
35513
|
-
owner_address: data.owner_address,
|
|
35514
|
-
...ownerMember === void 0 ? {} : { owner_member_address: ownerMember },
|
|
35515
|
-
invitation_hash: invitationHash,
|
|
35516
|
-
created_at: new Date(now()).toISOString(),
|
|
35517
|
-
...data.presence_profile === void 0 ? {} : { presence_profile: data.presence_profile }
|
|
35518
|
-
});
|
|
35250
|
+
const backups = readdirSync(directory).filter((name) => name.startsWith(prefix)).sort();
|
|
35251
|
+
for (const name of backups.slice(0, Math.max(0, backups.length - BACKUPS_KEPT))) unlinkSync(join(directory, name));
|
|
35252
|
+
} catch {}
|
|
35519
35253
|
}
|
|
35520
|
-
/**
|
|
35521
|
-
|
|
35522
|
-
const
|
|
35523
|
-
|
|
35524
|
-
|
|
35525
|
-
const directory = agentProfilesDirectory(params.configDir);
|
|
35526
|
-
let release;
|
|
35527
|
-
try {
|
|
35528
|
-
privateMailDirectory(directory, true);
|
|
35529
|
-
release = acquireListenLock(directory, "agent-connection-setup");
|
|
35530
|
-
} catch {
|
|
35531
|
-
throw new AgentConnectionSetupError("Agent setup is already running, or its private directory is unavailable. No invitation was submitted.");
|
|
35532
|
-
}
|
|
35533
|
-
try {
|
|
35534
|
-
const existing = loadConnectedAgentProfile(params.configDir, profileName);
|
|
35535
|
-
if (existing) {
|
|
35536
|
-
if (existing.invitation_hash !== invitationHash) throw new AgentConnectionSetupError("This agent profile is already configured. Its identity and credentials were not changed. Use a separate profile for another invitation.");
|
|
35537
|
-
return {
|
|
35538
|
-
status: "already_configured",
|
|
35539
|
-
identity: connectedAgentIdentity(profileName, existing)
|
|
35540
|
-
};
|
|
35541
|
-
}
|
|
35542
|
-
privateMailDirectory(join(directory, "profiles"), true);
|
|
35543
|
-
privateMailDirectory(agentProfileDirectory(params.configDir, profileName), true);
|
|
35544
|
-
const journal = join(directory, "claims", `${invitationHash}.json`);
|
|
35545
|
-
privateMailDirectory(join(directory, "claims"), true);
|
|
35546
|
-
if (readMailJson(journal) !== null) throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
35547
|
-
writeMailJson(journal, {
|
|
35548
|
-
version: 1,
|
|
35549
|
-
profile_name: profileName,
|
|
35550
|
-
status: "attempted"
|
|
35551
|
-
});
|
|
35254
|
+
/** Copy the current file beside itself before any change. Returns the copy's path. */
|
|
35255
|
+
function backupManagedFile(target, mode, now) {
|
|
35256
|
+
const base = `${target}${BACKUP_INFIX}${backupStamp$1(now)}`;
|
|
35257
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
35258
|
+
const candidate = attempt === 0 ? base : `${base}-${attempt}`;
|
|
35552
35259
|
try {
|
|
35553
|
-
|
|
35554
|
-
|
|
35555
|
-
|
|
35556
|
-
|
|
35557
|
-
|
|
35558
|
-
"content-type": "application/json",
|
|
35559
|
-
accept: "application/json"
|
|
35560
|
-
},
|
|
35561
|
-
body: JSON.stringify({
|
|
35562
|
-
token: invitation.token,
|
|
35563
|
-
...params.presence ? { capabilities: ["primitive.presence/1"] } : {}
|
|
35564
|
-
})
|
|
35565
|
-
});
|
|
35566
|
-
if (response.status !== 200) {
|
|
35567
|
-
await response.body?.cancel();
|
|
35568
|
-
throw new Error();
|
|
35569
|
-
}
|
|
35570
|
-
const profile = profileFromClaim(await readClaimResponse(response), invitation, invitationHash, params.now ?? Date.now);
|
|
35571
|
-
saveConnectedAgentProfile(params.configDir, profileName, profile);
|
|
35572
|
-
return {
|
|
35573
|
-
status: "claimed",
|
|
35574
|
-
identity: connectedAgentIdentity(profileName, profile)
|
|
35575
|
-
};
|
|
35576
|
-
} catch {
|
|
35577
|
-
throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
35260
|
+
copyFileSync(target, candidate, constants.COPYFILE_EXCL);
|
|
35261
|
+
chmodSync(candidate, mode);
|
|
35262
|
+
return candidate;
|
|
35263
|
+
} catch (error) {
|
|
35264
|
+
if (error.code !== "EEXIST") throw error;
|
|
35578
35265
|
}
|
|
35579
|
-
} finally {
|
|
35580
|
-
release();
|
|
35581
35266
|
}
|
|
35267
|
+
throw new MachineFileError(`Could not create a backup of ${target}.`);
|
|
35582
35268
|
}
|
|
35583
|
-
|
|
35584
|
-
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35269
|
+
/**
|
|
35270
|
+
* Replace a file atomically (temporary file plus rename in the same
|
|
35271
|
+
* directory), after backing up the existing content. The write is refused when
|
|
35272
|
+
* the file changed since it was read, so a concurrent edit is never lost.
|
|
35273
|
+
*/
|
|
35274
|
+
function writeManagedFile(params) {
|
|
35275
|
+
const { read } = params;
|
|
35276
|
+
const target = read.target;
|
|
35277
|
+
const expected = read.state === "present" ? read.text : null;
|
|
35278
|
+
const directory = dirname(target);
|
|
35279
|
+
mkdirSync(directory, {
|
|
35280
|
+
recursive: true,
|
|
35281
|
+
mode: 448
|
|
35282
|
+
});
|
|
35283
|
+
const current = () => {
|
|
35284
|
+
try {
|
|
35285
|
+
return readFileSync(target, "utf8");
|
|
35286
|
+
} catch (error) {
|
|
35287
|
+
if (error.code === "ENOENT") return null;
|
|
35288
|
+
throw error;
|
|
35289
|
+
}
|
|
35290
|
+
};
|
|
35291
|
+
if (current() !== expected) throw new MachineFileError(`${read.path} changed while it was being repaired; it was left unchanged.`);
|
|
35292
|
+
const mode = read.state === "present" ? read.mode : params.newFileMode ?? 420;
|
|
35293
|
+
const backup = read.state === "present" ? backupManagedFile(target, mode, (params.now ?? (() => /* @__PURE__ */ new Date()))()) : null;
|
|
35294
|
+
const temporary = join(directory, `.${basename(target)}.${randomUUID()}.tmp`);
|
|
35295
|
+
try {
|
|
35296
|
+
const fd = openSync(temporary, "wx", mode);
|
|
35297
|
+
try {
|
|
35298
|
+
writeFileSync(fd, params.content, "utf8");
|
|
35299
|
+
fsyncSync(fd);
|
|
35300
|
+
} finally {
|
|
35301
|
+
closeSync(fd);
|
|
35302
|
+
}
|
|
35303
|
+
chmodSync(temporary, mode);
|
|
35304
|
+
if (current() !== expected) throw new MachineFileError(`${read.path} changed while it was being repaired; it was left unchanged.`);
|
|
35305
|
+
renameSync(temporary, target);
|
|
35306
|
+
syncDirectory(directory);
|
|
35307
|
+
} finally {
|
|
35308
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
35309
|
+
}
|
|
35310
|
+
if (backup) pruneBackups(target);
|
|
35311
|
+
return { backup };
|
|
35312
|
+
}
|
|
35313
|
+
const START = /^<!-- primitive:managed-block v=(\d+) START -->\r?$/;
|
|
35314
|
+
const END = /^<!-- primitive:managed-block END -->\r?$/;
|
|
35315
|
+
/** Character offsets of the single managed block, or why the markers are unusable. */
|
|
35316
|
+
function locateBlock(text) {
|
|
35317
|
+
const starts = [];
|
|
35318
|
+
const ends = [];
|
|
35319
|
+
let offset = 0;
|
|
35320
|
+
for (const line of text.split("\n")) {
|
|
35321
|
+
const start = START.exec(line);
|
|
35322
|
+
if (start) starts.push({
|
|
35323
|
+
offset,
|
|
35324
|
+
version: Number(start[1])
|
|
35325
|
+
});
|
|
35326
|
+
else if (END.test(line)) ends.push({
|
|
35327
|
+
offset,
|
|
35328
|
+
length: line.endsWith("\r") ? line.length - 1 : line.length
|
|
35329
|
+
});
|
|
35330
|
+
else if (line.includes("primitive:managed-block")) return { malformed: "a primitive:managed-block marker line was edited; fix or remove it by hand" };
|
|
35331
|
+
offset += line.length + 1;
|
|
35332
|
+
}
|
|
35333
|
+
if (starts.length === 0 && ends.length === 0) return null;
|
|
35334
|
+
if (starts.length !== 1 || ends.length !== 1) return { malformed: `found ${starts.length} START and ${ends.length} END markers; expected one of each` };
|
|
35335
|
+
const [start] = starts;
|
|
35336
|
+
const [end] = ends;
|
|
35337
|
+
if (!start || !end || end.offset < start.offset) return { malformed: "the END marker comes before the START marker" };
|
|
35338
|
+
return {
|
|
35339
|
+
start: start.offset,
|
|
35340
|
+
end: end.offset + end.length,
|
|
35341
|
+
version: start.version
|
|
35342
|
+
};
|
|
35343
|
+
}
|
|
35344
|
+
/** The exact block for a body, using the file's own line ending. */
|
|
35345
|
+
function renderManagedBlock(body, eol = "\n") {
|
|
35346
|
+
return [
|
|
35347
|
+
`<!-- primitive:managed-block v=1 START -->`,
|
|
35348
|
+
...body.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n"),
|
|
35349
|
+
"<!-- primitive:managed-block END -->"
|
|
35350
|
+
].join(eol);
|
|
35351
|
+
}
|
|
35352
|
+
function lineEnding(text) {
|
|
35353
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
35354
|
+
}
|
|
35355
|
+
function inspectManagedBlock(text, body) {
|
|
35356
|
+
const found = locateBlock(text);
|
|
35357
|
+
if (found === null) return { state: "missing" };
|
|
35358
|
+
if ("malformed" in found) return {
|
|
35359
|
+
state: "malformed",
|
|
35360
|
+
detail: found.malformed
|
|
35361
|
+
};
|
|
35362
|
+
return text.slice(found.start, found.end).replace(/\r\n/g, "\n") === renderManagedBlock(body) ? { state: "ok" } : {
|
|
35363
|
+
state: "outdated",
|
|
35364
|
+
version: found.version
|
|
35365
|
+
};
|
|
35366
|
+
}
|
|
35367
|
+
/**
|
|
35368
|
+
* Replace only the managed block, or append it when absent. Text outside the
|
|
35369
|
+
* markers is preserved byte for byte. Throws for malformed markers.
|
|
35370
|
+
*/
|
|
35371
|
+
function upsertManagedBlock(text, body) {
|
|
35372
|
+
const found = locateBlock(text);
|
|
35373
|
+
if (found !== null && "malformed" in found) throw new MachineFileError(found.malformed);
|
|
35374
|
+
const eol = lineEnding(text);
|
|
35375
|
+
const block = renderManagedBlock(body, eol);
|
|
35376
|
+
if (found) return `${text.slice(0, found.start)}${block}${text.slice(found.end)}`;
|
|
35377
|
+
if (text.length === 0) return `${block}${eol}`;
|
|
35378
|
+
return `${text}${text.endsWith(`${eol}${eol}`) ? "" : text.endsWith(eol) ? eol : `${eol}${eol}`}${block}${eol}`;
|
|
35379
|
+
}
|
|
35380
|
+
/** Indentation the file already uses, so a rewrite keeps its style. */
|
|
35381
|
+
function jsonIndentation(text) {
|
|
35382
|
+
return /^[{[][^\n]*\n([ \t]+)\S/.exec(text)?.[1] ?? 2;
|
|
35383
|
+
}
|
|
35384
|
+
//#endregion
|
|
35385
|
+
//#region src/oclif/claude-machine-hooks.ts
|
|
35386
|
+
function record$4(value) {
|
|
35387
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35388
|
+
}
|
|
35389
|
+
function realpathOrNull(path) {
|
|
35390
|
+
try {
|
|
35391
|
+
return realpathSync(path);
|
|
35392
|
+
} catch {
|
|
35393
|
+
return null;
|
|
35394
|
+
}
|
|
35395
|
+
}
|
|
35396
|
+
/** Two paths name the same existing file. */
|
|
35397
|
+
function samePath(a, b) {
|
|
35398
|
+
if (typeof a !== "string" || !a) return false;
|
|
35399
|
+
if (a === b) return existsSync(a);
|
|
35400
|
+
const left = realpathOrNull(a);
|
|
35401
|
+
return left !== null && left === realpathOrNull(b);
|
|
35402
|
+
}
|
|
35403
|
+
function executable(path) {
|
|
35404
|
+
try {
|
|
35405
|
+
accessSync(path, constants.X_OK);
|
|
35406
|
+
return true;
|
|
35407
|
+
} catch {
|
|
35408
|
+
return false;
|
|
35409
|
+
}
|
|
35410
|
+
}
|
|
35411
|
+
/** The first PATH entry for a command, without running a shell. */
|
|
35412
|
+
function findOnPath(name, env) {
|
|
35413
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
35414
|
+
if (!directory) continue;
|
|
35415
|
+
const candidate = join(directory, name);
|
|
35416
|
+
if (existsSync(candidate) && executable(candidate)) return candidate;
|
|
35417
|
+
}
|
|
35418
|
+
return null;
|
|
35419
|
+
}
|
|
35420
|
+
/**
|
|
35421
|
+
* Prefer a PATH link (for example a package manager's `bin/node`) over a
|
|
35422
|
+
* versioned install directory when both are the same binary, so a hook keeps
|
|
35423
|
+
* working after the versioned directory is replaced by an upgrade.
|
|
35424
|
+
*/
|
|
35425
|
+
function stableNodePath(env, execPath = process.execPath) {
|
|
35426
|
+
const onPath = findOnPath("node", env);
|
|
35427
|
+
if (onPath && onPath !== execPath && samePath(onPath, execPath)) return onPath;
|
|
35428
|
+
return execPath;
|
|
35429
|
+
}
|
|
35430
|
+
/** Locate the running CLI. Null when its files are not where a package install puts them. */
|
|
35431
|
+
function currentCliLocation(entryArg, env, execPath = process.execPath) {
|
|
35432
|
+
const entry = realpathOrNull(entryArg);
|
|
35433
|
+
if (!entry) return null;
|
|
35434
|
+
const wake = realpathOrNull(join(dirname(entry), "claude-wake.mjs"));
|
|
35435
|
+
const pending = realpathOrNull(join(dirname(entry), "claude-pending-mail.mjs"));
|
|
35436
|
+
if (!wake || !pending) return null;
|
|
35437
|
+
return {
|
|
35438
|
+
node: stableNodePath(env, execPath),
|
|
35439
|
+
entry,
|
|
35440
|
+
wake,
|
|
35441
|
+
pending
|
|
35442
|
+
};
|
|
35443
|
+
}
|
|
35444
|
+
/** Package-runner caches are cleaned up behind the user's back. */
|
|
35445
|
+
function ephemeralCliReason(entry) {
|
|
35446
|
+
if (/[\\/]_npx[\\/]/.test(entry)) return "an npx cache";
|
|
35447
|
+
if (/[\\/]dlx[-\\/]/.test(entry)) return "a pnpm dlx cache";
|
|
35448
|
+
if (/[\\/]bunx-/.test(entry)) return "a bunx cache";
|
|
35449
|
+
return null;
|
|
35450
|
+
}
|
|
35451
|
+
function claudeConfigDir(env, home) {
|
|
35452
|
+
return resolve(env.CLAUDE_CONFIG_DIR || join(home, ".claude"));
|
|
35453
|
+
}
|
|
35454
|
+
function jsonErrorDetail(error) {
|
|
35455
|
+
const message = error instanceof Error ? error.message : "";
|
|
35456
|
+
const position = /position (\d+)/.exec(message)?.[1];
|
|
35457
|
+
const line = /line (\d+)/.exec(message)?.[1];
|
|
35458
|
+
return line ? ` near line ${line}` : position ? ` near character ${position}` : "";
|
|
35459
|
+
}
|
|
35460
|
+
/** Parse settings.json. Anything unexpected is reported, never repaired. */
|
|
35461
|
+
function readClaudeSettings(claudeDir) {
|
|
35462
|
+
const path = join(claudeDir, "settings.json");
|
|
35463
|
+
const read = readManagedFile(path);
|
|
35464
|
+
if (read.state === "invalid") return {
|
|
35465
|
+
ok: false,
|
|
35466
|
+
detail: read.detail
|
|
35467
|
+
};
|
|
35468
|
+
if (read.state === "absent") return {
|
|
35469
|
+
ok: true,
|
|
35470
|
+
read,
|
|
35471
|
+
settings: {}
|
|
35472
|
+
};
|
|
35473
|
+
let parsed;
|
|
35474
|
+
try {
|
|
35475
|
+
parsed = JSON.parse(read.text);
|
|
35476
|
+
} catch (error) {
|
|
35477
|
+
return {
|
|
35478
|
+
ok: false,
|
|
35479
|
+
detail: `${path} is not valid JSON${jsonErrorDetail(error)}. Fix it by hand; it was not modified.`
|
|
35480
|
+
};
|
|
35481
|
+
}
|
|
35482
|
+
if (!record$4(parsed)) return {
|
|
35483
|
+
ok: false,
|
|
35484
|
+
detail: `${path} must contain a JSON object. Fix it by hand; it was not modified.`
|
|
35485
|
+
};
|
|
35486
|
+
if (parsed.hooks !== void 0 && !record$4(parsed.hooks)) return {
|
|
35487
|
+
ok: false,
|
|
35488
|
+
detail: `${path} has a "hooks" value that is not an object. Fix it by hand; it was not modified.`
|
|
35489
|
+
};
|
|
35490
|
+
return {
|
|
35491
|
+
ok: true,
|
|
35492
|
+
read,
|
|
35493
|
+
settings: parsed
|
|
35494
|
+
};
|
|
35495
|
+
}
|
|
35496
|
+
const LOCK_WAIT$1 = new Int32Array(new SharedArrayBuffer(4));
|
|
35497
|
+
/**
|
|
35498
|
+
* Write settings under the same lock every Primitive hook installer uses,
|
|
35499
|
+
* keeping the file's indentation, after a timestamped backup.
|
|
35500
|
+
*/
|
|
35501
|
+
function writeClaudeSettings(params) {
|
|
35502
|
+
let release;
|
|
35503
|
+
for (let attempt = 0; attempt < 80; attempt++) try {
|
|
35504
|
+
release = acquireListenLock(params.claudeDir, "primitive-claude-settings");
|
|
35505
|
+
break;
|
|
35506
|
+
} catch {
|
|
35507
|
+
Atomics.wait(LOCK_WAIT$1, 0, 0, 25);
|
|
35508
|
+
}
|
|
35509
|
+
if (!release) throw new MachineFileError("Another Primitive command is editing Claude settings; try again shortly.");
|
|
35510
|
+
try {
|
|
35511
|
+
const original = params.read.state === "present" ? params.read.text : null;
|
|
35512
|
+
const indent = original === null ? 2 : jsonIndentation(original);
|
|
35513
|
+
const trailing = original === null || original.endsWith("\n") ? "\n" : "";
|
|
35514
|
+
return writeManagedFile({
|
|
35515
|
+
read: params.read,
|
|
35516
|
+
content: `${JSON.stringify(params.settings, null, indent)}${trailing}`,
|
|
35517
|
+
now: params.now,
|
|
35518
|
+
newFileMode: 384
|
|
35519
|
+
});
|
|
35520
|
+
} finally {
|
|
35521
|
+
release();
|
|
35522
|
+
}
|
|
35523
|
+
}
|
|
35524
|
+
const GLOBAL_HOOKS = {
|
|
35525
|
+
"claude.hook.session_start": {
|
|
35526
|
+
event: "SessionStart",
|
|
35527
|
+
subcommand: "session-register",
|
|
35528
|
+
timeout: 15
|
|
35529
|
+
},
|
|
35530
|
+
"claude.hook.session_end": {
|
|
35531
|
+
event: "SessionEnd",
|
|
35532
|
+
subcommand: "session-end",
|
|
35533
|
+
timeout: 10
|
|
35534
|
+
}
|
|
35535
|
+
};
|
|
35536
|
+
/** A hook this CLI installed for every Claude session, from any CLI path or version. */
|
|
35537
|
+
function isGlobalManaged(hook, spec) {
|
|
35538
|
+
if (!record$4(hook) || hook.type !== "command" || !Array.isArray(hook.args)) return false;
|
|
35539
|
+
const args = hook.args;
|
|
35540
|
+
return args.length === 6 && typeof args[0] === "string" && args[1] === "agent" && args[2] === spec.subcommand && args[3] === "--runtime" && args[4] === "claude" && args[5] === "--hook";
|
|
35541
|
+
}
|
|
35542
|
+
function globalHookArgs(cli, spec) {
|
|
35543
|
+
return [
|
|
35544
|
+
cli.entry,
|
|
35545
|
+
"agent",
|
|
35546
|
+
spec.subcommand,
|
|
35547
|
+
"--runtime",
|
|
35548
|
+
"claude",
|
|
35549
|
+
"--hook"
|
|
35550
|
+
];
|
|
35551
|
+
}
|
|
35552
|
+
function globalHookCurrent(hook, cli) {
|
|
35553
|
+
const args = hook.args;
|
|
35554
|
+
return samePath(hook.command, cli.node) && samePath(args[0], cli.entry);
|
|
35555
|
+
}
|
|
35556
|
+
const WAKE_MARKER = "primitive-agent-wake-v1";
|
|
35557
|
+
const PENDING_MARKER$1 = "primitive-pending-mail-v1";
|
|
35558
|
+
const SESSION_EVENTS = [
|
|
35559
|
+
"Stop",
|
|
35560
|
+
"SessionStart",
|
|
35561
|
+
"PostToolUse"
|
|
35562
|
+
];
|
|
35563
|
+
/** Which receive hook each event carries. */
|
|
35564
|
+
const SESSION_HOOK_KIND = {
|
|
35565
|
+
Stop: "wake",
|
|
35566
|
+
SessionStart: "wake",
|
|
35567
|
+
PostToolUse: "pending"
|
|
35568
|
+
};
|
|
35569
|
+
/**
|
|
35570
|
+
* The one test of whether an event already carries a current receive hook
|
|
35571
|
+
* for this exact profile, address and session, run by the Node binary at
|
|
35572
|
+
* `node` (compared as files). `agent connect --status` and machine doctor
|
|
35573
|
+
* both use it, so they never disagree about a hook being present.
|
|
35574
|
+
*/
|
|
35575
|
+
function hasSessionReceiveHook(hooks, event, target, node) {
|
|
35576
|
+
const entries = hooks[event];
|
|
35577
|
+
if (!Array.isArray(entries)) return false;
|
|
35578
|
+
const pending = SESSION_HOOK_KIND[event] === "pending";
|
|
35579
|
+
const configDir = resolve(target.configDir);
|
|
35580
|
+
const session = target.session.toLowerCase();
|
|
35581
|
+
return entries.some((entry) => record$4(entry) && Array.isArray(entry.hooks) && entry.hooks.some((hook) => {
|
|
35582
|
+
if (!record$4(hook) || hook.type !== "command") return false;
|
|
35583
|
+
const args = hook.args;
|
|
35584
|
+
return Array.isArray(args) && args.length === 7 && args.every((value) => typeof value === "string") && basename(args[0]) === (pending ? "claude-pending-mail.mjs" : "claude-wake.mjs") && resolve(args[2]) === configDir && args[3] === target.profile && args[4] === target.address && args[5].toLowerCase() === session && args[6] === (pending ? PENDING_MARKER$1 : WAKE_MARKER) && samePath(hook.command, node);
|
|
35585
|
+
}));
|
|
35586
|
+
}
|
|
35587
|
+
/** A per-session receive hook written by `agent connect` or `agent enroll`, any version. */
|
|
35588
|
+
function parseSessionHook(hook) {
|
|
35589
|
+
if (!record$4(hook) || hook.type !== "command" || !Array.isArray(hook.args)) return null;
|
|
35590
|
+
const args = hook.args;
|
|
35591
|
+
if (!args.every((value) => typeof value === "string")) return null;
|
|
35592
|
+
const strings = args;
|
|
35593
|
+
const script = basename(strings[0] ?? "");
|
|
35594
|
+
if (script === "claude-pending-mail.mjs") return strings.length === 7 && strings[6] === PENDING_MARKER$1 ? {
|
|
35595
|
+
kind: "pending",
|
|
35596
|
+
configDir: strings[2] ?? "",
|
|
35597
|
+
profile: strings[3] ?? null,
|
|
35598
|
+
address: strings[4] ?? null,
|
|
35599
|
+
session: strings[5] ?? null
|
|
35600
|
+
} : null;
|
|
35601
|
+
if (script !== "claude-wake.mjs") return null;
|
|
35602
|
+
if (strings.length === 7 && strings[6] === WAKE_MARKER) return {
|
|
35603
|
+
kind: "wake",
|
|
35604
|
+
configDir: strings[2] ?? "",
|
|
35605
|
+
profile: strings[3] ?? null,
|
|
35606
|
+
address: strings[4] ?? null,
|
|
35607
|
+
session: strings[5] ?? null
|
|
35608
|
+
};
|
|
35609
|
+
if (strings.length === 6 && strings[5] === WAKE_MARKER) return {
|
|
35610
|
+
kind: "wake",
|
|
35611
|
+
configDir: strings[2] ?? "",
|
|
35612
|
+
profile: strings[3] ?? null,
|
|
35613
|
+
address: strings[4] ?? null,
|
|
35614
|
+
session: null
|
|
35615
|
+
};
|
|
35616
|
+
if (strings.length === 4 && strings[3] === WAKE_MARKER) return {
|
|
35617
|
+
kind: "wake",
|
|
35618
|
+
configDir: strings[2] ?? "",
|
|
35619
|
+
profile: null,
|
|
35620
|
+
address: null,
|
|
35621
|
+
session: null
|
|
35622
|
+
};
|
|
35623
|
+
return null;
|
|
35624
|
+
}
|
|
35625
|
+
/** A saved connected profile still holds a credential. Unreadable counts as live. */
|
|
35626
|
+
function profileStillConnected(configDir, profile) {
|
|
35627
|
+
try {
|
|
35628
|
+
return loadConnectedAgentProfile(configDir, profile) !== null;
|
|
35629
|
+
} catch {
|
|
35630
|
+
return true;
|
|
35631
|
+
}
|
|
35632
|
+
}
|
|
35633
|
+
function sessionHookPathsCurrent(hook, parsed, cli) {
|
|
35634
|
+
const args = hook.args;
|
|
35635
|
+
if (!cli) return existsSync(String(hook.command)) && existsSync(args[0] ?? "") && existsSync(args[1] ?? "");
|
|
35636
|
+
return samePath(hook.command, cli.node) && samePath(args[0], parsed.kind === "wake" ? cli.wake : cli.pending) && samePath(args[1], cli.entry);
|
|
35637
|
+
}
|
|
35638
|
+
function judgeSessionHooks(hooks, cli, bound = []) {
|
|
35639
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
35640
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35641
|
+
for (const event of SESSION_EVENTS) {
|
|
35642
|
+
const entries = hooks[event];
|
|
35643
|
+
if (!Array.isArray(entries)) continue;
|
|
35644
|
+
for (const entry of entries) {
|
|
35645
|
+
if (!record$4(entry) || !Array.isArray(entry.hooks)) continue;
|
|
35646
|
+
for (const hook of entry.hooks) {
|
|
35647
|
+
const parsed = parseSessionHook(hook);
|
|
35648
|
+
if (!parsed || !record$4(hook)) continue;
|
|
35649
|
+
const pathsExist = existsSync(String(hook.command)) && existsSync(String(hook.args[0])) && existsSync(String(hook.args[1]));
|
|
35650
|
+
if (!(parsed.profile ? existsSync(parsed.configDir) && profileStillConnected(parsed.configDir, parsed.profile) : existsSync(parsed.configDir) && pathsExist)) {
|
|
35651
|
+
verdicts.set(hook, "stale");
|
|
35652
|
+
continue;
|
|
35653
|
+
}
|
|
35654
|
+
const current = bound.find((item) => item.configDir === resolve(parsed.configDir) && item.profile === parsed.profile && item.session === parsed.session?.toLowerCase());
|
|
35655
|
+
if (current && parsed.address !== current.address) {
|
|
35656
|
+
verdicts.set(hook, "old_address");
|
|
35657
|
+
continue;
|
|
35658
|
+
}
|
|
35659
|
+
const key = [
|
|
35660
|
+
event,
|
|
35661
|
+
parsed.kind,
|
|
35662
|
+
resolve(parsed.configDir),
|
|
35663
|
+
parsed.profile ?? "",
|
|
35664
|
+
parsed.session?.toLowerCase() ?? ""
|
|
35665
|
+
].join("\0");
|
|
35666
|
+
if (seen.has(key)) {
|
|
35667
|
+
verdicts.set(hook, "duplicate");
|
|
35668
|
+
continue;
|
|
35669
|
+
}
|
|
35670
|
+
seen.add(key);
|
|
35671
|
+
verdicts.set(hook, sessionHookPathsCurrent(hook, parsed, cli) ? "keep" : "outdated");
|
|
35672
|
+
}
|
|
35673
|
+
}
|
|
35674
|
+
}
|
|
35675
|
+
return verdicts;
|
|
35676
|
+
}
|
|
35677
|
+
function eventEntries(hooks, event) {
|
|
35678
|
+
const entries = hooks[event];
|
|
35679
|
+
if (entries === void 0) return [];
|
|
35680
|
+
return Array.isArray(entries) ? entries : "invalid";
|
|
35681
|
+
}
|
|
35682
|
+
/**
|
|
35683
|
+
* Connected profiles whose setup binds an external (hook) receiver to a
|
|
35684
|
+
* Claude session that this machine has not recorded as ended.
|
|
35685
|
+
*/
|
|
35686
|
+
function boundSessionProfiles(configDir) {
|
|
35687
|
+
const root = resolve(configDir);
|
|
35688
|
+
let names;
|
|
35689
|
+
try {
|
|
35690
|
+
names = readdirSync(join(agentProfilesDirectory(root), "profiles")).sort();
|
|
35691
|
+
} catch {
|
|
35692
|
+
return [];
|
|
35693
|
+
}
|
|
35694
|
+
const bound = [];
|
|
35695
|
+
for (const name of names) try {
|
|
35696
|
+
agentProfileName(name);
|
|
35697
|
+
const setup = readMailJson(join(agentProfileDirectory(root, name), "setup.json"));
|
|
35698
|
+
if (setup?.receiverMode !== "external" || typeof setup.session !== "string" || !SESSION_UUID$1.test(setup.session)) continue;
|
|
35699
|
+
const session = setup.session.toLowerCase();
|
|
35700
|
+
if (typeof readMailJson(join(root, "machine", "sessions", `${session}.json`))?.endedAt === "string") continue;
|
|
35701
|
+
const profile = loadConnectedAgentProfile(root, name);
|
|
35702
|
+
if (profile) bound.push({
|
|
35703
|
+
configDir: root,
|
|
35704
|
+
profile: name,
|
|
35705
|
+
address: profile.agent_address,
|
|
35706
|
+
session
|
|
35707
|
+
});
|
|
35708
|
+
} catch {}
|
|
35709
|
+
return bound;
|
|
35710
|
+
}
|
|
35711
|
+
/** Receive hooks each bound profile should have and does not. */
|
|
35712
|
+
function missingSessionHooks(hooks, bound, node) {
|
|
35713
|
+
return bound.flatMap((item) => SESSION_EVENTS.filter((event) => !hasSessionReceiveHook(hooks, event, item, node)).map((event) => ({
|
|
35714
|
+
event,
|
|
35715
|
+
bound: item
|
|
35716
|
+
})));
|
|
35717
|
+
}
|
|
35718
|
+
/** The exact entry `agent connect` and `session-register` install. */
|
|
35719
|
+
function sessionHookEntry(event, item, cli) {
|
|
35720
|
+
const pending = SESSION_HOOK_KIND[event] === "pending";
|
|
35721
|
+
const hook = {
|
|
35722
|
+
type: "command",
|
|
35723
|
+
command: cli.node,
|
|
35724
|
+
args: [
|
|
35725
|
+
pending ? cli.pending : cli.wake,
|
|
35726
|
+
cli.entry,
|
|
35727
|
+
item.configDir,
|
|
35728
|
+
item.profile,
|
|
35729
|
+
item.address,
|
|
35730
|
+
item.session,
|
|
35731
|
+
pending ? PENDING_MARKER$1 : WAKE_MARKER
|
|
35732
|
+
],
|
|
35733
|
+
...pending ? { timeout: 10 } : {
|
|
35734
|
+
asyncRewake: true,
|
|
35735
|
+
timeout: 604800
|
|
35736
|
+
}
|
|
35737
|
+
};
|
|
35738
|
+
return event === "SessionStart" ? {
|
|
35739
|
+
matcher: "resume",
|
|
35740
|
+
hooks: [hook]
|
|
35741
|
+
} : { hooks: [hook] };
|
|
35742
|
+
}
|
|
35743
|
+
/** Inspect every Primitive-owned Claude hook without changing anything. */
|
|
35744
|
+
function inspectClaudeHooks(settings, context) {
|
|
35745
|
+
const hooks = record$4(settings.hooks) ? settings.hooks : {};
|
|
35746
|
+
const canFix = context.cli !== null && context.blocked === null;
|
|
35747
|
+
const blockedNote = context.blocked ? ` ${context.blocked}` : "";
|
|
35748
|
+
const findings = {};
|
|
35749
|
+
for (const [id, spec] of Object.entries(GLOBAL_HOOKS)) {
|
|
35750
|
+
const entries = eventEntries(hooks, spec.event);
|
|
35751
|
+
if (entries === "invalid") {
|
|
35752
|
+
findings[id] = {
|
|
35753
|
+
status: "fail",
|
|
35754
|
+
detail: `hooks.${spec.event} is not a list. Fix settings.json by hand; it was not modified.`,
|
|
35755
|
+
fixable: false
|
|
35756
|
+
};
|
|
35757
|
+
continue;
|
|
35758
|
+
}
|
|
35759
|
+
const managed = entries.flatMap((entry) => record$4(entry) && Array.isArray(entry.hooks) ? entry.hooks.filter((hook) => isGlobalManaged(hook, spec)) : []);
|
|
35760
|
+
const [first] = managed;
|
|
35761
|
+
if (!first) findings[id] = {
|
|
35762
|
+
status: "fail",
|
|
35763
|
+
detail: `No ${spec.event} hook runs \`primitive agent ${spec.subcommand}\`.${blockedNote}`,
|
|
35764
|
+
fixable: canFix
|
|
35765
|
+
};
|
|
35766
|
+
else if (managed.length > 1) findings[id] = {
|
|
35767
|
+
status: "fail",
|
|
35768
|
+
detail: `${managed.length} ${spec.event} hooks run \`primitive agent ${spec.subcommand}\`; expected exactly one.${blockedNote}`,
|
|
35769
|
+
fixable: canFix
|
|
35770
|
+
};
|
|
35771
|
+
else if (!context.cli || !globalHookCurrent(first, context.cli)) findings[id] = {
|
|
35772
|
+
status: "fail",
|
|
35773
|
+
detail: `The ${spec.event} hook runs ${String(first.args[0])}, which ${existsSync(String(first.args[0])) ? "is not this CLI" : "no longer exists"}.${blockedNote}`,
|
|
35774
|
+
fixable: canFix
|
|
35775
|
+
};
|
|
35776
|
+
else findings[id] = {
|
|
35777
|
+
status: "ok",
|
|
35778
|
+
detail: `${spec.event} runs \`primitive agent ${spec.subcommand}\` from ${context.cli.entry}.`,
|
|
35779
|
+
fixable: false
|
|
35780
|
+
};
|
|
35781
|
+
}
|
|
35782
|
+
const invalid = SESSION_EVENTS.find((event) => eventEntries(hooks, event) === "invalid");
|
|
35783
|
+
if (invalid) {
|
|
35784
|
+
findings["claude.hook.stop"] = {
|
|
35785
|
+
status: "fail",
|
|
35786
|
+
detail: `hooks.${invalid} is not a list. Fix settings.json by hand; it was not modified.`,
|
|
35787
|
+
fixable: false
|
|
35788
|
+
};
|
|
35789
|
+
return findings;
|
|
35790
|
+
}
|
|
35791
|
+
const bound = context.bound ?? [];
|
|
35792
|
+
const counts = {
|
|
35793
|
+
keep: 0,
|
|
35794
|
+
stale: 0,
|
|
35795
|
+
duplicate: 0,
|
|
35796
|
+
outdated: 0,
|
|
35797
|
+
old_address: 0
|
|
35798
|
+
};
|
|
35799
|
+
for (const verdict of judgeSessionHooks(hooks, context.cli, bound).values()) counts[verdict]++;
|
|
35800
|
+
const missing = missingSessionHooks(hooks, bound, context.cli?.node ?? process.execPath).length;
|
|
35801
|
+
const problems = [
|
|
35802
|
+
missing ? `${missing} missing for connected sessions` : "",
|
|
35803
|
+
counts.old_address ? `${counts.old_address} naming an old agent address` : "",
|
|
35804
|
+
counts.stale ? `${counts.stale} for disconnected or removed sessions` : "",
|
|
35805
|
+
counts.duplicate ? `${counts.duplicate} duplicates` : "",
|
|
35806
|
+
counts.outdated ? `${counts.outdated} pointing at an old CLI path` : ""
|
|
35807
|
+
].filter(Boolean);
|
|
35808
|
+
findings["claude.hook.stop"] = problems.length ? {
|
|
35809
|
+
status: "fail",
|
|
35810
|
+
detail: `Per-session receive hooks need cleanup: ${problems.join(", ")}.${blockedNote}`,
|
|
35811
|
+
fixable: canFix || counts.outdated === 0 && missing === 0 && context.blocked === null
|
|
35812
|
+
} : {
|
|
35813
|
+
status: "ok",
|
|
35814
|
+
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.",
|
|
35815
|
+
fixable: false
|
|
35816
|
+
};
|
|
35817
|
+
return findings;
|
|
35818
|
+
}
|
|
35819
|
+
function rewriteEntries(entries, transform) {
|
|
35820
|
+
let changed = false;
|
|
35821
|
+
return {
|
|
35822
|
+
entries: entries.flatMap((entry) => {
|
|
35823
|
+
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35824
|
+
let touched = false;
|
|
35825
|
+
const kept = entry.hooks.flatMap((hook) => {
|
|
35826
|
+
const replacement = transform(hook);
|
|
35827
|
+
if (replacement === hook) return [hook];
|
|
35828
|
+
touched = true;
|
|
35829
|
+
return replacement === void 0 ? [] : [replacement];
|
|
35830
|
+
});
|
|
35831
|
+
if (!touched) return [entry];
|
|
35832
|
+
changed = true;
|
|
35833
|
+
return kept.length ? [{
|
|
35834
|
+
...entry,
|
|
35835
|
+
hooks: kept
|
|
35836
|
+
}] : [];
|
|
35837
|
+
}),
|
|
35838
|
+
changed
|
|
35839
|
+
};
|
|
35840
|
+
}
|
|
35841
|
+
/**
|
|
35842
|
+
* Apply fixes for the requested checks to a copy of the settings. Only
|
|
35843
|
+
* Primitive-owned hooks are added, rewritten or removed; every other key,
|
|
35844
|
+
* entry and sibling hook is carried over unchanged.
|
|
35845
|
+
*/
|
|
35846
|
+
function repairClaudeHooks(settings, context, checks) {
|
|
35847
|
+
const changed = /* @__PURE__ */ new Set();
|
|
35848
|
+
const hooks = record$4(settings.hooks) ? { ...settings.hooks } : {};
|
|
35849
|
+
const cli = context.blocked === null ? context.cli : null;
|
|
35850
|
+
for (const [id, spec] of Object.entries(GLOBAL_HOOKS)) {
|
|
35851
|
+
if (!checks.has(id) || !cli) continue;
|
|
35852
|
+
const entries = eventEntries(hooks, spec.event);
|
|
35853
|
+
if (entries === "invalid") continue;
|
|
35854
|
+
let placed = false;
|
|
35855
|
+
const result = rewriteEntries(entries, (hook) => {
|
|
35856
|
+
if (!isGlobalManaged(hook, spec)) return hook;
|
|
35857
|
+
if (placed) return void 0;
|
|
35858
|
+
placed = true;
|
|
35859
|
+
if (globalHookCurrent(hook, cli)) return hook;
|
|
35860
|
+
return {
|
|
35861
|
+
...hook,
|
|
35862
|
+
command: cli.node,
|
|
35863
|
+
args: globalHookArgs(cli, spec)
|
|
35864
|
+
};
|
|
35865
|
+
});
|
|
35866
|
+
let next = result.entries;
|
|
35867
|
+
if (!placed) next = [...next, { hooks: [{
|
|
35868
|
+
type: "command",
|
|
35869
|
+
command: cli.node,
|
|
35870
|
+
args: globalHookArgs(cli, spec),
|
|
35871
|
+
timeout: spec.timeout
|
|
35872
|
+
}] }];
|
|
35873
|
+
if (result.changed || !placed) {
|
|
35874
|
+
hooks[spec.event] = next;
|
|
35875
|
+
changed.add(id);
|
|
35876
|
+
}
|
|
35877
|
+
}
|
|
35878
|
+
if (checks.has("claude.hook.stop") && context.blocked === null) {
|
|
35879
|
+
if (!SESSION_EVENTS.some((event) => eventEntries(hooks, event) === "invalid")) {
|
|
35880
|
+
const bound = context.bound ?? [];
|
|
35881
|
+
const verdicts = judgeSessionHooks(hooks, context.cli, bound);
|
|
35882
|
+
for (const event of SESSION_EVENTS) {
|
|
35883
|
+
const entries = eventEntries(hooks, event);
|
|
35884
|
+
if (entries === "invalid" || entries.length === 0) continue;
|
|
35885
|
+
const result = rewriteEntries(entries, (hook) => {
|
|
35886
|
+
const verdict = verdicts.get(hook);
|
|
35887
|
+
if (verdict === "stale" || verdict === "duplicate" || verdict === "old_address") return void 0;
|
|
35888
|
+
if (verdict !== "outdated" || !cli || !record$4(hook)) return hook;
|
|
35889
|
+
const parsed = parseSessionHook(hook);
|
|
35890
|
+
const args = [...hook.args];
|
|
35891
|
+
args[0] = parsed?.kind === "pending" ? cli.pending : cli.wake;
|
|
35892
|
+
args[1] = cli.entry;
|
|
35893
|
+
return {
|
|
35894
|
+
...hook,
|
|
35895
|
+
command: cli.node,
|
|
35896
|
+
args
|
|
35897
|
+
};
|
|
35898
|
+
});
|
|
35899
|
+
if (result.changed) {
|
|
35900
|
+
hooks[event] = result.entries;
|
|
35901
|
+
changed.add("claude.hook.stop");
|
|
35902
|
+
}
|
|
35903
|
+
}
|
|
35904
|
+
if (cli) for (const missing of missingSessionHooks(hooks, bound, cli.node)) {
|
|
35905
|
+
const entries = eventEntries(hooks, missing.event);
|
|
35906
|
+
if (entries === "invalid") continue;
|
|
35907
|
+
hooks[missing.event] = [...entries, sessionHookEntry(missing.event, missing.bound, cli)];
|
|
35908
|
+
changed.add("claude.hook.stop");
|
|
35909
|
+
}
|
|
35910
|
+
}
|
|
35911
|
+
}
|
|
35912
|
+
if (!changed.size) return {
|
|
35913
|
+
settings,
|
|
35914
|
+
changed
|
|
35915
|
+
};
|
|
35916
|
+
return {
|
|
35917
|
+
settings: {
|
|
35918
|
+
...settings,
|
|
35919
|
+
hooks
|
|
35920
|
+
},
|
|
35921
|
+
changed
|
|
35922
|
+
};
|
|
35923
|
+
}
|
|
35924
|
+
//#endregion
|
|
35925
|
+
//#region src/oclif/claude-wake-install.ts
|
|
35926
|
+
const HOOK_MARKER = "primitive-agent-wake-v1";
|
|
35927
|
+
const WAKE_EVENTS = ["Stop", "SessionStart"];
|
|
35928
|
+
const PENDING_MARKER = "primitive-pending-mail-v1";
|
|
35929
|
+
const PENDING_EVENT = "PostToolUse";
|
|
35930
|
+
const LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
35931
|
+
function record$3(value) {
|
|
35932
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35933
|
+
}
|
|
35934
|
+
function claudeWakeHookStatus(options) {
|
|
35935
|
+
const unavailable = {
|
|
35936
|
+
installed: false,
|
|
35937
|
+
lastFiredAt: null,
|
|
35938
|
+
liveness: "unknown"
|
|
35939
|
+
};
|
|
35940
|
+
try {
|
|
35941
|
+
const configDir = resolve(options.configDir);
|
|
35942
|
+
const profileName = agentProfileName(options.profileName);
|
|
35943
|
+
const agentAddress = mailAddress(options.agentAddress);
|
|
35944
|
+
if (!SESSION_UUID$1.test(options.sessionId)) return unavailable;
|
|
35945
|
+
const sessionId = options.sessionId.toLowerCase();
|
|
35946
|
+
const read = readManagedFile(join(resolve(options.env?.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), "settings.json"));
|
|
35947
|
+
if (read.state !== "present") return unavailable;
|
|
35948
|
+
const settings = JSON.parse(read.text);
|
|
35949
|
+
if (!record$3(settings) || !record$3(settings.hooks)) return unavailable;
|
|
35950
|
+
const hooks = settings.hooks;
|
|
35951
|
+
const target = {
|
|
35952
|
+
configDir,
|
|
35953
|
+
profile: profileName,
|
|
35954
|
+
address: agentAddress,
|
|
35955
|
+
session: sessionId
|
|
35956
|
+
};
|
|
35957
|
+
const installed = [
|
|
35958
|
+
"Stop",
|
|
35959
|
+
"SessionStart",
|
|
35960
|
+
"PostToolUse"
|
|
35961
|
+
].every((event) => hasSessionReceiveHook(hooks, event, target, process.execPath));
|
|
35962
|
+
let lastFiredAt = null;
|
|
35963
|
+
const fired = readMailJson(join(agentProfileDirectory(configDir, profileName), `pending-mail-${sessionId}.fired.json`));
|
|
35964
|
+
if (record$3(fired) && fired.version === 1 && typeof fired.at === "string" && Number.isFinite(Date.parse(fired.at))) lastFiredAt = fired.at;
|
|
35965
|
+
return {
|
|
35966
|
+
installed,
|
|
35967
|
+
lastFiredAt,
|
|
35968
|
+
liveness: "unknown"
|
|
35969
|
+
};
|
|
35970
|
+
} catch {
|
|
35971
|
+
return unavailable;
|
|
35972
|
+
}
|
|
35973
|
+
}
|
|
35974
|
+
/**
|
|
35975
|
+
* Apply one edit to Claude's settings under the shared lock, backing up the
|
|
35976
|
+
* previous file. Exported for tests.
|
|
35977
|
+
*/
|
|
35978
|
+
function editClaudeSettings(claudeDir, edit) {
|
|
35979
|
+
mkdirSync(claudeDir, {
|
|
35980
|
+
recursive: true,
|
|
35981
|
+
mode: 448
|
|
35982
|
+
});
|
|
35983
|
+
let release;
|
|
35984
|
+
for (let attempt = 0; attempt < 40; attempt++) try {
|
|
35985
|
+
release = acquireListenLock(claudeDir, "primitive-claude-settings");
|
|
35986
|
+
break;
|
|
35987
|
+
} catch {
|
|
35988
|
+
Atomics.wait(LOCK_WAIT, 0, 0, 25);
|
|
35989
|
+
}
|
|
35990
|
+
if (!release) return false;
|
|
35991
|
+
try {
|
|
35992
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
35993
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
35994
|
+
const read = readManagedFile(settingsPath);
|
|
35995
|
+
if (read.state === "invalid") return false;
|
|
35996
|
+
let settings = {};
|
|
35997
|
+
if (read.state === "present") {
|
|
35998
|
+
const parsed = JSON.parse(read.text);
|
|
35999
|
+
if (!record$3(parsed)) return false;
|
|
36000
|
+
settings = parsed;
|
|
36001
|
+
}
|
|
36002
|
+
const next = edit(settings);
|
|
36003
|
+
if (next === null) return true;
|
|
36004
|
+
const content = `${JSON.stringify(next, null, 2)}\n`;
|
|
36005
|
+
if (read.state === "present" && content === read.text) return true;
|
|
36006
|
+
try {
|
|
36007
|
+
writeManagedFile({
|
|
36008
|
+
read,
|
|
36009
|
+
content,
|
|
36010
|
+
newFileMode: 384
|
|
36011
|
+
});
|
|
36012
|
+
return true;
|
|
36013
|
+
} catch (error) {
|
|
36014
|
+
if (!(error instanceof MachineFileError)) throw error;
|
|
36015
|
+
}
|
|
36016
|
+
}
|
|
36017
|
+
return false;
|
|
36018
|
+
} finally {
|
|
36019
|
+
release();
|
|
36020
|
+
}
|
|
36021
|
+
}
|
|
36022
|
+
/** Add exact-session fail-open receiving hooks while preserving unrelated settings. */
|
|
36023
|
+
function installClaudeWakeHook(options) {
|
|
36024
|
+
try {
|
|
36025
|
+
const cliPath = realpathSync(options.cliPath);
|
|
36026
|
+
const wrapperPath = realpathSync(join(dirname(cliPath), "claude-wake.mjs"));
|
|
36027
|
+
const pendingPath = realpathSync(join(dirname(cliPath), "claude-pending-mail.mjs"));
|
|
36028
|
+
const configDir = resolve(options.configDir);
|
|
36029
|
+
const profileName = agentProfileName(options.profileName);
|
|
36030
|
+
const agentAddress = mailAddress(options.agentAddress);
|
|
36031
|
+
if (!SESSION_UUID$1.test(options.sessionId)) return "unavailable";
|
|
36032
|
+
const sessionId = options.sessionId.toLowerCase();
|
|
36033
|
+
return editClaudeSettings(resolve((options.env ?? process.env).CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), (settings) => {
|
|
36034
|
+
const hooks = settings.hooks ?? {};
|
|
36035
|
+
if (!record$3(hooks)) throw new Error("Invalid hook settings");
|
|
36036
|
+
for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
|
|
36037
|
+
if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
|
|
36038
|
+
const hook = {
|
|
36039
|
+
type: "command",
|
|
36040
|
+
command: process.execPath,
|
|
36041
|
+
args: [
|
|
36042
|
+
wrapperPath,
|
|
36043
|
+
cliPath,
|
|
36044
|
+
configDir,
|
|
36045
|
+
profileName,
|
|
36046
|
+
agentAddress,
|
|
36047
|
+
sessionId,
|
|
36048
|
+
HOOK_MARKER
|
|
36049
|
+
],
|
|
36050
|
+
asyncRewake: true,
|
|
36051
|
+
timeout: 604800
|
|
36052
|
+
};
|
|
36053
|
+
const pendingHook = {
|
|
36054
|
+
type: "command",
|
|
36055
|
+
command: process.execPath,
|
|
36056
|
+
args: [
|
|
36057
|
+
pendingPath,
|
|
36058
|
+
cliPath,
|
|
36059
|
+
configDir,
|
|
36060
|
+
profileName,
|
|
36061
|
+
agentAddress,
|
|
36062
|
+
sessionId,
|
|
36063
|
+
PENDING_MARKER
|
|
36064
|
+
],
|
|
36065
|
+
timeout: 10
|
|
36066
|
+
};
|
|
36067
|
+
const isOwnPendingHook = (candidate) => record$3(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[3] === profileName && candidate.args[5] === sessionId && candidate.args[6] === PENDING_MARKER;
|
|
36068
|
+
const isOwnHook = (candidate) => {
|
|
36069
|
+
if (!record$3(candidate) || !Array.isArray(candidate.args)) return false;
|
|
36070
|
+
if (typeof candidate.args[0] !== "string" || basename(candidate.args[0]) !== "claude-wake.mjs" || candidate.args[2] !== configDir || candidate.args[3] !== profileName) return false;
|
|
36071
|
+
if (candidate.args.length === 7 && candidate.args[5] === sessionId && candidate.args[6] === HOOK_MARKER) return true;
|
|
36072
|
+
if (candidate.args.length !== 6 || candidate.args[5] !== HOOK_MARKER) return false;
|
|
36073
|
+
try {
|
|
36074
|
+
const state = readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json"));
|
|
36075
|
+
return record$3(state) && state.session === sessionId;
|
|
36076
|
+
} catch {
|
|
36077
|
+
return false;
|
|
36078
|
+
}
|
|
36079
|
+
};
|
|
36080
|
+
const nextHooks = { ...hooks };
|
|
36081
|
+
for (const event of WAKE_EVENTS) nextHooks[event] = [...(hooks[event] ?? []).flatMap((entry) => {
|
|
36082
|
+
if (!record$3(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
36083
|
+
const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
|
|
36084
|
+
if (siblings.length === entry.hooks.length) return [entry];
|
|
36085
|
+
return siblings.length ? [{
|
|
36086
|
+
...entry,
|
|
36087
|
+
hooks: siblings
|
|
36088
|
+
}] : [];
|
|
36089
|
+
}), event === "SessionStart" ? {
|
|
36090
|
+
matcher: "resume",
|
|
36091
|
+
hooks: [hook]
|
|
36092
|
+
} : { hooks: [hook] }];
|
|
36093
|
+
nextHooks[PENDING_EVENT] = [...(hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
|
|
36094
|
+
if (!record$3(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
36095
|
+
const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
|
|
36096
|
+
if (siblings.length === entry.hooks.length) return [entry];
|
|
36097
|
+
return siblings.length ? [{
|
|
36098
|
+
...entry,
|
|
36099
|
+
hooks: siblings
|
|
36100
|
+
}] : [];
|
|
36101
|
+
}), { hooks: [pendingHook] }];
|
|
36102
|
+
return {
|
|
36103
|
+
...settings,
|
|
36104
|
+
hooks: nextHooks
|
|
36105
|
+
};
|
|
36106
|
+
}) ? "installed_unverified" : "unavailable";
|
|
36107
|
+
} catch {
|
|
36108
|
+
return "unavailable";
|
|
36109
|
+
}
|
|
36110
|
+
}
|
|
36111
|
+
/** Remove only this profile's exact-session hooks after disconnection. */
|
|
36112
|
+
function uninstallClaudeWakeHook(options) {
|
|
36113
|
+
try {
|
|
36114
|
+
const configDir = resolve(options.configDir);
|
|
36115
|
+
const profileName = agentProfileName(options.profileName);
|
|
36116
|
+
const agentAddress = mailAddress(options.agentAddress);
|
|
36117
|
+
if (!SESSION_UUID$1.test(options.sessionId)) return false;
|
|
36118
|
+
const sessionId = options.sessionId.toLowerCase();
|
|
36119
|
+
const claudeDir = resolve((options.env ?? process.env).CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
|
|
36120
|
+
if (!existsSync(join(claudeDir, "settings.json"))) return true;
|
|
36121
|
+
return editClaudeSettings(claudeDir, (settings) => {
|
|
36122
|
+
const hooks = settings.hooks ?? {};
|
|
36123
|
+
if (!record$3(hooks)) throw new Error("Invalid hook settings");
|
|
36124
|
+
for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
|
|
36125
|
+
if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
|
|
36126
|
+
const legacySessionMatches = () => {
|
|
36127
|
+
try {
|
|
36128
|
+
const setup = readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json"));
|
|
36129
|
+
return record$3(setup) && setup.session === sessionId;
|
|
36130
|
+
} catch {
|
|
36131
|
+
return false;
|
|
36132
|
+
}
|
|
36133
|
+
};
|
|
36134
|
+
const isOwnHook = (candidate) => {
|
|
36135
|
+
if (!record$3(candidate) || !Array.isArray(candidate.args)) return false;
|
|
36136
|
+
const args = candidate.args;
|
|
36137
|
+
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());
|
|
36138
|
+
};
|
|
36139
|
+
const isOwnPendingHook = (candidate) => {
|
|
36140
|
+
if (!record$3(candidate) || !Array.isArray(candidate.args)) return false;
|
|
36141
|
+
const args = candidate.args;
|
|
36142
|
+
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;
|
|
36143
|
+
};
|
|
36144
|
+
let removed = false;
|
|
36145
|
+
const nextHooks = { ...hooks };
|
|
36146
|
+
for (const event of WAKE_EVENTS) nextHooks[event] = (hooks[event] ?? []).flatMap((entry) => {
|
|
36147
|
+
if (!record$3(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
36148
|
+
const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
|
|
36149
|
+
if (siblings.length === entry.hooks.length) return [entry];
|
|
36150
|
+
removed = true;
|
|
36151
|
+
return siblings.length ? [{
|
|
36152
|
+
...entry,
|
|
36153
|
+
hooks: siblings
|
|
36154
|
+
}] : [];
|
|
36155
|
+
});
|
|
36156
|
+
nextHooks[PENDING_EVENT] = (hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
|
|
36157
|
+
if (!record$3(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
36158
|
+
const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
|
|
36159
|
+
if (siblings.length === entry.hooks.length) return [entry];
|
|
36160
|
+
removed = true;
|
|
36161
|
+
return siblings.length ? [{
|
|
36162
|
+
...entry,
|
|
36163
|
+
hooks: siblings
|
|
36164
|
+
}] : [];
|
|
36165
|
+
});
|
|
36166
|
+
return removed ? {
|
|
36167
|
+
...settings,
|
|
36168
|
+
hooks: nextHooks
|
|
36169
|
+
} : null;
|
|
36170
|
+
});
|
|
36171
|
+
} catch {
|
|
36172
|
+
return false;
|
|
36173
|
+
}
|
|
36174
|
+
}
|
|
36175
|
+
//#endregion
|
|
36176
|
+
//#region src/oclif/agent-connect.ts
|
|
36177
|
+
const MAX_INVITATION_BYTES = 4096;
|
|
36178
|
+
const MAX_RESPONSE_BYTES$2 = 32768;
|
|
36179
|
+
const UNCERTAIN_CLAIM = "Setup did not complete safely. The invitation may have been consumed. Request a fresh invitation from the owner; do not retry this invitation.";
|
|
36180
|
+
/** Offline metadata only; a saved profile is not proof of current API or listener readiness. */
|
|
36181
|
+
function agentConnectionStatus(configDir, profileName, env = process.env) {
|
|
36182
|
+
agentProfileName(profileName);
|
|
36183
|
+
const profile = loadConnectedAgentProfile(configDir, profileName);
|
|
36184
|
+
const setup = profile ? readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json")) : null;
|
|
36185
|
+
const saved = setup && typeof setup === "object" && !Array.isArray(setup) ? setup : null;
|
|
36186
|
+
const session = saved && typeof saved.session === "string" && SESSION_UUID$1.test(saved.session) ? saved.session : null;
|
|
36187
|
+
const mode = saved?.receiverMode === "external" || saved?.receiverMode === "poll" ? saved.receiverMode : "native";
|
|
36188
|
+
const receiving = profile && saved && mode === "poll" ? {
|
|
36189
|
+
mode,
|
|
36190
|
+
sessionId: session,
|
|
36191
|
+
state: "poll",
|
|
36192
|
+
reason: "agent_checks_mail",
|
|
36193
|
+
lastSuccessfulMailCheckAt: null,
|
|
36194
|
+
liveness: "unknown"
|
|
36195
|
+
} : profile && session && mode === "native" ? (() => {
|
|
36196
|
+
const scope = notificationScope(profile.api_base_url, profile.api_key);
|
|
36197
|
+
const listener = backgroundListenStatus({
|
|
36198
|
+
configDir,
|
|
36199
|
+
scope,
|
|
36200
|
+
threadId: session
|
|
36201
|
+
});
|
|
36202
|
+
const mailOwner = readSharedMailOwner({ directory: join(configDir, "shared-mail", createHash("sha256").update(scope).digest("hex")) });
|
|
36203
|
+
return {
|
|
36204
|
+
mode,
|
|
36205
|
+
sessionId: session,
|
|
36206
|
+
state: listener.healthy ? listener.phase === "receiving" ? "running" : "degraded" : listener.reason === "restarting" ? "degraded" : listener.reason === "absent" ? "unknown" : "down",
|
|
36207
|
+
reason: listener.reason ?? listener.failureCode,
|
|
36208
|
+
failureCode: listener.failureCode,
|
|
36209
|
+
detail: listener.detail ?? null,
|
|
36210
|
+
lastSuccessfulMailCheckAt: mailOwner?.lastMailCheckAt ?? null,
|
|
36211
|
+
liveness: listener.healthy && listener.phase === "receiving" ? "live" : "unknown",
|
|
36212
|
+
listener
|
|
36213
|
+
};
|
|
36214
|
+
})() : profile && session ? (() => {
|
|
36215
|
+
const hook = claudeWakeHookStatus({
|
|
36216
|
+
configDir,
|
|
36217
|
+
profileName,
|
|
36218
|
+
agentAddress: profile.agent_address,
|
|
36219
|
+
sessionId: session,
|
|
36220
|
+
env
|
|
36221
|
+
});
|
|
36222
|
+
return {
|
|
36223
|
+
mode,
|
|
36224
|
+
sessionId: session,
|
|
36225
|
+
state: hook.installed ? "unknown" : "down",
|
|
36226
|
+
reason: hook.installed ? "hook_liveness_unverified" : "hook_missing",
|
|
36227
|
+
detail: hook.installed ? null : "The receive hook for this session is missing, so mail will not wake this session. Run `primitive machine doctor --fix` to reinstall it.",
|
|
36228
|
+
lastSuccessfulMailCheckAt: null,
|
|
36229
|
+
liveness: "unknown",
|
|
36230
|
+
hook
|
|
36231
|
+
};
|
|
36232
|
+
})() : {
|
|
36233
|
+
mode: "unknown",
|
|
36234
|
+
sessionId: null,
|
|
36235
|
+
state: "unknown",
|
|
36236
|
+
reason: "session_not_configured",
|
|
36237
|
+
lastSuccessfulMailCheckAt: null,
|
|
36238
|
+
liveness: "unknown"
|
|
36239
|
+
};
|
|
36240
|
+
return profile ? {
|
|
36241
|
+
status: "configured",
|
|
36242
|
+
identity: connectedAgentIdentity(profileName, profile),
|
|
36243
|
+
receiving
|
|
36244
|
+
} : {
|
|
36245
|
+
status: "not_configured",
|
|
36246
|
+
profileName
|
|
36247
|
+
};
|
|
36248
|
+
}
|
|
36249
|
+
function parseAgentInvitation(input) {
|
|
36250
|
+
try {
|
|
36251
|
+
if (Buffer.byteLength(input) > MAX_INVITATION_BYTES) throw new Error();
|
|
36252
|
+
const text = input.trim();
|
|
36253
|
+
let token;
|
|
36254
|
+
let base;
|
|
36255
|
+
if (text.startsWith("{")) {
|
|
36256
|
+
const value = JSON.parse(text);
|
|
36257
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
36258
|
+
const row = value;
|
|
36259
|
+
if (Object.keys(row).some((key) => key !== "token" && key !== "api_base_url")) throw new Error();
|
|
36260
|
+
token = row.token;
|
|
36261
|
+
base = row.api_base_url ?? "https://api.primitive.dev/v1";
|
|
36262
|
+
} else {
|
|
36263
|
+
const url = new URL(text);
|
|
36264
|
+
if (url.username || url.password || url.search || url.pathname !== "/v1/agent-connections/setup") throw new Error();
|
|
36265
|
+
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
36266
|
+
if (Array.from(fragment.keys()).length !== 1 || fragment.getAll("token").length !== 1) throw new Error();
|
|
36267
|
+
token = fragment.get("token");
|
|
36268
|
+
base = `${url.origin}/v1`;
|
|
36269
|
+
}
|
|
36270
|
+
if (typeof token !== "string" || token.length < 32 || token.length > 256 || /\s/.test(token) || Array.from(token).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw new Error();
|
|
36271
|
+
return {
|
|
36272
|
+
token,
|
|
36273
|
+
apiBaseUrl: connectedApiBaseUrl(base)
|
|
36274
|
+
};
|
|
36275
|
+
} catch {
|
|
36276
|
+
throw new AgentConnectionSetupError("Pipe the private Primitive setup URL or a JSON object containing token and optional api_base_url to stdin. Invitation values are never accepted as command arguments.");
|
|
36277
|
+
}
|
|
36278
|
+
}
|
|
36279
|
+
async function readAgentInvitation(input, isTTY) {
|
|
36280
|
+
if (isTTY) throw new AgentConnectionSetupError("Pipe the private setup invitation to stdin.");
|
|
36281
|
+
const chunks = [];
|
|
36282
|
+
let size = 0;
|
|
36283
|
+
for await (const chunk of input) {
|
|
36284
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
36285
|
+
size += bytes.length;
|
|
36286
|
+
if (size > MAX_INVITATION_BYTES) throw new AgentConnectionSetupError("Setup invitation exceeds the input limit.");
|
|
36287
|
+
chunks.push(bytes);
|
|
36288
|
+
}
|
|
36289
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
36290
|
+
}
|
|
36291
|
+
async function readClaimResponse(response) {
|
|
36292
|
+
const reader = response.body?.getReader();
|
|
36293
|
+
if (!reader) throw new Error();
|
|
36294
|
+
const chunks = [];
|
|
36295
|
+
let size = 0;
|
|
36296
|
+
try {
|
|
36297
|
+
for (;;) {
|
|
36298
|
+
const part = await reader.read();
|
|
36299
|
+
if (part.done) break;
|
|
36300
|
+
size += part.value.byteLength;
|
|
36301
|
+
if (size > MAX_RESPONSE_BYTES$2) throw new Error();
|
|
36302
|
+
chunks.push(part.value);
|
|
36303
|
+
}
|
|
36304
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
36305
|
+
} finally {
|
|
36306
|
+
await reader.cancel().catch(() => void 0);
|
|
36307
|
+
}
|
|
36308
|
+
}
|
|
36309
|
+
function profileFromClaim(value, invitation, invitationHash, now) {
|
|
36310
|
+
const response = value;
|
|
36311
|
+
const data = response?.data;
|
|
36312
|
+
const connection = data?.connection;
|
|
36313
|
+
if (response?.success !== true || !data || !connection || data.api_base_url !== invitation.apiBaseUrl || data.owner_address !== connection.owner_address || !["claimed", "connected"].includes(String(connection.status))) throw new Error();
|
|
36314
|
+
let ownerMember;
|
|
36315
|
+
try {
|
|
36316
|
+
ownerMember = Object.hasOwn(data, "owner_member_address") ? parseOwnerMemberAddress(data.owner_member_address, mailAddress(data.owner_address), mailAddress(connection.address)) : void 0;
|
|
36317
|
+
} catch {
|
|
36318
|
+
ownerMember = void 0;
|
|
36319
|
+
}
|
|
36320
|
+
return parseConnectedAgentProfile({
|
|
36321
|
+
version: 1,
|
|
36322
|
+
auth_method: "agent_connection",
|
|
36323
|
+
api_key: data.api_key,
|
|
36324
|
+
api_base_url: data.api_base_url,
|
|
36325
|
+
org_id: data.org_id,
|
|
36326
|
+
agent_address: connection.address,
|
|
36327
|
+
owner_address: data.owner_address,
|
|
36328
|
+
...ownerMember === void 0 ? {} : { owner_member_address: ownerMember },
|
|
36329
|
+
invitation_hash: invitationHash,
|
|
36330
|
+
created_at: new Date(now()).toISOString(),
|
|
36331
|
+
...data.presence_profile === void 0 ? {} : { presence_profile: data.presence_profile }
|
|
36332
|
+
});
|
|
36333
|
+
}
|
|
36334
|
+
/** Claims once; saved identity is local configuration, not proof of email readiness. */
|
|
36335
|
+
async function connectAgent(params) {
|
|
36336
|
+
const profileName = agentProfileName(params.profileName);
|
|
36337
|
+
const invitation = parseAgentInvitation(params.invitation);
|
|
36338
|
+
const invitationHash = createHash("sha256").update(invitation.apiBaseUrl).update("\0").update(invitation.token).digest("hex");
|
|
36339
|
+
const directory = agentProfilesDirectory(params.configDir);
|
|
36340
|
+
let release;
|
|
36341
|
+
try {
|
|
36342
|
+
privateMailDirectory(directory, true);
|
|
36343
|
+
release = acquireListenLock(directory, "agent-connection-setup");
|
|
36344
|
+
} catch {
|
|
36345
|
+
throw new AgentConnectionSetupError("Agent setup is already running, or its private directory is unavailable. No invitation was submitted.");
|
|
36346
|
+
}
|
|
36347
|
+
try {
|
|
36348
|
+
const existing = loadConnectedAgentProfile(params.configDir, profileName);
|
|
36349
|
+
if (existing) {
|
|
36350
|
+
if (existing.invitation_hash !== invitationHash) throw new AgentConnectionSetupError("This agent profile is already configured. Its identity and credentials were not changed. Use a separate profile for another invitation.");
|
|
36351
|
+
return {
|
|
36352
|
+
status: "already_configured",
|
|
36353
|
+
identity: connectedAgentIdentity(profileName, existing)
|
|
36354
|
+
};
|
|
36355
|
+
}
|
|
36356
|
+
privateMailDirectory(join(directory, "profiles"), true);
|
|
36357
|
+
privateMailDirectory(agentProfileDirectory(params.configDir, profileName), true);
|
|
36358
|
+
const journal = join(directory, "claims", `${invitationHash}.json`);
|
|
36359
|
+
privateMailDirectory(join(directory, "claims"), true);
|
|
36360
|
+
if (readMailJson(journal) !== null) throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
36361
|
+
writeMailJson(journal, {
|
|
36362
|
+
version: 1,
|
|
36363
|
+
profile_name: profileName,
|
|
36364
|
+
status: "attempted"
|
|
36365
|
+
});
|
|
36366
|
+
try {
|
|
36367
|
+
const response = await (params.fetch ?? fetch)(`${invitation.apiBaseUrl}/agent-connections/claim`, {
|
|
36368
|
+
method: "POST",
|
|
36369
|
+
redirect: "error",
|
|
36370
|
+
signal: AbortSignal.timeout(25e3),
|
|
36371
|
+
headers: {
|
|
36372
|
+
"content-type": "application/json",
|
|
36373
|
+
accept: "application/json"
|
|
36374
|
+
},
|
|
36375
|
+
body: JSON.stringify({
|
|
36376
|
+
token: invitation.token,
|
|
36377
|
+
...params.presence ? { capabilities: ["primitive.presence/1"] } : {}
|
|
36378
|
+
})
|
|
36379
|
+
});
|
|
36380
|
+
if (response.status !== 200) {
|
|
36381
|
+
await response.body?.cancel();
|
|
36382
|
+
throw new Error();
|
|
36383
|
+
}
|
|
36384
|
+
const profile = profileFromClaim(await readClaimResponse(response), invitation, invitationHash, params.now ?? Date.now);
|
|
36385
|
+
saveConnectedAgentProfile(params.configDir, profileName, profile);
|
|
36386
|
+
return {
|
|
36387
|
+
status: "claimed",
|
|
36388
|
+
identity: connectedAgentIdentity(profileName, profile)
|
|
36389
|
+
};
|
|
36390
|
+
} catch {
|
|
36391
|
+
throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
36392
|
+
}
|
|
36393
|
+
} finally {
|
|
36394
|
+
release();
|
|
36395
|
+
}
|
|
36396
|
+
}
|
|
36397
|
+
//#endregion
|
|
36398
|
+
//#region src/oclif/contacts.ts
|
|
36399
|
+
var ContactsApiError = class extends Error {
|
|
36400
|
+
directoryAvailable = false;
|
|
36401
|
+
constructor(payload) {
|
|
36402
|
+
super("Contact request failed.");
|
|
36403
|
+
this.payload = payload;
|
|
35590
36404
|
}
|
|
35591
36405
|
};
|
|
35592
36406
|
function contactAddress(value) {
|
|
@@ -36025,7 +36839,7 @@ function matchesContactSelector(input, sender) {
|
|
|
36025
36839
|
}
|
|
36026
36840
|
//#endregion
|
|
36027
36841
|
//#region src/oclif/contact-policy.ts
|
|
36028
|
-
function record$
|
|
36842
|
+
function record$2(value) {
|
|
36029
36843
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid contact policy.");
|
|
36030
36844
|
return value;
|
|
36031
36845
|
}
|
|
@@ -36034,11 +36848,11 @@ function fingerprint(value) {
|
|
|
36034
36848
|
return value;
|
|
36035
36849
|
}
|
|
36036
36850
|
function document(value, organization) {
|
|
36037
|
-
const row = record$
|
|
36851
|
+
const row = record$2(value);
|
|
36038
36852
|
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.");
|
|
36039
36853
|
const seen = /* @__PURE__ */ new Set();
|
|
36040
36854
|
const rules = row.rules.map((value) => {
|
|
36041
|
-
const rule = record$
|
|
36855
|
+
const rule = record$2(value);
|
|
36042
36856
|
if (typeof rule.pattern !== "string" || rule.effect !== "allow" && rule.effect !== "silence") throw new Error("Invalid contact policy rule.");
|
|
36043
36857
|
const pattern = canonicalContactSelector({
|
|
36044
36858
|
kind: "pattern",
|
|
@@ -36079,7 +36893,7 @@ function document(value, organization) {
|
|
|
36079
36893
|
};
|
|
36080
36894
|
}
|
|
36081
36895
|
function parseAgentContactPolicy(value, recipient) {
|
|
36082
|
-
const row = record$
|
|
36896
|
+
const row = record$2(value);
|
|
36083
36897
|
if (row.agent_address !== recipient || typeof row.allow_contact_requests !== "boolean") throw new Error("Contact policy recipient mismatch.");
|
|
36084
36898
|
const org = document(row.org_policy, true);
|
|
36085
36899
|
const agent = document(row.agent_policy, false);
|
|
@@ -38679,6 +39493,33 @@ function enrollmentAddress(configDir, profileName) {
|
|
|
38679
39493
|
};
|
|
38680
39494
|
}
|
|
38681
39495
|
}
|
|
39496
|
+
/**
|
|
39497
|
+
* Saved profiles, other than the session's own, whose setup says they were
|
|
39498
|
+
* connected for this exact session. Sorted for a stable choice.
|
|
39499
|
+
*/
|
|
39500
|
+
function profilesBoundToSession(configDir, sessionId, ownProfile, mode) {
|
|
39501
|
+
let names;
|
|
39502
|
+
try {
|
|
39503
|
+
names = readdirSync(join(agentProfilesDirectory(configDir), "profiles"));
|
|
39504
|
+
} catch {
|
|
39505
|
+
return [];
|
|
39506
|
+
}
|
|
39507
|
+
const bound = [];
|
|
39508
|
+
for (const name of names.sort()) {
|
|
39509
|
+
if (name === ownProfile) continue;
|
|
39510
|
+
try {
|
|
39511
|
+
const setup = readMailJson(join(agentProfileDirectory(configDir, name), "setup.json"));
|
|
39512
|
+
const saved = setup?.receiverMode === void 0 ? "native" : setup.receiverMode;
|
|
39513
|
+
if (typeof setup?.session !== "string" || setup.session.toLowerCase() !== sessionId || saved !== mode) continue;
|
|
39514
|
+
const profile = loadConnectedAgentProfile(configDir, name);
|
|
39515
|
+
if (profile) bound.push({
|
|
39516
|
+
name,
|
|
39517
|
+
profile
|
|
39518
|
+
});
|
|
39519
|
+
} catch {}
|
|
39520
|
+
}
|
|
39521
|
+
return bound;
|
|
39522
|
+
}
|
|
38682
39523
|
/** `agent disconnect` already confirmed revocation of this profile's credential. */
|
|
38683
39524
|
function disconnectConfirmedLocally(configDir, profileName) {
|
|
38684
39525
|
try {
|
|
@@ -38815,7 +39656,34 @@ async function registerSession(options) {
|
|
|
38815
39656
|
detail: "The session ID is not a UUID. Nothing was changed."
|
|
38816
39657
|
};
|
|
38817
39658
|
const sessionId = session.toLowerCase();
|
|
38818
|
-
const
|
|
39659
|
+
const ownProfile = `session-${sessionId}`;
|
|
39660
|
+
let profileName = ownProfile;
|
|
39661
|
+
try {
|
|
39662
|
+
const saved = readSessionRecord(options.configDir, sessionId);
|
|
39663
|
+
if (saved?.createdBy === "existing" && saved.profile !== ownProfile) profileName = agentProfileName(saved.profile);
|
|
39664
|
+
else if (!saved && !loadConnectedAgentProfile(options.configDir, ownProfile) && !enrollmentAddress(options.configDir, ownProfile).exists) {
|
|
39665
|
+
let offline = false;
|
|
39666
|
+
for (const candidate of profilesBoundToSession(options.configDir, sessionId, ownProfile, runtime === "codex" ? "native" : "external")) {
|
|
39667
|
+
const state = await deps.connectionState(candidate.profile);
|
|
39668
|
+
if (state === "connected") {
|
|
39669
|
+
profileName = candidate.name;
|
|
39670
|
+
offline = false;
|
|
39671
|
+
break;
|
|
39672
|
+
}
|
|
39673
|
+
if (state === "unavailable") offline = true;
|
|
39674
|
+
}
|
|
39675
|
+
if (offline) return {
|
|
39676
|
+
...base,
|
|
39677
|
+
session: sessionId,
|
|
39678
|
+
receiving: RECEIVING[runtime],
|
|
39679
|
+
status: "offline",
|
|
39680
|
+
detail: "A saved profile is connected for this session, but its status could not be checked right now. No new address was created."
|
|
39681
|
+
};
|
|
39682
|
+
}
|
|
39683
|
+
} catch {
|
|
39684
|
+
profileName = ownProfile;
|
|
39685
|
+
}
|
|
39686
|
+
const reused = profileName !== ownProfile;
|
|
38819
39687
|
const known = {
|
|
38820
39688
|
...base,
|
|
38821
39689
|
session: sessionId,
|
|
@@ -38848,7 +39716,7 @@ async function registerSession(options) {
|
|
|
38848
39716
|
let record = readSessionRecord(options.configDir, sessionId);
|
|
38849
39717
|
if (record?.endedAt) return await ended(record);
|
|
38850
39718
|
const finishAgentInfo = async () => {
|
|
38851
|
-
if (record?.agentInfo) return;
|
|
39719
|
+
if (reused || record?.agentInfo) return;
|
|
38852
39720
|
const seeded = await deps.seedAgentInfo(options.configDir, profileName, agentInfoForSession(runtime, cwd));
|
|
38853
39721
|
if (seeded !== "created" && seeded !== "already_present") return;
|
|
38854
39722
|
record = await updateSessionRecord(options.configDir, sessionId, (current) => current ? {
|
|
@@ -46081,7 +46949,7 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
46081
46949
|
url: "https://primitive.dev"
|
|
46082
46950
|
};
|
|
46083
46951
|
const SDK_VERSION_RANGE = "^1.34.0";
|
|
46084
|
-
const CLI_VERSION_RANGE = "^1.
|
|
46952
|
+
const CLI_VERSION_RANGE = "^1.41.0";
|
|
46085
46953
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
46086
46954
|
function renderHandler() {
|
|
46087
46955
|
return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
|
|
@@ -50526,6 +51394,22 @@ const RESUME_LOCK_RETRY_STEP_MS = 250;
|
|
|
50526
51394
|
function isListenLockContention(error) {
|
|
50527
51395
|
return error instanceof ListenStateError && error.message.startsWith("Another listener is using this subscription");
|
|
50528
51396
|
}
|
|
51397
|
+
const CLAUDE_NOTIFY_SESSION_GUIDANCE = "This is a Claude Code session. Claude Code sessions receive mail through hooks, not --notify-session. Run `primitive agent connect --status --profile <profile>` to check the hook, or `primitive machine doctor --fix` to install it.";
|
|
51398
|
+
/**
|
|
51399
|
+
* The session is a Claude Code session rather than a Codex thread: it is the
|
|
51400
|
+
* current Claude session, or this machine registered it as one.
|
|
51401
|
+
*/
|
|
51402
|
+
function claudeCodeSession(sessionId, env, configDir) {
|
|
51403
|
+
const id = sessionId.toLowerCase();
|
|
51404
|
+
if (env.CODEX_THREAD_ID?.toLowerCase() === id) return false;
|
|
51405
|
+
if (env.CLAUDE_CODE_SESSION_ID?.toLowerCase() === id) return true;
|
|
51406
|
+
try {
|
|
51407
|
+
const saved = readMailJson(join(configDir, "machine", "sessions", `${id}.json`));
|
|
51408
|
+
return saved?.session === id && saved.runtime === "claude";
|
|
51409
|
+
} catch {
|
|
51410
|
+
return false;
|
|
51411
|
+
}
|
|
51412
|
+
}
|
|
50529
51413
|
var ListenCommand = class ListenCommand extends Command {
|
|
50530
51414
|
static summary = "Receive webhook events locally without a public endpoint";
|
|
50531
51415
|
static description = "Subscribe once and reconnect using the same durable server queue. Connected-agent credentials automatically receive only their assigned address. Use --notify-session with an exact loaded session UUID and --contacts for saved contact preferences and eligible same-org network peers, or approved --sender addresses, for external mail events at tool-output authority, never synthetic user messages. Explicit silence still wins. Use a short --exec hook to durably accept an event, --forward-to for a local webhook, or newline-delimited JSON on stdout. Add --background to keep native receiving independent of the calling terminal process; --status reports receiver health and receipts, add --email-id for one email's routing evidence, and --stop stops that receiver. Status is JSON and stdout events are JSONL by default; --json is accepted explicitly without changing delivery mode. Notifications require an existing native local-session socket; the CLI subscribes only to the exact already-loaded thread and never launches a terminal or creates a session. Verified presence controls are handled without model turns and do not consume --once or --number. Claude hook capability: primitive-hook-profile-bound-v2.";
|
|
@@ -50737,6 +51621,7 @@ var ListenCommand = class ListenCommand extends Command {
|
|
|
50737
51621
|
}
|
|
50738
51622
|
if (flags.wake && !hookSessionId) throw new Errors.CLIError("--wake requires --hook-session with an exact session_id.");
|
|
50739
51623
|
if (flags["notify-session"] !== void 0 && !SESSION_UUID$1.test(flags["notify-session"])) throw new Errors.CLIError("--notify-session requires an exact session UUID.");
|
|
51624
|
+
if (flags["notify-session"] && !flags.status && !flags.stop && claudeCodeSession(flags["notify-session"], hookSessionId ? {} : process.env, this.config.configDir)) throw new Errors.CLIError(CLAUDE_NOTIFY_SESSION_GUIDANCE);
|
|
50740
51625
|
if (flags["notify-session"] && flags.transport === "poll") throw new Errors.CLIError("--notify-session requires --transport websocket.");
|
|
50741
51626
|
if (flags["notify-session"] && flags.subscription !== void 0) throw new Errors.CLIError("--notify-session uses the shared saved subscription; omit --subscription.");
|
|
50742
51627
|
if (flags.subscription?.trim().startsWith("local-mail-")) throw new Errors.CLIError("Subscription names beginning local-mail- are reserved for shared mail receiving.");
|
|
@@ -52010,426 +52895,83 @@ async function runSignupInteractiveWithCredentialLock(params) {
|
|
|
52010
52895
|
const verificationCode = await promptRequiredFn(`Verification code (${start.verification_code_length} digits): `);
|
|
52011
52896
|
if (verificationCode.toLowerCase() === "resend") {
|
|
52012
52897
|
const resend = await resendVerificationCode({
|
|
52013
|
-
apiBaseUrl,
|
|
52014
|
-
apiClient,
|
|
52015
|
-
configDir,
|
|
52016
|
-
deps,
|
|
52017
|
-
start
|
|
52018
|
-
});
|
|
52019
|
-
start = resend.pending;
|
|
52020
|
-
if (resend.resent) process$1.stderr.write(`Sent a new ${start.verification_code_length}-digit verification code. It expires in ${formatSignupSeconds(start.expires_in)}.\n`);
|
|
52021
|
-
continue;
|
|
52022
|
-
}
|
|
52023
|
-
try {
|
|
52024
|
-
await runSignupConfirmWithCredentialLock({
|
|
52025
|
-
code: verificationCode,
|
|
52026
|
-
configDir,
|
|
52027
|
-
deps,
|
|
52028
|
-
email: start.email,
|
|
52029
|
-
flags: {
|
|
52030
|
-
"api-base-url": flags["api-base-url"],
|
|
52031
|
-
force: true
|
|
52032
|
-
},
|
|
52033
|
-
skipExistingCredentialCheck: true
|
|
52034
|
-
});
|
|
52035
|
-
return;
|
|
52036
|
-
} catch (error) {
|
|
52037
|
-
if (error instanceof Errors.CLIError && error.message.startsWith("Invalid verification code.")) {
|
|
52038
|
-
process$1.stderr.write("Invalid verification code. Try again or type `resend`.\n");
|
|
52039
|
-
continue;
|
|
52040
|
-
}
|
|
52041
|
-
throw error;
|
|
52042
|
-
}
|
|
52043
|
-
}
|
|
52044
|
-
}
|
|
52045
|
-
function commonStartFlags() {
|
|
52046
|
-
return {
|
|
52047
|
-
"accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
|
|
52048
|
-
"api-base-url": Flags.string({
|
|
52049
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52050
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
52051
|
-
hidden: true
|
|
52052
|
-
}),
|
|
52053
|
-
"device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
|
|
52054
|
-
force: Flags.boolean({
|
|
52055
|
-
char: "f",
|
|
52056
|
-
description: "Replace saved credentials or pending signup state when needed"
|
|
52057
|
-
}),
|
|
52058
|
-
"signup-code": Flags.string({
|
|
52059
|
-
description: "Optional signup code. Omit if you do not have one.",
|
|
52060
|
-
env: "PRIMITIVE_SIGNUP_CODE"
|
|
52061
|
-
})
|
|
52062
|
-
};
|
|
52063
|
-
}
|
|
52064
|
-
var SignupCommand = class SignupCommand extends Command {
|
|
52065
|
-
static args = { email: Args.string({
|
|
52066
|
-
description: "Email address to sign up",
|
|
52067
|
-
required: false
|
|
52068
|
-
}) };
|
|
52069
|
-
static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
|
|
52070
|
-
static summary = "Start account signup";
|
|
52071
|
-
static examples = [
|
|
52072
|
-
"<%= config.bin %> signup user@example.com",
|
|
52073
|
-
"<%= config.bin %> signup user@example.com --accept-terms",
|
|
52074
|
-
"<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
|
|
52075
|
-
"<%= config.bin %> signup confirm user@example.com 123456"
|
|
52076
|
-
];
|
|
52077
|
-
static flags = commonStartFlags();
|
|
52078
|
-
async run() {
|
|
52079
|
-
const { args, flags } = await this.parse(SignupCommand);
|
|
52080
|
-
let releaseCredentialsLock;
|
|
52081
|
-
try {
|
|
52082
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52083
|
-
} catch (error) {
|
|
52084
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52085
|
-
}
|
|
52086
|
-
try {
|
|
52087
|
-
await runSignupStartWithCredentialLock({
|
|
52088
|
-
configDir: this.config.configDir,
|
|
52089
|
-
email: args.email,
|
|
52090
|
-
flags
|
|
52091
|
-
});
|
|
52092
|
-
} finally {
|
|
52093
|
-
releaseCredentialsLock();
|
|
52094
|
-
}
|
|
52095
|
-
}
|
|
52096
|
-
};
|
|
52097
|
-
function resolveVerificationCode(input) {
|
|
52098
|
-
const sources = [
|
|
52099
|
-
input.positional !== void 0 ? "positional" : null,
|
|
52100
|
-
input.fromStdin === true ? "--code-from-stdin" : null,
|
|
52101
|
-
input.fromFile !== void 0 ? "--code-from-file" : null,
|
|
52102
|
-
input.fromEnv !== void 0 ? "--code-from-env" : null
|
|
52103
|
-
].filter((v) => v !== null);
|
|
52104
|
-
if (sources.length === 0) return {
|
|
52105
|
-
kind: "error",
|
|
52106
|
-
message: "Pass the verification code as a positional argument or via one of --code-from-stdin, --code-from-file, or --code-from-env."
|
|
52107
|
-
};
|
|
52108
|
-
if (sources.length > 1) return {
|
|
52109
|
-
kind: "error",
|
|
52110
|
-
message: `Pass exactly one source for the verification code; got ${sources.join(", ")}.`
|
|
52111
|
-
};
|
|
52112
|
-
if (input.positional !== void 0) return {
|
|
52113
|
-
kind: "ok",
|
|
52114
|
-
code: input.positional
|
|
52115
|
-
};
|
|
52116
|
-
if (input.fromEnv !== void 0) {
|
|
52117
|
-
const value = (input.env ?? process$1.env)[input.fromEnv];
|
|
52118
|
-
if (value === void 0) return {
|
|
52119
|
-
kind: "error",
|
|
52120
|
-
message: `--code-from-env ${input.fromEnv}: environment variable is not set.`
|
|
52121
|
-
};
|
|
52122
|
-
return {
|
|
52123
|
-
kind: "ok",
|
|
52124
|
-
code: stripTrailingNewline(value)
|
|
52125
|
-
};
|
|
52126
|
-
}
|
|
52127
|
-
if (input.fromFile !== void 0) {
|
|
52128
|
-
const readFile = input.readFile ?? defaultReadCodeFile;
|
|
52129
|
-
try {
|
|
52130
|
-
return {
|
|
52131
|
-
kind: "ok",
|
|
52132
|
-
code: stripTrailingNewline(readFile(input.fromFile))
|
|
52133
|
-
};
|
|
52134
|
-
} catch (error) {
|
|
52135
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
52136
|
-
return {
|
|
52137
|
-
kind: "error",
|
|
52138
|
-
message: `--code-from-file ${input.fromFile}: could not read file: ${detail}`
|
|
52139
|
-
};
|
|
52140
|
-
}
|
|
52141
|
-
}
|
|
52142
|
-
const readStdin = input.readStdin ?? defaultReadCodeStdin;
|
|
52143
|
-
try {
|
|
52144
|
-
return {
|
|
52145
|
-
kind: "ok",
|
|
52146
|
-
code: stripTrailingNewline(readStdin())
|
|
52147
|
-
};
|
|
52148
|
-
} catch (error) {
|
|
52149
|
-
return {
|
|
52150
|
-
kind: "error",
|
|
52151
|
-
message: `--code-from-stdin: ${error instanceof Error ? error.message : String(error)}`
|
|
52152
|
-
};
|
|
52153
|
-
}
|
|
52154
|
-
}
|
|
52155
|
-
function stripTrailingNewline(value) {
|
|
52156
|
-
return value.replace(/\r?\n$/, "");
|
|
52157
|
-
}
|
|
52158
|
-
function defaultReadCodeFile(path) {
|
|
52159
|
-
return readFileSync(path, "utf8");
|
|
52160
|
-
}
|
|
52161
|
-
function defaultReadCodeStdin() {
|
|
52162
|
-
if (process$1.stdin.isTTY) throw new Error("stdin is a TTY; pipe the code into this command or use --code-from-file / --code-from-env instead.");
|
|
52163
|
-
return readFileSync(0, "utf8");
|
|
52164
|
-
}
|
|
52165
|
-
var SignupConfirmCommand = class SignupConfirmCommand extends Command {
|
|
52166
|
-
static args = {
|
|
52167
|
-
email: Args.string({
|
|
52168
|
-
description: "Email address used to start signup",
|
|
52169
|
-
required: true
|
|
52170
|
-
}),
|
|
52171
|
-
code: Args.string({
|
|
52172
|
-
description: "Verification code from the signup email. Optional when one of --code-from-stdin / --code-from-file / --code-from-env is passed; exactly one source must be set.",
|
|
52173
|
-
required: false
|
|
52174
|
-
})
|
|
52175
|
-
};
|
|
52176
|
-
static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
|
|
52177
|
-
static summary = "Confirm account signup";
|
|
52178
|
-
static examples = [
|
|
52179
|
-
"<%= config.bin %> signup confirm user@example.com 123456",
|
|
52180
|
-
"<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000",
|
|
52181
|
-
"read -rs CODE && CODE=\"$CODE\" <%= config.bin %> signup confirm user@example.com --code-from-env CODE && unset CODE",
|
|
52182
|
-
"read -rs CODE && printf '%s' \"$CODE\" | <%= config.bin %> signup confirm user@example.com --code-from-stdin && unset CODE",
|
|
52183
|
-
"<%= config.bin %> signup confirm user@example.com --code-from-file /run/user/$(id -u)/verification-code"
|
|
52184
|
-
];
|
|
52185
|
-
static flags = {
|
|
52186
|
-
"api-base-url": Flags.string({
|
|
52187
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52188
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
52189
|
-
hidden: true
|
|
52190
|
-
}),
|
|
52191
|
-
"code-from-stdin": Flags.boolean({ description: "Read the verification code from stdin instead of the positional argument. Use when an agent is constructing the command for the user to run, so the code never enters the agent's prompt context." }),
|
|
52192
|
-
"code-from-file": Flags.string({ description: "Read the verification code from a UTF-8 file at this path. Trailing newlines are stripped." }),
|
|
52193
|
-
"code-from-env": Flags.string({ description: "Read the verification code from this environment variable. Pair with `read -rs CODE && CODE=\"$CODE\" primitive signup confirm <email> --code-from-env CODE && unset CODE` so the value never appears on the command line or in shell history. Plain `read` creates a shell-local variable that child processes cannot see; the inline `CODE=\"$CODE\"` exports it for just the one command." }),
|
|
52194
|
-
force: Flags.boolean({
|
|
52195
|
-
char: "f",
|
|
52196
|
-
description: "Replace saved credentials after verification"
|
|
52197
|
-
}),
|
|
52198
|
-
"org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
|
|
52199
|
-
};
|
|
52200
|
-
async run() {
|
|
52201
|
-
const { args, flags } = await this.parse(SignupConfirmCommand);
|
|
52202
|
-
const resolvedCode = resolveVerificationCode({
|
|
52203
|
-
positional: args.code,
|
|
52204
|
-
fromStdin: flags["code-from-stdin"] === true,
|
|
52205
|
-
fromFile: flags["code-from-file"],
|
|
52206
|
-
fromEnv: flags["code-from-env"]
|
|
52207
|
-
});
|
|
52208
|
-
if (resolvedCode.kind === "error") throw cliError$2(resolvedCode.message);
|
|
52209
|
-
let releaseCredentialsLock;
|
|
52210
|
-
try {
|
|
52211
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52212
|
-
} catch (error) {
|
|
52213
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52214
|
-
}
|
|
52215
|
-
try {
|
|
52216
|
-
await runSignupConfirmWithCredentialLock({
|
|
52217
|
-
code: resolvedCode.code,
|
|
52218
|
-
configDir: this.config.configDir,
|
|
52219
|
-
email: args.email,
|
|
52220
|
-
flags
|
|
52221
|
-
});
|
|
52222
|
-
} finally {
|
|
52223
|
-
releaseCredentialsLock();
|
|
52224
|
-
}
|
|
52225
|
-
}
|
|
52226
|
-
};
|
|
52227
|
-
var SignupResendCommand = class SignupResendCommand extends Command {
|
|
52228
|
-
static args = { email: Args.string({
|
|
52229
|
-
description: "Email address used to start signup. Defaults to the saved pending signup.",
|
|
52230
|
-
required: false
|
|
52231
|
-
}) };
|
|
52232
|
-
static description = "Resend the verification code for a pending signup.";
|
|
52233
|
-
static summary = "Resend signup verification code";
|
|
52234
|
-
static examples = ["<%= config.bin %> signup resend", "<%= config.bin %> signup resend user@example.com"];
|
|
52235
|
-
static flags = { "api-base-url": Flags.string({
|
|
52236
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52237
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
52238
|
-
hidden: true
|
|
52239
|
-
}) };
|
|
52240
|
-
async run() {
|
|
52241
|
-
const { args, flags } = await this.parse(SignupResendCommand);
|
|
52242
|
-
let releaseCredentialsLock;
|
|
52243
|
-
try {
|
|
52244
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52245
|
-
} catch (error) {
|
|
52246
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52247
|
-
}
|
|
52248
|
-
try {
|
|
52249
|
-
await runSignupResendWithCredentialLock({
|
|
52250
|
-
configDir: this.config.configDir,
|
|
52251
|
-
email: args.email,
|
|
52252
|
-
flags
|
|
52253
|
-
});
|
|
52254
|
-
} finally {
|
|
52255
|
-
releaseCredentialsLock();
|
|
52256
|
-
}
|
|
52257
|
-
}
|
|
52258
|
-
};
|
|
52259
|
-
var SignupStatusCommand = class SignupStatusCommand extends Command {
|
|
52260
|
-
static args = { email: Args.string({
|
|
52261
|
-
description: "Email address expected in the pending signup",
|
|
52262
|
-
required: false
|
|
52263
|
-
}) };
|
|
52264
|
-
static description = "Inspect the locally saved pending Primitive signup state.";
|
|
52265
|
-
static summary = "Show pending signup status";
|
|
52266
|
-
static examples = [
|
|
52267
|
-
"<%= config.bin %> signup status",
|
|
52268
|
-
"<%= config.bin %> signup status user@example.com",
|
|
52269
|
-
"<%= config.bin %> signup status --json"
|
|
52270
|
-
];
|
|
52271
|
-
static flags = {
|
|
52272
|
-
"api-base-url": Flags.string({
|
|
52273
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52274
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
52275
|
-
hidden: true
|
|
52276
|
-
}),
|
|
52277
|
-
json: Flags.boolean({ description: "Print pending signup status as JSON" })
|
|
52278
|
-
};
|
|
52279
|
-
async run() {
|
|
52280
|
-
const { args, flags } = await this.parse(SignupStatusCommand);
|
|
52281
|
-
runSignupStatus({
|
|
52282
|
-
configDir: this.config.configDir,
|
|
52283
|
-
email: args.email,
|
|
52284
|
-
flags
|
|
52285
|
-
});
|
|
52286
|
-
}
|
|
52287
|
-
};
|
|
52288
|
-
var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
|
|
52289
|
-
static description = "Run the full signup flow in one interactive terminal session.";
|
|
52290
|
-
static summary = "Run interactive account signup";
|
|
52291
|
-
static examples = ["<%= config.bin %> signup interactive"];
|
|
52292
|
-
static flags = commonStartFlags();
|
|
52293
|
-
async run() {
|
|
52294
|
-
const { flags } = await this.parse(SignupInteractiveCommand);
|
|
52295
|
-
let releaseCredentialsLock;
|
|
52296
|
-
try {
|
|
52297
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52298
|
-
} catch (error) {
|
|
52299
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52300
|
-
}
|
|
52301
|
-
try {
|
|
52302
|
-
await runSignupInteractiveWithCredentialLock({
|
|
52303
|
-
configDir: this.config.configDir,
|
|
52304
|
-
flags
|
|
52305
|
-
});
|
|
52306
|
-
} finally {
|
|
52307
|
-
releaseCredentialsLock();
|
|
52308
|
-
}
|
|
52309
|
-
}
|
|
52310
|
-
};
|
|
52311
|
-
//#endregion
|
|
52312
|
-
//#region src/oclif/commands/logout.ts
|
|
52313
|
-
function cliError$1(message) {
|
|
52314
|
-
return new Errors.CLIError(message, { exit: 1 });
|
|
52315
|
-
}
|
|
52316
|
-
function unwrapData(value) {
|
|
52317
|
-
return value?.data ?? null;
|
|
52318
|
-
}
|
|
52319
|
-
function isSavedOAuthSessionExpiredError(error) {
|
|
52320
|
-
return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
|
|
52321
|
-
}
|
|
52322
|
-
async function runLogoutWithCredentialLock(params) {
|
|
52323
|
-
requireDefaultLoginProfile();
|
|
52324
|
-
const deps = {
|
|
52325
|
-
cliLogout,
|
|
52326
|
-
createAuthenticatedCliApiClient,
|
|
52327
|
-
...params.deps
|
|
52328
|
-
};
|
|
52329
|
-
let credentials;
|
|
52330
|
-
try {
|
|
52331
|
-
credentials = loadCliCredentials(params.configDir);
|
|
52332
|
-
} catch (error) {
|
|
52333
|
-
deleteCliCredentials(params.configDir);
|
|
52334
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
52335
|
-
process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
|
|
52336
|
-
process.exitCode = 1;
|
|
52337
|
-
return;
|
|
52338
|
-
}
|
|
52339
|
-
if (!credentials) throw cliError$1("Not logged in. Run `primitive signin` to create saved CLI credentials.");
|
|
52340
|
-
let authenticated;
|
|
52341
|
-
try {
|
|
52342
|
-
authenticated = await deps.createAuthenticatedCliApiClient({
|
|
52343
|
-
apiBaseUrl: params.flags["api-base-url"],
|
|
52344
|
-
configDir: params.configDir,
|
|
52345
|
-
credentialsLockHeld: true
|
|
52346
|
-
});
|
|
52347
|
-
} catch (error) {
|
|
52348
|
-
if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
|
|
52349
|
-
process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
|
|
52350
|
-
return;
|
|
52898
|
+
apiBaseUrl,
|
|
52899
|
+
apiClient,
|
|
52900
|
+
configDir,
|
|
52901
|
+
deps,
|
|
52902
|
+
start
|
|
52903
|
+
});
|
|
52904
|
+
start = resend.pending;
|
|
52905
|
+
if (resend.resent) process$1.stderr.write(`Sent a new ${start.verification_code_length}-digit verification code. It expires in ${formatSignupSeconds(start.expires_in)}.\n`);
|
|
52906
|
+
continue;
|
|
52351
52907
|
}
|
|
52352
|
-
|
|
52353
|
-
|
|
52354
|
-
|
|
52355
|
-
|
|
52356
|
-
|
|
52357
|
-
|
|
52358
|
-
|
|
52359
|
-
|
|
52360
|
-
|
|
52361
|
-
|
|
52362
|
-
|
|
52363
|
-
|
|
52364
|
-
deleteCliCredentials(params.configDir);
|
|
52365
|
-
writeErrorWithHints(payload);
|
|
52366
|
-
process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
|
|
52367
|
-
process.exitCode = 1;
|
|
52908
|
+
try {
|
|
52909
|
+
await runSignupConfirmWithCredentialLock({
|
|
52910
|
+
code: verificationCode,
|
|
52911
|
+
configDir,
|
|
52912
|
+
deps,
|
|
52913
|
+
email: start.email,
|
|
52914
|
+
flags: {
|
|
52915
|
+
"api-base-url": flags["api-base-url"],
|
|
52916
|
+
force: true
|
|
52917
|
+
},
|
|
52918
|
+
skipExistingCredentialCheck: true
|
|
52919
|
+
});
|
|
52368
52920
|
return;
|
|
52921
|
+
} catch (error) {
|
|
52922
|
+
if (error instanceof Errors.CLIError && error.message.startsWith("Invalid verification code.")) {
|
|
52923
|
+
process$1.stderr.write("Invalid verification code. Try again or type `resend`.\n");
|
|
52924
|
+
continue;
|
|
52925
|
+
}
|
|
52926
|
+
throw error;
|
|
52369
52927
|
}
|
|
52370
|
-
writeErrorWithHints(payload);
|
|
52371
|
-
throw cliError$1("Could not revoke the saved Primitive CLI OAuth grant.");
|
|
52372
|
-
}
|
|
52373
|
-
const logout = unwrapData(result.data);
|
|
52374
|
-
deleteCliCredentials(params.configDir);
|
|
52375
|
-
const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
|
|
52376
|
-
process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
|
|
52377
|
-
}
|
|
52378
|
-
function runForceLogout(params) {
|
|
52379
|
-
requireDefaultLoginProfile();
|
|
52380
|
-
const localCredentialsPath = credentialsPath(params.configDir);
|
|
52381
|
-
const pendingPath = pendingSignupPath(params.configDir);
|
|
52382
|
-
const lockPath = credentialsLockPath(params.configDir);
|
|
52383
|
-
const removed = [
|
|
52384
|
-
existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
|
|
52385
|
-
existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
|
|
52386
|
-
existsSync(pendingPath) ? "pending email-code auth state" : null,
|
|
52387
|
-
existsSync(lockPath) ? "credential lock" : null
|
|
52388
|
-
].filter((value) => value !== null);
|
|
52389
|
-
deleteCliCredentials(params.configDir);
|
|
52390
|
-
deletePendingAgentSignup(params.configDir);
|
|
52391
|
-
deleteCliCredentialsLock(params.configDir);
|
|
52392
|
-
if (removed.length === 0) {
|
|
52393
|
-
process.stderr.write("No local Primitive CLI auth state was present. Backing OAuth grant was not revoked.\n");
|
|
52394
|
-
return;
|
|
52395
52928
|
}
|
|
52396
|
-
process.stderr.write(`Removed ${formatList(removed)}. Backing OAuth grant was not revoked.\n`);
|
|
52397
|
-
}
|
|
52398
|
-
function formatList(values) {
|
|
52399
|
-
if (values.length <= 1) return values[0] ?? "";
|
|
52400
|
-
if (values.length === 2) return `${values[0]} and ${values[1]}`;
|
|
52401
|
-
return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
|
|
52402
52929
|
}
|
|
52403
|
-
|
|
52404
|
-
|
|
52405
|
-
|
|
52406
|
-
static examples = ["<%= config.bin %> logout", "<%= config.bin %> logout --force"];
|
|
52407
|
-
static flags = {
|
|
52930
|
+
function commonStartFlags() {
|
|
52931
|
+
return {
|
|
52932
|
+
"accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
|
|
52408
52933
|
"api-base-url": Flags.string({
|
|
52409
52934
|
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52410
52935
|
env: "PRIMITIVE_API_BASE_URL",
|
|
52411
52936
|
hidden: true
|
|
52412
52937
|
}),
|
|
52938
|
+
"device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
|
|
52413
52939
|
force: Flags.boolean({
|
|
52414
52940
|
char: "f",
|
|
52415
|
-
description: "
|
|
52941
|
+
description: "Replace saved credentials or pending signup state when needed"
|
|
52942
|
+
}),
|
|
52943
|
+
"signup-code": Flags.string({
|
|
52944
|
+
description: "Optional signup code. Omit if you do not have one.",
|
|
52945
|
+
env: "PRIMITIVE_SIGNUP_CODE"
|
|
52416
52946
|
})
|
|
52417
52947
|
};
|
|
52948
|
+
}
|
|
52949
|
+
var SignupCommand = class SignupCommand extends Command {
|
|
52950
|
+
static args = { email: Args.string({
|
|
52951
|
+
description: "Email address to sign up",
|
|
52952
|
+
required: false
|
|
52953
|
+
}) };
|
|
52954
|
+
static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
|
|
52955
|
+
static summary = "Start account signup";
|
|
52956
|
+
static examples = [
|
|
52957
|
+
"<%= config.bin %> signup user@example.com",
|
|
52958
|
+
"<%= config.bin %> signup user@example.com --accept-terms",
|
|
52959
|
+
"<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
|
|
52960
|
+
"<%= config.bin %> signup confirm user@example.com 123456"
|
|
52961
|
+
];
|
|
52962
|
+
static flags = commonStartFlags();
|
|
52418
52963
|
async run() {
|
|
52419
|
-
const { flags } = await this.parse(
|
|
52420
|
-
if (flags.force) {
|
|
52421
|
-
runForceLogout({ configDir: this.config.configDir });
|
|
52422
|
-
return;
|
|
52423
|
-
}
|
|
52964
|
+
const { args, flags } = await this.parse(SignupCommand);
|
|
52424
52965
|
let releaseCredentialsLock;
|
|
52425
52966
|
try {
|
|
52426
52967
|
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52427
52968
|
} catch (error) {
|
|
52428
|
-
throw cliError$
|
|
52969
|
+
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52429
52970
|
}
|
|
52430
52971
|
try {
|
|
52431
|
-
await
|
|
52972
|
+
await runSignupStartWithCredentialLock({
|
|
52432
52973
|
configDir: this.config.configDir,
|
|
52974
|
+
email: args.email,
|
|
52433
52975
|
flags
|
|
52434
52976
|
});
|
|
52435
52977
|
} finally {
|
|
@@ -52437,667 +52979,349 @@ var LogoutCommand = class LogoutCommand extends Command {
|
|
|
52437
52979
|
}
|
|
52438
52980
|
}
|
|
52439
52981
|
};
|
|
52440
|
-
|
|
52441
|
-
|
|
52442
|
-
|
|
52443
|
-
|
|
52444
|
-
|
|
52445
|
-
|
|
52446
|
-
|
|
52447
|
-
|
|
52448
|
-
|
|
52449
|
-
|
|
52450
|
-
* Read a user-owned text file without following it anywhere surprising. A
|
|
52451
|
-
* symlink is followed to its real file so a later write updates the file the
|
|
52452
|
-
* user linked instead of replacing the link.
|
|
52453
|
-
*/
|
|
52454
|
-
function readManagedFile(path) {
|
|
52455
|
-
let info;
|
|
52456
|
-
try {
|
|
52457
|
-
info = lstatSync(path);
|
|
52458
|
-
} catch (error) {
|
|
52459
|
-
if (error.code === "ENOENT") return {
|
|
52460
|
-
state: "absent",
|
|
52461
|
-
path,
|
|
52462
|
-
target: path
|
|
52463
|
-
};
|
|
52464
|
-
return {
|
|
52465
|
-
state: "invalid",
|
|
52466
|
-
path,
|
|
52467
|
-
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
52468
|
-
};
|
|
52469
|
-
}
|
|
52470
|
-
let target = path;
|
|
52471
|
-
if (info.isSymbolicLink()) try {
|
|
52472
|
-
target = realpathSync(path);
|
|
52473
|
-
info = statSync(target);
|
|
52474
|
-
} catch {
|
|
52475
|
-
return {
|
|
52476
|
-
state: "invalid",
|
|
52477
|
-
path,
|
|
52478
|
-
detail: `${path} is a symlink to a missing file; it was left unchanged.`
|
|
52479
|
-
};
|
|
52480
|
-
}
|
|
52481
|
-
if (!info.isFile()) return {
|
|
52482
|
-
state: "invalid",
|
|
52483
|
-
path,
|
|
52484
|
-
detail: `${path} is not a regular file; it was left unchanged.`
|
|
52982
|
+
function resolveVerificationCode(input) {
|
|
52983
|
+
const sources = [
|
|
52984
|
+
input.positional !== void 0 ? "positional" : null,
|
|
52985
|
+
input.fromStdin === true ? "--code-from-stdin" : null,
|
|
52986
|
+
input.fromFile !== void 0 ? "--code-from-file" : null,
|
|
52987
|
+
input.fromEnv !== void 0 ? "--code-from-env" : null
|
|
52988
|
+
].filter((v) => v !== null);
|
|
52989
|
+
if (sources.length === 0) return {
|
|
52990
|
+
kind: "error",
|
|
52991
|
+
message: "Pass the verification code as a positional argument or via one of --code-from-stdin, --code-from-file, or --code-from-env."
|
|
52485
52992
|
};
|
|
52486
|
-
if (
|
|
52487
|
-
|
|
52488
|
-
|
|
52489
|
-
detail: `${path} is larger than 1 MiB; it was left unchanged.`
|
|
52993
|
+
if (sources.length > 1) return {
|
|
52994
|
+
kind: "error",
|
|
52995
|
+
message: `Pass exactly one source for the verification code; got ${sources.join(", ")}.`
|
|
52490
52996
|
};
|
|
52491
|
-
|
|
52492
|
-
|
|
52493
|
-
|
|
52494
|
-
}
|
|
52997
|
+
if (input.positional !== void 0) return {
|
|
52998
|
+
kind: "ok",
|
|
52999
|
+
code: input.positional
|
|
53000
|
+
};
|
|
53001
|
+
if (input.fromEnv !== void 0) {
|
|
53002
|
+
const value = (input.env ?? process$1.env)[input.fromEnv];
|
|
53003
|
+
if (value === void 0) return {
|
|
53004
|
+
kind: "error",
|
|
53005
|
+
message: `--code-from-env ${input.fromEnv}: environment variable is not set.`
|
|
53006
|
+
};
|
|
52495
53007
|
return {
|
|
52496
|
-
|
|
52497
|
-
|
|
52498
|
-
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
53008
|
+
kind: "ok",
|
|
53009
|
+
code: stripTrailingNewline(value)
|
|
52499
53010
|
};
|
|
52500
53011
|
}
|
|
52501
|
-
|
|
52502
|
-
|
|
52503
|
-
state: "invalid",
|
|
52504
|
-
path,
|
|
52505
|
-
detail: `${path} is not UTF-8 text; it was left unchanged.`
|
|
52506
|
-
};
|
|
52507
|
-
return {
|
|
52508
|
-
state: "present",
|
|
52509
|
-
path,
|
|
52510
|
-
target,
|
|
52511
|
-
text,
|
|
52512
|
-
mode: info.mode & 511
|
|
52513
|
-
};
|
|
52514
|
-
}
|
|
52515
|
-
function backupStamp$1(now) {
|
|
52516
|
-
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
52517
|
-
}
|
|
52518
|
-
function syncDirectory(directory) {
|
|
52519
|
-
if (process.platform === "win32") return;
|
|
52520
|
-
try {
|
|
52521
|
-
const fd = openSync(directory, "r");
|
|
52522
|
-
try {
|
|
52523
|
-
fsyncSync(fd);
|
|
52524
|
-
} finally {
|
|
52525
|
-
closeSync(fd);
|
|
52526
|
-
}
|
|
52527
|
-
} catch {}
|
|
52528
|
-
}
|
|
52529
|
-
function pruneBackups(target) {
|
|
52530
|
-
const directory = dirname(target);
|
|
52531
|
-
const prefix = `${basename(target)}${BACKUP_INFIX}`;
|
|
52532
|
-
try {
|
|
52533
|
-
const backups = readdirSync(directory).filter((name) => name.startsWith(prefix)).sort();
|
|
52534
|
-
for (const name of backups.slice(0, Math.max(0, backups.length - BACKUPS_KEPT))) unlinkSync(join(directory, name));
|
|
52535
|
-
} catch {}
|
|
52536
|
-
}
|
|
52537
|
-
/** Copy the current file beside itself before any change. Returns the copy's path. */
|
|
52538
|
-
function backupManagedFile(target, mode, now) {
|
|
52539
|
-
const base = `${target}${BACKUP_INFIX}${backupStamp$1(now)}`;
|
|
52540
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
52541
|
-
const candidate = attempt === 0 ? base : `${base}-${attempt}`;
|
|
52542
|
-
try {
|
|
52543
|
-
copyFileSync(target, candidate, constants.COPYFILE_EXCL);
|
|
52544
|
-
chmodSync(candidate, mode);
|
|
52545
|
-
return candidate;
|
|
52546
|
-
} catch (error) {
|
|
52547
|
-
if (error.code !== "EEXIST") throw error;
|
|
52548
|
-
}
|
|
52549
|
-
}
|
|
52550
|
-
throw new MachineFileError(`Could not create a backup of ${target}.`);
|
|
52551
|
-
}
|
|
52552
|
-
/**
|
|
52553
|
-
* Replace a file atomically (temporary file plus rename in the same
|
|
52554
|
-
* directory), after backing up the existing content. The write is refused when
|
|
52555
|
-
* the file changed since it was read, so a concurrent edit is never lost.
|
|
52556
|
-
*/
|
|
52557
|
-
function writeManagedFile(params) {
|
|
52558
|
-
const { read } = params;
|
|
52559
|
-
const target = read.target;
|
|
52560
|
-
const expected = read.state === "present" ? read.text : null;
|
|
52561
|
-
const directory = dirname(target);
|
|
52562
|
-
mkdirSync(directory, {
|
|
52563
|
-
recursive: true,
|
|
52564
|
-
mode: 448
|
|
52565
|
-
});
|
|
52566
|
-
const current = () => {
|
|
53012
|
+
if (input.fromFile !== void 0) {
|
|
53013
|
+
const readFile = input.readFile ?? defaultReadCodeFile;
|
|
52567
53014
|
try {
|
|
52568
|
-
return
|
|
53015
|
+
return {
|
|
53016
|
+
kind: "ok",
|
|
53017
|
+
code: stripTrailingNewline(readFile(input.fromFile))
|
|
53018
|
+
};
|
|
52569
53019
|
} catch (error) {
|
|
52570
|
-
|
|
52571
|
-
|
|
52572
|
-
|
|
52573
|
-
|
|
52574
|
-
|
|
52575
|
-
const mode = read.state === "present" ? read.mode : params.newFileMode ?? 420;
|
|
52576
|
-
const backup = read.state === "present" ? backupManagedFile(target, mode, (params.now ?? (() => /* @__PURE__ */ new Date()))()) : null;
|
|
52577
|
-
const temporary = join(directory, `.${basename(target)}.${randomUUID()}.tmp`);
|
|
52578
|
-
try {
|
|
52579
|
-
const fd = openSync(temporary, "wx", mode);
|
|
52580
|
-
try {
|
|
52581
|
-
writeFileSync(fd, params.content, "utf8");
|
|
52582
|
-
fsyncSync(fd);
|
|
52583
|
-
} finally {
|
|
52584
|
-
closeSync(fd);
|
|
53020
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
53021
|
+
return {
|
|
53022
|
+
kind: "error",
|
|
53023
|
+
message: `--code-from-file ${input.fromFile}: could not read file: ${detail}`
|
|
53024
|
+
};
|
|
52585
53025
|
}
|
|
52586
|
-
chmodSync(temporary, mode);
|
|
52587
|
-
if (current() !== expected) throw new MachineFileError(`${read.path} changed while it was being repaired; it was left unchanged.`);
|
|
52588
|
-
renameSync(temporary, target);
|
|
52589
|
-
syncDirectory(directory);
|
|
52590
|
-
} finally {
|
|
52591
|
-
if (existsSync(temporary)) unlinkSync(temporary);
|
|
52592
|
-
}
|
|
52593
|
-
if (backup) pruneBackups(target);
|
|
52594
|
-
return { backup };
|
|
52595
|
-
}
|
|
52596
|
-
const START = /^<!-- primitive:managed-block v=(\d+) START -->\r?$/;
|
|
52597
|
-
const END = /^<!-- primitive:managed-block END -->\r?$/;
|
|
52598
|
-
/** Character offsets of the single managed block, or why the markers are unusable. */
|
|
52599
|
-
function locateBlock(text) {
|
|
52600
|
-
const starts = [];
|
|
52601
|
-
const ends = [];
|
|
52602
|
-
let offset = 0;
|
|
52603
|
-
for (const line of text.split("\n")) {
|
|
52604
|
-
const start = START.exec(line);
|
|
52605
|
-
if (start) starts.push({
|
|
52606
|
-
offset,
|
|
52607
|
-
version: Number(start[1])
|
|
52608
|
-
});
|
|
52609
|
-
else if (END.test(line)) ends.push({
|
|
52610
|
-
offset,
|
|
52611
|
-
length: line.endsWith("\r") ? line.length - 1 : line.length
|
|
52612
|
-
});
|
|
52613
|
-
else if (line.includes("primitive:managed-block")) return { malformed: "a primitive:managed-block marker line was edited; fix or remove it by hand" };
|
|
52614
|
-
offset += line.length + 1;
|
|
52615
|
-
}
|
|
52616
|
-
if (starts.length === 0 && ends.length === 0) return null;
|
|
52617
|
-
if (starts.length !== 1 || ends.length !== 1) return { malformed: `found ${starts.length} START and ${ends.length} END markers; expected one of each` };
|
|
52618
|
-
const [start] = starts;
|
|
52619
|
-
const [end] = ends;
|
|
52620
|
-
if (!start || !end || end.offset < start.offset) return { malformed: "the END marker comes before the START marker" };
|
|
52621
|
-
return {
|
|
52622
|
-
start: start.offset,
|
|
52623
|
-
end: end.offset + end.length,
|
|
52624
|
-
version: start.version
|
|
52625
|
-
};
|
|
52626
|
-
}
|
|
52627
|
-
/** The exact block for a body, using the file's own line ending. */
|
|
52628
|
-
function renderManagedBlock(body, eol = "\n") {
|
|
52629
|
-
return [
|
|
52630
|
-
`<!-- primitive:managed-block v=1 START -->`,
|
|
52631
|
-
...body.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n"),
|
|
52632
|
-
"<!-- primitive:managed-block END -->"
|
|
52633
|
-
].join(eol);
|
|
52634
|
-
}
|
|
52635
|
-
function lineEnding(text) {
|
|
52636
|
-
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
52637
|
-
}
|
|
52638
|
-
function inspectManagedBlock(text, body) {
|
|
52639
|
-
const found = locateBlock(text);
|
|
52640
|
-
if (found === null) return { state: "missing" };
|
|
52641
|
-
if ("malformed" in found) return {
|
|
52642
|
-
state: "malformed",
|
|
52643
|
-
detail: found.malformed
|
|
52644
|
-
};
|
|
52645
|
-
return text.slice(found.start, found.end).replace(/\r\n/g, "\n") === renderManagedBlock(body) ? { state: "ok" } : {
|
|
52646
|
-
state: "outdated",
|
|
52647
|
-
version: found.version
|
|
52648
|
-
};
|
|
52649
|
-
}
|
|
52650
|
-
/**
|
|
52651
|
-
* Replace only the managed block, or append it when absent. Text outside the
|
|
52652
|
-
* markers is preserved byte for byte. Throws for malformed markers.
|
|
52653
|
-
*/
|
|
52654
|
-
function upsertManagedBlock(text, body) {
|
|
52655
|
-
const found = locateBlock(text);
|
|
52656
|
-
if (found !== null && "malformed" in found) throw new MachineFileError(found.malformed);
|
|
52657
|
-
const eol = lineEnding(text);
|
|
52658
|
-
const block = renderManagedBlock(body, eol);
|
|
52659
|
-
if (found) return `${text.slice(0, found.start)}${block}${text.slice(found.end)}`;
|
|
52660
|
-
if (text.length === 0) return `${block}${eol}`;
|
|
52661
|
-
return `${text}${text.endsWith(`${eol}${eol}`) ? "" : text.endsWith(eol) ? eol : `${eol}${eol}`}${block}${eol}`;
|
|
52662
|
-
}
|
|
52663
|
-
/** Indentation the file already uses, so a rewrite keeps its style. */
|
|
52664
|
-
function jsonIndentation(text) {
|
|
52665
|
-
return /^[{[][^\n]*\n([ \t]+)\S/.exec(text)?.[1] ?? 2;
|
|
52666
|
-
}
|
|
52667
|
-
//#endregion
|
|
52668
|
-
//#region src/oclif/claude-machine-hooks.ts
|
|
52669
|
-
function record$2(value) {
|
|
52670
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52671
|
-
}
|
|
52672
|
-
function realpathOrNull(path) {
|
|
52673
|
-
try {
|
|
52674
|
-
return realpathSync(path);
|
|
52675
|
-
} catch {
|
|
52676
|
-
return null;
|
|
52677
53026
|
}
|
|
52678
|
-
|
|
52679
|
-
/** Two paths name the same existing file. */
|
|
52680
|
-
function samePath(a, b) {
|
|
52681
|
-
if (typeof a !== "string" || !a) return false;
|
|
52682
|
-
if (a === b) return existsSync(a);
|
|
52683
|
-
const left = realpathOrNull(a);
|
|
52684
|
-
return left !== null && left === realpathOrNull(b);
|
|
52685
|
-
}
|
|
52686
|
-
function executable(path) {
|
|
53027
|
+
const readStdin = input.readStdin ?? defaultReadCodeStdin;
|
|
52687
53028
|
try {
|
|
52688
|
-
|
|
52689
|
-
|
|
52690
|
-
|
|
52691
|
-
|
|
52692
|
-
}
|
|
52693
|
-
|
|
52694
|
-
|
|
52695
|
-
|
|
52696
|
-
|
|
52697
|
-
if (!directory) continue;
|
|
52698
|
-
const candidate = join(directory, name);
|
|
52699
|
-
if (existsSync(candidate) && executable(candidate)) return candidate;
|
|
53029
|
+
return {
|
|
53030
|
+
kind: "ok",
|
|
53031
|
+
code: stripTrailingNewline(readStdin())
|
|
53032
|
+
};
|
|
53033
|
+
} catch (error) {
|
|
53034
|
+
return {
|
|
53035
|
+
kind: "error",
|
|
53036
|
+
message: `--code-from-stdin: ${error instanceof Error ? error.message : String(error)}`
|
|
53037
|
+
};
|
|
52700
53038
|
}
|
|
52701
|
-
return null;
|
|
52702
|
-
}
|
|
52703
|
-
/**
|
|
52704
|
-
* Prefer a PATH link (for example a package manager's `bin/node`) over a
|
|
52705
|
-
* versioned install directory when both are the same binary, so a hook keeps
|
|
52706
|
-
* working after the versioned directory is replaced by an upgrade.
|
|
52707
|
-
*/
|
|
52708
|
-
function stableNodePath(env, execPath = process.execPath) {
|
|
52709
|
-
const onPath = findOnPath("node", env);
|
|
52710
|
-
if (onPath && onPath !== execPath && samePath(onPath, execPath)) return onPath;
|
|
52711
|
-
return execPath;
|
|
52712
|
-
}
|
|
52713
|
-
/** Locate the running CLI. Null when its files are not where a package install puts them. */
|
|
52714
|
-
function currentCliLocation(entryArg, env, execPath = process.execPath) {
|
|
52715
|
-
const entry = realpathOrNull(entryArg);
|
|
52716
|
-
if (!entry) return null;
|
|
52717
|
-
const wake = realpathOrNull(join(dirname(entry), "claude-wake.mjs"));
|
|
52718
|
-
const pending = realpathOrNull(join(dirname(entry), "claude-pending-mail.mjs"));
|
|
52719
|
-
if (!wake || !pending) return null;
|
|
52720
|
-
return {
|
|
52721
|
-
node: stableNodePath(env, execPath),
|
|
52722
|
-
entry,
|
|
52723
|
-
wake,
|
|
52724
|
-
pending
|
|
52725
|
-
};
|
|
52726
53039
|
}
|
|
52727
|
-
|
|
52728
|
-
|
|
52729
|
-
if (/[\\/]_npx[\\/]/.test(entry)) return "an npx cache";
|
|
52730
|
-
if (/[\\/]dlx[-\\/]/.test(entry)) return "a pnpm dlx cache";
|
|
52731
|
-
if (/[\\/]bunx-/.test(entry)) return "a bunx cache";
|
|
52732
|
-
return null;
|
|
53040
|
+
function stripTrailingNewline(value) {
|
|
53041
|
+
return value.replace(/\r?\n$/, "");
|
|
52733
53042
|
}
|
|
52734
|
-
function
|
|
52735
|
-
return
|
|
53043
|
+
function defaultReadCodeFile(path) {
|
|
53044
|
+
return readFileSync(path, "utf8");
|
|
52736
53045
|
}
|
|
52737
|
-
function
|
|
52738
|
-
|
|
52739
|
-
|
|
52740
|
-
const line = /line (\d+)/.exec(message)?.[1];
|
|
52741
|
-
return line ? ` near line ${line}` : position ? ` near character ${position}` : "";
|
|
53046
|
+
function defaultReadCodeStdin() {
|
|
53047
|
+
if (process$1.stdin.isTTY) throw new Error("stdin is a TTY; pipe the code into this command or use --code-from-file / --code-from-env instead.");
|
|
53048
|
+
return readFileSync(0, "utf8");
|
|
52742
53049
|
}
|
|
52743
|
-
|
|
52744
|
-
|
|
52745
|
-
|
|
52746
|
-
|
|
52747
|
-
|
|
52748
|
-
|
|
52749
|
-
|
|
52750
|
-
|
|
52751
|
-
|
|
52752
|
-
|
|
52753
|
-
read,
|
|
52754
|
-
settings: {}
|
|
52755
|
-
};
|
|
52756
|
-
let parsed;
|
|
52757
|
-
try {
|
|
52758
|
-
parsed = JSON.parse(read.text);
|
|
52759
|
-
} catch (error) {
|
|
52760
|
-
return {
|
|
52761
|
-
ok: false,
|
|
52762
|
-
detail: `${path} is not valid JSON${jsonErrorDetail(error)}. Fix it by hand; it was not modified.`
|
|
52763
|
-
};
|
|
52764
|
-
}
|
|
52765
|
-
if (!record$2(parsed)) return {
|
|
52766
|
-
ok: false,
|
|
52767
|
-
detail: `${path} must contain a JSON object. Fix it by hand; it was not modified.`
|
|
52768
|
-
};
|
|
52769
|
-
if (parsed.hooks !== void 0 && !record$2(parsed.hooks)) return {
|
|
52770
|
-
ok: false,
|
|
52771
|
-
detail: `${path} has a "hooks" value that is not an object. Fix it by hand; it was not modified.`
|
|
53050
|
+
var SignupConfirmCommand = class SignupConfirmCommand extends Command {
|
|
53051
|
+
static args = {
|
|
53052
|
+
email: Args.string({
|
|
53053
|
+
description: "Email address used to start signup",
|
|
53054
|
+
required: true
|
|
53055
|
+
}),
|
|
53056
|
+
code: Args.string({
|
|
53057
|
+
description: "Verification code from the signup email. Optional when one of --code-from-stdin / --code-from-file / --code-from-env is passed; exactly one source must be set.",
|
|
53058
|
+
required: false
|
|
53059
|
+
})
|
|
52772
53060
|
};
|
|
52773
|
-
|
|
52774
|
-
|
|
52775
|
-
|
|
52776
|
-
|
|
53061
|
+
static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
|
|
53062
|
+
static summary = "Confirm account signup";
|
|
53063
|
+
static examples = [
|
|
53064
|
+
"<%= config.bin %> signup confirm user@example.com 123456",
|
|
53065
|
+
"<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000",
|
|
53066
|
+
"read -rs CODE && CODE=\"$CODE\" <%= config.bin %> signup confirm user@example.com --code-from-env CODE && unset CODE",
|
|
53067
|
+
"read -rs CODE && printf '%s' \"$CODE\" | <%= config.bin %> signup confirm user@example.com --code-from-stdin && unset CODE",
|
|
53068
|
+
"<%= config.bin %> signup confirm user@example.com --code-from-file /run/user/$(id -u)/verification-code"
|
|
53069
|
+
];
|
|
53070
|
+
static flags = {
|
|
53071
|
+
"api-base-url": Flags.string({
|
|
53072
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
53073
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
53074
|
+
hidden: true
|
|
53075
|
+
}),
|
|
53076
|
+
"code-from-stdin": Flags.boolean({ description: "Read the verification code from stdin instead of the positional argument. Use when an agent is constructing the command for the user to run, so the code never enters the agent's prompt context." }),
|
|
53077
|
+
"code-from-file": Flags.string({ description: "Read the verification code from a UTF-8 file at this path. Trailing newlines are stripped." }),
|
|
53078
|
+
"code-from-env": Flags.string({ description: "Read the verification code from this environment variable. Pair with `read -rs CODE && CODE=\"$CODE\" primitive signup confirm <email> --code-from-env CODE && unset CODE` so the value never appears on the command line or in shell history. Plain `read` creates a shell-local variable that child processes cannot see; the inline `CODE=\"$CODE\"` exports it for just the one command." }),
|
|
53079
|
+
force: Flags.boolean({
|
|
53080
|
+
char: "f",
|
|
53081
|
+
description: "Replace saved credentials after verification"
|
|
53082
|
+
}),
|
|
53083
|
+
"org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
|
|
52777
53084
|
};
|
|
52778
|
-
|
|
52779
|
-
const
|
|
52780
|
-
|
|
52781
|
-
|
|
52782
|
-
|
|
52783
|
-
|
|
52784
|
-
|
|
52785
|
-
|
|
52786
|
-
|
|
52787
|
-
|
|
52788
|
-
|
|
52789
|
-
|
|
52790
|
-
|
|
53085
|
+
async run() {
|
|
53086
|
+
const { args, flags } = await this.parse(SignupConfirmCommand);
|
|
53087
|
+
const resolvedCode = resolveVerificationCode({
|
|
53088
|
+
positional: args.code,
|
|
53089
|
+
fromStdin: flags["code-from-stdin"] === true,
|
|
53090
|
+
fromFile: flags["code-from-file"],
|
|
53091
|
+
fromEnv: flags["code-from-env"]
|
|
53092
|
+
});
|
|
53093
|
+
if (resolvedCode.kind === "error") throw cliError$2(resolvedCode.message);
|
|
53094
|
+
let releaseCredentialsLock;
|
|
53095
|
+
try {
|
|
53096
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
53097
|
+
} catch (error) {
|
|
53098
|
+
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
53099
|
+
}
|
|
53100
|
+
try {
|
|
53101
|
+
await runSignupConfirmWithCredentialLock({
|
|
53102
|
+
code: resolvedCode.code,
|
|
53103
|
+
configDir: this.config.configDir,
|
|
53104
|
+
email: args.email,
|
|
53105
|
+
flags
|
|
53106
|
+
});
|
|
53107
|
+
} finally {
|
|
53108
|
+
releaseCredentialsLock();
|
|
53109
|
+
}
|
|
52791
53110
|
}
|
|
52792
|
-
|
|
52793
|
-
|
|
52794
|
-
|
|
52795
|
-
|
|
52796
|
-
|
|
52797
|
-
|
|
52798
|
-
|
|
52799
|
-
|
|
52800
|
-
|
|
52801
|
-
|
|
53111
|
+
};
|
|
53112
|
+
var SignupResendCommand = class SignupResendCommand extends Command {
|
|
53113
|
+
static args = { email: Args.string({
|
|
53114
|
+
description: "Email address used to start signup. Defaults to the saved pending signup.",
|
|
53115
|
+
required: false
|
|
53116
|
+
}) };
|
|
53117
|
+
static description = "Resend the verification code for a pending signup.";
|
|
53118
|
+
static summary = "Resend signup verification code";
|
|
53119
|
+
static examples = ["<%= config.bin %> signup resend", "<%= config.bin %> signup resend user@example.com"];
|
|
53120
|
+
static flags = { "api-base-url": Flags.string({
|
|
53121
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
53122
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
53123
|
+
hidden: true
|
|
53124
|
+
}) };
|
|
53125
|
+
async run() {
|
|
53126
|
+
const { args, flags } = await this.parse(SignupResendCommand);
|
|
53127
|
+
let releaseCredentialsLock;
|
|
53128
|
+
try {
|
|
53129
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
53130
|
+
} catch (error) {
|
|
53131
|
+
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
53132
|
+
}
|
|
53133
|
+
try {
|
|
53134
|
+
await runSignupResendWithCredentialLock({
|
|
53135
|
+
configDir: this.config.configDir,
|
|
53136
|
+
email: args.email,
|
|
53137
|
+
flags
|
|
53138
|
+
});
|
|
53139
|
+
} finally {
|
|
53140
|
+
releaseCredentialsLock();
|
|
53141
|
+
}
|
|
53142
|
+
}
|
|
53143
|
+
};
|
|
53144
|
+
var SignupStatusCommand = class SignupStatusCommand extends Command {
|
|
53145
|
+
static args = { email: Args.string({
|
|
53146
|
+
description: "Email address expected in the pending signup",
|
|
53147
|
+
required: false
|
|
53148
|
+
}) };
|
|
53149
|
+
static description = "Inspect the locally saved pending Primitive signup state.";
|
|
53150
|
+
static summary = "Show pending signup status";
|
|
53151
|
+
static examples = [
|
|
53152
|
+
"<%= config.bin %> signup status",
|
|
53153
|
+
"<%= config.bin %> signup status user@example.com",
|
|
53154
|
+
"<%= config.bin %> signup status --json"
|
|
53155
|
+
];
|
|
53156
|
+
static flags = {
|
|
53157
|
+
"api-base-url": Flags.string({
|
|
53158
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
53159
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
53160
|
+
hidden: true
|
|
53161
|
+
}),
|
|
53162
|
+
json: Flags.boolean({ description: "Print pending signup status as JSON" })
|
|
53163
|
+
};
|
|
53164
|
+
async run() {
|
|
53165
|
+
const { args, flags } = await this.parse(SignupStatusCommand);
|
|
53166
|
+
runSignupStatus({
|
|
53167
|
+
configDir: this.config.configDir,
|
|
53168
|
+
email: args.email,
|
|
53169
|
+
flags
|
|
52802
53170
|
});
|
|
52803
|
-
} finally {
|
|
52804
|
-
release();
|
|
52805
53171
|
}
|
|
52806
|
-
}
|
|
52807
|
-
|
|
52808
|
-
"
|
|
52809
|
-
|
|
52810
|
-
|
|
52811
|
-
|
|
52812
|
-
|
|
52813
|
-
|
|
52814
|
-
|
|
52815
|
-
|
|
52816
|
-
|
|
53172
|
+
};
|
|
53173
|
+
var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
|
|
53174
|
+
static description = "Run the full signup flow in one interactive terminal session.";
|
|
53175
|
+
static summary = "Run interactive account signup";
|
|
53176
|
+
static examples = ["<%= config.bin %> signup interactive"];
|
|
53177
|
+
static flags = commonStartFlags();
|
|
53178
|
+
async run() {
|
|
53179
|
+
const { flags } = await this.parse(SignupInteractiveCommand);
|
|
53180
|
+
let releaseCredentialsLock;
|
|
53181
|
+
try {
|
|
53182
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
53183
|
+
} catch (error) {
|
|
53184
|
+
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
53185
|
+
}
|
|
53186
|
+
try {
|
|
53187
|
+
await runSignupInteractiveWithCredentialLock({
|
|
53188
|
+
configDir: this.config.configDir,
|
|
53189
|
+
flags
|
|
53190
|
+
});
|
|
53191
|
+
} finally {
|
|
53192
|
+
releaseCredentialsLock();
|
|
53193
|
+
}
|
|
52817
53194
|
}
|
|
52818
53195
|
};
|
|
52819
|
-
|
|
52820
|
-
|
|
52821
|
-
|
|
52822
|
-
|
|
52823
|
-
return args.length === 6 && typeof args[0] === "string" && args[1] === "agent" && args[2] === spec.subcommand && args[3] === "--runtime" && args[4] === "claude" && args[5] === "--hook";
|
|
53196
|
+
//#endregion
|
|
53197
|
+
//#region src/oclif/commands/logout.ts
|
|
53198
|
+
function cliError$1(message) {
|
|
53199
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
52824
53200
|
}
|
|
52825
|
-
function
|
|
52826
|
-
return
|
|
52827
|
-
cli.entry,
|
|
52828
|
-
"agent",
|
|
52829
|
-
spec.subcommand,
|
|
52830
|
-
"--runtime",
|
|
52831
|
-
"claude",
|
|
52832
|
-
"--hook"
|
|
52833
|
-
];
|
|
53201
|
+
function unwrapData(value) {
|
|
53202
|
+
return value?.data ?? null;
|
|
52834
53203
|
}
|
|
52835
|
-
function
|
|
52836
|
-
|
|
52837
|
-
return samePath(hook.command, cli.node) && samePath(args[0], cli.entry);
|
|
53204
|
+
function isSavedOAuthSessionExpiredError(error) {
|
|
53205
|
+
return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
|
|
52838
53206
|
}
|
|
52839
|
-
|
|
52840
|
-
|
|
52841
|
-
const
|
|
52842
|
-
|
|
52843
|
-
|
|
52844
|
-
|
|
52845
|
-
];
|
|
52846
|
-
/** A per-session receive hook written by `agent connect` or `agent enroll`, any version. */
|
|
52847
|
-
function parseSessionHook(hook) {
|
|
52848
|
-
if (!record$2(hook) || hook.type !== "command" || !Array.isArray(hook.args)) return null;
|
|
52849
|
-
const args = hook.args;
|
|
52850
|
-
if (!args.every((value) => typeof value === "string")) return null;
|
|
52851
|
-
const strings = args;
|
|
52852
|
-
const script = basename(strings[0] ?? "");
|
|
52853
|
-
if (script === "claude-pending-mail.mjs") return strings.length === 7 && strings[6] === PENDING_MARKER ? {
|
|
52854
|
-
kind: "pending",
|
|
52855
|
-
configDir: strings[2] ?? "",
|
|
52856
|
-
profile: strings[3] ?? null,
|
|
52857
|
-
session: strings[5] ?? null
|
|
52858
|
-
} : null;
|
|
52859
|
-
if (script !== "claude-wake.mjs") return null;
|
|
52860
|
-
if (strings.length === 7 && strings[6] === WAKE_MARKER) return {
|
|
52861
|
-
kind: "wake",
|
|
52862
|
-
configDir: strings[2] ?? "",
|
|
52863
|
-
profile: strings[3] ?? null,
|
|
52864
|
-
session: strings[5] ?? null
|
|
52865
|
-
};
|
|
52866
|
-
if (strings.length === 6 && strings[5] === WAKE_MARKER) return {
|
|
52867
|
-
kind: "wake",
|
|
52868
|
-
configDir: strings[2] ?? "",
|
|
52869
|
-
profile: strings[3] ?? null,
|
|
52870
|
-
session: null
|
|
52871
|
-
};
|
|
52872
|
-
if (strings.length === 4 && strings[3] === WAKE_MARKER) return {
|
|
52873
|
-
kind: "wake",
|
|
52874
|
-
configDir: strings[2] ?? "",
|
|
52875
|
-
profile: null,
|
|
52876
|
-
session: null
|
|
53207
|
+
async function runLogoutWithCredentialLock(params) {
|
|
53208
|
+
requireDefaultLoginProfile();
|
|
53209
|
+
const deps = {
|
|
53210
|
+
cliLogout,
|
|
53211
|
+
createAuthenticatedCliApiClient,
|
|
53212
|
+
...params.deps
|
|
52877
53213
|
};
|
|
52878
|
-
|
|
52879
|
-
}
|
|
52880
|
-
/** A saved connected profile still holds a credential. Unreadable counts as live. */
|
|
52881
|
-
function profileStillConnected(configDir, profile) {
|
|
53214
|
+
let credentials;
|
|
52882
53215
|
try {
|
|
52883
|
-
|
|
52884
|
-
} catch {
|
|
52885
|
-
|
|
53216
|
+
credentials = loadCliCredentials(params.configDir);
|
|
53217
|
+
} catch (error) {
|
|
53218
|
+
deleteCliCredentials(params.configDir);
|
|
53219
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
53220
|
+
process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
|
|
53221
|
+
process.exitCode = 1;
|
|
53222
|
+
return;
|
|
52886
53223
|
}
|
|
52887
|
-
|
|
52888
|
-
|
|
52889
|
-
|
|
52890
|
-
|
|
52891
|
-
|
|
52892
|
-
|
|
52893
|
-
|
|
52894
|
-
|
|
52895
|
-
|
|
52896
|
-
|
|
52897
|
-
|
|
52898
|
-
|
|
52899
|
-
for (const entry of entries) {
|
|
52900
|
-
if (!record$2(entry) || !Array.isArray(entry.hooks)) continue;
|
|
52901
|
-
for (const hook of entry.hooks) {
|
|
52902
|
-
const parsed = parseSessionHook(hook);
|
|
52903
|
-
if (!parsed || !record$2(hook)) continue;
|
|
52904
|
-
const pathsExist = existsSync(String(hook.command)) && existsSync(String(hook.args[0])) && existsSync(String(hook.args[1]));
|
|
52905
|
-
if (!(parsed.profile ? existsSync(parsed.configDir) && profileStillConnected(parsed.configDir, parsed.profile) : existsSync(parsed.configDir) && pathsExist)) {
|
|
52906
|
-
verdicts.set(hook, "stale");
|
|
52907
|
-
continue;
|
|
52908
|
-
}
|
|
52909
|
-
const key = [
|
|
52910
|
-
event,
|
|
52911
|
-
parsed.kind,
|
|
52912
|
-
resolve(parsed.configDir),
|
|
52913
|
-
parsed.profile ?? "",
|
|
52914
|
-
parsed.session?.toLowerCase() ?? ""
|
|
52915
|
-
].join("\0");
|
|
52916
|
-
if (seen.has(key)) {
|
|
52917
|
-
verdicts.set(hook, "duplicate");
|
|
52918
|
-
continue;
|
|
52919
|
-
}
|
|
52920
|
-
seen.add(key);
|
|
52921
|
-
verdicts.set(hook, sessionHookPathsCurrent(hook, parsed, cli) ? "keep" : "outdated");
|
|
52922
|
-
}
|
|
53224
|
+
if (!credentials) throw cliError$1("Not logged in. Run `primitive signin` to create saved CLI credentials.");
|
|
53225
|
+
let authenticated;
|
|
53226
|
+
try {
|
|
53227
|
+
authenticated = await deps.createAuthenticatedCliApiClient({
|
|
53228
|
+
apiBaseUrl: params.flags["api-base-url"],
|
|
53229
|
+
configDir: params.configDir,
|
|
53230
|
+
credentialsLockHeld: true
|
|
53231
|
+
});
|
|
53232
|
+
} catch (error) {
|
|
53233
|
+
if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
|
|
53234
|
+
process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
|
|
53235
|
+
return;
|
|
52923
53236
|
}
|
|
53237
|
+
throw error;
|
|
52924
53238
|
}
|
|
52925
|
-
|
|
52926
|
-
|
|
52927
|
-
|
|
52928
|
-
|
|
52929
|
-
|
|
52930
|
-
|
|
52931
|
-
|
|
52932
|
-
|
|
52933
|
-
|
|
52934
|
-
|
|
52935
|
-
|
|
52936
|
-
|
|
52937
|
-
|
|
52938
|
-
|
|
52939
|
-
|
|
52940
|
-
if (entries === "invalid") {
|
|
52941
|
-
findings[id] = {
|
|
52942
|
-
status: "fail",
|
|
52943
|
-
detail: `hooks.${spec.event} is not a list. Fix settings.json by hand; it was not modified.`,
|
|
52944
|
-
fixable: false
|
|
52945
|
-
};
|
|
52946
|
-
continue;
|
|
53239
|
+
const freshCredentials = authenticated.auth.credentials ?? credentials;
|
|
53240
|
+
const result = await deps.cliLogout({
|
|
53241
|
+
body: { key_id: freshCredentials.oauth_grant_id },
|
|
53242
|
+
client: authenticated.apiClient.client,
|
|
53243
|
+
responseStyle: "fields"
|
|
53244
|
+
});
|
|
53245
|
+
if (result.error) {
|
|
53246
|
+
const payload = extractErrorPayload(result.error);
|
|
53247
|
+
const code = extractErrorCode(payload);
|
|
53248
|
+
if (code === API_ERROR_CODES.unauthorized || code === API_ERROR_CODES.notFound) {
|
|
53249
|
+
deleteCliCredentials(params.configDir);
|
|
53250
|
+
writeErrorWithHints(payload);
|
|
53251
|
+
process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
|
|
53252
|
+
process.exitCode = 1;
|
|
53253
|
+
return;
|
|
52947
53254
|
}
|
|
52948
|
-
|
|
52949
|
-
|
|
52950
|
-
if (!first) findings[id] = {
|
|
52951
|
-
status: "fail",
|
|
52952
|
-
detail: `No ${spec.event} hook runs \`primitive agent ${spec.subcommand}\`.${blockedNote}`,
|
|
52953
|
-
fixable: canFix
|
|
52954
|
-
};
|
|
52955
|
-
else if (managed.length > 1) findings[id] = {
|
|
52956
|
-
status: "fail",
|
|
52957
|
-
detail: `${managed.length} ${spec.event} hooks run \`primitive agent ${spec.subcommand}\`; expected exactly one.${blockedNote}`,
|
|
52958
|
-
fixable: canFix
|
|
52959
|
-
};
|
|
52960
|
-
else if (!context.cli || !globalHookCurrent(first, context.cli)) findings[id] = {
|
|
52961
|
-
status: "fail",
|
|
52962
|
-
detail: `The ${spec.event} hook runs ${String(first.args[0])}, which ${existsSync(String(first.args[0])) ? "is not this CLI" : "no longer exists"}.${blockedNote}`,
|
|
52963
|
-
fixable: canFix
|
|
52964
|
-
};
|
|
52965
|
-
else findings[id] = {
|
|
52966
|
-
status: "ok",
|
|
52967
|
-
detail: `${spec.event} runs \`primitive agent ${spec.subcommand}\` from ${context.cli.entry}.`,
|
|
52968
|
-
fixable: false
|
|
52969
|
-
};
|
|
53255
|
+
writeErrorWithHints(payload);
|
|
53256
|
+
throw cliError$1("Could not revoke the saved Primitive CLI OAuth grant.");
|
|
52970
53257
|
}
|
|
52971
|
-
const
|
|
52972
|
-
|
|
52973
|
-
|
|
52974
|
-
|
|
52975
|
-
|
|
52976
|
-
|
|
52977
|
-
|
|
52978
|
-
|
|
53258
|
+
const logout = unwrapData(result.data);
|
|
53259
|
+
deleteCliCredentials(params.configDir);
|
|
53260
|
+
const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
|
|
53261
|
+
process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
|
|
53262
|
+
}
|
|
53263
|
+
function runForceLogout(params) {
|
|
53264
|
+
requireDefaultLoginProfile();
|
|
53265
|
+
const localCredentialsPath = credentialsPath(params.configDir);
|
|
53266
|
+
const pendingPath = pendingSignupPath(params.configDir);
|
|
53267
|
+
const lockPath = credentialsLockPath(params.configDir);
|
|
53268
|
+
const removed = [
|
|
53269
|
+
existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
|
|
53270
|
+
existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
|
|
53271
|
+
existsSync(pendingPath) ? "pending email-code auth state" : null,
|
|
53272
|
+
existsSync(lockPath) ? "credential lock" : null
|
|
53273
|
+
].filter((value) => value !== null);
|
|
53274
|
+
deleteCliCredentials(params.configDir);
|
|
53275
|
+
deletePendingAgentSignup(params.configDir);
|
|
53276
|
+
deleteCliCredentialsLock(params.configDir);
|
|
53277
|
+
if (removed.length === 0) {
|
|
53278
|
+
process.stderr.write("No local Primitive CLI auth state was present. Backing OAuth grant was not revoked.\n");
|
|
53279
|
+
return;
|
|
52979
53280
|
}
|
|
52980
|
-
|
|
52981
|
-
keep: 0,
|
|
52982
|
-
stale: 0,
|
|
52983
|
-
duplicate: 0,
|
|
52984
|
-
outdated: 0
|
|
52985
|
-
};
|
|
52986
|
-
for (const verdict of judgeSessionHooks(hooks, context.cli).values()) counts[verdict]++;
|
|
52987
|
-
const problems = [
|
|
52988
|
-
counts.stale ? `${counts.stale} for disconnected or removed sessions` : "",
|
|
52989
|
-
counts.duplicate ? `${counts.duplicate} duplicates` : "",
|
|
52990
|
-
counts.outdated ? `${counts.outdated} pointing at an old CLI path` : ""
|
|
52991
|
-
].filter(Boolean);
|
|
52992
|
-
findings["claude.hook.stop"] = problems.length ? {
|
|
52993
|
-
status: "fail",
|
|
52994
|
-
detail: `Per-session receive hooks need cleanup: ${problems.join(", ")}.${blockedNote}`,
|
|
52995
|
-
fixable: canFix || counts.outdated === 0 && context.blocked === null
|
|
52996
|
-
} : {
|
|
52997
|
-
status: "ok",
|
|
52998
|
-
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.",
|
|
52999
|
-
fixable: false
|
|
53000
|
-
};
|
|
53001
|
-
return findings;
|
|
53281
|
+
process.stderr.write(`Removed ${formatList(removed)}. Backing OAuth grant was not revoked.\n`);
|
|
53002
53282
|
}
|
|
53003
|
-
function
|
|
53004
|
-
|
|
53005
|
-
return {
|
|
53006
|
-
|
|
53007
|
-
|
|
53008
|
-
|
|
53009
|
-
|
|
53010
|
-
|
|
53011
|
-
|
|
53012
|
-
|
|
53013
|
-
|
|
53014
|
-
|
|
53015
|
-
|
|
53016
|
-
|
|
53017
|
-
return kept.length ? [{
|
|
53018
|
-
...entry,
|
|
53019
|
-
hooks: kept
|
|
53020
|
-
}] : [];
|
|
53283
|
+
function formatList(values) {
|
|
53284
|
+
if (values.length <= 1) return values[0] ?? "";
|
|
53285
|
+
if (values.length === 2) return `${values[0]} and ${values[1]}`;
|
|
53286
|
+
return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
|
|
53287
|
+
}
|
|
53288
|
+
var LogoutCommand = class LogoutCommand extends Command {
|
|
53289
|
+
static description = "Log out by revoking the saved Primitive CLI OAuth grant and deleting local credentials. Use --force to remove local credentials, pending email-code auth state, and stale credential locks without contacting Primitive.";
|
|
53290
|
+
static summary = "Log out and revoke the saved CLI OAuth grant";
|
|
53291
|
+
static examples = ["<%= config.bin %> logout", "<%= config.bin %> logout --force"];
|
|
53292
|
+
static flags = {
|
|
53293
|
+
"api-base-url": Flags.string({
|
|
53294
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
53295
|
+
env: "PRIMITIVE_API_BASE_URL",
|
|
53296
|
+
hidden: true
|
|
53021
53297
|
}),
|
|
53022
|
-
|
|
53298
|
+
force: Flags.boolean({
|
|
53299
|
+
char: "f",
|
|
53300
|
+
description: "Remove local CLI credentials, pending email-code auth state, and any credential lock without revoking the server OAuth grant"
|
|
53301
|
+
})
|
|
53023
53302
|
};
|
|
53024
|
-
|
|
53025
|
-
|
|
53026
|
-
|
|
53027
|
-
|
|
53028
|
-
|
|
53029
|
-
*/
|
|
53030
|
-
function repairClaudeHooks(settings, context, checks) {
|
|
53031
|
-
const changed = /* @__PURE__ */ new Set();
|
|
53032
|
-
const hooks = record$2(settings.hooks) ? { ...settings.hooks } : {};
|
|
53033
|
-
const cli = context.blocked === null ? context.cli : null;
|
|
53034
|
-
for (const [id, spec] of Object.entries(GLOBAL_HOOKS)) {
|
|
53035
|
-
if (!checks.has(id) || !cli) continue;
|
|
53036
|
-
const entries = eventEntries(hooks, spec.event);
|
|
53037
|
-
if (entries === "invalid") continue;
|
|
53038
|
-
let placed = false;
|
|
53039
|
-
const result = rewriteEntries(entries, (hook) => {
|
|
53040
|
-
if (!isGlobalManaged(hook, spec)) return hook;
|
|
53041
|
-
if (placed) return void 0;
|
|
53042
|
-
placed = true;
|
|
53043
|
-
if (globalHookCurrent(hook, cli)) return hook;
|
|
53044
|
-
return {
|
|
53045
|
-
...hook,
|
|
53046
|
-
command: cli.node,
|
|
53047
|
-
args: globalHookArgs(cli, spec)
|
|
53048
|
-
};
|
|
53049
|
-
});
|
|
53050
|
-
let next = result.entries;
|
|
53051
|
-
if (!placed) next = [...next, { hooks: [{
|
|
53052
|
-
type: "command",
|
|
53053
|
-
command: cli.node,
|
|
53054
|
-
args: globalHookArgs(cli, spec),
|
|
53055
|
-
timeout: spec.timeout
|
|
53056
|
-
}] }];
|
|
53057
|
-
if (result.changed || !placed) {
|
|
53058
|
-
hooks[spec.event] = next;
|
|
53059
|
-
changed.add(id);
|
|
53303
|
+
async run() {
|
|
53304
|
+
const { flags } = await this.parse(LogoutCommand);
|
|
53305
|
+
if (flags.force) {
|
|
53306
|
+
runForceLogout({ configDir: this.config.configDir });
|
|
53307
|
+
return;
|
|
53060
53308
|
}
|
|
53061
|
-
|
|
53062
|
-
|
|
53063
|
-
|
|
53064
|
-
|
|
53065
|
-
|
|
53066
|
-
|
|
53067
|
-
|
|
53068
|
-
|
|
53069
|
-
|
|
53070
|
-
|
|
53071
|
-
|
|
53072
|
-
|
|
53073
|
-
|
|
53074
|
-
args[0] = parsed?.kind === "pending" ? cli.pending : cli.wake;
|
|
53075
|
-
args[1] = cli.entry;
|
|
53076
|
-
return {
|
|
53077
|
-
...hook,
|
|
53078
|
-
command: cli.node,
|
|
53079
|
-
args
|
|
53080
|
-
};
|
|
53081
|
-
});
|
|
53082
|
-
if (result.changed) {
|
|
53083
|
-
hooks[event] = result.entries;
|
|
53084
|
-
changed.add("claude.hook.stop");
|
|
53085
|
-
}
|
|
53086
|
-
}
|
|
53309
|
+
let releaseCredentialsLock;
|
|
53310
|
+
try {
|
|
53311
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
53312
|
+
} catch (error) {
|
|
53313
|
+
throw cliError$1(error instanceof Error ? error.message : String(error));
|
|
53314
|
+
}
|
|
53315
|
+
try {
|
|
53316
|
+
await runLogoutWithCredentialLock({
|
|
53317
|
+
configDir: this.config.configDir,
|
|
53318
|
+
flags
|
|
53319
|
+
});
|
|
53320
|
+
} finally {
|
|
53321
|
+
releaseCredentialsLock();
|
|
53087
53322
|
}
|
|
53088
53323
|
}
|
|
53089
|
-
|
|
53090
|
-
settings,
|
|
53091
|
-
changed
|
|
53092
|
-
};
|
|
53093
|
-
return {
|
|
53094
|
-
settings: {
|
|
53095
|
-
...settings,
|
|
53096
|
-
hooks
|
|
53097
|
-
},
|
|
53098
|
-
changed
|
|
53099
|
-
};
|
|
53100
|
-
}
|
|
53324
|
+
};
|
|
53101
53325
|
//#endregion
|
|
53102
53326
|
//#region src/oclif/codex-machine-hooks.ts
|
|
53103
53327
|
function record$1(value) {
|
|
@@ -53695,7 +53919,10 @@ async function runMachineDoctor(options) {
|
|
|
53695
53919
|
let settingsRead = readClaudeSettings(paths.claudeDir);
|
|
53696
53920
|
const hookContext = {
|
|
53697
53921
|
cli,
|
|
53698
|
-
blocked: hookBlocked
|
|
53922
|
+
blocked: hookBlocked,
|
|
53923
|
+
get bound() {
|
|
53924
|
+
return boundSessionProfiles(options.configDir);
|
|
53925
|
+
}
|
|
53699
53926
|
};
|
|
53700
53927
|
const hookCheck = (id) => ({
|
|
53701
53928
|
inspect: () => {
|