@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.
- package/README.md +25 -6
- package/dist/index.js +171 -14
- 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);
|
|
@@ -64,17 +64,38 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
64
64
|
this.path = path;
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
-
var
|
|
67
|
+
var SERVICES = {
|
|
68
|
+
agent: "https://topolo-agent-worker.topolo.workers.dev",
|
|
68
69
|
auth: "https://auth.topolo.app",
|
|
69
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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 (!
|
|
822
|
-
throw new Error(
|
|
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(
|
|
830
|
-
throw new Error(`Mutating ${
|
|
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:
|
|
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.
|
|
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": "
|
|
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"
|