ente-cli 0.4.84 → 0.4.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/SKILL.md CHANGED
@@ -188,7 +188,7 @@ ente activity delete <activityId> [--json]
188
188
  ente activity restore <activityId> [--json]
189
189
 
190
190
  # Database lists (entity + typed columns; each row is a note/task activity; no date column type)
191
- ente list create "<name>" --space "<space>" [--column "key[=Label]:text|number|boolean|single-select|multi-select[:opt,opt]"] [--pin "<parent>" [--share-parents --parent-space-grant-fingerprint <token>]] [--json]
191
+ ente list create "<name>" --space "<space>" [--icon <topic-icon>] [--column "key[=Label]:text|number|boolean|single-select|multi-select[:opt,opt]"] [--pin "<parent>" [--share-parents --parent-space-grant-fingerprint <token>]] [--json]
192
192
  ente list add "<list>" --title "…" [--attr k=v ...] [--key <textColumn>] [--content-file ./row.md] [--json] # --key = upsert by that cell
193
193
  ente list update "<list>" <rowId> [--attr k=v ...] [--title "…"] [--json] # empty value clears a cell
194
194
  ente list import "<list>" --file rows.jsonl --key <textColumn> [--dry-run] [--concurrency 4] [--json] # {"title","attrs","content"} per line → created/updated/unchanged
@@ -443,7 +443,7 @@ ente recording backups join <backupId> [--now] # retry a failed join; --now
443
443
  ### Feedback board
444
444
 
445
445
  ```bash
446
- ente feedback send "<summary>" [--category missing|confusing|wrong_data|workflow|bug|other] \
446
+ ente feedback send "<summary>" \
447
447
  [--situation "…"] [--goal "…"] [--need-to-see "…"] [--need-to-do "…"] [--reason "…"] [--workaround "…"]
448
448
  ente feedback list [--status offen|todo|bearbeitung|klaerung|abgeschlossen|spaeter] [--text "…"] [--limit 50] [--json]
449
449
  ente feedback view <feedbackId> [--json] # full item + comment thread
@@ -452,7 +452,7 @@ ente feedback status <feedbackId> <status> # move to a board colum
452
452
  ```
453
453
 
454
454
  - Same in-app feedback board as the web composer (`convex/feedback.ts` public functions) — no GitHub issue.
455
- - Every authenticated user may submit and manage the shared board. Screenshots/attachments are web-composer only.
455
+ - Human sign-in only: the board refuses agent-bound CLI keys (every CLI key since 2026-10-11) and the CLI says so; use the web board then. Categories became tags (set them in the web). Screenshots/attachments are web-composer only.
456
456
 
457
457
  ### Deadline formats
458
458
 
package/dist/cli.mjs CHANGED
@@ -3800,7 +3800,7 @@ async function runCli(cli2, argv, context) {
3800
3800
  }
3801
3801
 
3802
3802
  // ente/version.ts
3803
- var VERSION = "0.4.84";
3803
+ var VERSION = "0.4.86";
3804
3804
 
3805
3805
  // ente/branding.ts
3806
3806
  var NPM_REGISTRY = "https://registry.npmjs.org";
@@ -3907,17 +3907,20 @@ function keyToFile(cacheDir, key) {
3907
3907
  const safe = key.replace(/[^a-zA-Z0-9._-]/g, "_");
3908
3908
  return path4.join(cacheDir, `jwt-${safe}.json`);
3909
3909
  }
