@gramatr/client 0.5.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 (65) hide show
  1. package/AGENTS.md +17 -0
  2. package/CLAUDE.md +18 -0
  3. package/README.md +108 -0
  4. package/bin/add-api-key.ts +264 -0
  5. package/bin/clean-legacy-install.ts +28 -0
  6. package/bin/clear-creds.ts +141 -0
  7. package/bin/get-token.py +3 -0
  8. package/bin/gmtr-login.ts +599 -0
  9. package/bin/gramatr.js +36 -0
  10. package/bin/gramatr.ts +374 -0
  11. package/bin/install.ts +716 -0
  12. package/bin/lib/config.ts +57 -0
  13. package/bin/lib/git.ts +111 -0
  14. package/bin/lib/stdin.ts +53 -0
  15. package/bin/logout.ts +76 -0
  16. package/bin/render-claude-hooks.ts +16 -0
  17. package/bin/statusline.ts +81 -0
  18. package/bin/uninstall.ts +289 -0
  19. package/chatgpt/README.md +95 -0
  20. package/chatgpt/install.ts +140 -0
  21. package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
  22. package/codex/README.md +28 -0
  23. package/codex/hooks/session-start.ts +73 -0
  24. package/codex/hooks/stop.ts +34 -0
  25. package/codex/hooks/user-prompt-submit.ts +79 -0
  26. package/codex/install.ts +116 -0
  27. package/codex/lib/codex-hook-utils.ts +48 -0
  28. package/codex/lib/codex-install-utils.ts +123 -0
  29. package/core/auth.ts +170 -0
  30. package/core/feedback.ts +55 -0
  31. package/core/formatting.ts +179 -0
  32. package/core/install.ts +107 -0
  33. package/core/installer-cli.ts +122 -0
  34. package/core/migration.ts +479 -0
  35. package/core/routing.ts +108 -0
  36. package/core/session.ts +202 -0
  37. package/core/targets.ts +292 -0
  38. package/core/types.ts +179 -0
  39. package/core/version-check.ts +219 -0
  40. package/core/version.ts +47 -0
  41. package/desktop/README.md +72 -0
  42. package/desktop/build-mcpb.ts +166 -0
  43. package/desktop/install.ts +136 -0
  44. package/desktop/lib/desktop-install-utils.ts +70 -0
  45. package/gemini/README.md +95 -0
  46. package/gemini/hooks/session-start.ts +72 -0
  47. package/gemini/hooks/stop.ts +30 -0
  48. package/gemini/hooks/user-prompt-submit.ts +77 -0
  49. package/gemini/install.ts +281 -0
  50. package/gemini/lib/gemini-hook-utils.ts +63 -0
  51. package/gemini/lib/gemini-install-utils.ts +169 -0
  52. package/hooks/GMTRPromptEnricher.hook.ts +651 -0
  53. package/hooks/GMTRRatingCapture.hook.ts +198 -0
  54. package/hooks/GMTRSecurityValidator.hook.ts +399 -0
  55. package/hooks/GMTRToolTracker.hook.ts +181 -0
  56. package/hooks/StopOrchestrator.hook.ts +78 -0
  57. package/hooks/gmtr-tool-tracker-utils.ts +105 -0
  58. package/hooks/lib/gmtr-hook-utils.ts +770 -0
  59. package/hooks/lib/identity.ts +227 -0
  60. package/hooks/lib/notify.ts +46 -0
  61. package/hooks/lib/paths.ts +104 -0
  62. package/hooks/lib/transcript-parser.ts +452 -0
  63. package/hooks/session-end.hook.ts +168 -0
  64. package/hooks/session-start.hook.ts +501 -0
  65. package/package.json +63 -0
