@dotta/xc 0.1.0
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 +348 -0
- package/dist/__tests__/bookmarks.test.d.ts +4 -0
- package/dist/__tests__/bookmarks.test.js +104 -0
- package/dist/__tests__/bookmarks.test.js.map +1 -0
- package/dist/__tests__/budget.test.d.ts +6 -0
- package/dist/__tests__/budget.test.js +105 -0
- package/dist/__tests__/budget.test.js.map +1 -0
- package/dist/__tests__/dm.test.d.ts +4 -0
- package/dist/__tests__/dm.test.js +115 -0
- package/dist/__tests__/dm.test.js.map +1 -0
- package/dist/__tests__/followers.test.d.ts +4 -0
- package/dist/__tests__/followers.test.js +129 -0
- package/dist/__tests__/followers.test.js.map +1 -0
- package/dist/__tests__/lib/api.test.d.ts +5 -0
- package/dist/__tests__/lib/api.test.js +202 -0
- package/dist/__tests__/lib/api.test.js.map +1 -0
- package/dist/__tests__/lib/budget.test.d.ts +5 -0
- package/dist/__tests__/lib/budget.test.js +194 -0
- package/dist/__tests__/lib/budget.test.js.map +1 -0
- package/dist/__tests__/lib/config.test.d.ts +6 -0
- package/dist/__tests__/lib/config.test.js +228 -0
- package/dist/__tests__/lib/config.test.js.map +1 -0
- package/dist/__tests__/lib/cost.test.d.ts +6 -0
- package/dist/__tests__/lib/cost.test.js +177 -0
- package/dist/__tests__/lib/cost.test.js.map +1 -0
- package/dist/__tests__/lib/format.test.d.ts +4 -0
- package/dist/__tests__/lib/format.test.js +139 -0
- package/dist/__tests__/lib/format.test.js.map +1 -0
- package/dist/__tests__/lib/oauth.test.d.ts +5 -0
- package/dist/__tests__/lib/oauth.test.js +123 -0
- package/dist/__tests__/lib/oauth.test.js.map +1 -0
- package/dist/__tests__/lib/resolve.test.d.ts +4 -0
- package/dist/__tests__/lib/resolve.test.js +154 -0
- package/dist/__tests__/lib/resolve.test.js.map +1 -0
- package/dist/__tests__/lists.test.d.ts +4 -0
- package/dist/__tests__/lists.test.js +96 -0
- package/dist/__tests__/lists.test.js.map +1 -0
- package/dist/__tests__/media.test.d.ts +4 -0
- package/dist/__tests__/media.test.js +132 -0
- package/dist/__tests__/media.test.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +93 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +191 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/block.d.ts +15 -0
- package/dist/commands/block.js +117 -0
- package/dist/commands/block.js.map +1 -0
- package/dist/commands/bookmarks.d.ts +12 -0
- package/dist/commands/bookmarks.js +100 -0
- package/dist/commands/bookmarks.js.map +1 -0
- package/dist/commands/budget.d.ts +9 -0
- package/dist/commands/budget.js +124 -0
- package/dist/commands/budget.js.map +1 -0
- package/dist/commands/cost.d.ts +5 -0
- package/dist/commands/cost.js +75 -0
- package/dist/commands/cost.js.map +1 -0
- package/dist/commands/delete.d.ts +8 -0
- package/dist/commands/delete.js +31 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +179 -0
- package/dist/commands/dm.js.map +1 -0
- package/dist/commands/engagement.d.ts +14 -0
- package/dist/commands/engagement.js +167 -0
- package/dist/commands/engagement.js.map +1 -0
- package/dist/commands/followers.d.ts +14 -0
- package/dist/commands/followers.js +138 -0
- package/dist/commands/followers.js.map +1 -0
- package/dist/commands/get.d.ts +2 -0
- package/dist/commands/get.js +63 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/hide.d.ts +10 -0
- package/dist/commands/hide.js +58 -0
- package/dist/commands/hide.js.map +1 -0
- package/dist/commands/like.d.ts +3 -0
- package/dist/commands/like.js +52 -0
- package/dist/commands/like.js.map +1 -0
- package/dist/commands/lists.d.ts +20 -0
- package/dist/commands/lists.js +384 -0
- package/dist/commands/lists.js.map +1 -0
- package/dist/commands/media.d.ts +19 -0
- package/dist/commands/media.js +205 -0
- package/dist/commands/media.js.map +1 -0
- package/dist/commands/mentions.d.ts +8 -0
- package/dist/commands/mentions.js +59 -0
- package/dist/commands/mentions.js.map +1 -0
- package/dist/commands/mute.d.ts +12 -0
- package/dist/commands/mute.js +99 -0
- package/dist/commands/mute.js.map +1 -0
- package/dist/commands/post.d.ts +11 -0
- package/dist/commands/post.js +87 -0
- package/dist/commands/post.js.map +1 -0
- package/dist/commands/repost.d.ts +10 -0
- package/dist/commands/repost.js +59 -0
- package/dist/commands/repost.js.map +1 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +49 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/stream.d.ts +13 -0
- package/dist/commands/stream.js +251 -0
- package/dist/commands/stream.js.map +1 -0
- package/dist/commands/timeline.d.ts +2 -0
- package/dist/commands/timeline.js +61 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/commands/trends.d.ts +10 -0
- package/dist/commands/trends.js +59 -0
- package/dist/commands/trends.js.map +1 -0
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +52 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/user.d.ts +2 -0
- package/dist/commands/user.js +43 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/usersearch.d.ts +8 -0
- package/dist/commands/usersearch.js +48 -0
- package/dist/commands/usersearch.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +54 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +91 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/budget.d.ts +44 -0
- package/dist/lib/budget.js +119 -0
- package/dist/lib/budget.js.map +1 -0
- package/dist/lib/config.d.ts +39 -0
- package/dist/lib/config.js +63 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/cost.d.ts +43 -0
- package/dist/lib/cost.js +224 -0
- package/dist/lib/cost.js.map +1 -0
- package/dist/lib/format.d.ts +24 -0
- package/dist/lib/format.js +72 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/oauth.d.ts +32 -0
- package/dist/lib/oauth.js +132 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/lib/resolve.d.ts +12 -0
- package/dist/lib/resolve.js +48 -0
- package/dist/lib/resolve.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,MAAM,WAAW,GAAG;IAClB,YAAY;IACZ,aAAa;IACb,gBAAgB;IAChB,UAAU;IACV,UAAU;IACV,KAAK;IACL,mBAAmB;CACpB,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,6BAA6B,CAAC;SAC1C,MAAM,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;SAC9C,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC;SACnC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACrB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC;YAErE,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACd,UAAU,CAAC,MAAM,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;YACzB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;gBAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;YAED,MAAM,CAAC,GAAG,IAAI,CAAC,aAAmD,CAAC;YAEnE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAChD,IAAI,IAAI,CAAC,WAAW;gBAAE,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YAC3D,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YACxD,IAAI,IAAI,CAAC,GAAG;gBAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YAC9C,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YAC/C,IAAI,CAAC,EAAE,CAAC;gBACN,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,cAAc,EAAE,QAAQ,CAClK,CAAC;YACJ,CAAC;YACD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CACT,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,kBAAkB,EAAE,EAAE,CAC5D,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates authenticated SDK Client instances from stored credentials.
|
|
3
|
+
* Handles token refresh for OAuth2 accounts.
|
|
4
|
+
* Wraps the client with a Proxy that logs API calls and enforces budgets.
|
|
5
|
+
*/
|
|
6
|
+
import { Client } from "@xdevplatform/xdk";
|
|
7
|
+
/**
|
|
8
|
+
* Create an authenticated XDK Client from stored account credentials.
|
|
9
|
+
* Automatically refreshes OAuth2 tokens if expired.
|
|
10
|
+
* Returns a proxy-wrapped client that logs usage and enforces budgets.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getClient(accountName?: string): Promise<Client>;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates authenticated SDK Client instances from stored credentials.
|
|
3
|
+
* Handles token refresh for OAuth2 accounts.
|
|
4
|
+
* Wraps the client with a Proxy that logs API calls and enforces budgets.
|
|
5
|
+
*/
|
|
6
|
+
import { Client } from "@xdevplatform/xdk";
|
|
7
|
+
import { getAccount, setAccount, } from "./config.js";
|
|
8
|
+
import { refreshAccessToken } from "./oauth.js";
|
|
9
|
+
import { logApiCall } from "./cost.js";
|
|
10
|
+
import { checkBudget } from "./budget.js";
|
|
11
|
+
/**
|
|
12
|
+
* Wrap an SDK Client so every namespace method call (e.g. client.posts.searchRecent)
|
|
13
|
+
* is intercepted to check budget limits and log usage before forwarding.
|
|
14
|
+
*/
|
|
15
|
+
function wrapClient(client) {
|
|
16
|
+
return new Proxy(client, {
|
|
17
|
+
get(target, prop) {
|
|
18
|
+
const value = target[prop];
|
|
19
|
+
// Only proxy object namespaces (posts, users, usage, etc.)
|
|
20
|
+
if (value && typeof value === "object" && typeof prop === "string") {
|
|
21
|
+
return new Proxy(value, {
|
|
22
|
+
get(nsTarget, nsProp) {
|
|
23
|
+
const nsValue = nsTarget[nsProp];
|
|
24
|
+
// Wrap functions to add budget check + logging
|
|
25
|
+
if (typeof nsValue === "function" && typeof nsProp === "string") {
|
|
26
|
+
return async (...args) => {
|
|
27
|
+
const endpoint = `${prop}.${nsProp}`;
|
|
28
|
+
await checkBudget(endpoint);
|
|
29
|
+
logApiCall(endpoint);
|
|
30
|
+
return nsValue.apply(nsTarget, args);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return nsValue;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create an authenticated XDK Client from stored account credentials.
|
|
43
|
+
* Automatically refreshes OAuth2 tokens if expired.
|
|
44
|
+
* Returns a proxy-wrapped client that logs usage and enforces budgets.
|
|
45
|
+
*/
|
|
46
|
+
export async function getClient(accountName) {
|
|
47
|
+
const account = getAccount(accountName);
|
|
48
|
+
if (!account) {
|
|
49
|
+
throw new Error(`No account configured${accountName ? ` (${accountName})` : ""}. Run: xc auth login`);
|
|
50
|
+
}
|
|
51
|
+
const { auth } = account;
|
|
52
|
+
// Bearer token — straightforward
|
|
53
|
+
if (auth.type === "bearer") {
|
|
54
|
+
if (!auth.bearerToken) {
|
|
55
|
+
throw new Error("Bearer token is empty. Run: xc auth token <TOKEN>");
|
|
56
|
+
}
|
|
57
|
+
return wrapClient(new Client({ bearerToken: auth.bearerToken }));
|
|
58
|
+
}
|
|
59
|
+
// OAuth 2.0 — check expiry, refresh if needed
|
|
60
|
+
if (auth.type === "oauth2") {
|
|
61
|
+
if (!auth.accessToken) {
|
|
62
|
+
throw new Error("No access token. Run: xc auth login");
|
|
63
|
+
}
|
|
64
|
+
// Refresh if token is expired or within 60s of expiry
|
|
65
|
+
const expiresAt = auth.expiresAt ?? 0;
|
|
66
|
+
if (Date.now() >= expiresAt - 60_000) {
|
|
67
|
+
if (!auth.refreshToken || !auth.clientId) {
|
|
68
|
+
throw new Error("Token expired and no refresh token available. Run: xc auth login");
|
|
69
|
+
}
|
|
70
|
+
console.error("Refreshing access token...");
|
|
71
|
+
const result = await refreshAccessToken({
|
|
72
|
+
clientId: auth.clientId,
|
|
73
|
+
clientSecret: auth.clientSecret,
|
|
74
|
+
refreshToken: auth.refreshToken,
|
|
75
|
+
});
|
|
76
|
+
// Persist refreshed credentials
|
|
77
|
+
const updatedAuth = {
|
|
78
|
+
...auth,
|
|
79
|
+
accessToken: result.accessToken,
|
|
80
|
+
refreshToken: result.refreshToken ?? auth.refreshToken,
|
|
81
|
+
expiresAt: result.expiresAt,
|
|
82
|
+
};
|
|
83
|
+
const name = accountName ?? "default";
|
|
84
|
+
setAccount(name, { ...account, auth: updatedAuth });
|
|
85
|
+
return wrapClient(new Client({ accessToken: result.accessToken }));
|
|
86
|
+
}
|
|
87
|
+
return wrapClient(new Client({ accessToken: auth.accessToken }));
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unknown auth type: ${auth.type}`);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget enforcement for API cost tracking.
|
|
3
|
+
* Stores budget config in ~/.xc/budget.json.
|
|
4
|
+
* Checks daily spend against configured limits before each API call.
|
|
5
|
+
* Supports optional password protection via scrypt hashing.
|
|
6
|
+
*/
|
|
7
|
+
export type BudgetAction = "block" | "warn" | "confirm";
|
|
8
|
+
export interface BudgetConfig {
|
|
9
|
+
daily?: number;
|
|
10
|
+
action: BudgetAction;
|
|
11
|
+
/** scrypt hash of the lock password (hex). */
|
|
12
|
+
passwordHash?: string;
|
|
13
|
+
/** Salt used for scrypt hashing (hex). */
|
|
14
|
+
passwordSalt?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Path to budget configuration file. */
|
|
17
|
+
export declare function getBudgetPath(): string;
|
|
18
|
+
/** Load budget config, returning defaults if none exists. */
|
|
19
|
+
export declare function loadBudget(): BudgetConfig;
|
|
20
|
+
/** Save budget config to disk. */
|
|
21
|
+
export declare function saveBudget(config: BudgetConfig): void;
|
|
22
|
+
/** Remove budget configuration entirely. */
|
|
23
|
+
export declare function resetBudget(): void;
|
|
24
|
+
/** Hash a password with scrypt using the given salt. Returns hex string. */
|
|
25
|
+
export declare function hashPassword(password: string, salt: string): string;
|
|
26
|
+
/** Check if the budget is password-locked. */
|
|
27
|
+
export declare function isLocked(): boolean;
|
|
28
|
+
/** Verify a password against the stored hash. Returns true if correct. */
|
|
29
|
+
export declare function verifyPassword(password: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Lock the budget with a password.
|
|
32
|
+
* Generates a random salt and stores the scrypt hash.
|
|
33
|
+
*/
|
|
34
|
+
export declare function lockBudget(password: string): void;
|
|
35
|
+
/** Remove the password lock from the budget. */
|
|
36
|
+
export declare function unlockBudget(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Check whether an API call would exceed the daily budget.
|
|
39
|
+
* Behavior depends on the configured action:
|
|
40
|
+
* block — throw, refusing the call
|
|
41
|
+
* warn — print warning to stderr, proceed
|
|
42
|
+
* confirm — prompt user, throw if declined
|
|
43
|
+
*/
|
|
44
|
+
export declare function checkBudget(endpoint: string): Promise<void>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget enforcement for API cost tracking.
|
|
3
|
+
* Stores budget config in ~/.xc/budget.json.
|
|
4
|
+
* Checks daily spend against configured limits before each API call.
|
|
5
|
+
* Supports optional password protection via scrypt hashing.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import readline from "node:readline";
|
|
11
|
+
import { getConfigDir, ensureConfigDir } from "./config.js";
|
|
12
|
+
import { loadUsageLog, computeTodaySpend, estimateCost } from "./cost.js";
|
|
13
|
+
/** Path to budget configuration file. */
|
|
14
|
+
export function getBudgetPath() {
|
|
15
|
+
return path.join(getConfigDir(), "budget.json");
|
|
16
|
+
}
|
|
17
|
+
/** Load budget config, returning defaults if none exists. */
|
|
18
|
+
export function loadBudget() {
|
|
19
|
+
const budgetPath = getBudgetPath();
|
|
20
|
+
if (!fs.existsSync(budgetPath)) {
|
|
21
|
+
return { action: "warn" };
|
|
22
|
+
}
|
|
23
|
+
const raw = fs.readFileSync(budgetPath, "utf-8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
/** Save budget config to disk. */
|
|
27
|
+
export function saveBudget(config) {
|
|
28
|
+
ensureConfigDir();
|
|
29
|
+
fs.writeFileSync(getBudgetPath(), JSON.stringify(config, null, 2) + "\n");
|
|
30
|
+
}
|
|
31
|
+
/** Remove budget configuration entirely. */
|
|
32
|
+
export function resetBudget() {
|
|
33
|
+
const budgetPath = getBudgetPath();
|
|
34
|
+
if (fs.existsSync(budgetPath)) {
|
|
35
|
+
fs.unlinkSync(budgetPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Hash a password with scrypt using the given salt. Returns hex string. */
|
|
39
|
+
export function hashPassword(password, salt) {
|
|
40
|
+
return crypto.scryptSync(password, salt, 64).toString("hex");
|
|
41
|
+
}
|
|
42
|
+
/** Check if the budget is password-locked. */
|
|
43
|
+
export function isLocked() {
|
|
44
|
+
const budget = loadBudget();
|
|
45
|
+
return !!(budget.passwordHash && budget.passwordSalt);
|
|
46
|
+
}
|
|
47
|
+
/** Verify a password against the stored hash. Returns true if correct. */
|
|
48
|
+
export function verifyPassword(password) {
|
|
49
|
+
const budget = loadBudget();
|
|
50
|
+
if (!budget.passwordHash || !budget.passwordSalt)
|
|
51
|
+
return true;
|
|
52
|
+
const hash = hashPassword(password, budget.passwordSalt);
|
|
53
|
+
return hash === budget.passwordHash;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Lock the budget with a password.
|
|
57
|
+
* Generates a random salt and stores the scrypt hash.
|
|
58
|
+
*/
|
|
59
|
+
export function lockBudget(password) {
|
|
60
|
+
const budget = loadBudget();
|
|
61
|
+
const salt = crypto.randomBytes(32).toString("hex");
|
|
62
|
+
budget.passwordSalt = salt;
|
|
63
|
+
budget.passwordHash = hashPassword(password, salt);
|
|
64
|
+
saveBudget(budget);
|
|
65
|
+
}
|
|
66
|
+
/** Remove the password lock from the budget. */
|
|
67
|
+
export function unlockBudget() {
|
|
68
|
+
const budget = loadBudget();
|
|
69
|
+
delete budget.passwordHash;
|
|
70
|
+
delete budget.passwordSalt;
|
|
71
|
+
saveBudget(budget);
|
|
72
|
+
}
|
|
73
|
+
/** Prompt the user for y/N confirmation on stderr. */
|
|
74
|
+
async function confirmPrompt(message) {
|
|
75
|
+
const rl = readline.createInterface({
|
|
76
|
+
input: process.stdin,
|
|
77
|
+
output: process.stderr,
|
|
78
|
+
});
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
resolve(answer.toLowerCase() === "y");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check whether an API call would exceed the daily budget.
|
|
88
|
+
* Behavior depends on the configured action:
|
|
89
|
+
* block — throw, refusing the call
|
|
90
|
+
* warn — print warning to stderr, proceed
|
|
91
|
+
* confirm — prompt user, throw if declined
|
|
92
|
+
*/
|
|
93
|
+
export async function checkBudget(endpoint) {
|
|
94
|
+
const budget = loadBudget();
|
|
95
|
+
if (!budget.daily)
|
|
96
|
+
return;
|
|
97
|
+
const entries = loadUsageLog();
|
|
98
|
+
const todaySpend = computeTodaySpend(entries);
|
|
99
|
+
const callCost = estimateCost(endpoint);
|
|
100
|
+
if (todaySpend + callCost <= budget.daily)
|
|
101
|
+
return;
|
|
102
|
+
const msg = `Daily budget $${budget.daily.toFixed(2)} exceeded ` +
|
|
103
|
+
`(today: $${todaySpend.toFixed(2)} + $${callCost.toFixed(2)})`;
|
|
104
|
+
switch (budget.action) {
|
|
105
|
+
case "block":
|
|
106
|
+
throw new Error(`${msg}. Use 'xc budget reset' or increase your budget.`);
|
|
107
|
+
case "warn":
|
|
108
|
+
console.error(`Warning: ${msg}`);
|
|
109
|
+
break;
|
|
110
|
+
case "confirm": {
|
|
111
|
+
const proceed = await confirmPrompt(`${msg}. Continue?`);
|
|
112
|
+
if (!proceed) {
|
|
113
|
+
throw new Error("Cancelled by user.");
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/lib/budget.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAa1E,yCAAyC;AACzC,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,aAAa,CAAC,CAAC;AAClD,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,UAAU;IACxB,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;AACzC,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,UAAU,CAAC,MAAoB;IAC7C,eAAe,EAAE,CAAC;IAClB,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,WAAW;IACzB,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,IAAY;IACzD,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/D,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,QAAQ;IACtB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC;AACxD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAC9D,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IACzD,OAAO,IAAI,KAAK,MAAM,CAAC,YAAY,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,YAAY,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,YAAY,CAAC;IAC3B,OAAO,MAAM,CAAC,YAAY,CAAC;IAC3B,UAAU,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC;AAED,sDAAsD;AACtD,KAAK,UAAU,aAAa,CAAC,OAAe;IAC1C,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,EAAE,CAAC,QAAQ,CAAC,GAAG,OAAO,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE;YAC1C,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAgB;IAChD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO;IAE1B,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAExC,IAAI,UAAU,GAAG,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO;IAElD,MAAM,GAAG,GACP,iBAAiB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY;QACpD,YAAY,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAEjE,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,OAAO;YACV,MAAM,IAAI,KAAK,CACb,GAAG,GAAG,kDAAkD,CACzD,CAAC;QAEJ,KAAK,MAAM;YACT,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC,CAAC;YACjC,MAAM;QAER,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,GAAG,GAAG,aAAa,CAAC,CAAC;YACzD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;YACxC,CAAC;YACD,MAAM;QACR,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for xc CLI.
|
|
3
|
+
* All state lives under ~/.xc/ (or $XC_CONFIG_DIR).
|
|
4
|
+
* Automatically migrates from legacy ~/.config/xc/ location.
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthCredential {
|
|
7
|
+
type: "oauth2" | "bearer";
|
|
8
|
+
/** OAuth 2.0 access token */
|
|
9
|
+
accessToken?: string;
|
|
10
|
+
/** OAuth 2.0 refresh token */
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
/** Token expiry (epoch ms) */
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
/** App-only bearer token */
|
|
15
|
+
bearerToken?: string;
|
|
16
|
+
/** OAuth 2.0 client ID */
|
|
17
|
+
clientId?: string;
|
|
18
|
+
/** OAuth 2.0 client secret (confidential clients) */
|
|
19
|
+
clientSecret?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AccountConfig {
|
|
22
|
+
name: string;
|
|
23
|
+
auth: AuthCredential;
|
|
24
|
+
userId?: string;
|
|
25
|
+
username?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface XcConfig {
|
|
28
|
+
defaultAccount: string;
|
|
29
|
+
accounts: Record<string, AccountConfig>;
|
|
30
|
+
}
|
|
31
|
+
/** Return the xc data/config directory (~/.xc/). */
|
|
32
|
+
export declare function getConfigDir(): string;
|
|
33
|
+
export declare function getConfigPath(): string;
|
|
34
|
+
export declare function ensureConfigDir(): void;
|
|
35
|
+
export declare function loadConfig(): XcConfig;
|
|
36
|
+
export declare function saveConfig(config: XcConfig): void;
|
|
37
|
+
export declare function getAccount(name?: string): AccountConfig | undefined;
|
|
38
|
+
export declare function setAccount(name: string, account: AccountConfig): void;
|
|
39
|
+
export declare function setDefaultAccount(name: string): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for xc CLI.
|
|
3
|
+
* All state lives under ~/.xc/ (or $XC_CONFIG_DIR).
|
|
4
|
+
* Automatically migrates from legacy ~/.config/xc/ location.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
/** Primary config/data directory: ~/.xc/ */
|
|
10
|
+
const CONFIG_DIR = process.env.XC_CONFIG_DIR ?? path.join(os.homedir(), ".xc");
|
|
11
|
+
/** Legacy config location for migration */
|
|
12
|
+
const LEGACY_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "xc");
|
|
13
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
14
|
+
const LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIG_DIR, "config.json");
|
|
15
|
+
/** Return the xc data/config directory (~/.xc/). */
|
|
16
|
+
export function getConfigDir() {
|
|
17
|
+
return CONFIG_DIR;
|
|
18
|
+
}
|
|
19
|
+
export function getConfigPath() {
|
|
20
|
+
return CONFIG_FILE;
|
|
21
|
+
}
|
|
22
|
+
export function ensureConfigDir() {
|
|
23
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Migrate config.json from ~/.config/xc/ to ~/.xc/ if the new location
|
|
27
|
+
* doesn't exist yet but the legacy one does.
|
|
28
|
+
*/
|
|
29
|
+
function migrateConfigIfNeeded() {
|
|
30
|
+
if (!fs.existsSync(CONFIG_FILE) && fs.existsSync(LEGACY_CONFIG_FILE)) {
|
|
31
|
+
ensureConfigDir();
|
|
32
|
+
fs.copyFileSync(LEGACY_CONFIG_FILE, CONFIG_FILE);
|
|
33
|
+
console.error(`Migrated config: ${LEGACY_CONFIG_DIR} -> ${CONFIG_DIR}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function loadConfig() {
|
|
37
|
+
migrateConfigIfNeeded();
|
|
38
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
39
|
+
return { defaultAccount: "default", accounts: {} };
|
|
40
|
+
}
|
|
41
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
export function saveConfig(config) {
|
|
45
|
+
ensureConfigDir();
|
|
46
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
export function getAccount(name) {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
const accountName = name ?? config.defaultAccount;
|
|
51
|
+
return config.accounts[accountName];
|
|
52
|
+
}
|
|
53
|
+
export function setAccount(name, account) {
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
config.accounts[name] = account;
|
|
56
|
+
saveConfig(config);
|
|
57
|
+
}
|
|
58
|
+
export function setDefaultAccount(name) {
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
config.defaultAccount = name;
|
|
61
|
+
saveConfig(config);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AA8BzB,4CAA4C;AAC5C,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;AAE9D,2CAA2C;AAC3C,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CACjC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,EACjE,IAAI,CACL,CAAC;AAEF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACzD,MAAM,kBAAkB,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;AAEvE,oDAAoD;AACpD,MAAM,UAAU,YAAY;IAC1B,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAChD,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB;IAC5B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACrE,eAAe,EAAE,CAAC;QAClB,EAAE,CAAC,YAAY,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAC;QACjD,OAAO,CAAC,KAAK,CAAC,oBAAoB,iBAAiB,OAAO,UAAU,EAAE,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,qBAAqB,EAAE,CAAC;IAExB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACrD,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAa,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAgB;IACzC,eAAe,EAAE,CAAC;IAClB,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,WAAW,GAAG,IAAI,IAAI,MAAM,CAAC,cAAc,CAAC;IAClD,OAAO,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAsB;IAC7D,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;IAChC,UAAU,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API request cost tracking.
|
|
3
|
+
* Logs every API call to ~/.xc/usage.jsonl with timestamp,
|
|
4
|
+
* endpoint, HTTP method, and estimated dollar cost.
|
|
5
|
+
*/
|
|
6
|
+
/** Single usage log entry written to usage.jsonl. */
|
|
7
|
+
export interface UsageEntry {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
method: string;
|
|
11
|
+
estimatedCost: number;
|
|
12
|
+
}
|
|
13
|
+
/** Get estimated dollar cost for an endpoint. */
|
|
14
|
+
export declare function estimateCost(endpoint: string): number;
|
|
15
|
+
/** Infer the HTTP method for an endpoint. */
|
|
16
|
+
export declare function inferMethod(endpoint: string): string;
|
|
17
|
+
/** Path to the usage log file. */
|
|
18
|
+
export declare function getUsageLogPath(): string;
|
|
19
|
+
/** Append a usage entry to the JSONL log and track in-memory. */
|
|
20
|
+
export declare function logApiCall(endpoint: string): void;
|
|
21
|
+
/** Get cost summary for API calls made in this session. */
|
|
22
|
+
export declare function getSessionCost(): {
|
|
23
|
+
endpoints: string[];
|
|
24
|
+
total: number;
|
|
25
|
+
};
|
|
26
|
+
/** Print data as JSON with session cost included in the object. */
|
|
27
|
+
export declare function outputJson(data: unknown): void;
|
|
28
|
+
/** Read all usage entries from the log file. */
|
|
29
|
+
export declare function loadUsageLog(): UsageEntry[];
|
|
30
|
+
/** Sum estimated costs for entries within a time window (ms from now). */
|
|
31
|
+
export declare function computeSpend(entries: UsageEntry[], windowMs: number): number;
|
|
32
|
+
/** Sum estimated costs for entries from midnight today. */
|
|
33
|
+
export declare function computeTodaySpend(entries: UsageEntry[]): number;
|
|
34
|
+
/** Time window constants (milliseconds). */
|
|
35
|
+
export declare const HOUR = 3600000;
|
|
36
|
+
export declare const DAY = 86400000;
|
|
37
|
+
export declare const WEEK: number;
|
|
38
|
+
export declare const MONTH: number;
|
|
39
|
+
/**
|
|
40
|
+
* Build the compact cost footer line.
|
|
41
|
+
* Returns empty string if no usage has been recorded.
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatCostFooter(): string;
|
package/dist/lib/cost.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API request cost tracking.
|
|
3
|
+
* Logs every API call to ~/.xc/usage.jsonl with timestamp,
|
|
4
|
+
* endpoint, HTTP method, and estimated dollar cost.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { getConfigDir, ensureConfigDir } from "./config.js";
|
|
9
|
+
/**
|
|
10
|
+
* Estimated cost per SDK endpoint (in dollars).
|
|
11
|
+
* These are rough estimates based on X API pricing tiers.
|
|
12
|
+
*/
|
|
13
|
+
const COST_MAP = {
|
|
14
|
+
"posts.searchRecent": 0.01,
|
|
15
|
+
"posts.searchAll": 0.02,
|
|
16
|
+
"posts.create": 0.01,
|
|
17
|
+
"posts.delete": 0.005,
|
|
18
|
+
"users.getMe": 0.005,
|
|
19
|
+
"users.getByUsername": 0.005,
|
|
20
|
+
"users.getPosts": 0.005,
|
|
21
|
+
"users.getTimeline": 0.005,
|
|
22
|
+
"users.likePost": 0.005,
|
|
23
|
+
"users.unlikePost": 0.005,
|
|
24
|
+
"users.getBookmarks": 0.005,
|
|
25
|
+
"users.createBookmark": 0.005,
|
|
26
|
+
"users.deleteBookmark": 0.005,
|
|
27
|
+
"users.getFollowers": 0.005,
|
|
28
|
+
"users.getFollowing": 0.005,
|
|
29
|
+
"users.followUser": 0.005,
|
|
30
|
+
"users.unfollowUser": 0.005,
|
|
31
|
+
"users.getOwnedLists": 0.005,
|
|
32
|
+
"lists.getPosts": 0.005,
|
|
33
|
+
"directMessages.createByParticipantId": 0.01,
|
|
34
|
+
"directMessages.getEvents": 0.005,
|
|
35
|
+
"directMessages.getEventsByParticipantId": 0.005,
|
|
36
|
+
"media.upload": 0.01,
|
|
37
|
+
"media.initializeUpload": 0.005,
|
|
38
|
+
"media.appendUpload": 0.0,
|
|
39
|
+
"media.finalizeUpload": 0.005,
|
|
40
|
+
"media.getUploadStatus": 0.0,
|
|
41
|
+
"usage.get": 0.0,
|
|
42
|
+
"stream.getRules": 0.0,
|
|
43
|
+
"stream.updateRules": 0.005,
|
|
44
|
+
"stream.posts": 0.0,
|
|
45
|
+
// Reposts
|
|
46
|
+
"users.repostPost": 0.005,
|
|
47
|
+
"users.unrepostPost": 0.005,
|
|
48
|
+
// Mentions
|
|
49
|
+
"users.getMentions": 0.005,
|
|
50
|
+
// Engagement lookups
|
|
51
|
+
"posts.getQuoteTweets": 0.005,
|
|
52
|
+
"posts.getLikingUsers": 0.005,
|
|
53
|
+
"posts.getRetweetedBy": 0.005,
|
|
54
|
+
"users.getLikedPosts": 0.005,
|
|
55
|
+
// Hide replies
|
|
56
|
+
"posts.hideReply": 0.005,
|
|
57
|
+
"posts.unhideReply": 0.005,
|
|
58
|
+
// Mute
|
|
59
|
+
"users.muteUser": 0.005,
|
|
60
|
+
"users.unmuteUser": 0.005,
|
|
61
|
+
"users.getMuting": 0.005,
|
|
62
|
+
// Block
|
|
63
|
+
"users.blockUser": 0.005,
|
|
64
|
+
"users.unblockUser": 0.005,
|
|
65
|
+
"users.getBlocking": 0.005,
|
|
66
|
+
// User search
|
|
67
|
+
"users.search": 0.01,
|
|
68
|
+
// List management
|
|
69
|
+
"lists.create": 0.005,
|
|
70
|
+
"lists.update": 0.005,
|
|
71
|
+
"lists.delete": 0.005,
|
|
72
|
+
"lists.getMembers": 0.005,
|
|
73
|
+
"lists.addMember": 0.005,
|
|
74
|
+
"lists.removeMember": 0.005,
|
|
75
|
+
"users.followList": 0.005,
|
|
76
|
+
"users.unfollowList": 0.005,
|
|
77
|
+
"users.pinList": 0.005,
|
|
78
|
+
"users.unpinList": 0.005,
|
|
79
|
+
// Trends
|
|
80
|
+
"trends.getPersonalized": 0.005,
|
|
81
|
+
"trends.getByWoeid": 0.005,
|
|
82
|
+
};
|
|
83
|
+
const DEFAULT_COST = 0.005;
|
|
84
|
+
/** In-memory log of API calls made during this process. */
|
|
85
|
+
const sessionCalls = [];
|
|
86
|
+
/** Inferred HTTP method per endpoint. Defaults to GET. */
|
|
87
|
+
const METHOD_MAP = {
|
|
88
|
+
"posts.create": "POST",
|
|
89
|
+
"posts.delete": "DELETE",
|
|
90
|
+
"users.likePost": "POST",
|
|
91
|
+
"users.unlikePost": "DELETE",
|
|
92
|
+
"users.createBookmark": "POST",
|
|
93
|
+
"users.deleteBookmark": "DELETE",
|
|
94
|
+
"users.followUser": "POST",
|
|
95
|
+
"users.unfollowUser": "DELETE",
|
|
96
|
+
"directMessages.createByParticipantId": "POST",
|
|
97
|
+
"media.upload": "POST",
|
|
98
|
+
"media.initializeUpload": "POST",
|
|
99
|
+
"media.appendUpload": "POST",
|
|
100
|
+
"media.finalizeUpload": "POST",
|
|
101
|
+
"stream.updateRules": "POST",
|
|
102
|
+
"users.repostPost": "POST",
|
|
103
|
+
"users.unrepostPost": "DELETE",
|
|
104
|
+
"posts.hideReply": "PUT",
|
|
105
|
+
"posts.unhideReply": "PUT",
|
|
106
|
+
"users.muteUser": "POST",
|
|
107
|
+
"users.unmuteUser": "DELETE",
|
|
108
|
+
"users.blockUser": "POST",
|
|
109
|
+
"users.unblockUser": "DELETE",
|
|
110
|
+
"lists.create": "POST",
|
|
111
|
+
"lists.update": "PUT",
|
|
112
|
+
"lists.delete": "DELETE",
|
|
113
|
+
"lists.addMember": "POST",
|
|
114
|
+
"lists.removeMember": "DELETE",
|
|
115
|
+
"users.followList": "POST",
|
|
116
|
+
"users.unfollowList": "DELETE",
|
|
117
|
+
"users.pinList": "POST",
|
|
118
|
+
"users.unpinList": "DELETE",
|
|
119
|
+
};
|
|
120
|
+
/** Get estimated dollar cost for an endpoint. */
|
|
121
|
+
export function estimateCost(endpoint) {
|
|
122
|
+
return COST_MAP[endpoint] ?? DEFAULT_COST;
|
|
123
|
+
}
|
|
124
|
+
/** Infer the HTTP method for an endpoint. */
|
|
125
|
+
export function inferMethod(endpoint) {
|
|
126
|
+
return METHOD_MAP[endpoint] ?? "GET";
|
|
127
|
+
}
|
|
128
|
+
/** Path to the usage log file. */
|
|
129
|
+
export function getUsageLogPath() {
|
|
130
|
+
return path.join(getConfigDir(), "usage.jsonl");
|
|
131
|
+
}
|
|
132
|
+
/** Append a usage entry to the JSONL log and track in-memory. */
|
|
133
|
+
export function logApiCall(endpoint) {
|
|
134
|
+
ensureConfigDir();
|
|
135
|
+
const cost = estimateCost(endpoint);
|
|
136
|
+
sessionCalls.push({ endpoint, cost });
|
|
137
|
+
const entry = {
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
endpoint,
|
|
140
|
+
method: inferMethod(endpoint),
|
|
141
|
+
estimatedCost: cost,
|
|
142
|
+
};
|
|
143
|
+
fs.appendFileSync(getUsageLogPath(), JSON.stringify(entry) + "\n");
|
|
144
|
+
}
|
|
145
|
+
/** Get cost summary for API calls made in this session. */
|
|
146
|
+
export function getSessionCost() {
|
|
147
|
+
const total = sessionCalls.reduce((sum, c) => sum + c.cost, 0);
|
|
148
|
+
return { endpoints: sessionCalls.map((c) => c.endpoint), total };
|
|
149
|
+
}
|
|
150
|
+
/** Print data as JSON with session cost included in the object. */
|
|
151
|
+
export function outputJson(data) {
|
|
152
|
+
const cost = getSessionCost();
|
|
153
|
+
const wrapped = typeof data === "object" && data !== null && !Array.isArray(data)
|
|
154
|
+
? { ...data, _cost: cost }
|
|
155
|
+
: { data, _cost: cost };
|
|
156
|
+
console.log(JSON.stringify(wrapped, null, 2));
|
|
157
|
+
}
|
|
158
|
+
/** Read all usage entries from the log file. */
|
|
159
|
+
export function loadUsageLog() {
|
|
160
|
+
const logPath = getUsageLogPath();
|
|
161
|
+
if (!fs.existsSync(logPath))
|
|
162
|
+
return [];
|
|
163
|
+
const raw = fs.readFileSync(logPath, "utf-8").trim();
|
|
164
|
+
if (!raw)
|
|
165
|
+
return [];
|
|
166
|
+
const entries = [];
|
|
167
|
+
for (const line of raw.split("\n")) {
|
|
168
|
+
try {
|
|
169
|
+
entries.push(JSON.parse(line));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Skip malformed lines
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
/** Sum estimated costs for entries within a time window (ms from now). */
|
|
178
|
+
export function computeSpend(entries, windowMs) {
|
|
179
|
+
const cutoff = Date.now() - windowMs;
|
|
180
|
+
let total = 0;
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
if (new Date(entry.timestamp).getTime() >= cutoff) {
|
|
183
|
+
total += entry.estimatedCost;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return total;
|
|
187
|
+
}
|
|
188
|
+
/** Sum estimated costs for entries from midnight today. */
|
|
189
|
+
export function computeTodaySpend(entries) {
|
|
190
|
+
const midnight = new Date();
|
|
191
|
+
midnight.setHours(0, 0, 0, 0);
|
|
192
|
+
const cutoff = midnight.getTime();
|
|
193
|
+
let total = 0;
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (new Date(entry.timestamp).getTime() >= cutoff) {
|
|
196
|
+
total += entry.estimatedCost;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return total;
|
|
200
|
+
}
|
|
201
|
+
/** Time window constants (milliseconds). */
|
|
202
|
+
export const HOUR = 3_600_000;
|
|
203
|
+
export const DAY = 86_400_000;
|
|
204
|
+
export const WEEK = 7 * DAY;
|
|
205
|
+
export const MONTH = 30 * DAY;
|
|
206
|
+
/** Format a dollar amount as $X.XX. */
|
|
207
|
+
function fmt(amount) {
|
|
208
|
+
return `$${amount.toFixed(2)}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Build the compact cost footer line.
|
|
212
|
+
* Returns empty string if no usage has been recorded.
|
|
213
|
+
*/
|
|
214
|
+
export function formatCostFooter() {
|
|
215
|
+
const entries = loadUsageLog();
|
|
216
|
+
if (entries.length === 0)
|
|
217
|
+
return "";
|
|
218
|
+
const h1 = computeSpend(entries, HOUR);
|
|
219
|
+
const h24 = computeSpend(entries, DAY);
|
|
220
|
+
const d7 = computeSpend(entries, WEEK);
|
|
221
|
+
const d30 = computeSpend(entries, MONTH);
|
|
222
|
+
return `Cost: ${fmt(h1)} (1h) \u00b7 ${fmt(h24)} (24h) \u00b7 ${fmt(d7)} (7d) \u00b7 ${fmt(d30)} (30d)`;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=cost.js.map
|