tokengratis 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/package.json +1 -1
  3. package/src/cli.mjs +42 -13
package/README.md CHANGED
@@ -54,6 +54,9 @@ tokengratis search llama
54
54
  tokengratis models --provider groq
55
55
  ```
56
56
 
57
+ - `--refresh` (alias `--no-cache`) pada perintah apa pun akan mengabaikan cache
58
+ lokal dan mengambil ulang dari server. Output teks selalu menampilkan umur
59
+ datanya, jadi cache basi tidak pernah tersaji diam-diam.
57
60
  - Plain text output by default; add `--json` to any command for
58
61
  machine-readable output.
59
62
  - Color is auto-disabled when stdout isn't a TTY, or when `NO_COLOR` is set.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokengratis",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI + MCP server for tokengratis.id, the aggregator directory of free-tier & free-credit LLM APIs for Indonesian developers. Zero runtime dependencies.",
5
5
  "keywords": [
6
6
  "tokengratis",
package/src/cli.mjs CHANGED
@@ -29,6 +29,10 @@ Usage:
29
29
  tokengratis --help
30
30
  tokengratis --version
31
31
 
32
+ Flag global:
33
+ --json output machine-readable
34
+ --refresh abaikan cache, ambil ulang dari server (alias: --no-cache)
35
+
32
36
  Contoh:
33
37
  tokengratis list
34
38
  tokengratis list --modality vision
@@ -38,11 +42,29 @@ Contoh:
38
42
  tokengratis models --provider groq
39
43
 
40
44
  Data live dari https://tokengratis.id/api/providers
41
- (fallback otomatis ke GitHub raw kalau API down). Cached lokal ~1 jam.
45
+ (fallback otomatis ke GitHub raw kalau API down). Cached lokal ~1 jam
46
+ umur data selalu ditulis di bawah output; pakai --refresh buat maksa segar.
42
47
 
43
48
  Ini AGGREGATOR, bukan verifier — tiap provider bawa provenance
44
49
  (sources[] + syncedAt), transparan apa adanya dari sumber.`;
45
50
 
