primcli 1.39.0 → 1.40.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 CHANGED
@@ -862,6 +862,25 @@ choices when those options are omitted, and refuses only an option that
862
862
  conflicts with them, naming it. Select the saved profile for later commands with the result's
863
863
  `selectProfile`.
864
864
 
865
+ A runtime with no local session ID or hooks, such as a cloud-hosted session
866
+ whose commands run in a separate sandbox, connects with the same command and no
867
+ `--session` (an empty `--session` is treated the same way). It claims and
868
+ verifies the invitation normally and selects `--receiver poll`: nothing is
869
+ installed, the profile defaults to `connection-<invitation hash prefix>`, and
870
+ the result's `receiving.checkCommand` checks for new mail. The agent runs it at
871
+ the start of each turn and after it sends:
872
+
873
+ ```sh
874
+ npx -y primitive@latest agent connect --name Research --info "Reviews pull requests" --json < private-invitation.txt
875
+ PRIMITIVE_AGENT_PROFILE=connection-0123456789ab primitive agent check-mail --json
876
+ ```
877
+
878
+ `agent check-mail` prints the email IDs, senders and threads that arrived since
879
+ the profile's previous check (never subjects or bodies), leaving out fyi
880
+ acknowledgements, muted threads, and the setup and presence mail from the
881
+ connection's control address. Its position advances only after the result is
882
+ printed, so an interrupted check repeats mail rather than losing it.
883
+
865
884
  Verification submission and delivery are separate from receiver health. A queued
866
885
  verification reply is accepted for delivery. Do not claim again or resend because
867
886
  setup was interrupted. The CLI preserves its private recovery state.
@@ -34983,6 +34983,139 @@ function isMemoryJsonValue(value, seen = /* @__PURE__ */ new Set()) {
34983
34983
  }
34984
34984
  }
34985
34985
  //#endregion
34986
+ //#region src/oclif/agent-mail-check.ts
34987
+ /**
34988
+ * One check for mail that arrived since this profile's previous check. It is
34989
+ * the receiver for runtimes that nothing can wake (poll receiving): the agent
34990
+ * runs it at the start of each turn and after it sends.
34991
+ *
34992
+ * It tails `GET /emails?since=<cursor>`, which returns mail newer than the
34993
+ * cursor oldest first, without mail marked fyi (acknowledgements) or in
34994
+ * threads this address muted. The first check starts before the address's
34995
+ * first email. The cursor is saved privately per profile, only after the
34996
+ * result was handed to `emit`, so a failed or interrupted check repeats mail
34997
+ * rather than losing it. Reads also show this address's receiver as live.
34998
+ */
34999
+ const CURSOR_FILE = "mail-check.json";
35000
+ var MailCheckApiError = class extends Error {
35001
+ constructor(payload) {
35002
+ super("The mail check request failed.");
35003
+ this.payload = payload;
35004
+ }
35005
+ };
35006
+ function readCursor(path) {
35007
+ try {
35008
+ const saved = readMailJson(path);
35009
+ if (saved && typeof saved === "object" && !Array.isArray(saved) && saved.version === 1) {
35010
+ const cursor = saved.cursor;
35011
+ if (typeof cursor === "string" && cursor.length > 0) return cursor;
35012
+ }
35013
+ } catch {}
35014
+ return null;
35015
+ }
35016
+ /** Setup challenges and presence probes come from the control address. */
35017
+ function isControlMail(email, identity) {
35018
+ return email.presence_control !== void 0 || email.sender.trim().toLowerCase() === identity.ownerAddress.trim().toLowerCase();
35019
+ }
35020
+ async function checkAgentMail(options) {
35021
+ const { identity } = options;
35022
+ const directory = agentProfileDirectory(options.configDir, identity.profileName);
35023
+ privateMailDirectory(directory, true);
35024
+ const release = acquireListenLock(directory, "mail-check");
35025
+ try {
35026
+ const path = join(directory, CURSOR_FILE);
35027
+ const saved = readCursor(path);
35028
+ let cursor = saved;
35029
+ const emails = [];
35030
+ let controlSkipped = 0;
35031
+ let more = false;
35032
+ const maxPages = options.maxPages ?? 5;
35033
+ for (let page = 0; page < maxPages; page++) {
35034
+ const query = {
35035
+ since: cursor ?? "start",
35036
+ limit: 100,
35037
+ exclude_fyi: "true",
35038
+ exclude_muted: "true"
35039
+ };
35040
+ const result = await listEmails({
35041
+ client: options.client.client,
35042
+ query,
35043
+ responseStyle: "fields"
35044
+ });
35045
+ if (result.error || !result.data) throw new MailCheckApiError(result.error);
35046
+ const rows = result.data.data ?? [];
35047
+ for (const row of rows) if (isControlMail(row, identity)) controlSkipped++;
35048
+ else emails.push({
35049
+ id: row.id,
35050
+ received_at: row.received_at,
35051
+ sender: row.sender,
35052
+ thread_id: row.thread_id ?? null
35053
+ });
35054
+ const next = result.data.meta?.cursor ?? null;
35055
+ if (rows.length === 0 || !next) break;
35056
+ cursor = next;
35057
+ more = page === maxPages - 1;
35058
+ }
35059
+ const output = {
35060
+ outcome: emails.length > 0 ? "mail" : "empty",
35061
+ emails,
35062
+ more,
35063
+ control_skipped: controlSkipped,
35064
+ read_command: `PRIMITIVE_AGENT_PROFILE=${identity.profileName} ${options.invocation ?? "primitive"} emails get --id <id> --brief`
35065
+ };
35066
+ options.emit(output);
35067
+ if (cursor && cursor !== saved) writeMailJson(path, {
35068
+ version: 1,
35069
+ cursor
35070
+ });
35071
+ return output;
35072
+ } finally {
35073
+ release();
35074
+ }
35075
+ }
35076
+ //#endregion
35077
+ //#region src/oclif/commands/agent-check-mail.ts
35078
+ /** npx runs the CLI from its cache; print follow-up commands the same way. */
35079
+ function invocation$2(entry) {
35080
+ return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
35081
+ }
35082
+ var AgentCheckMailCommand = class AgentCheckMailCommand extends Command {
35083
+ static summary = "Check a connected agent's mail since its last check";
35084
+ 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\`.
35085
+
35086
+ 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.
35087
+
35088
+ 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\`.`;
35089
+ static examples = [`${AGENT_PROFILE_ENV}=connection-0123456789ab <%= config.bin %> agent check-mail --json`];
35090
+ static flags = { json: Flags.boolean({ description: "Print JSON (already the default)" }) };
35091
+ async run() {
35092
+ await this.parse(AgentCheckMailCommand);
35093
+ 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 });
35094
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({ configDir: this.config.configDir });
35095
+ if (!auth.connectedAgent) this.error(`Select a connected agent profile with ${AGENT_PROFILE_ENV}=<profile>.`, { exit: 1 });
35096
+ try {
35097
+ await checkAgentMail({
35098
+ configDir: this.config.configDir,
35099
+ identity: auth.connectedAgent,
35100
+ client: apiClient,
35101
+ invocation: invocation$2(process.argv[1]),
35102
+ emit: (result) => this.log(JSON.stringify(result))
35103
+ });
35104
+ } catch (error) {
35105
+ if (!(error instanceof MailCheckApiError)) throw error;
35106
+ const payload = extractErrorPayload(error.payload);
35107
+ writeErrorWithHints(payload);
35108
+ surfaceUnauthorizedHint({
35109
+ auth,
35110
+ baseUrlOverridden,
35111
+ configDir: this.config.configDir,
35112
+ payload
35113
+ });
35114
+ process.exitCode = 1;
35115
+ }
35116
+ }
35117
+ };
35118
+ //#endregion
34986
35119
  //#region src/oclif/claude-wake-install.ts
