@unifiedmemory/cli 1.3.0 → 1.3.6
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/.github/workflows/test-and-publish.yml +92 -0
- package/commands/record.js +35 -13
- package/lib/mcp-proxy.js +26 -0
- package/lib/memory-instructions.js +59 -49
- package/package.json +16 -4
- package/tests/fixtures/expired-auth.json +20 -0
- package/tests/fixtures/mock-auth.json +21 -0
- package/tests/fixtures/mock-project-config.json +12 -0
- package/tests/mocks/api.mock.js +200 -0
- package/tests/mocks/token-storage.mock.js +79 -0
- package/tests/setup.js +29 -0
- package/tests/unit/config.test.js +165 -0
- package/tests/unit/jwt-utils.test.js +217 -0
- package/tests/unit/mcp-proxy.test.js +459 -0
- package/tests/unit/provider-detector.test.js +344 -0
- package/tests/unit/token-storage.test.js +138 -0
- package/vitest.config.js +37 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Test and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [18, 20, 22]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: ${{ matrix.node-version }}
|
|
23
|
+
cache: 'npm'
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: npm run test:ci
|
|
30
|
+
env:
|
|
31
|
+
CI: true
|
|
32
|
+
|
|
33
|
+
- name: Upload test results
|
|
34
|
+
uses: actions/upload-artifact@v4
|
|
35
|
+
if: always()
|
|
36
|
+
with:
|
|
37
|
+
name: test-results-node-${{ matrix.node-version }}
|
|
38
|
+
path: test-results.xml
|
|
39
|
+
|
|
40
|
+
publish:
|
|
41
|
+
needs: test
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
44
|
+
permissions:
|
|
45
|
+
contents: write
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Setup Node.js
|
|
51
|
+
uses: actions/setup-node@v4
|
|
52
|
+
with:
|
|
53
|
+
node-version: 24
|
|
54
|
+
registry-url: 'https://registry.npmjs.org'
|
|
55
|
+
cache: 'npm'
|
|
56
|
+
|
|
57
|
+
- name: Install dependencies
|
|
58
|
+
run: npm ci
|
|
59
|
+
|
|
60
|
+
- name: Check if version changed
|
|
61
|
+
id: version-check
|
|
62
|
+
run: |
|
|
63
|
+
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
64
|
+
NPM_VERSION=$(npm view @unifiedmemory/cli version 2>/dev/null || echo "0.0.0")
|
|
65
|
+
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
|
66
|
+
echo "published=$NPM_VERSION" >> $GITHUB_OUTPUT
|
|
67
|
+
if [ "$CURRENT_VERSION" != "$NPM_VERSION" ]; then
|
|
68
|
+
echo "version_changed=true" >> $GITHUB_OUTPUT
|
|
69
|
+
echo "✅ Version changed: $NPM_VERSION → $CURRENT_VERSION"
|
|
70
|
+
else
|
|
71
|
+
echo "version_changed=false" >> $GITHUB_OUTPUT
|
|
72
|
+
echo "ℹ️ Version unchanged ($CURRENT_VERSION) - skipping publish"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
- name: Publish to npm
|
|
76
|
+
if: steps.version-check.outputs.version_changed == 'true'
|
|
77
|
+
run: npm publish --access public
|
|
78
|
+
env:
|
|
79
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
80
|
+
|
|
81
|
+
- name: Create Git tag
|
|
82
|
+
if: steps.version-check.outputs.version_changed == 'true'
|
|
83
|
+
run: |
|
|
84
|
+
git config user.name "github-actions[bot]"
|
|
85
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
86
|
+
git tag v${{ steps.version-check.outputs.current }}
|
|
87
|
+
git push origin v${{ steps.version-check.outputs.current }}
|
|
88
|
+
|
|
89
|
+
- name: Skip publish notification
|
|
90
|
+
if: steps.version-check.outputs.version_changed == 'false'
|
|
91
|
+
run: |
|
|
92
|
+
echo "::notice::Skipped npm publish - version ${{ steps.version-check.outputs.current }} already published"
|
package/commands/record.js
CHANGED
|
@@ -52,6 +52,7 @@ export async function record(summary, options = {}) {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
// 5. Prepare tool arguments
|
|
55
|
+
// Note: headers (X-Org-Id, X-User-Id) are handled at HTTP level by authHeaders
|
|
55
56
|
const toolArgs = {
|
|
56
57
|
body: {
|
|
57
58
|
summary_text: summary,
|
|
@@ -59,10 +60,6 @@ export async function record(summary, options = {}) {
|
|
|
59
60
|
source: options.source || 'um-cli',
|
|
60
61
|
confidence: options.confidence ? parseFloat(options.confidence) : 0.7,
|
|
61
62
|
tags: options.tags ? options.tags.split(',') : []
|
|
62
|
-
},
|
|
63
|
-
headers: {
|
|
64
|
-
'X-Org-Id': tokenData.selectedOrg?.id || tokenData.decoded?.sub,
|
|
65
|
-
'X-User-Id': tokenData.decoded?.sub
|
|
66
63
|
}
|
|
67
64
|
};
|
|
68
65
|
|
|
@@ -89,22 +86,47 @@ export async function record(summary, options = {}) {
|
|
|
89
86
|
projectContext
|
|
90
87
|
);
|
|
91
88
|
|
|
92
|
-
// 7. Handle response
|
|
89
|
+
// 7. Handle response - validate success properly
|
|
93
90
|
if (result.content && Array.isArray(result.content)) {
|
|
94
91
|
const textContent = result.content.find(c => c.type === 'text');
|
|
95
92
|
if (textContent) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
try {
|
|
94
|
+
const response = JSON.parse(textContent.text);
|
|
95
|
+
|
|
96
|
+
// Check for error in response
|
|
97
|
+
if (result.isError || response.error || response.detail) {
|
|
98
|
+
throw new Error(response.error || response.detail || 'Note creation failed');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.green('✓ Note created successfully'));
|
|
102
|
+
console.log(chalk.gray(` Note ID: ${response.note_id || 'N/A'}`));
|
|
103
|
+
if (response.confidence) {
|
|
104
|
+
console.log(chalk.gray(` Confidence: ${response.confidence}`));
|
|
105
|
+
}
|
|
106
|
+
return response;
|
|
107
|
+
} catch (parseError) {
|
|
108
|
+
if (parseError.message.includes('Note creation failed')) {
|
|
109
|
+
throw parseError;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Failed to parse API response: ${parseError.message}`);
|
|
101
112
|
}
|
|
102
|
-
return response;
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
// Check for direct note response (some backends return this directly)
|
|
117
|
+
if (result.note_id) {
|
|
118
|
+
console.log(chalk.green('✓ Note created successfully'));
|
|
119
|
+
console.log(chalk.gray(` Note ID: ${result.note_id}`));
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for error indicators
|
|
124
|
+
if (result.error || result.detail || result.isError) {
|
|
125
|
+
throw new Error(result.error || result.detail || 'Note creation failed');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Unknown response format - fail explicitly instead of silent success
|
|
129
|
+
throw new Error('Unexpected response format from API');
|
|
108
130
|
|
|
109
131
|
} catch (error) {
|
|
110
132
|
console.error(chalk.red('✗ Failed to create note:'));
|
package/lib/mcp-proxy.js
CHANGED
|
@@ -14,6 +14,23 @@ function transformToolSchema(tool) {
|
|
|
14
14
|
// Deep clone to avoid mutations
|
|
15
15
|
const transformed = JSON.parse(JSON.stringify(tool));
|
|
16
16
|
|
|
17
|
+
// Remove headers entirely - proxy handles all headers at HTTP level
|
|
18
|
+
// AI never needs to provide X-Org-Id, X-User-Id, etc.
|
|
19
|
+
if (transformed.inputSchema?.properties?.headers) {
|
|
20
|
+
delete transformed.inputSchema.properties.headers;
|
|
21
|
+
|
|
22
|
+
// Also remove from required array if present
|
|
23
|
+
if (transformed.inputSchema.required && Array.isArray(transformed.inputSchema.required)) {
|
|
24
|
+
transformed.inputSchema.required = transformed.inputSchema.required.filter(
|
|
25
|
+
field => field !== 'headers'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (transformed.inputSchema.required.length === 0) {
|
|
29
|
+
delete transformed.inputSchema.required;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
// Check if tool has pathParams in schema
|
|
18
35
|
if (!transformed.inputSchema?.properties?.pathParams) {
|
|
19
36
|
return transformed;
|
|
@@ -79,22 +96,31 @@ function injectContextParams(args, authContext, projectContext) {
|
|
|
79
96
|
injected.pathParams = {};
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
// Ensure headers object exists (gateway expects these in tool args)
|
|
100
|
+
if (!injected.headers) {
|
|
101
|
+
injected.headers = {};
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
// Inject user ID from decoded JWT
|
|
83
105
|
if (authContext?.decoded?.sub) {
|
|
84
106
|
injected.pathParams.user = authContext.decoded.sub;
|
|
107
|
+
injected.headers['X-User-Id'] = authContext.decoded.sub;
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
// Inject org ID (from selectedOrg or fallback to user ID for personal account)
|
|
88
111
|
if (authContext?.selectedOrg?.id) {
|
|
89
112
|
injected.pathParams.org = authContext.selectedOrg.id;
|
|
113
|
+
injected.headers['X-Org-Id'] = authContext.selectedOrg.id;
|
|
90
114
|
} else if (authContext?.decoded?.sub) {
|
|
91
115
|
// Fallback to user ID for personal account context
|
|
92
116
|
injected.pathParams.org = authContext.decoded.sub;
|
|
117
|
+
injected.headers['X-Org-Id'] = authContext.decoded.sub;
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
// Inject project ID from project config
|
|
96
121
|
if (projectContext?.project_id) {
|
|
97
122
|
injected.pathParams.proj = projectContext.project_id;
|
|
123
|
+
injected.headers['X-Project-Id'] = projectContext.project_id;
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
return injected;
|
|
@@ -1,72 +1,82 @@
|
|
|
1
1
|
// Unique marker to detect if instructions already present
|
|
2
|
-
export const MEMORY_INSTRUCTIONS_MARKER = '<!--
|
|
2
|
+
export const MEMORY_INSTRUCTIONS_MARKER = '<!-- UNIFIEDMEMORY_INSTRUCTIONS_V2 -->';
|
|
3
3
|
|
|
4
4
|
export function getMemoryInstructions() {
|
|
5
5
|
return `${MEMORY_INSTRUCTIONS_MARKER}
|
|
6
6
|
|
|
7
|
-
# UnifiedMemory
|
|
7
|
+
# UnifiedMemory Integration (REQUIRED)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
This project uses UnifiedMemory for persistent knowledge management. The \`unifiedmemory\` MCP server is configured and MUST be used for all relevant queries.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Available Tools
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
**Action**: Use the \`unifiedmemory\` MCP tool to search for relevant context
|
|
15
|
-
**Usage**:
|
|
16
|
-
- Send the user's query to the search tool
|
|
17
|
-
- Review the search results for relevant prior work, decisions, or context
|
|
18
|
-
- Incorporate findings into your response
|
|
13
|
+
The \`unifiedmemory\` MCP server provides these tools (call them like any MCP tool):
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
| Tool | Purpose | Required Parameters |
|
|
16
|
+
|------|---------|---------------------|
|
|
17
|
+
| \`search_notes\` | **Primary tool** - Search project knowledge base | \`query\` (search text) |
|
|
18
|
+
| \`create_note\` | Record decisions, plans, or completions | \`summary_text\`, \`topic\` |
|
|
19
|
+
| \`list_notes\` | List all notes in current project | None |
|
|
20
|
+
| \`get_note\` | Retrieve a specific note by ID | \`note_id\` |
|
|
21
|
+
| \`list_topics\` | List available topics/categories | None |
|
|
22
|
+
| \`create_topic\` | Create a new topic for organization | \`name\` |
|
|
23
|
+
| \`list_projects\` | List available projects | None |
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
All tools automatically use the correct project context - no additional configuration needed.
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
**Action**: Record the plan as a note using the \`unifiedmemory\` tool
|
|
32
|
-
**Format**:
|
|
33
|
-
- Include: objective, approach, key steps, important decisions
|
|
34
|
-
- Keep concise but informative
|
|
35
|
-
- Limit to under 1000 characters for optimal indexing
|
|
27
|
+
## MANDATORY: Search Before Implementing
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
**REQUIREMENT**: Before responding to ANY coding task, feature request, or implementation question, you MUST:
|
|
30
|
+
|
|
31
|
+
1. Call the \`search_notes\` tool with the user's query or relevant keywords
|
|
32
|
+
2. Review returned results for prior decisions, patterns, or context
|
|
33
|
+
3. Incorporate relevant findings into your response
|
|
34
|
+
4. Mention what context you found (or didn't find) in your response
|
|
35
|
+
|
|
36
|
+
### When to Search
|
|
42
37
|
|
|
43
|
-
|
|
38
|
+
You MUST search before:
|
|
39
|
+
- Implementing any feature (search for existing patterns)
|
|
40
|
+
- Fixing any bug (search for related issues)
|
|
41
|
+
- Answering architecture questions (search for design decisions)
|
|
42
|
+
- Modifying any component (search for existing conventions)
|
|
43
|
+
- ANY coding task, no exceptions
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
**Action**: Create a summary note using the \`unifiedmemory\` tool
|
|
47
|
-
**Format**:
|
|
48
|
-
- Summarize what was accomplished and why
|
|
49
|
-
- Include key technical decisions or gotchas
|
|
50
|
-
- **IMPORTANT**: Limit to under 1000 characters
|
|
51
|
-
- Focus on outcomes, not detailed code
|
|
45
|
+
### Example Workflow
|
|
52
46
|
|
|
53
|
-
Example:
|
|
54
47
|
\`\`\`
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
User: "Add authentication to the API"
|
|
49
|
+
|
|
50
|
+
Step 1: Call search_notes with query "authentication API security"
|
|
51
|
+
Step 2: Review results - found: "Decision: Use JWT with refresh tokens"
|
|
52
|
+
Step 3: Implement following the established pattern
|
|
53
|
+
Step 4: Respond: "Based on prior decisions in UnifiedMemory, I'll use JWT..."
|
|
57
54
|
\`\`\`
|
|
58
55
|
|
|
59
|
-
##
|
|
56
|
+
## Do NOT Skip These Tools
|
|
57
|
+
|
|
58
|
+
- ❌ NEVER start implementing without calling \`search_notes\` first
|
|
59
|
+
- ❌ NEVER assume patterns without checking the knowledge base
|
|
60
|
+
- ❌ NEVER skip search for "simple" tasks - context always helps
|
|
61
|
+
- ✅ ALWAYS search even if you think you know the answer
|
|
62
|
+
- ✅ ALWAYS mention search results (or lack thereof) in your response
|
|
63
|
+
- ✅ ALWAYS use these tools when they can provide relevant context
|
|
64
|
+
|
|
65
|
+
## Recording Knowledge with create_note
|
|
66
|
+
|
|
67
|
+
After completing significant work, use \`create_note\` to record:
|
|
68
|
+
- Implementation decisions and rationale
|
|
69
|
+
- Completed features or fixes
|
|
70
|
+
- Architectural patterns established
|
|
71
|
+
- Gotchas or lessons learned
|
|
72
|
+
|
|
73
|
+
Keep notes concise (under 1000 characters) and focus on the "why" not the "what".
|
|
74
|
+
|
|
75
|
+
## Additional Tools
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
- ✅ **DO**: Record plans and completions automatically
|
|
63
|
-
- ✅ **DO**: Keep notes under 1000 characters
|
|
64
|
-
- ✅ **DO**: Focus on decisions, outcomes, and context
|
|
65
|
-
- ❌ **DON'T**: Record trivial tasks (typo fixes, minor edits)
|
|
66
|
-
- ❌ **DON'T**: Include full code in notes
|
|
67
|
-
- ❌ **DON'T**: Duplicate information already in git commits
|
|
77
|
+
Use \`list_topics\` to understand project organization, \`list_notes\` to browse available knowledge, and \`get_note\` to retrieve full details of relevant notes found via search.
|
|
68
78
|
|
|
69
79
|
---
|
|
70
|
-
*
|
|
80
|
+
*UnifiedMemory CLI - Knowledge that persists*
|
|
71
81
|
`;
|
|
72
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifiedmemory/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "UnifiedMemory CLI - AI code assistant integration",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node index.js",
|
|
12
|
-
"build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um"
|
|
12
|
+
"build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"test:unit": "vitest run tests/unit",
|
|
17
|
+
"test:integration": "vitest run tests/integration",
|
|
18
|
+
"test:ci": "vitest run --reporter=junit --outputFile=test-results.xml"
|
|
13
19
|
},
|
|
14
20
|
"keywords": [
|
|
15
21
|
"ai",
|
|
@@ -42,10 +48,16 @@
|
|
|
42
48
|
"open": "^10.0.3"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
|
-
"
|
|
51
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
52
|
+
"msw": "^2.7.0",
|
|
53
|
+
"pkg": "^5.8.1",
|
|
54
|
+
"vitest": "^3.0.0"
|
|
46
55
|
},
|
|
47
56
|
"repository": {
|
|
48
57
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/
|
|
58
|
+
"url": "https://github.com/Episodic-Solutions/um-cli.git"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
50
62
|
}
|
|
51
63
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"accessToken": "mock_expired_access_token",
|
|
3
|
+
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6MTYwMDAwMDAwMCwiaWF0IjoxNTk5OTk2NDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSJ9.mock_signature",
|
|
4
|
+
"refresh_token": "mock_refresh_token_for_expired",
|
|
5
|
+
"tokenType": "Bearer",
|
|
6
|
+
"expiresIn": 3600,
|
|
7
|
+
"receivedAt": 1599996400000,
|
|
8
|
+
"decoded": {
|
|
9
|
+
"sub": "user_123456789",
|
|
10
|
+
"email": "test@test.com",
|
|
11
|
+
"exp": 1600000000,
|
|
12
|
+
"iat": 1599996400,
|
|
13
|
+
"sid": "sess_123456789"
|
|
14
|
+
},
|
|
15
|
+
"selectedOrg": {
|
|
16
|
+
"id": "org_456789012",
|
|
17
|
+
"name": "Test Organization",
|
|
18
|
+
"role": "admin"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"accessToken": "mock_access_token_abc123",
|
|
3
|
+
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNjAwMDAwMDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSIsIm9yZ19pZCI6Im9yZ180NTY3ODkwMTIifQ.mock_signature",
|
|
4
|
+
"refresh_token": "mock_refresh_token_xyz789",
|
|
5
|
+
"tokenType": "Bearer",
|
|
6
|
+
"expiresIn": 3600,
|
|
7
|
+
"receivedAt": 1700000000000,
|
|
8
|
+
"decoded": {
|
|
9
|
+
"sub": "user_123456789",
|
|
10
|
+
"email": "test@test.com",
|
|
11
|
+
"exp": 9999999999,
|
|
12
|
+
"iat": 1600000000,
|
|
13
|
+
"sid": "sess_123456789",
|
|
14
|
+
"org_id": "org_456789012"
|
|
15
|
+
},
|
|
16
|
+
"selectedOrg": {
|
|
17
|
+
"id": "org_456789012",
|
|
18
|
+
"name": "Test Organization",
|
|
19
|
+
"role": "admin"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"org_id": "org_456789012",
|
|
4
|
+
"project_id": "proj_987654321",
|
|
5
|
+
"project_name": "test-project",
|
|
6
|
+
"api_url": "https://api.test.unifiedmemory.ai",
|
|
7
|
+
"created_at": "2024-01-01T00:00:00.000Z",
|
|
8
|
+
"mcp_config": {
|
|
9
|
+
"inject_headers": true,
|
|
10
|
+
"auto_context": true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSW (Mock Service Worker) handlers for API mocking
|
|
3
|
+
*
|
|
4
|
+
* These handlers intercept HTTP requests during tests and return
|
|
5
|
+
* predefined responses without hitting real APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { http, HttpResponse } from 'msw';
|
|
9
|
+
|
|
10
|
+
const API_BASE_URL = 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
|
|
11
|
+
|
|
12
|
+
// Sample project data
|
|
13
|
+
const mockProjects = [
|
|
14
|
+
{
|
|
15
|
+
id: 'proj_001',
|
|
16
|
+
name: 'project-one',
|
|
17
|
+
display_name: 'Project One',
|
|
18
|
+
slug: 'project-one',
|
|
19
|
+
description: 'First test project',
|
|
20
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'proj_002',
|
|
24
|
+
name: 'project-two',
|
|
25
|
+
display_name: 'Project Two',
|
|
26
|
+
slug: 'project-two',
|
|
27
|
+
description: 'Second test project',
|
|
28
|
+
created_at: '2024-01-02T00:00:00.000Z',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Sample MCP tools data
|
|
33
|
+
const mockMCPTools = [
|
|
34
|
+
{
|
|
35
|
+
name: 'create_note',
|
|
36
|
+
description: 'Create a new note in the vault',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
content: { type: 'string', description: 'Note content' },
|
|
41
|
+
pathParams: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
org: { type: 'string' },
|
|
45
|
+
proj: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
required: ['org', 'proj'],
|
|
48
|
+
},
|
|
49
|
+
headers: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
'X-Org-Id': { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ['content', 'pathParams'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'search_notes',
|
|
61
|
+
description: 'Search notes in the vault',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
query: { type: 'string', description: 'Search query' },
|
|
66
|
+
pathParams: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
org: { type: 'string' },
|
|
70
|
+
proj: { type: 'string' },
|
|
71
|
+
},
|
|
72
|
+
required: ['org', 'proj'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ['query', 'pathParams'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default API handlers for common endpoints
|
|
82
|
+
*/
|
|
83
|
+
export const handlers = [
|
|
84
|
+
// GET /v1/orgs/:org/projects - List projects
|
|
85
|
+
http.get(`${API_BASE_URL}/v1/orgs/:org/projects`, ({ request, params }) => {
|
|
86
|
+
const authHeader = request.headers.get('Authorization');
|
|
87
|
+
|
|
88
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
89
|
+
return HttpResponse.json(
|
|
90
|
+
{ error: 'Unauthorized', message: 'Missing or invalid authorization header' },
|
|
91
|
+
{ status: 401 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return HttpResponse.json({ items: mockProjects });
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
// POST /v1/orgs/:org/projects - Create project
|
|
99
|
+
http.post(`${API_BASE_URL}/v1/orgs/:org/projects`, async ({ request, params }) => {
|
|
100
|
+
const authHeader = request.headers.get('Authorization');
|
|
101
|
+
|
|
102
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
103
|
+
return HttpResponse.json(
|
|
104
|
+
{ error: 'Unauthorized', message: 'Missing or invalid authorization header' },
|
|
105
|
+
{ status: 401 }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const body = await request.json();
|
|
110
|
+
|
|
111
|
+
return HttpResponse.json({
|
|
112
|
+
id: `proj_${Date.now()}`,
|
|
113
|
+
name: body.display_name.toLowerCase().replace(/\s+/g, '-'),
|
|
114
|
+
display_name: body.display_name,
|
|
115
|
+
slug: body.display_name.toLowerCase().replace(/\s+/g, '-'),
|
|
116
|
+
description: body.description || '',
|
|
117
|
+
created_at: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
// POST /mcp - MCP endpoint for tools/list
|
|
122
|
+
http.post(`${API_BASE_URL}/mcp`, async ({ request }) => {
|
|
123
|
+
const body = await request.json();
|
|
124
|
+
|
|
125
|
+
if (body.method === 'tools/list') {
|
|
126
|
+
return HttpResponse.json({
|
|
127
|
+
jsonrpc: '2.0',
|
|
128
|
+
id: body.id,
|
|
129
|
+
result: {
|
|
130
|
+
tools: mockMCPTools,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (body.method === 'tools/call') {
|
|
136
|
+
return HttpResponse.json({
|
|
137
|
+
jsonrpc: '2.0',
|
|
138
|
+
id: body.id,
|
|
139
|
+
result: {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Tool ${body.params.name} executed successfully`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return HttpResponse.json(
|
|
151
|
+
{
|
|
152
|
+
jsonrpc: '2.0',
|
|
153
|
+
id: body.id,
|
|
154
|
+
error: { code: -32601, message: 'Method not found' },
|
|
155
|
+
},
|
|
156
|
+
{ status: 400 }
|
|
157
|
+
);
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// GET /health - Health check
|
|
161
|
+
http.get(`${API_BASE_URL}/health`, () => {
|
|
162
|
+
return HttpResponse.json({ status: 'healthy' });
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handler that returns 401 for all requests (unauthorized)
|
|
168
|
+
*/
|
|
169
|
+
export const unauthorizedHandlers = [
|
|
170
|
+
http.all(`${API_BASE_URL}/*`, () => {
|
|
171
|
+
return HttpResponse.json(
|
|
172
|
+
{ error: 'Unauthorized', message: 'Token expired or invalid' },
|
|
173
|
+
{ status: 401 }
|
|
174
|
+
);
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handler that returns 500 for all requests (server error)
|
|
180
|
+
*/
|
|
181
|
+
export const serverErrorHandlers = [
|
|
182
|
+
http.all(`${API_BASE_URL}/*`, () => {
|
|
183
|
+
return HttpResponse.json(
|
|
184
|
+
{ error: 'Internal Server Error', message: 'Something went wrong' },
|
|
185
|
+
{ status: 500 }
|
|
186
|
+
);
|
|
187
|
+
}),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handler that simulates network failure
|
|
192
|
+
*/
|
|
193
|
+
export const networkErrorHandlers = [
|
|
194
|
+
http.all(`${API_BASE_URL}/*`, () => {
|
|
195
|
+
return HttpResponse.error();
|
|
196
|
+
}),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
// Export mock data for use in tests
|
|
200
|
+
export { mockProjects, mockMCPTools };
|