51
+ /**
52
+ * Baris umur data. Sengaja SELALU ditampilkan di output teks.
53
+ *
54
+ * Kenapa: data di-cache 1 jam di temp dir. Pas data upstream diperbaiki
55
+ * (INCIDENT 2026-07-25), siapa pun yang sempat jalanin CLI sebelum perbaikan
56
+ * bakal terus ngeliat angka lama sampai satu jam — dan sebelumnya ga ada satu
57
+ * pun petunjuk bahwa yang dia liat itu cache. Diam-diam nyajiin data basi itu
58
+ * pelanggaran prinsip proyek ini (selalu tunjukin KAPAN datanya). Sekarang
59
+ * umurnya kelihatan, dan --refresh kasih jalan keluar.
60
+ */
61
+ function ageNote(fetchedAt, color) {
62
+ const mins = Math.max(0, Math.round((Date.now() - fetchedAt) / 60000));
63
+ const when = mins < 1 ? "barusan" : mins === 1 ? "1 menit lalu" : `${mins} menit lalu`;
64
+ const hint = mins >= 1 ? " · paksa segarkan: --refresh" : "";
65
+ return color.dim(`Data diambil ${when}${hint}`);
66
+ }
67
+
46
68
  function getFlagValue(args, name) {
47
69
  const idx = args.indexOf(name);
48
70
  if (idx === -1 || idx === args.length - 1) return undefined;
@@ -65,10 +87,10 @@ function positionals(args, valueFlags) {
65
87
  return out;
66
88
  }
67
89
 
68
- async function cmdList(args, { json, color }) {
90
+ async function cmdList(args, { json, color, refresh }) {
69
91
  const modality = getFlagValue(args, "--modality");
70
92
  const minContext = getFlagValue(args, "--min-context");
71
- const { providers } = await getProviders();
93
+ const { providers, fetchedAt } = await getProviders({ forceRefresh: refresh });
72
94
 
73
95
  let list = providers;
74
96
  list = filterByModality(list, modality);
@@ -85,16 +107,17 @@ async function cmdList(args, { json, color }) {
85
107
  console.log(formatProviderListText(list, { color }));
86
108
  console.log("");
87
109
  console.log(color.dim(`${list.length} provider. Detail: tokengratis show <slug>`));
110
+ console.log(ageNote(fetchedAt, color));
88
111
  }
89
112
 
90
- async function cmdShow(args, { json, color }) {
113
+ async function cmdShow(args, { json, color, refresh }) {
91
114
  const [slug] = positionals(args, ["--modality", "--min-context", "--provider"]);
92
115
  if (!slug) {
93
116
  console.error("Error: slug wajib diisi. Contoh: tokengratis show openrouter");
94
117
  process.exitCode = 1;
95
118
  return;
96
119
  }
97
- const { providers } = await getProviders();
120
+ const { providers, fetchedAt } = await getProviders({ forceRefresh: refresh });
98
121
  const p = providers.find((x) => x.slug === slug);
99
122
  if (!p) {
100
123
  console.error(`Error: provider dengan slug "${slug}" tidak ditemukan. Coba: tokengratis list`);
@@ -106,16 +129,18 @@ async function cmdShow(args, { json, color }) {
106
129
  return;
107
130
  }
108
131
  console.log(formatProviderText(p, { color }));
132
+ console.log("");
133
+ console.log(ageNote(fetchedAt, color));
109
134
  }
110
135
 
111
- async function cmdSearch(args, { json, color }) {
136
+ async function cmdSearch(args, { json, color, refresh }) {
112
137
  const query = positionals(args, ["--modality", "--min-context", "--provider"]).join(" ").trim();
113
138
  if (!query) {
114
139
  console.error('Error: query wajib diisi. Contoh: tokengratis search "llama"');
115
140
  process.exitCode = 1;
116
141
  return;
117
142
  }
118
- const { providers } = await getProviders();
143
+ const { providers, fetchedAt } = await getProviders({ forceRefresh: refresh });
119
144
  const results = searchProviders(providers, query);
120
145
  if (json) {
121
146
  console.log(
@@ -133,11 +158,13 @@ async function cmdSearch(args, { json, color }) {
133
158
  return;
134
159
  }
135
160
  console.log(formatSearchResultsText(results, query, { color }));
161
+ console.log("");
162
+ console.log(ageNote(fetchedAt, color));
136
163
  }
137
164
 
138
- async function cmdModels(args, { json, color }) {
165
+ async function cmdModels(args, { json, color, refresh }) {
139
166
  const providerSlug = getFlagValue(args, "--provider");
140
- const { providers } = await getProviders();
167
+ const { providers, fetchedAt } = await getProviders({ forceRefresh: refresh });
141
168
  const rows = listModels(providers, providerSlug);
142
169
  if (json) {
143
170
  console.log(JSON.stringify(rows, null, 2));
@@ -146,6 +173,7 @@ async function cmdModels(args, { json, color }) {
146
173
  console.log(formatModelListText(rows, { color }));
147
174
  console.log("");
148
175
  console.log(color.dim(`${rows.length} model.`));
176
+ console.log(ageNote(fetchedAt, color));
149
177
  }
150
178
 
151
179
  async function main(argv) {
@@ -162,20 +190,21 @@ async function main(argv) {
162
190
 
163
191
  const [command, ...rest] = args;
164
192
  const json = args.includes("--json");
193
+ const refresh = args.includes("--refresh") || args.includes("--no-cache");
165
194
  const color = createColorizer();
166
195
 
167
196
  switch (command) {
168
197
  case "list":
169
- await cmdList(rest, { json, color });
198
+ await cmdList(rest, { json, color, refresh });
170
199
  break;
171
200
  case "show":
172
- await cmdShow(rest, { json, color });
201
+ await cmdShow(rest, { json, color, refresh });
173
202
  break;
174
203
  case "search":
175
- await cmdSearch(rest, { json, color });
204
+ await cmdSearch(rest, { json, color, refresh });
176
205
  break;
177
206
  case "models":
178
- await cmdModels(rest, { json, color });
207
+ await cmdModels(rest, { json, color, refresh });
179
208
  break;
180
209
  default:
181
210
  console.error(`Perintah tidak dikenal: "${command}"\n`);