@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.
- package/AGENTS.md +17 -0
- package/CLAUDE.md +18 -0
- package/README.md +108 -0
- package/bin/add-api-key.ts +264 -0
- package/bin/clean-legacy-install.ts +28 -0
- package/bin/clear-creds.ts +141 -0
- package/bin/get-token.py +3 -0
- package/bin/gmtr-login.ts +599 -0
- package/bin/gramatr.js +36 -0
- package/bin/gramatr.ts +374 -0
- package/bin/install.ts +716 -0
- package/bin/lib/config.ts +57 -0
- package/bin/lib/git.ts +111 -0
- package/bin/lib/stdin.ts +53 -0
- package/bin/logout.ts +76 -0
- package/bin/render-claude-hooks.ts +16 -0
- package/bin/statusline.ts +81 -0
- package/bin/uninstall.ts +289 -0
- package/chatgpt/README.md +95 -0
- package/chatgpt/install.ts +140 -0
- package/chatgpt/lib/chatgpt-install-utils.ts +89 -0
- package/codex/README.md +28 -0
- package/codex/hooks/session-start.ts +73 -0
- package/codex/hooks/stop.ts +34 -0
- package/codex/hooks/user-prompt-submit.ts +79 -0
- package/codex/install.ts +116 -0
- package/codex/lib/codex-hook-utils.ts +48 -0
- package/codex/lib/codex-install-utils.ts +123 -0
- package/core/auth.ts +170 -0
- package/core/feedback.ts +55 -0
- package/core/formatting.ts +179 -0
- package/core/install.ts +107 -0
- package/core/installer-cli.ts +122 -0
- package/core/migration.ts +479 -0
- package/core/routing.ts +108 -0
- package/core/session.ts +202 -0
- package/core/targets.ts +292 -0
- package/core/types.ts +179 -0
- package/core/version-check.ts +219 -0
- package/core/version.ts +47 -0
- package/desktop/README.md +72 -0
- package/desktop/build-mcpb.ts +166 -0
- package/desktop/install.ts +136 -0
- package/desktop/lib/desktop-install-utils.ts +70 -0
- package/gemini/README.md +95 -0
- package/gemini/hooks/session-start.ts +72 -0
- package/gemini/hooks/stop.ts +30 -0
- package/gemini/hooks/user-prompt-submit.ts +77 -0
- package/gemini/install.ts +281 -0
- package/gemini/lib/gemini-hook-utils.ts +63 -0
- package/gemini/lib/gemini-install-utils.ts +169 -0
- package/hooks/GMTRPromptEnricher.hook.ts +651 -0
- package/hooks/GMTRRatingCapture.hook.ts +198 -0
- package/hooks/GMTRSecurityValidator.hook.ts +399 -0
- package/hooks/GMTRToolTracker.hook.ts +181 -0
- package/hooks/StopOrchestrator.hook.ts +78 -0
- package/hooks/gmtr-tool-tracker-utils.ts +105 -0
- package/hooks/lib/gmtr-hook-utils.ts +770 -0
- package/hooks/lib/identity.ts +227 -0
- package/hooks/lib/notify.ts +46 -0
- package/hooks/lib/paths.ts +104 -0
- package/hooks/lib/transcript-parser.ts +452 -0
- package/hooks/session-end.hook.ts +168 -0
- package/hooks/session-start.hook.ts +501 -0
- 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
|
+
}
|
package/bin/get-token.py
ADDED