34987
35120
  const HOOK_MARKER = "primitive-agent-wake-v1";
34988
35121
  const WAKE_EVENTS = ["Stop", "SessionStart"];
@@ -35242,8 +35375,15 @@ function agentConnectionStatus(configDir, profileName) {
35242
35375
  const setup = profile ? readMailJson(join(agentProfileDirectory(configDir, profileName), "setup.json")) : null;
35243
35376
  const saved = setup && typeof setup === "object" && !Array.isArray(setup) ? setup : null;
35244
35377
  const session = saved && typeof saved.session === "string" && SESSION_UUID$1.test(saved.session) ? saved.session : null;
35245
- const mode = saved && saved.receiverMode === "external" ? "external" : "native";
35246
- const receiving = profile && session && mode === "native" ? (() => {
35378
+ const mode = saved?.receiverMode === "external" || saved?.receiverMode === "poll" ? saved.receiverMode : "native";
35379
+ const receiving = profile && saved && mode === "poll" ? {
35380
+ mode,
35381
+ sessionId: session,
35382
+ state: "poll",
35383
+ reason: "agent_checks_mail",
35384
+ lastSuccessfulMailCheckAt: null,
35385
+ liveness: "unknown"
35386
+ } : profile && session && mode === "native" ? (() => {
35247
35387
  const scope = notificationScope(profile.api_base_url, profile.api_key);
35248
35388
  const listener = backgroundListenStatus({
35249
35389
  configDir,
@@ -36225,6 +36365,11 @@ function createNotificationContactPolicy(options) {
36225
36365
  const SUBJECT = "Connect your agent to Primitive";
36226
36366
  const MARKER = /\bprimitive-connection:([a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}):([1-9]\d*)\b/g;
36227
36367
  const fail = (message) => new AgentConnectionSetupError(message);
36368
+ const RECEIVER_MODES = [
36369
+ "native",
36370
+ "external",
36371
+ "poll"
36372
+ ];
36228
36373
  const RECEIPT_STATUSES = new Set([
36229
36374
  "queued",
36230
36375
  "submitted_to_agent",
@@ -36333,7 +36478,7 @@ function parseState(value) {
36333
36478
  "receipt"
36334
36479
  ];
36335
36480
  const row = mailObject(value, value && typeof value === "object" && Object.hasOwn(value, "receiverMode") ? [...keys, "receiverMode"] : keys);
36336
- if (row.version !== 1 || row.receiverMode !== void 0 && !["native", "external"].includes(String(row.receiverMode)) || typeof row.contactRequests !== "boolean" || ![
36481
+ if (row.version !== 1 || row.receiverMode !== void 0 && !RECEIVER_MODES.includes(row.receiverMode) || typeof row.contactRequests !== "boolean" || ![
36337
36482
  "waiting",
36338
36483
  "sending",
36339
36484
  "sent"
@@ -36348,7 +36493,7 @@ function parseState(value) {
36348
36493
  if (!/^[a-f0-9]{64}$/.test(invitationHash)) throw fail("Saved setup identity is invalid.");
36349
36494
  const state = {
36350
36495
  version: 1,
36351
- session: mailId(row.session),
36496
+ session: row.session === null && row.receiverMode === "poll" ? null : mailId(row.session),
36352
36497
  receiverMode: row.receiverMode ?? "native",
36353
36498
  invitationHash,
36354
36499
  since: mailTime(row.since),
@@ -36569,7 +36714,7 @@ function defaults$1() {
36569
36714
  */
36570
36715
  function refuseSetupConflicts(state, params) {
36571
36716
  const conflicts = [
36572
- ...state.session !== params.session ? [`--session (this profile is bound to session ${state.session})`] : [],
36717
+ ...(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
36718
  ...params.receiverMode !== void 0 && state.receiverMode !== params.receiverMode ? [`--receiver ${params.receiverMode} (saved: ${state.receiverMode})`] : [],
36574
36719
  ...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
36720
  ];
@@ -36578,7 +36723,7 @@ function refuseSetupConflicts(state, params) {
36578
36723
  /** One resumable operation owns setup plumbing; it never stores or replays the invitation. */
36579
36724
  async function setupAgent(params) {
36580
36725
  const profileName = agentProfileName(params.profileName);
36581
- if (!SESSION_UUID$1.test(params.session)) throw fail("--session requires the exact loaded session UUID.");
36726
+ if (params.session !== void 0 && !SESSION_UUID$1.test(params.session)) throw fail("--session requires the exact loaded session UUID.");
36582
36727
  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
36728
  const timeoutMs = params.timeoutMs ?? 3e4;
36584
36729
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 12e4) throw fail("Setup timeout must be between zero and two minutes.");
@@ -36595,7 +36740,9 @@ async function setupAgent(params) {
36595
36740
  saved = value === null ? null : parseState(value);
36596
36741
  } catch {}
36597
36742
  if (saved) refuseSetupConflicts(saved, params);
36598
- if ((params.receiverMode ?? (params.resume ? saved?.receiverMode : void 0) ?? "native") === "native") try {
36743
+ const receiverMode = params.receiverMode ?? (params.resume ? saved?.receiverMode : void 0);
36744
+ 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.");
36745
+ if ((receiverMode ?? "native") === "native" && params.session) try {
36599
36746
  await dependencies.preflight(params.session);
36600
36747
  } catch {
36601
36748
  throw fail("This exact session is not available for native receiving. No invitation was claimed. Open a supported session and retry setup.");
@@ -36616,7 +36763,7 @@ async function setupAgent(params) {
36616
36763
  if (!state) {
36617
36764
  state = {
36618
36765
  version: 1,
36619
- session: params.session,
36766
+ session: params.session ?? null,
36620
36767
  receiverMode: params.receiverMode ?? "native",
36621
36768
  invitationHash: hash,
36622
36769
  since: (/* @__PURE__ */ new Date(dependencies.now() - 15 * 6e4)).toISOString(),
@@ -36666,7 +36813,7 @@ async function setupAgent(params) {
36666
36813
  ].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
36814
  const result = (receiving, ownerNotifications) => ({
36668
36815
  identity: context.identity,
36669
- sessionId: params.session,
36816
+ sessionId: params.session ?? state?.session ?? null,
36670
36817
  verification: {
36671
36818
  state: confirmed && submittedState() === "reply_submitted" ? "verified" : submittedState(),
36672
36819
  ...confirmed ? { verifiedAt: confirmed.verifiedAt } : {},
@@ -36677,7 +36824,7 @@ async function setupAgent(params) {
36677
36824
  },
36678
36825
  receiving: { state: receiving },
36679
36826
  ...ownerNotifications ? { ownerNotifications } : {},
36680
- resumeCommand: `primitive agent connect --profile ${profileName} --session ${params.session}${state?.receiverMode === "external" ? " --receiver external" : ""} --resume${state?.contactRequests ? " --contact-requests" : ""} --json`,
36827
+ 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
36828
  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
36829
  });
36683
36830
  const key = `connection-verification-${state.invitationHash}`;
@@ -36711,14 +36858,14 @@ async function setupAgent(params) {
36711
36858
  }
36712
36859
  const ownerNotifications = await dependencies.enableOwner(context);
36713
36860
  let healthy = false;
36714
- if (state.receiverMode !== "external") try {
36715
- healthy = await dependencies.startListener(profileName, params.session, state.contactRequests, params.configDir);
36861
+ if ((state.receiverMode ?? "native") === "native" && state.session) try {
36862
+ healthy = await dependencies.startListener(profileName, state.session, state.contactRequests, params.configDir);
36716
36863
  } catch {}
36717
36864
  if (verificationTimeoutMs > 0 && submittedState() === "reply_submitted") {
36718
36865
  const check = await awaitVerification(context, dependencies, verificationTimeoutMs);
36719
36866
  if (check.state === "verified") confirmed = { verifiedAt: check.verifiedAt };
36720
36867
  }
36721
- return result(state.receiverMode === "external" ? "external_setup_required" : healthy ? "healthy" : "not_ready", ownerNotifications);
36868
+ return result(state.receiverMode === "external" ? "external_setup_required" : state.receiverMode === "poll" ? "poll" : healthy ? "healthy" : "not_ready", ownerNotifications);
36722
36869
  } finally {
36723
36870
  release();
36724
36871
  }
@@ -36814,7 +36961,8 @@ const AGENT_CONNECT_CAPABILITIES = [
36814
36961
  "claude_external_hooks",
36815
36962
  "bundled_skill_install",
36816
36963
  "agent_info_seed",
36817
- "resume"
36964
+ "resume",
36965
+ "poll_receiver"
36818
36966
  ];
36819
36967
  const AGENT_INFO_NOTE = "AGENT_INFO";
36820
36968
  const MAX_NAME_LENGTH = 80;
@@ -36837,7 +36985,7 @@ function savedSetup(configDir, profileName) {
36837
36985
  if (!saved || typeof saved !== "object" || Array.isArray(saved)) return null;
36838
36986
  const row = saved;
36839
36987
  return {
36840
- receiverMode: row.receiverMode === "external" ? "external" : "native",
36988
+ receiverMode: row.receiverMode === "external" || row.receiverMode === "poll" ? row.receiverMode : "native",
36841
36989
  contactRequests: row.contactRequests === true
36842
36990
  };
36843
36991
  } catch {
@@ -36861,6 +37009,20 @@ function clearPendingAgentInfo(path) {
36861
37009
  function defaultAgentProfileName(session) {
36862
37010
  return `session-${session.toLowerCase()}`;
36863
37011
  }
37012
+ /**
37013
+ * The profile for a setup that binds no session. It is derived from the
37014
+ * invitation, so running the same invitation again finds the same profile and
37015
+ * its saved claim instead of claiming twice. A one-way hash prefix reveals
37016
+ * nothing usable about the invitation.
37017
+ */
37018
+ function invitationProfileName(invitation) {
37019
+ const parsed = parseAgentInvitation(invitation);
37020
+ return `connection-${createHash("sha256").update(parsed.apiBaseUrl).update("\0").update(parsed.token).digest("hex").slice(0, 12)}`;
37021
+ }
37022
+ /** The command that checks a poll-receiving profile for new mail. */
37023
+ function pollCheckCommand(invocation, profileName) {
37024
+ return `PRIMITIVE_AGENT_PROFILE=${profileName} ${invocation} agent check-mail --json`;
37025
+ }
36864
37026
  /** A short private role note; never a transcript or secret. */
36865
37027
  function agentInfoValue(name, info) {
36866
37028
  const clean = (value, max, label) => {
@@ -36948,11 +37110,15 @@ async function runAgentConnect(options) {
36948
37110
  ...defaults(options),
36949
37111
  ...Object.fromEntries(Object.entries(options.dependencies ?? {}).filter(([, value]) => value !== void 0))
36950
37112
  };
36951
- if (!SESSION_UUID$1.test(options.session)) throw new AgentConnectionSetupError("--session requires the exact loaded session UUID.");
36952
- const runtime = detectAgentRuntime(options.session, env);
36953
- const profileName = agentProfileName(options.profileName ?? defaultAgentProfileName(options.session));
36954
- const receiver = options.receiver ?? (options.resume ? savedSetup(options.configDir, profileName)?.receiverMode ?? null : null) ?? (runtime === "claude" ? "external" : "native");
36955
- if (receiver === "external" && env.CLAUDE_CODE_SESSION_ID?.trim().toLowerCase() !== options.session.toLowerCase()) throw new AgentConnectionSetupError("External receiving requires this exact Claude session ID. No invitation was claimed.");
37113
+ const session = options.session?.trim() ? options.session : void 0;
37114
+ if (session !== void 0 && !SESSION_UUID$1.test(session)) throw new AgentConnectionSetupError("--session requires the exact loaded session UUID.");
37115
+ 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.`);
37116
+ if (session === void 0 && options.resume && !options.profileName) throw new AgentConnectionSetupError("Pass --profile to resume a setup that binds no session.");
37117
+ const runtime = session ? detectAgentRuntime(session, env) : null;
37118
+ const invitation = options.resume || session || options.profileName ? void 0 : await options.readInvitation();
37119
+ const profileName = agentProfileName(options.profileName ?? (session ? defaultAgentProfileName(session) : invitationProfileName(invitation ?? "")));
37120
+ const receiver = options.receiver ?? (options.resume ? savedSetup(options.configDir, profileName)?.receiverMode ?? null : null) ?? (!session ? "poll" : runtime === "claude" ? "external" : "native");
37121
+ 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
37122
  const agentInfo = agentInfoValue(options.name, options.info);
36957
37123
  const skipped = [];
36958
37124
  let skill;
@@ -36989,11 +37155,11 @@ async function runAgentConnect(options) {
36989
37155
  const result = await dependencies.setupAgent({
36990
37156
  configDir: options.configDir,
36991
37157
  profileName,
36992
- session: options.session,
37158
+ ...session ? { session } : {},
36993
37159
  receiverMode: receiver,
36994
37160
  resume: options.resume,
36995
37161
  contactRequests: options.contactRequests,
36996
- ...options.resume ? {} : { invitation: await options.readInvitation() }
37162
+ ...options.resume ? {} : { invitation: invitation ?? await options.readInvitation() }
36997
37163
  });
36998
37164
  const verified = verificationReplySubmitted(result.verification.state);
36999
37165
  const ownerMemberAddress = await dependencies.refreshOwnerMemberAddress(result.identity.profileName);
@@ -37007,7 +37173,7 @@ async function runAgentConnect(options) {
37007
37173
  configDir: options.configDir,
37008
37174
  profileName: result.identity.profileName,
37009
37175
  agentAddress: result.identity.agentAddress,
37010
- sessionId: options.session,
37176
+ sessionId: session ?? "",
37011
37177
  env
37012
37178
  });
37013
37179
  else skipped.push({
@@ -37053,22 +37219,27 @@ async function runAgentConnect(options) {
37053
37219
  reason: "already_present"
37054
37220
  });
37055
37221
  }
37222
+ const invocation = options.invocation ?? "primitive";
37056
37223
  const receiving = receiver === "external" ? {
37057
37224
  ...result.receiving,
37058
37225
  mode: "external",
37059
37226
  hook: externalHook
37227
+ } : receiver === "poll" ? {
37228
+ ...result.receiving,
37229
+ mode: "poll",
37230
+ checkCommand: pollCheckCommand(invocation, result.identity.profileName)
37060
37231
  } : {
37061
37232
  ...result.receiving,
37062
37233
  mode: "native",
37063
37234
  mailCheck: mailCheck?.state ?? "not_started",
37064
37235
  lastSuccessfulMailCheckAt: mailCheck?.lastSuccessfulMailCheckAt ?? null
37065
37236
  };
37066
- const receiverReady = receiver === "external" ? externalHook === "installed_unverified" : result.receiving.state === "healthy";
37237
+ const receiverReady = receiver === "external" ? externalHook === "installed_unverified" : receiver === "poll" || result.receiving.state === "healthy";
37067
37238
  const resumeCommand = [
37068
- options.invocation ?? "primitive",
37239
+ invocation,
37069
37240
  "agent connect",
37070
37241
  `--profile ${result.identity.profileName}`,
37071
- `--session ${options.session}`,
37242
+ ...session ? [`--session ${session}`] : [],
37072
37243
  `--receiver ${receiver}`,
37073
37244
  "--resume",
37074
37245
  ...options.contactRequests ?? savedSetup(options.configDir, profileName)?.contactRequests ? ["--contact-requests"] : [],
@@ -37083,7 +37254,7 @@ async function runAgentConnect(options) {
37083
37254
  ownerAddress: result.identity.ownerAddress,
37084
37255
  ownerMemberAddress,
37085
37256
  profile: result.identity.profileName,
37086
- sessionId: options.session,
37257
+ sessionId: session ?? null,
37087
37258
  runtime,
37088
37259
  cli: {
37089
37260
  version: options.cliVersion,
@@ -37105,15 +37276,16 @@ async function runAgentConnect(options) {
37105
37276
  //#endregion
37106
37277
  //#region src/oclif/commands/agent-connect.ts
37107
37278
  /** npx runs the CLI from its cache; resume through npx too so the command works without a global install. */
37108
- function invocation(entry) {
37279
+ function invocation$1(entry) {
37109
37280
  return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
37110
37281
  }
37111
37282
  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>. --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.";
37283
+ 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
37284
  static summary = "Connect and verify an agent address";
37114
37285
  static examples = [
37115
37286
  "<%= config.bin %> agent connect --session \"$CODEX_THREAD_ID\" --name Research --info \"Reviews pull requests\" --json < private-invitation.txt",
37116
37287
  "<%= config.bin %> agent connect --session \"$CLAUDE_CODE_SESSION_ID\" --contact-requests --json < private-invitation.txt",
37288
+ "<%= config.bin %> agent connect --receiver poll --name Research --json < private-invitation.txt",
37117
37289
  "<%= config.bin %> agent connect --profile work --session 11111111-1111-4111-8111-111111111111 --contact-requests --json < private-invitation.txt",
37118
37290
  "<%= config.bin %> agent connect --profile work --session 11111111-1111-4111-8111-111111111111 --contact-requests --resume --json",
37119
37291
  "<%= config.bin %> agent connect --profile work --session 11111111-1111-4111-8111-111111111111 --receiver external --contact-requests --json < private-invitation.txt",
@@ -37122,33 +37294,31 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
37122
37294
  "PRIMITIVE_AGENT_PROFILE=work <%= config.bin %> whoami --json"
37123
37295
  ];
37124
37296
  static flags = {
37125
- profile: Flags.string({ description: "Local agent profile name; defaults to session-<session> with --session. Existing OAuth login is preserved" }),
37297
+ 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
37298
  session: Flags.string({
37127
- description: "Exact current session UUID; verify email and configure receiving after claiming",
37299
+ 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
37300
  exclusive: ["status"]
37129
37301
  }),
37130
37302
  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. Defaults to external in Claude Code and native elsewhere",
37132
- options: ["native", "external"],
37133
- dependsOn: ["session"]
37303
+ 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",
37304
+ options: [...RECEIVER_MODES],
37305
+ exclusive: ["status"]
37134
37306
  }),
37135
37307
  resume: Flags.boolean({
37136
37308
  description: "Resume this session's saved setup without reading stdin, claiming again, or resending verification",
37137
- dependsOn: ["session"],
37138
37309
  exclusive: ["status"]
37139
37310
  }),
37140
37311
  "contact-requests": Flags.boolean({
37141
37312
  description: "Receive owner-enabled first contact requests in this session; grants no task authority",
37142
- dependsOn: ["session"],
37143
37313
  exclusive: ["status"]
37144
37314
  }),
37145
37315
  name: Flags.string({
37146
37316
  description: "Short agent name for a private AGENT_INFO note, written only when the note is absent",
37147
- dependsOn: ["session"]
37317
+ exclusive: ["status"]
37148
37318
  }),
37149
37319
  info: Flags.string({
37150
37320
  description: "Short role and capability description for a private AGENT_INFO note, written only when the note is absent",
37151
- dependsOn: ["session"]
37321
+ exclusive: ["status"]
37152
37322
  }),
37153
37323
  skill: Flags.boolean({
37154
37324
  description: "Install or refresh the primitive-connect skill bundled with this CLI for the detected runtime",
@@ -37157,13 +37327,14 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
37157
37327
  }),
37158
37328
  project: Flags.boolean({
37159
37329
  description: "Install the skill into this project (.claude/skills or .agents/skills) instead of the user skills folder",
37160
- dependsOn: ["session"]
37330
+ exclusive: ["status"]
37161
37331
  }),
37162
37332
  status: Flags.boolean({ description: "Read saved identity and local receiver health offline without reading stdin" }),
37163
37333
  json: Flags.boolean({ description: "Print setup status and identity metadata, never the credential" })
37164
37334
  };
37165
37335
  async run() {
37166
37336
  const { flags } = await this.parse(AgentConnectCommand);
37337
+ let pollProfile;
37167
37338
  try {
37168
37339
  if (flags.status) {
37169
37340
  if (!flags.profile) throw new AgentConnectionSetupError("Pass --profile with --status.");
@@ -37177,15 +37348,24 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
37177
37348
  }
37178
37349
  return;
37179
37350
  }
37180
- if (flags.session) {
37351
+ const session = flags.session?.trim() || void 0;
37352
+ 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)) {
37353
+ 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.`);
37354
+ let readInvitation = () => readAgentInvitation(process.stdin, process.stdin.isTTY);
37355
+ if (!session && !flags.resume) if (flags.profile) pollProfile = flags.profile;
37356
+ else {
37357
+ const text = await readInvitation();
37358
+ pollProfile = invitationProfileName(text);
37359
+ readInvitation = async () => text;
37360
+ }
37181
37361
  const output = await runAgentConnect({
37182
37362
  configDir: this.config.configDir,
37183
37363
  packageRoot: this.config.root,
37184
37364
  cliVersion: this.config.version,
37185
37365
  cliPath: process.argv[1] ?? "",
37186
- invocation: invocation(process.argv[1]),
37187
- session: flags.session,
37188
- profileName: flags.profile,
37366
+ invocation: invocation$1(process.argv[1]),
37367
+ session,
37368
+ profileName: flags.profile ?? pollProfile,
37189
37369
  receiver: flags.receiver,
37190
37370
  resume: flags.resume,
37191
37371
  contactRequests: flags["contact-requests"],
@@ -37193,21 +37373,23 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
37193
37373
  info: flags.info,
37194
37374
  skill: flags.skill,
37195
37375
  project: flags.project,
37196
- readInvitation: () => readAgentInvitation(process.stdin, process.stdin.isTTY)
37376
+ readInvitation
37197
37377
  });
37198
37378
  const external = output.receiving.mode === "external";
37379
+ const poll = output.receiving.mode === "poll";
37199
37380
  if (flags.json) this.log(JSON.stringify(output));
37200
37381
  else {
37201
37382
  this.log(`Agent ${output.address}: verification ${output.verification.state}; receiving ${output.receiving.state}; skill ${output.skill.state}.`);
37202
37383
  this.log(`Select it with ${output.selectProfile}.`);
37203
37384
  if (output.ownerNotifications === "silenced") this.log("Owner notifications remain silenced by the existing preference or policy.");
37204
37385
  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.");
37386
+ 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
37387
  if (output.status !== "connected") this.log(`Resume: ${output.resumeCommand}`);
37206
37388
  }
37207
- if (external && output.externalHook !== "installed_unverified" || !external && output.receiving.state !== "healthy" || !verificationReplySubmitted(output.verification.state)) process.exitCode = 2;
37389
+ if (external && output.externalHook !== "installed_unverified" || !external && !poll && output.receiving.state !== "healthy" || !verificationReplySubmitted(output.verification.state)) process.exitCode = 2;
37208
37390
  return;
37209
37391
  }
37210
- if (!flags.profile) throw new AgentConnectionSetupError("Pass --session to connect this session, or --profile for a claim-only or status check.");
37392
+ if (!flags.profile) throw new AgentConnectionSetupError("Pass --profile for claim-only.");
37211
37393
  const claimed = await connectAgent({
37212
37394
  configDir: this.config.configDir,
37213
37395
  profileName: flags.profile,
@@ -37224,7 +37406,7 @@ var AgentConnectCommand = class AgentConnectCommand extends Command {
37224
37406
  this.log(result.ownerReportGuidance);
37225
37407
  }
37226
37408
  } 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 });
37409
+ 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
37410
  }
37229
37411
  }
37230
37412
  };
@@ -37413,6 +37595,7 @@ function boundSession(directory, profile) {
37413
37595
  if (raw === null) return null;
37414
37596
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new AgentDisconnectError("Saved session setup is invalid. The connection was not changed.");
37415
37597
  const row = raw;
37598
+ if (row.receiverMode === "poll" && row.session === null && row.invitationHash === profile.invitation_hash) return null;
37416
37599
  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
37600
  return row.session.toLowerCase();
37418
37601
  }
@@ -37580,7 +37763,7 @@ function savedEnrollment(value) {
37580
37763
  "ownerAddress"
37581
37764
  ];
37582
37765
  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" || !["native", "external"].includes(String(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.");
37766
+ 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
37767
  connectedApiBaseUrl(row.apiBaseUrl);
37585
37768
  if (row.ownerAddress !== null) try {
37586
37769
  if (mailAddress(row.ownerAddress) !== row.ownerAddress) throw new Error();
@@ -37606,6 +37789,7 @@ function resolvedEnrollment(state) {
37606
37789
  };
37607
37790
  }
37608
37791
  async function preflight(session, receiver, env) {
37792
+ if (receiver === "poll") return;
37609
37793
  if (receiver === "external") {
37610
37794
  if (env.CLAUDE_CODE_SESSION_ID !== session) throw refuse("External receiving requires this exact Claude session ID. No address was created.");
37611
37795
  return;
@@ -38036,6 +38220,10 @@ async function enrollAgent(params) {
38036
38220
  }
38037
38221
  //#endregion
38038
38222
  //#region src/oclif/commands/agent-enroll.ts
38223
+ /** npx runs the CLI from its cache; print follow-up commands the same way. */
38224
+ function invocation(entry) {
38225
+ return entry && /[\\/]_npx[\\/]/.test(entry) ? "npx -y primitive@latest" : "primitive";
38226
+ }
38039
38227
  var AgentEnrollCommand = class AgentEnrollCommand extends Command {
38040
38228
  static summary = "Give this coding session an address in your organization";
38041
38229
  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 +38236,8 @@ var AgentEnrollCommand = class AgentEnrollCommand extends Command {
38048
38236
  name: Flags.string({ description: "Short agent display name; defaults to Coding agent" }),
38049
38237
  "continue-setup": Flags.boolean({ description: "Explicitly issue one invitation only if the recovered agent is still pending; never reconnect a claimed agent" }),
38050
38238
  receiver: Flags.string({
38051
- options: ["native", "external"],
38052
- description: "Native session receiver, or external runtime event hook"
38239
+ options: [...RECEIVER_MODES],
38240
+ 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
38241
  }),
38054
38242
  "contact-requests": Flags.boolean({ description: "Enable this agent's first-contact policy with the saved member login, then receive relevant requests" }),
38055
38243
  json: Flags.boolean({ description: "Print status without credentials or invitation" })
@@ -38072,7 +38260,15 @@ var AgentEnrollCommand = class AgentEnrollCommand extends Command {
38072
38260
  agentAddress: result.identity.agentAddress,
38073
38261
  sessionId: flags.session
38074
38262
  }) : null;
38075
- const output = {
38263
+ const output = result.receiving.state === "poll" ? {
38264
+ ...result,
38265
+ receiving: {
38266
+ ...result.receiving,
38267
+ mode: "poll",
38268
+ checkCommand: pollCheckCommand(invocation(process.argv[1]), result.identity.profileName)
38269
+ },
38270
+ externalHook
38271
+ } : {
38076
38272
  ...result,
38077
38273
  externalHook
38078
38274
  };
@@ -38086,9 +38282,10 @@ var AgentEnrollCommand = class AgentEnrollCommand extends Command {
38086
38282
  if (externalHook === "installed_unverified") this.log("External receive hook installed for this runtime. Idle wake still needs a live mail check.");
38087
38283
  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
38284
  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.");
38285
+ 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
38286
  else if (result.receiving.state !== "healthy") this.log("Pairing is confirmed; receiving needs separate setup or recovery.");
38090
38287
  }
38091
- if (externalHook === "unavailable" || flags["contact-requests"] && result.contactRequestPolicy !== "enabled" || result.connection.status !== "connected" || flags.receiver !== "external" && result.receiving.state !== "healthy") process.exitCode = 2;
38288
+ 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
38289
  } catch (error) {
38093
38290
  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
38291
  }
@@ -40386,10 +40583,10 @@ function contactRequestSessionKey(context, env = process.env) {
40386
40583
  }
40387
40584
  if (typeof raw !== "object" || Array.isArray(raw)) throw sessionRequired();
40388
40585
  const setup = raw;
40389
- const session = mailId(setup.session);
40390
40586
  const receiverMode = setup.receiverMode ?? "native";
40587
+ const poll = receiverMode === "poll" && setup.session === null;
40391
40588
  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) || ![
40589
+ 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
40590
  "queued",
40394
40591
  "submitted_to_agent",
40395
40592
  "delivered",
@@ -40397,6 +40594,8 @@ function contactRequestSessionKey(context, env = process.env) {
40397
40594
  "scheduled"
40398
40595
  ].includes(String(receipt.status))) throw sessionRequired();
40399
40596
  mailId(receipt.id);
40597
+ if (poll) return null;
40598
+ const session = mailId(setup.session);
40400
40599
  const key = `${receiverMode === "external" ? "claude" : "codex"}:${session}`;
40401
40600
  if (hasRuntime && runtime !== key || !hasRuntime && receiverMode !== "external") throw sessionRequired();
40402
40601
  return key;
@@ -45882,7 +46081,7 @@ const PRIMITIVE_TEAM_AUTHOR = {
45882
46081
  url: "https://primitive.dev"
45883
46082
  };
45884
46083
  const SDK_VERSION_RANGE = "^1.34.0";
45885
- const CLI_VERSION_RANGE = "^1.39.0";
46084
+ const CLI_VERSION_RANGE = "^1.40.0";
45886
46085
  const ESBUILD_VERSION_RANGE = "^0.27.0";
45887
46086
  function renderHandler() {
45888
46087
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -60422,6 +60621,7 @@ const COMMANDS = {
60422
60621
  completion: CompletionCommand,
60423
60622
  "agent:upgrade": AgentUpgradeCommand,
60424
60623
  "agent:connect": AgentConnectCommand,
60624
+ "agent:check-mail": AgentCheckMailCommand,
60425
60625
  "agent:disconnect": AgentDisconnectCommand,
60426
60626
  "agent:enroll": AgentEnrollCommand,
60427
60627
  "agent:session-register": AgentSessionRegisterCommand,
@@ -14,9 +14,9 @@ authorizes this connection, private credential storage, the verification reply
14
14
  and continued receiving; do not ask for that approval again.
15
15
 
16
16
  The command validates the invitation's API origin, claims it once, answers the
17
- verification email, starts receiving for this exact session, installs or
18
- refreshes this skill for your runtime from the CLI package, and prints one JSON
19
- result. Give it the owner's setup URL on stdin through a quoted heredoc, never
17
+ verification email, starts receiving for this exact session (or sets up
18
+ checking for mail when nothing can wake this session), installs or refreshes
19
+ this skill for your runtime from the CLI package, and prints one JSON result. Give it the owner's setup URL on stdin through a quoted heredoc, never
20
20
  as a command argument, file in a shared directory, log entry or note. Run it in
21
21
  your own non-interactive tool shell, which keeps no command history; never
22
22
  paste it into an interactive terminal.
@@ -37,6 +37,24 @@ npx -y primitive@latest agent connect --session "${CODEX_THREAD_ID:-$CODEX_SESSI
37
37
  INVITATION
38
38
  ```
39
39
 
40
+ Without a session to bind, omit `--session`:
41
+
42
+ ```sh
43
+ npx -y primitive@latest agent connect --json <<'INVITATION'
44
+ <the owner's setup URL>
45
+ INVITATION
46
+ ```
47
+
48
+ Use this form, without asking the owner, when this session cannot be woken from
49
+ the machine that runs your commands: your runtime's session variable above is
50
+ empty or unset, or the conversation runs somewhere other than that machine (a
51
+ cloud-hosted session whose tools run in a separate sandbox), so a hook or
52
+ listener installed there could never reach you. An instruction that passes an
53
+ empty session variable is treated the same way. The invitation is claimed and
54
+ verified as usual; the CLI installs nothing and you check for new mail
55
+ yourself. That is a complete connection, not a fallback that needs the owner's
56
+ choice. Never guess, borrow or invent a session ID to get automatic wake.
57
+
40
58
  Add `--contact-requests` only when the owner asked this address to receive
41
59
  first-contact requests from unknown senders. Add `--name "<short name>"` and
42
60
  `--info "<role and useful capabilities>"` to seed this address's private
@@ -52,7 +70,10 @@ Read the one JSON result:
52
70
  `ownerAddress` is a control address for setup and presence only.
53
71
  For Claude Code, `externalHook: "installed_unverified"` means the wake hooks
54
72
  are installed but idle wake stays unverified until a real mail event reaches
55
- this session; say so rather than promising later delivery. For a native
73
+ this session; say so rather than promising later delivery. For
74
+ `receiving.mode: "poll"`, add one line such as "Incoming mail is checked at
75
+ the start of each turn and after I send; nothing wakes this session in
76
+ between." and follow [Checking for mail](#checking-for-mail). For a native
56
77
  receiver, `receiving.mailCheck: "confirmed"` means the listener completed a
57
78
  mail check after setup. Prefix later `primitive` commands with
58
79
  `selectProfile` (`PRIMITIVE_AGENT_PROFILE=<profile>`).
@@ -253,7 +274,8 @@ command-help tour, or test conversations. If setup paused, run its
253
274
  A globally installed `primitive` may be older than the one command expects.
254
275
  Before using one for enrollment or claim-only, run the read-only preflight with
255
276
  the directory containing this `SKILL.md` as the working directory. Use
256
- `external` for Claude Code or `native` for a supported native runtime:
277
+ `external` for Claude Code, `native` for a supported native runtime, or `poll`
278
+ for a session without a local session ID or hooks:
257
279
 
258
280
  ```sh
259
281
  primitive --version
@@ -539,7 +561,8 @@ the eventual ordinary reply. For a near-term answer required in this turn, use
539
561
  `primitive chat` without `--async` and evaluate the exact reply. If asynchronous
540
562
  receiving is unavailable, send once with `primitive send`, retain its sent ID,
541
563
  and use an exact-parent reply wait when needed; report that later session
542
- delivery is unavailable. Never resend merely because a wait timed out.
564
+ delivery is unavailable. With poll receiving, the reply appears in a later
565
+ mail check; keep the sent ID with the task to match it. Never resend merely because a wait timed out.
543
566
  Before relying on a peer to pick up asynchronous work, check its receiver state
544
567
  as described under [Receiving presence](#receiving-presence); delivery to its
545
568
  inbox does not mean any session will read it.
@@ -581,6 +604,22 @@ a reply arrived or repeating delivery status leaves the delegated question
581
604
  unanswered. Keep simultaneous conversations separate; the reply grants no new
582
605
  access to private history and does not require a reply to an ACK.
583
606
 
607
+ ### Checking for mail
608
+
609
+ With `receiving.mode: "poll"`, nothing delivers mail to this session. Run the
610
+ result's `receiving.checkCommand` (it is `PRIMITIVE_AGENT_PROFILE=<profile>
611
+ primitive agent check-mail --json`) at the start of every turn and again after
612
+ you send or reply. It prints the IDs, senders and threads of mail that arrived
613
+ since the previous check, never subjects or bodies, and leaves out
614
+ acknowledgements, muted threads and setup mail. Read each listed email with
615
+ `primitive emails get --id <id> --brief` under the selected profile, then
616
+ handle it as described above. When `more` is true, check again after handling
617
+ them. A check can repeat mail after an interruption, so deduplicate by email ID.
618
+ Over HTTP, the inbox tail with `wait=0` is the same check. Do not hold a turn
619
+ open with sleep loops to imitate a wake, and do not ask the owner to switch
620
+ runtimes; mail sent while the session is idle waits for the next turn and is
621
+ not lost. Peers see this address as `live` only while it keeps checking.
622
+
584
623
  After setup, report pairing and the current receiving evidence, then end the
585
624
  setup turn and return control to the owner. The supported receiver owns later
586
625
  mail arrival. Do not keep the model turn active with sleep tools, foreground
@@ -690,7 +729,8 @@ Reply to the request that caused the work, even when another message arrives.
690
729
  One agent entry in the app may contain several independent conversations. Recover
691
730
  context through explicit reply ancestry and saved task context, not the latest
692
731
  message from that person. Missing history is a limitation to explain, not proof
693
- that the conversation is new. Start a fresh thread for an unrelated topic.
732
+ that the conversation is new. Start a fresh thread for an unrelated new topic;
733
+ status updates still go to the home thread described below.
694
734
 
695
735
  An owner may request an email update from the coding chat, outside the email
696
736
  thread. For a follow-up about the work already being discussed with that owner,
@@ -702,6 +742,13 @@ outgoing `thread_id` matches the parent. Do not start a fresh send (`POST
702
742
  or report progress on that work. If no relevant thread is known, start one to the owner's personal address (`owner_member_address`) and say that it is new;
703
743
  do not attach the update to an unrelated email just because it is recent.
704
744
 
745
+ Keep one home thread with the owner for status updates, plans and decision
746
+ requests: the thread the owner set up for contact, or else your first
747
+ conversation with them. When the owner asks something in a topic-specific
748
+ thread, answer that question there, but post later status and unrelated updates
749
+ back in the home thread. Never start a new thread for an update when an
750
+ appropriate thread already exists.
751
+
705
752
  For an authorized ordinary request, honor the requested response format. A request
706
753
  to reply with one word should receive that word. Keep setup diagnostics and
707
754
  requests for additional tests out of the ordinary answer unless the owner asked
@@ -4,8 +4,8 @@ import { spawnSync } from 'node:child_process';
4
4
  const receiver = process.argv.length === 4 && process.argv[2] === '--receiver'
5
5
  ? process.argv[3]
6
6
  : null;
7
- if (receiver !== 'native' && receiver !== 'external') {
8
- process.stderr.write('Usage: node scripts/check-cli-capabilities.mjs --receiver native|external\n');
7
+ if (receiver !== 'native' && receiver !== 'external' && receiver !== 'poll') {
8
+ process.stderr.write('Usage: node scripts/check-cli-capabilities.mjs --receiver native|external|poll\n');
9
9
  process.exitCode = 2;
10
10
  } else {
11
11
  const checks = [
@@ -14,7 +14,12 @@ if (receiver !== 'native' && receiver !== 'external') {
14
14
  { args: ['listen', '--help'], label: 'listen', patterns: [/external mail events at tool-output authority/i, /never synthetic user messages/i] },
15
15
  { args: ['network', 'peers', '--help'], label: 'network peers', patterns: [/discover listed peers/i, /--owner\b/] },
16
16
  ];
17
- if (receiver === 'external') {
17
+ if (receiver === 'poll') {
18
+ // Nothing is installed and no listener runs: the agent checks for mail.
19
+ checks[0].patterns.push(/poll installs nothing/i);
20
+ checks[1].patterns.push(/\bpoll\b/);
21
+ checks[2] = { args: ['agent', 'check-mail', '--help'], label: 'agent check-mail', patterns: [/since its previous check/i] };
22
+ } else if (receiver === 'external') {
18
23
  checks[0].patterns.push(/installs the exact Claude session's fail-open Stop hook/i, /resume SessionStart hook/i);
19
24
  checks[1].patterns.push(/exact Claude session, install a fail-open Stop hook/i, /resume SessionStart hook/i);
20
25
  checks[2].patterns.push(/--wake\b/, /--hook-session\b/);
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "primitive-connect",
3
- "version": "7d4b2d633a05bb12",
3
+ "version": "a48b2ea39305f258",
4
4
  "source": {
5
5
  "repository": "https://github.com/primitivedotdev/skills",
6
6
  "path": "skills/primitive-connect",
7
- "commit": "5d5d80f7bea82cd5018c0e25262aa32fa7cad708"
7
+ "commit": "47e1cb5b6ecef4294c4f4067962c7e6041a625d0"
8
8
  },
9
9
  "files": {
10
- "SKILL.md": "aca1e46fe388dc184f2eb3f3b5e39e7cec70b1bf4c490d0bd1013df51b2b79b4",
10
+ "SKILL.md": "6631dab8209a823b3790df20856146676deb505a2f7a52de87e953fd75388694",
11
11
  "package-lock.json": "443289373a7495883a9a3b57bab3bc471cd5dcb4989a9e67f797b35917c1acc5",
12
12
  "package.json": "8cc45b2c28ba415960c9b85f6c1449f15f9c10bac0b96922e7907b40d50e3dbf",
13
13
  "references/communication.md": "c931aada9553a8592088c28de5b272d5e0a15234aa9c9318ea7d1d5f69b39a79",
14
14
  "references/contact-requests.md": "7923bc855a56f11b088c73c73b7d9c5b04fef3f45eb758f28040d44e46a0d9c6",
15
15
  "references/native-session.md": "d6d529d87edc68a450be4c8b162488efa666c59fc23c35576fe6887235ff3d33",
16
16
  "references/private-api-fallback.md": "63b5d8c8ab765561fc6031298462469a8f56fd348ac14c5003e214f780e9329b",
17
- "scripts/check-cli-capabilities.mjs": "e6d4c045601f7d3b5cec103bdb3648723b8e66ebd22d87aca606fb78aed844fd",
17
+ "scripts/check-cli-capabilities.mjs": "9625ddaba1989a5f52a5077fdd0513620ab81be640bd83dd678708eefe019222",
18
18
  "scripts/connection.mjs": "2c96eda1e9a277a241555f98dd858523b8f77a9ddd82d52a6ac34e844206c3d6",
19
19
  "scripts/mail.mjs": "bfdfb60a739f1537d0e676dc5ff44fd5a65dba4303e3781dce8754c8d671cf51"
20
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primcli",
3
- "version": "1.39.0",
3
+ "version": "1.40.0",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,