@gramatr/client 0.6.10 → 0.6.11
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 +12 -0
- package/README.md +1 -1
- package/core/install.ts +1 -0
- package/hooks/generated/schema-constants.ts +100 -0
- package/hooks/gramatr-input-validator.hook.ts +133 -0
- package/package.json +2 -1
- package/scripts/generate-schema-constants.ts +164 -0
- package/test/install-matrix/Dockerfile.linux-fresh +25 -0
- package/test/install-matrix/Dockerfile.linux-wsl +32 -0
- package/test/install-matrix/mock-auth-server.mjs +164 -0
- package/test/install-matrix/run-install-test.sh +335 -0
package/CLAUDE.md
CHANGED
|
@@ -10,6 +10,18 @@ ISC scaffold, capability audit, phase templates, and composed agents.
|
|
|
10
10
|
**Memory:** Use gramatr MCP tools (`search_semantic`, `create_entity`, `add_observation`),
|
|
11
11
|
not local markdown files.
|
|
12
12
|
|
|
13
|
+
**Entity types (33-type taxonomy):** When calling `create_entity`, `entity_type` MUST be
|
|
14
|
+
one of: user_profile, organization, team, project, decision, task, milestone, prd,
|
|
15
|
+
steering_rule, skill, agent_definition, playbook, standard, reference, blog_idea,
|
|
16
|
+
agent_diary, session, conversation, turn, classification_record, agent_execution,
|
|
17
|
+
intelligence_packet, learning_signal, learning_reflection, learning_pattern,
|
|
18
|
+
learning_correction, attachment, execution_record, model_evaluation, benchmark,
|
|
19
|
+
external_connection, infrastructure, audit_log. Invalid types will be blocked by
|
|
20
|
+
the PreToolUse hook. Call `gramatr_list_entity_types` for descriptions.
|
|
21
|
+
|
|
22
|
+
**Required fields:** task/milestone status: open|in_progress|blocked|review|done.
|
|
23
|
+
Decision requires status + project_id. Session requires status + project_id.
|
|
24
|
+
|
|
13
25
|
**Identity:** Read from `~/.gramatr/settings.json` — `daidentity` for your name,
|
|
14
26
|
`principal` for the user's name.
|
|
15
27
|
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Every interaction trains the classifier. Memory powers routing, routing generate
|
|
|
64
64
|
gramatr is a thin client + smart server:
|
|
65
65
|
|
|
66
66
|
- **Client** (this package): 29 files, ~290KB. Hooks into your AI agent, forwards to server.
|
|
67
|
-
- **Server**: Decision routing engine (BERT +
|
|
67
|
+
- **Server**: Decision routing engine (BERT + Llama 3.2 3B), knowledge graph (PostgreSQL + pgvector), pattern learning.
|
|
68
68
|
- **Protocol**: MCP (Model Context Protocol) for Claude Code/Codex, REST API for web/mobile.
|
|
69
69
|
|
|
70
70
|
The client never stores intelligence locally. The server delivers everything: behavioral rules, skill routing, agent composition, ISC scaffolds, capability audits.
|
package/core/install.ts
CHANGED
|
@@ -31,6 +31,7 @@ const CLAUDE_HOOKS: HookSpec[] = [
|
|
|
31
31
|
{ event: 'PreToolUse', matcher: 'Edit', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
32
32
|
{ event: 'PreToolUse', matcher: 'Write', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
33
33
|
{ event: 'PreToolUse', matcher: 'Read', relativeCommand: 'hooks/gramatr-security-validator.hook.ts' },
|
|
34
|
+
{ event: 'PreToolUse', matcher: 'mcp__.*gramatr.*__', relativeCommand: 'hooks/gramatr-input-validator.hook.ts' },
|
|
34
35
|
{ event: 'PostToolUse', matcher: 'mcp__.*gramatr.*__', relativeCommand: 'hooks/gramatr-tool-tracker.hook.ts' },
|
|
35
36
|
{ event: 'UserPromptSubmit', relativeCommand: 'hooks/gramatr-rating-capture.hook.ts' },
|
|
36
37
|
{ event: 'UserPromptSubmit', relativeCommand: 'hooks/gramatr-prompt-enricher.hook.ts' },
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED — do not edit manually.
|
|
3
|
+
* Source: @gramatr/core/contracts/mcp-schemas.ts
|
|
4
|
+
* Generator: scripts/generate-schema-constants.ts
|
|
5
|
+
* Generated: 2026-04-09T18:14:19.706Z
|
|
6
|
+
*
|
|
7
|
+
* These constants are extracted from canonical Zod schemas at build time
|
|
8
|
+
* so client hooks can validate MCP tool inputs without depending on
|
|
9
|
+
* @gramatr/core at runtime.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Classifier enums ──
|
|
13
|
+
export const EFFORT_LEVELS = ["instant","fast","standard","extended","deep","advanced","comprehensive"] as const;
|
|
14
|
+
export const INTENT_TYPES = ["search","retrieve","create","update","analyze","generate"] as const;
|
|
15
|
+
export const MEMORY_TIERS = ["hot","warm","cold","none"] as const;
|
|
16
|
+
|
|
17
|
+
// ── Entity type enum (33-type taxonomy) ──
|
|
18
|
+
export const ENTITY_TYPES = ["user_profile","organization","team","project","decision","task","milestone","prd","steering_rule","skill","agent_definition","playbook","standard","reference","blog_idea","agent_diary","session","conversation","turn","classification_record","agent_execution","intelligence_packet","learning_signal","learning_reflection","learning_pattern","learning_correction","attachment","execution_record","model_evaluation","benchmark","external_connection","infrastructure","audit_log"] as const;
|
|
19
|
+
|
|
20
|
+
// ── Lifecycle enums ──
|
|
21
|
+
export const WORK_STATUSES = ["open","in_progress","blocked","review","done"] as const;
|
|
22
|
+
export const PROJECT_STATUSES = ["active","archived","planning"] as const;
|
|
23
|
+
export const SESSION_STATUSES = ["active","completed","abandoned"] as const;
|
|
24
|
+
export const DECISION_STATUSES = ["proposed","accepted","superseded","deprecated"] as const;
|
|
25
|
+
export const PRD_STATUSES = ["draft","criteria_defined","planned","in_progress","verifying","complete","failed","blocked"] as const;
|
|
26
|
+
export const OUTCOMES = ["success","partial","failed","abandoned"] as const;
|
|
27
|
+
export const EMBEDDING_STATUSES = ["pending","processing","complete","failed"] as const;
|
|
28
|
+
|
|
29
|
+
// ── Domain & classification ──
|
|
30
|
+
export const DOMAINS = ["data-engineering","platform-engineering","ai-infrastructure","product","operations","security","frontend","backend","devops","design"] as const;
|
|
31
|
+
export const ALGORITHM_PHASES = ["observe","think","plan","build","execute","verify","learn"] as const;
|
|
32
|
+
export const AGENT_TYPES = ["Architect","Engineer","Researcher","QATester","Designer","Intern","Pentester","Artist","custom"] as const;
|
|
33
|
+
export const PATTERN_TYPES = ["sequential_actions","tool_preference","effort_pattern","time_pattern","domain_affinity","skill_chain"] as const;
|
|
34
|
+
export const DIARY_SCOPES = ["project","user","team","org"] as const;
|
|
35
|
+
|
|
36
|
+
// ── Infrastructure & connection ──
|
|
37
|
+
export const INFRA_TYPES = ["compute","database","storage","network","workstation","cluster","service"] as const;
|
|
38
|
+
export const EXTERNAL_SERVICES = ["superset","salesforce","github","jira","linear","slack","discord","notion","custom"] as const;
|
|
39
|
+
export const AUTH_TYPES = ["oauth2","api_key","basic","token"] as const;
|
|
40
|
+
export const CONNECTION_STATUSES = ["connected","expired","revoked","error"] as const;
|
|
41
|
+
|
|
42
|
+
// ── Knowledge & content ──
|
|
43
|
+
export const ENFORCEMENT_LEVELS = ["mandatory","recommended","informational"] as const;
|
|
44
|
+
export const DECISION_TYPES = ["architecture","tooling","process","security","data","infrastructure","product","operational"] as const;
|
|
45
|
+
export const REFERENCE_TYPES = ["documentation","api_spec","diagram","article","url"] as const;
|
|
46
|
+
export const MODEL_PREFERENCES = ["opus","sonnet","haiku","gpt-4o","gpt-4o-mini","o3","llama-3.2","qwen-3","gemini-2.5"] as const;
|
|
47
|
+
|
|
48
|
+
// ── Audit ──
|
|
49
|
+
export const AUDIT_EVENT_TYPES = ["auth","entity_crud","search","admin","api_key","config","oauth","export"] as const;
|
|
50
|
+
export const AUDIT_ACTIONS = ["create","read","update","delete","login","logout","grant","revoke","export","import"] as const;
|
|
51
|
+
export const AUDIT_RESOURCE_TYPES = ["entity","observation","relation","api_key","session","oauth_client","user","embedding_model"] as const;
|
|
52
|
+
|
|
53
|
+
// ── Lookup sets (for O(1) validation in hooks) ──
|
|
54
|
+
export const ENTITY_TYPE_SET = new Set<string>(ENTITY_TYPES);
|
|
55
|
+
export const EFFORT_LEVEL_SET = new Set<string>(EFFORT_LEVELS);
|
|
56
|
+
export const INTENT_TYPE_SET = new Set<string>(INTENT_TYPES);
|
|
57
|
+
export const MEMORY_TIER_SET = new Set<string>(MEMORY_TIERS);
|
|
58
|
+
export const WORK_STATUS_SET = new Set<string>(WORK_STATUSES);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Per-tool required field validation rules.
|
|
62
|
+
* Maps tool name -> array of { field, validValues? } checks.
|
|
63
|
+
* If validValues is provided, the field must be one of those values.
|
|
64
|
+
* If validValues is omitted, the field just needs to be present and non-empty.
|
|
65
|
+
*/
|
|
66
|
+
export const TOOL_INPUT_RULES: Record<string, Array<{ field: string; validValues?: readonly string[] }>> = {
|
|
67
|
+
create_entity: [
|
|
68
|
+
{ field: 'name' },
|
|
69
|
+
{ field: 'entity_type', validValues: ENTITY_TYPES },
|
|
70
|
+
],
|
|
71
|
+
update_entity: [
|
|
72
|
+
{ field: 'entity_id' },
|
|
73
|
+
],
|
|
74
|
+
add_observation: [
|
|
75
|
+
{ field: 'entity_id' },
|
|
76
|
+
{ field: 'content' },
|
|
77
|
+
],
|
|
78
|
+
search_semantic: [
|
|
79
|
+
{ field: 'query' },
|
|
80
|
+
],
|
|
81
|
+
search_entities: [], // all fields optional
|
|
82
|
+
create_relation: [
|
|
83
|
+
{ field: 'source_entity_id' },
|
|
84
|
+
{ field: 'target_entity_id' },
|
|
85
|
+
{ field: 'relation_type' },
|
|
86
|
+
],
|
|
87
|
+
mark_entity_inactive: [
|
|
88
|
+
{ field: 'entity_id' },
|
|
89
|
+
],
|
|
90
|
+
mark_observation_inactive: [
|
|
91
|
+
{ field: 'observation_id' },
|
|
92
|
+
],
|
|
93
|
+
reactivate_entity: [
|
|
94
|
+
{ field: 'entity_id' },
|
|
95
|
+
],
|
|
96
|
+
get_entities: [
|
|
97
|
+
{ field: 'entity_ids' },
|
|
98
|
+
],
|
|
99
|
+
list_entities: [], // all fields optional
|
|
100
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* gramatr-input-validator.hook.ts — PreToolUse Schema Validation
|
|
4
|
+
*
|
|
5
|
+
* Validates MCP tool inputs BEFORE the call reaches the server.
|
|
6
|
+
* Blocks invalid inputs (wrong entity_type, missing required fields)
|
|
7
|
+
* to save a wasted round trip and tokens.
|
|
8
|
+
*
|
|
9
|
+
* TRIGGER: PreToolUse (matcher: mcp__.*gramatr.*__)
|
|
10
|
+
* PERFORMANCE: <2ms — Set lookups only, no network calls.
|
|
11
|
+
* SAFETY: On any error, allows the call through (fail-open).
|
|
12
|
+
*
|
|
13
|
+
* Issue #459 — Strong typed MCP tool I/O shapes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { TOOL_INPUT_RULES, ENTITY_TYPE_SET } from './generated/schema-constants.ts';
|
|
17
|
+
|
|
18
|
+
// ── Types ──
|
|
19
|
+
|
|
20
|
+
interface HookInput {
|
|
21
|
+
tool_name: string;
|
|
22
|
+
tool_input: Record<string, unknown>;
|
|
23
|
+
session_id?: string;
|
|
24
|
+
hook_event_name?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HookOutput {
|
|
28
|
+
decision: 'allow' | 'block';
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Stdin Reader ──
|
|
33
|
+
|
|
34
|
+
function readStdin(timeoutMs: number): Promise<string> {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let data = '';
|
|
37
|
+
const timer = setTimeout(() => resolve(data), timeoutMs);
|
|
38
|
+
process.stdin.setEncoding('utf-8');
|
|
39
|
+
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
40
|
+
process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
|
|
41
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(data); });
|
|
42
|
+
process.stdin.resume();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Validation ──
|
|
47
|
+
|
|
48
|
+
function extractToolShortName(fullName: string): string {
|
|
49
|
+
// mcp__gramatr__create_entity -> create_entity
|
|
50
|
+
const parts = fullName.split('__');
|
|
51
|
+
return parts.length >= 3 ? parts.slice(2).join('__') : fullName;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validate(toolName: string, input: Record<string, unknown>): HookOutput {
|
|
55
|
+
const shortName = extractToolShortName(toolName);
|
|
56
|
+
const rules = TOOL_INPUT_RULES[shortName];
|
|
57
|
+
|
|
58
|
+
// No rules for this tool — allow
|
|
59
|
+
if (!rules || rules.length === 0) {
|
|
60
|
+
return { decision: 'allow' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const rule of rules) {
|
|
64
|
+
const value = input[rule.field];
|
|
65
|
+
|
|
66
|
+
// Check presence
|
|
67
|
+
if (value === undefined || value === null || value === '') {
|
|
68
|
+
return {
|
|
69
|
+
decision: 'block',
|
|
70
|
+
reason: `Missing required field "${rule.field}" for ${shortName}. Provide a non-empty value.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check enum values
|
|
75
|
+
if (rule.validValues && typeof value === 'string') {
|
|
76
|
+
const validSet = rule.field === 'entity_type' ? ENTITY_TYPE_SET : new Set(rule.validValues);
|
|
77
|
+
if (!validSet.has(value)) {
|
|
78
|
+
return {
|
|
79
|
+
decision: 'block',
|
|
80
|
+
reason: `Invalid ${rule.field}="${value}" for ${shortName}. Valid values: ${rule.validValues.join(', ')}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Additional: if any tool passes entity_type, validate it even if not in rules
|
|
87
|
+
if ('entity_type' in input && typeof input.entity_type === 'string') {
|
|
88
|
+
if (!ENTITY_TYPE_SET.has(input.entity_type)) {
|
|
89
|
+
return {
|
|
90
|
+
decision: 'block',
|
|
91
|
+
reason: `Invalid entity_type="${input.entity_type}". Valid types: ${[...ENTITY_TYPE_SET].slice(0, 10).join(', ')}... (33 total). Call gramatr_list_entity_types for the full list.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { decision: 'allow' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Main ──
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const raw = await readStdin(2000);
|
|
103
|
+
|
|
104
|
+
// Fail-open on any parse error
|
|
105
|
+
if (!raw.trim()) {
|
|
106
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let input: HookInput;
|
|
111
|
+
try {
|
|
112
|
+
input = JSON.parse(raw);
|
|
113
|
+
} catch {
|
|
114
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { tool_name, tool_input } = input;
|
|
119
|
+
if (!tool_name || !tool_input) {
|
|
120
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = validate(tool_name, tool_input);
|
|
126
|
+
console.log(JSON.stringify(result));
|
|
127
|
+
} catch {
|
|
128
|
+
// Fail-open on any validation error
|
|
129
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gramatr/client",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"build-mcpb": "npx tsx desktop/build-mcpb.ts",
|
|
44
44
|
"install-web": "echo 'No local install needed. See packages/client/web/README.md for setup instructions.'",
|
|
45
45
|
"migrate-clean-install": "npx tsx bin/clean-legacy-install.ts --apply",
|
|
46
|
+
"generate-schemas": "npx tsx scripts/generate-schema-constants.ts",
|
|
46
47
|
"gramatr": "npx tsx bin/gramatr.ts",
|
|
47
48
|
"lint": "pnpm exec biome lint --files-ignore-unknown=true package.json bin chatgpt codex core desktop gemini hooks lib tools web vitest.config.ts",
|
|
48
49
|
"test": "vitest run",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Build-time schema constant generator.
|
|
4
|
+
*
|
|
5
|
+
* Reads canonical Zod enums from @gramatr/core and writes plain TypeScript
|
|
6
|
+
* constants into hooks/generated/schema-constants.ts. This keeps the client
|
|
7
|
+
* hooks self-contained (no runtime dependency on @gramatr/core) while
|
|
8
|
+
* maintaining a single source of truth for enum values.
|
|
9
|
+
*
|
|
10
|
+
* Run: npx tsx scripts/generate-schema-constants.ts
|
|
11
|
+
* Or: pnpm generate-schemas
|
|
12
|
+
*
|
|
13
|
+
* Issue #459 — Strong typed MCP tool I/O shapes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
// Import canonical schemas from @gramatr/core
|
|
21
|
+
import {
|
|
22
|
+
EntityTypeEnum,
|
|
23
|
+
EffortLevelEnum,
|
|
24
|
+
IntentTypeEnum,
|
|
25
|
+
MemoryTierEnum,
|
|
26
|
+
WorkStatusEnum,
|
|
27
|
+
ProjectStatusEnum,
|
|
28
|
+
SessionStatusEnum,
|
|
29
|
+
DecisionStatusEnum,
|
|
30
|
+
PrdStatusEnum,
|
|
31
|
+
OutcomeEnum,
|
|
32
|
+
DomainEnum,
|
|
33
|
+
AlgorithmPhaseEnum,
|
|
34
|
+
AgentTypeEnum,
|
|
35
|
+
PatternTypeEnum,
|
|
36
|
+
DiaryScopeEnum,
|
|
37
|
+
InfraTypeEnum,
|
|
38
|
+
ExternalServiceEnum,
|
|
39
|
+
AuthTypeEnum,
|
|
40
|
+
ConnectionStatusEnum,
|
|
41
|
+
EnforcementEnum,
|
|
42
|
+
DecisionTypeEnum,
|
|
43
|
+
ReferenceTypeEnum,
|
|
44
|
+
AuditEventTypeEnum,
|
|
45
|
+
AuditActionEnum,
|
|
46
|
+
AuditResourceTypeEnum,
|
|
47
|
+
EmbeddingStatusEnum,
|
|
48
|
+
ModelPreferenceEnum,
|
|
49
|
+
} from '@gramatr/core';
|
|
50
|
+
|
|
51
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
const outDir = join(__dirname, '..', 'hooks', 'generated');
|
|
53
|
+
const outFile = join(outDir, 'schema-constants.ts');
|
|
54
|
+
|
|
55
|
+
// Extract .options from each Zod enum
|
|
56
|
+
function vals(zodEnum: { options: readonly string[] }): string {
|
|
57
|
+
return JSON.stringify([...zodEnum.options]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = `/**
|
|
61
|
+
* AUTO-GENERATED — do not edit manually.
|
|
62
|
+
* Source: @gramatr/core/contracts/mcp-schemas.ts
|
|
63
|
+
* Generator: scripts/generate-schema-constants.ts
|
|
64
|
+
* Generated: ${new Date().toISOString()}
|
|
65
|
+
*
|
|
66
|
+
* These constants are extracted from canonical Zod schemas at build time
|
|
67
|
+
* so client hooks can validate MCP tool inputs without depending on
|
|
68
|
+
* @gramatr/core at runtime.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
// ── Classifier enums ──
|
|
72
|
+
export const EFFORT_LEVELS = ${vals(EffortLevelEnum)} as const;
|
|
73
|
+
export const INTENT_TYPES = ${vals(IntentTypeEnum)} as const;
|
|
74
|
+
export const MEMORY_TIERS = ${vals(MemoryTierEnum)} as const;
|
|
75
|
+
|
|
76
|
+
// ── Entity type enum (33-type taxonomy) ──
|
|
77
|
+
export const ENTITY_TYPES = ${vals(EntityTypeEnum)} as const;
|
|
78
|
+
|
|
79
|
+
// ── Lifecycle enums ──
|
|
80
|
+
export const WORK_STATUSES = ${vals(WorkStatusEnum)} as const;
|
|
81
|
+
export const PROJECT_STATUSES = ${vals(ProjectStatusEnum)} as const;
|
|
82
|
+
export const SESSION_STATUSES = ${vals(SessionStatusEnum)} as const;
|
|
83
|
+
export const DECISION_STATUSES = ${vals(DecisionStatusEnum)} as const;
|
|
84
|
+
export const PRD_STATUSES = ${vals(PrdStatusEnum)} as const;
|
|
85
|
+
export const OUTCOMES = ${vals(OutcomeEnum)} as const;
|
|
86
|
+
export const EMBEDDING_STATUSES = ${vals(EmbeddingStatusEnum)} as const;
|
|
87
|
+
|
|
88
|
+
// ── Domain & classification ──
|
|
89
|
+
export const DOMAINS = ${vals(DomainEnum)} as const;
|
|
90
|
+
export const ALGORITHM_PHASES = ${vals(AlgorithmPhaseEnum)} as const;
|
|
91
|
+
export const AGENT_TYPES = ${vals(AgentTypeEnum)} as const;
|
|
92
|
+
export const PATTERN_TYPES = ${vals(PatternTypeEnum)} as const;
|
|
93
|
+
export const DIARY_SCOPES = ${vals(DiaryScopeEnum)} as const;
|
|
94
|
+
|
|
95
|
+
// ── Infrastructure & connection ──
|
|
96
|
+
export const INFRA_TYPES = ${vals(InfraTypeEnum)} as const;
|
|
97
|
+
export const EXTERNAL_SERVICES = ${vals(ExternalServiceEnum)} as const;
|
|
98
|
+
export const AUTH_TYPES = ${vals(AuthTypeEnum)} as const;
|
|
99
|
+
export const CONNECTION_STATUSES = ${vals(ConnectionStatusEnum)} as const;
|
|
100
|
+
|
|
101
|
+
// ── Knowledge & content ──
|
|
102
|
+
export const ENFORCEMENT_LEVELS = ${vals(EnforcementEnum)} as const;
|
|
103
|
+
export const DECISION_TYPES = ${vals(DecisionTypeEnum)} as const;
|
|
104
|
+
export const REFERENCE_TYPES = ${vals(ReferenceTypeEnum)} as const;
|
|
105
|
+
export const MODEL_PREFERENCES = ${vals(ModelPreferenceEnum)} as const;
|
|
106
|
+
|
|
107
|
+
// ── Audit ──
|
|
108
|
+
export const AUDIT_EVENT_TYPES = ${vals(AuditEventTypeEnum)} as const;
|
|
109
|
+
export const AUDIT_ACTIONS = ${vals(AuditActionEnum)} as const;
|
|
110
|
+
export const AUDIT_RESOURCE_TYPES = ${vals(AuditResourceTypeEnum)} as const;
|
|
111
|
+
|
|
112
|
+
// ── Lookup sets (for O(1) validation in hooks) ──
|
|
113
|
+
export const ENTITY_TYPE_SET = new Set<string>(ENTITY_TYPES);
|
|
114
|
+
export const EFFORT_LEVEL_SET = new Set<string>(EFFORT_LEVELS);
|
|
115
|
+
export const INTENT_TYPE_SET = new Set<string>(INTENT_TYPES);
|
|
116
|
+
export const MEMORY_TIER_SET = new Set<string>(MEMORY_TIERS);
|
|
117
|
+
export const WORK_STATUS_SET = new Set<string>(WORK_STATUSES);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Per-tool required field validation rules.
|
|
121
|
+
* Maps tool name -> array of { field, validValues? } checks.
|
|
122
|
+
* If validValues is provided, the field must be one of those values.
|
|
123
|
+
* If validValues is omitted, the field just needs to be present and non-empty.
|
|
124
|
+
*/
|
|
125
|
+
export const TOOL_INPUT_RULES: Record<string, Array<{ field: string; validValues?: readonly string[] }>> = {
|
|
126
|
+
create_entity: [
|
|
127
|
+
{ field: 'name' },
|
|
128
|
+
{ field: 'entity_type', validValues: ENTITY_TYPES },
|
|
129
|
+
],
|
|
130
|
+
update_entity: [
|
|
131
|
+
{ field: 'entity_id' },
|
|
132
|
+
],
|
|
133
|
+
add_observation: [
|
|
134
|
+
{ field: 'entity_id' },
|
|
135
|
+
{ field: 'content' },
|
|
136
|
+
],
|
|
137
|
+
search_semantic: [
|
|
138
|
+
{ field: 'query' },
|
|
139
|
+
],
|
|
140
|
+
search_entities: [], // all fields optional
|
|
141
|
+
create_relation: [
|
|
142
|
+
{ field: 'source_entity_id' },
|
|
143
|
+
{ field: 'target_entity_id' },
|
|
144
|
+
{ field: 'relation_type' },
|
|
145
|
+
],
|
|
146
|
+
mark_entity_inactive: [
|
|
147
|
+
{ field: 'entity_id' },
|
|
148
|
+
],
|
|
149
|
+
mark_observation_inactive: [
|
|
150
|
+
{ field: 'observation_id' },
|
|
151
|
+
],
|
|
152
|
+
reactivate_entity: [
|
|
153
|
+
{ field: 'entity_id' },
|
|
154
|
+
],
|
|
155
|
+
get_entities: [
|
|
156
|
+
{ field: 'entity_ids' },
|
|
157
|
+
],
|
|
158
|
+
list_entities: [], // all fields optional
|
|
159
|
+
};
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
mkdirSync(outDir, { recursive: true });
|
|
163
|
+
writeFileSync(outFile, content, 'utf-8');
|
|
164
|
+
console.log(`Generated ${outFile} (${content.length} bytes)`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM ubuntu:24.04
|
|
2
|
+
|
|
3
|
+
RUN apt-get update \
|
|
4
|
+
&& apt-get install -y --no-install-recommends \
|
|
5
|
+
curl ca-certificates git \
|
|
6
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
+
|
|
8
|
+
# Install Node.js 20.x
|
|
9
|
+
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
10
|
+
&& apt-get install -y --no-install-recommends nodejs \
|
|
11
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
12
|
+
|
|
13
|
+
# Create unprivileged test user (mirrors fresh Linux install)
|
|
14
|
+
RUN useradd -m -s /bin/bash testuser
|
|
15
|
+
|
|
16
|
+
USER testuser
|
|
17
|
+
WORKDIR /home/testuser
|
|
18
|
+
|
|
19
|
+
# Pre-create .claude so the installer doesn't fail on prerequisite check
|
|
20
|
+
RUN mkdir -p /home/testuser/.claude
|
|
21
|
+
|
|
22
|
+
ENV HOME=/home/testuser
|
|
23
|
+
ENV CI=1
|
|
24
|
+
|
|
25
|
+
ENTRYPOINT ["/bin/bash"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
FROM ubuntu:24.04
|
|
2
|
+
|
|
3
|
+
RUN apt-get update \
|
|
4
|
+
&& apt-get install -y --no-install-recommends \
|
|
5
|
+
curl ca-certificates git \
|
|
6
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
+
|
|
8
|
+
# Install Node.js 20.x
|
|
9
|
+
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
10
|
+
&& apt-get install -y --no-install-recommends nodejs \
|
|
11
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
12
|
+
|
|
13
|
+
# Create unprivileged test user
|
|
14
|
+
RUN useradd -m -s /bin/bash testuser
|
|
15
|
+
|
|
16
|
+
USER testuser
|
|
17
|
+
WORKDIR /home/testuser
|
|
18
|
+
|
|
19
|
+
# Pre-create .claude so the installer doesn't fail on prerequisite check
|
|
20
|
+
RUN mkdir -p /home/testuser/.claude
|
|
21
|
+
|
|
22
|
+
ENV HOME=/home/testuser
|
|
23
|
+
ENV CI=1
|
|
24
|
+
|
|
25
|
+
# WSL simulation: these env vars trigger WSL-specific paths in installer
|
|
26
|
+
ENV WSL_DISTRO_NAME=Ubuntu-24.04
|
|
27
|
+
ENV WSLENV=1
|
|
28
|
+
|
|
29
|
+
# Ensure no display (headless WSL scenario — no X11/Wayland)
|
|
30
|
+
# DISPLAY and WAYLAND_DISPLAY are intentionally unset
|
|
31
|
+
|
|
32
|
+
ENTRYPOINT ["/bin/bash"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mock auth server for install-matrix CI tests.
|
|
4
|
+
*
|
|
5
|
+
* Simulates the gramatr API server endpoints used by the installer and
|
|
6
|
+
* gmtr-login device flow. Uses only Node built-ins (node:http) so it can
|
|
7
|
+
* run inside Docker containers without tsx or any npm dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Binds to port 18930 (or MOCK_PORT env).
|
|
10
|
+
*
|
|
11
|
+
* Endpoints:
|
|
12
|
+
* GET /health -> server health check
|
|
13
|
+
* POST /device/start -> device authorization initiation
|
|
14
|
+
* POST /device/token -> device token polling (pending -> success)
|
|
15
|
+
* POST /mcp -> minimal JSON-RPC success (validates token)
|
|
16
|
+
* GET /verify -> mock verification page
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createServer } from 'node:http';
|
|
20
|
+
|
|
21
|
+
const PORT = parseInt(process.env.MOCK_PORT || '18930', 10);
|
|
22
|
+
|
|
23
|
+
// In-memory state for device flow polling.
|
|
24
|
+
// First poll returns authorization_pending, second returns the token.
|
|
25
|
+
let pollCount = 0;
|
|
26
|
+
|
|
27
|
+
const MOCK_TOKEN = 'mock-access-token-for-ci';
|
|
28
|
+
|
|
29
|
+
function json(res, status, body) {
|
|
30
|
+
const payload = JSON.stringify(body);
|
|
31
|
+
res.writeHead(status, {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
34
|
+
});
|
|
35
|
+
res.end(payload);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function html(res, status, body) {
|
|
39
|
+
res.writeHead(status, { 'Content-Type': 'text/html' });
|
|
40
|
+
res.end(body);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readBody(req) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
req.on('data', (c) => chunks.push(c));
|
|
47
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const server = createServer(async (req, res) => {
|
|
52
|
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
53
|
+
const method = req.method || 'GET';
|
|
54
|
+
|
|
55
|
+
// Log every request for CI debugging
|
|
56
|
+
process.stderr.write(`[mock-auth] ${method} ${url.pathname}\n`);
|
|
57
|
+
|
|
58
|
+
// ── GET /health ──
|
|
59
|
+
if (method === 'GET' && url.pathname === '/health') {
|
|
60
|
+
json(res, 200, { status: 'ok', version: '0.0.0-mock' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── POST /device/start ──
|
|
65
|
+
if (method === 'POST' && url.pathname === '/device/start') {
|
|
66
|
+
// Reset poll counter for each new device flow
|
|
67
|
+
pollCount = 0;
|
|
68
|
+
json(res, 200, {
|
|
69
|
+
device_code: 'mock-device-code-ci',
|
|
70
|
+
user_code: 'MOCK-1234',
|
|
71
|
+
verification_uri: `http://localhost:${PORT}/verify`,
|
|
72
|
+
verification_uri_complete: `http://localhost:${PORT}/verify?code=MOCK-1234`,
|
|
73
|
+
expires_in: 300,
|
|
74
|
+
interval: 1,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── POST /device/token ──
|
|
80
|
+
if (method === 'POST' && url.pathname === '/device/token') {
|
|
81
|
+
pollCount++;
|
|
82
|
+
if (pollCount < 2) {
|
|
83
|
+
// First poll: still pending
|
|
84
|
+
json(res, 428, {
|
|
85
|
+
error: 'authorization_pending',
|
|
86
|
+
error_description: 'The user has not yet authorized the device.',
|
|
87
|
+
interval: 1,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
// Second poll: success
|
|
91
|
+
json(res, 200, {
|
|
92
|
+
access_token: MOCK_TOKEN,
|
|
93
|
+
token_type: 'bearer',
|
|
94
|
+
expires_in: 31536000,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── POST /mcp ──
|
|
101
|
+
// Minimal JSON-RPC response so testToken() in gmtr-login.ts considers the
|
|
102
|
+
// token valid. The client sends a tools/call request and parses SSE lines.
|
|
103
|
+
if (method === 'POST' && url.pathname === '/mcp') {
|
|
104
|
+
const body = await readBody(req);
|
|
105
|
+
let id = 1;
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(body);
|
|
108
|
+
id = parsed.id || 1;
|
|
109
|
+
} catch { /* ignore */ }
|
|
110
|
+
|
|
111
|
+
// Check authorization header
|
|
112
|
+
const auth = req.headers.authorization || '';
|
|
113
|
+
if (!auth.includes(MOCK_TOKEN)) {
|
|
114
|
+
// Reject invalid tokens
|
|
115
|
+
const ssePayload = `data: ${JSON.stringify({
|
|
116
|
+
jsonrpc: '2.0',
|
|
117
|
+
id,
|
|
118
|
+
error: { code: -32000, message: 'JWT token is required' },
|
|
119
|
+
})}\n\n`;
|
|
120
|
+
res.writeHead(200, {
|
|
121
|
+
'Content-Type': 'text/event-stream',
|
|
122
|
+
'Cache-Control': 'no-cache',
|
|
123
|
+
});
|
|
124
|
+
res.end(ssePayload);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Valid token: return a successful aggregate_stats response
|
|
129
|
+
const ssePayload = `data: ${JSON.stringify({
|
|
130
|
+
jsonrpc: '2.0',
|
|
131
|
+
id,
|
|
132
|
+
result: {
|
|
133
|
+
content: [{ type: 'text', text: JSON.stringify({ entities: 0, observations: 0, relations: 0 }) }],
|
|
134
|
+
},
|
|
135
|
+
})}\n\n`;
|
|
136
|
+
res.writeHead(200, {
|
|
137
|
+
'Content-Type': 'text/event-stream',
|
|
138
|
+
'Cache-Control': 'no-cache',
|
|
139
|
+
});
|
|
140
|
+
res.end(ssePayload);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── GET /verify ──
|
|
145
|
+
if (method === 'GET' && url.pathname === '/verify') {
|
|
146
|
+
html(res, 200, `<!DOCTYPE html>
|
|
147
|
+
<html><head><title>Mock Verification</title></head>
|
|
148
|
+
<body><h1>Mock verification complete</h1>
|
|
149
|
+
<p>This is the CI mock auth server. Device code approved automatically.</p>
|
|
150
|
+
</body></html>`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Catch-all ──
|
|
155
|
+
json(res, 404, { error: 'not_found', path: url.pathname });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
159
|
+
process.stderr.write(`[mock-auth] Listening on http://127.0.0.1:${PORT}\n`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Graceful shutdown
|
|
163
|
+
process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
|
164
|
+
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# run-install-test.sh — Fresh-profile install harness for @gramatr/client
|
|
6
|
+
#
|
|
7
|
+
# Validates that `npx @gramatr/client install claude-code` works correctly
|
|
8
|
+
# on a clean machine with no prior gramatr installation.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# ./run-install-test.sh # auto-find tarball
|
|
12
|
+
# ./run-install-test.sh /path/to/gramatr.tgz # explicit tarball
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 — all phases passed
|
|
16
|
+
# 1 — one or more phases failed
|
|
17
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
PASS_COUNT=0
|
|
20
|
+
FAIL_COUNT=0
|
|
21
|
+
MOCK_PID=""
|
|
22
|
+
|
|
23
|
+
cleanup() {
|
|
24
|
+
if [ -n "$MOCK_PID" ] && kill -0 "$MOCK_PID" 2>/dev/null; then
|
|
25
|
+
kill "$MOCK_PID" 2>/dev/null || true
|
|
26
|
+
wait "$MOCK_PID" 2>/dev/null || true
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
trap cleanup EXIT
|
|
30
|
+
|
|
31
|
+
# ── Helpers ──
|
|
32
|
+
|
|
33
|
+
pass() {
|
|
34
|
+
echo " PASS: $1"
|
|
35
|
+
PASS_COUNT=$((PASS_COUNT + 1))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fail() {
|
|
39
|
+
echo " FAIL: $1"
|
|
40
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assert_file_exists() {
|
|
44
|
+
local path="$1"
|
|
45
|
+
local label="${2:-$1}"
|
|
46
|
+
if [ -f "$path" ]; then
|
|
47
|
+
pass "$label exists"
|
|
48
|
+
else
|
|
49
|
+
fail "$label missing ($path)"
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
assert_json_field() {
|
|
54
|
+
local file="$1"
|
|
55
|
+
local query="$2"
|
|
56
|
+
local label="$3"
|
|
57
|
+
local val
|
|
58
|
+
val=$(node -e "
|
|
59
|
+
const fs = require('fs');
|
|
60
|
+
const data = JSON.parse(fs.readFileSync('$file', 'utf8'));
|
|
61
|
+
const val = $query;
|
|
62
|
+
if (val !== undefined && val !== null) {
|
|
63
|
+
if (typeof val === 'object') process.stdout.write(JSON.stringify(val));
|
|
64
|
+
else process.stdout.write(String(val));
|
|
65
|
+
}
|
|
66
|
+
" 2>/dev/null || true)
|
|
67
|
+
if [ -n "$val" ]; then
|
|
68
|
+
pass "$label (= $val)"
|
|
69
|
+
else
|
|
70
|
+
fail "$label — field missing or null in $file"
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
assert_no_substring() {
|
|
75
|
+
local file="$1"
|
|
76
|
+
local needle="$2"
|
|
77
|
+
local label="$3"
|
|
78
|
+
if grep -q "$needle" "$file" 2>/dev/null; then
|
|
79
|
+
fail "$label — found '$needle' in $file"
|
|
80
|
+
else
|
|
81
|
+
pass "$label"
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ── Locate tarball ──
|
|
86
|
+
|
|
87
|
+
TARBALL="${1:-}"
|
|
88
|
+
|
|
89
|
+
if [ -z "$TARBALL" ]; then
|
|
90
|
+
for candidate in /artifact/*.tgz ./artifact/*.tgz; do
|
|
91
|
+
if [ -f "$candidate" ]; then
|
|
92
|
+
TARBALL="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")"
|
|
93
|
+
break
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
|
|
99
|
+
echo "ERROR: No tarball found. Pass path as \$1 or place in /artifact/ or ./artifact/"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# ── Install tarball into temp dir to get the bin ──
|
|
104
|
+
# npx cannot run a local tarball directly — it tries to execute the .tgz file.
|
|
105
|
+
# Instead: npm install the tarball, then call the bin via node_modules/.bin/.
|
|
106
|
+
|
|
107
|
+
INSTALL_DIR=$(mktemp -d)
|
|
108
|
+
cd "$INSTALL_DIR"
|
|
109
|
+
npm init -y > /dev/null 2>&1
|
|
110
|
+
npm install "$TARBALL" --no-fund --no-audit > /dev/null 2>&1 || {
|
|
111
|
+
echo "ERROR: npm install of tarball failed"
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
GRAMATR_BIN="$INSTALL_DIR/node_modules/.bin/gramatr"
|
|
115
|
+
if [ ! -f "$GRAMATR_BIN" ]; then
|
|
116
|
+
echo "ERROR: gramatr bin not found after npm install"
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo "========================================"
|
|
121
|
+
echo " gramatr install-matrix test harness"
|
|
122
|
+
echo "========================================"
|
|
123
|
+
echo ""
|
|
124
|
+
echo " Tarball: $TARBALL"
|
|
125
|
+
echo " Bin: $GRAMATR_BIN"
|
|
126
|
+
echo " HOME: $HOME"
|
|
127
|
+
echo " Node: $(node --version)"
|
|
128
|
+
echo " Platform: $(uname -s) $(uname -m)"
|
|
129
|
+
echo ""
|
|
130
|
+
|
|
131
|
+
# ── Phase 0: Start mock auth server ──
|
|
132
|
+
|
|
133
|
+
echo "--- Phase 0: Mock auth server ---"
|
|
134
|
+
|
|
135
|
+
MOCK_SERVER_SCRIPT=""
|
|
136
|
+
# Look for mock-auth-server.mjs in known locations
|
|
137
|
+
for candidate in \
|
|
138
|
+
/test/mock-auth-server.mjs \
|
|
139
|
+
"$(dirname "$0")/mock-auth-server.mjs" \
|
|
140
|
+
./packages/client/test/install-matrix/mock-auth-server.mjs; do
|
|
141
|
+
if [ -f "$candidate" ]; then
|
|
142
|
+
MOCK_SERVER_SCRIPT="$candidate"
|
|
143
|
+
break
|
|
144
|
+
fi
|
|
145
|
+
done
|
|
146
|
+
|
|
147
|
+
if [ -z "$MOCK_SERVER_SCRIPT" ]; then
|
|
148
|
+
echo " WARNING: mock-auth-server.mjs not found — skipping auth phases"
|
|
149
|
+
else
|
|
150
|
+
# Check if already running
|
|
151
|
+
if curl -sf http://127.0.0.1:18930/health >/dev/null 2>&1; then
|
|
152
|
+
pass "Mock auth server already running"
|
|
153
|
+
else
|
|
154
|
+
node "$MOCK_SERVER_SCRIPT" &
|
|
155
|
+
MOCK_PID=$!
|
|
156
|
+
# Wait for server to be ready (up to 5 seconds)
|
|
157
|
+
for i in $(seq 1 50); do
|
|
158
|
+
if curl -sf http://127.0.0.1:18930/health >/dev/null 2>&1; then
|
|
159
|
+
break
|
|
160
|
+
fi
|
|
161
|
+
sleep 0.1
|
|
162
|
+
done
|
|
163
|
+
if curl -sf http://127.0.0.1:18930/health >/dev/null 2>&1; then
|
|
164
|
+
pass "Mock auth server started (PID $MOCK_PID)"
|
|
165
|
+
else
|
|
166
|
+
fail "Mock auth server failed to start"
|
|
167
|
+
exit 1
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
echo ""
|
|
173
|
+
|
|
174
|
+
# ── Phase 1: Fresh install ──
|
|
175
|
+
|
|
176
|
+
echo "--- Phase 1: Fresh install ---"
|
|
177
|
+
|
|
178
|
+
# Set env so installer uses mock server and runs non-interactively
|
|
179
|
+
export GRAMATR_URL="http://127.0.0.1:18930/mcp"
|
|
180
|
+
export GRAMATR_API_KEY="mock-access-token-for-ci"
|
|
181
|
+
export CI=1
|
|
182
|
+
|
|
183
|
+
# Run the installer via the bin entry installed from the tarball
|
|
184
|
+
"$GRAMATR_BIN" install claude-code --yes --name TestUser --timezone UTC 2>&1 || {
|
|
185
|
+
fail "Installer exited with non-zero status"
|
|
186
|
+
exit 1
|
|
187
|
+
}
|
|
188
|
+
pass "Installer completed successfully"
|
|
189
|
+
|
|
190
|
+
GMTR_DIR="$HOME/.gramatr"
|
|
191
|
+
|
|
192
|
+
# Verify critical files
|
|
193
|
+
assert_file_exists "$GMTR_DIR/hooks/gramatr-prompt-enricher.hook.ts" "hooks/gramatr-prompt-enricher.hook.ts"
|
|
194
|
+
assert_file_exists "$GMTR_DIR/hooks/gramatr-tool-tracker.hook.ts" "hooks/gramatr-tool-tracker.hook.ts"
|
|
195
|
+
assert_file_exists "$GMTR_DIR/hooks/gramatr-security-validator.hook.ts" "hooks/gramatr-security-validator.hook.ts"
|
|
196
|
+
assert_file_exists "$GMTR_DIR/hooks/gramatr-rating-capture.hook.ts" "hooks/gramatr-rating-capture.hook.ts"
|
|
197
|
+
assert_file_exists "$GMTR_DIR/core/routing.ts" "core/routing.ts"
|
|
198
|
+
assert_file_exists "$GMTR_DIR/core/types.ts" "core/types.ts"
|
|
199
|
+
assert_file_exists "$GMTR_DIR/core/session.ts" "core/session.ts"
|
|
200
|
+
assert_file_exists "$GMTR_DIR/core/version.ts" "core/version.ts"
|
|
201
|
+
assert_file_exists "$GMTR_DIR/core/install.ts" "core/install.ts"
|
|
202
|
+
assert_file_exists "$GMTR_DIR/bin/statusline.ts" "bin/statusline.ts"
|
|
203
|
+
assert_file_exists "$GMTR_DIR/bin/gmtr-login.ts" "bin/gmtr-login.ts"
|
|
204
|
+
assert_file_exists "$GMTR_DIR/CLAUDE.md" "CLAUDE.md"
|
|
205
|
+
|
|
206
|
+
echo ""
|
|
207
|
+
|
|
208
|
+
# ── Phase 2: Settings integrity ──
|
|
209
|
+
|
|
210
|
+
echo "--- Phase 2: Settings integrity ---"
|
|
211
|
+
|
|
212
|
+
SETTINGS="$HOME/.claude/settings.json"
|
|
213
|
+
CLAUDE_JSON="$HOME/.claude.json"
|
|
214
|
+
|
|
215
|
+
assert_file_exists "$SETTINGS" "settings.json"
|
|
216
|
+
assert_file_exists "$CLAUDE_JSON" ".claude.json"
|
|
217
|
+
|
|
218
|
+
# Hooks present
|
|
219
|
+
assert_json_field "$SETTINGS" "data.hooks?.PreToolUse?.length" "PreToolUse hooks registered"
|
|
220
|
+
assert_json_field "$SETTINGS" "data.hooks?.PostToolUse?.length" "PostToolUse hooks registered"
|
|
221
|
+
assert_json_field "$SETTINGS" "data.hooks?.UserPromptSubmit?.length" "UserPromptSubmit hooks registered"
|
|
222
|
+
assert_json_field "$SETTINGS" "data.hooks?.SessionStart?.length" "SessionStart hooks registered"
|
|
223
|
+
assert_json_field "$SETTINGS" "data.hooks?.SessionEnd?.length" "SessionEnd hooks registered"
|
|
224
|
+
assert_json_field "$SETTINGS" "data.hooks?.Stop?.length" "Stop hooks registered"
|
|
225
|
+
|
|
226
|
+
# MCP server registered
|
|
227
|
+
assert_json_field "$CLAUDE_JSON" "data.mcpServers?.gramatr?.url" "MCP server 'gramatr' in .claude.json"
|
|
228
|
+
|
|
229
|
+
# GRAMATR_DIR env set
|
|
230
|
+
assert_json_field "$SETTINGS" "data.env?.GRAMATR_DIR" "GRAMATR_DIR env in settings.json"
|
|
231
|
+
|
|
232
|
+
# Status line configured
|
|
233
|
+
assert_json_field "$SETTINGS" "data.statusLine?.command" "statusLine command in settings.json"
|
|
234
|
+
|
|
235
|
+
# Identity config
|
|
236
|
+
assert_file_exists "$GMTR_DIR/settings.json" "gramatr settings.json (identity)"
|
|
237
|
+
assert_json_field "$GMTR_DIR/settings.json" "data.principal?.name" "principal.name set"
|
|
238
|
+
|
|
239
|
+
echo ""
|
|
240
|
+
|
|
241
|
+
# ── Phase 3: Headless login (mock device flow) ──
|
|
242
|
+
|
|
243
|
+
echo "--- Phase 3: Headless login validation ---"
|
|
244
|
+
|
|
245
|
+
# The installer used GRAMATR_API_KEY which resolveAuthToken picks up first.
|
|
246
|
+
# Verify the token made it through to the config files.
|
|
247
|
+
GMTR_JSON="$HOME/.gramatr.json"
|
|
248
|
+
if [ -f "$GMTR_JSON" ]; then
|
|
249
|
+
pass ".gramatr.json created"
|
|
250
|
+
assert_json_field "$GMTR_JSON" "data.token" "Token stored in .gramatr.json"
|
|
251
|
+
else
|
|
252
|
+
# Token may have been set only in settings.json env — that's also valid
|
|
253
|
+
pass ".gramatr.json not needed (token via env)"
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# Verify mock server accepted the token (smoke test the /mcp endpoint)
|
|
257
|
+
if [ -n "$MOCK_SERVER_SCRIPT" ]; then
|
|
258
|
+
MCP_RESULT=$(curl -sf -X POST http://127.0.0.1:18930/mcp \
|
|
259
|
+
-H "Content-Type: application/json" \
|
|
260
|
+
-H "Authorization: Bearer mock-access-token-for-ci" \
|
|
261
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"aggregate_stats","arguments":{}}}' \
|
|
262
|
+
2>/dev/null || true)
|
|
263
|
+
if echo "$MCP_RESULT" | grep -q '"result"'; then
|
|
264
|
+
pass "Mock MCP endpoint accepted token"
|
|
265
|
+
else
|
|
266
|
+
fail "Mock MCP endpoint rejected token"
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
echo ""
|
|
271
|
+
|
|
272
|
+
# ── Phase 4: Idempotent reinstall ──
|
|
273
|
+
|
|
274
|
+
echo "--- Phase 4: Idempotent reinstall ---"
|
|
275
|
+
|
|
276
|
+
# Run install again — should not corrupt settings
|
|
277
|
+
"$GRAMATR_BIN" install claude-code --yes --name TestUser --timezone UTC 2>&1 || {
|
|
278
|
+
fail "Reinstall exited with non-zero status"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Re-verify critical invariants
|
|
282
|
+
assert_file_exists "$GMTR_DIR/hooks/gramatr-prompt-enricher.hook.ts" "hooks/gramatr-prompt-enricher.hook.ts (after reinstall)"
|
|
283
|
+
assert_json_field "$SETTINGS" "data.hooks?.PreToolUse?.length" "PreToolUse hooks still registered (after reinstall)"
|
|
284
|
+
assert_json_field "$CLAUDE_JSON" "data.mcpServers?.gramatr?.url" "MCP server still registered (after reinstall)"
|
|
285
|
+
|
|
286
|
+
# Verify settings.json is valid JSON
|
|
287
|
+
if node -e "JSON.parse(require('fs').readFileSync('$SETTINGS', 'utf8'))" 2>/dev/null; then
|
|
288
|
+
pass "settings.json is valid JSON after reinstall"
|
|
289
|
+
else
|
|
290
|
+
fail "settings.json is corrupt after reinstall"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
echo ""
|
|
294
|
+
|
|
295
|
+
# ── Phase 5: No stale paths ──
|
|
296
|
+
|
|
297
|
+
echo "--- Phase 5: No stale paths ---"
|
|
298
|
+
|
|
299
|
+
assert_no_substring "$SETTINGS" "PAI_DIR" "No PAI_DIR in settings.json"
|
|
300
|
+
assert_no_substring "$SETTINGS" "aios-v2-client" "No aios-v2-client in settings.json"
|
|
301
|
+
|
|
302
|
+
if [ -f "$CLAUDE_JSON" ]; then
|
|
303
|
+
assert_no_substring "$CLAUDE_JSON" "PAI_DIR" "No PAI_DIR in .claude.json"
|
|
304
|
+
assert_no_substring "$CLAUDE_JSON" "aios-v2-client" "No aios-v2-client in .claude.json"
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# CLAUDE.md should contain GMTR section
|
|
308
|
+
CLAUDE_MD="$HOME/.claude/CLAUDE.md"
|
|
309
|
+
if [ -f "$CLAUDE_MD" ]; then
|
|
310
|
+
if grep -q "GMTR-START" "$CLAUDE_MD"; then
|
|
311
|
+
pass "CLAUDE.md contains GMTR-START marker"
|
|
312
|
+
else
|
|
313
|
+
fail "CLAUDE.md missing GMTR-START marker"
|
|
314
|
+
fi
|
|
315
|
+
else
|
|
316
|
+
fail "CLAUDE.md not created"
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
echo ""
|
|
320
|
+
|
|
321
|
+
# ── Summary ──
|
|
322
|
+
|
|
323
|
+
echo "========================================"
|
|
324
|
+
echo " Results: $PASS_COUNT passed, $FAIL_COUNT failed"
|
|
325
|
+
echo "========================================"
|
|
326
|
+
|
|
327
|
+
if [ "$FAIL_COUNT" -gt 0 ]; then
|
|
328
|
+
echo ""
|
|
329
|
+
echo " SOME TESTS FAILED"
|
|
330
|
+
exit 1
|
|
331
|
+
else
|
|
332
|
+
echo ""
|
|
333
|
+
echo " ALL TESTS PASSED"
|
|
334
|
+
exit 0
|
|
335
|
+
fi
|