3910
- function loadJwt(cacheDir, key) {
3910
+ function loadJwtEntry(cacheDir, key) {
3911
3911
  try {
3912
3912
  const file = keyToFile(cacheDir, key);
3913
3913
  if (!existsSync4(file)) return null;
3914
3914
  const entry = JSON.parse(readFileSync4(file, "utf8"));
3915
3915
  if (typeof entry.jwt !== "string" || typeof entry.expiresAt !== "number") return null;
3916
- return entry.expiresAt - SKEW_MS > Date.now() ? entry.jwt : null;
3916
+ return isJwtFresh(entry.expiresAt) ? { jwt: entry.jwt, expiresAt: entry.expiresAt } : null;
3917
3917
  } catch {
3918
3918
  return null;
3919
3919
  }
3920
3920
  }
3921
+ function isJwtFresh(expiresAt, now = Date.now()) {
3922
+ return expiresAt - SKEW_MS > now;
3923
+ }
3921
3924
  function storeJwt(cacheDir, key, jwt, expiresAt) {
3922
3925
  const file = keyToFile(cacheDir, key);
3923
3926
  mkdirSync4(path4.dirname(file), { recursive: true, mode: 448 });
@@ -10827,7 +10830,7 @@ async function exchangeToken(siteUrl, apiKey) {
10827
10830
  if (!body.token) throw new Error("/cli/token returned no token.");
10828
10831
  return { jwt: body.token, expiresAt: body.expiresAt ?? Date.now() + DEFAULT_TTL_MS };
10829
10832
  }
10830
- function isAuthError(err) {
10833
+ function isAuthError(err, structuredOnly = false) {
10831
10834
  if (err && typeof err === "object" && "data" in err) {
10832
10835
  const data = err.data;
10833
10836
  if (data && typeof data === "object" && "code" in data) {
@@ -10836,33 +10839,74 @@ function isAuthError(err) {
10836
10839
  }
10837
10840
  }
10838
10841
  const msg = err instanceof Error ? err.message : String(err);
10842
+ if (convexHttpErrorCode(msg) === "InvalidAuthHeader") return true;
10843
+ if (structuredOnly) return false;
10839
10844
  return /\b(401|403|unauthenticated|unauthorized|not authenticated)\b/i.test(msg);
10840
10845
  }
10846
+ function convexHttpErrorCode(message) {
10847
+ try {
10848
+ const body = JSON.parse(message);
10849
+ return body && typeof body === "object" && "code" in body && typeof body.code === "string" ? body.code : null;
10850
+ } catch {
10851
+ return null;
10852
+ }
10853
+ }
10854
+ function isWriteConflict(err) {
10855
+ const msg = err instanceof Error ? err.message : String(err);
10856
+ return /OptimisticConcurrencyControlFailure/u.test(msg);
10857
+ }
10858
+ var MAX_CONFLICT_ATTEMPTS = 6;
10859
+ var DEFAULT_CONFLICT_BACKOFF_MS = 250;
10841
10860
  async function mintAndCache(opts) {
10842
10861
  const { jwt, expiresAt } = await exchangeToken(opts.siteUrl, opts.apiKey);
10843
10862
  storeJwt(opts.cacheDir, opts.cacheKey, jwt, expiresAt);
10844
- return jwt;
10863
+ return { jwt, expiresAt };
10845
10864
  }
10846
10865
  async function makeCaller(opts) {
10847
10866
  const client = new ConvexHttpClient(opts.convexUrl);
10848
- const cached = loadJwt(opts.cacheDir, opts.cacheKey);
10849
- const jwt = cached ?? await mintAndCache(opts);
10850
- client.setAuth(jwt);
10851
- async function withRetry(fn) {
10867
+ const backoffMs = opts.conflictBackoffMs ?? DEFAULT_CONFLICT_BACKOFF_MS;
10868
+ let current = loadJwtEntry(opts.cacheDir, opts.cacheKey) ?? await mintAndCache(opts);
10869
+ client.setAuth(current.jwt);
10870
+ let renewal = null;
10871
+ function renew() {
10872
+ renewal ??= mintAndCache(opts).then((entry) => {
10873
+ current = entry;
10874
+ client.setAuth(entry.jwt);
10875
+ }).finally(() => {
10876
+ renewal = null;
10877
+ });
10878
+ return renewal;
10879
+ }
10880
+ async function withAuth(fn, structuredOnly = false) {
10881
+ if (renewal) await renewal;
10882
+ else if (!isJwtFresh(current.expiresAt)) await renew();
10883
+ const used = current;
10852
10884
  try {
10853
10885
  return await fn();
10854
10886
  } catch (err) {
10855
- if (!isAuthError(err)) throw err;
10856
- client.setAuth(await mintAndCache(opts));
10887
+ if (!isAuthError(err, structuredOnly)) throw err;
10888
+ if (current === used) await renew();
10889
+ else if (renewal) await renewal;
10857
10890
  return fn();
10858
10891
  }
10859
10892
  }
10893
+ async function withConflictRetry(fn) {
10894
+ for (let attempt = 1; ; attempt++) {
10895
+ try {
10896
+ return await withAuth(fn);
10897
+ } catch (err) {
10898
+ if (!isWriteConflict(err) || attempt >= MAX_CONFLICT_ATTEMPTS) throw err;
10899
+ const delay = backoffMs * 2 ** (attempt - 1);
10900
+ await new Promise((resolve2) => setTimeout(resolve2, delay / 2 + Math.random() * delay));
10901
+ }
10902
+ }
10903
+ }
10860
10904
  return {
10861
10905
  convexUrl: opts.convexUrl,
10862
- query: (ref, ...args) => withRetry(() => client.query(ref, ...args)),
10863
- mutation: (ref, ...args) => withRetry(() => client.mutation(ref, ...args)),
10864
- mutationUnqueued: (ref, ...args) => withRetry(() => client.mutation(ref, args[0] ?? {}, { skipQueue: true })),
10865
- action: (ref, ...args) => withRetry(() => client.action(ref, ...args))
10906
+ query: (ref, ...args) => withAuth(() => client.query(ref, ...args)),
10907
+ mutation: (ref, ...args) => withConflictRetry(() => client.mutation(ref, ...args)),
10908
+ mutationUnqueued: (ref, ...args) => withConflictRetry(() => client.mutation(ref, args[0] ?? {}, { skipQueue: true })),
10909
+ action: (ref, ...args) => withAuth(() => client.action(ref, ...args), true)
10866
10910
  };
10867
10911
  }
10868
10912
 
@@ -11904,10 +11948,12 @@ var EntityCreate = class extends ProfileCommand {
11904
11948
  if (this.parent && this.pin?.length) throw new Error("Use --parent for atomic child creation, or --pin for legacy parent linking, not both.");
11905
11949
  const parentEntityId = this.parent ? await resolveEntityC(caller, this.parent) : void 0;
11906
11950
  const attributes = parseAttrs(this.attr);
11951
+ const type = ENTITY_TYPE_VALUES.find((type2) => type2 === this.type);
11952
+ if (!type) throw new Error(`Invalid entity type '${this.type}'. Use one of: ${ENTITY_TYPE_VALUES.join(" | ")}.`);
11907
11953
  const entityId = await caller.mutation(entitiesCreate, {
11908
11954
  spaceId,
11909
11955
  name: this.name,
11910
- type: this.type,
11956
+ type,
11911
11957
  ...parentEntityId ? { parentEntityId, exclusiveParent: this.exclusiveParent } : {}
11912
11958
  });
11913
11959
  if (this.alias) for (const a of this.alias) await caller.mutation(entitiesAddAlias, { id: entityId, alias: a });
@@ -12338,6 +12384,51 @@ var ENTITY_COMMANDS = [
12338
12384
  import { readFileSync as readFileSync9 } from "node:fs";
12339
12385
  import { basename as basename2 } from "node:path";
12340
12386
 
12387
+ // ../packages/shared/src/activity-dates.ts
12388
+ var DEFAULT_DEADLINE_TIME_ZONE = "Europe/Berlin";
12389
+ var deadlineJsonSchema = {
12390
+ anyOf: [
12391
+ {
12392
+ type: "object",
12393
+ properties: {
12394
+ date: {
12395
+ type: "string",
12396
+ description: "Local calendar date in YYYY-MM-DD format, for example 2026-05-23."
12397
+ },
12398
+ time: {
12399
+ anyOf: [{ type: "string" }, { type: "null" }],
12400
+ description: "Optional local time in 24-hour HH:mm format. Use null or omit for date-only tasks."
12401
+ },
12402
+ timezone: {
12403
+ type: "string",
12404
+ description: `IANA timezone for the local date/time. Defaults to ${DEFAULT_DEADLINE_TIME_ZONE}.`
12405
+ }
12406
+ },
12407
+ required: ["date"],
12408
+ additionalProperties: false
12409
+ },
12410
+ {
12411
+ type: "number",
12412
+ description: "Legacy UTC epoch milliseconds. Prefer the object format instead of calculating this."
12413
+ }
12414
+ ],
12415
+ description: `Prefer { "date": "YYYY-MM-DD", "time": "HH:mm" or null, "timezone": "${DEFAULT_DEADLINE_TIME_ZONE}" }. Do not calculate epoch milliseconds unless a legacy client requires it.`
12416
+ };
12417
+ var nullableDeadlineJsonSchema = {
12418
+ anyOf: [{ type: "null" }, ...deadlineJsonSchema.anyOf],
12419
+ description: `${deadlineJsonSchema.description} Use null to clear an existing deadline.`
12420
+ };
12421
+
12422
+ // ../packages/shared/src/activity-contracts.ts
12423
+ var ACTIVITY_TYPE_VALUES = ["note", "conversation", "meeting", "call", "message", "task"];
12424
+
12425
+ // ente/activityType.ts
12426
+ function parseActivityType(value) {
12427
+ const type = ACTIVITY_TYPE_VALUES.find((type2) => type2 === value);
12428
+ if (!type) throw new Error(`Invalid activity type '${value}'. Use one of: ${ACTIVITY_TYPE_VALUES.join(" | ")}.`);
12429
+ return type;
12430
+ }
12431
+
12341
12432
  // ente/content.ts
12342
12433
  import { readFileSync as readFileSync7 } from "node:fs";
12343
12434
  function readContentOption(content, contentFile) {
@@ -12716,7 +12807,7 @@ var ActivityCreate = class extends ProfileCommand {
12716
12807
  ownerSpaceId: spaceId,
12717
12808
  grantMissingEntityAccess: this.autoLink,
12718
12809
  ...this.entitySpaceGrantFingerprint ? { entitySpaceGrantFingerprint: this.entitySpaceGrantFingerprint } : {},
12719
- type: this.type,
12810
+ type: parseActivityType(this.type),
12720
12811
  ...this.agentAssignee?.length ? { agentAssigneeIds: this.agentAssignee } : {},
12721
12812
  title: this.title,
12722
12813
  ...content !== void 0 ? { contentMarkdown: content } : {},
@@ -12746,7 +12837,7 @@ var ActivitySearch = class extends ProfileCommand {
12746
12837
  let results = await caller.query(activitiesSearchRich, {
12747
12838
  spaceId,
12748
12839
  query: this.query,
12749
- ...this.type ? { type: this.type } : {},
12840
+ ...this.type ? { type: parseActivityType(this.type) } : {},
12750
12841
  ...this.limit ? { limit: Number(this.limit) } : {}
12751
12842
  });
12752
12843
  if (this.entity) {
@@ -13729,6 +13820,123 @@ var TASK_COMMANDS = [
13729
13820
  // ente/commands/list.ts
13730
13821
  import { readFileSync as readFileSync10 } from "node:fs";
13731
13822
 
13823
+ // ../packages/shared/src/agent-icon.ts
13824
+ var legacyCustomIconIds = [
13825
+ "rocket",
13826
+ "flask-conical",
13827
+ "palette",
13828
+ "atom",
13829
+ "scale",
13830
+ "sticky-note",
13831
+ "notebook-pen",
13832
+ "pen-line",
13833
+ "highlighter",
13834
+ "quote",
13835
+ "list",
13836
+ "list-checks",
13837
+ "clipboard-list",
13838
+ "paperclip",
13839
+ "link",
13840
+ "at-sign",
13841
+ "hash",
13842
+ "calendar",
13843
+ "clock-3",
13844
+ "bell",
13845
+ "mic",
13846
+ "camera",
13847
+ "image",
13848
+ "code",
13849
+ "terminal",
13850
+ "database",
13851
+ "folder-open",
13852
+ "archive",
13853
+ "inbox",
13854
+ "send",
13855
+ "audio-lines",
13856
+ "video",
13857
+ "scan-text",
13858
+ "fingerprint",
13859
+ "circle-dashed",
13860
+ "magnet",
13861
+ "megaphone",
13862
+ "mountain",
13863
+ "shell",
13864
+ "feather",
13865
+ "flower-2"
13866
+ ];
13867
+ var entityCustomIconIds = [
13868
+ "lightbulb",
13869
+ "star",
13870
+ "bookmark",
13871
+ "target",
13872
+ "flag",
13873
+ "brain",
13874
+ "book-open",
13875
+ "puzzle",
13876
+ "zap",
13877
+ "compass",
13878
+ "map",
13879
+ "wrench",
13880
+ "shield",
13881
+ "heart-handshake",
13882
+ "trophy",
13883
+ "globe-2",
13884
+ "network",
13885
+ "landmark",
13886
+ "leaf",
13887
+ "route",
13888
+ "binoculars",
13889
+ "gem",
13890
+ "crown",
13891
+ "key",
13892
+ "satellite",
13893
+ "sun"
13894
+ ];
13895
+ var activityCustomIconIds = [
13896
+ "fish",
13897
+ "bird",
13898
+ "rabbit",
13899
+ "turtle",
13900
+ "squirrel",
13901
+ "snail",
13902
+ "bug",
13903
+ "wand-sparkles",
13904
+ "drama",
13905
+ "music",
13906
+ "dice-5",
13907
+ "gift",
13908
+ "flame",
13909
+ "coffee",
13910
+ "party-popper",
13911
+ "rainbow",
13912
+ "umbrella",
13913
+ "anchor",
13914
+ "sailboat",
13915
+ "gamepad-2",
13916
+ "ghost",
13917
+ "bot",
13918
+ "cloud",
13919
+ "moon",
13920
+ "smile"
13921
+ ];
13922
+ var AGENT_ICON_IDS = [...entityCustomIconIds, ...activityCustomIconIds];
13923
+ function parseAgentIconInput(raw) {
13924
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Expected { icon: curated icon ID or null }");
13925
+ const input = raw;
13926
+ if (Object.keys(input).length !== 1 || !("icon" in input) || input.icon !== null && !AGENT_ICON_IDS.some((icon) => icon === input.icon)) {
13927
+ throw new Error("Expected only icon: a curated icon ID or null; no agent ID is accepted");
13928
+ }
13929
+ return { icon: input.icon };
13930
+ }
13931
+
13932
+ // ente/entityIcon.ts
13933
+ var ICONS = [...AGENT_ICON_IDS, ...legacyCustomIconIds];
13934
+ function parseEntityIcon(value) {
13935
+ const icon = ICONS.find((icon2) => icon2 === value);
13936
+ if (!icon) throw new Error(`Invalid entity icon '${value}'.`);
13937
+ return icon;
13938
+ }
13939
+
13732
13940
  // ente/databaseList.ts
13733
13941
  var COLUMN_TYPES = ["text", "number", "boolean", "single-select", "multi-select"];
13734
13942
  var SELECT_TYPES = /* @__PURE__ */ new Set(["single-select", "multi-select"]);
@@ -13950,6 +14158,12 @@ ${failures.map((e) => ` - ${e instanceof Error ? e.message : String(e)}`).join(
13950
14158
 
13951
14159
  // ente/commands/list.ts
13952
14160
  var DATABASE_LIST_TABS = ["database", "activities"];
14161
+ function assertListIcon(icon, type) {
14162
+ if (type !== "topic")
14163
+ throw new Error(`--icon works only for topic lists; drop it or use --type topic (got '${type}').`);
14164
+ if (!entityCustomIconIds.includes(icon))
14165
+ throw new Error(`'${icon}' is not a list icon. Choose one of: ${entityCustomIconIds.join(", ")}.`);
14166
+ }
13953
14167
  var ROW_PAGE_SIZE = 100;
13954
14168
  var entitiesCreate2 = makeFunctionReference("entities:create");
13955
14169
  var updateDatabaseView = makeFunctionReference("entities:updateDatabaseView");
@@ -14105,7 +14319,7 @@ var ListCreate = class extends ProfileCommand {
14105
14319
  });
14106
14320
  description = options_exports.String("--description", { description: "Searchable description for the list" });
14107
14321
  icon = options_exports.String("--icon", {
14108
- description: "Entity icon as named in the web icon picker (e.g. book-open, leaf, star)"
14322
+ description: `Topic icon from the web icon picker: ${entityCustomIconIds.join(", ")}`
14109
14323
  });
14110
14324
  pin = options_exports.Array("--pin", { description: "Pin the list under parent entities (resolved by name)" });
14111
14325
  shareParents = options_exports.Boolean("--share-parents", false, {
@@ -14119,6 +14333,8 @@ var ListCreate = class extends ProfileCommand {
14119
14333
  throw new Error(`--type must be 'topic' or 'project' (got '${this.type}').`);
14120
14334
  }
14121
14335
  const columns = (this.column ?? []).map(parseColumn);
14336
+ if (this.icon !== void 0) assertListIcon(this.icon, this.type);
14337
+ const icon = this.icon !== void 0 ? parseEntityIcon(this.icon) : void 0;
14122
14338
  const caller = await resolveCaller();
14123
14339
  const spaceId = await resolveSpaceByNameC(caller, this.space);
14124
14340
  const entityId = await caller.mutation(entitiesCreate2, {
@@ -14136,8 +14352,8 @@ var ListCreate = class extends ProfileCommand {
14136
14352
  if (this.description) {
14137
14353
  await caller.mutation(setDescriptionFromMarkdown, { entityId, markdown: this.description });
14138
14354
  }
14139
- if (this.icon) {
14140
- await caller.mutation(setCustomIcon, { id: entityId, icon: this.icon });
14355
+ if (icon) {
14356
+ await caller.mutation(setCustomIcon, { id: entityId, icon });
14141
14357
  }
14142
14358
  const pins = await pinToTargets(caller, entityId, this.pin ?? [], {
14143
14359
  shareParents: this.shareParents,
@@ -15699,72 +15915,6 @@ var RECORDINGS_COMMANDS = [
15699
15915
  RecordingBackupJoinCommand
15700
15916
  ];
15701
15917
 
15702
- // ../packages/shared/src/agent-icon.ts
15703
- var entityCustomIconIds = [
15704
- "lightbulb",
15705
- "star",
15706
- "bookmark",
15707
- "target",
15708
- "flag",
15709
- "brain",
15710
- "book-open",
15711
- "puzzle",
15712
- "zap",
15713
- "compass",
15714
- "map",
15715
- "wrench",
15716
- "shield",
15717
- "heart-handshake",
15718
- "trophy",
15719
- "globe-2",
15720
- "network",
15721
- "landmark",
15722
- "leaf",
15723
- "route",
15724
- "binoculars",
15725
- "gem",
15726
- "crown",
15727
- "key",
15728
- "satellite",
15729
- "sun"
15730
- ];
15731
- var activityCustomIconIds = [
15732
- "fish",
15733
- "bird",
15734
- "rabbit",
15735
- "turtle",
15736
- "squirrel",
15737
- "snail",
15738
- "bug",
15739
- "wand-sparkles",
15740
- "drama",
15741
- "music",
15742
- "dice-5",
15743
- "gift",
15744
- "flame",
15745
- "coffee",
15746
- "party-popper",
15747
- "rainbow",
15748
- "umbrella",
15749
- "anchor",
15750
- "sailboat",
15751
- "gamepad-2",
15752
- "ghost",
15753
- "bot",
15754
- "cloud",
15755
- "moon",
15756
- "smile"
15757
- ];
15758
- var AGENT_ICON_IDS = [...entityCustomIconIds, ...activityCustomIconIds];
15759
- function parseAgentIconInput(raw) {
15760
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Expected { icon: curated icon ID or null }");
15761
- const input = raw;
15762
- if (Object.keys(input).length !== 1 || !("icon" in input) || input.icon !== null && !AGENT_ICON_IDS.some((icon) => icon === input.icon)) {
15763
- throw new Error("Expected only icon: a curated icon ID or null; no agent ID is accepted");
15764
- }
15765
- return { icon: input.icon };
15766
- }
15767
-
15768
15918
  // ente/commands/agent.ts
15769
15919
  import { readFileSync as readFileSync12 } from "node:fs";
15770
15920
  import { randomUUID } from "node:crypto";
@@ -16336,7 +16486,6 @@ var AGENDA_COMMANDS = [
16336
16486
  ];
16337
16487
 
16338
16488
  // ente/commands/feedback.ts
16339
- var CATEGORIES = ["missing", "confusing", "wrong_data", "workflow", "bug", "other"];
16340
16489
  var STATUSES = ["offen", "todo", "bearbeitung", "klaerung", "abgeschlossen", "spaeter"];
16341
16490
  var createFeedback = makeFunctionReference(
16342
16491
  "feedback:create"
@@ -16381,9 +16530,23 @@ async function collectColumn(caller, status, text, limit, scope) {
16381
16530
  }
16382
16531
  return items.slice(0, limit);
16383
16532
  }
16384
- function parseCategory(value) {
16385
- if (CATEGORIES.includes(value)) return value;
16386
- throw new Error(`Invalid category '${value}'. Use one of: ${CATEGORIES.join(" | ")}.`);
16533
+ function isAgentRefusal(err) {
16534
+ const data = err && typeof err === "object" && "data" in err ? err.data : null;
16535
+ return !!data && typeof data === "object" && "code" in data && data.code === "FORBIDDEN" && "message" in data && data.message === "Use the agent-scoped operation interface";
16536
+ }
16537
+ async function humanOnlyBoard(fn) {
16538
+ const caller = await resolveCaller();
16539
+ try {
16540
+ return await fn(caller);
16541
+ } catch (err) {
16542
+ if (!isAgentRefusal(err)) throw err;
16543
+ throw new Error(
16544
+ `The feedback board accepts only people signed in to the web app. This CLI key is bound to an Ente agent, and agents may not act as you there (${errorMessage(err)}). File, comment on or move feedback in the web app's feedback board.`
16545
+ );
16546
+ }
16547
+ }
16548
+ function tagNames(item) {
16549
+ return item.tags.map((t) => t.name).join(", ");
16387
16550
  }
16388
16551
  function parseStatus(value) {
16389
16552
  if (STATUSES.includes(value)) return value;
@@ -16396,10 +16559,9 @@ var FeedbackSendCommand = class extends ProfileCommand {
16396
16559
  static paths = [["feedback", "send"]];
16397
16560
  static usage = Command.Usage({
16398
16561
  category: "Feedback",
16399
- description: "File feedback on the in-app board (category + summary + optional detail fields)"
16562
+ description: "File feedback on the in-app board (summary + optional detail fields; human sign-in only)"
16400
16563
  });
16401
16564
  summary = options_exports.String({ required: true, name: "summary" });
16402
- category = options_exports.String("--category", { description: `Category: ${CATEGORIES.join("|")} (default other)` });
16403
16565
  situation = options_exports.String("--situation", { description: "What happened / where you were" });
16404
16566
  goal = options_exports.String("--goal", { description: "What you were trying to do" });
16405
16567
  needToSee = options_exports.String("--need-to-see", { description: "What you expected to see" });
@@ -16409,10 +16571,7 @@ var FeedbackSendCommand = class extends ProfileCommand {
16409
16571
  async run() {
16410
16572
  const summary = this.summary.trim();
16411
16573
  if (summary.length === 0) throw new Error("feedback send: summary must not be empty.");
16412
- const category = parseCategory(this.category ?? "other");
16413
- const caller = await resolveCaller();
16414
- const res = await caller.mutation(createFeedback, {
16415
- category,
16574
+ const res = await humanOnlyBoard((caller) => caller.mutation(createFeedback, {
16416
16575
  summary,
16417
16576
  ...this.situation !== void 0 ? { situation: this.situation } : {},
16418
16577
  ...this.goal !== void 0 ? { goal: this.goal } : {},
@@ -16421,10 +16580,10 @@ var FeedbackSendCommand = class extends ProfileCommand {
16421
16580
  ...this.reason !== void 0 ? { reason: this.reason } : {},
16422
16581
  ...this.workaround !== void 0 ? { currentWorkaround: this.workaround } : {},
16423
16582
  context: CLI_CONTEXT
16424
- });
16583
+ }));
16425
16584
  this.emitText(
16426
- { data: { feedbackId: res.feedbackId, category, summary } },
16427
- () => `Filed feedback ${res.feedbackId} [${category}] "${summary}".`
16585
+ { data: { feedbackId: res.feedbackId, summary } },
16586
+ () => `Filed feedback ${res.feedbackId} "${summary}".`
16428
16587
  );
16429
16588
  }
16430
16589
  };
@@ -16445,12 +16604,13 @@ var FeedbackListCommand = class extends ProfileCommand {
16445
16604
  }
16446
16605
  const statuses = this.status ? [parseStatus(this.status)] : [...STATUSES];
16447
16606
  const scope = this.all ? "all" : "mine";
16448
- const caller = await resolveCaller();
16449
16607
  const collected = [];
16450
- for (const status of statuses) {
16451
- if (collected.length >= limit) break;
16452
- collected.push(...await collectColumn(caller, status, this.text, limit - collected.length, scope));
16453
- }
16608
+ await humanOnlyBoard(async (caller) => {
16609
+ for (const status of statuses) {
16610
+ if (collected.length >= limit) break;
16611
+ collected.push(...await collectColumn(caller, status, this.text, limit - collected.length, scope));
16612
+ }
16613
+ });
16454
16614
  collected.sort((a, b) => b.createdAt - a.createdAt);
16455
16615
  const items = collected.slice(0, limit);
16456
16616
  this.print(
@@ -16458,12 +16618,12 @@ var FeedbackListCommand = class extends ProfileCommand {
16458
16618
  () => items.map((it) => ({
16459
16619
  id: it.id,
16460
16620
  status: it.status,
16461
- category: it.category,
16621
+ tags: tagNames(it),
16462
16622
  summary: it.summary.length > 60 ? `${it.summary.slice(0, 57)}\u2026` : it.summary,
16463
16623
  by: submitter(it),
16464
16624
  created: formatDatetime(it.createdAt)
16465
16625
  })),
16466
- ["id", "status", "category", "summary", "by", "created"]
16626
+ ["id", "status", "tags", "summary", "by", "created"]
16467
16627
  );
16468
16628
  }
16469
16629
  };
@@ -16475,13 +16635,14 @@ var FeedbackViewCommand = class extends ProfileCommand {
16475
16635
  });
16476
16636
  feedbackId = options_exports.String({ required: true, name: "feedbackId" });
16477
16637
  async run() {
16478
- const caller = await resolveCaller();
16479
- const item = await caller.query(getFeedback, { id: this.feedbackId });
16638
+ const { item, comments } = await humanOnlyBoard(async (caller) => ({
16639
+ item: await caller.query(getFeedback, { id: this.feedbackId }),
16640
+ comments: await caller.query(listComments, { id: this.feedbackId })
16641
+ }));
16480
16642
  if (item === null) throw new Error(`No feedback item with id ${this.feedbackId}.`);
16481
- const comments = await caller.query(listComments, { id: this.feedbackId });
16482
16643
  this.emitText({ data: { item, comments } }, () => {
16483
16644
  const lines = [
16484
- `${item.id} [${item.category}] ${item.status}`,
16645
+ `${item.id} ${item.status}`,
16485
16646
  item.summary,
16486
16647
  "",
16487
16648
  `Submitted by: ${submitter(item)} ${formatDatetime(item.createdAt)}`
@@ -16525,8 +16686,7 @@ var FeedbackCommentCommand = class extends ProfileCommand {
16525
16686
  body = options_exports.String({ required: true, name: "body" });
16526
16687
  async run() {
16527
16688
  if (this.body.trim().length === 0) throw new Error("feedback comment: body must not be empty.");
16528
- const caller = await resolveCaller();
16529
- const comment = await caller.mutation(addComment, { id: this.feedbackId, body: this.body });
16689
+ const comment = await humanOnlyBoard((caller) => caller.mutation(addComment, { id: this.feedbackId, body: this.body }));
16530
16690
  this.emitText(
16531
16691
  { data: comment },
16532
16692
  () => `Added comment ${comment.id} by ${comment.authorName || "(unknown)"}.`
@@ -16543,8 +16703,7 @@ var FeedbackStatusCommand = class extends ProfileCommand {
16543
16703
  status = options_exports.String({ required: true, name: "status" });
16544
16704
  async run() {
16545
16705
  const status = parseStatus(this.status);
16546
- const caller = await resolveCaller();
16547
- await caller.mutation(updateStatus, { id: this.feedbackId, status });
16706
+ await humanOnlyBoard((caller) => caller.mutation(updateStatus, { id: this.feedbackId, status }));
16548
16707
  this.emitText(
16549
16708
  { data: { feedbackId: this.feedbackId, status } },
16550
16709
  () => `Moved feedback ${this.feedbackId} to '${status}'.`