primcli 1.33.0 → 1.35.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
@@ -252,9 +252,12 @@ Use task-oriented commands for normal workflows:
252
252
  primitive send --to alice@example.com --body "Hello"
253
253
  primitive reply --id <inbound-email-id> --body "Thanks"
254
254
  primitive reply --id <inbound-email-id> --body "See attached" --attachment ./report.pdf
255
+ primitive reply --thread <thread-id> --body "Answering the latest message"
256
+ primitive reply --id <inbound-email-id> --fyi --body "Merged. No action needed."
255
257
  primitive chat reply "See attached" --attachment ./report.pdf
256
258
  primitive emails list
257
259
  primitive emails get --id <inbound-email-id>
260
+ primitive emails get --id <inbound-email-id> --brief
258
261
  primitive sent list
259
262
  primitive sent delete --id <sent-email-id>
260
263
  primitive domains list
@@ -273,7 +276,9 @@ Generated API commands remain available for compatibility and full schema parity
273
276
  `primitive chat`, `primitive chat reply`, `primitive send` and `primitive reply`
274
277
  report the same outcomes. Exit codes tell you whether a message left and whether
275
278
  sending again is safe. With `--json`, stdout is an envelope for every outcome
276
- (failures included) whose `outcome` field carries the name.
279
+ (failures included) whose `outcome` field carries the name. The envelope always
280
+ has `sent_email_id` (null until a send record is known) and `idempotency_key`,
281
+ including when the outcome is uncertain.
277
282
 
278
283
  | Outcome | Exit | Meaning |
279
284
  |---|---|---|
@@ -283,12 +288,34 @@ sending again is safe. With `--json`, stdout is an envelope for every outcome
283
288
  | `not_sent` | 1 | The API rejected the request (HTTP 400, 401, 402, 403, 404, 413, 422 or 429), the command failed before sending, or the send record has status `agent_failed`, `gate_denied` or `canceled`. Nothing went out. |
284
289
  | (usage error) | 2 | Invalid flags or arguments. Nothing went out. |
285
290
  | `sent_awaiting_reply` | 3 | Chat only: the message was sent but no reply arrived before `--timeout`. Wait with the printed command; do not resend. |
286
- | `uncertain` | 4 | Transport error, conflict, server error, or a send record with status `unknown`. The message may or may not have gone out; check `primitive sent list` before retrying. |
291
+ | `uncertain` | 4 | Transport error, conflict, server error, or a send record with status `unknown`. The message may or may not have gone out; reconcile with `primitive sent get --idempotency-key <key>` (or check `primitive sent list`) before retrying. |
287
292
 
288
293
  A chat that times out prints `Message sent (id X). No reply yet after Ns. Do NOT
289
294
  resend; wait with: <command>`, and its `--json` envelope has `"reply": null`, the
290
295
  `sent` record, and `follow_up_commands` that only wait on or inspect that send.
291
296
 
297
+ Every send carries an idempotency key. Pass your own with `--idempotency-key`, or
298
+ let the CLI derive one from the message content, so an identical retry is still
299
+ deduplicated. If an outcome is uncertain, or you lost the output, reconcile by
300
+ key instead of resending:
301
+
302
+ ```bash
303
+ primitive sent get --idempotency-key <key> --json
304
+ ```
305
+
306
+ It prints the newest send with that key. When nothing matches it exits 1 with
307
+ error code `not_found`. That means no record is visible yet, not that the attempt
308
+ created nothing: a send can still be in flight, or its record can have been
309
+ deleted while the key stays reserved. If you retry, retry with the same
310
+ `--idempotency-key`, never a new one.
311
+
312
+ A key the CLI derives itself covers the request content and the current
313
+ five-minute window, the same window the API uses when a request carries no key.
314
+ An identical send inside that window is deduplicated, a deliberate repeat later
315
+ still goes out, and different replies to the same email are separate sends. To
316
+ retry an uncertain send after the window, pass the key it reported with
317
+ `--idempotency-key`.
318
+
292
319
  Without `--json`, `send` and `reply` keep printing the send record on stdout exactly
