@simplepush/cli 0.3.0 → 0.5.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/dist/main.mjs CHANGED
@@ -9,10 +9,13 @@ import { wordlist } from "@scure/bip39/wordlists/english.js";
9
9
  import { homedir, tmpdir } from "node:os";
10
10
  import { createServer } from "node:http";
11
11
  import { spawn } from "node:child_process";
12
- import { Client, Keyring, OrgClient, TypeFilter, cancelSubtask, cancelTask, cancelTaskGroup, decryptBytes, decryptSubmission, decryptTaskPayload, decryptTaskSummary, deriveKey, encrypt, fetchUserInfo, isSubtaskGroupResponse, tryDecryptEventData } from "@simplepush/sdk";
12
+ import { Client, DownloadError, OrgClient, TypeFilter, decryptSubmission, decryptTaskPayload, decryptTaskSummary, isSubtaskGroupResponse, tryDecryptEventData } from "@simplepush/sdk";
13
13
  import { mkdir, stat, writeFile } from "node:fs/promises";
14
14
  import { connect, createConnection } from "node:net";
15
15
  import path, { basename, extname, join } from "node:path";
16
+ //#region package.json
17
+ var version = "0.5.0";
18
+ //#endregion
16
19
  //#region src/errors.ts
17
20
  /** No CLI session saved — `sp auth login` hasn't been run (or was logged out). */
18
21
  var NotLoggedIn = class extends Data.TaggedError("NotLoggedIn") {};
@@ -1525,7 +1528,7 @@ const loadOrgMasterKeys = Effect.gen(function* () {
1525
1528
  });
