@lotics/cli 0.41.0 → 0.42.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.
@@ -434,12 +434,16 @@ export async function appDev(client, args) {
434
434
  port: args.port,
435
435
  vitePort: args.vitePort,
436
436
  client,
437
+ commentsEnabled: meta.capabilities?.comments,
437
438
  });
438
439
  await handle.ready;
439
440
  const url = `http://localhost:${handle.port}`;
440
441
  console.error(`\n lotics app dev`);
441
442
  console.error(` app: ${app.name} (${meta.app_id})`);
442
443
  console.error(` workspace: ${meta.workspace_id}`);
444
+ if (client.viewAsMemberId) {
445
+ console.error(` view as: ${client.viewAsMemberId} (is_current_member resolves to this member)`);
446
+ }
443
447
  console.error(` vite: http://localhost:${handle.vitePort}/`);
444
448
  console.error(` open: ${url}`);
445
449
  console.error(` rpc: ${client.baseUrl} (via Bearer API key)\n`);
package/dist/args.d.ts CHANGED
@@ -22,6 +22,7 @@ export declare function parseArgs(argv: string[]): {
22
22
  as?: string;
23
23
  apiKey?: string;
24
24
  workspace?: string;
25
+ viewAs?: string;
25
26
  name?: string;
26
27
  timezone?: string;
27
28
  message?: string;
package/dist/args.js CHANGED
@@ -18,6 +18,7 @@ export function parseArgs(argv) {
18
18
  as: undefined,
19
19
  apiKey: undefined,
20
20
  workspace: undefined,
21
+ viewAs: undefined,
21
22
  name: undefined,
22
23
  timezone: undefined,
23
24
  message: undefined,
@@ -55,6 +56,9 @@ export function parseArgs(argv) {
55
56
  case "-w":
56
57
  flags.workspace = argv[++i];
57
58
  break;
59
+ case "--view-as":
60
+ flags.viewAs = argv[++i];
61
+ break;
58
62
  case "--name":
59
63
  flags.name = argv[++i];
60
64
  break;
package/dist/args.test.js CHANGED
@@ -45,6 +45,12 @@ describe("parseArgs", () => {
45
45
  const r = parseArgs(["run", "query_tables", "-w", "wsp_123"]);
46
46
  expect(r.flags.workspace).toBe("wsp_123");
47
47
  });
48
+ it("parses --view-as as a value flag", () => {
49
+ const r = parseArgs(["app", "dev", "--view-as", "mbr_abc"]);
50
+ expect(r.flags.viewAs).toBe("mbr_abc");
51
+ expect(r.command).toBe("app");
52
+ expect(r.subcommand).toBe("dev");
53
+ });
48
54
  it("parses --all as a boolean flag", () => {
49
55
  const r = parseArgs(["auth", "logout", "--all"]);
50
56
  expect(r.flags.all).toBe(true);
package/dist/cli.js CHANGED
@@ -81,6 +81,9 @@ FLAGS
81
81
  --as <name> Override upload filename
82
82
  --api-key <key> One-off API key (overrides saved config + env)
83
83
  --workspace <id> One-off workspace override (alias: -w)
84
+ --view-as <id> Admin "View as": run every request as this member, so
85
+ is_current_member / row-scoping resolve to them (also
86
+ LOTICS_VIEW_AS env; admin key only; writes stay yours)
84
87
  --local Pin the current directory (lotics org use / auth api-key)
85
88
  --all (lotics auth logout) Remove every saved credential
86
89
  --version Show version
@@ -279,7 +282,13 @@ function requireClient(flags) {
279
282
  console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
280
283
  process.exit(1);
281
284
  }
282
- return { client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId }), ctx };
285
+ // Admin "View as": flag wins over env. The server enforces admin-only; a
286
+ // non-admin key gets a clear 403 on the first request.
287
+ const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
288
+ return {
289
+ client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
290
+ ctx,
291
+ };
283
292
  }
284
293
  const SOURCE_LABELS = {
285
294
  flag: "--api-key flag",
@@ -378,8 +387,9 @@ async function main() {
378
387
  const info = await client.whoami();
379
388
  const existing = loadGlobalConfig() ?? {};
380
389
  saveGlobalConfig({ ...existing, email: info.email });
390
+ const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
381
391
  if (flags.json) {
382
- console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source }, null, 2));
392
+ console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source, view_as: viewAs ?? null }, null, 2));
383
393
  }
