primcli 1.39.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/README.md +19 -0
- 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 +2310 -1883
- 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/dist/skills/primitive-connect/SKILL.md +54 -7
- package/dist/skills/primitive-connect/scripts/check-cli-capabilities.mjs +8 -3
- package/dist/skills/primitive-connect.json +4 -4
- 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": [
|
|
@@ -34983,440 +35022,1355 @@ function isMemoryJsonValue(value, seen = /* @__PURE__ */ new Set()) {
|
|
|
34983
35022
|
}
|
|
34984
35023
|
}
|
|
34985
35024
|
//#endregion
|
|
34986
|
-
//#region src/oclif/
|
|
34987
|
-
|
|
34988
|
-
|
|
34989
|
-
|
|
34990
|
-
|
|
34991
|
-
|
|
34992
|
-
|
|
34993
|
-
|
|
34994
|
-
|
|
34995
|
-
|
|
34996
|
-
|
|
34997
|
-
|
|
34998
|
-
|
|
34999
|
-
|
|
35000
|
-
|
|
35001
|
-
|
|
35002
|
-
|
|
35003
|
-
|
|
35004
|
-
const agentAddress = mailAddress(options.agentAddress);
|
|
35005
|
-
if (!SESSION_UUID$1.test(options.sessionId)) return unavailable;
|
|
35006
|
-
const sessionId = options.sessionId.toLowerCase();
|
|
35007
|
-
const settings = readMailJson(join(resolve(options.env?.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), "settings.json"));
|
|
35008
|
-
if (!record$4(settings) || !record$4(settings.hooks)) return unavailable;
|
|
35009
|
-
const hooks = settings.hooks;
|
|
35010
|
-
const owns = (event, script, marker) => {
|
|
35011
|
-
const entries = hooks[event];
|
|
35012
|
-
return Array.isArray(entries) && entries.some((entry) => record$4(entry) && Array.isArray(entry.hooks) && entry.hooks.some((candidate) => {
|
|
35013
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35014
|
-
const args = candidate.args;
|
|
35015
|
-
const offset = 2;
|
|
35016
|
-
return candidate.type === "command" && candidate.command === process.execPath && args.length === 7 && typeof args[0] === "string" && basename(args[0]) === script && args[offset] === configDir && args[offset + 1] === profileName && args[offset + 2] === agentAddress && args[offset + 3] === sessionId && args[offset + 4] === marker;
|
|
35017
|
-
}));
|
|
35018
|
-
};
|
|
35019
|
-
const installed = owns("Stop", "claude-wake.mjs", HOOK_MARKER) && owns("SessionStart", "claude-wake.mjs", HOOK_MARKER) && owns(PENDING_EVENT, "claude-pending-mail.mjs", PENDING_MARKER$1);
|
|
35020
|
-
let lastFiredAt = null;
|
|
35021
|
-
const fired = readMailJson(join(agentProfileDirectory(configDir, profileName), `pending-mail-${sessionId}.fired.json`));
|
|
35022
|
-
if (record$4(fired) && fired.version === 1 && typeof fired.at === "string" && Number.isFinite(Date.parse(fired.at))) lastFiredAt = fired.at;
|
|
35023
|
-
return {
|
|
35024
|
-
installed,
|
|
35025
|
-
lastFiredAt,
|
|
35026
|
-
liveness: "unknown"
|
|
35027
|
-
};
|
|
35028
|
-
} catch {
|
|
35029
|
-
return unavailable;
|
|
35030
|
-
}
|
|
35031
|
-
}
|
|
35032
|
-
function editClaudeSettings(claudeDir, edit) {
|
|
35033
|
-
mkdirSync(claudeDir, {
|
|
35034
|
-
recursive: true,
|
|
35035
|
-
mode: 448
|
|
35036
|
-
});
|
|
35037
|
-
let release;
|
|
35038
|
-
for (let attempt = 0; attempt < 40; attempt++) try {
|
|
35039
|
-
release = acquireListenLock(claudeDir, "primitive-claude-settings");
|
|
35040
|
-
break;
|
|
35041
|
-
} catch {
|
|
35042
|
-
Atomics.wait(LOCK_WAIT$1, 0, 0, 25);
|
|
35025
|
+
//#region src/oclif/agent-mail-check.ts
|
|
35026
|
+
/**
|
|
35027
|
+
* One check for mail that arrived since this profile's previous check. It is
|
|
35028
|
+
* the receiver for runtimes that nothing can wake (poll receiving): the agent
|
|
35029
|
+
* runs it at the start of each turn and after it sends.
|
|
35030
|
+
*
|
|
35031
|
+
* It tails `GET /emails?since=<cursor>`, which returns mail newer than the
|
|
35032
|
+
* cursor oldest first, without mail marked fyi (acknowledgements) or in
|
|
35033
|
+
* threads this address muted. The first check starts before the address's
|
|
35034
|
+
* first email. The cursor is saved privately per profile, only after the
|
|
35035
|
+
* result was handed to `emit`, so a failed or interrupted check repeats mail
|
|
35036
|
+
* rather than losing it. Reads also show this address's receiver as live.
|
|
35037
|
+
*/
|
|
35038
|
+
const CURSOR_FILE = "mail-check.json";
|
|
35039
|
+
var MailCheckApiError = class extends Error {
|
|
35040
|
+
constructor(payload) {
|
|
35041
|
+
super("The mail check request failed.");
|
|
35042
|
+
this.payload = payload;
|
|
35043
35043
|
}
|
|
35044
|
-
|
|
35044
|
+
};
|
|
35045
|
+
function readCursor(path) {
|
|
35045
35046
|
try {
|
|
35046
|
-
const
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
if (existsSync(settingsPath)) {
|
|
35051
|
-
const stat = lstatSync(settingsPath);
|
|
35052
|
-
if (!stat.isFile() || stat.size > 1048576) return false;
|
|
35053
|
-
original = readFileSync(settingsPath, "utf8");
|
|
35054
|
-
const parsed = JSON.parse(original);
|
|
35055
|
-
if (!record$4(parsed)) return false;
|
|
35056
|
-
settings = parsed;
|
|
35057
|
-
}
|
|
35058
|
-
const next = edit(settings);
|
|
35059
|
-
if (next === null) return true;
|
|
35060
|
-
const temporary = join(claudeDir, `.settings.json.${randomUUID()}.tmp`);
|
|
35061
|
-
const fd = openSync(temporary, "wx", 384);
|
|
35062
|
-
try {
|
|
35063
|
-
writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
35064
|
-
} finally {
|
|
35065
|
-
closeSync(fd);
|
|
35066
|
-
}
|
|
35067
|
-
try {
|
|
35068
|
-
if ((existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null) !== original) continue;
|
|
35069
|
-
renameSync(temporary, settingsPath);
|
|
35070
|
-
return true;
|
|
35071
|
-
} finally {
|
|
35072
|
-
if (existsSync(temporary)) unlinkSync(temporary);
|
|
35073
|
-
}
|
|
35047
|
+
const saved = readMailJson(path);
|
|
35048
|
+
if (saved && typeof saved === "object" && !Array.isArray(saved) && saved.version === 1) {
|
|
35049
|
+
const cursor = saved.cursor;
|
|
35050
|
+
if (typeof cursor === "string" && cursor.length > 0) return cursor;
|
|
35074
35051
|
}
|
|
35075
|
-
|
|
35076
|
-
|
|
35077
|
-
release();
|
|
35078
|
-
}
|
|
35052
|
+
} catch {}
|
|
35053
|
+
return null;
|
|
35079
35054
|
}
|
|
35080
|
-
/**
|
|
35081
|
-
function
|
|
35082
|
-
|
|
35083
|
-
const cliPath = realpathSync(options.cliPath);
|
|
35084
|
-
const wrapperPath = realpathSync(join(dirname(cliPath), "claude-wake.mjs"));
|
|
35085
|
-
const pendingPath = realpathSync(join(dirname(cliPath), "claude-pending-mail.mjs"));
|
|
35086
|
-
const configDir = resolve(options.configDir);
|
|
35087
|
-
const profileName = agentProfileName(options.profileName);
|
|
35088
|
-
const agentAddress = mailAddress(options.agentAddress);
|
|
35089
|
-
if (!SESSION_UUID$1.test(options.sessionId)) return "unavailable";
|
|
35090
|
-
const sessionId = options.sessionId.toLowerCase();
|
|
35091
|
-
return editClaudeSettings(resolve((options.env ?? process.env).CLAUDE_CONFIG_DIR || join(homedir(), ".claude")), (settings) => {
|
|
35092
|
-
const hooks = settings.hooks ?? {};
|
|
35093
|
-
if (!record$4(hooks)) throw new Error("Invalid hook settings");
|
|
35094
|
-
for (const event of WAKE_EVENTS) if (!Array.isArray(hooks[event] ?? [])) throw new Error(`Invalid ${event} hooks`);
|
|
35095
|
-
if (!Array.isArray(hooks[PENDING_EVENT] ?? [])) throw new Error(`Invalid ${PENDING_EVENT} hooks`);
|
|
35096
|
-
const hook = {
|
|
35097
|
-
type: "command",
|
|
35098
|
-
command: process.execPath,
|
|
35099
|
-
args: [
|
|
35100
|
-
wrapperPath,
|
|
35101
|
-
cliPath,
|
|
35102
|
-
configDir,
|
|
35103
|
-
profileName,
|
|
35104
|
-
agentAddress,
|
|
35105
|
-
sessionId,
|
|
35106
|
-
HOOK_MARKER
|
|
35107
|
-
],
|
|
35108
|
-
asyncRewake: true,
|
|
35109
|
-
timeout: 604800
|
|
35110
|
-
};
|
|
35111
|
-
const pendingHook = {
|
|
35112
|
-
type: "command",
|
|
35113
|
-
command: process.execPath,
|
|
35114
|
-
args: [
|
|
35115
|
-
pendingPath,
|
|
35116
|
-
cliPath,
|
|
35117
|
-
configDir,
|
|
35118
|
-
profileName,
|
|
35119
|
-
agentAddress,
|
|
35120
|
-
sessionId,
|
|
35121
|
-
PENDING_MARKER$1
|
|
35122
|
-
],
|
|
35123
|
-
timeout: 10
|
|
35124
|
-
};
|
|
35125
|
-
const isOwnPendingHook = (candidate) => record$4(candidate) && candidate.type === "command" && Array.isArray(candidate.args) && candidate.args.length === 7 && typeof candidate.args[0] === "string" && basename(candidate.args[0]) === "claude-pending-mail.mjs" && candidate.args[2] === configDir && candidate.args[5] === sessionId && candidate.args[6] === PENDING_MARKER$1;
|
|
35126
|
-
const isOwnHook = (candidate) => {
|
|
35127
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35128
|
-
if (typeof candidate.args[0] !== "string" || basename(candidate.args[0]) !== "claude-wake.mjs" || candidate.args[2] !== configDir) return false;
|
|
35129
|
-
if (candidate.args.length === 7 && candidate.args[5] === sessionId && candidate.args[6] === HOOK_MARKER) return true;
|
|
35130
|
-
if (candidate.args.length !== 6 || candidate.args[5] !== HOOK_MARKER) return false;
|
|
35131
|
-
try {
|
|
35132
|
-
const state = readMailJson(join(agentProfileDirectory(configDir, agentProfileName(String(candidate.args[3]))), "setup.json"));
|
|
35133
|
-
return record$4(state) && state.session === sessionId;
|
|
35134
|
-
} catch {
|
|
35135
|
-
return false;
|
|
35136
|
-
}
|
|
35137
|
-
};
|
|
35138
|
-
const nextHooks = { ...hooks };
|
|
35139
|
-
for (const event of WAKE_EVENTS) nextHooks[event] = [...(hooks[event] ?? []).flatMap((entry) => {
|
|
35140
|
-
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35141
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
|
|
35142
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35143
|
-
return siblings.length ? [{
|
|
35144
|
-
...entry,
|
|
35145
|
-
hooks: siblings
|
|
35146
|
-
}] : [];
|
|
35147
|
-
}), event === "SessionStart" ? {
|
|
35148
|
-
matcher: "resume",
|
|
35149
|
-
hooks: [hook]
|
|
35150
|
-
} : { hooks: [hook] }];
|
|
35151
|
-
nextHooks[PENDING_EVENT] = [...(hooks[PENDING_EVENT] ?? []).flatMap((entry) => {
|
|
35152
|
-
if (!record$4(entry) || !Array.isArray(entry.hooks)) return [entry];
|
|
35153
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnPendingHook(candidate));
|
|
35154
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35155
|
-
return siblings.length ? [{
|
|
35156
|
-
...entry,
|
|
35157
|
-
hooks: siblings
|
|
35158
|
-
}] : [];
|
|
35159
|
-
}), { hooks: [pendingHook] }];
|
|
35160
|
-
return {
|
|
35161
|
-
...settings,
|
|
35162
|
-
hooks: nextHooks
|
|
35163
|
-
};
|
|
35164
|
-
}) ? "installed_unverified" : "unavailable";
|
|
35165
|
-
} catch {
|
|
35166
|
-
return "unavailable";
|
|
35167
|
-
}
|
|
35055
|
+
/** Setup challenges and presence probes come from the control address. */
|
|
35056
|
+
function isControlMail(email, identity) {
|
|
35057
|
+
return email.presence_control !== void 0 || email.sender.trim().toLowerCase() === identity.ownerAddress.trim().toLowerCase();
|
|
35168
35058
|
}
|
|
35169
|
-
|
|
35170
|
-
|
|
35059
|
+
async function checkAgentMail(options) {
|
|
35060
|
+
const { identity } = options;
|
|
35061
|
+
const directory = agentProfileDirectory(options.configDir, identity.profileName);
|
|
35062
|
+
privateMailDirectory(directory, true);
|
|
35063
|
+
const release = acquireListenLock(directory, "mail-check");
|
|
35171
35064
|
try {
|
|
35172
|
-
const
|
|
35173
|
-
const
|
|
35174
|
-
|
|
35175
|
-
|
|
35176
|
-
|
|
35177
|
-
|
|
35178
|
-
|
|
35179
|
-
|
|
35180
|
-
const
|
|
35181
|
-
|
|
35182
|
-
|
|
35183
|
-
|
|
35184
|
-
|
|
35185
|
-
try {
|
|
35186
|
-
const setup = readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json"));
|
|
35187
|
-
return record$4(setup) && setup.session === sessionId;
|
|
35188
|
-
} catch {
|
|
35189
|
-
return false;
|
|
35190
|
-
}
|
|
35191
|
-
};
|
|
35192
|
-
const isOwnHook = (candidate) => {
|
|
35193
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35194
|
-
const args = candidate.args;
|
|
35195
|
-
return candidate.type === "command" && typeof candidate.command === "string" && candidate.asyncRewake === true && typeof args[0] === "string" && basename(args[0]) === "claude-wake.mjs" && typeof args[1] === "string" && args[2] === configDir && args[3] === profileName && args[4] === agentAddress && (args.length === 7 && args[5] === sessionId && args[6] === HOOK_MARKER || args.length === 6 && args[5] === HOOK_MARKER && legacySessionMatches());
|
|
35196
|
-
};
|
|
35197
|
-
const isOwnPendingHook = (candidate) => {
|
|
35198
|
-
if (!record$4(candidate) || !Array.isArray(candidate.args)) return false;
|
|
35199
|
-
const args = candidate.args;
|
|
35200
|
-
return candidate.type === "command" && typeof candidate.command === "string" && typeof args[0] === "string" && basename(args[0]) === "claude-pending-mail.mjs" && args[2] === configDir && args[3] === profileName && args[4] === agentAddress && args[5] === sessionId && args[6] === PENDING_MARKER$1 && args.length === 7;
|
|
35065
|
+
const path = join(directory, CURSOR_FILE);
|
|
35066
|
+
const saved = readCursor(path);
|
|
35067
|
+
let cursor = saved;
|
|
35068
|
+
const emails = [];
|
|
35069
|
+
let controlSkipped = 0;
|
|
35070
|
+
let more = false;
|
|
35071
|
+
const maxPages = options.maxPages ?? 5;
|
|
35072
|
+
for (let page = 0; page < maxPages; page++) {
|
|
35073
|
+
const query = {
|
|
35074
|
+
since: cursor ?? "start",
|
|
35075
|
+
limit: 100,
|
|
35076
|
+
exclude_fyi: "true",
|
|
35077
|
+
exclude_muted: "true"
|
|
35201
35078
|
};
|
|
35202
|
-
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35206
|
-
const siblings = entry.hooks.filter((candidate) => !isOwnHook(candidate));
|
|
35207
|
-
if (siblings.length === entry.hooks.length) return [entry];
|
|
35208
|
-
removed = true;
|
|
35209
|
-
return siblings.length ? [{
|
|
35210
|
-
...entry,
|
|
35211
|
-
hooks: siblings
|
|
35212
|
-
}] : [];
|
|
35079
|
+
const result = await listEmails({
|
|
35080
|
+
client: options.client.client,
|
|
35081
|
+
query,
|
|
35082
|
+
responseStyle: "fields"
|
|
35213
35083
|
});
|
|
35214
|
-
|
|
35215
|
-
|
|
35216
|
-
|
|
35217
|
-
|
|
35218
|
-
|
|
35219
|
-
|
|
35220
|
-
|
|
35221
|
-
|
|
35222
|
-
}] : [];
|
|
35084
|
+
if (result.error || !result.data) throw new MailCheckApiError(result.error);
|
|
35085
|
+
const rows = result.data.data ?? [];
|
|
35086
|
+
for (const row of rows) if (isControlMail(row, identity)) controlSkipped++;
|
|
35087
|
+
else emails.push({
|
|
35088
|
+
id: row.id,
|
|
35089
|
+
received_at: row.received_at,
|
|
35090
|
+
sender: row.sender,
|
|
35091
|
+
thread_id: row.thread_id ?? null
|
|
35223
35092
|
});
|
|
35224
|
-
|
|
35225
|
-
|
|
35226
|
-
|
|
35227
|
-
|
|
35093
|
+
const next = result.data.meta?.cursor ?? null;
|
|
35094
|
+
if (rows.length === 0 || !next) break;
|
|
35095
|
+
cursor = next;
|
|
35096
|
+
more = page === maxPages - 1;
|
|
35097
|
+
}
|
|
35098
|
+
const output = {
|
|
35099
|
+
outcome: emails.length > 0 ? "mail" : "empty",
|
|
35100
|
+
emails,
|
|
35101
|
+
more,
|
|
35102
|
+
control_skipped: controlSkipped,
|
|
35103
|
+
read_command: `PRIMITIVE_AGENT_PROFILE=${identity.profileName} ${options.invocation ?? "primitive"} emails get --id <id> --brief`
|
|
35104
|
+
};
|
|
35105
|
+
options.emit(output);
|
|
35106
|
+
if (cursor && cursor !== saved) writeMailJson(path, {
|
|
35107
|
+
version: 1,
|
|
35108
|
+
cursor
|
|
35228
35109
|
});
|
|
35229
|
-
|
|
35230
|
-
|
|
35110
|
+
return output;
|
|
35111
|
+
} finally {
|
|
35112
|
+
release();
|
|
35231
35113
|
}
|
|
35232
35114
|
}
|
|
35233
35115
|
//#endregion
|
|
35234
|
-
//#region src/oclif/agent-
|
|
35235
|
-
|
|
35236
|
-
|
|
35237
|
-
|
|
35238
|
-
|
|
35239
|
-
|
|
35240
|
-
|
|
35241
|
-
|
|
35242
|
-
|
|
35243
|
-
|
|
35244
|
-
|
|
35245
|
-
|
|
35246
|
-
|
|
35247
|
-
|
|
35248
|
-
|
|
35249
|
-
|
|
35250
|
-
|
|
35251
|
-
|
|
35252
|
-
});
|
|
35253
|
-
|
|
35116
|
+
//#region src/oclif/commands/agent-check-mail.ts
|
|
35117
|
+
/** npx runs the CLI from its cache; print follow-up commands the same way. */
|
|
35118
|
+
function invocation$2(entry) {
|
|
35119
|
+
return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
|
|
35120
|
+
}
|
|
35121
|
+
var AgentCheckMailCommand = class AgentCheckMailCommand extends Command {
|
|
35122
|
+
static summary = "Check a connected agent's mail since its last check";
|
|
35123
|
+
static description = `Print the mail that reached the selected connected agent profile since its previous check, oldest first, once, without waiting. This is how an agent connected with poll receiving (no local session ID or hooks, so nothing can wake it) receives mail: run it at the start of each turn and after sending, then read each email with \`primitive emails get --id <id> --brief\`.
|
|
35124
|
+
|
|
35125
|
+
Acknowledgements marked fyi and mail in muted threads are left out. Setup and presence mail from the connection's control address is handled by the CLI and only counted. The position is saved privately with the profile and moves forward only after mail was read, so mail is reported again rather than lost when a check fails or is interrupted; deduplicate by email ID. The first check starts before the address's first email. \`more: true\` means more new mail remains: handle these and check again.
|
|
35126
|
+
|
|
35127
|
+
Requires a connected agent profile selected with ${AGENT_PROFILE_ENV}. Output is one JSON document: \`outcome\` ("mail" | "empty"), \`emails\` (id, received_at, sender, thread_id; never subject or body), \`more\`, \`control_skipped\` and \`read_command\`.`;
|
|
35128
|
+
static examples = [`${AGENT_PROFILE_ENV}=connection-0123456789ab <%= config.bin %> agent check-mail --json`];
|
|
35129
|
+
static flags = { json: Flags.boolean({ description: "Print JSON (already the default)" }) };
|
|
35130
|
+
async run() {
|
|
35131
|
+
await this.parse(AgentCheckMailCommand);
|
|
35132
|
+
if (!process.env["PRIMITIVE_AGENT_PROFILE"]?.trim()) this.error(`Select a connected agent profile with ${AGENT_PROFILE_ENV}=<profile>. The profile is printed by agent connect.`, { exit: 1 });
|
|
35133
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({ configDir: this.config.configDir });
|
|
35134
|
+
if (!auth.connectedAgent) this.error(`Select a connected agent profile with ${AGENT_PROFILE_ENV}=<profile>.`, { exit: 1 });
|
|
35135
|
+
try {
|
|
35136
|
+
await checkAgentMail({
|
|
35137
|
+
configDir: this.config.configDir,
|
|
35138
|
+
identity: auth.connectedAgent,
|
|
35139
|
+
client: apiClient,
|
|
35140
|
+
invocation: invocation$2(process.argv[1]),
|
|
35141
|
+
emit: (result) => this.log(JSON.stringify(result))
|
|
35142
|
+
});
|
|
35143
|
+
} catch (error) {
|
|
35144
|
+
if (!(error instanceof MailCheckApiError)) throw error;
|
|
35145
|
+
const payload = extractErrorPayload(error.payload);
|
|
35146
|
+
writeErrorWithHints(payload);
|
|
35147
|
+
surfaceUnauthorizedHint({
|
|
35148
|
+
auth,
|
|
35149
|
+
baseUrlOverridden,
|
|
35150
|
+
configDir: this.config.configDir,
|
|
35151
|
+
payload
|
|
35152
|
+
});
|
|
35153
|
+
process.exitCode = 1;
|
|
35154
|
+
}
|
|
35155
|
+
}
|
|
35156
|
+
};
|
|
35157
|
+
//#endregion
|
|
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;
|
|
35173
|
+
try {
|
|
35174
|
+
info = lstatSync(path);
|
|
35175
|
+
} catch (error) {
|
|
35176
|
+
if (error.code === "ENOENT") return {
|
|
35177
|
+
state: "absent",
|
|
35178
|
+
path,
|
|
35179
|
+
target: path
|
|
35180
|
+
};
|
|
35254
35181
|
return {
|
|
35255
|
-
|
|
35256
|
-
|
|
35257
|
-
|
|
35258
|
-
reason: listener.reason ?? listener.failureCode,
|
|
35259
|
-
failureCode: listener.failureCode,
|
|
35260
|
-
detail: listener.detail ?? null,
|
|
35261
|
-
lastSuccessfulMailCheckAt: mailOwner?.lastMailCheckAt ?? null,
|
|
35262
|
-
liveness: listener.healthy && listener.phase === "receiving" ? "live" : "unknown",
|
|
35263
|
-
listener
|
|
35182
|
+
state: "invalid",
|
|
35183
|
+
path,
|
|
35184
|
+
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
35264
35185
|
};
|
|
35265
|
-
}
|
|
35266
|
-
|
|
35267
|
-
|
|
35268
|
-
|
|
35269
|
-
|
|
35270
|
-
|
|
35271
|
-
|
|
35272
|
-
|
|
35273
|
-
|
|
35274
|
-
|
|
35275
|
-
|
|
35276
|
-
|
|
35277
|
-
|
|
35278
|
-
|
|
35279
|
-
|
|
35280
|
-
|
|
35281
|
-
state: "unknown",
|
|
35282
|
-
reason: "session_not_configured",
|
|
35283
|
-
lastSuccessfulMailCheckAt: null,
|
|
35284
|
-
liveness: "unknown"
|
|
35186
|
+
}
|
|
35187
|
+
let target = path;
|
|
35188
|
+
if (info.isSymbolicLink()) try {
|
|
35189
|
+
target = realpathSync(path);
|
|
35190
|
+
info = statSync(target);
|
|
35191
|
+
} catch {
|
|
35192
|
+
return {
|
|
35193
|
+
state: "invalid",
|
|
35194
|
+
path,
|
|
35195
|
+
detail: `${path} is a symlink to a missing file; it was left unchanged.`
|
|
35196
|
+
};
|
|
35197
|
+
}
|
|
35198
|
+
if (!info.isFile()) return {
|
|
35199
|
+
state: "invalid",
|
|
35200
|
+
path,
|
|
35201
|
+
detail: `${path} is not a regular file; it was left unchanged.`
|
|
35285
35202
|
};
|
|
35286
|
-
|
|
35287
|
-
|
|
35288
|
-
|
|
35289
|
-
|
|
35290
|
-
} : {
|
|
35291
|
-
status: "not_configured",
|
|
35292
|
-
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.`
|
|
35293
35207
|
};
|
|
35294
|
-
|
|
35295
|
-
function parseAgentInvitation(input) {
|
|
35208
|
+
let raw;
|
|
35296
35209
|
try {
|
|
35297
|
-
|
|
35298
|
-
|
|
35299
|
-
let token;
|
|
35300
|
-
let base;
|
|
35301
|
-
if (text.startsWith("{")) {
|
|
35302
|
-
const value = JSON.parse(text);
|
|
35303
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
35304
|
-
const row = value;
|
|
35305
|
-
if (Object.keys(row).some((key) => key !== "token" && key !== "api_base_url")) throw new Error();
|
|
35306
|
-
token = row.token;
|
|
35307
|
-
base = row.api_base_url ?? "https://api.primitive.dev/v1";
|
|
35308
|
-
} else {
|
|
35309
|
-
const url = new URL(text);
|
|
35310
|
-
if (url.username || url.password || url.search || url.pathname !== "/v1/agent-connections/setup") throw new Error();
|
|
35311
|
-
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
35312
|
-
if (Array.from(fragment.keys()).length !== 1 || fragment.getAll("token").length !== 1) throw new Error();
|
|
35313
|
-
token = fragment.get("token");
|
|
35314
|
-
base = `${url.origin}/v1`;
|
|
35315
|
-
}
|
|
35316
|
-
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) {
|
|
35317
35212
|
return {
|
|
35318
|
-
|
|
35319
|
-
|
|
35213
|
+
state: "invalid",
|
|
35214
|
+
path,
|
|
35215
|
+
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
35320
35216
|
};
|
|
35321
|
-
} catch {
|
|
35322
|
-
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.");
|
|
35323
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
|
+
};
|
|
35324
35231
|
}
|
|
35325
|
-
|
|
35326
|
-
|
|
35327
|
-
const chunks = [];
|
|
35328
|
-
let size = 0;
|
|
35329
|
-
for await (const chunk of input) {
|
|
35330
|
-
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
35331
|
-
size += bytes.length;
|
|
35332
|
-
if (size > MAX_INVITATION_BYTES) throw new AgentConnectionSetupError("Setup invitation exceeds the input limit.");
|
|
35333
|
-
chunks.push(bytes);
|
|
35334
|
-
}
|
|
35335
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
35232
|
+
function backupStamp$1(now) {
|
|
35233
|
+
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
35336
35234
|
}
|
|
35337
|
-
|
|
35338
|
-
|
|
35339
|
-
if (!reader) throw new Error();
|
|
35340
|
-
const chunks = [];
|
|
35341
|
-
let size = 0;
|
|
35235
|
+
function syncDirectory(directory) {
|
|
35236
|
+
if (process.platform === "win32") return;
|
|
35342
35237
|
try {
|
|
35343
|
-
|
|
35344
|
-
|
|
35345
|
-
|
|
35346
|
-
|
|
35347
|
-
|
|
35348
|
-
chunks.push(part.value);
|
|
35238
|
+
const fd = openSync(directory, "r");
|
|
35239
|
+
try {
|
|
35240
|
+
fsyncSync(fd);
|
|
35241
|
+
} finally {
|
|
35242
|
+
closeSync(fd);
|
|
35349
35243
|
}
|
|
35350
|
-
|
|
35351
|
-
} finally {
|
|
35352
|
-
await reader.cancel().catch(() => void 0);
|
|
35353
|
-
}
|
|
35244
|
+
} catch {}
|
|
35354
35245
|
}
|
|
35355
|
-
function
|
|
35356
|
-
const
|
|
35357
|
-
const
|
|
35358
|
-
const connection = data?.connection;
|
|
35359
|
-
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();
|
|
35360
|
-
let ownerMember;
|
|
35246
|
+
function pruneBackups(target) {
|
|
35247
|
+
const directory = dirname(target);
|
|
35248
|
+
const prefix = `${basename(target)}${BACKUP_INFIX}`;
|
|
35361
35249
|
try {
|
|
35362
|
-
|
|
35363
|
-
|
|
35364
|
-
|
|
35365
|
-
}
|
|
35366
|
-
return parseConnectedAgentProfile({
|
|
35367
|
-
version: 1,
|
|
35368
|
-
auth_method: "agent_connection",
|
|
35369
|
-
api_key: data.api_key,
|
|
35370
|
-
api_base_url: data.api_base_url,
|
|
35371
|
-
org_id: data.org_id,
|
|
35372
|
-
agent_address: connection.address,
|
|
35373
|
-
owner_address: data.owner_address,
|
|
35374
|
-
...ownerMember === void 0 ? {} : { owner_member_address: ownerMember },
|
|
35375
|
-
invitation_hash: invitationHash,
|
|
35376
|
-
created_at: new Date(now()).toISOString(),
|
|
35377
|
-
...data.presence_profile === void 0 ? {} : { presence_profile: data.presence_profile }
|
|
35378
|
-
});
|
|
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 {}
|
|
35379
35253
|
}
|
|
35380
|
-
/**
|
|
35381
|
-
|
|
35382
|
-
const
|
|
35383
|
-
|
|
35384
|
-
|
|
35385
|
-
const directory = agentProfilesDirectory(params.configDir);
|
|
35386
|
-
let release;
|
|
35387
|
-
try {
|
|
35388
|
-
privateMailDirectory(directory, true);
|
|
35389
|
-
release = acquireListenLock(directory, "agent-connection-setup");
|
|
35390
|
-
} catch {
|
|
35391
|
-
throw new AgentConnectionSetupError("Agent setup is already running, or its private directory is unavailable. No invitation was submitted.");
|
|
35392
|
-
}
|
|
35393
|
-
try {
|
|
35394
|
-
const existing = loadConnectedAgentProfile(params.configDir, profileName);
|
|
35395
|
-
if (existing) {
|
|
35396
|
-
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.");
|
|
35397
|
-
return {
|
|
35398
|
-
status: "already_configured",
|
|
35399
|
-
identity: connectedAgentIdentity(profileName, existing)
|
|
35400
|
-
};
|
|
35401
|
-
}
|
|
35402
|
-
privateMailDirectory(join(directory, "profiles"), true);
|
|
35403
|
-
privateMailDirectory(agentProfileDirectory(params.configDir, profileName), true);
|
|
35404
|
-
const journal = join(directory, "claims", `${invitationHash}.json`);
|
|
35405
|
-
privateMailDirectory(join(directory, "claims"), true);
|
|
35406
|
-
if (readMailJson(journal) !== null) throw new AgentConnectionSetupError(UNCERTAIN_CLAIM);
|
|
35407
|
-
writeMailJson(journal, {
|
|
35408
|
-
version: 1,
|
|
35409
|
-
profile_name: profileName,
|
|
35410
|
-
status: "attempted"
|
|
35411
|
-
});
|
|
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}`;
|
|
35412
35259
|
try {
|
|
35413
|
-
|
|
35414
|
-
|
|
35415
|
-
|
|
35416
|
-
|
|
35417
|
-
|
|
35418
|
-
|
|
35419
|
-
|
|
35260
|
+
copyFileSync(target, candidate, constants.COPYFILE_EXCL);
|
|
35261
|
+
chmodSync(candidate, mode);
|
|
35262
|
+
return candidate;
|
|
35263
|
+
} catch (error) {
|
|
35264
|
+
if (error.code !== "EEXIST") throw error;
|
|
35265
|
+
}
|
|
35266
|
+
}
|
|
35267
|
+
throw new MachineFileError(`Could not create a backup of ${target}.`);
|
|
35268
|
+
}
|
|
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"
|
|
35420
36374
|
},
|
|
35421
36375
|
body: JSON.stringify({
|
|
35422
36376
|
token: invitation.token,
|
|
@@ -35885,7 +36839,7 @@ function matchesContactSelector(input, sender) {
|
|
|
35885
36839
|
}
|
|
35886
36840
|
//#endregion
|
|
35887
36841
|
//#region src/oclif/contact-policy.ts
|
|
35888
|
-
function record$
|
|
36842
|
+
function record$2(value) {
|
|
35889
36843
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid contact policy.");
|
|
35890
36844
|
return value;
|
|
35891
36845
|
}
|
|
@@ -35894,11 +36848,11 @@ function fingerprint(value) {
|
|
|
35894
36848
|
return value;
|
|
35895
36849
|
}
|
|
35896
36850
|
function document(value, organization) {
|
|
35897
|
-
const row = record$
|
|
36851
|
+
const row = record$2(value);
|
|
35898
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.");
|
|
35899
36853
|
const seen = /* @__PURE__ */ new Set();
|
|
35900
36854
|
const rules = row.rules.map((value) => {
|
|
35901
|
-
const rule = record$
|
|
36855
|
+
const rule = record$2(value);
|
|
35902
36856
|
if (typeof rule.pattern !== "string" || rule.effect !== "allow" && rule.effect !== "silence") throw new Error("Invalid contact policy rule.");
|
|
35903
36857
|
const pattern = canonicalContactSelector({
|
|
35904
36858
|
kind: "pattern",
|
|
@@ -35939,7 +36893,7 @@ function document(value, organization) {
|
|
|
35939
36893
|
};
|
|
35940
36894
|
}
|
|
35941
36895
|
function parseAgentContactPolicy(value, recipient) {
|
|
35942
|
-
const row = record$
|
|
36896
|
+
const row = record$2(value);
|
|
35943
36897
|
if (row.agent_address !== recipient || typeof row.allow_contact_requests !== "boolean") throw new Error("Contact policy recipient mismatch.");
|
|
35944
36898
|
const org = document(row.org_policy, true);
|
|
35945
36899
|
const agent = document(row.agent_policy, false);
|
|
@@ -36225,6 +37179,11 @@ function createNotificationContactPolicy(options) {
|
|
|
36225
37179
|
const SUBJECT = "Connect your agent to Primitive";
|
|
36226
37180
|
const MARKER = /\bprimitive-connection:([a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}):([1-9]\d*)\b/g;
|
|
36227
37181
|
const fail = (message) => new AgentConnectionSetupError(message);
|
|
37182
|
+
const RECEIVER_MODES = [
|
|
37183
|
+
"native",
|
|
37184
|
+
"external",
|
|
37185
|
+
"poll"
|
|
37186
|
+
];
|
|
36228
37187
|
const RECEIPT_STATUSES = new Set([
|
|
36229
37188
|
"queued",
|
|
36230
37189
|
"submitted_to_agent",
|
|
@@ -36333,7 +37292,7 @@ function parseState(value) {
|
|
|
36333
37292
|
"receipt"
|
|
36334
37293
|
];
|
|
36335
37294
|
const row = mailObject(value, value && typeof value === "object" && Object.hasOwn(value, "receiverMode") ? [...keys, "receiverMode"] : keys);
|
|
36336
|
-
if (row.version !== 1 || row.receiverMode !== void 0 && !
|
|
37295
|
+
if (row.version !== 1 || row.receiverMode !== void 0 && !RECEIVER_MODES.includes(row.receiverMode) || typeof row.contactRequests !== "boolean" || ![
|
|
36337
37296
|
"waiting",
|
|
36338
37297
|
"sending",
|
|
36339
37298
|
"sent"
|
|
@@ -36348,7 +37307,7 @@ function parseState(value) {
|
|
|
36348
37307
|
if (!/^[a-f0-9]{64}$/.test(invitationHash)) throw fail("Saved setup identity is invalid.");
|
|
36349
37308
|
const state = {
|
|
36350
37309
|
version: 1,
|
|
36351
|
-
session: mailId(row.session),
|
|
37310
|
+
session: row.session === null && row.receiverMode === "poll" ? null : mailId(row.session),
|
|
36352
37311
|
receiverMode: row.receiverMode ?? "native",
|
|
36353
37312
|
invitationHash,
|
|
36354
37313
|
since: mailTime(row.since),
|
|
@@ -36569,7 +37528,7 @@ function defaults$1() {
|
|
|
36569
37528
|
*/
|
|
36570
37529
|
function refuseSetupConflicts(state, params) {
|
|
36571
37530
|
const conflicts = [
|
|
36572
|
-
...state.session !== params.session ? [`--session (this profile is bound to session ${state.session})`] : [],
|
|
37531
|
+
...(params.session === void 0 ? state.receiverMode !== "poll" : state.session !== params.session) ? [state.session ? `--session (this profile is bound to session ${state.session})` : "--session (this profile's setup binds no session)"] : [],
|
|
36573
37532
|
...params.receiverMode !== void 0 && state.receiverMode !== params.receiverMode ? [`--receiver ${params.receiverMode} (saved: ${state.receiverMode})`] : [],
|
|
36574
37533
|
...params.contactRequests !== void 0 && state.contactRequests !== params.contactRequests ? [state.contactRequests ? "contact requests off (saved setup has --contact-requests)" : "--contact-requests (saved setup did not enable it)"] : []
|
|
36575
37534
|
];
|
|
@@ -36578,7 +37537,7 @@ function refuseSetupConflicts(state, params) {
|
|
|
36578
37537
|
/** One resumable operation owns setup plumbing; it never stores or replays the invitation. */
|
|
36579
37538
|
async function setupAgent(params) {
|
|
36580
37539
|
const profileName = agentProfileName(params.profileName);
|
|
36581
|
-
if (!SESSION_UUID$1.test(params.session)) throw fail("--session requires the exact loaded session UUID.");
|
|
37540
|
+
if (params.session !== void 0 && !SESSION_UUID$1.test(params.session)) throw fail("--session requires the exact loaded session UUID.");
|
|
36582
37541
|
if (params.resume ? params.invitation !== void 0 : params.invitation === void 0) throw fail("Use an invitation on stdin for setup, or --resume without an invitation.");
|
|
36583
37542
|
const timeoutMs = params.timeoutMs ?? 3e4;
|
|
36584
37543
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 12e4) throw fail("Setup timeout must be between zero and two minutes.");
|
|
@@ -36595,7 +37554,9 @@ async function setupAgent(params) {
|
|
|
36595
37554
|
saved = value === null ? null : parseState(value);
|
|
36596
37555
|
} catch {}
|
|
36597
37556
|
if (saved) refuseSetupConflicts(saved, params);
|
|
36598
|
-
|
|
37557
|
+
const receiverMode = params.receiverMode ?? (params.resume ? saved?.receiverMode : void 0);
|
|
37558
|
+
if (params.session === void 0 && (receiverMode ?? "native") !== "poll") throw fail("--session requires the exact loaded session UUID. Without one, use --receiver poll. No invitation was claimed.");
|
|
37559
|
+
if ((receiverMode ?? "native") === "native" && params.session) try {
|
|
36599
37560
|
await dependencies.preflight(params.session);
|
|
36600
37561
|
} catch {
|
|
36601
37562
|
throw fail("This exact session is not available for native receiving. No invitation was claimed. Open a supported session and retry setup.");
|
|
@@ -36616,7 +37577,7 @@ async function setupAgent(params) {
|
|
|
36616
37577
|
if (!state) {
|
|
36617
37578
|
state = {
|
|
36618
37579
|
version: 1,
|
|
36619
|
-
session: params.session,
|
|
37580
|
+
session: params.session ?? null,
|
|
36620
37581
|
receiverMode: params.receiverMode ?? "native",
|
|
36621
37582
|
invitationHash: hash,
|
|
36622
37583
|
since: (/* @__PURE__ */ new Date(dependencies.now() - 15 * 6e4)).toISOString(),
|
|
@@ -36666,7 +37627,7 @@ async function setupAgent(params) {
|
|
|
36666
37627
|
].includes(state.receipt?.status ?? "") ? "reply_failed" : ["unknown", "wait_timeout"].includes(state.receipt?.status ?? "") ? "delivery_unknown" : "reply_submitted" : state?.phase === "sending" ? "send_unknown" : "challenge_pending";
|
|
36667
37628
|
const result = (receiving, ownerNotifications) => ({
|
|
36668
37629
|
identity: context.identity,
|
|
36669
|
-
sessionId: params.session,
|
|
37630
|
+
sessionId: params.session ?? state?.session ?? null,
|
|
36670
37631
|
verification: {
|
|
36671
37632
|
state: confirmed && submittedState() === "reply_submitted" ? "verified" : submittedState(),
|
|
36672
37633
|
...confirmed ? { verifiedAt: confirmed.verifiedAt } : {},
|
|
@@ -36677,7 +37638,7 @@ async function setupAgent(params) {
|
|
|
36677
37638
|
},
|
|
36678
37639
|
receiving: { state: receiving },
|
|
36679
37640
|
...ownerNotifications ? { ownerNotifications } : {},
|
|
36680
|
-
resumeCommand: `primitive agent connect --profile ${profileName} --session ${
|
|
37641
|
+
resumeCommand: `primitive agent connect --profile ${profileName}${state?.session ? ` --session ${state.session}` : ""}${state?.receiverMode === "external" || state?.receiverMode === "poll" ? ` --receiver ${state.receiverMode}` : ""} --resume${state?.contactRequests ? " --contact-requests" : ""} --json`,
|
|
36681
37642
|
guidance: confirmed ? "Primitive verified this connection. Receiving health is reported separately. Keep this profile for all mail commands." : submittedState() === "reply_submitted" ? "The verification reply was submitted and Primitive has not confirmed it yet. Verification usually completes within seconds; check it later with GET /agent-connections/me. Receiving health is reported separately. Keep this profile for all mail commands." : "Reply submission is not proof of delivery or app verification. Receiving health is reported separately. Keep this profile for all mail commands."
|
|
36682
37643
|
});
|
|
36683
37644
|
const key = `connection-verification-${state.invitationHash}`;
|
|
@@ -36711,14 +37672,14 @@ async function setupAgent(params) {
|
|
|
36711
37672
|
}
|
|
36712
37673
|
const ownerNotifications = await dependencies.enableOwner(context);
|
|
36713
37674
|
let healthy = false;
|
|
36714
|
-
if (state.receiverMode
|
|
36715
|
-
healthy = await dependencies.startListener(profileName,
|
|
37675
|
+
if ((state.receiverMode ?? "native") === "native" && state.session) try {
|
|
37676
|
+
healthy = await dependencies.startListener(profileName, state.session, state.contactRequests, params.configDir);
|
|
36716
37677
|
} catch {}
|
|
36717
37678
|
if (verificationTimeoutMs > 0 && submittedState() === "reply_submitted") {
|
|
36718
37679
|
const check = await awaitVerification(context, dependencies, verificationTimeoutMs);
|
|
36719
37680
|
if (check.state === "verified") confirmed = { verifiedAt: check.verifiedAt };
|
|
36720
37681
|
}
|
|
36721
|
-
return result(state.receiverMode === "external" ? "external_setup_required" : healthy ? "healthy" : "not_ready", ownerNotifications);
|
|
37682
|
+
return result(state.receiverMode === "external" ? "external_setup_required" : state.receiverMode === "poll" ? "poll" : healthy ? "healthy" : "not_ready", ownerNotifications);
|
|
36722
37683
|
} finally {
|
|
36723
37684
|
release();
|
|
36724
37685
|
}
|
|
@@ -36814,7 +37775,8 @@ const AGENT_CONNECT_CAPABILITIES = [
|
|
|
36814
37775
|
"claude_external_hooks",
|
|
36815
37776
|
"bundled_skill_install",
|
|
36816
37777
|
"agent_info_seed",
|
|
36817
|
-
"resume"
|
|
37778
|
+
"resume",
|
|
37779
|
+
"poll_receiver"
|
|
36818
37780
|
];
|
|
36819
37781
|
const AGENT_INFO_NOTE = "AGENT_INFO";
|
|
36820
37782
|
const MAX_NAME_LENGTH = 80;
|
|
@@ -36837,7 +37799,7 @@ function savedSetup(configDir, profileName) {
|
|
|
36837
37799
|
if (!saved || typeof saved !== "object" || Array.isArray(saved)) return null;
|
|
36838
37800
|
const row = saved;
|
|
36839
37801
|
return {
|
|
36840
|
-
receiverMode: row.receiverMode === "external"
|
|
37802
|
+
receiverMode: row.receiverMode === "external" || row.receiverMode === "poll" ? row.receiverMode : "native",
|
|
36841
37803
|
contactRequests: row.contactRequests === true
|
|
36842
37804
|
};
|
|
36843
37805
|
} catch {
|
|
@@ -36861,6 +37823,20 @@ function clearPendingAgentInfo(path) {
|
|
|
36861
37823
|
function defaultAgentProfileName(session) {
|
|
36862
37824
|
return `session-${session.toLowerCase()}`;
|
|
36863
37825
|
}
|
|
37826
|
+
/**
|
|
37827
|
+
* The profile for a setup that binds no session. It is derived from the
|
|
37828
|
+
* invitation, so running the same invitation again finds the same profile and
|
|
37829
|
+
* its saved claim instead of claiming twice. A one-way hash prefix reveals
|
|
37830
|
+
* nothing usable about the invitation.
|
|
37831
|
+
*/
|
|
37832
|
+
function invitationProfileName(invitation) {
|
|
37833
|
+
const parsed = parseAgentInvitation(invitation);
|
|
37834
|
+
return `connection-${createHash("sha256").update(parsed.apiBaseUrl).update("\0").update(parsed.token).digest("hex").slice(0, 12)}`;
|
|
37835
|
+
}
|
|
37836
|
+
/** The command that checks a poll-receiving profile for new mail. */
|
|
37837
|
+
function pollCheckCommand(invocation, profileName) {
|
|
37838
|
+
return `PRIMITIVE_AGENT_PROFILE=${profileName} ${invocation} agent check-mail --json`;
|
|
37839
|
+
}
|
|
36864
37840
|
/** A short private role note; never a transcript or secret. */
|
|
36865
37841
|
function agentInfoValue(name, info) {
|
|
36866
37842
|
const clean = (value, max, label) => {
|
|
@@ -36948,11 +37924,15 @@ async function runAgentConnect(options) {
|
|
|
36948
37924
|
...defaults(options),
|
|
36949
37925
|
...Object.fromEntries(Object.entries(options.dependencies ?? {}).filter(([, value]) => value !== void 0))
|
|
36950
37926
|
};
|
|
36951
|
-
|
|
36952
|
-
|
|
36953
|
-
|
|
36954
|
-
|
|
36955
|
-
|
|
37927
|
+
const session = options.session?.trim() ? options.session : void 0;
|
|
37928
|
+
if (session !== void 0 && !SESSION_UUID$1.test(session)) throw new AgentConnectionSetupError("--session requires the exact loaded session UUID.");
|
|
37929
|
+
if (session === void 0 && options.receiver && options.receiver !== "poll") throw new AgentConnectionSetupError(`--receiver ${options.receiver} requires the exact loaded session UUID. Without one, use --receiver poll. No invitation was claimed.`);
|
|
37930
|
+
if (session === void 0 && options.resume && !options.profileName) throw new AgentConnectionSetupError("Pass --profile to resume a setup that binds no session.");
|
|
37931
|
+
const runtime = session ? detectAgentRuntime(session, env) : null;
|
|
37932
|
+
const invitation = options.resume || session || options.profileName ? void 0 : await options.readInvitation();
|
|
37933
|
+
const profileName = agentProfileName(options.profileName ?? (session ? defaultAgentProfileName(session) : invitationProfileName(invitation ?? "")));
|
|
37934
|
+
const receiver = options.receiver ?? (options.resume ? savedSetup(options.configDir, profileName)?.receiverMode ?? null : null) ?? (!session ? "poll" : runtime === "claude" ? "external" : "native");
|
|
37935
|
+
if (receiver === "external" && env.CLAUDE_CODE_SESSION_ID?.trim().toLowerCase() !== session?.toLowerCase()) throw new AgentConnectionSetupError("External receiving requires this exact Claude session ID. No invitation was claimed.");
|
|
36956
37936
|
const agentInfo = agentInfoValue(options.name, options.info);
|
|
36957
37937
|
const skipped = [];
|
|
36958
37938
|
let skill;
|
|
@@ -36989,11 +37969,11 @@ async function runAgentConnect(options) {
|
|
|
36989
37969
|
const result = await dependencies.setupAgent({
|
|
36990
37970
|
configDir: options.configDir,
|
|
36991
37971
|
profileName,
|
|
36992
|
-
session:
|
|
37972
|
+
...session ? { session } : {},
|
|
36993
37973
|
receiverMode: receiver,
|
|
36994
37974
|
resume: options.resume,
|
|
36995
37975
|
contactRequests: options.contactRequests,
|
|
36996
|
-
...options.resume ? {} : { invitation: await options.readInvitation() }
|
|
37976
|
+
...options.resume ? {} : { invitation: invitation ?? await options.readInvitation() }
|
|
36997
37977
|
});
|
|
36998
37978
|
const verified = verificationReplySubmitted(result.verification.state);
|
|
36999
37979
|
const ownerMemberAddress = await dependencies.refreshOwnerMemberAddress(result.identity.profileName);
|
|
@@ -37007,7 +37987,7 @@ async function runAgentConnect(options) {
|
|
|
37007
37987
|
configDir: options.configDir,
|
|
37008
37988
|
profileName: result.identity.profileName,
|
|
37009
37989
|
agentAddress: result.identity.agentAddress,
|
|
37010
|
-
sessionId:
|
|
37990
|
+
sessionId: session ?? "",
|
|
37011
37991
|
env
|
|
37012
37992
|
});
|
|
37013
37993
|
else skipped.push({
|
|
@@ -37053,22 +38033,27 @@ async function runAgentConnect(options) {
|
|
|
37053
38033
|
reason: "already_present"
|
|
37054
38034
|
});
|
|
37055
38035
|
}
|
|
38036
|
+
const invocation = options.invocation ?? "primitive";
|
|
37056
38037
|
const receiving = receiver === "external" ? {
|
|
37057
38038
|
...result.receiving,
|
|
37058
38039
|
mode: "external",
|
|
37059
38040
|
hook: externalHook
|
|
38041
|
+
} : receiver === "poll" ? {
|
|
38042
|
+
...result.receiving,
|
|
38043
|
+
mode: "poll",
|
|
38044
|
+
checkCommand: pollCheckCommand(invocation, result.identity.profileName)
|
|
37060
38045
|
} : {
|
|
37061
38046
|
...result.receiving,
|
|
37062
38047
|
mode: "native",
|
|
37063
38048
|
mailCheck: mailCheck?.state ?? "not_started",
|
|
37064
38049
|
lastSuccessfulMailCheckAt: mailCheck?.lastSuccessfulMailCheckAt ?? null
|
|
37065
38050
|
};
|
|
37066
|
-
const receiverReady = receiver === "external" ? externalHook === "installed_unverified" : result.receiving.state === "healthy";
|
|
38051
|
+
const receiverReady = receiver === "external" ? externalHook === "installed_unverified" : receiver === "poll" || result.receiving.state === "healthy";
|
|
37067
38052
|
const resumeCommand = [
|
|
37068
|
-
|
|
38053
|
+
invocation,
|
|
37069
38054
|
"agent connect",
|
|
37070
38055
|
`--profile ${result.identity.profileName}`,
|
|
37071
|
-
`--session ${
|
|
38056
|
+
...session ? [`--session ${session}`] : [],
|
|
37072
38057
|
`--receiver ${receiver}`,
|
|
37073
38058
|
"--resume",
|
|
37074
38059
|
...options.contactRequests ?? savedSetup(options.configDir, profileName)?.contactRequests ? ["--contact-requests"] : [],
|
|
@@ -37083,7 +38068,7 @@ async function runAgentConnect(options) {
|
|
|
37083
38068
|
ownerAddress: result.identity.ownerAddress,
|
|
37084
38069
|
ownerMemberAddress,
|
|
37085
38070
|
profile: result.identity.profileName,
|
|
37086
|
-
sessionId:
|
|
38071
|
+
sessionId: session ?? null,
|
|
37087
38072
|
runtime,
|
|
37088
38073
|
cli: {
|
|
37089
38074
|
version: options.cliVersion,
|
|
@@ -37105,15 +38090,16 @@ async function runAgentConnect(options) {
|
|
|
37105
38090
|
//#endregion
|
|
37106
38091
|
//#region src/oclif/commands/agent-connect.ts
|
|
37107
38092
|
/** npx runs the CLI from its cache; resume through npx too so the command works without a global install. */
|
|
37108
|
-
function invocation(entry) {
|
|
38093
|
+
function invocation$1(entry) {
|
|
37109
38094
|
return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
|
|
37110
38095
|
}
|
|
37111
38096
|
var AgentConnectCommand = class AgentConnectCommand extends Command {
|
|
37112
|
-
static description = "Connect this coding session to Primitive in one call. With --session, the command checks this CLI's capabilities, installs or refreshes the matching primitive-connect skill for the detected runtime (Claude Code or Codex) from files bundled in this CLI, claims the owner's private setup invitation from piped stdin, answers one authenticated setup challenge, enables owner notifications, starts receiving, seeds a private AGENT_INFO note from --name and --info when absent, and prints one JSON result. The receiver defaults to external hooks in Claude Code and the native background listener elsewhere. Native receiving starts or reuses a supervised background listener and waits briefly for its first successful mail check; external receiving installs the exact Claude session's fail-open Stop hook and resume SessionStart hook after the verification reply; a real idle mail event must still verify wake. The profile defaults to session-<session
|
|
38097
|
+
static description = "Connect this coding session to Primitive in one call. With --session, the command checks this CLI's capabilities, installs or refreshes the matching primitive-connect skill for the detected runtime (Claude Code or Codex) from files bundled in this CLI, claims the owner's private setup invitation from piped stdin, answers one authenticated setup challenge, enables owner notifications, starts receiving, seeds a private AGENT_INFO note from --name and --info when absent, and prints one JSON result. The receiver defaults to external hooks in Claude Code and the native background listener elsewhere. Without a session (no --session, or an empty one) the receiver is poll: for runtimes with no local session ID or hooks, such as a cloud-hosted conversation whose commands run in a separate sandbox, the invitation is claimed and verified the same way, nothing is installed, and the result's receiving.checkCommand is the command the agent runs to check for new mail at the start of each turn and after it sends. Native receiving starts or reuses a supervised background listener and waits briefly for its first successful mail check; external receiving installs the exact Claude session's fail-open Stop hook and resume SessionStart hook after the verification reply; a real idle mail event must still verify wake. The profile defaults to session-<session>, or to connection-<invitation hash prefix> without a session. --resume continues saved progress without reading stdin or replaying a claim or uncertain verification send. After the reply is submitted it waits up to a minute for the server to confirm the connection and reports verification.state verified, or reply_submitted while confirmation is still pending. Verification is separate from receiver health. Both official production (https://api.primitive.dev/v1) and staging (https://api.primitive-staging-1.com/v1) invitations pin their API origin. Never pass an invitation as a command argument. Select the saved identity with PRIMITIVE_AGENT_PROFILE. Use --profile with --status --json to inspect saved identity and local receiver health offline.";
|
|
37113
38098
|
static summary = "Connect and verify an agent address";
|
|
37114
38099
|
static examples = [
|
|
37115
38100
|
"<%= config.bin %> agent connect --session \"$CODEX_THREAD_ID\" --name Research --info \"Reviews pull requests\" --json < private-invitation.txt",
|
|
37116
38101
|
"<%= config.bin %> agent connect --session \"$CLAUDE_CODE_SESSION_ID\" --contact-requests --json < private-invitation.txt",
|
|
38102
|
+
"<%= config.bin %> agent connect --receiver poll --name Research --json < private-invitation.txt",
|
|
37117
38103
|
"<%= config.bin %> agent connect --profile work --session 11111111-1111-4111-8111-111111111111 --contact-requests --json < private-invitation.txt",
|
|
37118
38104
|
"<%= config.bin %> agent connect --profile work --session 11111111-1111-4111-8111-111111111111 --contact-requests --resume --json",
|
|
37119
38105
|
"<%= config.bin %> agent connect --profile work --session 11111111-1111-4111-8111-111111111111 --receiver external --contact-requests --json < private-invitation.txt",
|
|
@@ -37122,33 +38108,31 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
|
|
|
37122
38108
|
"PRIMITIVE_AGENT_PROFILE=work <%= config.bin %> whoami --json"
|
|
37123
38109
|
];
|
|
37124
38110
|
static flags = {
|
|
37125
|
-
profile: Flags.string({ description: "Local agent profile name; defaults to session-<session> with --session. Existing OAuth login is preserved" }),
|
|
38111
|
+
profile: Flags.string({ description: "Local agent profile name; defaults to session-<session> with --session, or connection-<invitation hash prefix> for poll receiving. Existing OAuth login is preserved" }),
|
|
37126
38112
|
session: Flags.string({
|
|
37127
|
-
description: "Exact current session UUID; verify email and configure receiving after claiming",
|
|
38113
|
+
description: "Exact current session UUID; verify email and configure receiving after claiming. Omit it (or pass it empty) when the runtime exposes none, which selects poll receiving",
|
|
37128
38114
|
exclusive: ["status"]
|
|
37129
38115
|
}),
|
|
37130
38116
|
receiver: Flags.string({
|
|
37131
|
-
description: "Native starts a supported session receiver; external installs the exact Claude session's fail-open Stop hook and resume SessionStart hook
|
|
37132
|
-
options: [
|
|
37133
|
-
|
|
38117
|
+
description: "Native starts a supported session receiver; external installs the exact Claude session's fail-open Stop hook and resume SessionStart hook; poll installs nothing and the agent checks for mail itself with `agent check-mail`. Defaults to external in Claude Code, native elsewhere, and poll without a session",
|
|
38118
|
+
options: [...RECEIVER_MODES],
|
|
38119
|
+
exclusive: ["status"]
|
|
37134
38120
|
}),
|
|
37135
38121
|
resume: Flags.boolean({
|
|
37136
38122
|
description: "Resume this session's saved setup without reading stdin, claiming again, or resending verification",
|
|
37137
|
-
dependsOn: ["session"],
|
|
37138
38123
|
exclusive: ["status"]
|
|
37139
38124
|
}),
|
|
37140
38125
|
"contact-requests": Flags.boolean({
|
|
37141
38126
|
description: "Receive owner-enabled first contact requests in this session; grants no task authority",
|
|
37142
|
-
dependsOn: ["session"],
|
|
37143
38127
|
exclusive: ["status"]
|
|
37144
38128
|
}),
|
|
37145
38129
|
name: Flags.string({
|
|
37146
38130
|
description: "Short agent name for a private AGENT_INFO note, written only when the note is absent",
|
|
37147
|
-
|
|
38131
|
+
exclusive: ["status"]
|
|
37148
38132
|
}),
|
|
37149
38133
|
info: Flags.string({
|
|
37150
38134
|
description: "Short role and capability description for a private AGENT_INFO note, written only when the note is absent",
|
|
37151
|
-
|
|
38135
|
+
exclusive: ["status"]
|
|
37152
38136
|
}),
|
|
37153
38137
|
skill: Flags.boolean({
|
|
37154
38138
|
description: "Install or refresh the primitive-connect skill bundled with this CLI for the detected runtime",
|
|
@@ -37157,13 +38141,14 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
|
|
|
37157
38141
|
}),
|
|
37158
38142
|
project: Flags.boolean({
|
|
37159
38143
|
description: "Install the skill into this project (.claude/skills or .agents/skills) instead of the user skills folder",
|
|
37160
|
-
|
|
38144
|
+
exclusive: ["status"]
|
|
37161
38145
|
}),
|
|
37162
38146
|
status: Flags.boolean({ description: "Read saved identity and local receiver health offline without reading stdin" }),
|
|
37163
38147
|
json: Flags.boolean({ description: "Print setup status and identity metadata, never the credential" })
|
|
37164
38148
|
};
|
|
37165
38149
|
async run() {
|
|
37166
38150
|
const { flags } = await this.parse(AgentConnectCommand);
|
|
38151
|
+
let pollProfile;
|
|
37167
38152
|
try {
|
|
37168
38153
|
if (flags.status) {
|
|
37169
38154
|
if (!flags.profile) throw new AgentConnectionSetupError("Pass --profile with --status.");
|
|
@@ -37177,15 +38162,24 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
|
|
|
37177
38162
|
}
|
|
37178
38163
|
return;
|
|
37179
38164
|
}
|
|
37180
|
-
|
|
38165
|
+
const session = flags.session?.trim() || void 0;
|
|
38166
|
+
if (!(flags.session === void 0 && flags.profile !== void 0 && flags.receiver === void 0 && !flags.resume && !flags["contact-requests"] && flags.name === void 0 && flags.info === void 0)) {
|
|
38167
|
+
if (!session && (flags.receiver === "native" || flags.receiver === "external")) throw new AgentConnectionSetupError(`--receiver ${flags.receiver} requires the exact loaded session UUID. Without one, use --receiver poll. No invitation was claimed.`);
|
|
38168
|
+
let readInvitation = () => readAgentInvitation(process.stdin, process.stdin.isTTY);
|
|
38169
|
+
if (!session && !flags.resume) if (flags.profile) pollProfile = flags.profile;
|
|
38170
|
+
else {
|
|
38171
|
+
const text = await readInvitation();
|
|
38172
|
+
pollProfile = invitationProfileName(text);
|
|
38173
|
+
readInvitation = async () => text;
|
|
38174
|
+
}
|
|
37181
38175
|
const output = await runAgentConnect({
|
|
37182
38176
|
configDir: this.config.configDir,
|
|
37183
38177
|
packageRoot: this.config.root,
|
|
37184
38178
|
cliVersion: this.config.version,
|
|
37185
38179
|
cliPath: process.argv[1] ?? "",
|
|
37186
|
-
invocation: invocation(process.argv[1]),
|
|
37187
|
-
session
|
|
37188
|
-
profileName: flags.profile,
|
|
38180
|
+
invocation: invocation$1(process.argv[1]),
|
|
38181
|
+
session,
|
|
38182
|
+
profileName: flags.profile ?? pollProfile,
|
|
37189
38183
|
receiver: flags.receiver,
|
|
37190
38184
|
resume: flags.resume,
|
|
37191
38185
|
contactRequests: flags["contact-requests"],
|
|
@@ -37193,21 +38187,23 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
|
|
|
37193
38187
|
info: flags.info,
|
|
37194
38188
|
skill: flags.skill,
|
|
37195
38189
|
project: flags.project,
|
|
37196
|
-
readInvitation
|
|
38190
|
+
readInvitation
|
|
37197
38191
|
});
|
|
37198
38192
|
const external = output.receiving.mode === "external";
|
|
38193
|
+
const poll = output.receiving.mode === "poll";
|
|
37199
38194
|
if (flags.json) this.log(JSON.stringify(output));
|
|
37200
38195
|
else {
|
|
37201
38196
|
this.log(`Agent ${output.address}: verification ${output.verification.state}; receiving ${output.receiving.state}; skill ${output.skill.state}.`);
|
|
37202
38197
|
this.log(`Select it with ${output.selectProfile}.`);
|
|
37203
38198
|
if (output.ownerNotifications === "silenced") this.log("Owner notifications remain silenced by the existing preference or policy.");
|
|
37204
38199
|
if (external && verificationReplySubmitted(output.verification.state)) this.log(output.externalHook === "installed_unverified" ? "External receive hook installed. Idle wake still needs a live mail check." : "External receive hook is unavailable. Pairing may complete, but this session will not wake automatically.");
|
|
38200
|
+
if (poll && "checkCommand" in output.receiving) this.log(`Nothing wakes this session for new mail. Check it at the start of each turn and after sending with: ${output.receiving.checkCommand}`);
|
|
37205
38201
|
if (output.status !== "connected") this.log(`Resume: ${output.resumeCommand}`);
|
|
37206
38202
|
}
|
|
37207
|
-
if (external && output.externalHook !== "installed_unverified" || !external && output.receiving.state !== "healthy" || !verificationReplySubmitted(output.verification.state)) process.exitCode = 2;
|
|
38203
|
+
if (external && output.externalHook !== "installed_unverified" || !external && !poll && output.receiving.state !== "healthy" || !verificationReplySubmitted(output.verification.state)) process.exitCode = 2;
|
|
37208
38204
|
return;
|
|
37209
38205
|
}
|
|
37210
|
-
if (!flags.profile) throw new AgentConnectionSetupError("Pass --
|
|
38206
|
+
if (!flags.profile) throw new AgentConnectionSetupError("Pass --profile for claim-only.");
|
|
37211
38207
|
const claimed = await connectAgent({
|
|
37212
38208
|
configDir: this.config.configDir,
|
|
37213
38209
|
profileName: flags.profile,
|
|
@@ -37224,7 +38220,7 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
|
|
|
37224
38220
|
this.log(result.ownerReportGuidance);
|
|
37225
38221
|
}
|
|
37226
38222
|
} catch (error) {
|
|
37227
|
-
throw new Errors.CLIError(error instanceof AgentConnectionSetupError ? error.message : flags.session ? `Setup paused. Preserve the private profile and run ${invocation(process.argv[1])} agent connect --profile ${flags.profile ?? defaultAgentProfileName(flags.session)} --session ${flags.session}${flags.receiver ? ` --receiver ${flags.receiver}` : ""} --resume --json. Do not manually resend verification or reclaim the invitation.` : "Agent setup did not complete. Preserve the private profile and request a fresh owner invitation if a claim may have been submitted.", { exit: 1 });
|
|
38223
|
+
throw new Errors.CLIError(error instanceof AgentConnectionSetupError ? error.message : flags.session?.trim() ? `Setup paused. Preserve the private profile and run ${invocation$1(process.argv[1])} agent connect --profile ${flags.profile ?? defaultAgentProfileName(flags.session.trim())} --session ${flags.session.trim()}${flags.receiver ? ` --receiver ${flags.receiver}` : ""} --resume --json. Do not manually resend verification or reclaim the invitation.` : pollProfile ? `Setup paused. Preserve the private profile and run ${invocation$1(process.argv[1])} agent connect --profile ${pollProfile} --receiver poll --resume --json. Do not manually resend verification or reclaim the invitation.` : "Agent setup did not complete. Preserve the private profile and request a fresh owner invitation if a claim may have been submitted.", { exit: 1 });
|
|
37228
38224
|
}
|
|
37229
38225
|
}
|
|
37230
38226
|
};
|
|
@@ -37413,6 +38409,7 @@ function boundSession(directory, profile) {
|
|
|
37413
38409
|
if (raw === null) return null;
|
|
37414
38410
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new AgentDisconnectError("Saved session setup is invalid. The connection was not changed.");
|
|
37415
38411
|
const row = raw;
|
|
38412
|
+
if (row.receiverMode === "poll" && row.session === null && row.invitationHash === profile.invitation_hash) return null;
|
|
37416
38413
|
if (typeof row.session !== "string" || !SESSION_UUID$1.test(row.session) || row.invitationHash !== profile.invitation_hash) throw new AgentDisconnectError("Saved session binding does not match this profile. The connection was not changed.");
|
|
37417
38414
|
return row.session.toLowerCase();
|
|
37418
38415
|
}
|
|
@@ -37580,7 +38577,7 @@ function savedEnrollment(value) {
|
|
|
37580
38577
|
"ownerAddress"
|
|
37581
38578
|
];
|
|
37582
38579
|
if (row?.version === 2) keys.push("createRequestId", "continueAttempted");
|
|
37583
|
-
if (!row || Object.keys(row).length !== keys.length || keys.some((key) => !Object.hasOwn(row, key)) || row.version !== 1 && row.version !== 2 || row.version === 2 && (typeof row.createRequestId !== "string" || !SESSION_UUID$1.test(row.createRequestId) || typeof row.continueAttempted !== "boolean") || typeof row.session !== "string" || !SESSION_UUID$1.test(row.session) || row.profile !== `session-${row.session}` || typeof row.name !== "string" || !validName(row.name) || !(typeof row.address === "string" && validAddress(row.address) || row.version === 2 && row.address === null && row.phase === "create_attempted") || typeof row.orgId !== "string" || !row.orgId || typeof row.grantId !== "string" || !row.grantId || typeof row.apiBaseUrl !== "string" || !
|
|
38580
|
+
if (!row || Object.keys(row).length !== keys.length || keys.some((key) => !Object.hasOwn(row, key)) || row.version !== 1 && row.version !== 2 || row.version === 2 && (typeof row.createRequestId !== "string" || !SESSION_UUID$1.test(row.createRequestId) || typeof row.continueAttempted !== "boolean") || typeof row.session !== "string" || !SESSION_UUID$1.test(row.session) || row.profile !== `session-${row.session}` || typeof row.name !== "string" || !validName(row.name) || !(typeof row.address === "string" && validAddress(row.address) || row.version === 2 && row.address === null && row.phase === "create_attempted") || typeof row.orgId !== "string" || !row.orgId || typeof row.grantId !== "string" || !row.grantId || typeof row.apiBaseUrl !== "string" || !RECEIVER_MODES.includes(row.receiverMode) || typeof row.contactRequests !== "boolean" || typeof row.startedAt !== "string" || !Number.isFinite(Date.parse(row.startedAt)) || !["create_attempted", "setup_attempted"].includes(String(row.phase)) || row.phase === "create_attempted" && (row.invitationHash !== null || row.ownerAddress !== null) || row.phase === "setup_attempted" && (typeof row.address !== "string" || typeof row.invitationHash !== "string" || !/^[a-f0-9]{64}$/.test(row.invitationHash) || typeof row.ownerAddress !== "string" || !row.ownerAddress.includes("@"))) throw refuse("Saved enrollment state is invalid. Preserve its private files.");
|
|
37584
38581
|
connectedApiBaseUrl(row.apiBaseUrl);
|
|
37585
38582
|
if (row.ownerAddress !== null) try {
|
|
37586
38583
|
if (mailAddress(row.ownerAddress) !== row.ownerAddress) throw new Error();
|
|
@@ -37606,6 +38603,7 @@ function resolvedEnrollment(state) {
|
|
|
37606
38603
|
};
|
|
37607
38604
|
}
|
|
37608
38605
|
async function preflight(session, receiver, env) {
|
|
38606
|
+
if (receiver === "poll") return;
|
|
37609
38607
|
if (receiver === "external") {
|
|
37610
38608
|
if (env.CLAUDE_CODE_SESSION_ID !== session) throw refuse("External receiving requires this exact Claude session ID. No address was created.");
|
|
37611
38609
|
return;
|
|
@@ -38036,6 +39034,10 @@ async function enrollAgent(params) {
|
|
|
38036
39034
|
}
|
|
38037
39035
|
//#endregion
|
|
38038
39036
|
//#region src/oclif/commands/agent-enroll.ts
|
|
39037
|
+
/** npx runs the CLI from its cache; print follow-up commands the same way. */
|
|
39038
|
+
function invocation(entry) {
|
|
39039
|
+
return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
|
|
39040
|
+
}
|
|
38039
39041
|
var AgentEnrollCommand = class AgentEnrollCommand extends Command {
|
|
38040
39042
|
static summary = "Give this coding session an address in your organization";
|
|
38041
39043
|
static description = "On this trusted machine, use the saved member OAuth login to create one address for the exact loaded session, claim its one-use invitation privately, answer the email challenge, and poll the owner's connection list for confirmed pairing. Native receiving starts when supported. With --receiver external in the exact Claude session, install a fail-open Stop hook and resume SessionStart hook in that runtime's settings after the verification reply; idle wake remains unverified until tested with real mail. With --contact-requests, the login conditionally enables first-contact intake for this exact address after verification, preserving existing policy rules. An explicit disable or uncertain policy update is reported separately without losing pairing or receiver setup. The server allocates a readable address. An uncertain creation resumes the same saved request; recovered results do not include another invitation. Use --continue-setup once to explicitly continue a recovered pending connection. An uncertain continuation or claim is held for owner recovery, never repeated automatically. Requires a verified Primitive-managed domain. This command does not accept API keys, a connection profile, or an invitation argument.";
|
|
@@ -38048,8 +39050,8 @@ var AgentEnrollCommand = class AgentEnrollCommand extends Command {
|
|
|
38048
39050
|
name: Flags.string({ description: "Short agent display name; defaults to Coding agent" }),
|
|
38049
39051
|
"continue-setup": Flags.boolean({ description: "Explicitly issue one invitation only if the recovered agent is still pending; never reconnect a claimed agent" }),
|
|
38050
39052
|
receiver: Flags.string({
|
|
38051
|
-
options: [
|
|
38052
|
-
description: "Native session receiver,
|
|
39053
|
+
options: [...RECEIVER_MODES],
|
|
39054
|
+
description: "Native session receiver, external runtime event hook, or poll: nothing is installed and the agent checks for mail itself with `agent check-mail`"
|
|
38053
39055
|
}),
|
|
38054
39056
|
"contact-requests": Flags.boolean({ description: "Enable this agent's first-contact policy with the saved member login, then receive relevant requests" }),
|
|
38055
39057
|
json: Flags.boolean({ description: "Print status without credentials or invitation" })
|
|
@@ -38072,7 +39074,15 @@ var AgentEnrollCommand = class AgentEnrollCommand extends Command {
|
|
|
38072
39074
|
agentAddress: result.identity.agentAddress,
|
|
38073
39075
|
sessionId: flags.session
|
|
38074
39076
|
}) : null;
|
|
38075
|
-
const output = {
|
|
39077
|
+
const output = result.receiving.state === "poll" ? {
|
|
39078
|
+
...result,
|
|
39079
|
+
receiving: {
|
|
39080
|
+
...result.receiving,
|
|
39081
|
+
mode: "poll",
|
|
39082
|
+
checkCommand: pollCheckCommand(invocation(process.argv[1]), result.identity.profileName)
|
|
39083
|
+
},
|
|
39084
|
+
externalHook
|
|
39085
|
+
} : {
|
|
38076
39086
|
...result,
|
|
38077
39087
|
externalHook
|
|
38078
39088
|
};
|
|
@@ -38086,9 +39096,10 @@ var AgentEnrollCommand = class AgentEnrollCommand extends Command {
|
|
|
38086
39096
|
if (externalHook === "installed_unverified") this.log("External receive hook installed for this runtime. Idle wake still needs a live mail check.");
|
|
38087
39097
|
else if (externalHook === "unavailable") this.log("Could not install the external receive hook. Pairing is confirmed, but later mail will not wake this session automatically.");
|
|
38088
39098
|
if (result.connection.status !== "connected") this.log("Pairing is not yet confirmed. Rerun this command with the same options to resume the saved session; do not create another address.");
|
|
39099
|
+
else if ("checkCommand" in output.receiving) this.log(`Nothing wakes this session for new mail. Check it at the start of each turn and after sending with: ${output.receiving.checkCommand}`);
|
|
38089
39100
|
else if (result.receiving.state !== "healthy") this.log("Pairing is confirmed; receiving needs separate setup or recovery.");
|
|
38090
39101
|
}
|
|
38091
|
-
if (externalHook === "unavailable" || flags["contact-requests"] && result.contactRequestPolicy !== "enabled" || result.connection.status !== "connected" || flags.receiver !== "external" && result.receiving.state !== "healthy") process.exitCode = 2;
|
|
39102
|
+
if (externalHook === "unavailable" || flags["contact-requests"] && result.contactRequestPolicy !== "enabled" || result.connection.status !== "connected" || flags.receiver !== "external" && flags.receiver !== "poll" && result.receiving.state !== "healthy") process.exitCode = 2;
|
|
38092
39103
|
} catch (error) {
|
|
38093
39104
|
throw new Errors.CLIError(error instanceof AgentConnectionSetupError ? error.message : "Enrollment paused. Preserve this session's private profile and inspect the pending agent in the app before retrying. No second address was requested automatically.", { exit: 1 });
|
|
38094
39105
|
}
|
|
@@ -38482,6 +39493,33 @@ function enrollmentAddress(configDir, profileName) {
|
|
|
38482
39493
|
};
|
|
38483
39494
|
}
|
|
38484
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
|
+
}
|
|
38485
39523
|
/** `agent disconnect` already confirmed revocation of this profile's credential. */
|
|
38486
39524
|
function disconnectConfirmedLocally(configDir, profileName) {
|
|
38487
39525
|
try {
|
|
@@ -38618,7 +39656,34 @@ async function registerSession(options) {
|
|
|
38618
39656
|
detail: "The session ID is not a UUID. Nothing was changed."
|
|
38619
39657
|
};
|
|
38620
39658
|
const sessionId = session.toLowerCase();
|
|
38621
|
-
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;
|
|
38622
39687
|
const known = {
|
|
38623
39688
|
...base,
|
|
38624
39689
|
session: sessionId,
|
|
@@ -38651,7 +39716,7 @@ async function registerSession(options) {
|
|
|
38651
39716
|
let record = readSessionRecord(options.configDir, sessionId);
|
|
38652
39717
|
if (record?.endedAt) return await ended(record);
|
|
38653
39718
|
const finishAgentInfo = async () => {
|
|
38654
|
-
if (record?.agentInfo) return;
|
|
39719
|
+
if (reused || record?.agentInfo) return;
|
|
38655
39720
|
const seeded = await deps.seedAgentInfo(options.configDir, profileName, agentInfoForSession(runtime, cwd));
|
|
38656
39721
|
if (seeded !== "created" && seeded !== "already_present") return;
|
|
38657
39722
|
record = await updateSessionRecord(options.configDir, sessionId, (current) => current ? {
|
|
@@ -40386,10 +41451,10 @@ function contactRequestSessionKey(context, env = process.env) {
|
|
|
40386
41451
|
}
|
|
40387
41452
|
if (typeof raw !== "object" || Array.isArray(raw)) throw sessionRequired();
|
|
40388
41453
|
const setup = raw;
|
|
40389
|
-
const session = mailId(setup.session);
|
|
40390
41454
|
const receiverMode = setup.receiverMode ?? "native";
|
|
41455
|
+
const poll = receiverMode === "poll" && setup.session === null;
|
|
40391
41456
|
const receipt = setup.receipt;
|
|
40392
|
-
if (setup.version !== 1 || setup.invitationHash !== profile.invitation_hash || setup.phase !== "sent" || !["native", "external"].includes(String(receiverMode)) || !receipt || typeof receipt !== "object" || Array.isArray(receipt) || ![
|
|
41457
|
+
if (setup.version !== 1 || setup.invitationHash !== profile.invitation_hash || setup.phase !== "sent" || !(poll || ["native", "external"].includes(String(receiverMode))) || !receipt || typeof receipt !== "object" || Array.isArray(receipt) || ![
|
|
40393
41458
|
"queued",
|
|
40394
41459
|
"submitted_to_agent",
|
|
40395
41460
|
"delivered",
|
|
@@ -40397,6 +41462,8 @@ function contactRequestSessionKey(context, env = process.env) {
|
|
|
40397
41462
|
"scheduled"
|
|
40398
41463
|
].includes(String(receipt.status))) throw sessionRequired();
|
|
40399
41464
|
mailId(receipt.id);
|
|
41465
|
+
if (poll) return null;
|
|
41466
|
+
const session = mailId(setup.session);
|
|
40400
41467
|
const key = `${receiverMode === "external" ? "claude" : "codex"}:${session}`;
|
|
40401
41468
|
if (hasRuntime && runtime !== key || !hasRuntime && receiverMode !== "external") throw sessionRequired();
|
|
40402
41469
|
return key;
|
|
@@ -45882,7 +46949,7 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
45882
46949
|
url: "https://primitive.dev"
|
|
45883
46950
|
};
|
|
45884
46951
|
const SDK_VERSION_RANGE = "^1.34.0";
|
|
45885
|
-
const CLI_VERSION_RANGE = "^1.
|
|
46952
|
+
const CLI_VERSION_RANGE = "^1.41.0";
|
|
45886
46953
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
45887
46954
|
function renderHandler() {
|
|
45888
46955
|
return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
|
|
@@ -50327,6 +51394,22 @@ const RESUME_LOCK_RETRY_STEP_MS = 250;
|
|
|
50327
51394
|
function isListenLockContention(error) {
|
|
50328
51395
|
return error instanceof ListenStateError && error.message.startsWith("Another listener is using this subscription");
|
|
50329
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
|
+
}
|
|
50330
51413
|
var ListenCommand = class ListenCommand extends Command {
|
|
50331
51414
|
static summary = "Receive webhook events locally without a public endpoint";
|
|
50332
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.";
|
|
@@ -50538,6 +51621,7 @@ var ListenCommand = class ListenCommand extends Command {
|
|
|
50538
51621
|
}
|
|
50539
51622
|
if (flags.wake && !hookSessionId) throw new Errors.CLIError("--wake requires --hook-session with an exact session_id.");
|
|
50540
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);
|
|
50541
51625
|
if (flags["notify-session"] && flags.transport === "poll") throw new Errors.CLIError("--notify-session requires --transport websocket.");
|
|
50542
51626
|
if (flags["notify-session"] && flags.subscription !== void 0) throw new Errors.CLIError("--notify-session uses the shared saved subscription; omit --subscription.");
|
|
50543
51627
|
if (flags.subscription?.trim().startsWith("local-mail-")) throw new Errors.CLIError("Subscription names beginning local-mail- are reserved for shared mail receiving.");
|
|
@@ -51811,426 +52895,83 @@ async function runSignupInteractiveWithCredentialLock(params) {
|
|
|
51811
52895
|
const verificationCode = await promptRequiredFn(`Verification code (${start.verification_code_length} digits): `);
|
|
51812
52896
|
if (verificationCode.toLowerCase() === "resend") {
|
|
51813
52897
|
const resend = await resendVerificationCode({
|
|
51814
|
-
apiBaseUrl,
|
|
51815
|
-
apiClient,
|
|
51816
|
-
configDir,
|
|
51817
|
-
deps,
|
|
51818
|
-
start
|
|
51819
|
-
});
|
|
51820
|
-
start = resend.pending;
|
|
51821
|
-
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`);
|
|
51822
|
-
continue;
|
|
51823
|
-
}
|
|
51824
|
-
try {
|
|
51825
|
-
await runSignupConfirmWithCredentialLock({
|
|
51826
|
-
code: verificationCode,
|
|
51827
|
-
configDir,
|
|
51828
|
-
deps,
|
|
51829
|
-
email: start.email,
|
|
51830
|
-
flags: {
|
|
51831
|
-
"api-base-url": flags["api-base-url"],
|
|
51832
|
-
force: true
|
|
51833
|
-
},
|
|
51834
|
-
skipExistingCredentialCheck: true
|
|
51835
|
-
});
|
|
51836
|
-
return;
|
|
51837
|
-
} catch (error) {
|
|
51838
|
-
if (error instanceof Errors.CLIError && error.message.startsWith("Invalid verification code.")) {
|
|
51839
|
-
process$1.stderr.write("Invalid verification code. Try again or type `resend`.\n");
|
|
51840
|
-
continue;
|
|
51841
|
-
}
|
|
51842
|
-
throw error;
|
|
51843
|
-
}
|
|
51844
|
-
}
|
|
51845
|
-
}
|
|
51846
|
-
function commonStartFlags() {
|
|
51847
|
-
return {
|
|
51848
|
-
"accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
|
|
51849
|
-
"api-base-url": Flags.string({
|
|
51850
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
51851
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
51852
|
-
hidden: true
|
|
51853
|
-
}),
|
|
51854
|
-
"device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
|
|
51855
|
-
force: Flags.boolean({
|
|
51856
|
-
char: "f",
|
|
51857
|
-
description: "Replace saved credentials or pending signup state when needed"
|
|
51858
|
-
}),
|
|
51859
|
-
"signup-code": Flags.string({
|
|
51860
|
-
description: "Optional signup code. Omit if you do not have one.",
|
|
51861
|
-
env: "PRIMITIVE_SIGNUP_CODE"
|
|
51862
|
-
})
|
|
51863
|
-
};
|
|
51864
|
-
}
|
|
51865
|
-
var SignupCommand = class SignupCommand extends Command {
|
|
51866
|
-
static args = { email: Args.string({
|
|
51867
|
-
description: "Email address to sign up",
|
|
51868
|
-
required: false
|
|
51869
|
-
}) };
|
|
51870
|
-
static description = "Start a Primitive account signup, send an email verification code, and save a pending signup token locally.";
|
|
51871
|
-
static summary = "Start account signup";
|
|
51872
|
-
static examples = [
|
|
51873
|
-
"<%= config.bin %> signup user@example.com",
|
|
51874
|
-
"<%= config.bin %> signup user@example.com --accept-terms",
|
|
51875
|
-
"<%= config.bin %> signup user@example.com --signup-code invite-code --accept-terms",
|
|
51876
|
-
"<%= config.bin %> signup confirm user@example.com 123456"
|
|
51877
|
-
];
|
|
51878
|
-
static flags = commonStartFlags();
|
|
51879
|
-
async run() {
|
|
51880
|
-
const { args, flags } = await this.parse(SignupCommand);
|
|
51881
|
-
let releaseCredentialsLock;
|
|
51882
|
-
try {
|
|
51883
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
51884
|
-
} catch (error) {
|
|
51885
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
51886
|
-
}
|
|
51887
|
-
try {
|
|
51888
|
-
await runSignupStartWithCredentialLock({
|
|
51889
|
-
configDir: this.config.configDir,
|
|
51890
|
-
email: args.email,
|
|
51891
|
-
flags
|
|
51892
|
-
});
|
|
51893
|
-
} finally {
|
|
51894
|
-
releaseCredentialsLock();
|
|
51895
|
-
}
|
|
51896
|
-
}
|
|
51897
|
-
};
|
|
51898
|
-
function resolveVerificationCode(input) {
|
|
51899
|
-
const sources = [
|
|
51900
|
-
input.positional !== void 0 ? "positional" : null,
|
|
51901
|
-
input.fromStdin === true ? "--code-from-stdin" : null,
|
|
51902
|
-
input.fromFile !== void 0 ? "--code-from-file" : null,
|
|
51903
|
-
input.fromEnv !== void 0 ? "--code-from-env" : null
|
|
51904
|
-
].filter((v) => v !== null);
|
|
51905
|
-
if (sources.length === 0) return {
|
|
51906
|
-
kind: "error",
|
|
51907
|
-
message: "Pass the verification code as a positional argument or via one of --code-from-stdin, --code-from-file, or --code-from-env."
|
|
51908
|
-
};
|
|
51909
|
-
if (sources.length > 1) return {
|
|
51910
|
-
kind: "error",
|
|
51911
|
-
message: `Pass exactly one source for the verification code; got ${sources.join(", ")}.`
|
|
51912
|
-
};
|
|
51913
|
-
if (input.positional !== void 0) return {
|
|
51914
|
-
kind: "ok",
|
|
51915
|
-
code: input.positional
|
|
51916
|
-
};
|
|
51917
|
-
if (input.fromEnv !== void 0) {
|
|
51918
|
-
const value = (input.env ?? process$1.env)[input.fromEnv];
|
|
51919
|
-
if (value === void 0) return {
|
|
51920
|
-
kind: "error",
|
|
51921
|
-
message: `--code-from-env ${input.fromEnv}: environment variable is not set.`
|
|
51922
|
-
};
|
|
51923
|
-
return {
|
|
51924
|
-
kind: "ok",
|
|
51925
|
-
code: stripTrailingNewline(value)
|
|
51926
|
-
};
|
|
51927
|
-
}
|
|
51928
|
-
if (input.fromFile !== void 0) {
|
|
51929
|
-
const readFile = input.readFile ?? defaultReadCodeFile;
|
|
51930
|
-
try {
|
|
51931
|
-
return {
|
|
51932
|
-
kind: "ok",
|
|
51933
|
-
code: stripTrailingNewline(readFile(input.fromFile))
|
|
51934
|
-
};
|
|
51935
|
-
} catch (error) {
|
|
51936
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
51937
|
-
return {
|
|
51938
|
-
kind: "error",
|
|
51939
|
-
message: `--code-from-file ${input.fromFile}: could not read file: ${detail}`
|
|
51940
|
-
};
|
|
51941
|
-
}
|
|
51942
|
-
}
|
|
51943
|
-
const readStdin = input.readStdin ?? defaultReadCodeStdin;
|
|
51944
|
-
try {
|
|
51945
|
-
return {
|
|
51946
|
-
kind: "ok",
|
|
51947
|
-
code: stripTrailingNewline(readStdin())
|
|
51948
|
-
};
|
|
51949
|
-
} catch (error) {
|
|
51950
|
-
return {
|
|
51951
|
-
kind: "error",
|
|
51952
|
-
message: `--code-from-stdin: ${error instanceof Error ? error.message : String(error)}`
|
|
51953
|
-
};
|
|
51954
|
-
}
|
|
51955
|
-
}
|
|
51956
|
-
function stripTrailingNewline(value) {
|
|
51957
|
-
return value.replace(/\r?\n$/, "");
|
|
51958
|
-
}
|
|
51959
|
-
function defaultReadCodeFile(path) {
|
|
51960
|
-
return readFileSync(path, "utf8");
|
|
51961
|
-
}
|
|
51962
|
-
function defaultReadCodeStdin() {
|
|
51963
|
-
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.");
|
|
51964
|
-
return readFileSync(0, "utf8");
|
|
51965
|
-
}
|
|
51966
|
-
var SignupConfirmCommand = class SignupConfirmCommand extends Command {
|
|
51967
|
-
static args = {
|
|
51968
|
-
email: Args.string({
|
|
51969
|
-
description: "Email address used to start signup",
|
|
51970
|
-
required: true
|
|
51971
|
-
}),
|
|
51972
|
-
code: Args.string({
|
|
51973
|
-
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.",
|
|
51974
|
-
required: false
|
|
51975
|
-
})
|
|
51976
|
-
};
|
|
51977
|
-
static description = "Confirm a pending Primitive signup, create an OAuth session, and save CLI credentials locally.";
|
|
51978
|
-
static summary = "Confirm account signup";
|
|
51979
|
-
static examples = [
|
|
51980
|
-
"<%= config.bin %> signup confirm user@example.com 123456",
|
|
51981
|
-
"<%= config.bin %> signup confirm user@example.com 123456 --org-id 00000000-0000-4000-8000-000000000000",
|
|
51982
|
-
"read -rs CODE && CODE=\"$CODE\" <%= config.bin %> signup confirm user@example.com --code-from-env CODE && unset CODE",
|
|
51983
|
-
"read -rs CODE && printf '%s' \"$CODE\" | <%= config.bin %> signup confirm user@example.com --code-from-stdin && unset CODE",
|
|
51984
|
-
"<%= config.bin %> signup confirm user@example.com --code-from-file /run/user/$(id -u)/verification-code"
|
|
51985
|
-
];
|
|
51986
|
-
static flags = {
|
|
51987
|
-
"api-base-url": Flags.string({
|
|
51988
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
51989
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
51990
|
-
hidden: true
|
|
51991
|
-
}),
|
|
51992
|
-
"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." }),
|
|
51993
|
-
"code-from-file": Flags.string({ description: "Read the verification code from a UTF-8 file at this path. Trailing newlines are stripped." }),
|
|
51994
|
-
"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." }),
|
|
51995
|
-
force: Flags.boolean({
|
|
51996
|
-
char: "f",
|
|
51997
|
-
description: "Replace saved credentials after verification"
|
|
51998
|
-
}),
|
|
51999
|
-
"org-id": Flags.string({ description: "Workspace id to target when the email belongs to multiple workspaces" })
|
|
52000
|
-
};
|
|
52001
|
-
async run() {
|
|
52002
|
-
const { args, flags } = await this.parse(SignupConfirmCommand);
|
|
52003
|
-
const resolvedCode = resolveVerificationCode({
|
|
52004
|
-
positional: args.code,
|
|
52005
|
-
fromStdin: flags["code-from-stdin"] === true,
|
|
52006
|
-
fromFile: flags["code-from-file"],
|
|
52007
|
-
fromEnv: flags["code-from-env"]
|
|
52008
|
-
});
|
|
52009
|
-
if (resolvedCode.kind === "error") throw cliError$2(resolvedCode.message);
|
|
52010
|
-
let releaseCredentialsLock;
|
|
52011
|
-
try {
|
|
52012
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52013
|
-
} catch (error) {
|
|
52014
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52015
|
-
}
|
|
52016
|
-
try {
|
|
52017
|
-
await runSignupConfirmWithCredentialLock({
|
|
52018
|
-
code: resolvedCode.code,
|
|
52019
|
-
configDir: this.config.configDir,
|
|
52020
|
-
email: args.email,
|
|
52021
|
-
flags
|
|
52022
|
-
});
|
|
52023
|
-
} finally {
|
|
52024
|
-
releaseCredentialsLock();
|
|
52025
|
-
}
|
|
52026
|
-
}
|
|
52027
|
-
};
|
|
52028
|
-
var SignupResendCommand = class SignupResendCommand extends Command {
|
|
52029
|
-
static args = { email: Args.string({
|
|
52030
|
-
description: "Email address used to start signup. Defaults to the saved pending signup.",
|
|
52031
|
-
required: false
|
|
52032
|
-
}) };
|
|
52033
|
-
static description = "Resend the verification code for a pending signup.";
|
|
52034
|
-
static summary = "Resend signup verification code";
|
|
52035
|
-
static examples = ["<%= config.bin %> signup resend", "<%= config.bin %> signup resend user@example.com"];
|
|
52036
|
-
static flags = { "api-base-url": Flags.string({
|
|
52037
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52038
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
52039
|
-
hidden: true
|
|
52040
|
-
}) };
|
|
52041
|
-
async run() {
|
|
52042
|
-
const { args, flags } = await this.parse(SignupResendCommand);
|
|
52043
|
-
let releaseCredentialsLock;
|
|
52044
|
-
try {
|
|
52045
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52046
|
-
} catch (error) {
|
|
52047
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52048
|
-
}
|
|
52049
|
-
try {
|
|
52050
|
-
await runSignupResendWithCredentialLock({
|
|
52051
|
-
configDir: this.config.configDir,
|
|
52052
|
-
email: args.email,
|
|
52053
|
-
flags
|
|
52054
|
-
});
|
|
52055
|
-
} finally {
|
|
52056
|
-
releaseCredentialsLock();
|
|
52057
|
-
}
|
|
52058
|
-
}
|
|
52059
|
-
};
|
|
52060
|
-
var SignupStatusCommand = class SignupStatusCommand extends Command {
|
|
52061
|
-
static args = { email: Args.string({
|
|
52062
|
-
description: "Email address expected in the pending signup",
|
|
52063
|
-
required: false
|
|
52064
|
-
}) };
|
|
52065
|
-
static description = "Inspect the locally saved pending Primitive signup state.";
|
|
52066
|
-
static summary = "Show pending signup status";
|
|
52067
|
-
static examples = [
|
|
52068
|
-
"<%= config.bin %> signup status",
|
|
52069
|
-
"<%= config.bin %> signup status user@example.com",
|
|
52070
|
-
"<%= config.bin %> signup status --json"
|
|
52071
|
-
];
|
|
52072
|
-
static flags = {
|
|
52073
|
-
"api-base-url": Flags.string({
|
|
52074
|
-
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52075
|
-
env: "PRIMITIVE_API_BASE_URL",
|
|
52076
|
-
hidden: true
|
|
52077
|
-
}),
|
|
52078
|
-
json: Flags.boolean({ description: "Print pending signup status as JSON" })
|
|
52079
|
-
};
|
|
52080
|
-
async run() {
|
|
52081
|
-
const { args, flags } = await this.parse(SignupStatusCommand);
|
|
52082
|
-
runSignupStatus({
|
|
52083
|
-
configDir: this.config.configDir,
|
|
52084
|
-
email: args.email,
|
|
52085
|
-
flags
|
|
52086
|
-
});
|
|
52087
|
-
}
|
|
52088
|
-
};
|
|
52089
|
-
var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
|
|
52090
|
-
static description = "Run the full signup flow in one interactive terminal session.";
|
|
52091
|
-
static summary = "Run interactive account signup";
|
|
52092
|
-
static examples = ["<%= config.bin %> signup interactive"];
|
|
52093
|
-
static flags = commonStartFlags();
|
|
52094
|
-
async run() {
|
|
52095
|
-
const { flags } = await this.parse(SignupInteractiveCommand);
|
|
52096
|
-
let releaseCredentialsLock;
|
|
52097
|
-
try {
|
|
52098
|
-
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52099
|
-
} catch (error) {
|
|
52100
|
-
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52101
|
-
}
|
|
52102
|
-
try {
|
|
52103
|
-
await runSignupInteractiveWithCredentialLock({
|
|
52104
|
-
configDir: this.config.configDir,
|
|
52105
|
-
flags
|
|
52106
|
-
});
|
|
52107
|
-
} finally {
|
|
52108
|
-
releaseCredentialsLock();
|
|
52109
|
-
}
|
|
52110
|
-
}
|
|
52111
|
-
};
|
|
52112
|
-
//#endregion
|
|
52113
|
-
//#region src/oclif/commands/logout.ts
|
|
52114
|
-
function cliError$1(message) {
|
|
52115
|
-
return new Errors.CLIError(message, { exit: 1 });
|
|
52116
|
-
}
|
|
52117
|
-
function unwrapData(value) {
|
|
52118
|
-
return value?.data ?? null;
|
|
52119
|
-
}
|
|
52120
|
-
function isSavedOAuthSessionExpiredError(error) {
|
|
52121
|
-
return error instanceof Error && error.message === "Saved Primitive CLI OAuth session expired or was revoked. Run `primitive signin` to authenticate again.";
|
|
52122
|
-
}
|
|
52123
|
-
async function runLogoutWithCredentialLock(params) {
|
|
52124
|
-
requireDefaultLoginProfile();
|
|
52125
|
-
const deps = {
|
|
52126
|
-
cliLogout,
|
|
52127
|
-
createAuthenticatedCliApiClient,
|
|
52128
|
-
...params.deps
|
|
52129
|
-
};
|
|
52130
|
-
let credentials;
|
|
52131
|
-
try {
|
|
52132
|
-
credentials = loadCliCredentials(params.configDir);
|
|
52133
|
-
} catch (error) {
|
|
52134
|
-
deleteCliCredentials(params.configDir);
|
|
52135
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
52136
|
-
process.stderr.write(`Removed unreadable Primitive CLI credentials. Backing OAuth grant was not revoked: ${detail}\n`);
|
|
52137
|
-
process.exitCode = 1;
|
|
52138
|
-
return;
|
|
52139
|
-
}
|
|
52140
|
-
if (!credentials) throw cliError$1("Not logged in. Run `primitive signin` to create saved CLI credentials.");
|
|
52141
|
-
let authenticated;
|
|
52142
|
-
try {
|
|
52143
|
-
authenticated = await deps.createAuthenticatedCliApiClient({
|
|
52144
|
-
apiBaseUrl: params.flags["api-base-url"],
|
|
52145
|
-
configDir: params.configDir,
|
|
52146
|
-
credentialsLockHeld: true
|
|
52147
|
-
});
|
|
52148
|
-
} catch (error) {
|
|
52149
|
-
if (isSavedOAuthSessionExpiredError(error) && loadCliCredentials(params.configDir) === null) {
|
|
52150
|
-
process.stderr.write("Logged out (OAuth session was already expired or revoked on the server).\n");
|
|
52151
|
-
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;
|
|
52152
52907
|
}
|
|
52153
|
-
|
|
52154
|
-
|
|
52155
|
-
|
|
52156
|
-
|
|
52157
|
-
|
|
52158
|
-
|
|
52159
|
-
|
|
52160
|
-
|
|
52161
|
-
|
|
52162
|
-
|
|
52163
|
-
|
|
52164
|
-
|
|
52165
|
-
deleteCliCredentials(params.configDir);
|
|
52166
|
-
writeErrorWithHints(payload);
|
|
52167
|
-
process.stderr.write("Removed saved Primitive CLI credentials because the backing OAuth grant is already unavailable.\n");
|
|
52168
|
-
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
|
+
});
|
|
52169
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;
|
|
52170
52927
|
}
|
|
52171
|
-
writeErrorWithHints(payload);
|
|
52172
|
-
throw cliError$1("Could not revoke the saved Primitive CLI OAuth grant.");
|
|
52173
|
-
}
|
|
52174
|
-
const logout = unwrapData(result.data);
|
|
52175
|
-
deleteCliCredentials(params.configDir);
|
|
52176
|
-
const grantId = logout?.oauth_grant_id ?? freshCredentials.oauth_grant_id;
|
|
52177
|
-
process.stderr.write(`Logged out and revoked OAuth grant ${grantId}.\n`);
|
|
52178
|
-
}
|
|
52179
|
-
function runForceLogout(params) {
|
|
52180
|
-
requireDefaultLoginProfile();
|
|
52181
|
-
const localCredentialsPath = credentialsPath(params.configDir);
|
|
52182
|
-
const pendingPath = pendingSignupPath(params.configDir);
|
|
52183
|
-
const lockPath = credentialsLockPath(params.configDir);
|
|
52184
|
-
const removed = [
|
|
52185
|
-
existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
|
|
52186
|
-
existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
|
|
52187
|
-
existsSync(pendingPath) ? "pending email-code auth state" : null,
|
|
52188
|
-
existsSync(lockPath) ? "credential lock" : null
|
|
52189
|
-
].filter((value) => value !== null);
|
|
52190
|
-
deleteCliCredentials(params.configDir);
|
|
52191
|
-
deletePendingAgentSignup(params.configDir);
|
|
52192
|
-
deleteCliCredentialsLock(params.configDir);
|
|
52193
|
-
if (removed.length === 0) {
|
|
52194
|
-
process.stderr.write("No local Primitive CLI auth state was present. Backing OAuth grant was not revoked.\n");
|
|
52195
|
-
return;
|
|
52196
52928
|
}
|
|
52197
|
-
process.stderr.write(`Removed ${formatList(removed)}. Backing OAuth grant was not revoked.\n`);
|
|
52198
|
-
}
|
|
52199
|
-
function formatList(values) {
|
|
52200
|
-
if (values.length <= 1) return values[0] ?? "";
|
|
52201
|
-
if (values.length === 2) return `${values[0]} and ${values[1]}`;
|
|
52202
|
-
return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
|
|
52203
52929
|
}
|
|
52204
|
-
|
|
52205
|
-
|
|
52206
|
-
|
|
52207
|
-
static examples = ["<%= config.bin %> logout", "<%= config.bin %> logout --force"];
|
|
52208
|
-
static flags = {
|
|
52930
|
+
function commonStartFlags() {
|
|
52931
|
+
return {
|
|
52932
|
+
"accept-terms": Flags.boolean({ description: "Confirm acceptance of Primitive's Terms of Service and Privacy Policy" }),
|
|
52209
52933
|
"api-base-url": Flags.string({
|
|
52210
52934
|
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
52211
52935
|
env: "PRIMITIVE_API_BASE_URL",
|
|
52212
52936
|
hidden: true
|
|
52213
52937
|
}),
|
|
52938
|
+
"device-name": Flags.string({ description: "Device name used for the created CLI OAuth session" }),
|
|
52214
52939
|
force: Flags.boolean({
|
|
52215
52940
|
char: "f",
|
|
52216
|
-
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"
|
|
52217
52946
|
})
|
|
52218
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();
|
|
52219
52963
|
async run() {
|
|
52220
|
-
const { flags } = await this.parse(
|
|
52221
|
-
if (flags.force) {
|
|
52222
|
-
runForceLogout({ configDir: this.config.configDir });
|
|
52223
|
-
return;
|
|
52224
|
-
}
|
|
52964
|
+
const { args, flags } = await this.parse(SignupCommand);
|
|
52225
52965
|
let releaseCredentialsLock;
|
|
52226
52966
|
try {
|
|
52227
52967
|
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
52228
52968
|
} catch (error) {
|
|
52229
|
-
throw cliError$
|
|
52969
|
+
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
52230
52970
|
}
|
|
52231
52971
|
try {
|
|
52232
|
-
await
|
|
52972
|
+
await runSignupStartWithCredentialLock({
|
|
52233
52973
|
configDir: this.config.configDir,
|
|
52974
|
+
email: args.email,
|
|
52234
52975
|
flags
|
|
52235
52976
|
});
|
|
52236
52977
|
} finally {
|
|
@@ -52238,667 +52979,349 @@ var LogoutCommand = class LogoutCommand extends Command {
|
|
|
52238
52979
|
}
|
|
52239
52980
|
}
|
|
52240
52981
|
};
|
|
52241
|
-
|
|
52242
|
-
|
|
52243
|
-
|
|
52244
|
-
|
|
52245
|
-
|
|
52246
|
-
|
|
52247
|
-
|
|
52248
|
-
|
|
52249
|
-
|
|
52250
|
-
|
|
52251
|
-
* Read a user-owned text file without following it anywhere surprising. A
|
|
52252
|
-
* symlink is followed to its real file so a later write updates the file the
|
|
52253
|
-
* user linked instead of replacing the link.
|
|
52254
|
-
*/
|
|
52255
|
-
function readManagedFile(path) {
|
|
52256
|
-
let info;
|
|
52257
|
-
try {
|
|
52258
|
-
info = lstatSync(path);
|
|
52259
|
-
} catch (error) {
|
|
52260
|
-
if (error.code === "ENOENT") return {
|
|
52261
|
-
state: "absent",
|
|
52262
|
-
path,
|
|
52263
|
-
target: path
|
|
52264
|
-
};
|
|
52265
|
-
return {
|
|
52266
|
-
state: "invalid",
|
|
52267
|
-
path,
|
|
52268
|
-
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
52269
|
-
};
|
|
52270
|
-
}
|
|
52271
|
-
let target = path;
|
|
52272
|
-
if (info.isSymbolicLink()) try {
|
|
52273
|
-
target = realpathSync(path);
|
|
52274
|
-
info = statSync(target);
|
|
52275
|
-
} catch {
|
|
52276
|
-
return {
|
|
52277
|
-
state: "invalid",
|
|
52278
|
-
path,
|
|
52279
|
-
detail: `${path} is a symlink to a missing file; it was left unchanged.`
|
|
52280
|
-
};
|
|
52281
|
-
}
|
|
52282
|
-
if (!info.isFile()) return {
|
|
52283
|
-
state: "invalid",
|
|
52284
|
-
path,
|
|
52285
|
-
detail: `${path} is not a regular file; it was left unchanged.`
|
|
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."
|
|
52286
52992
|
};
|
|
52287
|
-
if (
|
|
52288
|
-
|
|
52289
|
-
|
|
52290
|
-
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(", ")}.`
|
|
52291
52996
|
};
|
|
52292
|
-
|
|
52293
|
-
|
|
52294
|
-
|
|
52295
|
-
}
|
|
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
|
+
};
|
|
52296
53007
|
return {
|
|
52297
|
-
|
|
52298
|
-
|
|
52299
|
-
detail: `${path} cannot be read (${error.code ?? "error"}).`
|
|
53008
|
+
kind: "ok",
|
|
53009
|
+
code: stripTrailingNewline(value)
|
|
52300
53010
|
};
|
|
52301
53011
|
}
|
|
52302
|
-
|
|
52303
|
-
|
|
52304
|
-
state: "invalid",
|
|
52305
|
-
path,
|
|
52306
|
-
detail: `${path} is not UTF-8 text; it was left unchanged.`
|
|
52307
|
-
};
|
|
52308
|
-
return {
|
|
52309
|
-
state: "present",
|
|
52310
|
-
path,
|
|
52311
|
-
target,
|
|
52312
|
-
text,
|
|
52313
|
-
mode: info.mode & 511
|
|
52314
|
-
};
|
|
52315
|
-
}
|
|
52316
|
-
function backupStamp$1(now) {
|
|
52317
|
-
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
52318
|
-
}
|
|
52319
|
-
function syncDirectory(directory) {
|
|
52320
|
-
if (process.platform === "win32") return;
|
|
52321
|
-
try {
|
|
52322
|
-
const fd = openSync(directory, "r");
|
|
52323
|
-
try {
|
|
52324
|
-
fsyncSync(fd);
|
|
52325
|
-
} finally {
|
|
52326
|
-
closeSync(fd);
|
|
52327
|
-
}
|
|
52328
|
-
} catch {}
|
|
52329
|
-
}
|
|
52330
|
-
function pruneBackups(target) {
|
|
52331
|
-
const directory = dirname(target);
|
|
52332
|
-
const prefix = `${basename(target)}${BACKUP_INFIX}`;
|
|
52333
|
-
try {
|
|
52334
|
-
const backups = readdirSync(directory).filter((name) => name.startsWith(prefix)).sort();
|
|
52335
|
-
for (const name of backups.slice(0, Math.max(0, backups.length - BACKUPS_KEPT))) unlinkSync(join(directory, name));
|
|
52336
|
-
} catch {}
|
|
52337
|
-
}
|
|
52338
|
-
/** Copy the current file beside itself before any change. Returns the copy's path. */
|
|
52339
|
-
function backupManagedFile(target, mode, now) {
|
|
52340
|
-
const base = `${target}${BACKUP_INFIX}${backupStamp$1(now)}`;
|
|
52341
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
52342
|
-
const candidate = attempt === 0 ? base : `${base}-${attempt}`;
|
|
52343
|
-
try {
|
|
52344
|
-
copyFileSync(target, candidate, constants.COPYFILE_EXCL);
|
|
52345
|
-
chmodSync(candidate, mode);
|
|
52346
|
-
return candidate;
|
|
52347
|
-
} catch (error) {
|
|
52348
|
-
if (error.code !== "EEXIST") throw error;
|
|
52349
|
-
}
|
|
52350
|
-
}
|
|
52351
|
-
throw new MachineFileError(`Could not create a backup of ${target}.`);
|
|
52352
|
-
}
|
|
52353
|
-
/**
|
|
52354
|
-
* Replace a file atomically (temporary file plus rename in the same
|
|
52355
|
-
* directory), after backing up the existing content. The write is refused when
|
|
52356
|
-
* the file changed since it was read, so a concurrent edit is never lost.
|
|
52357
|
-
*/
|
|
52358
|
-
function writeManagedFile(params) {
|
|
52359
|
-
const { read } = params;
|
|
52360
|
-
const target = read.target;
|
|
52361
|
-
const expected = read.state === "present" ? read.text : null;
|
|
52362
|
-
const directory = dirname(target);
|
|
52363
|
-
mkdirSync(directory, {
|
|
52364
|
-
recursive: true,
|
|
52365
|
-
mode: 448
|
|
52366
|
-
});
|
|
52367
|
-
const current = () => {
|
|
53012
|
+
if (input.fromFile !== void 0) {
|
|
53013
|
+
const readFile = input.readFile ?? defaultReadCodeFile;
|
|
52368
53014
|
try {
|
|
52369
|
-
return
|
|
53015
|
+
return {
|
|
53016
|
+
kind: "ok",
|
|
53017
|
+
code: stripTrailingNewline(readFile(input.fromFile))
|
|
53018
|
+
};
|
|
52370
53019
|
} catch (error) {
|
|
52371
|
-
|
|
52372
|
-
|
|
52373
|
-
|
|
52374
|
-
|
|
52375
|
-
|
|
52376
|
-
const mode = read.state === "present" ? read.mode : params.newFileMode ?? 420;
|
|
52377
|
-
const backup = read.state === "present" ? backupManagedFile(target, mode, (params.now ?? (() => /* @__PURE__ */ new Date()))()) : null;
|
|
52378
|
-
const temporary = join(directory, `.${basename(target)}.${randomUUID()}.tmp`);
|
|
52379
|
-
try {
|
|
52380
|
-
const fd = openSync(temporary, "wx", mode);
|
|
52381
|
-
try {
|
|
52382
|
-
writeFileSync(fd, params.content, "utf8");
|
|
52383
|
-
fsyncSync(fd);
|
|
52384
|
-
} finally {
|
|
52385
|
-
closeSync(fd);
|
|
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
|
+
};
|
|
52386
53025
|
}
|
|
52387
|
-
chmodSync(temporary, mode);
|
|
52388
|
-
if (current() !== expected) throw new MachineFileError(`${read.path} changed while it was being repaired; it was left unchanged.`);
|
|
52389
|
-
renameSync(temporary, target);
|
|
52390
|
-
syncDirectory(directory);
|
|
52391
|
-
} finally {
|
|
52392
|
-
if (existsSync(temporary)) unlinkSync(temporary);
|
|
52393
|
-
}
|
|
52394
|
-
if (backup) pruneBackups(target);
|
|
52395
|
-
return { backup };
|
|
52396
|
-
}
|
|
52397
|
-
const START = /^<!-- primitive:managed-block v=(\d+) START -->\r?$/;
|
|
52398
|
-
const END = /^<!-- primitive:managed-block END -->\r?$/;
|
|
52399
|
-
/** Character offsets of the single managed block, or why the markers are unusable. */
|
|
52400
|
-
function locateBlock(text) {
|
|
52401
|
-
const starts = [];
|
|
52402
|
-
const ends = [];
|
|
52403
|
-
let offset = 0;
|
|
52404
|
-
for (const line of text.split("\n")) {
|
|
52405
|
-
const start = START.exec(line);
|
|
52406
|
-
if (start) starts.push({
|
|
52407
|
-
offset,
|
|
52408
|
-
version: Number(start[1])
|
|
52409
|
-
});
|
|
52410
|
-
else if (END.test(line)) ends.push({
|
|
52411
|
-
offset,
|
|
52412
|
-
length: line.endsWith("\r") ? line.length - 1 : line.length
|
|
52413
|
-
});
|
|
52414
|
-
else if (line.includes("primitive:managed-block")) return { malformed: "a primitive:managed-block marker line was edited; fix or remove it by hand" };
|
|
52415
|
-
offset += line.length + 1;
|
|
52416
|
-
}
|
|
52417
|
-
if (starts.length === 0 && ends.length === 0) return null;
|
|
52418
|
-
if (starts.length !== 1 || ends.length !== 1) return { malformed: `found ${starts.length} START and ${ends.length} END markers; expected one of each` };
|
|
52419
|
-
const [start] = starts;
|
|
52420
|
-
const [end] = ends;
|
|
52421
|
-
if (!start || !end || end.offset < start.offset) return { malformed: "the END marker comes before the START marker" };
|
|
52422
|
-
return {
|
|
52423
|
-
start: start.offset,
|
|
52424
|
-
end: end.offset + end.length,
|
|
52425
|
-
version: start.version
|
|
52426
|
-
};
|
|
52427
|
-
}
|
|
52428
|
-
/** The exact block for a body, using the file's own line ending. */
|
|
52429
|
-
function renderManagedBlock(body, eol = "\n") {
|
|
52430
|
-
return [
|
|
52431
|
-
`<!-- primitive:managed-block v=1 START -->`,
|
|
52432
|
-
...body.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n"),
|
|
52433
|
-
"<!-- primitive:managed-block END -->"
|
|
52434
|
-
].join(eol);
|
|
52435
|
-
}
|
|
52436
|
-
function lineEnding(text) {
|
|
52437
|
-
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
52438
|
-
}
|
|
52439
|
-
function inspectManagedBlock(text, body) {
|
|
52440
|
-
const found = locateBlock(text);
|
|
52441
|
-
if (found === null) return { state: "missing" };
|
|
52442
|
-
if ("malformed" in found) return {
|
|
52443
|
-
state: "malformed",
|
|
52444
|
-
detail: found.malformed
|
|
52445
|
-
};
|
|
52446
|
-
return text.slice(found.start, found.end).replace(/\r\n/g, "\n") === renderManagedBlock(body) ? { state: "ok" } : {
|
|
52447
|
-
state: "outdated",
|
|
52448
|
-
version: found.version
|
|
52449
|
-
};
|
|
52450
|
-
}
|
|
52451
|
-
/**
|
|
52452
|
-
* Replace only the managed block, or append it when absent. Text outside the
|
|
52453
|
-
* markers is preserved byte for byte. Throws for malformed markers.
|
|
52454
|
-
*/
|
|
52455
|
-
function upsertManagedBlock(text, body) {
|
|
52456
|
-
const found = locateBlock(text);
|
|
52457
|
-
if (found !== null && "malformed" in found) throw new MachineFileError(found.malformed);
|
|
52458
|
-
const eol = lineEnding(text);
|
|
52459
|
-
const block = renderManagedBlock(body, eol);
|
|
52460
|
-
if (found) return `${text.slice(0, found.start)}${block}${text.slice(found.end)}`;
|
|
52461
|
-
if (text.length === 0) return `${block}${eol}`;
|
|
52462
|
-
return `${text}${text.endsWith(`${eol}${eol}`) ? "" : text.endsWith(eol) ? eol : `${eol}${eol}`}${block}${eol}`;
|
|
52463
|
-
}
|
|
52464
|
-
/** Indentation the file already uses, so a rewrite keeps its style. */
|
|
52465
|
-
function jsonIndentation(text) {
|
|
52466
|
-
return /^[{[][^\n]*\n([ \t]+)\S/.exec(text)?.[1] ?? 2;
|
|
52467
|
-
}
|
|
52468
|
-
//#endregion
|
|
52469
|
-
//#region src/oclif/claude-machine-hooks.ts
|
|
52470
|
-
function record$2(value) {
|
|
52471
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52472
|
-
}
|
|
52473
|
-
function realpathOrNull(path) {
|
|
52474
|
-
try {
|
|
52475
|
-
return realpathSync(path);
|
|
52476
|
-
} catch {
|
|
52477
|
-
return null;
|
|
52478
53026
|
}
|
|
52479
|
-
|
|
52480
|
-
/** Two paths name the same existing file. */
|
|
52481
|
-
function samePath(a, b) {
|
|
52482
|
-
if (typeof a !== "string" || !a) return false;
|
|
52483
|
-
if (a === b) return existsSync(a);
|
|
52484
|
-
const left = realpathOrNull(a);
|
|
52485
|
-
return left !== null && left === realpathOrNull(b);
|
|
52486
|
-
}
|
|
52487
|
-
function executable(path) {
|
|
53027
|
+
const readStdin = input.readStdin ?? defaultReadCodeStdin;
|
|
52488
53028
|
try {
|
|
52489
|
-
|
|
52490
|
-
|
|
52491
|
-
|
|
52492
|
-
|
|
52493
|
-
}
|
|
52494
|
-
|
|
52495
|
-
|
|
52496
|
-
|
|
52497
|
-
|
|
52498
|
-
if (!directory) continue;
|
|
52499
|
-
const candidate = join(directory, name);
|
|
52500
|
-
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
|
+
};
|
|
52501
53038
|
}
|
|
52502
|
-
return null;
|
|
52503
|
-
}
|
|
52504
|
-
/**
|
|
52505
|
-
* Prefer a PATH link (for example a package manager's `bin/node`) over a
|
|
52506
|
-
* versioned install directory when both are the same binary, so a hook keeps
|
|
52507
|
-
* working after the versioned directory is replaced by an upgrade.
|
|
52508
|
-
*/
|
|
52509
|
-
function stableNodePath(env, execPath = process.execPath) {
|
|
52510
|
-
const onPath = findOnPath("node", env);
|
|
52511
|
-
if (onPath && onPath !== execPath && samePath(onPath, execPath)) return onPath;
|
|
52512
|
-
return execPath;
|
|
52513
|
-
}
|
|
52514
|
-
/** Locate the running CLI. Null when its files are not where a package install puts them. */
|
|
52515
|
-
function currentCliLocation(entryArg, env, execPath = process.execPath) {
|
|
52516
|
-
const entry = realpathOrNull(entryArg);
|
|
52517
|
-
if (!entry) return null;
|
|
52518
|
-
const wake = realpathOrNull(join(dirname(entry), "claude-wake.mjs"));
|
|
52519
|
-
const pending = realpathOrNull(join(dirname(entry), "claude-pending-mail.mjs"));
|
|
52520
|
-
if (!wake || !pending) return null;
|
|
52521
|
-
return {
|
|
52522
|
-
node: stableNodePath(env, execPath),
|
|
52523
|
-
entry,
|
|
52524
|
-
wake,
|
|
52525
|
-
pending
|
|
52526
|
-
};
|
|
52527
53039
|
}
|
|
52528
|
-
|
|
52529
|
-
|
|
52530
|
-
if (/[\\/]_npx[\\/]/.test(entry)) return "an npx cache";
|
|
52531
|
-
if (/[\\/]dlx[-\\/]/.test(entry)) return "a pnpm dlx cache";
|
|
52532
|
-
if (/[\\/]bunx-/.test(entry)) return "a bunx cache";
|
|
52533
|
-
return null;
|
|
53040
|
+
function stripTrailingNewline(value) {
|
|
53041
|
+
return value.replace(/\r?\n$/, "");
|
|
52534
53042
|
}
|
|
52535
|
-
function
|
|
52536
|
-
return
|
|
53043
|
+
function defaultReadCodeFile(path) {
|
|
53044
|
+
return readFileSync(path, "utf8");
|
|
52537
53045
|
}
|
|
52538
|
-
function
|
|
52539
|
-
|
|
52540
|
-
|
|
52541
|
-
const line = /line (\d+)/.exec(message)?.[1];
|
|
52542
|
-
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");
|
|
52543
53049
|
}
|
|
52544
|
-
|
|
52545
|
-
|
|
52546
|
-
|
|
52547
|
-
|
|
52548
|
-
|
|
52549
|
-
|
|
52550
|
-
|
|
52551
|
-
|
|
52552
|
-
|
|
52553
|
-
|
|
52554
|
-
read,
|
|
52555
|
-
settings: {}
|
|
52556
|
-
};
|
|
52557
|
-
let parsed;
|
|
52558
|
-
try {
|
|
52559
|
-
parsed = JSON.parse(read.text);
|
|
52560
|
-
} catch (error) {
|
|
52561
|
-
return {
|
|
52562
|
-
ok: false,
|
|
52563
|
-
detail: `${path} is not valid JSON${jsonErrorDetail(error)}. Fix it by hand; it was not modified.`
|
|
52564
|
-
};
|
|
52565
|
-
}
|
|
52566
|
-
if (!record$2(parsed)) return {
|
|
52567
|
-
ok: false,
|
|
52568
|
-
detail: `${path} must contain a JSON object. Fix it by hand; it was not modified.`
|
|
52569
|
-
};
|
|
52570
|
-
if (parsed.hooks !== void 0 && !record$2(parsed.hooks)) return {
|
|
52571
|
-
ok: false,
|
|
52572
|
-
detail: `${path} has a "hooks" value that is not an object. Fix it by hand; it was not modified.`
|
|
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
|
+
})
|
|
52573
53060
|
};
|
|
52574
|
-
|
|
52575
|
-
|
|
52576
|
-
|
|
52577
|
-
|
|
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" })
|
|
52578
53084
|
};
|
|
52579
|
-
|
|
52580
|
-
const
|
|
52581
|
-
|
|
52582
|
-
|
|
52583
|
-
|
|
52584
|
-
|
|
52585
|
-
|
|
52586
|
-
|
|
52587
|
-
|
|
52588
|
-
|
|
52589
|
-
|
|
52590
|
-
|
|
52591
|
-
|
|
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
|
+
}
|
|
52592
53110
|
}
|
|
52593
|
-
|
|
52594
|
-
|
|
52595
|
-
|
|
52596
|
-
|
|
52597
|
-
|
|
52598
|
-
|
|
52599
|
-
|
|
52600
|
-
|
|
52601
|
-
|
|
52602
|
-
|
|
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
|
|
52603
53170
|
});
|
|
52604
|
-
} finally {
|
|
52605
|
-
release();
|
|
52606
53171
|
}
|
|
52607
|
-
}
|
|
52608
|
-
|
|
52609
|
-
"
|
|
52610
|
-
|
|
52611
|
-
|
|
52612
|
-
|
|
52613
|
-
|
|
52614
|
-
|
|
52615
|
-
|
|
52616
|
-
|
|
52617
|
-
|
|
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
|
+
}
|
|
52618
53194
|
}
|
|
52619
53195
|
};
|
|
52620
|
-
|
|
52621
|
-
|
|
52622
|
-
|
|
52623
|
-
|
|
52624
|
-
return args.length === 6 && typeof args[0] === "string" && args[1] === "agent" && args[2] === spec.subcommand && args[3] === "--runtime" && args[4] === "claude" && args[5] === "--hook";
|
|
53196
|
+
//#endregion
|
|
53197
|
+
//#region src/oclif/commands/logout.ts
|
|
53198
|
+
function cliError$1(message) {
|
|
53199
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
52625
53200
|
}
|
|
52626
|
-
function
|
|
52627
|
-
return
|
|
52628
|
-
cli.entry,
|
|
52629
|
-
"agent",
|
|
52630
|
-
spec.subcommand,
|
|
52631
|
-
"--runtime",
|
|
52632
|
-
"claude",
|
|
52633
|
-
"--hook"
|
|
52634
|
-
];
|
|
53201
|
+
function unwrapData(value) {
|
|
53202
|
+
return value?.data ?? null;
|
|
52635
53203
|
}
|
|
52636
|
-
function
|
|
52637
|
-
|
|
52638
|
-
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.";
|
|
52639
53206
|
}
|
|
52640
|
-
|
|
52641
|
-
|
|
52642
|
-
const
|
|
52643
|
-
|
|
52644
|
-
|
|
52645
|
-
|
|
52646
|
-
];
|
|
52647
|
-
/** A per-session receive hook written by `agent connect` or `agent enroll`, any version. */
|
|
52648
|
-
function parseSessionHook(hook) {
|
|
52649
|
-
if (!record$2(hook) || hook.type !== "command" || !Array.isArray(hook.args)) return null;
|
|
52650
|
-
const args = hook.args;
|
|
52651
|
-
if (!args.every((value) => typeof value === "string")) return null;
|
|
52652
|
-
const strings = args;
|
|
52653
|
-
const script = basename(strings[0] ?? "");
|
|
52654
|
-
if (script === "claude-pending-mail.mjs") return strings.length === 7 && strings[6] === PENDING_MARKER ? {
|
|
52655
|
-
kind: "pending",
|
|
52656
|
-
configDir: strings[2] ?? "",
|
|
52657
|
-
profile: strings[3] ?? null,
|
|
52658
|
-
session: strings[5] ?? null
|
|
52659
|
-
} : null;
|
|
52660
|
-
if (script !== "claude-wake.mjs") return null;
|
|
52661
|
-
if (strings.length === 7 && strings[6] === WAKE_MARKER) return {
|
|
52662
|
-
kind: "wake",
|
|
52663
|
-
configDir: strings[2] ?? "",
|
|
52664
|
-
profile: strings[3] ?? null,
|
|
52665
|
-
session: strings[5] ?? null
|
|
52666
|
-
};
|
|
52667
|
-
if (strings.length === 6 && strings[5] === WAKE_MARKER) return {
|
|
52668
|
-
kind: "wake",
|
|
52669
|
-
configDir: strings[2] ?? "",
|
|
52670
|
-
profile: strings[3] ?? null,
|
|
52671
|
-
session: null
|
|
52672
|
-
};
|
|
52673
|
-
if (strings.length === 4 && strings[3] === WAKE_MARKER) return {
|
|
52674
|
-
kind: "wake",
|
|
52675
|
-
configDir: strings[2] ?? "",
|
|
52676
|
-
profile: null,
|
|
52677
|
-
session: null
|
|
53207
|
+
async function runLogoutWithCredentialLock(params) {
|
|
53208
|
+
requireDefaultLoginProfile();
|
|
53209
|
+
const deps = {
|
|
53210
|
+
cliLogout,
|
|
53211
|
+
createAuthenticatedCliApiClient,
|
|
53212
|
+
...params.deps
|
|
52678
53213
|
};
|
|
52679
|
-
|
|
52680
|
-
}
|
|
52681
|
-
/** A saved connected profile still holds a credential. Unreadable counts as live. */
|
|
52682
|
-
function profileStillConnected(configDir, profile) {
|
|
53214
|
+
let credentials;
|
|
52683
53215
|
try {
|
|
52684
|
-
|
|
52685
|
-
} catch {
|
|
52686
|
-
|
|
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;
|
|
52687
53223
|
}
|
|
52688
|
-
|
|
52689
|
-
|
|
52690
|
-
|
|
52691
|
-
|
|
52692
|
-
|
|
52693
|
-
|
|
52694
|
-
|
|
52695
|
-
|
|
52696
|
-
|
|
52697
|
-
|
|
52698
|
-
|
|
52699
|
-
|
|
52700
|
-
for (const entry of entries) {
|
|
52701
|
-
if (!record$2(entry) || !Array.isArray(entry.hooks)) continue;
|
|
52702
|
-
for (const hook of entry.hooks) {
|
|
52703
|
-
const parsed = parseSessionHook(hook);
|
|
52704
|
-
if (!parsed || !record$2(hook)) continue;
|
|
52705
|
-
const pathsExist = existsSync(String(hook.command)) && existsSync(String(hook.args[0])) && existsSync(String(hook.args[1]));
|
|
52706
|
-
if (!(parsed.profile ? existsSync(parsed.configDir) && profileStillConnected(parsed.configDir, parsed.profile) : existsSync(parsed.configDir) && pathsExist)) {
|
|
52707
|
-
verdicts.set(hook, "stale");
|
|
52708
|
-
continue;
|
|
52709
|
-
}
|
|
52710
|
-
const key = [
|
|
52711
|
-
event,
|
|
52712
|
-
parsed.kind,
|
|
52713
|
-
resolve(parsed.configDir),
|
|
52714
|
-
parsed.profile ?? "",
|
|
52715
|
-
parsed.session?.toLowerCase() ?? ""
|
|
52716
|
-
].join("\0");
|
|
52717
|
-
if (seen.has(key)) {
|
|
52718
|
-
verdicts.set(hook, "duplicate");
|
|
52719
|
-
continue;
|
|
52720
|
-
}
|
|
52721
|
-
seen.add(key);
|
|
52722
|
-
verdicts.set(hook, sessionHookPathsCurrent(hook, parsed, cli) ? "keep" : "outdated");
|
|
52723
|
-
}
|
|
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;
|
|
52724
53236
|
}
|
|
53237
|
+
throw error;
|
|
52725
53238
|
}
|
|
52726
|
-
|
|
52727
|
-
|
|
52728
|
-
|
|
52729
|
-
|
|
52730
|
-
|
|
52731
|
-
|
|
52732
|
-
|
|
52733
|
-
|
|
52734
|
-
|
|
52735
|
-
|
|
52736
|
-
|
|
52737
|
-
|
|
52738
|
-
|
|
52739
|
-
|
|
52740
|
-
|
|
52741
|
-
if (entries === "invalid") {
|
|
52742
|
-
findings[id] = {
|
|
52743
|
-
status: "fail",
|
|
52744
|
-
detail: `hooks.${spec.event} is not a list. Fix settings.json by hand; it was not modified.`,
|
|
52745
|
-
fixable: false
|
|
52746
|
-
};
|
|
52747
|
-
continue;
|
|
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;
|
|
52748
53254
|
}
|
|
52749
|
-
|
|
52750
|
-
|
|
52751
|
-
if (!first) findings[id] = {
|
|
52752
|
-
status: "fail",
|
|
52753
|
-
detail: `No ${spec.event} hook runs \`primitive agent ${spec.subcommand}\`.${blockedNote}`,
|
|
52754
|
-
fixable: canFix
|
|
52755
|
-
};
|
|
52756
|
-
else if (managed.length > 1) findings[id] = {
|
|
52757
|
-
status: "fail",
|
|
52758
|
-
detail: `${managed.length} ${spec.event} hooks run \`primitive agent ${spec.subcommand}\`; expected exactly one.${blockedNote}`,
|
|
52759
|
-
fixable: canFix
|
|
52760
|
-
};
|
|
52761
|
-
else if (!context.cli || !globalHookCurrent(first, context.cli)) findings[id] = {
|
|
52762
|
-
status: "fail",
|
|
52763
|
-
detail: `The ${spec.event} hook runs ${String(first.args[0])}, which ${existsSync(String(first.args[0])) ? "is not this CLI" : "no longer exists"}.${blockedNote}`,
|
|
52764
|
-
fixable: canFix
|
|
52765
|
-
};
|
|
52766
|
-
else findings[id] = {
|
|
52767
|
-
status: "ok",
|
|
52768
|
-
detail: `${spec.event} runs \`primitive agent ${spec.subcommand}\` from ${context.cli.entry}.`,
|
|
52769
|
-
fixable: false
|
|
52770
|
-
};
|
|
53255
|
+
writeErrorWithHints(payload);
|
|
53256
|
+
throw cliError$1("Could not revoke the saved Primitive CLI OAuth grant.");
|
|
52771
53257
|
}
|
|
52772
|
-
const
|
|
52773
|
-
|
|
52774
|
-
|
|
52775
|
-
|
|
52776
|
-
|
|
52777
|
-
|
|
52778
|
-
|
|
52779
|
-
|
|
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;
|
|
52780
53280
|
}
|
|
52781
|
-
|
|
52782
|
-
keep: 0,
|
|
52783
|
-
stale: 0,
|
|
52784
|
-
duplicate: 0,
|
|
52785
|
-
outdated: 0
|
|
52786
|
-
};
|
|
52787
|
-
for (const verdict of judgeSessionHooks(hooks, context.cli).values()) counts[verdict]++;
|
|
52788
|
-
const problems = [
|
|
52789
|
-
counts.stale ? `${counts.stale} for disconnected or removed sessions` : "",
|
|
52790
|
-
counts.duplicate ? `${counts.duplicate} duplicates` : "",
|
|
52791
|
-
counts.outdated ? `${counts.outdated} pointing at an old CLI path` : ""
|
|
52792
|
-
].filter(Boolean);
|
|
52793
|
-
findings["claude.hook.stop"] = problems.length ? {
|
|
52794
|
-
status: "fail",
|
|
52795
|
-
detail: `Per-session receive hooks need cleanup: ${problems.join(", ")}.${blockedNote}`,
|
|
52796
|
-
fixable: canFix || counts.outdated === 0 && context.blocked === null
|
|
52797
|
-
} : {
|
|
52798
|
-
status: "ok",
|
|
52799
|
-
detail: counts.keep ? `${counts.keep} per-session receive hooks, each present once and current.` : "No per-session receive hooks; each registered session adds its own exact-session hooks.",
|
|
52800
|
-
fixable: false
|
|
52801
|
-
};
|
|
52802
|
-
return findings;
|
|
53281
|
+
process.stderr.write(`Removed ${formatList(removed)}. Backing OAuth grant was not revoked.\n`);
|
|
52803
53282
|
}
|
|
52804
|
-
function
|
|
52805
|
-
|
|
52806
|
-
return {
|
|
52807
|
-
|
|
52808
|
-
|
|
52809
|
-
|
|
52810
|
-
|
|
52811
|
-
|
|
52812
|
-
|
|
52813
|
-
|
|
52814
|
-
|
|
52815
|
-
|
|
52816
|
-
|
|
52817
|
-
|
|
52818
|
-
return kept.length ? [{
|
|
52819
|
-
...entry,
|
|
52820
|
-
hooks: kept
|
|
52821
|
-
}] : [];
|
|
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
|
|
52822
53297
|
}),
|
|
52823
|
-
|
|
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
|
+
})
|
|
52824
53302
|
};
|
|
52825
|
-
|
|
52826
|
-
|
|
52827
|
-
|
|
52828
|
-
|
|
52829
|
-
|
|
52830
|
-
*/
|
|
52831
|
-
function repairClaudeHooks(settings, context, checks) {
|
|
52832
|
-
const changed = /* @__PURE__ */ new Set();
|
|
52833
|
-
const hooks = record$2(settings.hooks) ? { ...settings.hooks } : {};
|
|
52834
|
-
const cli = context.blocked === null ? context.cli : null;
|
|
52835
|
-
for (const [id, spec] of Object.entries(GLOBAL_HOOKS)) {
|
|
52836
|
-
if (!checks.has(id) || !cli) continue;
|
|
52837
|
-
const entries = eventEntries(hooks, spec.event);
|
|
52838
|
-
if (entries === "invalid") continue;
|
|
52839
|
-
let placed = false;
|
|
52840
|
-
const result = rewriteEntries(entries, (hook) => {
|
|
52841
|
-
if (!isGlobalManaged(hook, spec)) return hook;
|
|
52842
|
-
if (placed) return void 0;
|
|
52843
|
-
placed = true;
|
|
52844
|
-
if (globalHookCurrent(hook, cli)) return hook;
|
|
52845
|
-
return {
|
|
52846
|
-
...hook,
|
|
52847
|
-
command: cli.node,
|
|
52848
|
-
args: globalHookArgs(cli, spec)
|
|
52849
|
-
};
|
|
52850
|
-
});
|
|
52851
|
-
let next = result.entries;
|
|
52852
|
-
if (!placed) next = [...next, { hooks: [{
|
|
52853
|
-
type: "command",
|
|
52854
|
-
command: cli.node,
|
|
52855
|
-
args: globalHookArgs(cli, spec),
|
|
52856
|
-
timeout: spec.timeout
|
|
52857
|
-
}] }];
|
|
52858
|
-
if (result.changed || !placed) {
|
|
52859
|
-
hooks[spec.event] = next;
|
|
52860
|
-
changed.add(id);
|
|
53303
|
+
async run() {
|
|
53304
|
+
const { flags } = await this.parse(LogoutCommand);
|
|
53305
|
+
if (flags.force) {
|
|
53306
|
+
runForceLogout({ configDir: this.config.configDir });
|
|
53307
|
+
return;
|
|
52861
53308
|
}
|
|
52862
|
-
|
|
52863
|
-
|
|
52864
|
-
|
|
52865
|
-
|
|
52866
|
-
|
|
52867
|
-
|
|
52868
|
-
|
|
52869
|
-
|
|
52870
|
-
|
|
52871
|
-
|
|
52872
|
-
|
|
52873
|
-
|
|
52874
|
-
|
|
52875
|
-
args[0] = parsed?.kind === "pending" ? cli.pending : cli.wake;
|
|
52876
|
-
args[1] = cli.entry;
|
|
52877
|
-
return {
|
|
52878
|
-
...hook,
|
|
52879
|
-
command: cli.node,
|
|
52880
|
-
args
|
|
52881
|
-
};
|
|
52882
|
-
});
|
|
52883
|
-
if (result.changed) {
|
|
52884
|
-
hooks[event] = result.entries;
|
|
52885
|
-
changed.add("claude.hook.stop");
|
|
52886
|
-
}
|
|
52887
|
-
}
|
|
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();
|
|
52888
53322
|
}
|
|
52889
53323
|
}
|
|
52890
|
-
|
|
52891
|
-
settings,
|
|
52892
|
-
changed
|
|
52893
|
-
};
|
|
52894
|
-
return {
|
|
52895
|
-
settings: {
|
|
52896
|
-
...settings,
|
|
52897
|
-
hooks
|
|
52898
|
-
},
|
|
52899
|
-
changed
|
|
52900
|
-
};
|
|
52901
|
-
}
|
|
53324
|
+
};
|
|
52902
53325
|
//#endregion
|
|
52903
53326
|
//#region src/oclif/codex-machine-hooks.ts
|
|
52904
53327
|
function record$1(value) {
|
|
@@ -53496,7 +53919,10 @@ async function runMachineDoctor(options) {
|
|
|
53496
53919
|
let settingsRead = readClaudeSettings(paths.claudeDir);
|
|
53497
53920
|
const hookContext = {
|
|
53498
53921
|
cli,
|
|
53499
|
-
blocked: hookBlocked
|
|
53922
|
+
blocked: hookBlocked,
|
|
53923
|
+
get bound() {
|
|
53924
|
+
return boundSessionProfiles(options.configDir);
|
|
53925
|
+
}
|
|
53500
53926
|
};
|
|
53501
53927
|
const hookCheck = (id) => ({
|
|
53502
53928
|
inspect: () => {
|
|
@@ -60422,6 +60848,7 @@ const COMMANDS = {
|
|
|
60422
60848
|
completion: CompletionCommand,
|
|
60423
60849
|
"agent:upgrade": AgentUpgradeCommand,
|
|
60424
60850
|
"agent:connect": AgentConnectCommand,
|
|
60851
|
+
"agent:check-mail": AgentCheckMailCommand,
|
|
60425
60852
|
"agent:disconnect": AgentDisconnectCommand,
|
|
60426
60853
|
"agent:enroll": AgentEnrollCommand,
|
|
60427
60854
|
"agent:session-register": AgentSessionRegisterCommand,
|