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,244 @@
|
|
|
1
|
+
import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { buildClaudeHooksFile } from './install.ts';
|
|
4
|
+
|
|
5
|
+
const MANAGED_EVENTS = [
|
|
6
|
+
'PreToolUse',
|
|
7
|
+
'PostToolUse',
|
|
8
|
+
'UserPromptSubmit',
|
|
9
|
+
'SessionStart',
|
|
10
|
+
'SessionEnd',
|
|
11
|
+
'Stop',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
const LEGACY_HOOK_BASENAMES = [
|
|
15
|
+
'LoadContext.hook.ts',
|
|
16
|
+
'SecurityValidator.hook.ts',
|
|
17
|
+
'RatingCapture.hook.ts',
|
|
18
|
+
'VoiceGate.hook.ts',
|
|
19
|
+
'AutoWorkCreation.hook.ts',
|
|
20
|
+
'WorkCompletionLearning.hook.ts',
|
|
21
|
+
'RelationshipMemory.hook.ts',
|
|
22
|
+
'SessionSummary.hook.ts',
|
|
23
|
+
'UpdateCounts.hook.ts',
|
|
24
|
+
'IntegrityCheck.hook.ts',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const LEGACY_CLIENT_DIRS = ['aios-v2-client'];
|
|
28
|
+
export const LEGACY_REMOVE_DIRS = ['.claude/hooks'];
|
|
29
|
+
export const LEGACY_REMOVE_HOOK_FILES = LEGACY_HOOK_BASENAMES;
|
|
30
|
+
|
|
31
|
+
export interface StaleArtifact {
|
|
32
|
+
kind: 'directory' | 'hook-file';
|
|
33
|
+
path: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MigrationOptions {
|
|
38
|
+
homeDir: string;
|
|
39
|
+
clientDir: string;
|
|
40
|
+
includeOptionalUx?: boolean;
|
|
41
|
+
apply: boolean;
|
|
42
|
+
log?: (message: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface JsonObject {
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is JsonObject {
|
|
50
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function commandLooksLegacy(command: string): boolean {
|
|
54
|
+
if (command.includes('/.claude/hooks/')) return true;
|
|
55
|
+
if (command.includes('/aios-v2-client/')) return true;
|
|
56
|
+
return LEGACY_HOOK_BASENAMES.some((basename) => command.includes(`/${basename}`));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stripLegacyEntriesFromHookEvent(value: unknown): unknown {
|
|
60
|
+
if (!Array.isArray(value)) return value;
|
|
61
|
+
|
|
62
|
+
return value
|
|
63
|
+
.map((entry) => {
|
|
64
|
+
if (!isRecord(entry) || !Array.isArray(entry.hooks)) return entry;
|
|
65
|
+
const hooks = entry.hooks.filter((hook) => {
|
|
66
|
+
if (!isRecord(hook) || typeof hook.command !== 'string') return true;
|
|
67
|
+
return !commandLooksLegacy(hook.command);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (hooks.length === 0) return null;
|
|
71
|
+
return { ...entry, hooks };
|
|
72
|
+
})
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function sanitizeClaudeSettings(
|
|
77
|
+
settings: JsonObject,
|
|
78
|
+
clientDir: string,
|
|
79
|
+
_includeOptionalUx: boolean = false, // deprecated — thin client has no optional hooks
|
|
80
|
+
): JsonObject {
|
|
81
|
+
const next = { ...settings };
|
|
82
|
+
const hooks = isRecord(next.hooks) ? { ...next.hooks } : {};
|
|
83
|
+
|
|
84
|
+
for (const [eventName, eventValue] of Object.entries(hooks)) {
|
|
85
|
+
hooks[eventName] = stripLegacyEntriesFromHookEvent(eventValue);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const managed = buildClaudeHooksFile(clientDir).hooks;
|
|
89
|
+
for (const eventName of MANAGED_EVENTS) {
|
|
90
|
+
hooks[eventName] = managed[eventName];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
next.hooks = hooks;
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function sanitizeClaudeJson(claudeJson: JsonObject): JsonObject {
|
|
98
|
+
if (!isRecord(claudeJson.mcpServers)) return claudeJson;
|
|
99
|
+
const mcpServers = { ...claudeJson.mcpServers };
|
|
100
|
+
delete mcpServers.aios;
|
|
101
|
+
return { ...claudeJson, mcpServers };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function findStaleArtifacts(
|
|
105
|
+
homeDir: string,
|
|
106
|
+
clientDir: string,
|
|
107
|
+
exists: (path: string) => boolean,
|
|
108
|
+
): StaleArtifact[] {
|
|
109
|
+
const artifacts: StaleArtifact[] = [];
|
|
110
|
+
|
|
111
|
+
for (const dir of LEGACY_CLIENT_DIRS) {
|
|
112
|
+
const path = `${homeDir}/${dir}`;
|
|
113
|
+
if (exists(path)) {
|
|
114
|
+
artifacts.push({
|
|
115
|
+
kind: 'directory',
|
|
116
|
+
path,
|
|
117
|
+
reason: 'Legacy client payload directory',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const dir of LEGACY_REMOVE_DIRS) {
|
|
123
|
+
const path = `${homeDir}/${dir}`;
|
|
124
|
+
if (exists(path)) {
|
|
125
|
+
artifacts.push({
|
|
126
|
+
kind: 'directory',
|
|
127
|
+
path,
|
|
128
|
+
reason: 'Legacy Claude hook directory',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const basename of LEGACY_REMOVE_HOOK_FILES) {
|
|
134
|
+
const path = `${clientDir}/hooks/${basename}`;
|
|
135
|
+
if (exists(path)) {
|
|
136
|
+
artifacts.push({
|
|
137
|
+
kind: 'hook-file',
|
|
138
|
+
path,
|
|
139
|
+
reason: 'Deprecated legacy hook file',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return artifacts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function backupFile(path: string, log?: (message: string) => void): void {
|
|
148
|
+
if (!existsSync(path)) return;
|
|
149
|
+
const backup = `${path}.backup-${Date.now()}`;
|
|
150
|
+
cpSync(path, backup);
|
|
151
|
+
log?.(`OK Backed up ${path} -> ${backup}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readJson(path: string): Record<string, unknown> {
|
|
155
|
+
return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeJson(path: string, value: Record<string, unknown>): void {
|
|
159
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function removeIfExists(path: string, log?: (message: string) => void): void {
|
|
163
|
+
if (!existsSync(path)) return;
|
|
164
|
+
rmSync(path, { recursive: true, force: true });
|
|
165
|
+
log?.(`OK Removed ${path}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function removeLegacyHookFiles(clientDir: string, log?: (message: string) => void): void {
|
|
169
|
+
const hooksDir = join(clientDir, 'hooks');
|
|
170
|
+
if (!existsSync(hooksDir)) return;
|
|
171
|
+
|
|
172
|
+
for (const basename of LEGACY_REMOVE_HOOK_FILES) {
|
|
173
|
+
const path = join(hooksDir, basename);
|
|
174
|
+
if (existsSync(path)) {
|
|
175
|
+
rmSync(path, { force: true });
|
|
176
|
+
log?.(`OK Removed stale hook ${path}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const entry of readdirSync(hooksDir)) {
|
|
181
|
+
if (!entry.endsWith('.sh')) continue;
|
|
182
|
+
const path = join(hooksDir, entry);
|
|
183
|
+
if (statSync(path).isFile()) {
|
|
184
|
+
rmSync(path, { force: true });
|
|
185
|
+
log?.(`OK Removed stale shell hook ${path}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function runLegacyMigration(options: MigrationOptions): void {
|
|
191
|
+
const { homeDir, clientDir, includeOptionalUx = false, apply, log } = options;
|
|
192
|
+
const claudeSettingsPath = join(homeDir, '.claude', 'settings.json');
|
|
193
|
+
const claudeJsonPath = join(homeDir, '.claude.json');
|
|
194
|
+
|
|
195
|
+
log?.('grāmatr legacy install cleanup');
|
|
196
|
+
log?.(`Mode: ${apply ? 'apply' : 'dry-run'}`);
|
|
197
|
+
log?.(`Client dir: ${clientDir}`);
|
|
198
|
+
|
|
199
|
+
if (existsSync(claudeSettingsPath)) {
|
|
200
|
+
const sanitized = sanitizeClaudeSettings(readJson(claudeSettingsPath), clientDir, includeOptionalUx);
|
|
201
|
+
if (apply) {
|
|
202
|
+
backupFile(claudeSettingsPath, log);
|
|
203
|
+
writeJson(claudeSettingsPath, sanitized);
|
|
204
|
+
log?.(`OK Sanitized ${claudeSettingsPath}`);
|
|
205
|
+
} else {
|
|
206
|
+
log?.(`Would sanitize ${claudeSettingsPath}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (existsSync(claudeJsonPath)) {
|
|
211
|
+
const sanitized = sanitizeClaudeJson(readJson(claudeJsonPath));
|
|
212
|
+
if (apply) {
|
|
213
|
+
backupFile(claudeJsonPath, log);
|
|
214
|
+
writeJson(claudeJsonPath, sanitized);
|
|
215
|
+
log?.(`OK Sanitized ${claudeJsonPath}`);
|
|
216
|
+
} else {
|
|
217
|
+
log?.(`Would sanitize ${claudeJsonPath}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const dir of LEGACY_CLIENT_DIRS) {
|
|
222
|
+
const path = join(homeDir, dir);
|
|
223
|
+
if (apply) {
|
|
224
|
+
removeIfExists(path, log);
|
|
225
|
+
} else if (existsSync(path)) {
|
|
226
|
+
log?.(`Would remove ${path}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const dir of LEGACY_REMOVE_DIRS) {
|
|
231
|
+
const path = join(homeDir, dir);
|
|
232
|
+
if (apply) {
|
|
233
|
+
removeIfExists(path, log);
|
|
234
|
+
} else if (existsSync(path)) {
|
|
235
|
+
log?.(`Would remove ${path}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (apply) {
|
|
240
|
+
removeLegacyHookFiles(clientDir, log);
|
|
241
|
+
} else if (existsSync(join(clientDir, 'hooks'))) {
|
|
242
|
+
log?.(`Would remove stale hook files from ${join(clientDir, 'hooks')}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
package/core/routing.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callMcpToolDetailed,
|
|
3
|
+
resolveMcpUrl,
|
|
4
|
+
saveLastClassification,
|
|
5
|
+
type MctToolCallError,
|
|
6
|
+
} from '../hooks/lib/gmtr-hook-utils.ts';
|
|
7
|
+
import type { RouteResponse } from './types.ts';
|
|
8
|
+
|
|
9
|
+
export const MIN_PROMPT_LENGTH = 10;
|
|
10
|
+
export const TRIVIAL_PATTERNS =
|
|
11
|
+
/^(hi|hey|hello|yo|ok|yes|no|thanks|thank you|sure|yep|nope|k|bye|quit|exit)\b/i;
|
|
12
|
+
|
|
13
|
+
export function shouldSkipPromptRouting(prompt: string): boolean {
|
|
14
|
+
const trimmed = prompt.trim();
|
|
15
|
+
return trimmed.length < MIN_PROMPT_LENGTH || TRIVIAL_PATTERNS.test(trimmed);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function routePrompt(options: {
|
|
19
|
+
prompt: string;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}): Promise<{ route: RouteResponse | null; error: MctToolCallError | null }> {
|
|
24
|
+
const result = await callMcpToolDetailed<RouteResponse>(
|
|
25
|
+
'gmtr_route_request',
|
|
26
|
+
{
|
|
27
|
+
prompt: options.prompt,
|
|
28
|
+
...(options.projectId ? { project_id: options.projectId } : {}),
|
|
29
|
+
...(options.sessionId ? { session_id: options.sessionId } : {}),
|
|
30
|
+
},
|
|
31
|
+
options.timeoutMs ?? 15000,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
route: result.data,
|
|
36
|
+
error: result.error,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function describeRoutingFailure(error: MctToolCallError): {
|
|
41
|
+
title: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
action: string;
|
|
44
|
+
} {
|
|
45
|
+
switch (error.reason) {
|
|
46
|
+
case 'auth':
|
|
47
|
+
return {
|
|
48
|
+
title: 'Routing request failed due to authentication.',
|
|
49
|
+
detail: error.detail,
|
|
50
|
+
action: 'Check the configured GMTR token for the hook runtime.',
|
|
51
|
+
};
|
|
52
|
+
case 'timeout':
|
|
53
|
+
return {
|
|
54
|
+
title: 'Routing request timed out.',
|
|
55
|
+
detail: error.detail,
|
|
56
|
+
action: 'Check gramatr server latency or increase the hook timeout.',
|
|
57
|
+
};
|
|
58
|
+
case 'network_error':
|
|
59
|
+
return {
|
|
60
|
+
title: 'Routing request could not reach the gramatr MCP endpoint.',
|
|
61
|
+
detail: error.detail,
|
|
62
|
+
action: `Verify connectivity to ${resolveMcpUrl()}.`,
|
|
63
|
+
};
|
|
64
|
+
case 'http_error':
|
|
65
|
+
case 'mcp_error':
|
|
66
|
+
case 'parse_error':
|
|
67
|
+
case 'unknown':
|
|
68
|
+
default:
|
|
69
|
+
return {
|
|
70
|
+
title: 'Routing request failed before intelligence could be injected.',
|
|
71
|
+
detail: error.detail,
|
|
72
|
+
action: `Inspect the response from ${resolveMcpUrl()} and the gmtr_route_request handler.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function persistClassificationResult(options: {
|
|
78
|
+
rootDir: string;
|
|
79
|
+
prompt: string;
|
|
80
|
+
route: RouteResponse | null;
|
|
81
|
+
downstreamModel: string | null;
|
|
82
|
+
clientType: string;
|
|
83
|
+
agentName: string;
|
|
84
|
+
}): void {
|
|
85
|
+
saveLastClassification(options.rootDir, {
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
original_prompt: options.prompt,
|
|
88
|
+
effort_level: options.route?.classification?.effort_level || null,
|
|
89
|
+
intent_type: options.route?.classification?.intent_type || null,
|
|
90
|
+
confidence: options.route?.classification?.confidence ?? null,
|
|
91
|
+
classifier_model: options.route?.execution_summary?.qwen_model || null,
|
|
92
|
+
downstream_model: options.downstreamModel || null,
|
|
93
|
+
client_type: options.clientType,
|
|
94
|
+
agent_name: options.agentName,
|
|
95
|
+
pending_feedback: true,
|
|
96
|
+
feedback_submitted_at: null,
|
|
97
|
+
});
|
|
98
|
+
}
|
package/core/session.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { basename, join } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
callMcpTool,
|
|
5
|
+
createDefaultConfig,
|
|
6
|
+
deriveProjectId,
|
|
7
|
+
type GitContext,
|
|
8
|
+
type GmtrConfig,
|
|
9
|
+
migrateConfig,
|
|
10
|
+
now,
|
|
11
|
+
readGmtrConfig,
|
|
12
|
+
writeGmtrConfig,
|
|
13
|
+
} from '../hooks/lib/gmtr-hook-utils.ts';
|
|
14
|
+
import type { HandoffResponse, SessionStartResponse } from './types.ts';
|
|
15
|
+
|
|
16
|
+
export interface PreparedProjectSession {
|
|
17
|
+
projectId: string;
|
|
18
|
+
config: GmtrConfig;
|
|
19
|
+
projectEntityId: string | null;
|
|
20
|
+
restoreNeeded: boolean;
|
|
21
|
+
hasRestoreContext: boolean;
|
|
22
|
+
created: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CurrentProjectContextPayload {
|
|
26
|
+
type: 'git_project' | 'non_git';
|
|
27
|
+
session_id: string;
|
|
28
|
+
project_name: string;
|
|
29
|
+
working_directory: string;
|
|
30
|
+
session_start: string;
|
|
31
|
+
gmtr_config_path: string;
|
|
32
|
+
project_entity_id: string | null;
|
|
33
|
+
action_required: string;
|
|
34
|
+
project_id?: string;
|
|
35
|
+
git_root?: string;
|
|
36
|
+
git_branch?: string;
|
|
37
|
+
git_commit?: string;
|
|
38
|
+
git_remote?: string;
|
|
39
|
+
restore_needed?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeSessionStartResponse(
|
|
43
|
+
response: SessionStartResponse | null | undefined,
|
|
44
|
+
): {
|
|
45
|
+
interactionId: string | null;
|
|
46
|
+
entityId: string | null;
|
|
47
|
+
resumed: boolean;
|
|
48
|
+
handoffContext: string | null;
|
|
49
|
+
} {
|
|
50
|
+
return {
|
|
51
|
+
interactionId: response?.interaction_id || response?.interactionId || null,
|
|
52
|
+
entityId: response?.entity_id || response?.entityId || null,
|
|
53
|
+
resumed: response?.interaction_resumed === true || response?.interactionResumed === true,
|
|
54
|
+
handoffContext: response?.handoff_context || response?.handoffContext || null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function prepareProjectSessionState(options: {
|
|
59
|
+
git: GitContext;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
transcriptPath: string;
|
|
62
|
+
}): PreparedProjectSession {
|
|
63
|
+
const { git, sessionId, transcriptPath } = options;
|
|
64
|
+
const projectId = deriveProjectId(git.remote, git.projectName);
|
|
65
|
+
const timestamp = now();
|
|
66
|
+
|
|
67
|
+
let config = readGmtrConfig(git.root);
|
|
68
|
+
let created = false;
|
|
69
|
+
|
|
70
|
+
if (config) {
|
|
71
|
+
config = migrateConfig(config, projectId);
|
|
72
|
+
config.last_session_id = sessionId;
|
|
73
|
+
config.project_id = projectId;
|
|
74
|
+
config.current_session = {
|
|
75
|
+
...config.current_session,
|
|
76
|
+
session_id: sessionId,
|
|
77
|
+
transcript_path: transcriptPath,
|
|
78
|
+
last_updated: timestamp,
|
|
79
|
+
token_limit: 200000,
|
|
80
|
+
};
|
|
81
|
+
config.continuity_stats = config.continuity_stats || {};
|
|
82
|
+
config.continuity_stats.total_sessions = (config.continuity_stats.total_sessions || 0) + 1;
|
|
83
|
+
config.metadata = config.metadata || {};
|
|
84
|
+
config.metadata.updated = timestamp;
|
|
85
|
+
} else {
|
|
86
|
+
config = createDefaultConfig({
|
|
87
|
+
projectId,
|
|
88
|
+
projectName: git.projectName,
|
|
89
|
+
gitRemote: git.remote,
|
|
90
|
+
sessionId,
|
|
91
|
+
transcriptPath,
|
|
92
|
+
});
|
|
93
|
+
created = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
writeGmtrConfig(git.root, config);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
projectId,
|
|
100
|
+
config,
|
|
101
|
+
projectEntityId: config.project_entity_id || null,
|
|
102
|
+
restoreNeeded: config.last_compact?.timestamp != null,
|
|
103
|
+
hasRestoreContext: config.last_compact?.summary != null,
|
|
104
|
+
created,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function startRemoteSession(options: {
|
|
109
|
+
clientType: string;
|
|
110
|
+
sessionId?: string;
|
|
111
|
+
projectId: string;
|
|
112
|
+
projectName?: string;
|
|
113
|
+
gitRemote: string;
|
|
114
|
+
directory: string;
|
|
115
|
+
}): Promise<SessionStartResponse | null> {
|
|
116
|
+
return (await callMcpTool(
|
|
117
|
+
'gmtr_session_start',
|
|
118
|
+
{
|
|
119
|
+
client_type: options.clientType,
|
|
120
|
+
project_id: options.projectId,
|
|
121
|
+
...(options.projectName ? { project_name: options.projectName } : {}),
|
|
122
|
+
git_remote: options.gitRemote,
|
|
123
|
+
directory: options.directory,
|
|
124
|
+
...(options.sessionId ? { session_id: options.sessionId } : {}),
|
|
125
|
+
},
|
|
126
|
+
15000,
|
|
127
|
+
)) as SessionStartResponse | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function loadProjectHandoff(projectId: string): Promise<HandoffResponse | null> {
|
|
131
|
+
return (await callMcpTool(
|
|
132
|
+
'gmtr_load_handoff',
|
|
133
|
+
{ project_id: projectId },
|
|
134
|
+
15000,
|
|
135
|
+
)) as HandoffResponse | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function persistSessionRegistration(rootDir: string, response: SessionStartResponse | null): GmtrConfig | null {
|
|
139
|
+
const normalized = normalizeSessionStartResponse(response);
|
|
140
|
+
if (!normalized.interactionId && !normalized.entityId) return readGmtrConfig(rootDir);
|
|
141
|
+
|
|
142
|
+
const config = readGmtrConfig(rootDir);
|
|
143
|
+
if (!config) return null;
|
|
144
|
+
config.current_session = config.current_session || {};
|
|
145
|
+
if (normalized.interactionId) config.current_session.interaction_id = normalized.interactionId;
|
|
146
|
+
if (normalized.entityId) config.current_session.gmtr_entity_id = normalized.entityId;
|
|
147
|
+
writeGmtrConfig(rootDir, config);
|
|
148
|
+
return config;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function writeCurrentProjectContextFile(payload: CurrentProjectContextPayload): void {
|
|
152
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
153
|
+
const claudeDir = join(home, '.claude');
|
|
154
|
+
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
|
|
155
|
+
const contextFile = join(claudeDir, 'current-project-context.json');
|
|
156
|
+
writeFileSync(contextFile, JSON.stringify(payload, null, 2) + '\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildGitProjectContextPayload(options: {
|
|
160
|
+
git: GitContext;
|
|
161
|
+
sessionId: string;
|
|
162
|
+
workingDirectory: string;
|
|
163
|
+
sessionStart: string;
|
|
164
|
+
projectId: string;
|
|
165
|
+
projectEntityId: string | null;
|
|
166
|
+
restoreNeeded: boolean;
|
|
167
|
+
}): CurrentProjectContextPayload {
|
|
168
|
+
return {
|
|
169
|
+
type: 'git_project',
|
|
170
|
+
session_id: options.sessionId,
|
|
171
|
+
project_name: options.git.projectName,
|
|
172
|
+
project_id: options.projectId,
|
|
173
|
+
git_root: options.git.root,
|
|
174
|
+
git_branch: options.git.branch,
|
|
175
|
+
git_commit: options.git.commit,
|
|
176
|
+
git_remote: options.git.remote,
|
|
177
|
+
working_directory: options.workingDirectory,
|
|
178
|
+
session_start: options.sessionStart,
|
|
179
|
+
gmtr_config_path: join(options.git.root, '.gmtr', 'settings.json'),
|
|
180
|
+
project_entity_id: options.projectEntityId,
|
|
181
|
+
restore_needed: options.restoreNeeded,
|
|
182
|
+
action_required: 'check_or_create_project_entity',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function buildNonGitProjectContextPayload(options: {
|
|
187
|
+
cwd: string;
|
|
188
|
+
sessionId: string;
|
|
189
|
+
sessionStart: string;
|
|
190
|
+
projectEntityId: string | null;
|
|
191
|
+
}): CurrentProjectContextPayload {
|
|
192
|
+
return {
|
|
193
|
+
type: 'non_git',
|
|
194
|
+
session_id: options.sessionId,
|
|
195
|
+
project_name: basename(options.cwd),
|
|
196
|
+
working_directory: options.cwd,
|
|
197
|
+
session_start: options.sessionStart,
|
|
198
|
+
gmtr_config_path: join(options.cwd, '.gmtr', 'settings.json'),
|
|
199
|
+
project_entity_id: options.projectEntityId,
|
|
200
|
+
action_required: 'gmtr_init_needed',
|
|
201
|
+
};
|
|
202
|
+
}
|