@topolo/cli 0.1.3 → 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.
- package/README.md +25 -6
- package/dist/index.js +113 -6
- 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
|
-
##
|
|
140
|
+
## Debug output
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
/**
|
|
@@ -409,6 +457,37 @@ async function resolveCredential() {
|
|
|
409
457
|
const config = await readConfig();
|
|
410
458
|
return config.credential ?? null;
|
|
411
459
|
}
|
|
460
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
461
|
+
async function refreshCredentialIfNeeded() {
|
|
462
|
+
if (process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
463
|
+
return resolveCredential();
|
|
464
|
+
}
|
|
465
|
+
const config = await readConfig();
|
|
466
|
+
const cred = config.credential;
|
|
467
|
+
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
468
|
+
if (!cred.refreshToken || !cred.expiresAt || !config.oauthClientId) return cred;
|
|
469
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
470
|
+
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
471
|
+
try {
|
|
472
|
+
const oauth = new TopoloOAuth({
|
|
473
|
+
serviceUrls: resolveServiceUrls(config.serviceUrls)
|
|
474
|
+
});
|
|
475
|
+
const tokens = await oauth.refreshToken({
|
|
476
|
+
clientId: config.oauthClientId,
|
|
477
|
+
refreshToken: cred.refreshToken
|
|
478
|
+
});
|
|
479
|
+
const refreshed = {
|
|
480
|
+
kind: "access_token",
|
|
481
|
+
accessToken: tokens.access_token,
|
|
482
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
483
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
484
|
+
};
|
|
485
|
+
await writeConfig({ ...config, credential: refreshed });
|
|
486
|
+
return refreshed;
|
|
487
|
+
} catch {
|
|
488
|
+
return cred;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
412
491
|
function resolveAgentName(configAgentName) {
|
|
413
492
|
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
414
493
|
}
|
|
@@ -585,6 +664,7 @@ async function runDeviceFlow(options) {
|
|
|
585
664
|
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
586
665
|
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
587
666
|
};
|
|
667
|
+
updated.oauthClientId = options.clientId;
|
|
588
668
|
await writeConfig(updated);
|
|
589
669
|
emit(
|
|
590
670
|
{
|
|
@@ -664,11 +744,11 @@ function sleep(ms) {
|
|
|
664
744
|
}
|
|
665
745
|
|
|
666
746
|
// src/version.ts
|
|
667
|
-
var CLI_VERSION = "0.1.
|
|
747
|
+
var CLI_VERSION = "0.1.4";
|
|
668
748
|
|
|
669
749
|
// src/client.ts
|
|
670
750
|
async function buildClient(options) {
|
|
671
|
-
const credential = await
|
|
751
|
+
const credential = await refreshCredentialIfNeeded();
|
|
672
752
|
if (!credential) return null;
|
|
673
753
|
const config = await readConfig();
|
|
674
754
|
const serviceUrls = resolveServiceUrls(config.serviceUrls);
|
|
@@ -680,9 +760,36 @@ async function buildClient(options) {
|
|
|
680
760
|
agentName: resolveAgentName(config.agentName)
|
|
681
761
|
},
|
|
682
762
|
serviceUrls,
|
|
683
|
-
requireConfirmForWrites: options?.requireConfirmForWrites
|
|
763
|
+
requireConfirmForWrites: options?.requireConfirmForWrites,
|
|
764
|
+
...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
|
|
684
765
|
});
|
|
685
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
|
+
}
|
|
686
793
|
|
|
687
794
|
// src/commands/whoami.ts
|
|
688
795
|
function registerWhoamiCommand(program2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.1.
|
|
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": "
|
|
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"
|