1526
1529
  const collectCommand = Command.make("collect", {
1527
1530
  group: Options.text("group").pipe(Options.withDescription("Group id (grptsk_…) to collect over. Usually supplied via the piped `sent` line instead."), Options.optional),
1528
- instance: Options.text("instance").pipe(Options.withDescription("Instance id to collect: a task (tsk_…), a notification (ntf_…), or a subtask-scoped pair (tsk_…/sub_…). Repeatable. Augments/overrides the piped `sent` line's instances."), Options.repeated),
1531
+ instance: Options.text("instance").pipe(Options.withDescription("Instance id to collect: a task (tsk_…), a notification (ntf_…), or a subtask (sub_…, or the tsk_…/sub_… pair). Repeatable. Augments/overrides the piped `sent` line's instances."), Options.repeated),
1529
1532
  replies: Options.boolean("replies").pipe(Options.withDescription("Collect only replies. Default (no mode flag) is the full activity stream: inputs, replies, and completions.")),
1530
1533
  inputs: Options.boolean("inputs").pipe(Options.withDescription("Collect only input events (waits for every instance to complete by default).")),
1531
1534
  submissions: Options.boolean("submissions").pipe(Options.withDescription("Collect submissions (your inbox) instead of a group's events.")),
@@ -1606,14 +1609,16 @@ const collectCommand = Command.make("collect", {
1606
1609
  } : null, m.subtaskId);
1607
1610
  for (const spec of args.instance) {
1608
1611
  const [id, subtaskId] = spec.split("/", 2);
1609
- if (id.startsWith("sub_")) return yield* Effect.fail(new UserError({ message: `a subtask cannot be collected by its id alone (events route under the parent task) — pass --instance <parent tsk_…>/${id}` }));
1610
- addMember(id, null, subtaskId);
1612
+ if (id.startsWith("sub_")) {
1613
+ const parent = yield* sdkCall("read subtask", () => client.getSubtask(id));
1614
+ addMember(String(parent.parentTaskId), null, id);
1615
+ } else addMember(id, null, subtaskId);
1611
1616
  }
1612
- if (members.length === 0) return yield* Effect.fail(new UserError({ message: "no instances to collect: pipe a send's `sent` line (`sp task --format json | sp collect`) or pass --instance <tsk_…|ntf_…|tsk_…/sub_…> (repeatable)" }));
1617
+ if (members.length === 0) return yield* Effect.fail(new UserError({ message: "no instances to collect: pipe a send's `sent` line (`sp task --format json | sp collect`) or pass --instance <tsk_…|ntf_…|sub_…> (repeatable)" }));
1613
1618
  const notifRoster = members[0].kind === "notification";
1614
1619
  if (members.some((m) => m.kind === "notification" !== notifRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix task (tsk_…) and notification (ntf_…) instances in one collect — run one per kind" }));
1615
1620
  const subRoster = members[0].subtaskId !== void 0;
1616
- if (members.some((m) => m.subtaskId !== void 0 !== subRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix subtask-scoped instances (a piped `sp subtask --format json` line or --instance tsk_…/sub_…) with plain task/notification instances in one collect" }));
1621
+ if (members.some((m) => m.subtaskId !== void 0 !== subRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix subtask-scoped instances (a piped `sp subtask --format json` line or --instance sub_…) with plain task/notification instances in one collect" }));
1617
1622
  const streamOpts = {
1618
1623
  replay: true,
1619
1624
  ...until.idleMs !== void 0 ? { idleMs: until.idleMs } : {}
@@ -1782,7 +1787,7 @@ const reasonOption = Options.choice("reason", [
1782
1787
  "answered",
1783
1788
  "superseded"
1784
1789
  ]).pipe(Options.withDescription("Why: plain withdrawal (default), another recipient's answer made the rest moot (`answered`), or a replacement exists (`superseded`)."), Options.withDefault("canceled"));
1785
- const noteOption = Options.text("note").pipe(Options.withDescription("Free-text explanation shown on the recipients' canceled card. Encrypted under the org master key on the org path (default when enabled); on the personal path seal it with --password pw@topic (topic send) or a bare --password (self-send, account default key)."), Options.optional);
1790
+ const noteOption = Options.text("note").pipe(Options.withDescription("Free-text explanation shown on the recipients' canceled card. Sealed under the target's key when this CLI holds it: the org vault on an org session, or the matching -p/--password (`pw@topic`, or a bare `pw` for a self-send)."), Options.optional);
1786
1791
  const supersededByOption = Options.text("superseded-by").pipe(Options.withDescription("The replacement's id — a task id, a subtask id of the same chain, or (for a group cancel) the replacement group id. Requires --reason superseded."), Options.optional);
1787
1792
  const noEncryptOption$3 = Options.boolean("no-encrypt").pipe(Options.withDescription("For org cancels: send the note in plaintext even when the org vault is unlocked."));
1788
1793
  const cancelCommand = Command.make("cancel", {
@@ -1801,89 +1806,46 @@ const cancelCommand = Command.make("cancel", {
1801
1806
  const note = Option.getOrUndefined(args.note);
1802
1807
  const supersededBy = Option.getOrUndefined(args["superseded-by"]);
1803
1808
  if (supersededBy !== void 0 && args.reason !== "superseded") return yield* Effect.fail(new UserError({ message: "--superseded-by requires --reason superseded" }));
1804
- const credential = yield* resolveCredential(args["api-token"], args["base-url"]);
1805
- let noteFields = {};
1806
- if (note !== void 0) if (credential.kind === "org") {
1807
- const vault = yield* (yield* VaultAccess).forSendOrPlaintext(args["no-encrypt"]);
1808
- if (vault) noteFields = {
1809
- note: yield* sdkCall("encrypt note", () => encrypt(vault.masterKeyCurrent.key, note)),
1810
- encryption: {
1811
- type: "org",
1812
- v: vault.masterKeyCurrent.version
1813
- }
1809
+ const kind = args.id.startsWith("grptsk_") ? "group" : args.id.startsWith("tsk_") ? "task" : args.id.startsWith("sub_") ? "subtask" : void 0;
1810
+ if (kind === void 0) return yield* Effect.fail(new UserError({ message: `cannot cancel \`${args.id}\`: expected a tsk_…, sub_…, or grptsk_… id` }));
1811
+ const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
1812
+ const orgKeys = cred.kind === "org" && !args["no-encrypt"] ? yield* loadOrgMasterKeys : void 0;
1813
+ return yield* Effect.scoped(Effect.gen(function* () {
1814
+ const client = cred.kind === "personal" ? yield* acquireClient({
1815
+ baseUrl: cred.baseUrl,
1816
+ apiToken: cred.apiToken,
1817
+ passwords: args.password
1818
+ }) : yield* acquireOrgClient({
1819
+ baseUrl: cred.baseUrl,
1820
+ bearerToken: cred.bearer,
1821
+ ...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
1822
+ });
1823
+ const opts = {
1824
+ reason: args.reason,
1825
+ ...note !== void 0 ? { note } : {},
1826
+ ...supersededBy !== void 0 ? { supersededBy } : {}
1814
1827
  };
1815
- else {
1816
- noteFields = { note };
1817
- yield* out.warn("note sent unencrypted (org encryption disabled or --no-encrypt)");
1828
+ if (note !== void 0) {
1829
+ const marker = yield* sdkCall("read cancel target", async () => kind === "task" ? (await client.getTask(args.id)).encryption : kind === "subtask" ? (await client.getSubtask(args.id)).encryption : (await client.getTaskGroup(args.id)).tasks[0]?.encryption);
1830
+ if (marker !== void 0) {
1831
+ if ((yield* sdkCall("keyring", async () => (await client.keyring({ includePasswordSalt: marker.type === "personal" })).keyForMarker(marker))) === void 0) yield* out.warn(marker.type === "org" ? "note sent unencrypted (org vault locked, or --no-encrypt)" : "note sent unencrypted — seal it with --password <pw>@<topic> (topic send) or a bare --password (self-send)");
1832
+ }
1818
1833
  }
1819
- } else {
1820
- const passwords = args.password;
1821
- if (passwords.length > 1) return yield* Effect.fail(new UserError({ message: "pass exactly one --password so the note's key is unambiguous" }));
1822
- const flag = passwords[0];
1823
- if (flag === void 0) {
1824
- noteFields = { note };
1825
- yield* out.warn("note sent unencrypted — seal it with --password <pw>@<topic> (topic send) or a bare --password (self-send)");
1826
- } else if (Array.isArray(flag)) {
1827
- const derived = yield* sdkCall("derive note key", () => deriveKey(flag[0], flag[1]));
1828
- noteFields = {
1829
- note: yield* sdkCall("encrypt note", () => encrypt(derived.symmetricKey, note)),
1830
- encryption: {
1831
- type: "personal",
1832
- keyFingerprint: derived.fingerprint
1833
- }
1834
- };
1835
- } else {
1836
- const info = yield* sdkCall("fetch password salt", () => fetchUserInfo({
1837
- baseUrl: new URL(credential.baseUrl),
1838
- apiToken: credential.apiToken,
1839
- fetch
1834
+ if (kind === "group") {
1835
+ const result = yield* sdkCall("cancel task group", () => client.cancelTaskGroup(args.id, opts));
1836
+ yield* out.print(JSON.stringify({
1837
+ type: "canceled",
1838
+ groupId: args.id,
1839
+ ...result
1840
1840
  }));
1841
- const derived = yield* sdkCall("derive note key", () => deriveKey(flag, info.passwordSalt));
1842
- noteFields = {
1843
- note: yield* sdkCall("encrypt note", () => encrypt(derived.symmetricKey, note)),
1844
- encryption: {
1845
- type: "personal",
1846
- keyFingerprint: derived.fingerprint
1847
- }
1848
- };
1841
+ } else if (kind === "task") {
1842
+ yield* sdkCall("cancel task", () => client.cancelTask(args.id, opts));
1843
+ yield* out.info(`canceled ${args.id}`);
1844
+ } else {
1845
+ yield* sdkCall("cancel subtask", () => client.cancelSubtask(args.id, opts));
1846
+ yield* out.info(`canceled ${args.id}`);
1849
1847
  }
1850
- }
1851
- const body = {
1852
- reason: args.reason,
1853
- ...noteFields,
1854
- ...supersededBy !== void 0 ? { supersededBy } : {}
1855
- };
1856
- const baseUrl = new URL(credential.baseUrl);
1857
- const authHeaders = credential.kind === "personal" ? { "API-Token": credential.apiToken } : { Authorization: `Bearer ${credential.bearer}` };
1858
- if (args.id.startsWith("grptsk_")) {
1859
- const result = yield* sdkCall("cancel task group", () => cancelTaskGroup({
1860
- baseUrl,
1861
- groupId: args.id,
1862
- body,
1863
- authHeaders
1864
- }));
1865
- yield* out.print(JSON.stringify({
1866
- type: "canceled",
1867
- groupId: args.id,
1868
- ...result
1869
- }));
1870
- } else if (args.id.startsWith("tsk_")) {
1871
- yield* sdkCall("cancel task", () => cancelTask({
1872
- baseUrl,
1873
- taskId: args.id,
1874
- body,
1875
- authHeaders
1876
- }));
1877
- yield* out.info(`canceled ${args.id}`);
1878
- } else if (args.id.startsWith("sub_")) {
1879
- yield* sdkCall("cancel subtask", () => cancelSubtask({
1880
- baseUrl,
1881
- subtaskId: args.id,
1882
- body,
1883
- authHeaders
1884
- }));
1885
- yield* out.info(`canceled ${args.id}`);
1886
- } else return yield* Effect.fail(new UserError({ message: `cannot cancel \`${args.id}\`: expected a tsk_…, sub_…, or grptsk_… id` }));
1848
+ }));
1887
1849
  })).pipe(Command.withDescription("Cancel a pending task, subtask, or task group you sent (sender-side withdrawal)."));
1888
1850
  //#endregion
1889
1851
  //#region src/commands/daemon.ts
@@ -1916,6 +1878,133 @@ const daemonCommand = Command.make("daemon", {
1916
1878
  });
1917
1879
  }));
1918
1880
  //#endregion
1881
+ //#region src/commands/query-support.ts
1882
+ /** Options every read command shares: credential, decryption keys, paging, output. */
1883
+ const credentialOptions = {
1884
+ "api-token": apiTokenOption,
1885
+ password: passwordOption,
1886
+ "base-url": baseUrlOption,
1887
+ quiet: quietOption
1888
+ };
1889
+ const formatOption$4 = Options.choice("format", ["json", "pretty"]).pipe(Options.withDefault("json"), Options.withDescription("json = one JSON object per line (agents, jq); pretty = human-readable lines."));
1890
+ const sinceOption$1 = mappedText("since", resolveSince).pipe(Options.optional, Options.withDescription("Only items created at or after this instant: ISO-8601, or a relative window like 7d, 12h, 30m."));
1891
+ const untilOption$1 = mappedText("until", (raw) => resolveUntil(raw).toISOString()).pipe(Options.optional, Options.withDescription("Only items created at or before this instant (same forms as --since)."));
1892
+ const memberOption$3 = Options.text("member").pipe(Options.optional, Options.withDescription("Only items involving this person: a usr_ id, or a member name on an org."));
1893
+ const limitOption$2 = Options.integer("limit").pipe(Options.optional, Options.withDescription("Page size (server default 50). With --all this is the page size, not a cap."));
1894
+ const cursorOption = Options.text("cursor").pipe(Options.optional, Options.withDescription("Continue a previous page: pass the cursor it reported."));
1895
+ const allOption = Options.boolean("all").pipe(Options.withDescription("Follow cursors until the last page instead of printing one page and a cursor."));
1896
+ const STATUSES = [
1897
+ "pending",
1898
+ "completed",
1899
+ "canceled",
1900
+ "declined",
1901
+ "expired"
1902
+ ];
1903
+ /** `--status` accepts repeats and comma lists: `--status expired --status declined` or `--status expired,declined`. */
1904
+ const statusOption = Options.text("status").pipe(Options.repeated, Options.mapTryCatch((values) => {
1905
+ const flat = values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
1906
+ const bad = flat.filter((s) => !STATUSES.includes(s));
1907
+ if (bad.length > 0) throw new Error(`unknown status ${bad.join(", ")} — use ${STATUSES.join(", ")}`);
1908
+ return flat;
1909
+ }, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.withDescription("Only tasks in these states (repeatable or comma-separated)."));
1910
+ /** Resolves the credential (personal API-Token or the saved org session),
1911
+ * builds the matching client and — when keys are available — a keyring, and
1912
+ * runs `body` inside the client's scope. Mirrors what `sp events` does. */
1913
+ const withQueryClient = (args, body) => Effect.gen(function* () {
1914
+ const out = yield* CliOutput;
1915
+ yield* out.setQuiet(args.quiet);
1916
+ const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
1917
+ const orgKeys = cred.kind === "org" ? yield* loadOrgMasterKeys : void 0;
1918
+ return yield* Effect.scoped(Effect.gen(function* () {
1919
+ const client = cred.kind === "personal" ? yield* acquireClient({
1920
+ baseUrl: cred.baseUrl,
1921
+ apiToken: cred.apiToken,
1922
+ passwords: args.password
1923
+ }) : yield* acquireOrgClient({
1924
+ baseUrl: cred.baseUrl,
1925
+ bearerToken: cred.bearer,
1926
+ ...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
1927
+ });
1928
+ return yield* body({
1929
+ client,
1930
+ keyring: cred.kind === "org" ? orgKeys !== void 0 ? yield* sdkCall("keyring", () => client.keyring()) : void 0 : args.password.length > 0 ? yield* sdkCall("keyring", () => client.keyring({ includePasswordSalt: true })) : void 0,
1931
+ org: cred.kind === "org",
1932
+ out
1933
+ });
1934
+ }));
1935
+ });
1936
+ /** Decrypts what the keyring can and counts what it cannot, so a command can
1937
+ * report "N values left as ciphertext" once instead of per row. Each method
1938
+ * maps one wire shape onto its SDK field-schema decryptor. */
1939
+ var Decryptor = class {
1940
+ undecryptable = 0;
1941
+ constructor(keyring) {
1942
+ this.keyring = keyring;
1943
+ }
1944
+ run(value, go) {
1945
+ if (this.keyring === void 0) return Effect.succeed(value);
1946
+ const keyring = this.keyring;
1947
+ return Effect.promise(() => go(keyring)).pipe(Effect.map((r) => {
1948
+ this.undecryptable += r.undecryptable;
1949
+ return r.value;
1950
+ }));
1951
+ }
1952
+ /** A task or subtask payload (chain reads). */
1953
+ payload(value) {
1954
+ return this.run(value, (kr) => decryptTaskPayload(value, kr));
1955
+ }
1956
+ /** A task index / roster row. */
1957
+ summary(value) {
1958
+ return this.run(value, (kr) => decryptTaskSummary(value, kr));
1959
+ }
1960
+ /** A submission under its feed entry's marker. */
1961
+ submission(value, marker) {
1962
+ return this.run(value, (kr) => decryptSubmission(value, kr, marker));
1963
+ }
1964
+ report(out) {
1965
+ return this.undecryptable > 0 ? out.warn(`${this.undecryptable} value(s) are encrypted under a key this CLI does not hold and were left as ciphertext`) : Effect.void;
1966
+ }
1967
+ };
1968
+ /** Runs `page` for the first cursor, then keeps following cursors when `all`
1969
+ * is set; otherwise reports the cursor of the next page so the caller can
1970
+ * continue by hand. */
1971
+ const forEachPage = (ctx, format, all, first, page, handle) => Effect.gen(function* () {
1972
+ let cursor = first;
1973
+ for (;;) {
1974
+ const p = yield* page(cursor);
1975
+ yield* handle(p);
1976
+ if (p.nextCursor === void 0) return;
1977
+ if (!all) {
1978
+ yield* format === "json" ? ctx.out.print(JSON.stringify({
1979
+ type: "more",
1980
+ cursor: p.nextCursor
1981
+ })) : ctx.out.info(`more available — continue with --cursor ${p.nextCursor} (or pass --all)`);
1982
+ return;
1983
+ }
1984
+ cursor = p.nextCursor;
1985
+ }
1986
+ });
1987
+ function recipientLabels(t) {
1988
+ return t.recipients.map((r) => r.name ?? r.publicId);
1989
+ }
1990
+ /** One line per task: id, state, when, title, who, subtask counts, group. */
1991
+ function formatTaskSummary(t) {
1992
+ const subs = Object.entries(t.subtasks).map(([k, v]) => `${k} ${v}`).join(", ");
1993
+ return [
1994
+ `${t.taskId} [${t.status}] ${t.createdAt}`,
1995
+ t.title !== void 0 ? ` ${t.title}` : void 0,
1996
+ ` to: ${recipientLabels(t).join(", ") || "-"}`,
1997
+ t.topic !== void 0 ? ` topic: ${t.topic}` : void 0,
1998
+ t.tag !== void 0 ? ` tag: ${t.tag}` : void 0,
1999
+ t.inputs.length > 0 ? ` inputs: ${t.inputs.join(", ")}` : void 0,
2000
+ t.attachments.length > 0 ? ` attachments: ${t.attachments.join(", ")}` : void 0,
2001
+ t.reply !== void 0 ? ` reply: ${t.reply}` : void 0,
2002
+ subs ? ` subtasks: ${subs}` : void 0,
2003
+ t.expiresAt !== void 0 ? ` expires: ${t.expiresAt}` : void 0,
2004
+ t.groupId !== void 0 ? ` group: ${t.groupId}` : void 0
2005
+ ].filter((l) => l !== void 0).join("\n");
2006
+ }
2007
+ //#endregion
1919
2008
  //#region src/commands/download.ts
1920
2009
  const ID_GLOSSARY = "(ids: tsk_ task, sub_ subtask, sbm_ submission; files: inp_ input upload, rfl_ reply file, sbf_ submission file)";
1921
2010
  const resolveScope = (scopeId, fileId) => {
@@ -1927,11 +2016,6 @@ const resolveScope = (scopeId, fileId) => {
1927
2016
  if (wantsSubmission !== (scope === "submission")) return Effect.fail(new UserError({ message: wantsSubmission ? `${fileId} is a submission file — pass its sbm_… id, not ${scopeId} ${ID_GLOSSARY}` : `${fileId} lives on a task chain — pass the tsk_… or sub_… id it belongs to, not ${scopeId} ${ID_GLOSSARY}` }));
1928
2017
  return Effect.succeed(scope);
1929
2018
  };
1930
- const downloadUrlPath = (scope, scopeId, fileId) => {
1931
- if (scope === "submission") return `/v1/submissions/${scopeId}/files/${fileId}/download-url`;
1932
- const kind = fileId.startsWith("inp_") ? "inputs" : "replies";
1933
- return `/v1/${scope === "task" ? "tasks" : "subtasks"}/${scopeId}/${kind}/${fileId}/download-url`;
1934
- };
1935
2019
  /** Resolve where to write: an explicit file path, an existing directory (the
1936
2020
  * server-declared filename inside it), or the current directory. */
1937
2021
  const resolveTarget = async (outArg, filename) => {
@@ -1953,94 +2037,31 @@ const downloadCommand = Command.make("download", {
1953
2037
  "base-url": baseUrlOption,
1954
2038
  quiet: quietOption
1955
2039
  }, (args) => Effect.gen(function* () {
1956
- const out = yield* CliOutput;
1957
- yield* out.setQuiet(args.quiet);
1958
- const scope = yield* resolveScope(args.scopeId, args.fileId);
1959
- const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
1960
- const orgKeys = cred.kind === "org" ? yield* loadOrgMasterKeys : void 0;
1961
- const baseUrl = cred.baseUrl.replace(/\/+$/, "");
1962
- const authHeaders = cred.kind === "personal" ? { "API-Token": cred.apiToken } : { Authorization: `Bearer ${cred.bearer}` };
1963
- const meta = yield* Effect.tryPromise({
1964
- try: async () => {
1965
- const res = await fetch(`${baseUrl}${downloadUrlPath(scope, args.scopeId, args.fileId)}`, {
1966
- method: "POST",
1967
- headers: authHeaders
1968
- });
1969
- if (!res.ok) {
1970
- const body = await res.text().catch(() => "");
1971
- let msg = `download-url failed (${res.status})`;
1972
- try {
1973
- const parsed = JSON.parse(body);
1974
- if (parsed.msg) msg = `${msg}: ${parsed.msg}`;
1975
- } catch {
1976
- if (body) msg = `${msg}: ${body.slice(0, 200)}`;
1977
- }
1978
- throw new Error(msg);
1979
- }
1980
- return await res.json();
1981
- },
1982
- catch: (e) => new UserError({ message: e instanceof Error ? e.message : String(e) })
1983
- });
1984
- const stored = yield* Effect.tryPromise({
1985
- try: async () => {
1986
- const res = await fetch(meta.presignedGetUrl);
1987
- if (!res.ok) throw new Error(`fetching the file failed (${res.status})`);
1988
- return new Uint8Array(await res.arrayBuffer());
1989
- },
1990
- catch: (e) => new UserError({ message: e instanceof Error ? e.message : String(e) })
1991
- });
1992
- if (meta.checksumSha256 !== void 0) {
1993
- const actual = createHash("sha256").update(stored).digest("base64");
1994
- if (actual !== meta.checksumSha256) return yield* Effect.fail(new UserError({ message: `checksum mismatch: stored blob hashes to ${actual}, server declared ${meta.checksumSha256}` }));
1995
- }
1996
- let bytes = stored;
1997
- if (meta.encryption !== void 0) {
1998
- const passwords = [...args.password];
1999
- const passwordSalt = passwords.filter((p) => typeof p === "string").length > 0 && cred.kind === "personal" ? yield* Effect.tryPromise({
2000
- try: async () => (await fetchUserInfo({
2001
- baseUrl: new URL(baseUrl),
2002
- apiToken: cred.apiToken,
2003
- fetch
2004
- })).passwordSalt,
2005
- catch: (e) => new UserError({ message: `fetching the password salt failed: ${e instanceof Error ? e.message : String(e)}` })
2006
- }) : void 0;
2007
- const key = yield* Effect.tryPromise({
2040
+ yield* resolveScope(args.scopeId, args.fileId);
2041
+ return yield* withQueryClient(args, (ctx) => Effect.gen(function* () {
2042
+ const file = yield* Effect.tryPromise({
2043
+ try: () => ctx.client.downloadFile(args.scopeId, args.fileId),
2044
+ catch: (e) => new UserError({ message: e instanceof DownloadError && e.message.includes("no matching key") ? ctx.org ? "the file is sealed under an org master key — unlock the vault (org session) to decrypt" : "the file is encrypted — pass the matching -p/--password (`pw@topic`, or a bare account password)" : e instanceof Error ? e.message : String(e) })
2045
+ });
2046
+ const filename = file.filename ?? args.fileId;
2047
+ const target = yield* Effect.tryPromise({
2008
2048
  try: async () => {
2009
- const ring = await Keyring.build({
2010
- passwords: [],
2011
- topics: [],
2012
- ...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
2013
- });
2014
- for (const p of passwords) if (typeof p !== "string") ring.add(await deriveKey(p[0], p[1]));
2015
- else if (passwordSalt !== void 0) ring.add(await deriveKey(p, passwordSalt));
2016
- return ring.keyForMarker(meta.encryption);
2049
+ const t = await resolveTarget(Option.getOrUndefined(args.out), filename);
2050
+ await writeFile(t, file.bytes);
2051
+ return t;
2017
2052
  },
2018
- catch: (e) => new UserError({ message: `building the keyring failed: ${e instanceof Error ? e.message : String(e)}` })
2053
+ catch: (e) => new UserError({ message: `saving failed: ${e instanceof Error ? e.message : String(e)}` })
2019
2054
  });
2020
- if (key === void 0) return yield* Effect.fail(new UserError({ message: meta.encryption.type === "org" ? `the file is sealed under org master_key v${meta.encryption.v} — unlock the vault (org session) to decrypt` : "the file is encrypted — pass the matching -p/--password (`pw@topic`, or a bare account password)" }));
2021
- bytes = yield* Effect.tryPromise({
2022
- try: () => decryptBytes(key, stored),
2023
- catch: (e) => new UserError({ message: `decryption failed: ${e instanceof Error ? e.message : String(e)}` })
2024
- });
2025
- }
2026
- const filename = meta.filename ?? args.fileId;
2027
- const target = yield* Effect.tryPromise({
2028
- try: async () => {
2029
- const t = await resolveTarget(Option.getOrUndefined(args.out), filename);
2030
- await writeFile(t, bytes);
2031
- return t;
2032
- },
2033
- catch: (e) => new UserError({ message: `saving failed: ${e instanceof Error ? e.message : String(e)}` })
2034
- });
2035
- if (args.format === "json") yield* out.print(JSON.stringify({
2036
- type: "downloaded",
2037
- id: args.fileId,
2038
- path: target,
2039
- filename: meta.filename ?? null,
2040
- contentType: meta.contentType ?? null,
2041
- size: meta.size ?? null
2055
+ if (args.format === "json") yield* ctx.out.print(JSON.stringify({
2056
+ type: "downloaded",
2057
+ id: args.fileId,
2058
+ path: target,
2059
+ filename: file.filename ?? null,
2060
+ contentType: file.contentType ?? null,
2061
+ size: file.size ?? null
2062
+ }));
2063
+ else yield* ctx.out.print(target);
2042
2064
  }));
2043
- else yield* out.print(target);
2044
2065
  }));
2045
2066
  //#endregion
2046
2067
  //#region src/output.ts
@@ -2099,12 +2120,12 @@ function extractRaw(v) {
2099
2120
  //#endregion
2100
2121
  //#region src/commands/events.ts
2101
2122
  const eventTypeOption = Options.text("type").pipe(Options.withDescription("Filter by event type. Repeatable."), Options.repeated);
2102
- const sinceOption$1 = mappedText("since", resolveSince).pipe(Options.withDescription("Replay from this point. Accepts `24h`, `7d`, or an ISO 8601 timestamp."), Options.optional);
2103
- const untilOption$1 = mappedText("until", resolveUntil).pipe(Options.withDescription("Stop at this timestamp. Forces a finite range, so `--follow` is ignored."), Options.optional);
2123
+ const sinceOption = mappedText("since", resolveSince).pipe(Options.withDescription("Replay from this point. Accepts `24h`, `7d`, or an ISO 8601 timestamp."), Options.optional);
2124
+ const untilOption = mappedText("until", resolveUntil).pipe(Options.withDescription("Stop at this timestamp. Forces a finite range, so `--follow` is ignored."), Options.optional);
2104
2125
  const limitOption$1 = Options.integer("limit").pipe(Options.withDescription("Maximum number of events to print, then exit."), Options.optional);
2105
2126
  const followOption = Options.boolean("follow").pipe(Options.withAlias("f"), Options.withDescription("After history is drained, keep streaming live instead of exiting. Only meaningful with `--since`."));
2106
- const memberOption$3 = Options.text("member").pipe(Options.optional, Options.withDescription("Only events by this person: a usr_ id, or the actor's name."));
2107
- const formatOption$4 = Options.choice("format", [
2127
+ const memberOption$2 = Options.text("member").pipe(Options.optional, Options.withDescription("Only events by this person: a usr_ id, or the actor's name."));
2128
+ const formatOption$3 = Options.choice("format", [
2108
2129
  "json",
2109
2130
  "pretty",
2110
2131
  "raw"
@@ -2113,12 +2134,12 @@ const directOption = Options.boolean("direct").pipe(Options.withDescription("Ope
2113
2134
  const QUIET_EXIT_MS = 2e3;
2114
2135
  const eventsCommand = Command.make("events", {
2115
2136
  type: eventTypeOption,
2116
- member: memberOption$3,
2117
- since: sinceOption$1,
2118
- until: untilOption$1,
2137
+ member: memberOption$2,
2138
+ since: sinceOption,
2139
+ until: untilOption,
2119
2140
  limit: limitOption$1,
2120
2141
  follow: followOption,
2121
- format: formatOption$4,
2142
+ format: formatOption$3,
2122
2143
  direct: directOption,
2123
2144
  topic: topicOption$1,
2124
2145
  "api-token": apiTokenOption,
@@ -2216,130 +2237,6 @@ const eventsCommand = Command.make("events", {
2216
2237
  }));
2217
2238
  }));
2218
2239
  //#endregion
2219
- //#region src/commands/query-support.ts
2220
- /** Options every read command shares: credential, decryption keys, paging, output. */
2221
- const credentialOptions = {
2222
- "api-token": apiTokenOption,
2223
- password: passwordOption,
2224
- "base-url": baseUrlOption,
2225
- quiet: quietOption
2226
- };
2227
- const formatOption$3 = Options.choice("format", ["json", "pretty"]).pipe(Options.withDefault("json"), Options.withDescription("json = one JSON object per line (agents, jq); pretty = human-readable lines."));
2228
- const sinceOption = mappedText("since", resolveSince).pipe(Options.optional, Options.withDescription("Only items created at or after this instant: ISO-8601, or a relative window like 7d, 12h, 30m."));
2229
- const untilOption = mappedText("until", (raw) => resolveUntil(raw).toISOString()).pipe(Options.optional, Options.withDescription("Only items created at or before this instant (same forms as --since)."));
2230
- const memberOption$2 = Options.text("member").pipe(Options.optional, Options.withDescription("Only items involving this person: a usr_ id, or a member name on an org."));
2231
- const limitOption = Options.integer("limit").pipe(Options.optional, Options.withDescription("Page size (server default 50). With --all this is the page size, not a cap."));
2232
- const cursorOption = Options.text("cursor").pipe(Options.optional, Options.withDescription("Continue a previous page: pass the cursor it reported."));
2233
- const allOption = Options.boolean("all").pipe(Options.withDescription("Follow cursors until the last page instead of printing one page and a cursor."));
2234
- const STATUSES = [
2235
- "pending",
2236
- "completed",
2237
- "canceled",
2238
- "declined",
2239
- "expired"
2240
- ];
2241
- /** `--status` accepts repeats and comma lists: `--status expired --status declined` or `--status expired,declined`. */
2242
- const statusOption = Options.text("status").pipe(Options.repeated, Options.mapTryCatch((values) => {
2243
- const flat = values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
2244
- const bad = flat.filter((s) => !STATUSES.includes(s));
2245
- if (bad.length > 0) throw new Error(`unknown status ${bad.join(", ")} — use ${STATUSES.join(", ")}`);
2246
- return flat;
2247
- }, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.withDescription("Only tasks in these states (repeatable or comma-separated)."));
2248
- /** Resolves the credential (personal API-Token or the saved org session),
2249
- * builds the matching client and — when keys are available — a keyring, and
2250
- * runs `body` inside the client's scope. Mirrors what `sp events` does. */
2251
- const withQueryClient = (args, body) => Effect.gen(function* () {
2252
- const out = yield* CliOutput;
2253
- yield* out.setQuiet(args.quiet);
2254
- const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
2255
- const orgKeys = cred.kind === "org" ? yield* loadOrgMasterKeys : void 0;
2256
- return yield* Effect.scoped(Effect.gen(function* () {
2257
- const client = cred.kind === "personal" ? yield* acquireClient({
2258
- baseUrl: cred.baseUrl,
2259
- apiToken: cred.apiToken,
2260
- passwords: args.password
2261
- }) : yield* acquireOrgClient({
2262
- baseUrl: cred.baseUrl,
2263
- bearerToken: cred.bearer,
2264
- ...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
2265
- });
2266
- return yield* body({
2267
- client,
2268
- keyring: cred.kind === "org" ? orgKeys !== void 0 ? yield* sdkCall("keyring", () => client.keyring()) : void 0 : args.password.length > 0 ? yield* sdkCall("keyring", () => client.keyring({ includePasswordSalt: true })) : void 0,
2269
- org: cred.kind === "org",
2270
- out
2271
- });
2272
- }));
2273
- });
2274
- /** Decrypts what the keyring can and counts what it cannot, so a command can
2275
- * report "N values left as ciphertext" once instead of per row. Each method
2276
- * maps one wire shape onto its SDK field-schema decryptor. */
2277
- var Decryptor = class {
2278
- undecryptable = 0;
2279
- constructor(keyring) {
2280
- this.keyring = keyring;
2281
- }
2282
- run(value, go) {
2283
- if (this.keyring === void 0) return Effect.succeed(value);
2284
- const keyring = this.keyring;
2285
- return Effect.promise(() => go(keyring)).pipe(Effect.map((r) => {
2286
- this.undecryptable += r.undecryptable;
2287
- return r.value;
2288
- }));
2289
- }
2290
- /** A task or subtask payload (chain reads). */
2291
- payload(value) {
2292
- return this.run(value, (kr) => decryptTaskPayload(value, kr));
2293
- }
2294
- /** A task index / roster row. */
2295
- summary(value) {
2296
- return this.run(value, (kr) => decryptTaskSummary(value, kr));
2297
- }
2298
- /** A submission under its feed entry's marker. */
2299
- submission(value, marker) {
2300
- return this.run(value, (kr) => decryptSubmission(value, kr, marker));
2301
- }
2302
- report(out) {
2303
- return this.undecryptable > 0 ? out.warn(`${this.undecryptable} value(s) are encrypted under a key this CLI does not hold and were left as ciphertext`) : Effect.void;
2304
- }
2305
- };
2306
- /** Runs `page` for the first cursor, then keeps following cursors when `all`
2307
- * is set; otherwise reports the cursor of the next page so the caller can
2308
- * continue by hand. */
2309
- const forEachPage = (ctx, format, all, first, page, handle) => Effect.gen(function* () {
2310
- let cursor = first;
2311
- for (;;) {
2312
- const p = yield* page(cursor);
2313
- yield* handle(p);
2314
- if (p.nextCursor === void 0) return;
2315
- if (!all) {
2316
- yield* format === "json" ? ctx.out.print(JSON.stringify({
2317
- type: "more",
2318
- cursor: p.nextCursor
2319
- })) : ctx.out.info(`more available — continue with --cursor ${p.nextCursor} (or pass --all)`);
2320
- return;
2321
- }
2322
- cursor = p.nextCursor;
2323
- }
2324
- });
2325
- function recipientLabels(t) {
2326
- return t.recipients.map((r) => r.name ?? r.publicId);
2327
- }
2328
- /** One line per task: id, state, when, title, who, subtask counts, group. */
2329
- function formatTaskSummary(t) {
2330
- const subs = Object.entries(t.subtasks).map(([k, v]) => `${k} ${v}`).join(", ");
2331
- return [
2332
- `${t.taskId} [${t.status}] ${t.createdAt}`,
2333
- t.title !== void 0 ? ` ${t.title}` : void 0,
2334
- ` to: ${recipientLabels(t).join(", ") || "-"}`,
2335
- t.inputs.length > 0 ? ` inputs: ${t.inputs.join(", ")}` : void 0,
2336
- t.reply !== void 0 ? ` reply: ${t.reply}` : void 0,
2337
- subs ? ` subtasks: ${subs}` : void 0,
2338
- t.expiresAt !== void 0 ? ` expires: ${t.expiresAt}` : void 0,
2339
- t.groupId !== void 0 ? ` group: ${t.groupId}` : void 0
2340
- ].filter((l) => l !== void 0).join("\n");
2341
- }
2342
- //#endregion
2343
2240
  //#region src/commands/get.ts
2344
2241
  const topicOption = Options.text("topic").pipe(Options.optional, Options.withDescription("Only tasks sent to this topic: its value (name), or its id."));
2345
2242
  const groupOption = Options.text("group").pipe(Options.optional, Options.withDescription("Only the instances of this grptsk_ group."));
@@ -2387,6 +2284,20 @@ function getChain(args, ctx) {
2387
2284
  yield* dec.report(ctx.out);
2388
2285
  });
2389
2286
  }
2287
+ function getSubtaskRead(args, ctx) {
2288
+ return Effect.gen(function* () {
2289
+ yield* rejectListFlags(args, "one subtask", false);
2290
+ const subtask = yield* sdkCall("read subtask", () => ctx.client.getSubtask(args.what));
2291
+ const dec = new Decryptor(ctx.keyring);
2292
+ const view = yield* dec.payload(subtask);
2293
+ if (args.format === "json") yield* ctx.out.print(JSON.stringify(view));
2294
+ else {
2295
+ yield* ctx.out.print(`${view.subtaskId} [${view.status}] parent ${String(view.parentTaskId ?? "-")}`);
2296
+ yield* ctx.out.print(JSON.stringify(view, null, 2));
2297
+ }
2298
+ yield* dec.report(ctx.out);
2299
+ });
2300
+ }
2390
2301
  function getRoster(args, ctx) {
2391
2302
  return Effect.gen(function* () {
2392
2303
  yield* rejectListFlags(args, "a task group roster", true);
@@ -2502,26 +2413,121 @@ function getSubmissions(args, ctx) {
2502
2413
  });
2503
2414
  }
2504
2415
  const getCommand = Command.make("get", {
2505
- what: Args.text({ name: "what" }).pipe(Args.withDescription("What to read: tasks, submissions, a tsk_ id, or a grptsk_ id.")),
2416
+ what: Args.text({ name: "what" }).pipe(Args.withDescription("What to read: tasks, submissions, a tsk_ id, a sub_ id, or a grptsk_ id.")),
2506
2417
  status: statusOption,
2507
- since: sinceOption,
2508
- until: untilOption,
2418
+ since: sinceOption$1,
2419
+ until: untilOption$1,
2509
2420
  topic: topicOption,
2510
- member: memberOption$2,
2421
+ member: memberOption$3,
2511
2422
  group: groupOption,
2512
- limit: limitOption,
2423
+ limit: limitOption$2,
2513
2424
  cursor: cursorOption,
2514
2425
  all: allOption,
2515
- format: formatOption$3,
2426
+ format: formatOption$4,
2516
2427
  ...credentialOptions
2517
2428
  }, (args) => withQueryClient(args, (ctx) => {
2518
2429
  if (args.what === "tasks") return getTasks(args, ctx);
2519
2430
  if (args.what === "submissions") return getSubmissions(args, ctx);
2520
2431
  if (args.what.startsWith("tsk_")) return getChain(args, ctx);
2432
+ if (args.what.startsWith("sub_")) return getSubtaskRead(args, ctx);
2521
2433
  if (args.what.startsWith("grptsk_")) return getRoster(args, ctx);
2522
- const hint = args.what.startsWith("sub_") ? "a subtask is read through its parent: sp get tsk_…" : args.what.startsWith("sbm_") ? "a single submission has no read endpoint; find it in the listing: sp get submissions --since … --format json" : "expected tasks, submissions, a tsk_ id, or a grptsk_ id";
2434
+ const hint = args.what.startsWith("sbm_") ? "a single submission has no read endpoint; find it in the listing: sp get submissions --since … --format json" : "expected tasks, submissions, a tsk_ id, a sub_ id, or a grptsk_ id";
2523
2435
  return Effect.fail(new UserError({ message: `cannot read '${args.what}': ${hint}` }));
2524
- })).pipe(Command.withDescription("Read what came back: 'tasks' or 'submissions' for filtered listings, a tsk_ id for one task with its subtasks, a grptsk_ id for a group's per-recipient roster."));
2436
+ })).pipe(Command.withDescription("Read what came back: 'tasks' or 'submissions' for filtered listings, a tsk_ id for one task with its subtasks, a sub_ id for one subtask, a grptsk_ id for a group's per-recipient roster."));
2437
+ //#endregion
2438
+ //#region src/commands/search.ts
2439
+ const KINDS = [
2440
+ "task",
2441
+ "subtask",
2442
+ "answer",
2443
+ "reply",
2444
+ "notification",
2445
+ "notification_answer",
2446
+ "submission"
2447
+ ];
2448
+ const kindOption = Options.text("kind").pipe(Options.repeated, Options.mapTryCatch((values) => {
2449
+ const flat = values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
2450
+ const bad = flat.filter((s) => !KINDS.includes(s));
2451
+ if (bad.length > 0) throw new Error(`unknown kind ${bad.join(", ")} — use ${KINDS.join(", ")}`);
2452
+ return flat;
2453
+ }, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.withDescription("Only these hit kinds (repeatable or comma-separated)."));
2454
+ function parsePoint(raw) {
2455
+ const parts = raw.split(",");
2456
+ if (parts.length === 2) {
2457
+ const latitude = Number(parts[0].trim());
2458
+ const longitude = Number(parts[1].trim());
2459
+ if (Number.isFinite(latitude) && Number.isFinite(longitude) && Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180) return {
2460
+ latitude,
2461
+ longitude
2462
+ };
2463
+ }
2464
+ throw new Error(`expected \`lat,lng\` with lat in [-90,90] and lng in [-180,180], got '${raw}'`);
2465
+ }
2466
+ const nearOption = Options.text("near").pipe(Options.mapTryCatch(parsePoint, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.optional, Options.withDescription("Center of a radius filter as `lat,lng`; requires --radius. Hits come back with their point and its distance, nearest first when no words are given."));
2467
+ const radiusOption = Options.integer("radius").pipe(Options.optional, Options.withDescription("Radius around --near, in meters (1..1000000)."));
2468
+ const withinOption = Options.text("within").pipe(Options.mapTryCatch((raw) => {
2469
+ const points = raw.split(";").map((s) => s.trim()).filter((s) => s.length > 0).map(parsePoint);
2470
+ if (points.length < 3) throw new Error("--within needs at least 3 `lat,lng` points separated by `;`");
2471
+ return points;
2472
+ }, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.optional, Options.withDescription("Polygon to search inside: `lat,lng` corners joined with `;` (at least 3). Not combinable with --near/--radius; hits carry no distance and come newest first without words."));
2473
+ const limitOption = Options.integer("limit").pipe(Options.optional, Options.withDescription("Best hits to return; server default 20, at most 100."));
2474
+ /** One block per hit: id, kind, when, then whatever the hit carries. */
2475
+ function formatHit(h) {
2476
+ return [
2477
+ `${h.ref} [${h.kind}] ${h.createdAt}`,
2478
+ h.title !== void 0 ? ` ${h.title}` : void 0,
2479
+ h.actor !== void 0 ? ` by: ${h.actor}` : void 0,
2480
+ h.snippet !== void 0 ? ` ${h.snippet}` : void 0,
2481
+ h.location !== void 0 ? ` at: ${h.location.latitude},${h.location.longitude}${h.location.distanceMeters !== void 0 ? ` (${Math.round(h.location.distanceMeters)}m away)` : ""}` : void 0
2482
+ ].filter((l) => l !== void 0).join("\n");
2483
+ }
2484
+ const searchCommand = Command.make("search", {
2485
+ words: Args.text({ name: "words" }).pipe(Args.withDescription("The words to look for; all must appear. Quote a phrase (nested quotes) for adjacency. Optional when --near/--radius or --within is given."), Args.repeated),
2486
+ kind: kindOption,
2487
+ since: sinceOption$1,
2488
+ until: untilOption$1,
2489
+ member: memberOption$3,
2490
+ near: nearOption,
2491
+ radius: radiusOption,
2492
+ within: withinOption,
2493
+ limit: limitOption,
2494
+ format: formatOption$4,
2495
+ ...credentialOptions
2496
+ }, (args) => withQueryClient(args, (ctx) => Effect.gen(function* () {
2497
+ const query = args.words.join(" ").trim();
2498
+ const near = Option.getOrUndefined(args.near);
2499
+ const radius = Option.getOrUndefined(args.radius);
2500
+ const within = Option.getOrUndefined(args.within);
2501
+ const problem = near !== void 0 && radius === void 0 ? "--near requires --radius (meters)" : radius !== void 0 && near === void 0 ? "--radius requires --near" : within !== void 0 && near !== void 0 ? "--within cannot be combined with --near/--radius" : query === "" && near === void 0 && within === void 0 ? "give words to search for, or an area (--near with --radius, or --within)" : void 0;
2502
+ if (problem !== void 0) return yield* Effect.fail(new UserError({ message: problem }));
2503
+ const opts = {
2504
+ ...args.kind.length > 0 ? { kind: args.kind } : {},
2505
+ ...Option.match(args.since, {
2506
+ onNone: () => ({}),
2507
+ onSome: (since) => ({ since })
2508
+ }),
2509
+ ...Option.match(args.until, {
2510
+ onNone: () => ({}),
2511
+ onSome: (until) => ({ until })
2512
+ }),
2513
+ ...Option.match(args.member, {
2514
+ onNone: () => ({}),
2515
+ onSome: (member) => ({ member })
2516
+ }),
2517
+ ...Option.match(args.limit, {
2518
+ onNone: () => ({}),
2519
+ onSome: (limit) => ({ limit })
2520
+ }),
2521
+ ...near !== void 0 && radius !== void 0 ? {
2522
+ near,
2523
+ radiusMeters: radius
2524
+ } : {},
2525
+ ...within !== void 0 ? { within } : {}
2526
+ };
2527
+ const res = yield* sdkCall("search", () => ctx.client.search(query === "" ? void 0 : query, opts));
2528
+ for (const h of res.hits) yield* ctx.out.print(args.format === "json" ? JSON.stringify(h) : formatHit(h));
2529
+ if (res.hits.length === 0 && args.format === "pretty") yield* ctx.out.info("no hits");
2530
+ }))).pipe(Command.withDescription("Ranked full-text and location search over everything this credential reads, across all time. Words match literally plus stemmed in the org's configured languages; --near/--radius or --within search by place instead of or on top of words."));
2525
2531
  //#endregion
2526
2532
  //#region src/input-spec.ts
2527
2533
  const SETTING_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -3806,7 +3812,7 @@ const fileInput$1 = repeatedText("file-input", void 0, "Add a file upload input.
3806
3812
  const locationInput$1 = repeatedText("location-input", void 0, "Add a location input (the recipient shares their device GPS position from the app). Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable.");
3807
3813
  const linkOption$1 = repeatedText("link", "l", "Attach a remote URL (a link attachment). Repeatable. For local files use --file.");
3808
3814
  const fileOption$1 = repeatedText("file", "f", "Attach a local file, uploaded as a file attachment (encrypted under the send's key — topic password or org master key — when the send is encrypted). Repeatable.");
3809
- const submitOption$1 = Options.boolean("submit").pipe(Options.withDescription("Require the recipient to explicitly submit the task. Without this, it auto-completes once the required inputs are filled."));
3815
+ const autoCommitOption$1 = Options.boolean("auto-commit").pipe(Options.withDescription("Complete the task as inputs are filled, without an explicit Submit. Default is form mode: the recipient submits everything at once."));
3810
3816
  const waitOption$1 = Options.boolean("wait").pipe(Options.withDescription("Block until the task is completed; print the result to stdout. Requires exactly one input on the request."));
3811
3817
  const replyOption$1 = Options.choice("reply", [
3812
3818
  "one-shot",
@@ -3835,7 +3841,7 @@ const taskCommand = Command.make("task", {
3835
3841
  "location-input": locationInput$1,
3836
3842
  link: linkOption$1,
3837
3843
  file: fileOption$1,
3838
- submit: submitOption$1,
3844
+ "auto-commit": autoCommitOption$1,
3839
3845
  wait: waitOption$1,
3840
3846
  reply: replyOption$1,
3841
3847
  member: memberOption,
@@ -3886,7 +3892,7 @@ const taskCommand = Command.make("task", {
3886
3892
  inputs,
3887
3893
  links: [...args.link],
3888
3894
  files: [...args.file],
3889
- autoCommit: !args.submit,
3895
+ autoCommit: args["auto-commit"],
3890
3896
  reply: Option.getOrUndefined(args.reply),
3891
3897
  markdown: args.markdown,
3892
3898
  noEncrypt: args["no-encrypt"],
@@ -3913,7 +3919,7 @@ const taskCommand = Command.make("task", {
3913
3919
  inputs,
3914
3920
  links: [...args.link],
3915
3921
  ...files.length > 0 ? { files } : {},
3916
- autoCommit: !args.submit,
3922
+ autoCommit: args["auto-commit"],
3917
3923
  ...Option.isSome(args.reply) ? { reply: args.reply.value } : {},
3918
3924
  ...args.markdown ? { contentFormat: "markdown" } : {},
3919
3925
  ...expiresAt !== void 0 ? { expiresAt } : {}
@@ -4120,7 +4126,7 @@ const photoInput = Options.text("photo-input").pipe(Options.withDescription("Add
4120
4126
  const voiceRecordingInput = Options.text("voice-recording-input").pipe(Options.withDescription("Add a voice recording input. Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable."), Options.repeated);
4121
4127
  const fileInput = Options.text("file-input").pipe(Options.withDescription("Add a file upload input. Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable."), Options.repeated);
4122
4128
  const locationInput = Options.text("location-input").pipe(Options.withDescription("Add a location input (the recipient shares their device GPS position from the app). Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable."), Options.repeated);
4123
- const submitOption = Options.boolean("submit").pipe(Options.withDescription("Require the recipient to explicitly submit the subtask. Without this, it auto-completes once the required inputs are filled."));
4129
+ const autoCommitOption = Options.boolean("auto-commit").pipe(Options.withDescription("Complete the subtask as inputs are filled, without an explicit Submit. Default is form mode: the recipient submits everything at once."));
4124
4130
  const waitOption = Options.boolean("wait").pipe(Options.withDescription("Block until the subtask is completed; print the result to stdout. Requires at least one input on the append. On a group append, the first member's completion wins."));
4125
4131
  const formatOption = Options.choice("format", ["text", "json"]).pipe(Options.withDescription("stdout format for an append: `text` (the bare sub_ id(s), default) or `json` (a `sent` line piped to `sp collect`)."), Options.withDefault("text"));
4126
4132
  const replyOption = Options.choice("reply", [
@@ -4145,7 +4151,7 @@ const subtaskCommand = Command.make("subtask", {
4145
4151
  "location-input": locationInput,
4146
4152
  link: linkOption,
4147
4153
  file: fileOption,
4148
- submit: submitOption,
4154
+ "auto-commit": autoCommitOption,
4149
4155
  wait: waitOption,
4150
4156
  format: formatOption,
4151
4157
  reply: replyOption,
@@ -4175,7 +4181,7 @@ const subtaskCommand = Command.make("subtask", {
4175
4181
  ...content !== void 0 ? { content } : {},
4176
4182
  ...inputs.length > 0 ? { inputs } : {},
4177
4183
  links: [...args.link],
4178
- autoCommit: !args.submit,
4184
+ autoCommit: args["auto-commit"],
4179
4185
  ...Option.isSome(args.reply) ? { reply: args.reply.value } : {},
4180
4186
  ...args.markdown ? { contentFormat: "markdown" } : {}
4181
4187
  };
@@ -4334,11 +4340,12 @@ const root = Command.make("simplepush").pipe(Command.withSubcommands([
4334
4340
  taskCommand,
4335
4341
  subtaskCommand,
4336
4342
  cancelCommand,
4337
- getCommand
4343
+ getCommand,
4344
+ searchCommand
4338
4345
  ]));
4339
4346
  const cli = Command.run(root, {
4340
4347
  name: "Simplepush CLI",
4341
- version: "0.2.0"
4348
+ version
4342
4349
  });
4343
4350
  const MainLive = Layer.mergeAll(CliOutput.Default, Sodium.Default, AuthStore.Default, VaultStore.Default, InviteStore.Default, Api.Default, VaultAccess.Default).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(NodeContext.layer));
4344
4351
  /** Render any failure through the typed-error table, then re-fail so the