package/AGENTS.md ADDED
@@ -0,0 +1,17 @@
1
+ # Gramatr Codex Guidance
2
+
3
+ ## Repo Expectations
4
+
5
+ - Prefer gramatr memory and project handoff context over ad hoc local summaries.
6
+ - Treat Codex hook augmentation as authoritative when a `[GMTR Intelligence]` block is present.
7
+ - If gramatr augmentation is missing, query gramatr memory before relying on stale local notes.
8
+ - Prefer TypeScript implementations over shell scripts for client integration code unless the platform requires shell.
9
+ - Maintain test coverage for new hook and utility logic.
10
+
11
+ ## Codex Integration
12
+
13
+ - Repo-local Codex hooks are configured in `.codex/hooks.json`.
14
+ - `UserPromptSubmit` is responsible for prompt enrichment.
15
+ - `SessionStart` is responsible for session restore and continuity.
16
+ - Hook failures must degrade cleanly and never block the user session.
17
+
package/CLAUDE.md ADDED
@@ -0,0 +1,18 @@
1
+ <!-- GMTR-START — Do not edit between these markers. Managed by gramatr installer. -->
2
+ # gramatr
3
+
4
+ You have gramatr installed. A hook pre-classifies every user request and injects
5
+ intelligence as `[GMTR Intelligence — ...]` into your context.
6
+
7
+ **Follow the intelligence packet.** It contains behavioral directives, effort level,
8
+ ISC scaffold, capability audit, phase templates, and composed agents.
9
+
10
+ **Memory:** Use gramatr MCP tools (`search_semantic`, `create_entity`, `add_observation`),
11
+ not local markdown files.
12
+
13
+ **Identity:** Read from `~/gmtr-client/settings.json` — `daidentity` for your name,
14
+ `principal` for the user's name.
15
+
16
+ **If the server is unreachable:** Use 7-phase structure (OBSERVE → THINK → PLAN → BUILD →
17
+ EXECUTE → VERIFY → LEARN). Create ISC before work. Never combine phases.
18
+ <!-- GMTR-END -->
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # gramatr
2
+
3
+ **Intelligence layer for AI coding agents.** Pre-classifies every request so Claude Code, Codex, and Gemini CLI spend tokens on your work, not on routing overhead.
4
+
5
+ ## What it does
6
+
7
+ gramatr sits between you and your AI agent. Before the agent sees your prompt, gramatr's decision router (BERT classifiers running in <5ms) pre-classifies:
8
+
9
+ - **Effort level** — instant, fast, standard, extended, advanced, deep, comprehensive
10
+ - **Intent type** — search, retrieve, create, update, analyze, generate
11
+ - **Skill matching** — which of 25 capabilities are relevant
12
+ - **Memory tier** — what context to pre-load from your knowledge graph
13
+ - **Reverse engineering** — what you want, what you don't want, gotchas
14
+
15
+ The agent receives this as a pre-computed intelligence packet, saving ~2,700 tokens per request that would otherwise be spent on the agent figuring out what you need.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npx @gramatr/client install claude-code
21
+ ```
22
+
23
+ That's it. One command installs hooks, registers the MCP server, and configures your AI agent.
24
+
25
+ ### Non-interactive (CI, remote provisioning)
26
+
27
+ ```bash
28
+ npx @gramatr/client install claude-code --yes --name "Your Name" --timezone "America/Chicago"
29
+ ```
30
+
31
+ ### Supported platforms
32
+
33
+ | Platform | Status | Install |
34
+ |---|---|---|
35
+ | Claude Code | Stable | `npx @gramatr/client install claude-code` |
36
+ | OpenAI Codex | Stable | Auto-detected during install |
37
+ | Google Gemini CLI | Stable | Auto-detected during install |
38
+ | Claude Desktop | Coming soon | — |
39
+ | ChatGPT Desktop | Coming soon | — |
40
+
41
+ ## How it works
42
+
43
+ ```
44
+ You type a prompt
45
+ |
46
+ gramatr hooks intercept (UserPromptSubmit)
47
+ |
48
+ Decision router classifies in <5ms (BERT on GPU)
49
+ |
50
+ Intelligence packet injected into agent context
51
+ |
52
+ Agent receives pre-classified request
53
+ = skips routing overhead
54
+ = starts working immediately
55
+ = saves ~2,700 tokens per request
56
+ ```
57
+
58
+ ### The flywheel
59
+
60
+ Every interaction trains the classifier. Memory powers routing, routing generates patterns, patterns improve routing. The system gets smarter with use.
61
+
62
+ ## Architecture
63
+
64
+ gramatr is a thin client + smart server:
65
+
66
+ - **Client** (this package): 29 files, ~290KB. Hooks into your AI agent, forwards to server.
67
+ - **Server**: Decision routing engine (BERT + Qwen), knowledge graph (PostgreSQL + pgvector), pattern learning.
68
+ - **Protocol**: MCP (Model Context Protocol) for Claude Code/Codex, REST API for web/mobile.
69
+
70
+ The client never stores intelligence locally. The server delivers everything: behavioral rules, skill routing, agent composition, ISC scaffolds, capability audits.
71
+
72
+ ## What gets installed
73
+
74
+ ```
75
+ ~/gmtr-client/ # Client runtime
76
+ hooks/ # 8 lifecycle hooks
77
+ core/ # Shared routing + session logic
78
+ bin/ # Status line, login, utilities
79
+ CLAUDE.md # Minimal behavioral framework
80
+ ~/.claude/settings.json # Hook configuration (merged, not overwritten)
81
+ ~/.claude.json # MCP server registration
82
+ ~/.gmtr.json # Auth token (canonical source)
83
+ ```
84
+
85
+ ## Commands
86
+
87
+ ```bash
88
+ gramatr install # Interactive target selection
89
+ gramatr install claude-code # Install for Claude Code
90
+ gramatr detect # Show detected AI platforms
91
+ gramatr doctor # Health check (coming soon)
92
+ gramatr uninstall # Clean removal
93
+ gramatr --version # Show version
94
+ ```
95
+
96
+ ## Requirements
97
+
98
+ - Node.js 20+
99
+ - One of: Claude Code, OpenAI Codex, Google Gemini CLI
100
+
101
+ ## Links
102
+
103
+ - [gramatr.com](https://gramatr.com)
104
+ - [GitHub](https://github.com/gramatr/gramatr)
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gramatr add-api-key — Explicit API key ingestion command (issue #484).
4
+ *
5
+ * Three modes:
6
+ * 1. Interactive prompt: gramatr add-api-key
7
+ * 2. Piped stdin: echo "gmtr_sk_..." | gramatr add-api-key
8
+ * 3. Env-sourced: gramatr add-api-key --from-env GRAMATR_API_KEY
9
+ *
10
+ * The key is validated against the gramatr server before being written
11
+ * to ~/.gmtr.json. Use --force to skip server validation when offline.
12
+ *
13
+ * This command is the ONLY way to put an API key into ~/.gmtr.json.
14
+ * Installers never prompt for API keys — see packages/client/core/auth.ts.
15
+ */
16
+
17
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+ import { createInterface } from "readline";
21
+
22
+ function gmtrJsonPath(): string {
23
+ return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".gmtr.json");
24
+ }
25
+ const SERVER_BASE = (process.env.GMTR_URL || "https://api.gramatr.com").replace(/\/mcp\/?$/, "");
26
+
27
+ // Accept gmtr_sk_, gmtr_pk_, aios_sk_, aios_pk_ (legacy), and Firebase-style
28
+ // long opaque tokens (length >= 32, base64url-ish characters).
29
+ const KEY_FORMAT = /^(gmtr|aios)_(sk|pk)_[A-Za-z0-9_-]+$/;
30
+ const LEGACY_OPAQUE = /^[A-Za-z0-9_.-]{32,}$/;
31
+
32
+ function log(msg: string = ""): void {
33
+ process.stdout.write(`${msg}\n`);
34
+ }
35
+
36
+ function err(msg: string): void {
37
+ process.stderr.write(`${msg}\n`);
38
+ }
39
+
40
+ function parseArgs(argv: string[]): {
41
+ fromEnv?: string;
42
+ force: boolean;
43
+ help: boolean;
44
+ } {
45
+ let fromEnv: string | undefined;
46
+ let force = false;
47
+ let help = false;
48
+ for (let i = 0; i < argv.length; i++) {
49
+ const a = argv[i];
50
+ if (a === "--from-env") {
51
+ fromEnv = argv[++i];
52
+ } else if (a === "--force") {
53
+ force = true;
54
+ } else if (a === "--help" || a === "-h") {
55
+ help = true;
56
+ }
57
+ }
58
+ return { fromEnv, force, help };
59
+ }
60
+
61
+ function showHelp(): void {
62
+ log(`gramatr add-api-key — Add a gramatr API key to ~/.gmtr.json
63
+
64
+ Usage:
65
+ gramatr add-api-key Interactive prompt for the key
66
+ echo "gmtr_sk_..." | gramatr add-api-key Read key from piped stdin
67
+ gramatr add-api-key --from-env VAR Read key from named env variable
68
+ gramatr add-api-key --force Skip server validation (offline use)
69
+
70
+ The key is validated against the gramatr server before being written.
71
+ Server: ${SERVER_BASE}`);
72
+ }
73
+
74
+ function validateFormat(key: string): boolean {
75
+ if (KEY_FORMAT.test(key)) return true;
76
+ // Allow legacy opaque tokens (e.g. Firebase IDs) — must still be sane.
77
+ if (LEGACY_OPAQUE.test(key) && !key.includes(" ")) return true;
78
+ return false;
79
+ }
80
+
81
+ async function readPipedStdin(): Promise<string | null> {
82
+ if (process.stdin.isTTY) return null;
83
+ return new Promise((resolve) => {
84
+ const chunks: Buffer[] = [];
85
+ process.stdin.on("data", (c) => chunks.push(Buffer.from(c)));
86
+ process.stdin.on("end", () => {
87
+ const out = Buffer.concat(chunks).toString("utf8").trim();
88
+ resolve(out || null);
89
+ });
90
+ process.stdin.on("error", () => resolve(null));
91
+ });
92
+ }
93
+
94
+ async function readInteractive(): Promise<string> {
95
+ log("");
96
+ log("Paste your gramatr API key below.");
97
+ log("(Get one at https://gramatr.com/settings — keys start with gmtr_sk_)");
98
+ log("");
99
+ process.stdout.write(" Key: ");
100
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
101
+ return new Promise((resolve) => {
102
+ rl.on("line", (line: string) => {
103
+ rl.close();
104
+ resolve(line.trim());
105
+ });
106
+ });
107
+ }
108
+
109
+ interface ValidationResult {
110
+ ok: boolean;
111
+ status?: number;
112
+ error?: string;
113
+ }
114
+
115
+ async function validateAgainstServer(key: string): Promise<ValidationResult> {
116
+ // Use the MCP aggregate_stats path the same way gmtr-login.ts does —
117
+ // this is the lightest authenticated endpoint we know works on every
118
+ // deployment without requiring a /api/v1/me route.
119
+ try {
120
+ const res = await fetch(`${SERVER_BASE}/mcp`, {
121
+ method: "POST",
122
+ headers: {
123
+ "Content-Type": "application/json",
124
+ Accept: "application/json, text/event-stream",
125
+ Authorization: `Bearer ${key}`,
126
+ },
127
+ body: JSON.stringify({
128
+ jsonrpc: "2.0",
129
+ id: 1,
130
+ method: "tools/call",
131
+ params: { name: "aggregate_stats", arguments: {} },
132
+ }),
133
+ signal: AbortSignal.timeout(10000),
134
+ });
135
+
136
+ const text = await res.text();
137
+
138
+ if (res.status === 401 || res.status === 403) {
139
+ return { ok: false, status: res.status, error: "Server rejected key (401/403)" };
140
+ }
141
+ if (
142
+ text.includes("JWT token is required") ||
143
+ text.includes("signature validation failed") ||
144
+ text.includes("Unauthorized")
145
+ ) {
146
+ return { ok: false, status: 401, error: "Server rejected key" };
147
+ }
148
+ if (res.status >= 500) {
149
+ return { ok: false, status: res.status, error: `Server error HTTP ${res.status}` };
150
+ }
151
+ if (!res.ok) {
152
+ return { ok: false, status: res.status, error: `HTTP ${res.status}` };
153
+ }
154
+ return { ok: true, status: res.status };
155
+ } catch (e: any) {
156
+ return { ok: false, error: e?.message || "Network failure" };
157
+ }
158
+ }
159
+
160
+ function writeKey(key: string): void {
161
+ let existing: Record<string, any> = {};
162
+ if (existsSync(gmtrJsonPath())) {
163
+ try {
164
+ existing = JSON.parse(readFileSync(gmtrJsonPath(), "utf8"));
165
+ } catch {
166
+ existing = {};
167
+ }
168
+ }
169
+ existing.token = key;
170
+ existing.token_type =
171
+ key.startsWith("gmtr_sk_") || key.startsWith("aios_sk_") ? "api_key" : "oauth";
172
+ existing.authenticated_at = new Date().toISOString();
173
+ writeFileSync(gmtrJsonPath(), `${JSON.stringify(existing, null, 2)}\n`, "utf8");
174
+ try {
175
+ chmodSync(gmtrJsonPath(), 0o600);
176
+ } catch {
177
+ /* ignore */
178
+ }
179
+ }
180
+
181
+ export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
182
+ const opts = parseArgs(argv);
183
+ if (opts.help) {
184
+ showHelp();
185
+ return 0;
186
+ }
187
+
188
+ let key: string | null = null;
189
+
190
+ // Source 1: --from-env
191
+ if (opts.fromEnv) {
192
+ const v = process.env[opts.fromEnv];
193
+ if (!v || !v.trim()) {
194
+ err(`ERROR: env var ${opts.fromEnv} is unset or empty`);
195
+ return 1;
196
+ }
197
+ key = v.trim();
198
+ }
199
+
200
+ // Source 2: piped stdin
201
+ if (!key) {
202
+ key = await readPipedStdin();
203
+ }
204
+
205
+ // Source 3: interactive
206
+ if (!key && process.stdin.isTTY) {
207
+ key = (await readInteractive()).trim();
208
+ }
209
+
210
+ if (!key) {
211
+ err("ERROR: no API key provided. See `gramatr add-api-key --help`.");
212
+ return 1;
213
+ }
214
+
215
+ // Format validation
216
+ if (!validateFormat(key)) {
217
+ err("ERROR: key format is invalid. Expected gmtr_sk_... or gmtr_pk_...");
218
+ return 1;
219
+ }
220
+
221
+ // Server validation
222
+ if (!opts.force) {
223
+ log("Validating key against gramatr server...");
224
+ const result = await validateAgainstServer(key);
225
+ if (!result.ok) {
226
+ if (result.status === 401 || result.status === 403) {
227
+ err(`ERROR: server rejected key — ${result.error}`);
228
+ err("Key was NOT written.");
229
+ return 1;
230
+ }
231
+ // Network or 5xx — surface as warning, exit non-zero unless --force.
232
+ err(`WARN: could not validate key — ${result.error}`);
233
+ err("Key was NOT written. Re-run with --force to skip server validation.");
234
+ return 1;
235
+ }
236
+ log(" OK Server accepted key");
237
+ } else {
238
+ log(" Skipping server validation (--force)");
239
+ }
240
+
241
+ writeKey(key);
242
+ log(`OK Key written to ${gmtrJsonPath()}`);
243
+ log("gramatr is now authenticated.");
244
+ return 0;
245
+ }
246
+
247
+ // Only auto-run when invoked directly, not when imported by tests.
248
+ const isDirect = (() => {
249
+ try {
250
+ const invoked = process.argv[1] || "";
251
+ return invoked.endsWith("add-api-key.ts") || invoked.endsWith("add-api-key.js");
252
+ } catch {
253
+ return false;
254
+ }
255
+ })();
256
+
257
+ if (isDirect) {
258
+ main()
259
+ .then((code) => process.exit(code))
260
+ .catch((e) => {
261
+ err(`ERROR: ${e?.message || e}`);
262
+ process.exit(1);
263
+ });
264
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join } from 'path';
4
+ import { runLegacyMigration } from '../core/migration.ts';
5
+
6
+ function log(message: string): void {
7
+ process.stdout.write(`${message}\n`);
8
+ }
9
+
10
+ function main(): void {
11
+ const home = process.env.HOME || process.env.USERPROFILE;
12
+ if (!home) {
13
+ throw new Error('HOME is not set');
14
+ }
15
+
16
+ const apply = process.argv.includes('--apply');
17
+ const includeOptionalUx = process.env.GMTR_ENABLE_OPTIONAL_CLAUDE_UX === '1';
18
+ const clientDir = process.env.GMTR_DIR || join(home, 'gmtr-client');
19
+ runLegacyMigration({
20
+ homeDir: home,
21
+ clientDir,
22
+ includeOptionalUx,
23
+ apply,
24
+ log,
25
+ });
26
+ }
27
+
28
+ main();
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gramatr clear-creds — Nuke ALL stored gramatr credentials.
4
+ *
5
+ * Stronger than `gramatr logout` (which only removes ~/.gmtr.json).
6
+ * This sweeps every credential location in the resolveAuthToken chain
7
+ * (core/auth.ts), so the next install/login is forced through OAuth.
8
+ *
9
+ * Locations cleared:
10
+ * 1. ~/.gmtr.json — deleted entirely
11
+ * 2. ~/gmtr-client/settings.json `auth.api_key` — stripped, file preserved
12
+ *
13
+ * Env vars (GRAMATR_API_KEY, GMTR_TOKEN) cannot be cleared from a child
14
+ * process — they live in the parent shell. We detect them and warn.
15
+ *
16
+ * Not-logged-in is not an error: exits 0 with a clean message.
17
+ */
18
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
19
+ import { homedir } from "os";
20
+ import { join } from "path";
21
+
22
+ function getHome(): string {
23
+ return process.env.HOME || process.env.USERPROFILE || homedir();
24
+ }
25
+
26
+ function gmtrJsonPath(): string {
27
+ return join(getHome(), ".gmtr.json");
28
+ }
29
+
30
+ function legacySettingsPath(): string {
31
+ const gmtrDir = process.env.GMTR_DIR || join(getHome(), "gmtr-client");
32
+ return join(gmtrDir, "settings.json");
33
+ }
34
+
35
+ function log(msg: string = ""): void {
36
+ process.stdout.write(`${msg}\n`);
37
+ }
38
+
39
+ function showHelp(): void {
40
+ log(`gramatr clear-creds — Remove every stored gramatr credential
41
+
42
+ Usage:
43
+ gramatr clear-creds Sweep ~/.gmtr.json + auth.api_key from gmtr-client/settings.json
44
+
45
+ After running, the next install or login will be forced through OAuth.
46
+
47
+ Env vars (GRAMATR_API_KEY, GMTR_TOKEN) cannot be unset from this process.
48
+ If they are set, this command warns and tells you the shell command to run.`);
49
+ }
50
+
51
+ interface ClearResult {
52
+ removedGmtrJson: boolean;
53
+ strippedLegacyKey: boolean;
54
+ envVarsSet: string[];
55
+ }
56
+
57
+ export function clearAll(): ClearResult {
58
+ const result: ClearResult = {
59
+ removedGmtrJson: false,
60
+ strippedLegacyKey: false,
61
+ envVarsSet: [],
62
+ };
63
+
64
+ // 1. ~/.gmtr.json — delete entirely
65
+ const gj = gmtrJsonPath();
66
+ if (existsSync(gj)) {
67
+ unlinkSync(gj);
68
+ result.removedGmtrJson = true;
69
+ }
70
+
71
+ // 2. ~/gmtr-client/settings.json — strip auth.api_key, preserve rest
72
+ const ls = legacySettingsPath();
73
+ if (existsSync(ls)) {
74
+ try {
75
+ const data = JSON.parse(readFileSync(ls, "utf8"));
76
+ if (data && data.auth && typeof data.auth === "object" && "api_key" in data.auth) {
77
+ delete data.auth.api_key;
78
+ // If auth is now empty, remove it too to keep the file tidy
79
+ if (Object.keys(data.auth).length === 0) delete data.auth;
80
+ writeFileSync(ls, `${JSON.stringify(data, null, 2)}\n`);
81
+ result.strippedLegacyKey = true;
82
+ }
83
+ } catch {
84
+ // malformed JSON — leave it alone, user can fix manually
85
+ }
86
+ }
87
+
88
+ // 3. Env vars — detect only, cannot mutate parent shell
89
+ for (const v of ["GRAMATR_API_KEY", "GMTR_TOKEN"]) {
90
+ if (process.env[v]) result.envVarsSet.push(v);
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ export function main(argv: string[] = process.argv.slice(2)): number {
97
+ if (argv.includes("--help") || argv.includes("-h")) {
98
+ showHelp();
99
+ return 0;
100
+ }
101
+
102
+ const r = clearAll();
103
+
104
+ if (!r.removedGmtrJson && !r.strippedLegacyKey && r.envVarsSet.length === 0) {
105
+ log("No stored credentials found. Already fully cleared.");
106
+ return 0;
107
+ }
108
+
109
+ if (r.removedGmtrJson) log(`Removed ${gmtrJsonPath()}`);
110
+ if (r.strippedLegacyKey) log(`Stripped auth.api_key from ${legacySettingsPath()}`);
111
+
112
+ if (r.envVarsSet.length > 0) {
113
+ log("");
114
+ log(`WARNING: the following env vars are still set in your shell:`);
115
+ for (const v of r.envVarsSet) log(` ${v}`);
116
+ log("");
117
+ log("These take precedence over file-based credentials. To clear them:");
118
+ log(` unset ${r.envVarsSet.join(" ")} # bash/zsh`);
119
+ log(` Remove-Item Env:${r.envVarsSet.join(", Env:")} # PowerShell`);
120
+ log("");
121
+ log("Until then, the next install will still resolve a token from the env.");
122
+ return 0;
123
+ }
124
+
125
+ log("");
126
+ log("All credentials cleared. Next 'gramatr login' or 'gramatr install' will use OAuth.");
127
+ return 0;
128
+ }
129
+
130
+ const isDirect = (() => {
131
+ try {
132
+ const invoked = process.argv[1] || "";
133
+ return invoked.endsWith("clear-creds.ts") || invoked.endsWith("clear-creds.js");
134
+ } catch {
135
+ return false;
136
+ }
137
+ })();
138
+
139
+ if (isDirect) {
140
+ process.exit(main());
141
+ }
@@ -0,0 +1,3 @@
1
+ import json, os
2
+ s = os.path.expanduser("~/gmtr-client/settings.json")
3
+ print(json.load(open(s)).get("auth",{}).get("api_key",""))