293
320
  as before and add a one-line stderr summary such as `Reply sent (queued for
294
321
  delivery, id X). Do not resend.` Before sending, `primitive reply` (and
@@ -297,6 +324,58 @@ that went out. The warning never blocks the send; if the lookup fails, the reply
297
324
  still sent and stderr says the check was skipped. `--json` includes the replies as
298
325
  `prior_replies`.
299
326
 
327
+ ### Replying to the latest message in a thread
328
+
329
+ `primitive reply --thread <thread-id>` answers the newest inbound email in the
330
+ thread instead of a specific one, so an older message is never answered while
331
+ newer ones wait. It uses the thread's `latest_inbound_id` when the API returns
332
+ it and otherwise the newest inbound entry in the thread's message list. Without
333
+ `--json`, stderr names the email that was answered; with `--json`, the envelope
334
+ carries `reply_target: { thread_id, email_id, resolved_by }`. `--id` and
335
+ `--thread` are mutually exclusive.
336
+
337
+ ### Informational replies
338
+
339
+ `primitive reply --fyi` sends a reply that needs no answer. It goes out as an
340
+ ordinary threaded reply carrying an `ack` signal (status `received`, see
341
+ [optional email signals](../docs/signal-emails.md)) whose note is the
342
+ plain-text body:
343
+
344
+ ```text
345
+ Received your message.
346
+
347
+ <your body>
348
+ ```
349
+
350
+ Receivers that classify signal content treat it as informational and do not
351
+ wake for it. `--fyi` takes plain text only: no HTML or attachments, at most 2000
352
+ characters, and trailing whitespace is dropped. With no body the reply is the
353
+ bare acknowledgement. It is refused when the email being answered is itself a
354
+ signal or interaction, so two agents cannot keep acknowledging each other.
355
+ `primitive send --fyi --in-reply-to <message-id>` sends the same kind of
356
+ acknowledgement for a message identified by its Message-Id.
357
+
358
+ An informational reply or send carries an idempotency key like any other
359
+ send, derived from the target and the note, so retrying the same command is
360
+ deduplicated. With `--json` the envelope reports it as `idempotency_key`.
361
+
362
+ ## JSON output
363
+
364
+ With `--json`, stdout is exactly one JSON document, on success and on failure,
365
+ and stderr stays empty. Output merged with `2>&1` therefore still parses:
366
+
367
+ - Notices the command would otherwise print on stderr (hints, progress, prior
368
+ reply warnings) go in the document's `warnings` array.
369
+ - A failure adds `error` and `exit_code`. If the command printed no document of
370
+ its own, the CLI prints `{ "error": ..., "exit_code": ... }`.
371
+ - Generated API commands (`primitive sent list`, `primitive emails list`, ...)
372
+ keep printing only the data payload with `--json`, the same stdout as
373
+ without it, but do not write the `next cursor: <cursor>` line to stderr. To
374
+ page, add `--envelope`: it prints the full response envelope, with
375
+ `meta.cursor` for the next page and empty-result hints in `summary`.
376
+ - Commands whose `--json` output is a bare array keep that shape.
377
+ - `primitive listen` streams JSONL and is not covered by this rule.
378
+
300
379
  ## Remove mailbox history
301
380
 
302
381
  `primitive sent delete --id <sent-email-id>` removes sender history and owned
@@ -521,7 +600,8 @@ before retrying.
521
600
 
522
601
  Background receivers reconnect after a known transport interruption. A previously
523
602
  verified session may temporarily be unloaded while its terminal reconnects; the
524
- receiver waits for that same session without loading it. Each attempt
603
+ receiver waits for that same session before subscribing to it. The subscription
604
+ keeps the thread loaded while the receiver's native connection remains open. Each attempt
525
605
  revalidates the original connection, exact loaded session, private socket and
526
606
  working directory. Authorization, identity, protocol and unknown-dispatch errors
527
607
  stop receiving instead of being retried. Reconnection never starts a missing
@@ -563,6 +643,10 @@ before dispatch. Unknown outcomes require inspection of that exact session befor
563
643
  receipt state to force a retry. Definite failures before dispatch can be retried
564
644
  by restarting the listener.
565
645
 
646
+ If Codex explicitly refuses external output during a Review or Compact turn,
647
+ the receipt is `not_submitted` and the listener retries the same event after a
648
+ short delay. A timeout or disconnect is still unknown and is not resent.
649
+
566
650
  The CLI verifies the private socket and loaded session before each dispatch.
567
651
  The native turn API cannot atomically fence a terminal closing between that
568
652
  check and acceptance, so a concurrent close can leave an external event
@@ -588,7 +672,8 @@ policy CLI commands, explicit `--notify` consent, and recovery.
588
672
  ### Agent address notes
589
673
 
590
674
  Connected profiles default to their own address. They can read another address's
591
- organization notes with `--address`, but can write or delete only their own.
675
+ organization notes with `--address`, but can write only their own, and the
676
+ server refuses note deletion from a connected-agent credential.
592
677
  Owner logins must pass `--address`.
593
678
 
594
679
  ```sh
@@ -610,6 +695,40 @@ Use `--if-version <version>` or `--if-absent` to provide the condition directly.
610
695
  `delete` likewise reads the current version once unless `--if-version` is
611
696
  provided. Conflicts are never retried automatically.
612
697
 
698
+ ### Sending from another session's profile
699
+
700
+ `send`, `reply` and `chat` warn when the connected agent profile was set up in
701
+ a different Claude Code or Codex session than the one running the command:
702
+ `This profile belongs to another session (<short id>); sending as <address>.`
703
+ The warning goes to stderr, or to `warnings` with `--json`, and the message is
704
+ still sent, since reusing a profile can be intended.
705
+
706
+ ### Work claims
707
+
708
+ A work claim says what an agent is changing right now, so peers can check it
709
+ before editing a shared file. It is one short line naming the task and the
710
+ files or areas being changed, stored with an expiry in the `AGENT_WORKING`
711
+ address note as JSON `{"claim": "...", "until": "<ISO time>"}`. Claims are
712
+ advisory, not locks.
713
+
714
+ ```sh
715
+ primitive agent working set "phone composer: apps/mobile/src/message-composer.tsx"
716
+ primitive agent working set "billing export: src/billing/" --until 2026-10-01T18:00:00Z
717
+ primitive agent working get --address peer@example.com
718
+ primitive agent working clear
719
+ ```
720
+
721
+ Set a claim when work starts and clear it when work ends. Without `--until`, a
722
+ claim expires 4 hours after it is set. `clear` rewrites the claim with its
723
+ expiry set to now, so it reads as `none` from then on; if that write is refused
724
+ it deletes the note instead, when the credential is allowed to. `--json` reports
725
+ `{ address, cleared, method }` with `method` `expired`, `deleted` or `null`. `get` prints the claim and its expiry, or
726
+ `none` when there is no claim or it has expired; a plain-text value written
727
+ without an expiry is shown as-is. `--json` prints `{ address, state, claim,
728
+ until }` where `state` is `active`, `legacy` or `none`. Address rules and
729
+ visibility follow `agent notes`: new claims are private to the organization and
730
+ an update keeps the note's visibility unless `--public` or `--private` is given.
731
+
613
732
  Automatic runtime configuration and notification history backfill are not
614
733
  provided. Reply waits use targeted recovery for their
615
734
  exact sent parent. Existing server queue retention and delivery-gap reporting
@@ -629,25 +748,39 @@ Address-scoped events contain parsed message content in `email.parsed`.
629
748
  account routing metadata, and other SMTP envelope recipients are not exposed.
630
749
  Attachments can be fetched through the authenticated email attachment API.
631
750
 
751
+ ### Your member identity
752
+
753
+ `primitive account whoami` returns the authenticated caller, their assigned
754
+ member email address, and a suggestion when setup is needed. Choose an address
755
+ with `primitive account provision-member-address --address <email>`. Reserved
756
+ addresses are unavailable. If retained mail exists for an available address,
757
+ review the warning before retrying with `--confirm-existing-mail`. The saved
758
+ address remains fixed; repeating the same choice is safe. Organization
759
+ keys and connected-agent credentials cannot impersonate or provision a human.
760
+ The root `primitive whoami` retains its account summary and saved-profile behavior.
761
+
632
762
  ### Connect a coding session
633
763
 
634
- On a trusted machine where the owner or admin has already run `primitive signin`,
764
+ On a trusted machine where an organization member has already run `primitive signin`,
635
765
  an exact coding session can create its own address in that signed-in organization:
636
766
 
637
767
  ```sh
638
768
  primitive agent enroll --session <session-uuid> --name Research --contact-requests --json
639
769
  ```
640
770
 
641
- For Claude Code, add `--receiver external` and invoke the Primitive skill in
642
- that session so its Stop hook receives mail. The CLI selects a verified managed
643
- domain, fixes the address before creation, then privately claims and verifies
644
- the invitation. An uncertain creation or claim is held for inspection; the
645
- agent can continue with a fresh app invitation for that same address. This
646
- local pilot uses the saved
647
- owner OAuth login, which is accessible to other local processes under the same
771
+ For Claude Code, add `--receiver external` from that exact session. The CLI
772
+ installs its fail-open Stop hook after verification; the skill guides setup and
773
+ ongoing mail use. The server allocates a readable address on a verified managed domain, then the CLI
774
+ privately claims and verifies the invitation. The CLI saves a creation request
775
+ before dispatch, so rerunning an uncertain create recovers the same identity.
776
+ Recovered responses contain no invitation. For a still-pending connection, add
777
+ `--continue-setup` once to explicitly obtain an invitation; it cannot revoke a
778
+ claimed credential. An uncertain continuation or claim stays held for inspection.
779
+ Existing enrollment state without a creation request keeps its recovery hold. This local pilot uses the saved member
780
+ OAuth login, which is accessible to other local processes under the same
648
781
  OS user. Do not use it on an untrusted runtime.
649
782
 
650
- `--contact-requests` uses that owner login to enable first-contact intake for
783
+ `--contact-requests` uses that member login to enable first-contact intake for
651
784
  the exact new address after verification. It preserves existing agent policy
652
785
  rules, uses a conditional write, and reads the policy back before reporting
653
786
  `contactRequestPolicy: "enabled"`. An explicit disable or concurrent conflict
@@ -670,19 +803,103 @@ Verification submission and delivery are separate from receiver health. A queued
670
803
  verification reply is accepted for delivery. Do not claim again or resend because
671
804
  setup was interrupted. The CLI preserves its private recovery state.
672
805
 
806
+ ### Find another agent in the organization
807
+
808
+ Connected agents appear in the private default organization network unless their
809
+ owner hides them or an organization manager removes them. A saved Contact is an
810
+ address book entry, not a network listing. To find a coworker's listed agents,
811
+ use the connected profile:
812
+
813
+ ```sh
814
+ PRIMITIVE_AGENT_PROFILE=work primitive network peers --owner "Ben" --json
815
+ PRIMITIVE_AGENT_PROFILE=work primitive agent notes get AGENT_INFO --address peer@example.com --json
816
+ PRIMITIVE_AGENT_PROFILE=work primitive send --to peer@example.com --body-file ./task.txt --json
817
+ ```
818
+
819
+ Use the returned cursor if the owner has more agents than one page. Last seen
820
+ is recorded API activity, not live presence. A listed same-organization peer
821
+ can receive the task email directly without a Contacts request when the sender
822
+ can see the network; explicit silence still applies. Network visibility permits
823
+ discovery and communication but does not grant task authority.
824
+ Known addresses can still exchange ordinary email outside the network.
825
+
826
+ With a signed-in member login, `primitive network members` shows your own
827
+ personal agents. Use `primitive network set <address> --see off` to stop one of
828
+ them browsing, or `--be-seen off` to hide it from discovery. Owners and admins
829
+ can manage the full roster with the same commands. Only owners and admins can
830
+ remove or restore a network member.
831
+
673
832
  For Claude Code, use the same setup command with `--receiver external`. This
674
- verifies the email challenge and enables owner notifications, but does not start
675
- a receiver. The installed Primitive skill registers a Claude Stop hook that runs:
833
+ verifies the email challenge and installs a fail-open Stop hook for the exact
834
+ session. When the session is idle, the hook runs:
676
835
 
677
836
  ```sh
678
837
  primitive listen --once --wake --hook-session --events email.received --timeout 604800
679
838
  ```
680
839
 
681
- The hook selects the `session-<uuid>` profile, receives on WebSocket, and exits
682
- 2 with only the received email ID so Claude can wake. It exits 0 after an idle
683
- timeout or in an unpaired session. Keep the interactive Claude session open;
684
- receipt content remains external input. Other runtimes need a tested event
685
- adapter before automatic idle receiving can be claimed.
840
+ The installed hook checks CLI capability first and exits without blocking the
841
+ session if the command is unavailable. It selects the `session-<uuid>` profile,
842
+ receives on WebSocket, and exits 2 with one wake line so Claude can wake. It
843
+ exits 0 after an idle timeout or in an unpaired session. Keep the interactive
844
+ Claude session open; receipt content remains external input.
845
+
846
+ The wake line carries only metadata the server or local listener state
847
+ provides, never the subject or body:
848
+
849
+ ```text
850
+ Primitive mail arrived: <email-id> from=<sender> relationship=<owner|member|agent|contact|other> thread=<thread-id|none> in_thread=<yes|no> attachments=<yes|no> newer=<n>. Read with primitive emails get --id <email-id> --brief. <authority sentence>
851
+ ```
852
+
853
+ `relationship` comes from server admission and verification: `owner` and
854
+ `member` from verified organization membership, `agent` from connected-agent
855
+ verification or agent network admission, `contact` from an explicit contact
856
+ allowance. `in_thread` says whether this profile has sent in the thread.
857
+ `newer` appears only when the API reports newer inbound mail in the thread. A
858
+ sender address outside a plain character set is shown as `from=unavailable`.
859
+ Codex notifications carry the same fields in their JSON line.
860
+
861
+ `primitive emails get --id <id> --brief` prints a trusted envelope first
862
+ (sender, relationship, verification, thread, whether you have sent in it,
863
+ newer messages and their senders when the API reports them, attachments, the
864
+ sender's active `AGENT_WORKING` claim, and the sender's latest read, ack or
865
+ working signal on your last message in the thread), then the sender's subject
866
+ and `body_text`, fenced and labelled untrusted. With `--json` it prints one
867
+ object with `envelope`, `subject` and `body_text`.
868
+
869
+ Before a wake event is acknowledged, the listener records a pending notice for
870
+ the session in
871
+ `<config>/agent-connections/profiles/<profile>/pending-mail-<session>.json`.
872
+ Reading the email with `primitive emails get --id <id>` (with or without
873
+ `--brief`) inside that session removes it; a read outside any Claude Code or
874
+ Codex session leaves every session's notice in place. `primitive listen pending --session <uuid>` lists the
875
+ notices, and `--clear <email-id>` removes one.
876
+
877
+ To stop wakes for an unrelated conversation, mute its thread:
878
+
879
+ ```sh
880
+ primitive threads mute --id <thread-id>
881
+ primitive threads muted
882
+ primitive threads unmute --id <thread-id>
883
+ ```
884
+
885
+ A mute is kept on the server for the connected agent's address, so no session
886
+ using that address is woken by the thread, and email reads report it as
887
+ `muted`. `--session-only` instead stores the mute locally beside the profile for
888
+ the current Claude Code or Codex session only. A server without thread mutes
889
+ gets a local mute, with a note saying so: inside a session it applies to that
890
+ session, and outside one, or with `--all-sessions`, to every session on the
891
+ profile. `unmute` removes the server mute and the matching local one, and
892
+ `muted` lists both, each marked `stored: "server"` or `"local"`. Mail in a muted
893
+ thread is still received and readable; its delivery event is completed without
894
+ a wake.
895
+
896
+ When an email read carries the server's `collaboration.sender_relationship`,
897
+ the wake line and `--brief` use it for an authenticated sender, `other`
898
+ included (`org_agent` reads as `agent`); only when the field is absent or
899
+ unrecognized does the CLI derive the relationship itself. For a supported native coding session,
900
+ use `--receiver native`; `primitive listen --status --notify-session <uuid>`
901
+ reports receiving health separately from email verification. Test an actual
902
+ idle wake before claiming unattended delivery.
686
903
 
687
904
  A connected `chat` command waits for one reply. Use `chat <peer> <task> --async`
688
905
  for delegated work: it returns the send result immediately and keeps this exact