@topolo/cli 0.1.4 → 0.1.5

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 +79 -4
  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);
@@ -87,6 +87,7 @@ var TopoloClient = class {
87
87
  requireConfirmForWrites;
88
88
  timeoutMs;
89
89
  fetchImpl;
90
+ debug;
90
91
  constructor(options) {
91
92
  if (!options.credential) throw new TopoloAuthError("credential is required");
92
93
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
@@ -96,6 +97,14 @@ var TopoloClient = class {
96
97
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
97
98
  this.timeoutMs = options.timeoutMs ?? 3e4;
98
99
  this.fetchImpl = options.fetch ?? fetch;
100
+ this.debug = options.debug;
101
+ }
102
+ emit(event) {
103
+ if (!this.debug) return;
104
+ try {
105
+ this.debug(event);
106
+ } catch {
107
+ }
99
108
  }
100
109
  /**
101
110
  * Low-level JSON request. Prefer the typed module helpers (identity, crm, ...)
@@ -122,27 +131,56 @@ var TopoloClient = class {
122
131
  const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
123
132
  if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
124
133
  applyAuthHeaders(headers, this.credential);
125
- applyAuditHeaders(headers, this.agent, generateRequestId());
134
+ const requestId = generateRequestId();
135
+ applyAuditHeaders(headers, this.agent, requestId);
126
136
  if (opts.headers) {
127
137
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
128
138
  }
129
139
  const controller = new AbortController();
130
140
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
131
141
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
142
+ const urlStr = url.toString();
143
+ const startedAt = Date.now();
144
+ this.emit({ phase: "request", method, service: opts.service, path: opts.path, url: urlStr, requestId });
132
145
  let res;
133
146
  try {
134
- res = await this.fetchImpl(url.toString(), {
147
+ res = await this.fetchImpl(urlStr, {
135
148
  method,
136
149
  headers,
137
150
  body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
138
151
  signal
139
152
  });
153
+ } catch (err) {
154
+ this.emit({
155
+ phase: "error",
156
+ method,
157
+ service: opts.service,
158
+ path: opts.path,
159
+ url: urlStr,
160
+ requestId,
161
+ durationMs: Date.now() - startedAt,
162
+ error: err instanceof Error ? err.message : String(err)
163
+ });
164
+ throw err;
140
165
  } finally {
141
166
  clearTimeout(timeoutId);
142
167
  }
143
168
  const contentType = res.headers.get("Content-Type") ?? "";
144
169
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
145
170
  if (!res.ok) {
171
+ const durationMs = Date.now() - startedAt;
172
+ const message = describeError(parsed, `HTTP ${res.status}`);
173
+ this.emit({
174
+ phase: "error",
175
+ method,
176
+ service: opts.service,
177
+ path: opts.path,
178
+ url: urlStr,
179
+ requestId,
180
+ durationMs,
181
+ error: message,
182
+ status: res.status
183
+ });
146
184
  if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
147
185
  if (res.status === 403) {
148
186
  throw new TopoloPermissionError(
@@ -152,6 +190,16 @@ var TopoloClient = class {
152
190
  }
153
191
  throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
154
192
  }
193
+ this.emit({
194
+ phase: "response",
195
+ method,
196
+ service: opts.service,
197
+ path: opts.path,
198
+ url: urlStr,
199
+ requestId,
200
+ status: res.status,
201
+ durationMs: Date.now() - startedAt
202
+ });
155
203
  return parsed;
156
204
  }
157
205
  /**
@@ -712,9 +760,36 @@ async function buildClient(options) {
712
760
  agentName: resolveAgentName(config.agentName)
713
761
  },
714
762
  serviceUrls,
715
- requireConfirmForWrites: options?.requireConfirmForWrites
763
+ requireConfirmForWrites: options?.requireConfirmForWrites,
764
+ ...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
716
765
  });
717
766
  }
767
+ function isDebugEnabled() {
768
+ const v = process.env.TOPOLO_DEBUG;
769
+ if (!v) return false;
770
+ return v !== "0" && v.toLowerCase() !== "false";
771
+ }
772
+ function stderrDebugLogger(event) {
773
+ if (event.phase === "request") {
774
+ process.stderr.write(
775
+ `[topolo] \u2192 ${event.method} ${event.service} ${event.path} (req=${event.requestId})
776
+ `
777
+ );
778
+ return;
779
+ }
780
+ if (event.phase === "response") {
781
+ process.stderr.write(
782
+ `[topolo] \u2190 ${event.status} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${event.requestId})
783
+ `
784
+ );
785
+ return;
786
+ }
787
+ const statusPart = event.status ? ` ${event.status}` : "";
788
+ process.stderr.write(
789
+ `[topolo] \u2717${statusPart} ${event.method} ${event.service} ${event.path} (${event.durationMs}ms, req=${event.requestId}): ${event.error}
790
+ `
791
+ );
792
+ }
718
793
 
719
794
  // src/commands/whoami.ts
720
795
  function registerWhoamiCommand(program2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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"