papiers 0.1.5 → 0.1.7

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
@@ -2,8 +2,10 @@
2
2
 
3
3
  Search and read your Papiers library from the terminal.
4
4
 
5
- The CLI is read-only: it can inspect your saved documents, highlights, notes,
6
- and conversations, but it cannot change your library.
5
+ The CLI can inspect your saved documents, highlights, notes, and
6
+ conversations. It changes your library only to import a public PDF or webpage
7
+ (`papiers import --help`) or highlight a passage in a saved PDF or webpage
8
+ (`papiers highlight --help`), and only with a token that allows it.
7
9
 
8
10
  ## Install
9
11
 
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { realpathSync } from "fs";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
10
10
 
11
11
  // src/commands.ts
12
+ import { randomUUID as randomUUID2 } from "crypto";
12
13
  import { readFile, stat } from "fs/promises";
13
14
  import { isAbsolute, resolve as resolve2 } from "path";
14
15
 
@@ -412,6 +413,63 @@ function isNodeError(error) {
412
413
  return Boolean(error && typeof error === "object" && "code" in error);
413
414
  }
414
415
 
416
+ // ../../src/shared/papiers-public-ids.ts
417
+ var PAPIERS_PUBLIC_ID_PREFIX_BY_KIND = {
418
+ document: "doc",
419
+ highlight: "hl",
420
+ conversation: "cnv",
421
+ section: "sec",
422
+ figure: "fig",
423
+ table: "tbl",
424
+ note: "nte"
425
+ };
426
+ var PAPIERS_PUBLIC_ID_KIND_BY_PREFIX = Object.fromEntries(
427
+ Object.entries(PAPIERS_PUBLIC_ID_PREFIX_BY_KIND).map(([kind, prefix]) => [
428
+ prefix,
429
+ kind
430
+ ])
431
+ );
432
+ var PAPIERS_PUBLIC_ID_PATTERN = /^(doc|hl|cnv|sec|fig|tbl|nte)_([a-f0-9]{32})$/;
433
+ var PAPIERS_PUBLIC_REF_PATTERN = /^(doc_[a-f0-9]{32})#(?:p(0|[1-9][0-9]{0,9})|e([a-z0-9_]{1,64}))\.([a-f0-9]{8})$/;
434
+ var MAX_CHUNK_INDEX = 2147483647;
435
+ function parsePapiersPublicId(value) {
436
+ const id = value.trim();
437
+ const ref = id.match(PAPIERS_PUBLIC_REF_PATTERN);
438
+ if (ref) {
439
+ const [, documentId, chunkIndex, astNodeId, fingerprint] = ref;
440
+ if (chunkIndex !== void 0) {
441
+ const index = Number(chunkIndex);
442
+ if (index > MAX_CHUNK_INDEX) return null;
443
+ return {
444
+ id,
445
+ kind: "passage",
446
+ documentId,
447
+ chunkIndex: index,
448
+ fingerprint
449
+ };
450
+ }
451
+ return { id, kind: "equation", documentId, astNodeId, fingerprint };
452
+ }
453
+ const match = id.match(PAPIERS_PUBLIC_ID_PATTERN);
454
+ if (!match) {
455
+ return null;
456
+ }
457
+ const prefix = match[1];
458
+ const kind = PAPIERS_PUBLIC_ID_KIND_BY_PREFIX[prefix];
459
+ if (!kind) {
460
+ return null;
461
+ }
462
+ return {
463
+ id,
464
+ kind,
465
+ prefix,
466
+ opaqueId: match[2]
467
+ };
468
+ }
469
+ function staleReferenceMessage(kind) {
470
+ return kind === "passage" ? "Passage text has changed. Search or read the document again." : "Equation text has changed. Search or read the document again.";
471
+ }
472
+
415
473
  // ../../src/shared/papiers-resource-locators.ts
416
474
  var UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
417
475
  var PAPIERS_RESOURCE_LOCATOR_PATTERN_SOURCE = `papiers://(?:note|highlight|item|conversation|artifact)/${UUID_PATTERN}`;
@@ -533,54 +591,20 @@ var DOCUMENT_MAX_FILTER_VALUES = DOCUMENT_MAX_IDS;
533
591
  var DOCUMENT_DEFAULT_READ_CHARS_PER_ITEM = 5e4;
534
592
  var DOCUMENT_MAX_READ_CHARS_PER_ITEM = 2e5;
535
593
  var DOCUMENT_MAX_READ_CHARS_PER_REQUEST = 5e5;
536
- var DOCUMENT_PUBLIC_ID_PATTERN = /^doc_[a-f0-9]{32}$/u;
537
- var PASSAGE_PUBLIC_ID_PATTERN = /^pas_[a-f0-9]{32}$/u;
538
- var HIGHLIGHT_PUBLIC_ID_PATTERN = /^hl_[a-f0-9]{32}$/u;
539
- var CONVERSATION_PUBLIC_ID_PATTERN = /^cnv_[a-f0-9]{32}$/u;
540
- var EQUATION_PUBLIC_ID_PATTERN = /^eq_[a-f0-9]{32}$/u;
541
- var NOTE_PUBLIC_ID_PATTERN = /^nte_[a-f0-9]{32}$/u;
542
594
  var SEARCH_RANKING_ID_PATTERN = /^srch_[a-f0-9]{32}$/u;
543
595
  function isSearchRankingId(value) {
544
596
  return SEARCH_RANKING_ID_PATTERN.test(value);
545
597
  }