384
394
  else {
385
395
  console.log(`Name: ${info.name}`);
@@ -387,6 +397,8 @@ async function main() {
387
397
  console.log(`Org: ${info.organization_name} (${info.organization_id})`);
388
398
  console.log(`Workspace: ${ctx.workspaceId ?? "(none selected)"}`);
389
399
  console.log(`Source: ${SOURCE_LABELS[ctx.source]}`);
400
+ if (viewAs)
401
+ console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
390
402
  }
391
403
  return;
392
404
  }
package/dist/client.d.ts CHANGED
@@ -22,6 +22,11 @@ export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup;
22
22
  export interface LoticsClientOptions {
23
23
  apiKey: string;
24
24
  workspaceId?: string;
25
+ /** Admin "View as": when set, every request carries `x-view-as-member-id`, so
26
+ * the backend evaluates IAM scoping (and `is_current_member`) as this member.
27
+ * Admin-only — the server rejects a non-admin key. Writes stay attributed to
28
+ * the key's owner. */
29
+ viewAsMemberId?: string;
25
30
  }
26
31
  export interface WorkspaceInfo {
27
32
  id: string;
@@ -56,6 +61,9 @@ export declare const API_BASE_URL: string;
56
61
  export declare class LoticsClient {
57
62
  private apiKey;
58
63
  private workspaceId;
64
+ /** The active "View as" target member id, if any. Read-only after
65
+ * construction — surfaced so `lotics app dev` can show it in the banner. */
66
+ readonly viewAsMemberId: string | undefined;
59
67
  /** API URL the client is configured against. Read-only after construction.
60
68
  * Surfaced for callers that need to display or log it (e.g., `lotics app dev`
61
69
  * shows it in the banner). */
@@ -65,6 +73,7 @@ export declare class LoticsClient {
65
73
  private buildHeaders;
66
74
  private request;
67
75
  whoami(): Promise<{
76
+ member_id: string;
68
77
  email: string;
69
78
  name: string;
70
79
  organization_id: string;
package/dist/client.js CHANGED
@@ -49,6 +49,9 @@ export const API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai
49
49
  export class LoticsClient {
50
50
  apiKey;
51
51
  workspaceId;
52
+ /** The active "View as" target member id, if any. Read-only after
53
+ * construction — surfaced so `lotics app dev` can show it in the banner. */
54
+ viewAsMemberId;
52
55
  /** API URL the client is configured against. Read-only after construction.
53
56
  * Surfaced for callers that need to display or log it (e.g., `lotics app dev`
54
57
  * shows it in the banner). */
@@ -56,6 +59,7 @@ export class LoticsClient {
56
59
  constructor(options) {
57
60
  this.apiKey = options.apiKey;
58
61
  this.workspaceId = options.workspaceId;
62
+ this.viewAsMemberId = options.viewAsMemberId;
59
63
  this.baseUrl = API_BASE_URL;
60
64
  }
61
65
  async throwResponseError(response) {
@@ -77,6 +81,9 @@ export class LoticsClient {
77
81
  if (this.workspaceId) {
78
82
  headers["x-workspace-id"] = this.workspaceId;
79
83
  }
84
+ if (this.viewAsMemberId) {
85
+ headers["x-view-as-member-id"] = this.viewAsMemberId;
86
+ }
80
87
  return headers;
81
88
  }
82
89
  async request(method, path, body) {
@@ -6,10 +6,16 @@
6
6
  * { message }. Same shape as the production iframe-host's error path.
7
7
  */
8
8
  import { LoticsClient } from "../client.js";
9
- export type RpcOp = "query" | "workflow" | "members" | "upload_url" | "upload_complete";
9
+ export type RpcOp = "query" | "workflow" | "members" | "context" | "upload_url" | "upload_complete";
10
10
  export interface RpcRequest {
11
11
  app_id: string;
12
12
  op: RpcOp;
13
13
  payload: unknown;
14
14
  }
15
- export declare function dispatchRpc(client: LoticsClient, body: RpcRequest): Promise<unknown>;
15
+ /** Per-app facts the dev server knows from the local manifest that the
16
+ * production iframe-host supplies from the embedding context. */
17
+ export interface DispatchOptions {
18
+ /** From `package.json#lotics.capabilities.comments`. */
19
+ commentsEnabled?: boolean;
20
+ }
21
+ export declare function dispatchRpc(client: LoticsClient, body: RpcRequest, opts?: DispatchOptions): Promise<unknown>;
@@ -9,10 +9,11 @@ const SUPPORTED_OPS = new Set([
9
9
  "query",
10
10
  "workflow",
11
11
  "members",
12
+ "context",
12
13
  "upload_url",
13
14
  "upload_complete",
14
15
  ]);
15
- export async function dispatchRpc(client, body) {
16
+ export async function dispatchRpc(client, body, opts) {
16
17
  if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
17
18
  throw new Error("RPC envelope must include app_id and op");
18
19
  }
@@ -20,6 +21,17 @@ export async function dispatchRpc(client, body) {
20
21
  throw new Error(`Unknown RPC op: ${body.op}`);
21
22
  }
22
23
  switch (body.op) {
24
+ case "context": {
25
+ // In production the iframe host supplies the context; here we resolve the
26
+ // effective viewer from the CLI key (the view-as target when the client
27
+ // carries `x-view-as-member-id`, else the key's owner), so `useViewer`
28
+ // and `is_current_member` agree in the dev loop.
29
+ const who = await client.whoami();
30
+ return {
31
+ member_id: who.member_id,
32
+ comments_enabled: opts?.commentsEnabled ?? false,
33
+ };
34
+ }
23
35
  case "query": {
24
36
  const p = body.payload;
25
37
  if (!p || typeof p.alias !== "string") {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { dispatchRpc } from "./rpc_handler.js";
3
+ function mockClient(over) {
4
+ return over;
5
+ }
6
+ const WHOAMI = {
7
+ member_id: "mbr_viewer",
8
+ email: "v@test.local",
9
+ name: "Viewer",
10
+ organization_id: "org_1",
11
+ organization_name: "Org",
12
+ };
13
+ describe("dispatchRpc — context op", () => {
14
+ it("resolves the viewer member_id from whoami + manifest comments_enabled", async () => {
15
+ const client = mockClient({ whoami: async () => WHOAMI });
16
+ const result = await dispatchRpc(client, { app_id: "app_x", op: "context", payload: {} }, { commentsEnabled: true });
17
+ expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: true });
18
+ });
19
+ it("defaults comments_enabled to false when the manifest declares none", async () => {
20
+ const client = mockClient({ whoami: async () => WHOAMI });
21
+ const result = await dispatchRpc(client, { app_id: "app_x", op: "context", payload: {} });
22
+ expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: false });
23
+ });
24
+ it("rejects an op that isn't supported", async () => {
25
+ const client = mockClient({});
26
+ await expect(dispatchRpc(client, { app_id: "app_x", op: "bogus", payload: {} })).rejects.toThrow(/Unknown RPC op/);
27
+ });
28
+ });
@@ -25,6 +25,9 @@ export interface DevServerArgs {
25
25
  /** Preferred Vite port. If taken, auto-picks a free one. */
26
26
  vitePort?: number;
27
27
  client: LoticsClient;
28
+ /** From `package.json#lotics.capabilities.comments` — fed to the `context` op
29
+ * so `useComments` reflects the manifest in the dev loop. */
30
+ commentsEnabled?: boolean;
28
31
  }
29
32
  export interface DevServerHandle {
30
33
  port: number;
@@ -91,7 +91,7 @@ export async function startDevServer(args) {
91
91
  app_id: body.app_id,
92
92
  op: body.op,
93
93
  payload: body.payload,
94
- });
94
+ }, { commentsEnabled: args.commentsEnabled });
95
95
  const ms = Date.now() - startedAt;
96
96
  process.stderr.write(`[rpc] ${body.op} ${ms}ms\n`);
97
97
  res.writeHead(200, { "Content-Type": "application/json" });
package/dist/src/cli.js CHANGED
@@ -29625,6 +29625,9 @@ var API_BASE_URL = process.env.LOTICS_API_URL ?? "https://api.lotics.ai";
29625
29625
  var LoticsClient = class {
29626
29626
  apiKey;
29627
29627
  workspaceId;
29628
+ /** The active "View as" target member id, if any. Read-only after
29629
+ * construction — surfaced so `lotics app dev` can show it in the banner. */
29630
+ viewAsMemberId;
29628
29631
  /** API URL the client is configured against. Read-only after construction.
29629
29632
  * Surfaced for callers that need to display or log it (e.g., `lotics app dev`
29630
29633
  * shows it in the banner). */
@@ -29632,6 +29635,7 @@ var LoticsClient = class {
29632
29635
  constructor(options) {
29633
29636
  this.apiKey = options.apiKey;
29634
29637
  this.workspaceId = options.workspaceId;
29638
+ this.viewAsMemberId = options.viewAsMemberId;
29635
29639
  this.baseUrl = API_BASE_URL;
29636
29640
  }
29637
29641
  async throwResponseError(response) {
@@ -29652,6 +29656,9 @@ var LoticsClient = class {
29652
29656
  if (this.workspaceId) {
29653
29657
  headers["x-workspace-id"] = this.workspaceId;
29654
29658
  }
29659
+ if (this.viewAsMemberId) {
29660
+ headers["x-view-as-member-id"] = this.viewAsMemberId;
29661
+ }
29655
29662
  return headers;
29656
29663
  }
29657
29664
  async request(method, path7, body) {
@@ -30642,10 +30649,11 @@ var SUPPORTED_OPS = /* @__PURE__ */ new Set([
30642
30649
  "query",
30643
30650
  "workflow",
30644
30651
  "members",
30652
+ "context",
30645
30653
  "upload_url",
30646
30654
  "upload_complete"
30647
30655
  ]);
30648
- async function dispatchRpc(client, body) {
30656
+ async function dispatchRpc(client, body, opts) {
30649
30657
  if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
30650
30658
  throw new Error("RPC envelope must include app_id and op");
30651
30659
  }
@@ -30653,6 +30661,13 @@ async function dispatchRpc(client, body) {
30653
30661
  throw new Error(`Unknown RPC op: ${body.op}`);
30654
30662
  }
30655
30663
  switch (body.op) {
30664
+ case "context": {
30665
+ const who = await client.whoami();
30666
+ return {
30667
+ member_id: who.member_id,
30668
+ comments_enabled: opts?.commentsEnabled ?? false
30669
+ };
30670
+ }
30656
30671
  case "query": {
30657
30672
  const p = body.payload;
30658
30673
  if (!p || typeof p.alias !== "string") {
@@ -30943,11 +30958,15 @@ async function startDevServer(args) {
30943
30958
  try {
30944
30959
  const body = await readJson(req);
30945
30960
  const startedAt = Date.now();
30946
- const result = await dispatchRpc(args.client, {
30947
- app_id: body.app_id,
30948
- op: body.op,
30949
- payload: body.payload
30950
- });
30961
+ const result = await dispatchRpc(
30962
+ args.client,
30963
+ {
30964
+ app_id: body.app_id,
30965
+ op: body.op,
30966
+ payload: body.payload
30967
+ },
30968
+ { commentsEnabled: args.commentsEnabled }
30969
+ );
30951
30970
  const ms = Date.now() - startedAt;
30952
30971
  process.stderr.write(`[rpc] ${body.op} ${ms}ms
30953
30972
  `);
@@ -31469,7 +31488,8 @@ async function appDev(client, args) {
31469
31488
  api_url: client.baseUrl,
31470
31489
  port: args.port,
31471
31490
  vitePort: args.vitePort,
31472
- client
31491
+ client,
31492
+ commentsEnabled: meta.capabilities?.comments
31473
31493
  });
31474
31494
  await handle.ready;
31475
31495
  const url = `http://localhost:${handle.port}`;
@@ -31477,6 +31497,9 @@ async function appDev(client, args) {
31477
31497
  lotics app dev`);
31478
31498
  console.error(` app: ${app.name} (${meta.app_id})`);
31479
31499
  console.error(` workspace: ${meta.workspace_id}`);
31500
+ if (client.viewAsMemberId) {
31501
+ console.error(` view as: ${client.viewAsMemberId} (is_current_member resolves to this member)`);
31502
+ }
31480
31503
  console.error(` vite: http://localhost:${handle.vitePort}/`);
31481
31504
  console.error(` open: ${url}`);
31482
31505
  console.error(` rpc: ${client.baseUrl} (via Bearer API key)
@@ -31506,6 +31529,7 @@ function parseArgs(argv) {
31506
31529
  as: void 0,
31507
31530
  apiKey: void 0,
31508
31531
  workspace: void 0,
31532
+ viewAs: void 0,
31509
31533
  name: void 0,
31510
31534
  timezone: void 0,
31511
31535
  message: void 0,
@@ -31543,6 +31567,9 @@ function parseArgs(argv) {
31543
31567
  case "-w":
31544
31568
  flags.workspace = argv[++i];
31545
31569
  break;
31570
+ case "--view-as":
31571
+ flags.viewAs = argv[++i];
31572
+ break;
31546
31573
  case "--name":
31547
31574
  flags.name = argv[++i];
31548
31575
  break;
@@ -47363,6 +47390,9 @@ FLAGS
47363
47390
  --as <name> Override upload filename
47364
47391
  --api-key <key> One-off API key (overrides saved config + env)
47365
47392
  --workspace <id> One-off workspace override (alias: -w)
47393
+ --view-as <id> Admin "View as": run every request as this member, so
47394
+ is_current_member / row-scoping resolve to them (also
47395
+ LOTICS_VIEW_AS env; admin key only; writes stay yours)
47366
47396
  --local Pin the current directory (lotics org use / auth api-key)
47367
47397
  --all (lotics auth logout) Remove every saved credential
47368
47398
  --version Show version
@@ -47545,7 +47575,11 @@ function requireClient(flags) {
47545
47575
  console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
47546
47576
  process.exit(1);
47547
47577
  }
47548
- return { client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId }), ctx };
47578
+ const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
47579
+ return {
47580
+ client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
47581
+ ctx
47582
+ };
47549
47583
  }
47550
47584
  var SOURCE_LABELS = {
47551
47585
  flag: "--api-key flag",
@@ -47635,14 +47669,16 @@ async function main() {
47635
47669
  const info = await client2.whoami();
47636
47670
  const existing = loadGlobalConfig() ?? {};
47637
47671
  saveGlobalConfig({ ...existing, email: info.email });
47672
+ const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
47638
47673
  if (flags.json) {
47639
- console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, source: ctx2.source }, null, 2));
47674
+ console.log(JSON.stringify({ ...info, workspace_id: ctx2.workspaceId ?? null, source: ctx2.source, view_as: viewAs ?? null }, null, 2));
47640
47675
  } else {
47641
47676
  console.log(`Name: ${info.name}`);
47642
47677
  console.log(`Email: ${info.email}`);
47643
47678
  console.log(`Org: ${info.organization_name} (${info.organization_id})`);
47644
47679
  console.log(`Workspace: ${ctx2.workspaceId ?? "(none selected)"}`);
47645
47680
  console.log(`Source: ${SOURCE_LABELS[ctx2.source]}`);
47681
+ if (viewAs) console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
47646
47682
  }
47647
47683
  return;
47648
47684
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {