@topolo/cli 0.1.2 → 0.1.4
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/dist/index.js +396 -8
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -8,7 +8,366 @@ import { spawn } from "child_process";
|
|
|
8
8
|
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
|
|
13
|
+
function applyAuthHeaders(headers, credential) {
|
|
14
|
+
if (credential.kind === "api_key") {
|
|
15
|
+
headers.set("X-Api-Key", credential.apiKey);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
headers.set("Authorization", `Bearer ${credential.accessToken}`);
|
|
19
|
+
}
|
|
20
|
+
function applyAuditHeaders(headers, agent, requestId) {
|
|
21
|
+
headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
|
|
22
|
+
if (agent.agentName) headers.set("X-Topolo-Agent", agent.agentName);
|
|
23
|
+
headers.set("X-Topolo-Request-Id", requestId);
|
|
24
|
+
}
|
|
25
|
+
function generateRequestId() {
|
|
26
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
27
|
+
return crypto.randomUUID();
|
|
28
|
+
}
|
|
29
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
30
|
+
}
|
|
31
|
+
var TopoloSdkError = class extends Error {
|
|
32
|
+
code;
|
|
33
|
+
constructor(code, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "TopoloSdkError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var TopoloAuthError = class extends TopoloSdkError {
|
|
40
|
+
constructor(message, code = "auth_error") {
|
|
41
|
+
super(code, message);
|
|
42
|
+
this.name = "TopoloAuthError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var TopoloPermissionError = class extends TopoloSdkError {
|
|
46
|
+
required;
|
|
47
|
+
constructor(message, required) {
|
|
48
|
+
super("permission_denied", message);
|
|
49
|
+
this.name = "TopoloPermissionError";
|
|
50
|
+
this.required = required;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var TopoloHttpError = class extends TopoloSdkError {
|
|
54
|
+
status;
|
|
55
|
+
body;
|
|
56
|
+
service;
|
|
57
|
+
path;
|
|
58
|
+
constructor(service, path, status, body, message) {
|
|
59
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
|
|
60
|
+
this.name = "TopoloHttpError";
|
|
61
|
+
this.status = status;
|
|
62
|
+
this.body = body;
|
|
63
|
+
this.service = service;
|
|
64
|
+
this.path = path;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var DEFAULT_SERVICE_URLS = {
|
|
68
|
+
auth: "https://auth.topolo.app",
|
|
69
|
+
crm: "https://topolo-crm-worker.topolo.workers.dev"
|
|
70
|
+
};
|
|
71
|
+
var PLATFORM_SERVICE_IDS = {
|
|
72
|
+
crm: "srv_iCwM4jGXcwlj"
|
|
73
|
+
};
|
|
74
|
+
function resolveServiceUrl(service, overrides) {
|
|
75
|
+
const override = overrides?.[service];
|
|
76
|
+
if (override) return override;
|
|
77
|
+
const envKey = `TOPOLO_SERVICE_URL_${service.toUpperCase()}`;
|
|
78
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
79
|
+
if (envValue) return envValue;
|
|
80
|
+
return DEFAULT_SERVICE_URLS[service];
|
|
81
|
+
}
|
|
82
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
83
|
+
var TopoloClient = class {
|
|
84
|
+
credential;
|
|
85
|
+
agent;
|
|
86
|
+
serviceUrls;
|
|
87
|
+
requireConfirmForWrites;
|
|
88
|
+
timeoutMs;
|
|
89
|
+
fetchImpl;
|
|
90
|
+
constructor(options) {
|
|
91
|
+
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
92
|
+
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
93
|
+
this.credential = options.credential;
|
|
94
|
+
this.agent = options.agent;
|
|
95
|
+
this.serviceUrls = options.serviceUrls;
|
|
96
|
+
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
97
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
98
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Low-level JSON request. Prefer the typed module helpers (identity, crm, ...)
|
|
102
|
+
* for anything a caller would reach for; this stays exported for escape-hatch
|
|
103
|
+
* use and for the generic `topolo api` / MCP passthrough tool.
|
|
104
|
+
*/
|
|
105
|
+
async request(opts) {
|
|
106
|
+
const method = opts.method ?? "GET";
|
|
107
|
+
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
108
|
+
throw new TopoloAuthError(
|
|
109
|
+
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const baseUrl = resolveServiceUrl(opts.service, this.serviceUrls);
|
|
113
|
+
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
114
|
+
if (opts.query) {
|
|
115
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
116
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const headers = new Headers();
|
|
120
|
+
headers.set("Accept", "application/json");
|
|
121
|
+
if (opts.body !== void 0) headers.set("Content-Type", "application/json");
|
|
122
|
+
const platformServiceId = PLATFORM_SERVICE_IDS[opts.service];
|
|
123
|
+
if (platformServiceId) headers.set("X-Service-ID", platformServiceId);
|
|
124
|
+
applyAuthHeaders(headers, this.credential);
|
|
125
|
+
applyAuditHeaders(headers, this.agent, generateRequestId());
|
|
126
|
+
if (opts.headers) {
|
|
127
|
+
for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
|
|
128
|
+
}
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
131
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
res = await this.fetchImpl(url.toString(), {
|
|
135
|
+
method,
|
|
136
|
+
headers,
|
|
137
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
138
|
+
signal
|
|
139
|
+
});
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
}
|
|
143
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
144
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
|
|
147
|
+
if (res.status === 403) {
|
|
148
|
+
throw new TopoloPermissionError(
|
|
149
|
+
describeError(parsed, "Permission denied"),
|
|
150
|
+
extractRequired(parsed)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
throw new TopoloHttpError(opts.service, opts.path, res.status, parsed);
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Introspect the current credential. Returns the caller's resolved identity,
|
|
159
|
+
* organization, and permission set. Used by CLI `whoami` and by MCP to gate
|
|
160
|
+
* advertised tools by scope.
|
|
161
|
+
*
|
|
162
|
+
* Both JWT access tokens and platform API keys are resolved via the unified
|
|
163
|
+
* `GET /api/auth/me` endpoint on TopoloAuth, which does not require service
|
|
164
|
+
* credentials. The caller's possession of the credential secret is proof.
|
|
165
|
+
*/
|
|
166
|
+
async introspect() {
|
|
167
|
+
const res = await this.request({
|
|
168
|
+
service: "auth",
|
|
169
|
+
method: "GET",
|
|
170
|
+
path: "/api/auth/me"
|
|
171
|
+
});
|
|
172
|
+
const payload = res.data ?? res;
|
|
173
|
+
const user = payload.user;
|
|
174
|
+
const organization = payload.organization;
|
|
175
|
+
const kind = payload.credentialType === "api_key" ? "api_key" : "access_token";
|
|
176
|
+
return {
|
|
177
|
+
kind,
|
|
178
|
+
user: {
|
|
179
|
+
id: user.id,
|
|
180
|
+
email: user.email,
|
|
181
|
+
name: user.name ?? null,
|
|
182
|
+
role: user.role ?? (kind === "api_key" ? "service" : "member"),
|
|
183
|
+
permissions: payload.permissions ?? user.permissions ?? []
|
|
184
|
+
},
|
|
185
|
+
organization: organization ? {
|
|
186
|
+
id: organization.id,
|
|
187
|
+
slug: organization.slug,
|
|
188
|
+
name: organization.name ?? organization.slug
|
|
189
|
+
} : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function describeError(body, fallback) {
|
|
194
|
+
if (body && typeof body === "object") {
|
|
195
|
+
const maybe = body;
|
|
196
|
+
const resolved = pickString(maybe.message) ?? pickString(maybe.error);
|
|
197
|
+
if (resolved) return resolved;
|
|
198
|
+
}
|
|
199
|
+
return fallback;
|
|
200
|
+
}
|
|
201
|
+
function pickString(value) {
|
|
202
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
203
|
+
if (value && typeof value === "object") {
|
|
204
|
+
const nested = value;
|
|
205
|
+
if (typeof nested.message === "string" && nested.message.trim().length > 0) return nested.message;
|
|
206
|
+
if (typeof nested.description === "string" && nested.description.trim().length > 0)
|
|
207
|
+
return nested.description;
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
function extractRequired(body) {
|
|
212
|
+
if (body && typeof body === "object") {
|
|
213
|
+
const maybe = body;
|
|
214
|
+
if (Array.isArray(maybe.required)) return maybe.required.map(String);
|
|
215
|
+
}
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
function mergeSignals(a, b) {
|
|
219
|
+
if (a.aborted) return a;
|
|
220
|
+
if (b.aborted) return b;
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const onAbort = () => controller.abort();
|
|
223
|
+
a.addEventListener("abort", onAbort, { once: true });
|
|
224
|
+
b.addEventListener("abort", onAbort, { once: true });
|
|
225
|
+
return controller.signal;
|
|
226
|
+
}
|
|
227
|
+
var CrmModule = class {
|
|
228
|
+
constructor(client) {
|
|
229
|
+
this.client = client;
|
|
230
|
+
}
|
|
231
|
+
client;
|
|
232
|
+
async listContacts(options = {}) {
|
|
233
|
+
const res = await this.client.request({
|
|
234
|
+
service: "crm",
|
|
235
|
+
path: "/api/contacts",
|
|
236
|
+
query: {
|
|
237
|
+
q: options.q,
|
|
238
|
+
page: options.page,
|
|
239
|
+
pageSize: options.pageSize
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
const rows = Array.isArray(res?.contacts) ? res.contacts : Array.isArray(res?.data) ? res.data : [];
|
|
243
|
+
return {
|
|
244
|
+
contacts: rows.map(toSummary),
|
|
245
|
+
total: res?.total ?? res?.pagination?.total ?? rows.length,
|
|
246
|
+
page: res?.page ?? res?.pagination?.page ?? options.page ?? 1,
|
|
247
|
+
pageSize: res?.pageSize ?? res?.pagination?.pageSize ?? options.pageSize ?? rows.length
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async getContact(contactId) {
|
|
251
|
+
if (!contactId) throw new Error("contactId is required");
|
|
252
|
+
const res = await this.client.request({
|
|
253
|
+
service: "crm",
|
|
254
|
+
path: `/api/contacts/${encodeURIComponent(contactId)}`
|
|
255
|
+
});
|
|
256
|
+
const row = res.data ?? res;
|
|
257
|
+
return row ? toSummary(row) : null;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
function toSummary(row) {
|
|
261
|
+
return {
|
|
262
|
+
id: row.id,
|
|
263
|
+
firstName: row.first ?? null,
|
|
264
|
+
lastName: row.last ?? null,
|
|
265
|
+
email: row.email ?? null,
|
|
266
|
+
phone: row.phone ?? null,
|
|
267
|
+
company: row.company ?? null,
|
|
268
|
+
leadStatus: row.lead_status ?? null,
|
|
269
|
+
lifecycleStage: row.lifecycle_stage ?? null,
|
|
270
|
+
updatedAt: row.updated_at ?? null
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
var IdentityModule = class {
|
|
274
|
+
constructor(client) {
|
|
275
|
+
this.client = client;
|
|
276
|
+
}
|
|
277
|
+
client;
|
|
278
|
+
/** Returns the resolved user, organization, and permission set for the
|
|
279
|
+
* current credential. Equivalent to `TopoloClient.introspect()` — exposed
|
|
280
|
+
* on this module for symmetry with the other domain modules. */
|
|
281
|
+
whoami() {
|
|
282
|
+
return this.client.introspect();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
286
|
+
var TopoloOAuth = class {
|
|
287
|
+
baseUrl;
|
|
288
|
+
fetchImpl;
|
|
289
|
+
constructor(options = {}) {
|
|
290
|
+
this.baseUrl = resolveServiceUrl("auth", options.serviceUrls);
|
|
291
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* RFC 8628 device authorization grant — starts the flow by asking the server
|
|
295
|
+
* for a device_code + user_code. The caller prints the user_code +
|
|
296
|
+
* verification_uri and then polls `pollDeviceToken` until approval.
|
|
297
|
+
*/
|
|
298
|
+
async requestDeviceCode(params) {
|
|
299
|
+
const body = new URLSearchParams();
|
|
300
|
+
body.set("client_id", params.clientId);
|
|
301
|
+
if (params.scope) {
|
|
302
|
+
body.set("scope", Array.isArray(params.scope) ? params.scope.join(" ") : params.scope);
|
|
303
|
+
}
|
|
304
|
+
return this.#postForm("/api/developer-oauth/device_authorization", body);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Polls the token endpoint once. Returns the token pair on success, or
|
|
308
|
+
* throws a `TopoloAuthError` whose `code` is one of the RFC 8628 poll
|
|
309
|
+
* states: `authorization_pending`, `slow_down`, `access_denied`,
|
|
310
|
+
* `expired_token`. Callers should back off on `slow_down`, wait at least
|
|
311
|
+
* one `interval` on `authorization_pending`, and give up on the rest.
|
|
312
|
+
*/
|
|
313
|
+
async pollDeviceToken(params) {
|
|
314
|
+
const body = new URLSearchParams();
|
|
315
|
+
body.set("grant_type", DEVICE_GRANT);
|
|
316
|
+
body.set("client_id", params.clientId);
|
|
317
|
+
body.set("device_code", params.deviceCode);
|
|
318
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
319
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
320
|
+
}
|
|
321
|
+
/** Exchange an authorization code (with PKCE) for tokens. */
|
|
322
|
+
async exchangeAuthorizationCode(params) {
|
|
323
|
+
const body = new URLSearchParams();
|
|
324
|
+
body.set("grant_type", "authorization_code");
|
|
325
|
+
body.set("client_id", params.clientId);
|
|
326
|
+
body.set("code", params.code);
|
|
327
|
+
body.set("redirect_uri", params.redirectUri);
|
|
328
|
+
if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
|
|
329
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
330
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
331
|
+
}
|
|
332
|
+
/** Rotate a refresh token for a new token pair. */
|
|
333
|
+
async refreshToken(params) {
|
|
334
|
+
const body = new URLSearchParams();
|
|
335
|
+
body.set("grant_type", "refresh_token");
|
|
336
|
+
body.set("client_id", params.clientId);
|
|
337
|
+
body.set("refresh_token", params.refreshToken);
|
|
338
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
339
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
340
|
+
}
|
|
341
|
+
async #postForm(path, body) {
|
|
342
|
+
const url = new URL(path, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
343
|
+
const res = await this.fetchImpl(url.toString(), {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
347
|
+
Accept: "application/json"
|
|
348
|
+
},
|
|
349
|
+
body: body.toString()
|
|
350
|
+
});
|
|
351
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
352
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
353
|
+
if (!res.ok) {
|
|
354
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
355
|
+
const err = parsed;
|
|
356
|
+
throw new TopoloAuthError(err.error_description || err.error, err.error);
|
|
357
|
+
}
|
|
358
|
+
throw new TopoloHttpError("auth", path, res.status, parsed);
|
|
359
|
+
}
|
|
360
|
+
return parsed;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function createTopolo(options) {
|
|
364
|
+
const client = new TopoloClient(options);
|
|
365
|
+
return {
|
|
366
|
+
client,
|
|
367
|
+
identity: new IdentityModule(client),
|
|
368
|
+
crm: new CrmModule(client)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
12
371
|
|
|
13
372
|
// src/config.ts
|
|
14
373
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
@@ -50,6 +409,37 @@ async function resolveCredential() {
|
|
|
50
409
|
const config = await readConfig();
|
|
51
410
|
return config.credential ?? null;
|
|
52
411
|
}
|
|
412
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
413
|
+
async function refreshCredentialIfNeeded() {
|
|
414
|
+
if (process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
415
|
+
return resolveCredential();
|
|
416
|
+
}
|
|
417
|
+
const config = await readConfig();
|
|
418
|
+
const cred = config.credential;
|
|
419
|
+
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
420
|
+
if (!cred.refreshToken || !cred.expiresAt || !config.oauthClientId) return cred;
|
|
421
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
422
|
+
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
423
|
+
try {
|
|
424
|
+
const oauth = new TopoloOAuth({
|
|
425
|
+
serviceUrls: resolveServiceUrls(config.serviceUrls)
|
|
426
|
+
});
|
|
427
|
+
const tokens = await oauth.refreshToken({
|
|
428
|
+
clientId: config.oauthClientId,
|
|
429
|
+
refreshToken: cred.refreshToken
|
|
430
|
+
});
|
|
431
|
+
const refreshed = {
|
|
432
|
+
kind: "access_token",
|
|
433
|
+
accessToken: tokens.access_token,
|
|
434
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
435
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
436
|
+
};
|
|
437
|
+
await writeConfig({ ...config, credential: refreshed });
|
|
438
|
+
return refreshed;
|
|
439
|
+
} catch {
|
|
440
|
+
return cred;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
53
443
|
function resolveAgentName(configAgentName) {
|
|
54
444
|
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
55
445
|
}
|
|
@@ -226,6 +616,7 @@ async function runDeviceFlow(options) {
|
|
|
226
616
|
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
227
617
|
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
228
618
|
};
|
|
619
|
+
updated.oauthClientId = options.clientId;
|
|
229
620
|
await writeConfig(updated);
|
|
230
621
|
emit(
|
|
231
622
|
{
|
|
@@ -304,15 +695,12 @@ function sleep(ms) {
|
|
|
304
695
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
305
696
|
}
|
|
306
697
|
|
|
307
|
-
// src/client.ts
|
|
308
|
-
import { createTopolo } from "@topolo/sdk";
|
|
309
|
-
|
|
310
698
|
// src/version.ts
|
|
311
|
-
var CLI_VERSION = "0.1.
|
|
699
|
+
var CLI_VERSION = "0.1.4";
|
|
312
700
|
|
|
313
701
|
// src/client.ts
|
|
314
702
|
async function buildClient(options) {
|
|
315
|
-
const credential = await
|
|
703
|
+
const credential = await refreshCredentialIfNeeded();
|
|
316
704
|
if (!credential) return null;
|
|
317
705
|
const config = await readConfig();
|
|
318
706
|
const serviceUrls = resolveServiceUrls(config.serviceUrls);
|
|
@@ -423,7 +811,7 @@ function formatContact(c) {
|
|
|
423
811
|
|
|
424
812
|
// src/commands/api.ts
|
|
425
813
|
var KNOWN_SERVICES = ["auth", "crm"];
|
|
426
|
-
var
|
|
814
|
+
var WRITE_METHODS2 = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
427
815
|
function registerApiCommand(program2) {
|
|
428
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(
|
|
429
817
|
async (service, method, path, opts) => {
|
|
@@ -438,7 +826,7 @@ function registerApiCommand(program2) {
|
|
|
438
826
|
`Unknown service "${service}". Known services: ${KNOWN_SERVICES.join(", ")}`
|
|
439
827
|
);
|
|
440
828
|
}
|
|
441
|
-
if (
|
|
829
|
+
if (WRITE_METHODS2.has(upperMethod) && !opts.confirm) {
|
|
442
830
|
throw new Error(`Mutating ${upperMethod} requires --confirm.`);
|
|
443
831
|
}
|
|
444
832
|
let body = void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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": {
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
"start": "node dist/index.js"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@topolo/sdk": "file:../packages/topolo-sdk",
|
|
20
19
|
"commander": "^12.1.0"
|
|
21
20
|
},
|
|
22
21
|
"devDependencies": {
|
|
22
|
+
"@topolo/sdk": "file:../packages/topolo-sdk",
|
|
23
23
|
"@types/node": "^20.12.0",
|
|
24
24
|
"tsup": "^8.0.0",
|
|
25
25
|
"typescript": "^5.4.0"
|