546
- function isDocumentPublicId(value) {
547
- return DOCUMENT_PUBLIC_ID_PATTERN.test(value);
548
- }
549
- function isPassagePublicId(value) {
550
- return PASSAGE_PUBLIC_ID_PATTERN.test(value);
551
- }
552
- function isHighlightPublicId(value) {
553
- return HIGHLIGHT_PUBLIC_ID_PATTERN.test(value);
554
- }
555
- function isConversationPublicId(value) {
556
- return CONVERSATION_PUBLIC_ID_PATTERN.test(value);
557
- }
558
- function isEquationPublicId(value) {
559
- return EQUATION_PUBLIC_ID_PATTERN.test(value);
560
- }
561
- function isNotePublicId(value) {
562
- return NOTE_PUBLIC_ID_PATTERN.test(value);
563
- }
598
+ var CLI_READABLE_KINDS = new Set(PUBLIC_KINDS);
564
599
  function publicReadKindForId(value) {
565
- if (isDocumentPublicId(value)) {
566
- return "document";
567
- }
568
- if (isPassagePublicId(value)) {
569
- return "passage";
570
- }
571
- if (isHighlightPublicId(value)) {
572
- return "highlight";
573
- }
574
- if (isConversationPublicId(value)) {
575
- return "conversation";
576
- }
577
- if (isEquationPublicId(value)) {
578
- return "equation";
579
- }
580
- if (isNotePublicId(value)) {
581
- return "note";
600
+ const parsed = parsePapiersPublicId(value);
601
+ if (!parsed || parsed.id !== value || !CLI_READABLE_KINDS.has(parsed.kind)) {
602
+ return null;
582
603
  }
583
- return null;
604
+ return parsed.kind;
605
+ }
606
+ function isDocumentPublicId(value) {
607
+ return publicReadKindForId(value) === "document";
584
608
  }
585
609
  function readKindForIdentifier(value) {
586
610
  const publicKind = publicReadKindForId(value);
@@ -607,8 +631,10 @@ var PAPIERS_SCHEMA_VERSIONS = {
607
631
  readRequest: "papiers.read.request.v1",
608
632
  read: "papiers.read.v1",
609
633
  capabilities: "papiers.capabilities.v1",
610
- tokenList: "papiers.tokens.list.response.v1",
611
- tokenRevoke: "papiers.tokens.revoke.response.v1",
634
+ importRequest: "papiers.import.request.v1",
635
+ import: "papiers.import.v1",
636
+ highlightRequest: "papiers.highlight.request.v1",
637
+ highlight: "papiers.highlight.v1",
612
638
  error: "papiers.error.v1"
613
639
  };
614
640
  var PAPIERS_PUBLIC_DATA_PLANE_SCOPES = [
@@ -665,10 +691,7 @@ var PAPIERS_SEARCH_MATCH_REASONS = [
665
691
  var PAPIERS_PUBLIC_NOT_READY_REASONS_BY_FEATURE = {
666
692
  document_content_read: ["explicit_enablement_required"],
667
693
  non_document_kinds: ["public_id_projection_not_ready"],
668
- content_search: [
669
- "content_search_not_ready",
670
- "public_passage_projection_not_ready"
671
- ],
694
+ content_search: ["content_search_not_ready"],
672
695
  list_cursors: ["cursor_signing_not_configured"],
673
696
  read_cursors: [
674
697
  "cursor_signing_not_configured",
@@ -703,6 +726,7 @@ var PAPIERS_PUBLIC_ERROR_CODES = [
703
726
  "invalid_request",
704
727
  "not_found",
705
728
  "source_superseded",
729
+ "stale_reference",
706
730
  "content_limit_exceeded",
707
731
  "request_limit_exceeded",
708
732
  "rate_limited",
@@ -799,9 +823,6 @@ function isTokenUuid(value) {
799
823
  }
800
824
 
801
825
  // src/response-validation.ts
802
- var PUBLIC_DATA_PLANE_SCOPE_SET = new Set(
803
- PAPIERS_PUBLIC_DATA_PLANE_SCOPES
804
- );
805
826
  var PUBLIC_ERROR_CODE_SET = new Set(PAPIERS_PUBLIC_ERROR_CODES);
806
827
  var SAFE_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
807
828
  var LIST_RESPONSE_KEYS = /* @__PURE__ */ new Set([
@@ -850,26 +871,6 @@ var READ_RESPONSE_KEYS = /* @__PURE__ */ new Set([
850
871
  "items",
851
872
  "usage"
852
873
  ]);
853
- var TOKEN_LIST_RESPONSE_KEYS = /* @__PURE__ */ new Set([
854
- "schemaVersion",
855
- "requestId",
856
- "tokens"
857
- ]);
858
- var TOKEN_REVOKE_RESPONSE_KEYS = /* @__PURE__ */ new Set([
859
- "schemaVersion",
860
- "requestId",
861
- "tokenId",
862
- "revoked"
863
- ]);
864
- var TOKEN_SUMMARY_KEYS = /* @__PURE__ */ new Set([
865
- "tokenId",
866
- "name",
867
- "scopes",
868
- "createdAt",
869
- "expiresAt",
870
- "revokedAt",
871
- "lastUsedAt"
872
- ]);
873
874
  var PAGE_KEYS = /* @__PURE__ */ new Set(["nextCursor", "returnedCount"]);
874
875
  var READ_USAGE_KEYS = /* @__PURE__ */ new Set(["returnedChars", "truncatedItemCount"]);
875
876
  var LIST_DOCUMENT_ITEM_KEYS = /* @__PURE__ */ new Set([
@@ -949,6 +950,7 @@ var READ_SUCCESS_PASSAGE_ITEM_KEYS = /* @__PURE__ */ new Set([
949
950
  "truncated",
950
951
  "charCount",
951
952
  "tokenEstimate",
953
+ "metadata",
952
954
  "warnings"
953
955
  ]);
954
956
  var READ_SUCCESS_HIGHLIGHT_ITEM_KEYS = /* @__PURE__ */ new Set([
@@ -1056,6 +1058,7 @@ var CONVERSATION_READ_NOT_FOUND_MESSAGE = "Conversation is not available.";
1056
1058
  var EQUATION_READ_NOT_FOUND_MESSAGE = "Equation is not available.";
1057
1059
  var NOTE_READ_NOT_FOUND_MESSAGE = "Note is not available.";
1058
1060
  var DOCUMENT_READ_SOURCE_SUPERSEDED_CODE = "source_superseded";
1061
+ var READ_STALE_REFERENCE_CODE = "stale_reference";
1059
1062
  var CONVERSATION_READ_CONTENT_LIMIT_CODE = "content_limit_exceeded";
1060
1063
  var CONVERSATION_READ_REQUEST_LIMIT_CODE = "request_limit_exceeded";
1061
1064
  var DOCUMENT_READ_SOURCE_SUPERSEDED_MESSAGE = "Document content is not available for the current source.";
@@ -1299,28 +1302,64 @@ function assertPapiersReadResponse(label, body, expectedIds, expected = "metadat
1299
1302
  function assertPapiersReadPipelineEnvelope(label, body) {
1300
1303
  validatePapiersReadResponse(label, body);
1301
1304
  }
1302
- function assertPapiersTokenListResponse(label, body) {
1303
- assertNoInternalResponseKeys(label, body);
1304
- const value = assertSchema(label, body, PAPIERS_SCHEMA_VERSIONS.tokenList);
1305
- rejectUnexpectedKeys(label, value, TOKEN_LIST_RESPONSE_KEYS);
1305
+ var IMPORT_RESPONSE_KEYS = /* @__PURE__ */ new Set([
1306
+ "schemaVersion",
1307
+ "requestId",
1308
+ "operationId",
1309
+ "status",
1310
+ "documentId",
1311
+ "error"
1312
+ ]);
1313
+ var IMPORT_ERROR_KEYS = new Set(
1314
+ Object.keys({
1315
+ code: true,
1316
+ message: true,
1317
+ retryable: true,
1318
+ details: true
1319
+ })
1320
+ );
1321
+ var IMPORT_ERROR_DETAIL_KEYS = /* @__PURE__ */ new Set(["importCode"]);
1322
+ function assertPapiersImportResponse(label, body) {
1323
+ const value = assertSchema(label, body, PAPIERS_SCHEMA_VERSIONS.import);
1324
+ rejectUnexpectedKeys(label, value, IMPORT_RESPONSE_KEYS);
1306
1325
  assertPublicRequestId(`${label} requestId`, value.requestId);
1307
- const tokens = assertArray(`${label} tokens`, value.tokens);
1308
- for (const [index, rawToken] of tokens.entries()) {
1309
- validateTokenSummary(`${label} token ${index}`, rawToken);
1326
+ assertNonEmptyString(`${label} operationId`, value.operationId);
1327
+ assertNonEmptyString(`${label} status`, value.status);
1328
+ if (value.documentId !== null) {
1329
+ assertDocumentReadIdentifier(`${label} documentId`, value.documentId);
1330
+ }
1331
+ if (value.error !== null) {
1332
+ const error = assertRecord(`${label} error`, value.error);
1333
+ rejectUnexpectedKeys(`${label} error`, error, IMPORT_ERROR_KEYS);
1334
+ assertPublicErrorCode(`${label} error.code`, error.code);
1335
+ assertNonEmptyString(`${label} error.message`, error.message);
1336
+ assertBoolean(`${label} error.retryable`, error.retryable);
1337
+ const details = assertRecord(`${label} error.details`, error.details);
1338
+ rejectUnexpectedKeys(
1339
+ `${label} error.details`,
1340
+ details,
1341
+ IMPORT_ERROR_DETAIL_KEYS
1342
+ );
1343
+ assertNonEmptyString(
1344
+ `${label} error.details.importCode`,
1345
+ details.importCode
1346
+ );
1310
1347
  }
1311
1348
  }
1312
- function assertPapiersTokenRevokeResponse(label, body, expectedTokenId) {
1313
- assertNoInternalResponseKeys(label, body);
1314
- const value = assertSchema(label, body, PAPIERS_SCHEMA_VERSIONS.tokenRevoke);
1315
- rejectUnexpectedKeys(label, value, TOKEN_REVOKE_RESPONSE_KEYS);
1349
+ var HIGHLIGHT_RESPONSE_KEYS = /* @__PURE__ */ new Set([
1350
+ "schemaVersion",
1351
+ "requestId",
1352
+ "highlightId"
1353
+ ]);
1354
+ function assertPapiersHighlightResponse(label, body) {
1355
+ const value = assertSchema(label, body, PAPIERS_SCHEMA_VERSIONS.highlight);
1356
+ rejectUnexpectedKeys(label, value, HIGHLIGHT_RESPONSE_KEYS);
1316
1357
  assertPublicRequestId(`${label} requestId`, value.requestId);
1317
- const tokenId = assertTokenId(`${label} tokenId`, value.tokenId);
1318
- if (tokenId !== expectedTokenId) {
1358
+ if (typeof value.highlightId !== "string" || !value.highlightId.startsWith("papiers://highlight/")) {
1319
1359
  throw new PapiersCliResponseValidationError(
1320
- `${label} did not echo the requested token id.`
1360
+ `${label} returned an invalid highlight identifier.`
1321
1361
  );
1322
1362
  }
1323
- assertBoolean(`${label} revoked`, value.revoked);
1324
1363
  }
1325
1364
  function assertPapiersErrorResponse(label, body, expectedHttpStatus) {
1326
1365
  assertNoInternalResponseKeys(label, body);
@@ -1438,28 +1477,6 @@ function validatePapiersReadResponse(label, body, options = {}) {
1438
1477
  );
1439
1478
  }
1440
1479
  }
1441
- function validateTokenSummary(label, rawToken) {
1442
- const token = assertRecord(label, rawToken);
1443
- rejectUnexpectedKeys(label, token, TOKEN_SUMMARY_KEYS);
1444
- assertTokenId(`${label} tokenId`, token.tokenId);
1445
- assertNonEmptyString(`${label} name`, token.name);
1446
- const scopes = assertArray(`${label} scopes`, token.scopes);
1447
- if (scopes.length === 0) {
1448
- throw new PapiersCliResponseValidationError(`${label} scopes was empty.`);
1449
- }
1450
- for (const [index, rawScope] of scopes.entries()) {
1451
- const scope = assertNonEmptyString(`${label} scope ${index}`, rawScope);
1452
- if (!PUBLIC_DATA_PLANE_SCOPE_SET.has(scope)) {
1453
- throw new PapiersCliResponseValidationError(
1454
- `${label} scope ${index} was not a public data-plane scope.`
1455
- );
1456
- }
1457
- }
1458
- assertTimestampString(`${label} createdAt`, token.createdAt);
1459
- validateOptionalTimestamp(`${label} expiresAt`, token.expiresAt);
1460
- validateOptionalTimestamp(`${label} revokedAt`, token.revokedAt);
1461
- validateOptionalTimestamp(`${label} lastUsedAt`, token.lastUsedAt);
1462
- }
1463
1480
  function validateSearchResults(label, results, options = {}) {
1464
1481
  const values = assertArray(`${label} results`, results);
1465
1482
  for (const [index, rawResult] of values.entries()) {
@@ -1807,7 +1824,7 @@ function validateOptionalListSearchMetadata(label, value, kind) {
1807
1824
  );
1808
1825
  }
1809
1826
  if (kind === "passage") {
1810
- return validatePassageListSearchMetadata(label, value);
1827
+ return validatePassageMetadata(label, value);
1811
1828
  }
1812
1829
  if (kind === "highlight") {
1813
1830
  return validateHighlightMetadata(label, value, assertDocId);
@@ -1821,7 +1838,7 @@ function validateOptionalListSearchMetadata(label, value, kind) {
1821
1838
  }
1822
1839
  return validateEquationReadMetadata(label, value);
1823
1840
  }
1824
- function validatePassageListSearchMetadata(label, value) {
1841
+ function validatePassageMetadata(label, value) {
1825
1842
  const metadata = assertRecord(label, value);
1826
1843
  rejectUnexpectedKeys(label, metadata, PASSAGE_METADATA_KEYS);
1827
1844
  const documentId = assertDocId(`${label}.documentId`, metadata.documentId);
@@ -1951,6 +1968,19 @@ function validatePassageReadSuccessItem(label, item, options) {
1951
1968
  const contentResult = validateReadContent(label, item, itemFormat, {
1952
1969
  expectedMaxCharsPerItem: options.expectedMaxCharsPerItem
1953
1970
  });
1971
+ const metadataRequested = options.expectedInclude?.has("metadata") ?? false;
1972
+ if (Object.prototype.hasOwnProperty.call(item, "metadata")) {
1973
+ if (!metadataRequested) {
1974
+ throw new PapiersCliResponseValidationError(
1975
+ `${label} returned metadata that was not requested.`
1976
+ );
1977
+ }
1978
+ validatePassageMetadata(`${label} metadata`, item.metadata);
1979
+ } else if (metadataRequested) {
1980
+ throw new PapiersCliResponseValidationError(
1981
+ `${label} did not return requested metadata.`
1982
+ );
1983
+ }
1954
1984
  const hasReadTruncatedWarning = validateOptionalReadWarnings(
1955
1985
  `${label} warnings`,
1956
1986
  item.warnings,
@@ -2383,7 +2413,8 @@ function validateReadItemError(label, value, kind, expectedFormat) {
2383
2413
  const sourceSupersededAllowed = error.code === DOCUMENT_READ_SOURCE_SUPERSEDED_CODE && (kind === "document" || kind === "passage" || kind === "note" || kind === "conversation");
2384
2414
  const contentLimitAllowed = error.code === CONVERSATION_READ_CONTENT_LIMIT_CODE && kind === "conversation";
2385
2415
  const requestLimitAllowed = error.code === CONVERSATION_READ_REQUEST_LIMIT_CODE && kind === "conversation";
2386
- if (error.code !== DOCUMENT_READ_NOT_FOUND_CODE && !sourceSupersededAllowed && !contentLimitAllowed && !requestLimitAllowed) {
2416
+ const staleReferenceKind = error.code === READ_STALE_REFERENCE_CODE && (kind === "passage" || kind === "equation") ? kind : null;
2417
+ if (error.code !== DOCUMENT_READ_NOT_FOUND_CODE && !sourceSupersededAllowed && !contentLimitAllowed && !requestLimitAllowed && !staleReferenceKind) {
2387
2418
  throw new PapiersCliResponseValidationError(
2388
2419
  `${label}.code was not a supported read error code.`
2389
2420
  );
@@ -2393,7 +2424,7 @@ function validateReadItemError(label, value, kind, expectedFormat) {
2393
2424
  `${label}.code was not supported for metadata reads.`
2394
2425
  );
2395
2426
  }
2396
- const expectedMessage = error.code === DOCUMENT_READ_NOT_FOUND_CODE ? readNotFoundMessageForKind(kind) : error.code === CONVERSATION_READ_CONTENT_LIMIT_CODE ? CONVERSATION_READ_CONTENT_LIMIT_MESSAGE : error.code === CONVERSATION_READ_REQUEST_LIMIT_CODE ? CONVERSATION_READ_REQUEST_LIMIT_MESSAGE : kind === "conversation" ? CONVERSATION_READ_SOURCE_SUPERSEDED_MESSAGE : kind === "passage" ? PASSAGE_READ_CONTENT_UNAVAILABLE_MESSAGE : kind === "note" ? NOTE_READ_SOURCE_SUPERSEDED_MESSAGE : DOCUMENT_READ_SOURCE_SUPERSEDED_MESSAGE;
2427
+ const expectedMessage = staleReferenceKind ? staleReferenceMessage(staleReferenceKind) : error.code === DOCUMENT_READ_NOT_FOUND_CODE ? readNotFoundMessageForKind(kind) : error.code === CONVERSATION_READ_CONTENT_LIMIT_CODE ? CONVERSATION_READ_CONTENT_LIMIT_MESSAGE : error.code === CONVERSATION_READ_REQUEST_LIMIT_CODE ? CONVERSATION_READ_REQUEST_LIMIT_MESSAGE : kind === "conversation" ? CONVERSATION_READ_SOURCE_SUPERSEDED_MESSAGE : kind === "passage" ? PASSAGE_READ_CONTENT_UNAVAILABLE_MESSAGE : kind === "note" ? NOTE_READ_SOURCE_SUPERSEDED_MESSAGE : DOCUMENT_READ_SOURCE_SUPERSEDED_MESSAGE;
2397
2428
  if (error.message !== expectedMessage) {
2398
2429
  throw new PapiersCliResponseValidationError(
2399
2430
  `${label}.message was not a supported read error message.`
@@ -2920,14 +2951,6 @@ function assertReadIdentifier(label, value) {
2920
2951
  }
2921
2952
  return value;
2922
2953
  }
2923
- function assertTokenId(label, value) {
2924
- if (typeof value !== "string" || !isTokenUuid(value)) {
2925
- throw new PapiersCliResponseValidationError(
2926
- `${label} was not a token UUID.`
2927
- );
2928
- }
2929
- return value;
2930
- }
2931
2954
  function assertPublicRequestId(label, value) {
2932
2955
  if (typeof value !== "string" || !SAFE_REQUEST_ID_PATTERN.test(value)) {
2933
2956
  throw new PapiersCliResponseValidationError(
@@ -3015,7 +3038,7 @@ async function getCapabilities(config, fetchImpl) {
3015
3038
  config,
3016
3039
  fetchImpl,
3017
3040
  "/api/v1/papiers/capabilities",
3018
- { method: "GET", retryable: true }
3041
+ { method: "GET" }
3019
3042
  );
3020
3043
  assertPapiersCapabilitiesResponse("capabilities", body);
3021
3044
  return body;
@@ -3026,7 +3049,7 @@ async function listDocuments(config, fetchImpl, request) {
3026
3049
  config,
3027
3050
  fetchImpl,
3028
3051
  "/api/v1/papiers/list",
3029
- { method: "POST", body: request, retryable: true }
3052
+ { method: "POST", body: request }
3030
3053
  );
3031
3054
  assertPapiersListResponse("list", body, { expectedKind });
3032
3055
  return body;
@@ -3037,7 +3060,7 @@ async function searchDocuments(config, fetchImpl, request) {
3037
3060
  config,
3038
3061
  fetchImpl,
3039
3062
  "/api/v1/papiers/search",
3040
- { method: "POST", body: request, retryable: true }
3063
+ { method: "POST", body: request }
3041
3064
  );
3042
3065
  assertPapiersSearchResponse("search", body, request.query, expected);
3043
3066
  return body;
@@ -3047,7 +3070,7 @@ async function browseSearchResults(config, fetchImpl, request) {
3047
3070
  config,
3048
3071
  fetchImpl,
3049
3072
  "/api/v1/papiers/search/results",
3050
- { method: "POST", body: request, retryable: true }
3073
+ { method: "POST", body: request }
3051
3074
  );
3052
3075
  assertPapiersSearchResultsResponse("search results", body, request);
3053
3076
  return body;
@@ -3057,7 +3080,7 @@ async function readDocuments(config, fetchImpl, request) {
3057
3080
  config,
3058
3081
  fetchImpl,
3059
3082
  "/api/v1/papiers/read",
3060
- { method: "POST", body: request, retryable: true }
3083
+ { method: "POST", body: request }
3061
3084
  );
3062
3085
  const readKind = readKindForIdentifier(request.ids[0] ?? "");
3063
3086
  assertPapiersReadResponse("read", body, request.ids, {
@@ -3070,26 +3093,6 @@ async function readDocuments(config, fetchImpl, request) {
3070
3093
  });
3071
3094
  return body;
3072
3095
  }
3073
- async function listPapiersTokens(config, fetchImpl) {
3074
- const body = await requestPapiersApi(
3075
- config,
3076
- fetchImpl,
3077
- "/api/v1/papiers/tokens",
3078
- { method: "GET", retryable: true }
3079
- );
3080
- assertPapiersTokenListResponse("tokens list", body);
3081
- return body;
3082
- }
3083
- async function revokePapiersToken(config, fetchImpl, tokenId) {
3084
- const body = await requestPapiersApi(
3085
- config,
3086
- fetchImpl,
3087
- `/api/v1/papiers/tokens/${encodeURIComponent(tokenId)}/revoke`,
3088
- { method: "POST" }
3089
- );
3090
- assertPapiersTokenRevokeResponse("tokens revoke", body, tokenId);
3091
- return body;
3092
- }
3093
3096
  function expectedKindForListSearchRequest(request) {
3094
3097
  const kinds = request.kinds;
3095
3098
  if (kinds === void 0) {
@@ -3135,6 +3138,38 @@ function expectedKindsForSearchRequest(request) {
3135
3138
  const [onlyKind] = expectedKinds;
3136
3139
  return expectedKinds.size === 1 ? { expectedKind: onlyKind } : { expectedKinds };
3137
3140
  }
3141
+ async function importDocument(config, fetchImpl, request) {
3142
+ const body = await requestPapiersApi(
3143
+ config,
3144
+ fetchImpl,
3145
+ "/api/v1/papiers/imports",
3146
+ { method: "POST", body: request }
3147
+ );
3148
+ assertPapiersImportResponse("import", body);
3149
+ return body;
3150
+ }
3151
+ async function getImportStatus(config, fetchImpl, operationId) {
3152
+ const body = await requestPapiersApi(
3153
+ config,
3154
+ fetchImpl,
3155
+ `/api/v1/papiers/imports/${encodeURIComponent(operationId)}`,
3156
+ { method: "GET" }
3157
+ );
3158
+ assertPapiersImportResponse("import-status", body);
3159
+ return body;
3160
+ }
3161
+ async function highlightDocument(config, fetchImpl, request) {
3162
+ const body = await requestPapiersApi(
3163
+ config,
3164
+ fetchImpl,
3165
+ "/api/v1/papiers/highlights",
3166
+ // Outlasts the server's 10 s queue wait plus 20 s search, so its answer
3167
+ // arrives.
3168
+ { method: "POST", body: request, timeoutMs: 4e4 }
3169
+ );
3170
+ assertPapiersHighlightResponse("highlight", body);
3171
+ return body;
3172
+ }
3138
3173
  async function requestPapiersApi(config, fetchImpl, path, init) {
3139
3174
  if (!config.token) {
3140
3175
  throw new CliUsageError(
@@ -3164,10 +3199,13 @@ async function requestPapiersApiWithBearer(config, fetchImpl, bearerToken, path,
3164
3199
  headers["content-type"] = "application/json";
3165
3200
  requestInit.body = JSON.stringify(init.body);
3166
3201
  }
3167
- const response = await fetchWithRetry(fetchImpl, requestUrl, requestInit, {
3168
- retryable: init.retryable === true
3169
- });
3170
- const body = await parseResponseBody(response);
3202
+ const { response, text } = await fetchWithRetry(
3203
+ fetchImpl,
3204
+ requestUrl,
3205
+ requestInit,
3206
+ init.timeoutMs ?? REQUEST_TIMEOUT_MS
3207
+ );
3208
+ const body = parseResponseBody(text);
3171
3209
  if (!response.ok) {
3172
3210
  const errorEnvelope = extractErrorEnvelope(body, response.status);
3173
3211
  throw new PapiersCliApiError({
@@ -3179,44 +3217,45 @@ async function requestPapiersApiWithBearer(config, fetchImpl, bearerToken, path,
3179
3217
  }
3180
3218
  return body;
3181
3219
  }
3182
- async function fetchWithRetry(fetchImpl, requestUrl, requestInit, options) {
3183
- const maxAttempts = options.retryable ? MAX_REQUEST_ATTEMPTS : 1;
3220
+ async function fetchWithRetry(fetchImpl, requestUrl, requestInit, timeoutMs) {
3184
3221
  let attempt = 1;
3185
3222
  while (true) {
3186
3223
  let response;
3224
+ let text;
3187
3225
  try {
3188
3226
  response = await fetchImpl(requestUrl, {
3189
3227
  ...requestInit,
3190
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
3228
+ signal: AbortSignal.timeout(timeoutMs)
3191
3229
  });
3230
+ text = await response.text();
3192
3231
  } catch (error) {
3193
- if (attempt >= maxAttempts) {
3232
+ if (attempt >= MAX_REQUEST_ATTEMPTS) {
3194
3233
  throw error;
3195
3234
  }
3196
3235
  await sleep(RETRY_BACKOFF_MS[attempt - 1] ?? 1e3);
3197
3236
  attempt += 1;
3198
3237
  continue;
3199
3238
  }
3200
- if (attempt < maxAttempts && RETRYABLE_STATUS_CODES.has(response.status)) {
3201
- await sleep(retryDelayMs(response, attempt));
3239
+ const delayMs = retryDelayMs(response, attempt);
3240
+ if (attempt < MAX_REQUEST_ATTEMPTS && RETRYABLE_STATUS_CODES.has(response.status) && delayMs <= MAX_RETRY_AFTER_MS) {
3241
+ await sleep(delayMs);
3202
3242
  attempt += 1;
3203
3243
  continue;
3204
3244
  }
3205
- return response;
3245
+ return { response, text };
3206
3246
  }
3207
3247
  }
3208
3248
  function retryDelayMs(response, attempt) {
3209
3249
  const retryAfterSeconds = Number(response.headers.get("retry-after") ?? "");
3210
3250
  if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
3211
- return Math.min(retryAfterSeconds * 1e3, MAX_RETRY_AFTER_MS);
3251
+ return retryAfterSeconds * 1e3;
3212
3252
  }
3213
3253
  return RETRY_BACKOFF_MS[attempt - 1] ?? 1e3;
3214
3254
  }
3215
3255
  function sleep(ms) {
3216
3256
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
3217
3257
  }
3218
- async function parseResponseBody(response) {
3219
- const text = await response.text();
3258
+ function parseResponseBody(text) {
3220
3259
  if (!text) {
3221
3260
  return null;
3222
3261
  }
@@ -3241,6 +3280,26 @@ function formatApiErrorMessage(params) {
3241
3280
  return `Papiers API error ${params.status}.`;
3242
3281
  }
3243
3282
 
3283
+ // ../../src/shared/highlight-colors.ts
3284
+ var HIGHLIGHT_HEX_COLOR_REGEX = /^#[0-9a-f]{6}$/;
3285
+
3286
+ // ../../src/shared/papiers-public-api.ts
3287
+ var PAPIERS_PUBLIC_DATA_PLANE_SCOPES2 = [
3288
+ "library:metadata:read",
3289
+ "library:search",
3290
+ "content:search",
3291
+ "content:snippet:read",
3292
+ "content:read",
3293
+ "content:structure:read"
3294
+ ];
3295
+ var PAPIERS_PUBLIC_SCOPES = [
3296
+ ...PAPIERS_PUBLIC_DATA_PLANE_SCOPES2,
3297
+ "library:import",
3298
+ "library:highlight"
3299
+ ];
3300
+ var PAPIERS_HIGHLIGHT_MAX_SCOPE_PAGES = 20;
3301
+ var PAPIERS_HIGHLIGHT_MAX_PAGE = 1e4;
3302
+
3244
3303
  // src/filters.ts
3245
3304
  var FILTER_OPERATORS = [
3246
3305
  "eq",
@@ -3709,7 +3768,16 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
3709
3768
  "fail-fast",
3710
3769
  "version"
3711
3770
  ]);
3771
+ var HIGHLIGHT_VALUE_FLAGS = [
3772
+ "start",
3773
+ "end",
3774
+ "passage",
3775
+ "pages",
3776
+ "note",
3777
+ "color"
3778
+ ];
3712
3779
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
3780
+ ...HIGHLIGHT_VALUE_FLAGS,
3713
3781
  "api-url",
3714
3782
  "cursor",
3715
3783
  "document-id",
@@ -3827,8 +3895,9 @@ COMMANDS
3827
3895
  list List your documents, highlights, notes, and conversations
3828
3896
  read Print a document, highlight, note, or conversation
3829
3897
  browse Page through an earlier search's results
3898
+ import Add a public PDF or webpage to your library
3899
+ highlight Highlight a passage in a saved document
3830
3900
  auth Sign in, check status, or sign out
3831
- tokens List or revoke your API tokens
3832
3901
  doctor Check your setup, connection, and what's available
3833
3902
  version Print the CLI version
3834
3903
 
@@ -3886,7 +3955,7 @@ USAGE
3886
3955
  papiers list --kind highlight|conversation [--since WHEN]
3887
3956
 
3888
3957
  OPTIONS
3889
- --kind KIND document (default) \xB7 passage \xB7 highlight \xB7 conversation \xB7 equation \xB7 note.
3958
+ --kind KIND document (default) \xB7 highlight \xB7 conversation \xB7 note.
3890
3959
  --filter FIELD.OP=VALUE narrow document results \u2014 repeatable, up to 25. document kind only.
3891
3960
  e.g. --filter lastEngagedAt.gte=30d --filter annotated=true. Fields/ops: help --all.
3892
3961
  --order-by KEY addedAt:desc (default) or lastEngagedAt:desc. document kind only.
@@ -3916,7 +3985,7 @@ OPTIONS
3916
3985
  highlight, conversation: metadata|plain. note: markdown|plain.
3917
3986
  equation: metadata.
3918
3987
  --include SECTION extra sections, comma-separated: metadata, source, locations.
3919
- document only (highlight/conversation/equation/note: metadata only; passage: none).
3988
+ document only (passage/highlight/conversation/equation/note: metadata only).
3920
3989
  --pages RANGE restrict a document read to pages, e.g. 1,3-5. document only.
3921
3990
  --max-chars-per-item N truncate each item's content to N chars, 1\u2013200000.
3922
3991
  --cursor CURSOR continue one long document or note \u2014 use items[].nextCursor from the
@@ -3959,13 +4028,40 @@ OPTIONS
3959
4028
  --force when signing in, replace an already-saved token.
3960
4029
 
3961
4030
  Signing in saves your token on this machine. For scripts or an AI agent, set PAPIERS_TOKEN instead.`,
3962
- tokens: `papiers tokens \u2014 list or revoke your API tokens.
4031
+ import: `papiers import \u2014 add a public PDF or webpage to your library.
3963
4032
 
3964
4033
  USAGE
3965
- papiers tokens list [--json]
3966
- papiers tokens revoke TOKEN_ID [--json]
4034
+ papiers import URL [--json]
4035
+ papiers import-status OPERATION_ID [--json]
4036
+
4037
+ Needs a token created with "Allow document imports" (Settings -> API tokens).
4038
+ Import only sources the user approved. An import is queued, not finished:
4039
+ check import-status until it is ready or failed; any other status means it
4040
+ is still in progress. Ready means papiers read works on its documentId now.
4041
+ A status is as of that check: reprocessing or trashing the document can
4042
+ change it later.`,
4043
+ highlight: `papiers highlight \u2014 highlight a passage in a saved PDF or webpage.
3967
4044
 
3968
- Create and name tokens in Papiers (Settings -> API tokens). Revoking is permanent.`,
4045
+ USAGE
4046
+ papiers highlight DOCUMENT_ID --start TEXT [--end TEXT]
4047
+ [--passage PASSAGE_REF | --pages N[-M]]
4048
+ [--note TEXT] [--color #rrggbb] [--json]
4049
+
4050
+ Needs a token created with "Allow highlights" (Settings -> API tokens).
4051
+ DOCUMENT_ID is a doc_ ID or papiers://item/... locator. In a PDF, name what you
4052
+ read: --passage is the reference of a search result in this document
4053
+ (doc_\u2026#p\u2026.\u2026), --pages the page or pages (3, 3-4 or 3,5) you read it on. The
4054
+ passage is only looked for there; a webpage is searched whole and takes neither.
4055
+ --start is the passage's first words (or all of a short passage); --end its
4056
+ last words, for a longer passage or one crossing a formula. The highlight runs
4057
+ from --start through --end, so formulas and footnote marks between need not be
4058
+ quoted; if either repeats within reach (that page and the next, a few
4059
+ paragraphs), add words to it. Quote both as you read them; punctuation, hyphens,
4060
+ spacing and formatting don't matter. Keep math out of --start and --end, even
4061
+ a single symbol like V or \u03B2t: what you read spells it differently from the
4062
+ page, so start and end on plain words. If the passage appears more than once,
4063
+ add words until it is unique. Highlight only passages the user asked for or
4064
+ approved.`,
3969
4065
  doctor: `papiers doctor \u2014 check your setup, connection, and what's available.
3970
4066
 
3971
4067
  USAGE
@@ -3981,10 +4077,18 @@ USAGE
3981
4077
  var REFERENCE = `REFERENCE \u2014 IDs, piping, pagination, and limits.
3982
4078
 
3983
4079
  ID PREFIXES
3984
- doc_ document pas_ passage hl_ highlight
3985
- cnv_ conversation eq_ equation nte_ note
3986
- srch_ a saved search ranking (for browse)
3987
- On read, the kind \u2014 and which --format/--include apply \u2014 is read from the prefix.
4080
+ doc_ document hl_ highlight cnv_ conversation
4081
+ nte_ note srch_ a saved search ranking (for browse)
4082
+ On read, the kind \u2014 and which --format/--include apply \u2014 is read from the ID.
4083
+
4084
+ PASSAGES AND EQUATIONS
4085
+ A passage or equation is a place in a document plus a fingerprint of its text:
4086
+ doc_<id>#p12.3f9c01aa passage 12 of that document
4087
+ doc_<id>#enode_812.77d0e2b1 an equation in that document
4088
+ It keeps working while that text is unchanged. If the document is processed
4089
+ again and the text changed, you get stale_reference: search or read the
4090
+ document again. Quote references in the shell ('doc_\u2026#p12.3f9c01aa'): some
4091
+ shells treat # specially.
3988
4092
 
3989
4093
  RESOURCE LOCATORS
3990
4094
  papiers://note/<uuid> papiers://highlight/<uuid>
@@ -4041,11 +4145,12 @@ var SUGGESTIBLE_COMMANDS = [
4041
4145
  "auth",
4042
4146
  "doctor",
4043
4147
  "capabilities",
4044
- "tokens",
4045
4148
  "list",
4046
4149
  "search",
4047
4150
  "browse",
4048
4151
  "read",
4152
+ "import",
4153
+ "highlight",
4049
4154
  "version",
4050
4155
  "help"
4051
4156
  ];
@@ -4098,7 +4203,8 @@ function parseCliArgs(argv) {
4098
4203
  if (parsed.booleanFlags.has("all")) {
4099
4204
  return { type: "help", all: true, ...global };
4100
4205
  }
4101
- const topic = command === "help" ? rest[0] : command;
4206
+ const named = command === "help" ? rest[0] : command;
4207
+ const topic = named === "import-status" ? "import" : named;
4102
4208
  if (topic && HELP_TOPIC_NAMES.includes(topic)) {
4103
4209
  return { type: "help", topic, ...global };
4104
4210
  }
@@ -4125,9 +4231,61 @@ function parseCliArgs(argv) {
4125
4231
  );
4126
4232
  assertNoExtraPositionals(command, rest);
4127
4233
  return { type: "capabilities", ...global };
4128
- case "tokens":
4129
- assertAllowedFlags(parsed, NO_COMMAND_VALUE_FLAGS, "tokens");
4130
- return parseTokensCommand(rest, parsed, global);
4234
+ case "import":
4235
+ case "import-status": {
4236
+ assertAllowedFlags(parsed, NO_COMMAND_VALUE_FLAGS, command);
4237
+ assertAllowedBooleanFlags(parsed, NO_COMMAND_BOOLEAN_FLAGS, command);
4238
+ const [target, ...extra] = rest;
4239
+ assertNoExtraPositionals(command, extra);
4240
+ if (!target) {
4241
+ throw new CliUsageError(
4242
+ command === "import" ? "papiers import requires a URL." : "papiers import-status requires an operation ID."
4243
+ );
4244
+ }
4245
+ return command === "import" ? { type: "import", ...global, url: target } : { type: "import-status", ...global, operationId: target };
4246
+ }
4247
+ case "highlight": {
4248
+ assertAllowedFlags(
4249
+ parsed,
4250
+ /* @__PURE__ */ new Set([...GLOBAL_VALUE_FLAGS, ...HIGHLIGHT_VALUE_FLAGS]),
4251
+ command
4252
+ );
4253
+ assertAllowedBooleanFlags(parsed, NO_COMMAND_BOOLEAN_FLAGS, command);
4254
+ const [documentId, ...extra] = rest;
4255
+ assertNoExtraPositionals(command, extra);
4256
+ const start = singleFlagValue(parsed, "start");
4257
+ if (!documentId || !start) {
4258
+ throw new CliUsageError(
4259
+ "papiers highlight requires a document ID and --start."
4260
+ );
4261
+ }
4262
+ const passageId = singleFlagValue(parsed, "passage");
4263
+ const pagesFlag = singleFlagValue(parsed, "pages");
4264
+ if (passageId !== void 0 && pagesFlag !== void 0) {
4265
+ throw new CliUsageError("Give --passage or --pages, not both.");
4266
+ }
4267
+ const color = singleFlagValue(parsed, "color");
4268
+ if (color !== void 0 && !HIGHLIGHT_HEX_COLOR_REGEX.test(color)) {
4269
+ throw new CliUsageError(
4270
+ "--color must be a lowercase #rrggbb hex color, like #fde68a."
4271
+ );
4272
+ }
4273
+ const optional = (name) => {
4274
+ const value = singleFlagValue(parsed, name);
4275
+ return value === void 0 ? {} : { [name]: value };
4276
+ };
4277
+ return {
4278
+ type: "highlight",
4279
+ ...global,
4280
+ documentId,
4281
+ start,
4282
+ ...optional("end"),
4283
+ ...passageId === void 0 ? {} : { passageId },
4284
+ ...pagesFlag === void 0 ? {} : { pages: parsePages(pagesFlag) },
4285
+ ...optional("note"),
4286
+ ...optional("color")
4287
+ };
4288
+ }
4131
4289
  case "list": {
4132
4290
  assertAllowedFlags(parsed, LIST_VALUE_FLAGS, "list");
4133
4291
  assertAllowedBooleanFlags(parsed, IDS_PIPELINE_BOOLEAN_FLAGS, "list");
@@ -4226,35 +4384,6 @@ function parsePrettyOutput(parsed, idsOnly) {
4226
4384
  }
4227
4385
  return true;
4228
4386
  }
4229
- function parseTokensCommand(rest, parsed, global) {
4230
- const [subcommand, tokenId, ...extra] = rest;
4231
- switch (subcommand) {
4232
- case "list":
4233
- assertAllowedBooleanFlags(
4234
- parsed,
4235
- NO_COMMAND_BOOLEAN_FLAGS,
4236
- "tokens list"
4237
- );
4238
- assertNoExtraPositionals("tokens list", rest.slice(1));
4239
- return { type: "tokens-list", ...global };
4240
- case "revoke":
4241
- assertAllowedBooleanFlags(
4242
- parsed,
4243
- NO_COMMAND_BOOLEAN_FLAGS,
4244
- "tokens revoke"
4245
- );
4246
- if (!tokenId) {
4247
- throw new CliUsageError("papiers tokens revoke requires a token UUID.");
4248
- }
4249
- assertNoExtraPositionals("tokens revoke", extra);
4250
- if (!isUuid(tokenId)) {
4251
- throw new CliUsageError("papiers tokens revoke requires a token UUID.");
4252
- }
4253
- return { type: "tokens-revoke", ...global, tokenId };
4254
- default:
4255
- throw new CliUsageError("Supported tokens commands: list, revoke.");
4256
- }
4257
- }
4258
4387
  function parseAuthCommand(rest, parsed, global) {
4259
4388
  const [subcommand, ...extra] = rest;
4260
4389
  switch (subcommand) {
@@ -4357,7 +4486,7 @@ function buildReadRequest(parsed) {
4357
4486
  };
4358
4487
  }
4359
4488
  function buildListRequest(parsed, documentIds) {
4360
- const kind = parseListSearchKind(parsed, "list");
4489
+ const kind = parseListKind(parsed);
4361
4490
  const since = parseSinceFlag(parsed, kind, "list");
4362
4491
  if (kind !== "document") {
4363
4492
  if (documentIds?.length && kind !== "highlight") {
@@ -4649,7 +4778,13 @@ function validateReadOptionsForKind(parsed, kind, format) {
4649
4778
  "Public passage read supports --format markdown or --format plain."
4650
4779
  );
4651
4780
  }
4652
- rejectReadFlagsForKind(parsed, "passage", ["include", "pages", "cursor"]);
4781
+ const includes = parseReadInclude(parsed);
4782
+ if (includes?.some((include) => include !== "metadata")) {
4783
+ throw new CliUsageError(
4784
+ "Public passage read supports only --include metadata."
4785
+ );
4786
+ }
4787
+ rejectReadFlagsForKind(parsed, "passage", ["pages", "cursor"]);
4653
4788
  return;
4654
4789
  }
4655
4790
  if (kind === "highlight") {
@@ -4834,19 +4969,24 @@ function parseGlobalOptions(parsed) {
4834
4969
  apiBaseUrl: singleFlagValue(parsed, "api-url")
4835
4970
  };
4836
4971
  }
4837
- function parseListSearchKind(parsed, command) {
4972
+ function parseListKind(parsed) {
4838
4973
  const kind = singleFlagValue(parsed, "kind") ?? "document";
4839
4974
  const normalized = normalizeCliPublicKind(kind);
4975
+ if (normalized === "passage" || normalized === "equation") {
4976
+ throw new CliUsageError(
4977
+ `papiers list does not list ${normalized}s. Read a document (papiers read doc_\u2026) to page through its ${normalized}s in order, or search for them.`
4978
+ );
4979
+ }
4840
4980
  if (normalized) {
4841
4981
  return normalized;
4842
4982
  }
4843
4983
  if (kind.trim().toLowerCase() === "all") {
4844
4984
  throw new CliUsageError(
4845
- `papiers ${command} does not support --kind all here; multi-kind requests are available via papiers search --kind all.`
4985
+ "papiers list does not support --kind all here; multi-kind requests are available via papiers search --kind all."
4846
4986
  );
4847
4987
  }
4848
4988
  throw new CliUsageError(
4849
- `papiers ${command} supports --kind document, passage, highlight, conversation, equation, or note; received "${kind}".${didYouMeanKind(kind)}`
4989
+ `papiers list supports --kind document, highlight, conversation, or note; received "${kind}".${didYouMeanKind(kind)}`
4850
4990
  );
4851
4991
  }
4852
4992
  function normalizeCliPublicKind(value) {
@@ -5025,11 +5165,6 @@ function singleFlagValue(parsed, name) {
5025
5165
  }
5026
5166
  return values[0];
5027
5167
  }
5028
- function isUuid(value) {
5029
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(
5030
- value
5031
- );
5032
- }
5033
5168
  function splitCommaSeparatedValues(rawValue) {
5034
5169
  return rawValue.split(",").map((value) => value.trim()).filter(Boolean);
5035
5170
  }
@@ -5045,6 +5180,26 @@ function pruneUndefined(value) {
5045
5180
  Object.entries(value).filter(([, entry]) => entry !== void 0)
5046
5181
  );
5047
5182
  }
5183
+ function parsePages(value) {
5184
+ const pages = [];
5185
+ for (const part of value.split(",")) {
5186
+ const match = /^\s*(\d+)\s*(?:-\s*(\d+)\s*)?$/.exec(part);
5187
+ const first = match ? Number(match[1]) : Number.NaN;
5188
+ const last = match?.[2] ? Number(match[2]) : first;
5189
+ if (!(first >= 1 && first <= last && last <= PAPIERS_HIGHLIGHT_MAX_PAGE)) {
5190
+ throw new CliUsageError(
5191
+ `--pages must be a page, a range like 3-4, or a list like 3,5, of pages up to ${PAPIERS_HIGHLIGHT_MAX_PAGE}.`
5192
+ );
5193
+ }
5194
+ if (pages.length + (last - first + 1) > PAPIERS_HIGHLIGHT_MAX_SCOPE_PAGES) {
5195
+ throw new CliUsageError(
5196
+ `--pages can name at most ${PAPIERS_HIGHLIGHT_MAX_SCOPE_PAGES} pages.`
5197
+ );
5198
+ }
5199
+ for (let page = first; page <= last; page++) pages.push(page);
5200
+ }
5201
+ return pages;
5202
+ }
5048
5203
 
5049
5204
  // src/capabilities.ts
5050
5205
  var CAPABILITIES_ACCOUNT_KEYS = [
@@ -5074,11 +5229,11 @@ function assertSupportedDocumentCapabilities(value) {
5074
5229
  assertNonEmptyString2("capabilities account.userId", account.userId);
5075
5230
  assertExactString("capabilities account.profile", account.profile, "pat-v1");
5076
5231
  const scopes = assertStringArray("capabilities scopes", capabilities.scopes);
5077
- assertSameStringSet(
5078
- "capabilities scopes",
5079
- scopes,
5080
- PAPIERS_PUBLIC_DATA_PLANE_SCOPES
5081
- );
5232
+ if (!PAPIERS_PUBLIC_DATA_PLANE_SCOPES.every((s) => scopes.includes(s))) {
5233
+ throw new Error(
5234
+ `capabilities scopes was ${scopes.join(", ") || "empty"}; expected every public read scope.`
5235
+ );
5236
+ }
5082
5237
  const searchKinds = assertStringArray(
5083
5238
  "capabilities searchKinds",
5084
5239
  capabilities.searchKinds
@@ -5740,22 +5895,36 @@ async function runCli(argv, dependencies = {}) {
5740
5895
  );
5741
5896
  return 0;
5742
5897
  }
5743
- case "tokens-list":
5744
- writeTokenListOutput(
5898
+ case "import":
5899
+ writeJson(
5900
+ stdout,
5901
+ await importDocument(config, resolveFetch(dependencies.fetch), {
5902
+ schemaVersion: PAPIERS_SCHEMA_VERSIONS.importRequest,
5903
+ url: command.url,
5904
+ submissionId: randomUUID2()
5905
+ })
5906
+ );
5907
+ return 0;
5908
+ case "highlight": {
5909
+ const { type: _, json: __, apiBaseUrl: ___, ...highlight } = command;
5910
+ writeJson(
5745
5911
  stdout,
5746
- await listPapiersTokens(config, resolveFetch(dependencies.fetch)),
5747
- command.json
5912
+ await highlightDocument(config, resolveFetch(dependencies.fetch), {
5913
+ schemaVersion: PAPIERS_SCHEMA_VERSIONS.highlightRequest,
5914
+ ...highlight,
5915
+ referenceId: randomUUID2()
5916
+ })
5748
5917
  );
5749
5918
  return 0;
5750
- case "tokens-revoke":
5751
- writeTokenRevokeOutput(
5919
+ }
5920
+ case "import-status":
5921
+ writeJson(
5752
5922
  stdout,
5753
- await revokePapiersToken(
5923
+ await getImportStatus(
5754
5924
  config,
5755
5925
  resolveFetch(dependencies.fetch),
5756
- command.tokenId
5757
- ),
5758
- command.json
5926
+ command.operationId
5927
+ )
5759
5928
  );
5760
5929
  return 0;
5761
5930
  case "list":
@@ -6016,50 +6185,6 @@ function writeAuthInspectOutput(stdout, output, json) {
6016
6185
  `
6017
6186
  );
6018
6187
  }
6019
- function writeTokenListOutput(stdout, output, json) {
6020
- if (json) {
6021
- writeJson(stdout, output);
6022
- return;
6023
- }
6024
- if (output.tokens.length === 0) {
6025
- stdout.write(
6026
- `No active Papiers tokens found. requestId=${output.requestId}
6027
- `
6028
- );
6029
- return;
6030
- }
6031
- const lines = [
6032
- `Papiers tokens requestId=${output.requestId}`,
6033
- ...output.tokens.map(
6034
- (token) => [
6035
- token.tokenId,
6036
- token.name,
6037
- `scopes=${token.scopes.join(",")}`,
6038
- `created=${token.createdAt}`,
6039
- `expires=${token.expiresAt ?? "never"}`,
6040
- `lastUsed=${token.lastUsedAt ?? "never"}`
6041
- ].join(" | ")
6042
- )
6043
- ];
6044
- stdout.write(`${redactSensitiveText(lines.join("\n"))}
6045
- `);
6046
- }
6047
- function writeTokenRevokeOutput(stdout, output, json) {
6048
- if (json) {
6049
- writeJson(stdout, output);
6050
- return;
6051
- }
6052
- const state = output.revoked ? "revoked" : "already revoked";
6053
- stdout.write(
6054
- `${redactSensitiveText(
6055
- [
6056
- `Token ${output.tokenId} ${state}. requestId=${output.requestId}`,
6057
- "Local auth files were not changed; run papiers auth logout if this was your configured token."
6058
- ].join("\n")
6059
- )}
6060
- `
6061
- );
6062
- }
6063
6188
  async function runAuthInspect(params) {
6064
6189
  const capabilities = await getCapabilities(params.config, params.fetchImpl);
6065
6190
  assertCliSupportedCapabilities(capabilities);
@@ -6613,9 +6738,9 @@ function validateExpandedReadRequest(request, ids, requestedReadKind) {
6613
6738
  "Public passage read supports --format markdown or --format plain."
6614
6739
  );
6615
6740
  }
6616
- if (request.include?.length) {
6741
+ if (request.include?.some((include) => include !== "metadata")) {
6617
6742
  throw new CliUsageError(
6618
- "Public passage read does not support --include."
6743
+ "Public passage read supports only --include metadata."
6619
6744
  );
6620
6745
  }
6621
6746
  if (request.scope?.pages || request.scope?.sectionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "papiers",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Read-only CLI for your Papiers library.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://papiers.ai",
@@ -85,6 +85,9 @@ once for the full contract. `PAPIERS_TOKEN` must be configured.
85
85
 
86
86
  Most highlights carry no annotation (quote-only); `annotationText` is
87
87
  only present on reads, so read the highlights you intend to lean on.
88
+ It contains sanitized Markdown source, including emphasis markers, escapes,
89
+ and backticks; interpret those as part of the user's note. The read's
90
+ `format` describes the quoted content, not the annotation's syntax.
88
91
 
89
92
  5. **Write the judgement brief.** Before generating anything, summarize what
90
93
  you found: verbatim annotations with dates and IDs, stances, open
@@ -111,8 +114,24 @@ once for the full contract. `PAPIERS_TOKEN` must be configured.
111
114
  ISO timestamps or `30d`-style shorthand.
112
115
  - **Honor the quality signal.** `quality.signal: "empty"` or `"low"` means
113
116
  the library has nothing — report that rather than citing weak matches.
114
- - **Cite IDs.** `hl_/cnv_/doc_/pas_/eq_/nte_` IDs are stable; include them so the
115
- user can jump to sources.
117
+ - **Cite IDs.** Include `doc_/hl_/cnv_/nte_` IDs so the user can jump to
118
+ sources. A passage or equation is a place in a document plus a fingerprint
119
+ of its text (`'doc_<id>#p12.3f9c01aa'`); if its text has changed since you got
120
+ it, Papiers reports `stale_reference` — search or read the document again.
121
+ Quote these references in the shell: some shells treat `#` specially.
122
+ - **Import only what the user approved.** `papiers import URL` needs a token
123
+ with "Allow document imports". Show the user the exact URLs first. An import
124
+ is queued until `papiers import-status OPERATION_ID` says `ready`, which
125
+ means `papiers read` works on its `documentId`; don't call it added before
126
+ then. On `failed`, report `error.message`; retry only if `error.retryable`.
127
+ - **Highlight only what the user asked for or approved.** `papiers highlight
128
+ DOC_ID --start "..." [--end "..."]` needs a token with "Allow highlights".
129
+ In a PDF, name what you read: `--passage` with the search result's passage
130
+ reference, or `--pages` with the pages you read; a webpage needs neither. Give the
131
+ passage's first words as `--start` and, for a longer passage or one crossing
132
+ a formula, its last words as `--end`; both as you read them, on plain words:
133
+ keep math, even a single symbol like V, out of both (formatting doesn't
134
+ matter). If it repeats, add words until unique.
116
135
  - **Budgets are policy, not protocol.** Default to ~10–20 queries and 3–8
117
136
  document reads per task. The user may override any policy in this skill
118
137
  (read more, weight recency differently, skip curation); the loop's shape