@topolo/cli 0.1.4 → 0.1.6

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.
Files changed (3) hide show
  1. package/README.md +25 -6
  2. package/dist/index.js +171 -14
  3. package/package.json +7 -4
package/README.md CHANGED
@@ -137,10 +137,29 @@ Both docs cover the same CLI surface: credential handling, the JSON invocation
137
137
  contract, the write-safety `--confirm` guardrail, and the cross-org isolation
138
138
  rules. Keep them in sync when adding new commands.
139
139
 
140
- ## Phase 2 (planned)
140
+ ## Debug output
141
141
 
142
- - `topolo auth login` replaces the paste-API-key flow with an OAuth 2.1 device
143
- authorization grant issued by TopoloAuth.
144
- - Refresh-token storage + automatic renewal.
145
- - OAuth client registration lives in TopoloDevelopers — consent UI will follow
146
- the existing `@topolo/design-tokens` + `@topolo/ui-kit` patterns.
142
+ Set `TOPOLO_DEBUG=1` to trace every SDK request to stderr (stdout stays clean
143
+ so `--json` piping still works):
144
+
145
+ ```bash
146
+ TOPOLO_DEBUG=1 topolo whoami
147
+ # [topolo] → GET auth /api/auth/me (req=8f3...)
148
+ # [topolo] ← 200 GET auth /api/auth/me (142ms, req=8f3...)
149
+ ```
150
+
151
+ Request IDs match the `X-Topolo-Request-Id` header, so you can correlate a
152
+ failing CLI call with the backend access log. Accepted truthy values:
153
+ anything except `0` or `false`.
154
+
155
+ ## Token auto-refresh
156
+
157
+ If the stored access token is within 60 s of expiry, the CLI transparently
158
+ exchanges the stored refresh token for a new pair before making the request
159
+ and rewrites the config — no user action required. The OAuth `client_id` used
160
+ at login is persisted alongside the credential so refresh can re-authenticate.
161
+
162
+ If refresh fails (network, rotated/revoked refresh token), the CLI proceeds
163
+ with the stale credential and the request returns `auth_error` — run
164
+ `topolo auth login` to re-authenticate. The stored config is not cleared on
165
+ transient refresh failures.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin as input } from "process";
11
11
 
