gramatr 0.3.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/CLAUDE.md +18 -0
- package/README.md +78 -0
- package/bin/clean-legacy-install.ts +28 -0
- package/bin/get-token.py +3 -0
- package/bin/gmtr-login.ts +547 -0
- package/bin/gramatr.js +33 -0
- package/bin/gramatr.ts +248 -0
- package/bin/install.ts +756 -0
- package/bin/render-claude-hooks.ts +16 -0
- package/bin/statusline.ts +437 -0
- package/bin/uninstall.ts +289 -0
- package/bin/version-sync.ts +46 -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 +76 -0
- package/codex/install.ts +99 -0
- package/codex/lib/codex-hook-utils.ts +48 -0
- package/codex/lib/codex-install-utils.ts +123 -0
- package/core/feedback.ts +55 -0
- package/core/formatting.ts +167 -0
- package/core/install.ts +114 -0
- package/core/installer-cli.ts +122 -0
- package/core/migration.ts +244 -0
- package/core/routing.ts +98 -0
- package/core/session.ts +202 -0
- package/core/targets.ts +292 -0
- package/core/types.ts +178 -0
- package/core/version.ts +2 -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 +74 -0
- package/gemini/install.ts +272 -0
- package/gemini/lib/gemini-hook-utils.ts +63 -0
- package/gemini/lib/gemini-install-utils.ts +169 -0
- package/hooks/GMTRPromptEnricher.hook.ts +650 -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 +771 -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 +490 -0
- package/package.json +54 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Identity Loader
|
|
3
|
+
* Single source of truth for DA (Digital Assistant) and Principal identity
|
|
4
|
+
*
|
|
5
|
+
* Reads from settings.json - the programmatic way, not markdown parsing.
|
|
6
|
+
* All hooks and tools should import from here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
const HOME = process.env.HOME!;
|
|
13
|
+
const GMTR_DIR = process.env.GMTR_DIR || join(HOME, 'gmtr-client');
|
|
14
|
+
const GMTR_SETTINGS_PATH = join(GMTR_DIR, 'settings.json'); // gramatr-owned, vendor-agnostic
|
|
15
|
+
const CLAUDE_SETTINGS_PATH = join(HOME, '.claude/settings.json'); // legacy fallback only
|
|
16
|
+
|
|
17
|
+
// Default identity (fallback if settings.json doesn't have identity section)
|
|
18
|
+
const DEFAULT_IDENTITY = {
|
|
19
|
+
name: 'gramatr',
|
|
20
|
+
fullName: 'gramatr — Personal AI',
|
|
21
|
+
displayName: 'gramatr',
|
|
22
|
+
mainDAVoiceID: '',
|
|
23
|
+
color: '#3B82F6',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PRINCIPAL = {
|
|
27
|
+
name: 'User',
|
|
28
|
+
pronunciation: '',
|
|
29
|
+
timezone: 'UTC',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface VoiceProsody {
|
|
33
|
+
stability: number;
|
|
34
|
+
similarity_boost: number;
|
|
35
|
+
style: number;
|
|
36
|
+
speed: number;
|
|
37
|
+
use_speaker_boost: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VoicePersonality {
|
|
41
|
+
baseVoice: string;
|
|
42
|
+
enthusiasm: number;
|
|
43
|
+
energy: number;
|
|
44
|
+
expressiveness: number;
|
|
45
|
+
resilience: number;
|
|
46
|
+
composure: number;
|
|
47
|
+
optimism: number;
|
|
48
|
+
warmth: number;
|
|
49
|
+
formality: number;
|
|
50
|
+
directness: number;
|
|
51
|
+
precision: number;
|
|
52
|
+
curiosity: number;
|
|
53
|
+
playfulness: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Identity {
|
|
57
|
+
name: string;
|
|
58
|
+
fullName: string;
|
|
59
|
+
displayName: string;
|
|
60
|
+
mainDAVoiceID: string;
|
|
61
|
+
color: string;
|
|
62
|
+
voice?: VoiceProsody;
|
|
63
|
+
personality?: VoicePersonality;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Principal {
|
|
67
|
+
name: string;
|
|
68
|
+
pronunciation: string;
|
|
69
|
+
timezone: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Settings {
|
|
73
|
+
daidentity?: Partial<Identity>;
|
|
74
|
+
principal?: Partial<Principal>;
|
|
75
|
+
env?: Record<string, string>;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let cachedSettings: Settings | null = null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load settings — gramatr-owned config first, Claude vendor file as legacy fallback.
|
|
83
|
+
*
|
|
84
|
+
* Priority:
|
|
85
|
+
* 1. ~/gmtr-client/settings.json (gramatr-owned, vendor-agnostic)
|
|
86
|
+
* 2. ~/.claude/settings.json (Claude vendor file — legacy fallback only)
|
|
87
|
+
*
|
|
88
|
+
* Merges both: gramatr settings take precedence for identity fields,
|
|
89
|
+
* Claude settings provide hook/permission config that gramatr doesn't own.
|
|
90
|
+
*/
|
|
91
|
+
function loadSettings(): Settings {
|
|
92
|
+
if (cachedSettings) return cachedSettings;
|
|
93
|
+
|
|
94
|
+
let gmtrSettings: Settings = {};
|
|
95
|
+
let claudeSettings: Settings = {};
|
|
96
|
+
|
|
97
|
+
// Load gramatr-owned settings (primary source for identity)
|
|
98
|
+
try {
|
|
99
|
+
if (existsSync(GMTR_SETTINGS_PATH)) {
|
|
100
|
+
gmtrSettings = JSON.parse(readFileSync(GMTR_SETTINGS_PATH, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
} catch { /* ignore parse errors */ }
|
|
103
|
+
|
|
104
|
+
// Load Claude vendor settings (fallback, also provides hooks/permissions)
|
|
105
|
+
try {
|
|
106
|
+
if (existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
107
|
+
claudeSettings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8'));
|
|
108
|
+
}
|
|
109
|
+
} catch { /* ignore parse errors */ }
|
|
110
|
+
|
|
111
|
+
// Merge: gramatr settings override Claude settings for identity fields
|
|
112
|
+
cachedSettings = {
|
|
113
|
+
...claudeSettings,
|
|
114
|
+
// gramatr-owned fields take precedence
|
|
115
|
+
...(gmtrSettings.daidentity ? { daidentity: gmtrSettings.daidentity } : {}),
|
|
116
|
+
...(gmtrSettings.principal ? { principal: gmtrSettings.principal } : {}),
|
|
117
|
+
...(gmtrSettings.notifications ? { notifications: gmtrSettings.notifications } : {}),
|
|
118
|
+
...(gmtrSettings.counts ? { counts: gmtrSettings.counts } : {}),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return cachedSettings!;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get DA (Digital Assistant) identity from settings.json
|
|
126
|
+
*/
|
|
127
|
+
export function getIdentity(): Identity {
|
|
128
|
+
const settings = loadSettings();
|
|
129
|
+
|
|
130
|
+
// Prefer settings.daidentity, fall back to env.DA for backward compat
|
|
131
|
+
const daidentity = settings.daidentity || {};
|
|
132
|
+
const envDA = settings.env?.DA;
|
|
133
|
+
|
|
134
|
+
// Support both old (daidentity.voice) and new (daidentity.voices.main) structures
|
|
135
|
+
const voices = (daidentity as any).voices || {};
|
|
136
|
+
const voiceConfig = voices.main || (daidentity as any).voice;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
name: daidentity.name || envDA || DEFAULT_IDENTITY.name,
|
|
140
|
+
fullName: daidentity.fullName || daidentity.name || envDA || DEFAULT_IDENTITY.fullName,
|
|
141
|
+
displayName: daidentity.displayName || daidentity.name || envDA || DEFAULT_IDENTITY.displayName,
|
|
142
|
+
mainDAVoiceID: voiceConfig?.voiceId || (daidentity as any).voiceId || daidentity.mainDAVoiceID || DEFAULT_IDENTITY.mainDAVoiceID,
|
|
143
|
+
color: daidentity.color || DEFAULT_IDENTITY.color,
|
|
144
|
+
voice: voiceConfig as VoiceProsody | undefined,
|
|
145
|
+
personality: (daidentity as any).personality as VoicePersonality | undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get Principal (human owner) identity from settings.json
|
|
151
|
+
*/
|
|
152
|
+
export function getPrincipal(): Principal {
|
|
153
|
+
const settings = loadSettings();
|
|
154
|
+
|
|
155
|
+
// Prefer settings.principal, fall back to env.PRINCIPAL for backward compat
|
|
156
|
+
const principal = settings.principal || {};
|
|
157
|
+
const envPrincipal = settings.env?.PRINCIPAL;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name: principal.name || envPrincipal || DEFAULT_PRINCIPAL.name,
|
|
161
|
+
pronunciation: principal.pronunciation || DEFAULT_PRINCIPAL.pronunciation,
|
|
162
|
+
timezone: principal.timezone || DEFAULT_PRINCIPAL.timezone,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear cache (useful for testing or when settings.json changes)
|
|
168
|
+
*/
|
|
169
|
+
export function clearCache(): void {
|
|
170
|
+
cachedSettings = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get just the DA name (convenience function)
|
|
175
|
+
*/
|
|
176
|
+
export function getDAName(): string {
|
|
177
|
+
return getIdentity().name;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get just the Principal name (convenience function)
|
|
182
|
+
*/
|
|
183
|
+
export function getPrincipalName(): string {
|
|
184
|
+
return getPrincipal().name;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get just the voice ID (convenience function)
|
|
189
|
+
*/
|
|
190
|
+
export function getVoiceId(): string {
|
|
191
|
+
return getIdentity().mainDAVoiceID;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the full settings object (for advanced use)
|
|
196
|
+
*/
|
|
197
|
+
export function getSettings(): Settings {
|
|
198
|
+
return loadSettings();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the default identity (for documentation/testing)
|
|
203
|
+
*/
|
|
204
|
+
export function getDefaultIdentity(): Identity {
|
|
205
|
+
return { ...DEFAULT_IDENTITY };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the default principal (for documentation/testing)
|
|
210
|
+
*/
|
|
211
|
+
export function getDefaultPrincipal(): Principal {
|
|
212
|
+
return { ...DEFAULT_PRINCIPAL };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get voice prosody settings (convenience function) - legacy ElevenLabs
|
|
217
|
+
*/
|
|
218
|
+
export function getVoiceProsody(): VoiceProsody | undefined {
|
|
219
|
+
return getIdentity().voice;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get voice personality settings (convenience function)
|
|
224
|
+
*/
|
|
225
|
+
export function getVoicePersonality(): VoicePersonality | undefined {
|
|
226
|
+
return getIdentity().personality;
|
|
227
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gramatr Notification Utility
|
|
3
|
+
*
|
|
4
|
+
* Sends local push notifications on macOS via osascript.
|
|
5
|
+
* Fails silently on non-macOS or if osascript is unavailable.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { notify } from './lib/notify';
|
|
9
|
+
* notify('Task complete', 'Built 3 files');
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { platform } from 'os';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send a local push notification.
|
|
16
|
+
* macOS: uses osascript display notification
|
|
17
|
+
* Other platforms: no-op (silent)
|
|
18
|
+
*/
|
|
19
|
+
export function notify(subtitle: string, message: string): void {
|
|
20
|
+
if (platform() !== 'darwin') return;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Escape double quotes in message and subtitle
|
|
24
|
+
const safeMsg = message.replace(/"/g, '\\"');
|
|
25
|
+
const safeSub = subtitle.replace(/"/g, '\\"');
|
|
26
|
+
|
|
27
|
+
const { spawn } = require('child_process');
|
|
28
|
+
spawn('osascript', ['-e',
|
|
29
|
+
`display notification "${safeMsg}" with title "gramatr" subtitle "${safeSub}"`,
|
|
30
|
+
], { stdio: 'ignore', detached: true }).unref();
|
|
31
|
+
} catch {
|
|
32
|
+
// Notification is best-effort — never block on failure
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send a notification for a security event.
|
|
38
|
+
*/
|
|
39
|
+
export function notifySecurity(event: 'block' | 'confirm' | 'alert', reason: string): void {
|
|
40
|
+
const labels = {
|
|
41
|
+
block: 'Security Block',
|
|
42
|
+
confirm: 'Security Confirm',
|
|
43
|
+
alert: 'Security Alert',
|
|
44
|
+
};
|
|
45
|
+
notify(labels[event], reason);
|
|
46
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Path Resolution
|
|
3
|
+
*
|
|
4
|
+
* Handles environment variable expansion for portable GMTR configuration.
|
|
5
|
+
* Claude Code doesn't expand $HOME in settings.json env values, so we do it here.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { getGmtrDir, getSettingsPath } from './lib/paths';
|
|
9
|
+
* const gmtrDir = getGmtrDir(); // Always returns expanded absolute path
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expand shell variables in a path string
|
|
18
|
+
* Supports: $HOME, ${HOME}, ~
|
|
19
|
+
*/
|
|
20
|
+
export function expandPath(path: string): string {
|
|
21
|
+
const home = homedir();
|
|
22
|
+
|
|
23
|
+
return path
|
|
24
|
+
.replace(/^\$HOME(?=\/|$)/, home)
|
|
25
|
+
.replace(/^\$\{HOME\}(?=\/|$)/, home)
|
|
26
|
+
.replace(/^~(?=\/|$)/, home);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the GMTR base directory (expanded)
|
|
31
|
+
* Priority: GMTR_DIR env var → PAI_DIR env var (migration) → ~/.claude
|
|
32
|
+
*/
|
|
33
|
+
export function getGmtrDir(): string {
|
|
34
|
+
const envGmtrDir = process.env.GMTR_DIR;
|
|
35
|
+
if (envGmtrDir) {
|
|
36
|
+
return expandPath(envGmtrDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Migration fallback: honor PAI_DIR if GMTR_DIR not set yet
|
|
40
|
+
const envPaiDir = process.env.PAI_DIR;
|
|
41
|
+
if (envPaiDir) {
|
|
42
|
+
return expandPath(envPaiDir);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return join(homedir(), '.claude');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// getPaiDir removed — all callers migrated to getGmtrDir (#116)
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the settings.json path
|
|
52
|
+
* Always ~/.claude/settings.json — NOT relative to GMTR_DIR.
|
|
53
|
+
* GMTR_DIR is the client payload directory (~/gmtr-client), not ~/.claude.
|
|
54
|
+
*/
|
|
55
|
+
export function getSettingsPath(): string {
|
|
56
|
+
return join(homedir(), '.claude', 'settings.json');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a path relative to GMTR_DIR
|
|
61
|
+
*/
|
|
62
|
+
export function gmtrPath(...segments: string[]): string {
|
|
63
|
+
return join(getGmtrDir(), ...segments);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Migration alias
|
|
67
|
+
export const paiPath = gmtrPath;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the hooks directory
|
|
71
|
+
*/
|
|
72
|
+
export function getHooksDir(): string {
|
|
73
|
+
return gmtrPath('hooks');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the skills directory
|
|
78
|
+
*/
|
|
79
|
+
export function getSkillsDir(): string {
|
|
80
|
+
return gmtrPath('skills');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the MEMORY directory
|
|
85
|
+
*/
|
|
86
|
+
export function getMemoryDir(): string {
|
|
87
|
+
return gmtrPath('MEMORY');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the tools directory
|
|
92
|
+
* Contains deterministic tools for hook lifecycle, agent-callable, and skill-workflow use.
|
|
93
|
+
*/
|
|
94
|
+
export function getToolsDir(): string {
|
|
95
|
+
return gmtrPath('tools');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the system skill directory
|
|
100
|
+
* Post-restructure: files live directly in skills/ (no GMTR/ or PAI/ subdirectory)
|
|
101
|
+
*/
|
|
102
|
+
export function getSystemSkillDir(): string {
|
|
103
|
+
return getSkillsDir();
|
|
104
|
+
}
|