12
- // ../packages/topolo-sdk/dist/index.js
12
+ // node_modules/@topolo/sdk/dist/index.js
13
13
  function applyAuthHeaders(headers, credential) {
14
14
  if (credential.kind === "api_key") {
15
15
  headers.set("X-Api-Key", credential.apiKey);
@@ -64,17 +64,38 @@ var TopoloHttpError = class extends TopoloSdkError {
64
64
  this.path = path;
65
65
  }
66
66
  };
67
- var DEFAULT_SERVICE_URLS = {
67
+ var SERVICES = {
68
+ agent: "https://topolo-agent-worker.topolo.workers.dev",
68
69
  auth: "https://auth.topolo.app",
69
- crm: "https://topolo-crm-worker.topolo.workers.dev"
70
+ bugfix: "https://bugfix.topolo.app",
71
+ bytes: "https://r2-media-manager-worker.topolo.workers.dev",
72
+ chat: "https://chat.topolo.app",
73
+ commerce: "https://topolo-commerce-api.topolo.workers.dev",
74
+ crm: "https://crm-api.topolo.app",
75
+ forecast: "https://forecast-worker-production.topolo.workers.dev",
76
+ learn: "https://topololearn-api.topolo.workers.dev",
77
+ mdm: "https://nodo-mdm-api.topolo.app",
78
+ messaging: "https://messages.topolo.app",
79
+ nexus: "https://topolo-nexus-gateway.topolo.workers.dev",
80
+ one: "https://topoloone-worker.topolo.workers.dev",
81
+ pay: "https://topolo-pay.topolo.workers.dev",
82
+ "quro.api": "https://api.ol0.me",
83
+ "quro.redirect": "https://ol0.me",
84
+ roadmapper: "https://roadmapper-api.topolo.app",
85
+ social_studio: "https://topolo-social-studio-api.topolo.workers.dev",
86
+ socialize: "https://socialize-prod-api.topolo.workers.dev",
87
+ support: "https://support.topolo.app",
88
+ "web.api": "https://web-api.topolo.app",
89
+ "web.runtime": "https://sites.topolo.app"
70
90
  };
71
91
  var PLATFORM_SERVICE_IDS = {
72
92
  crm: "srv_iCwM4jGXcwlj"
73
93
  };
94
+ var DEFAULT_SERVICE_URLS = SERVICES;
74
95
  function resolveServiceUrl(service, overrides) {
75
96
  const override = overrides?.[service];
76
97
  if (override) return override;
77
- const envKey = `TOPOLO_SERVICE_URL_${service.toUpperCase()}`;
98
+ const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
78
99
  const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
79
100
  if (envValue) return envValue;
80
101
  return DEFAULT_SERVICE_URLS[service];
@@ -87,6 +108,7 @@ var TopoloClient = class {
87
108
  requireConfirmForWrites;
88
109
  timeoutMs;
89
110
  fetchImpl;
111
+ debug;
90
112
  constructor(options) {
91
113
  if (!options.credential) throw new TopoloAuthError("credential is required");
92
114
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
@@ -96,6 +118,14 @@ var TopoloClient = class {
96
118
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
97
119
  this.timeoutMs = options.timeoutMs ?? 3e4;
98
120
  this.fetchImpl = options.fetch ?? fetch;
121
+ this.debug = options.debug;
122
+ }
123
+ emit(event) {
124
+ if (!this.debug) return;
125
+ try {
126
+ this.debug(event);
127
+ } catch {
128
+ }
99
129
  }
100
130
  /**
101
131
  * Low-level JSON request. Prefer the typed module helpers (identity, crm, ...)
@@ -122,27 +152,56 @@ var TopoloClient = class {
122
152
  const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
123
153
  if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
124
154
  applyAuthHeaders(headers, this.credential);
125
- applyAuditHeaders(headers, this.agent, generateRequestId());
155
+ const requestId = generateRequestId();
156
+ applyAuditHeaders(headers, this.agent, requestId);
126
157
  if (opts.headers) {
127
158
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
128
159
  }
129
160
  const controller = new AbortController();
130
161
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
131
162
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
163
+ const urlStr = url.toString();
164
+ const startedAt = Date.now();
165
+ this.emit({ phase: "request", method, service: opts.service, path: opts.path, url: urlStr, requestId });
132
166
  let res;
133
167
  try {
134
- res = await this.fetchImpl(url.toString(), {
168
+ res = await this.fetchImpl(urlStr, {
135
169
  method,
136
170
  headers,
137
171
  body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
138
172
  signal
139
173
  });
174
+ } catch (err) {
175
+ this.emit({
176
+ phase: "error",
177
+ method,
178
+ service: opts.service,
179
+ path: opts.path,
180
+ url: urlStr,
181
+ requestId,
182
+ durationMs: Date.now() - startedAt,
183
+ error: err instanceof Error ? err.message : String(err)
184
+ });
185
+ throw err;
140
186
  } finally {
141
187
  clearTimeout(timeoutId);
142
188
  }
143
189
  const contentType = res.headers.get("Content-Type") ?? "";
144
190
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
145
191
  if (!res.ok) {
192
+ const durationMs = Date.now() - startedAt;
193
+ const message = describeError(parsed, `HTTP ${res.status}`);
194
+ this.emit({
195
+ phase: "error",
196
+ method,
197
+ service: opts.service,
198
+ path: opts.path,
199
+ url: urlStr,
200
+ requestId,
201
+ durationMs,
202
+ error: message,
203
+ status: res.status
204
+ });
146
205
  if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
147
206
  if (res.status === 403) {
148
207
  throw new TopoloPermissionError(
@@ -152,6 +211,16 @@ var TopoloClient = class {
152
211
  }
153
212
  throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
154
213
  }
214
+ this.emit({
215
+ phase: "response",
216
+ method,
217
+ service: opts.service,
218
+ path: opts.path,
219
+ url: urlStr,
220
+ requestId,
221
+ status: res.status,
222
+ durationMs: Date.now() - startedAt
223
+ });
155
224
  return parsed;
156
225
  }
157
226
  /**
@@ -712,9 +781,36 @@ async function buildClient(options) {
712
781
  agentName: resolveAgentName(config.agentName)
713
782
  },
714
783
  serviceUrls,
715
- requireConfirmForWrites: options?.requireConfirmForWrites
784
+ requireConfirmForWrites: options?.requireConfirmForWrites,
785
+ ...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
716
786
  });
717
787
  }
788
+ function isDebugEnabled() {
789
+ const v = process.env.TOPOLO_DEBUG;
790
+ if (!v) return false;
791
+ return v !== "0" && v.toLowerCase() !== "false";
792
+ }
793
+ function stderrDebugLogger(event) {
794
+ if (event.phase === "request") {
795
+ process.stderr.write(
796
+ `[topolo] \u2192 ${event.method} ${event.service} ${event.path} (req=${event.requestId})
797
+ `
798
+ );
799
+ return;
800
+ }
801
+ if (event.phase === "response") {
802
+ process.stderr.write(
803
+ `[topolo] \u2190 ${event.status} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${event.requestId})
804
+ `
805
+ );
806
+ return;
807
+ }
808
+ const statusPart = event.status ? ` ${event.status}` : "";
809
+ process.stderr.write(
810
+ `[topolo] \u2717${statusPart} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${event.requestId}): ${event.error}
811
+ `
812
+ );
813
+ }
718
814
 
719
815
  // src/commands/whoami.ts
720
816
  function registerWhoamiCommand(program2) {
@@ -810,24 +906,33 @@ function formatContact(c) {
810
906
  }
811
907
 
812
908
  // src/commands/api.ts
813
- var KNOWN_SERVICES = ["auth", "crm"];
909
+ var KNOWN_SERVICES = Object.keys(DEFAULT_SERVICE_URLS).sort();
910
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
814
911
  var WRITE_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
815
912
  function registerApiCommand(program2) {
816
- program2.command("api <service> <method> <path>").description("Generic passthrough to a Topolo service (escape hatch for untyped endpoints)").option("-d, --data <json>", "JSON body (for POST/PUT/PATCH)").option("--confirm", "Acknowledge a mutating request").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
913
+ program2.command("api <service> <method> <path>").description(
914
+ `Generic passthrough to a Topolo service. Known services: ${KNOWN_SERVICES.join(", ")}.`
915
+ ).option("-d, --data <json>", "JSON body (for POST/PUT/PATCH)").option(
916
+ "-q, --query <kv...>",
917
+ "Query params as key=value (repeatable), e.g. --query source=cli page=2"
918
+ ).option("--confirm", "Acknowledge a mutating request").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
817
919
  async (service, method, path, opts) => {
818
920
  const mode = resolveOutputMode(opts.json);
819
921
  try {
820
922
  const upperMethod = method.toUpperCase();
821
- if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(upperMethod)) {
822
- throw new Error(`Unsupported method: ${method}`);
923
+ if (!HTTP_METHODS.includes(upperMethod)) {
924
+ throw new Error(
925
+ `Unsupported method "${method}". Known: ${HTTP_METHODS.join(", ")}`
926
+ );
823
927
  }
928
+ const httpMethod = upperMethod;
824
929
  if (!KNOWN_SERVICES.includes(service)) {
825
930
  throw new Error(
826
931
  `Unknown service "${service}". Known services: ${KNOWN_SERVICES.join(", ")}`
827
932
  );
828
933
  }
829
- if (WRITE_METHODS2.has(upperMethod) && !opts.confirm) {
830
- throw new Error(`Mutating ${upperMethod} requires --confirm.`);
934
+ if (WRITE_METHODS2.has(httpMethod) && !opts.confirm) {
935
+ throw new Error(`Mutating ${httpMethod} requires --confirm.`);
831
936
  }
832
937
  let body = void 0;
833
938
  if (opts.data) {
@@ -837,6 +942,7 @@ function registerApiCommand(program2) {
837
942
  throw new Error("--data must be valid JSON");
838
943
  }
839
944
  }
945
+ const query = parseQueryPairs(opts.query);
840
946
  const topolo = await buildClient();
841
947
  if (!topolo) {
842
948
  throw new Error(
@@ -845,9 +951,10 @@ function registerApiCommand(program2) {
845
951
  }
846
952
  const result = await topolo.client.request({
847
953
  service,
848
- method: upperMethod,
954
+ method: httpMethod,
849
955
  path,
850
956
  body,
957
+ ...query ? { query } : {},
851
958
  confirm: opts.confirm === true
852
959
  });
853
960
  emit(result, mode, () => JSON.stringify(result, null, 2));
@@ -857,6 +964,55 @@ function registerApiCommand(program2) {
857
964
  }
858
965
  );
859
966
  }
967
+ function parseQueryPairs(pairs) {
968
+ if (!pairs || pairs.length === 0) return void 0;
969
+ const out = {};
970
+ for (const pair of pairs) {
971
+ const eq = pair.indexOf("=");
972
+ if (eq <= 0) {
973
+ throw new Error(`Invalid --query entry "${pair}". Expected key=value.`);
974
+ }
975
+ const key = pair.slice(0, eq);
976
+ const value = pair.slice(eq + 1);
977
+ out[key] = value;
978
+ }
979
+ return out;
980
+ }
981
+
982
+ // src/commands/services.ts
983
+ function registerServicesCommand(program2) {
984
+ program2.command("services").description("List every Topolo service the SDK can route to").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action((opts) => {
985
+ const mode = resolveOutputMode(opts.json);
986
+ try {
987
+ const rows = Object.keys(DEFAULT_SERVICE_URLS).sort().map((id) => {
988
+ const defaultUrl = DEFAULT_SERVICE_URLS[id];
989
+ const effectiveUrl = resolveServiceUrl(id);
990
+ return {
991
+ id,
992
+ defaultUrl,
993
+ effectiveUrl,
994
+ overridden: defaultUrl !== effectiveUrl
995
+ };
996
+ });
997
+ emit({ services: rows }, mode, () => formatRows(rows));
998
+ } catch (err) {
999
+ fail(err, mode);
1000
+ }
1001
+ });
1002
+ }
1003
+ function formatRows(rows) {
1004
+ const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
1005
+ const urlWidth = Math.max(7, ...rows.map((r) => r.effectiveUrl.length));
1006
+ const lines = [];
1007
+ lines.push(`${"ID".padEnd(idWidth)} ${"URL".padEnd(urlWidth)} OVERRIDE`);
1008
+ lines.push(`${"-".repeat(idWidth)} ${"-".repeat(urlWidth)} --------`);
1009
+ for (const r of rows) {
1010
+ lines.push(
1011
+ `${r.id.padEnd(idWidth)} ${r.effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : ""}`
1012
+ );
1013
+ }
1014
+ return lines.join("\n");
1015
+ }
860
1016
 
861
1017
  // src/index.ts
862
1018
  var program = new Command();
@@ -867,6 +1023,7 @@ registerAuthCommands(program);
867
1023
  registerWhoamiCommand(program);
868
1024
  registerCrmCommands(program);
869
1025
  registerApiCommand(program);
1026
+ registerServicesCommand(program);
870
1027
  program.parseAsync(process.argv).catch((err) => {
871
1028
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
872
1029
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Command-line interface for the Topolo platform. Designed for humans and for agents (Claude, Codex, etc.) that prefer shell invocation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,16 +13,19 @@
13
13
  "build": "tsup",
14
14
  "dev": "tsup --watch",
15
15
  "typecheck": "tsc --noEmit",
16
- "start": "node dist/index.js"
16
+ "start": "node dist/index.js",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
17
19
  },
18
20
  "dependencies": {
19
21
  "commander": "^12.1.0"
20
22
  },
21
23
  "devDependencies": {
22
- "@topolo/sdk": "file:../packages/topolo-sdk",
24
+ "@topolo/sdk": "latest",
23
25
  "@types/node": "^20.12.0",
24
26
  "tsup": "^8.0.0",
25
- "typescript": "^5.4.0"
27
+ "typescript": "^5.4.0",
28
+ "vitest": "^2.1.0"
26
29
  },
27
30
  "engines": {
28
31
  "node": ">=20"