fraim-hub 2.0.204
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/README.md +467 -0
- package/bin/fraim-hub.js +17 -0
- package/dist/src/ai-hub/brand-store.js +17 -0
- package/dist/src/ai-hub/catalog.js +412 -0
- package/dist/src/ai-hub/cert-store.js +70 -0
- package/dist/src/ai-hub/cli.js +212 -0
- package/dist/src/ai-hub/configured-agents.js +283 -0
- package/dist/src/ai-hub/conversation-store.js +428 -0
- package/dist/src/ai-hub/desktop-main.js +449 -0
- package/dist/src/ai-hub/hosts.js +1669 -0
- package/dist/src/ai-hub/hub-latest-version.js +52 -0
- package/dist/src/ai-hub/hub-launch-decision.js +57 -0
- package/dist/src/ai-hub/hub-runtime-file.js +43 -0
- package/dist/src/ai-hub/managed-browser.js +269 -0
- package/dist/src/ai-hub/manager-turns.js +64 -0
- package/dist/src/ai-hub/office-sideload.js +156 -0
- package/dist/src/ai-hub/openclaw-bridge.js +250 -0
- package/dist/src/ai-hub/preferences.js +201 -0
- package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
- package/dist/src/ai-hub/server.js +4013 -0
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/ai-hub/url-safety.js +19 -0
- package/dist/src/api/admin/payments.js +33 -0
- package/dist/src/api/admin/sales-leads.js +21 -0
- package/dist/src/api/payment/create-session.js +338 -0
- package/dist/src/api/payment/dashboard-link.js +149 -0
- package/dist/src/api/payment/session-details.js +31 -0
- package/dist/src/api/payment/webhook.js +587 -0
- package/dist/src/api/sales/contact.js +44 -0
- package/dist/src/cli/api/get-provider-client.js +41 -0
- package/dist/src/cli/api/provider-client.js +107 -0
- package/dist/src/cli/commands/add-ide.js +462 -0
- package/dist/src/cli/fraim-hub.js +50 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/command-resolution.js +81 -0
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
- package/dist/src/cli/mcp/ide-formats.js +317 -0
- package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +173 -0
- package/dist/src/cli/providers/local-provider-registry.js +165 -0
- package/dist/src/cli/providers/provider-registry.js +230 -0
- package/dist/src/cli/setup/claude-code-telemetry.js +59 -0
- package/dist/src/cli/setup/codex-local-config.js +37 -0
- package/dist/src/cli/setup/ide-detector.js +383 -0
- package/dist/src/cli/setup/ide-global-integration.js +100 -0
- package/dist/src/cli/setup/ide-invocation-surfaces.js +160 -0
- package/dist/src/cli/setup/mcp-config-generator.js +225 -0
- package/dist/src/cli/setup/provider-prompts.js +339 -0
- package/dist/src/cli/utils/managed-agent-paths.js +118 -0
- package/dist/src/cli/utils/script-sync-utils.js +221 -0
- package/dist/src/cli/utils/user-config.js +100 -0
- package/dist/src/cli/utils/version-utils.js +35 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +277 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/brand-store.js +232 -0
- package/dist/src/core/quality-evidence.js +331 -0
- package/dist/src/core/utils/git-utils.js +194 -0
- package/dist/src/core/utils/ports.js +32 -0
- package/dist/src/core/utils/project-fraim-paths.js +220 -0
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/first-run/types.js +93 -0
- package/dist/src/fraim/db-service.js +2409 -0
- package/dist/src/local-mcp-server/agent-token-prices.js +167 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +975 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/persona-entitlement-service.js +352 -0
- package/dist/src/services/workspace-identity.js +21 -0
- package/dist/src/types/analytics.js +2 -0
- package/dist/src/utils/payment-calculator.js +52 -0
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +33 -0
- package/extensions/office-word/taskpane.html +242 -0
- package/index.js +85 -0
- package/package.json +106 -0
- package/public/ai-hub/index.html +910 -0
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -0
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
- package/public/ai-hub/review.css +444 -0
- package/public/ai-hub/script.js +11737 -0
- package/public/ai-hub/styles.css +5102 -0
- package/public/first-run/error-frame.js +100 -0
- package/public/first-run/index.html +35 -0
- package/public/first-run/script.js +739 -0
- package/public/first-run/styles.css +929 -0
- package/public/portfolio/ashley.html +523 -0
- package/public/portfolio/auditya.html +83 -0
- package/public/portfolio/banke.html +83 -0
- package/public/portfolio/beza.html +659 -0
- package/public/portfolio/careena.html +632 -0
- package/public/portfolio/casey.html +568 -0
- package/public/portfolio/celia.html +490 -0
- package/public/portfolio/deidre.html +642 -0
- package/public/portfolio/gautam.html +597 -0
- package/public/portfolio/hari.html +469 -0
- package/public/portfolio/huxley.html +1354 -0
- package/public/portfolio/index.html +741 -0
- package/public/portfolio/maestro.html +518 -0
- package/public/portfolio/mandy.html +590 -0
- package/public/portfolio/mona.html +597 -0
- package/public/portfolio/pam.html +887 -0
- package/public/portfolio/procella.html +107 -0
- package/public/portfolio/qasm.html +569 -0
- package/public/portfolio/ricardo.html +489 -0
- package/public/portfolio/sade.html +560 -0
- package/public/portfolio/sam.html +654 -0
- package/public/portfolio/sechar.html +580 -0
- package/public/portfolio/sreya.html +599 -0
- package/public/portfolio/swen.html +601 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isExplicitLocalRepoPath = isExplicitLocalRepoPath;
|
|
4
|
+
exports.getPort = getPort;
|
|
5
|
+
exports.determineDatabaseName = determineDatabaseName;
|
|
6
|
+
exports.getCurrentGitBranch = getCurrentGitBranch;
|
|
7
|
+
exports.determineSchema = determineSchema;
|
|
8
|
+
exports.getDefaultBranch = getDefaultBranch;
|
|
9
|
+
exports.sanitizeRepoIdentifier = sanitizeRepoIdentifier;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
function extractLocalFolderLabel(rawPath) {
|
|
12
|
+
const normalized = rawPath
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/^file:\/\//i, '')
|
|
15
|
+
.replace(/\\/g, '/')
|
|
16
|
+
.replace(/\/+$/, '');
|
|
17
|
+
if (!normalized)
|
|
18
|
+
return undefined;
|
|
19
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
20
|
+
const candidate = parts[parts.length - 1];
|
|
21
|
+
return candidate || undefined;
|
|
22
|
+
}
|
|
23
|
+
function isExplicitLocalRepoPath(repoUrl) {
|
|
24
|
+
if (!repoUrl || typeof repoUrl !== 'string') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const trimmed = repoUrl.trim();
|
|
28
|
+
return (trimmed.startsWith('/') ||
|
|
29
|
+
trimmed.startsWith('\\') ||
|
|
30
|
+
/^[a-zA-Z]:[\\/]/.test(trimmed) ||
|
|
31
|
+
trimmed.startsWith('./') ||
|
|
32
|
+
trimmed.startsWith('../') ||
|
|
33
|
+
trimmed.startsWith('file://'));
|
|
34
|
+
}
|
|
35
|
+
function readExplicitPortFromEnv() {
|
|
36
|
+
for (const value of [
|
|
37
|
+
process.env.PLAYWRIGHT_FRAIM_MCP_PORT,
|
|
38
|
+
process.env.PLAYWRIGHT_WEBSITE_PORT,
|
|
39
|
+
process.env.FRAIM_MCP_PORT,
|
|
40
|
+
process.env.PORT,
|
|
41
|
+
process.env.WEBSITES_PORT
|
|
42
|
+
]) {
|
|
43
|
+
const port = Number(value);
|
|
44
|
+
if (Number.isFinite(port) && port > 0) {
|
|
45
|
+
return port;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Gets a unique port based on the current git branch name (if it's an issue branch)
|
|
52
|
+
* Default to 15302 if not on an issue branch
|
|
53
|
+
*/
|
|
54
|
+
function getPort() {
|
|
55
|
+
// Explicit runtime ports must win so tests and subprocesses all resolve the
|
|
56
|
+
// same active server instead of drifting back to a branch-derived default.
|
|
57
|
+
const explicitPort = readExplicitPortFromEnv();
|
|
58
|
+
if (explicitPort) {
|
|
59
|
+
return explicitPort;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const branchName = process.env.FRAIM_BRANCH || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
63
|
+
// Match issue-123 or 123-feature-name or feature/123-name
|
|
64
|
+
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
65
|
+
if (issueMatch) {
|
|
66
|
+
const issueNum = parseInt(issueMatch[1], 10);
|
|
67
|
+
// Ensure port is in a safe range (10000-65535)
|
|
68
|
+
return 10000 + (issueNum % 55535);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
// Silently fail and use default
|
|
73
|
+
}
|
|
74
|
+
return 15302;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Determines the database name based on the git branch
|
|
78
|
+
*/
|
|
79
|
+
function determineDatabaseName() {
|
|
80
|
+
try {
|
|
81
|
+
const branchName = process.env.FRAIM_BRANCH || process.env.FRAIM_BRANCH_NAME || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
82
|
+
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
83
|
+
if (issueMatch) {
|
|
84
|
+
return `fraim_issue_${issueMatch[1]}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Silently fail
|
|
89
|
+
}
|
|
90
|
+
return process.env.MONGODB_DB_NAME || (process.env.NODE_ENV === 'production' ? 'fraim_prod' : 'fraim_dev');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Gets the current git branch name
|
|
94
|
+
*/
|
|
95
|
+
function getCurrentGitBranch() {
|
|
96
|
+
try {
|
|
97
|
+
return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
98
|
+
timeout: 2000, // 2 second timeout
|
|
99
|
+
stdio: 'pipe'
|
|
100
|
+
}).toString().trim();
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return 'master';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Determines the database schema prefix based on the branch
|
|
108
|
+
*/
|
|
109
|
+
function determineSchema(branchName) {
|
|
110
|
+
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
111
|
+
if (issueMatch) {
|
|
112
|
+
return `issue_${issueMatch[1]}`;
|
|
113
|
+
}
|
|
114
|
+
return 'prod';
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Gets the default branch name from git remote
|
|
118
|
+
*/
|
|
119
|
+
function getDefaultBranch() {
|
|
120
|
+
try {
|
|
121
|
+
// Try to get the default branch from remote HEAD
|
|
122
|
+
const remoteHead = (0, child_process_1.execSync)('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
123
|
+
timeout: 2000, // 2 second timeout
|
|
124
|
+
stdio: 'pipe'
|
|
125
|
+
}).toString().trim();
|
|
126
|
+
const match = remoteHead.match(/refs\/remotes\/origin\/(.+)$/);
|
|
127
|
+
if (match) {
|
|
128
|
+
return match[1];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
// If that fails, try to get it from the current branch
|
|
133
|
+
try {
|
|
134
|
+
return getCurrentGitBranch();
|
|
135
|
+
}
|
|
136
|
+
catch (e2) {
|
|
137
|
+
// Fall back to common defaults
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Default fallback
|
|
141
|
+
return 'main';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Sanitizes a repository identifier by stripping local paths and normalizing to standard HTTPS URLs.
|
|
145
|
+
* Mandatory per Compliance requirement (C1) to ensure data privacy.
|
|
146
|
+
*
|
|
147
|
+
* @param repoUrl The raw repository URL or path to sanitize
|
|
148
|
+
* @returns Normalized HTTPS URL string, or undefined if the input is a local path or invalid
|
|
149
|
+
*/
|
|
150
|
+
function sanitizeRepoIdentifier(repoUrl) {
|
|
151
|
+
if (!repoUrl || typeof repoUrl !== 'string') {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const trimmed = repoUrl.trim();
|
|
155
|
+
// Convert local paths to just the folder name for privacy-safe workspace labeling.
|
|
156
|
+
if (isExplicitLocalRepoPath(trimmed)) {
|
|
157
|
+
return extractLocalFolderLabel(trimmed);
|
|
158
|
+
}
|
|
159
|
+
// Normalize GitHub/GitLab SSH and HTTPS URLs
|
|
160
|
+
// Patterns:
|
|
161
|
+
// git@github.com:owner/repo.git
|
|
162
|
+
// https://github.com/owner/repo.git
|
|
163
|
+
// git@gitlab.com:owner/repo.git
|
|
164
|
+
// https://gitlab.com/owner/repo.git
|
|
165
|
+
const gitPattern = /^(?:git@|https:\/\/)(github\.com|gitlab\.com)[:/]([^/]+\/[^/.]+?)(?:\.git)?$/i;
|
|
166
|
+
const match = trimmed.match(gitPattern);
|
|
167
|
+
if (match) {
|
|
168
|
+
const [, domain, path] = match;
|
|
169
|
+
return `https://${domain.toLowerCase()}/${path}`;
|
|
170
|
+
}
|
|
171
|
+
// If it looks like a generic URL but not from approved providers, reject for privacy
|
|
172
|
+
try {
|
|
173
|
+
const url = new URL(trimmed);
|
|
174
|
+
if (url.hostname.includes('github.com') || url.hostname.includes('gitlab.com')) {
|
|
175
|
+
let path = url.pathname;
|
|
176
|
+
if (path.startsWith('/'))
|
|
177
|
+
path = path.substring(1);
|
|
178
|
+
if (path.endsWith('.git'))
|
|
179
|
+
path = path.substring(0, path.length - 4);
|
|
180
|
+
// Only return if it matches owner/repo structure
|
|
181
|
+
if (path.split('/').length >= 2) {
|
|
182
|
+
return `https://${url.hostname.toLowerCase()}/${path}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
// Not a valid URL
|
|
188
|
+
}
|
|
189
|
+
// Preserve plain folder labels that are already privacy-safe and not path-like.
|
|
190
|
+
if (!trimmed.includes('/') && !trimmed.includes('\\') && !trimmed.includes('://')) {
|
|
191
|
+
return trimmed;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findAvailablePort = findAvailablePort;
|
|
7
|
+
exports.findAvailablePortExcluding = findAvailablePortExcluding;
|
|
8
|
+
const net_1 = __importDefault(require("net"));
|
|
9
|
+
async function findAvailablePort(preferredPort) {
|
|
10
|
+
return findAvailablePortExcluding(preferredPort, new Set());
|
|
11
|
+
}
|
|
12
|
+
async function findAvailablePortExcluding(preferredPort, excludedPorts) {
|
|
13
|
+
let port = preferredPort;
|
|
14
|
+
while (port < preferredPort + 20) {
|
|
15
|
+
if (excludedPorts.has(port)) {
|
|
16
|
+
port += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const available = await new Promise((resolve) => {
|
|
20
|
+
const server = net_1.default.createServer();
|
|
21
|
+
server.once('error', () => resolve(false));
|
|
22
|
+
server.once('listening', () => {
|
|
23
|
+
server.close(() => resolve(true));
|
|
24
|
+
});
|
|
25
|
+
server.listen(port, '127.0.0.1');
|
|
26
|
+
});
|
|
27
|
+
if (available)
|
|
28
|
+
return port;
|
|
29
|
+
port += 1;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`No free loopback port found near ${preferredPort}.`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WORKSPACE_SYNCED_CONTENT_DIRS = exports.WORKSPACE_FRAIM_DIRNAME = void 0;
|
|
7
|
+
exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
|
|
8
|
+
exports.workspaceFraimExists = workspaceFraimExists;
|
|
9
|
+
exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
|
|
10
|
+
exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
|
|
11
|
+
exports.assertWorkspaceFraimRoot = assertWorkspaceFraimRoot;
|
|
12
|
+
exports.assertWorkspaceFraimPath = assertWorkspaceFraimPath;
|
|
13
|
+
exports.createWorkspaceFraimPathAsserter = createWorkspaceFraimPathAsserter;
|
|
14
|
+
exports.assertExactWorkspaceFraimPath = assertExactWorkspaceFraimPath;
|
|
15
|
+
exports.getWorkspaceFraimDisplayPath = getWorkspaceFraimDisplayPath;
|
|
16
|
+
exports.getUserFraimDisplayPath = getUserFraimDisplayPath;
|
|
17
|
+
exports.getUserFraimDirPath = getUserFraimDirPath;
|
|
18
|
+
exports.getUserFraimLearningsDir = getUserFraimLearningsDir;
|
|
19
|
+
exports.getWorkspaceLearningsDir = getWorkspaceLearningsDir;
|
|
20
|
+
exports.getConfiguredPortableLearningsDir = getConfiguredPortableLearningsDir;
|
|
21
|
+
exports.getConfiguredPortableLearningsDisplayPath = getConfiguredPortableLearningsDisplayPath;
|
|
22
|
+
exports.getEffectiveFraimDir = getEffectiveFraimDir;
|
|
23
|
+
const fs_1 = require("fs");
|
|
24
|
+
const path_1 = require("path");
|
|
25
|
+
const os_1 = __importDefault(require("os"));
|
|
26
|
+
exports.WORKSPACE_FRAIM_DIRNAME = 'fraim';
|
|
27
|
+
exports.WORKSPACE_SYNCED_CONTENT_DIRS = [
|
|
28
|
+
'workflows',
|
|
29
|
+
'ai-employee',
|
|
30
|
+
'ai-manager',
|
|
31
|
+
'docs'
|
|
32
|
+
];
|
|
33
|
+
function normalizeRelativePath(relativePath) {
|
|
34
|
+
return relativePath.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '/');
|
|
35
|
+
}
|
|
36
|
+
function normalizeDisplayPath(fullPath) {
|
|
37
|
+
return fullPath.replace(/\\/g, '/');
|
|
38
|
+
}
|
|
39
|
+
function getWorkspaceFraimDir(projectRoot = process.cwd()) {
|
|
40
|
+
return (0, path_1.join)(projectRoot, exports.WORKSPACE_FRAIM_DIRNAME);
|
|
41
|
+
}
|
|
42
|
+
function isPathInsideOrEqual(parentPath, childPath) {
|
|
43
|
+
const relativePath = (0, path_1.relative)(parentPath, childPath);
|
|
44
|
+
return relativePath === '' || (relativePath.length > 0 &&
|
|
45
|
+
!relativePath.startsWith('..') &&
|
|
46
|
+
!(0, path_1.isAbsolute)(relativePath));
|
|
47
|
+
}
|
|
48
|
+
function realpathIfExists(fullPath) {
|
|
49
|
+
try {
|
|
50
|
+
return fs_1.realpathSync.native(fullPath);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function deepestExistingAncestor(fullPath) {
|
|
57
|
+
let current = (0, path_1.resolve)(fullPath);
|
|
58
|
+
while (!(0, fs_1.existsSync)(current)) {
|
|
59
|
+
const parent = (0, path_1.dirname)(current);
|
|
60
|
+
if (parent === current) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
function workspaceFraimExists(projectRoot = process.cwd()) {
|
|
68
|
+
try {
|
|
69
|
+
return (0, fs_1.readdirSync)(projectRoot, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name === exports.WORKSPACE_FRAIM_DIRNAME);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getWorkspaceConfigPath(projectRoot = process.cwd()) {
|
|
76
|
+
return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), 'config.json');
|
|
77
|
+
}
|
|
78
|
+
function getWorkspaceFraimPath(projectRoot = process.cwd(), ...parts) {
|
|
79
|
+
return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), ...parts);
|
|
80
|
+
}
|
|
81
|
+
function assertWorkspaceFraimRoot(projectRoot = process.cwd()) {
|
|
82
|
+
const resolvedProjectRoot = (0, path_1.resolve)(projectRoot);
|
|
83
|
+
const fraimDir = getWorkspaceFraimDir(resolvedProjectRoot);
|
|
84
|
+
if (!workspaceFraimExists(resolvedProjectRoot)) {
|
|
85
|
+
throw new Error(`Refusing to use FRAIM workspace root "${resolvedProjectRoot}": expected exact lowercase fraim directory at "${fraimDir}".`);
|
|
86
|
+
}
|
|
87
|
+
const projectRootRealPath = realpathIfExists(resolvedProjectRoot);
|
|
88
|
+
const fraimRealPath = realpathIfExists(fraimDir);
|
|
89
|
+
if (projectRootRealPath &&
|
|
90
|
+
fraimRealPath &&
|
|
91
|
+
!isPathInsideOrEqual(projectRootRealPath, fraimRealPath)) {
|
|
92
|
+
throw new Error(`Refusing to use FRAIM workspace root "${resolvedProjectRoot}": "${fraimDir}" resolves outside the project root.`);
|
|
93
|
+
}
|
|
94
|
+
return fraimDir;
|
|
95
|
+
}
|
|
96
|
+
function assertWorkspaceFraimPath(projectRoot = process.cwd(), targetPath) {
|
|
97
|
+
createWorkspaceFraimPathAsserter(projectRoot)(targetPath);
|
|
98
|
+
}
|
|
99
|
+
function createWorkspaceFraimPathAsserter(projectRoot = process.cwd()) {
|
|
100
|
+
const fraimDir = assertWorkspaceFraimRoot(projectRoot);
|
|
101
|
+
const resolvedFraimDir = (0, path_1.resolve)(fraimDir);
|
|
102
|
+
const fraimRealPath = realpathIfExists(fraimDir);
|
|
103
|
+
const realpathCache = new Map();
|
|
104
|
+
const cachedRealpath = (fullPath) => {
|
|
105
|
+
const resolvedPath = (0, path_1.resolve)(fullPath);
|
|
106
|
+
if (!realpathCache.has(resolvedPath)) {
|
|
107
|
+
realpathCache.set(resolvedPath, realpathIfExists(resolvedPath));
|
|
108
|
+
}
|
|
109
|
+
return realpathCache.get(resolvedPath) ?? null;
|
|
110
|
+
};
|
|
111
|
+
return (targetPath) => {
|
|
112
|
+
const resolvedTargetPath = (0, path_1.resolve)(targetPath);
|
|
113
|
+
if (resolvedTargetPath === resolvedFraimDir ||
|
|
114
|
+
!isPathInsideOrEqual(resolvedFraimDir, resolvedTargetPath)) {
|
|
115
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": expected a path under "${resolvedFraimDir}".`);
|
|
116
|
+
}
|
|
117
|
+
const existingAncestor = deepestExistingAncestor(resolvedTargetPath);
|
|
118
|
+
const ancestorRealPath = existingAncestor ? cachedRealpath(existingAncestor) : null;
|
|
119
|
+
if (fraimRealPath &&
|
|
120
|
+
ancestorRealPath &&
|
|
121
|
+
!isPathInsideOrEqual(fraimRealPath, ancestorRealPath)) {
|
|
122
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": existing path "${existingAncestor}" resolves outside "${fraimDir}".`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function assertExactWorkspaceFraimPath(projectRoot = process.cwd(), targetPath, ...parts) {
|
|
127
|
+
const expectedPath = (0, path_1.resolve)(getWorkspaceFraimDir((0, path_1.resolve)(projectRoot)), ...parts);
|
|
128
|
+
const resolvedTargetPath = (0, path_1.resolve)(targetPath);
|
|
129
|
+
if (resolvedTargetPath !== expectedPath) {
|
|
130
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": expected exact managed path "${expectedPath}".`);
|
|
131
|
+
}
|
|
132
|
+
assertWorkspaceFraimPath(projectRoot, targetPath);
|
|
133
|
+
}
|
|
134
|
+
function getWorkspaceFraimDisplayPath(relativePath = '') {
|
|
135
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
136
|
+
return normalized.length > 0
|
|
137
|
+
? `${exports.WORKSPACE_FRAIM_DIRNAME}/${normalized}`
|
|
138
|
+
: `${exports.WORKSPACE_FRAIM_DIRNAME}/`;
|
|
139
|
+
}
|
|
140
|
+
function getUserFraimDisplayPath(relativePath = '') {
|
|
141
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
142
|
+
return normalized.length > 0 ? `~/.fraim/${normalized}` : '~/.fraim/';
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the user-level FRAIM directory (~/.fraim/).
|
|
146
|
+
* Can be overridden with FRAIM_USER_DIR env var for testing.
|
|
147
|
+
*/
|
|
148
|
+
function getUserFraimDirPath() {
|
|
149
|
+
return process.env.FRAIM_USER_DIR || (0, path_1.join)(os_1.default.homedir(), '.fraim');
|
|
150
|
+
}
|
|
151
|
+
function getUserFraimLearningsDir() {
|
|
152
|
+
return (0, path_1.join)(getUserFraimDirPath(), 'personalized-employee', 'learnings');
|
|
153
|
+
}
|
|
154
|
+
function getWorkspaceLearningsDir(projectRoot = process.cwd()) {
|
|
155
|
+
return getWorkspaceFraimPath(projectRoot, 'personalized-employee', 'learnings');
|
|
156
|
+
}
|
|
157
|
+
function resolveConfiguredLearningGlobalPath(rawPath) {
|
|
158
|
+
if (typeof rawPath !== 'string')
|
|
159
|
+
return null;
|
|
160
|
+
const trimmed = rawPath.trim();
|
|
161
|
+
if (!trimmed)
|
|
162
|
+
return null;
|
|
163
|
+
if (/^[a-z]+:\/\//i.test(trimmed))
|
|
164
|
+
return null;
|
|
165
|
+
if (trimmed.startsWith('~')) {
|
|
166
|
+
const homeRelative = trimmed.slice(1).replace(/^[\\/]+/, '');
|
|
167
|
+
return (0, path_1.normalize)((0, path_1.join)(os_1.default.homedir(), homeRelative));
|
|
168
|
+
}
|
|
169
|
+
if (!(0, path_1.isAbsolute)(trimmed))
|
|
170
|
+
return null;
|
|
171
|
+
return (0, path_1.normalize)(trimmed);
|
|
172
|
+
}
|
|
173
|
+
function readWorkspaceConfig(projectRoot) {
|
|
174
|
+
try {
|
|
175
|
+
const configPath = getWorkspaceConfigPath(projectRoot);
|
|
176
|
+
if (!(0, fs_1.existsSync)(configPath))
|
|
177
|
+
return null;
|
|
178
|
+
return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function getConfiguredPortableLearningsDir(projectRoot = process.cwd()) {
|
|
185
|
+
const config = readWorkspaceConfig(projectRoot);
|
|
186
|
+
const configured = resolveConfiguredLearningGlobalPath(config?.learning?.globalPath);
|
|
187
|
+
return configured || getUserFraimLearningsDir();
|
|
188
|
+
}
|
|
189
|
+
function getConfiguredPortableLearningsDisplayPath(projectRoot = process.cwd()) {
|
|
190
|
+
const config = readWorkspaceConfig(projectRoot);
|
|
191
|
+
const configured = resolveConfiguredLearningGlobalPath(config?.learning?.globalPath);
|
|
192
|
+
if (configured) {
|
|
193
|
+
return normalizeDisplayPath(configured);
|
|
194
|
+
}
|
|
195
|
+
return getUserFraimDisplayPath('personalized-employee/learnings').replace(/\/$/, '');
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Determine the effective FRAIM content root directory.
|
|
199
|
+
*
|
|
200
|
+
* Shadow semantics: if the project has a local fraim/ directory, it completely
|
|
201
|
+
* shadows the user-level ~/.fraim/ — no mixing, no layering.
|
|
202
|
+
*
|
|
203
|
+
* @param projectRoot - The project/workspace root directory
|
|
204
|
+
* @param userFraimDir - Optional override for the user-level dir (for testing)
|
|
205
|
+
* @returns The effective fraim content root directory path, or '' if neither exists
|
|
206
|
+
*/
|
|
207
|
+
function getEffectiveFraimDir(projectRoot = process.cwd(), userFraimDir) {
|
|
208
|
+
// 1. Check for project-level fraim/ directory
|
|
209
|
+
const projectFraimDir = getWorkspaceFraimDir(projectRoot);
|
|
210
|
+
if (workspaceFraimExists(projectRoot)) {
|
|
211
|
+
return projectFraimDir;
|
|
212
|
+
}
|
|
213
|
+
// 2. Fall back to user-level ~/.fraim/
|
|
214
|
+
const userDir = userFraimDir || getUserFraimDirPath();
|
|
215
|
+
if ((0, fs_1.existsSync)(userDir)) {
|
|
216
|
+
return userDir;
|
|
217
|
+
}
|
|
218
|
+
// 3. Neither exists
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PaymentRepository = void 0;
|
|
4
|
+
const COLLECTION_NAME = 'fraim_payments';
|
|
5
|
+
class PaymentRepository {
|
|
6
|
+
constructor(client, dbName = 'fraim') {
|
|
7
|
+
this.db = client.db(dbName);
|
|
8
|
+
this.collection = this.db.collection(COLLECTION_NAME);
|
|
9
|
+
}
|
|
10
|
+
async ensureIndexes() {
|
|
11
|
+
await this.collection.createIndex({ email: 1 });
|
|
12
|
+
await this.collection.createIndex({ timestamp: -1 });
|
|
13
|
+
await this.collection.createIndex({ status: 1 });
|
|
14
|
+
await this.collection.createIndex({ stripeCustomerId: 1 });
|
|
15
|
+
}
|
|
16
|
+
async createPayment(payment) {
|
|
17
|
+
const result = await this.collection.insertOne(payment);
|
|
18
|
+
return { ...payment, _id: result.insertedId.toString() };
|
|
19
|
+
}
|
|
20
|
+
async updatePaymentStatus(stripePaymentIntentId, status, metadata) {
|
|
21
|
+
const result = await this.collection.updateOne({ stripePaymentIntentId }, {
|
|
22
|
+
$set: {
|
|
23
|
+
status,
|
|
24
|
+
...(metadata && { metadata })
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return result.modifiedCount > 0;
|
|
28
|
+
}
|
|
29
|
+
async getPaymentById(id) {
|
|
30
|
+
return await this.collection.findOne({ _id: id });
|
|
31
|
+
}
|
|
32
|
+
async getPaymentByStripePaymentIntent(stripePaymentIntentId) {
|
|
33
|
+
return await this.collection.findOne({ stripePaymentIntentId });
|
|
34
|
+
}
|
|
35
|
+
async getPaymentsByEmail(email) {
|
|
36
|
+
return await this.collection.find({ email }).sort({ timestamp: -1 }).toArray();
|
|
37
|
+
}
|
|
38
|
+
async listPayments(filters) {
|
|
39
|
+
const query = {};
|
|
40
|
+
if (filters.plan)
|
|
41
|
+
query.plan = filters.plan;
|
|
42
|
+
if (filters.status)
|
|
43
|
+
query.status = filters.status;
|
|
44
|
+
if (filters.startDate || filters.endDate) {
|
|
45
|
+
query.timestamp = {};
|
|
46
|
+
if (filters.startDate)
|
|
47
|
+
query.timestamp.$gte = filters.startDate;
|
|
48
|
+
if (filters.endDate)
|
|
49
|
+
query.timestamp.$lte = filters.endDate;
|
|
50
|
+
}
|
|
51
|
+
const total = await this.collection.countDocuments(query);
|
|
52
|
+
const payments = await this.collection
|
|
53
|
+
.find(query)
|
|
54
|
+
.sort({ timestamp: -1 })
|
|
55
|
+
.skip(filters.offset || 0)
|
|
56
|
+
.limit(filters.limit || 50)
|
|
57
|
+
.toArray();
|
|
58
|
+
return { payments, total };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.PaymentRepository = PaymentRepository;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Setup checklist surface — the row-keyed model that replaces the prior
|
|
4
|
+
* linear step machine for issue #352 v1.
|
|
5
|
+
*
|
|
6
|
+
* The same surface serves every user. What changes between a fresh-machine
|
|
7
|
+
* run and an experienced-engineer run is the distribution of row statuses,
|
|
8
|
+
* not the surface itself. Per R2.0 acceptance, the DOM and CSS chrome are
|
|
9
|
+
* byte-identical between cases — only `data-row-status`, the verb-text
|
|
10
|
+
* node, and the primary-button label differ.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.FIRST_RUN_AGENT_OPTIONS = exports.FIRST_RUN_RESOURCES_URL = exports.FIRST_RUN_PROMPT = exports.FIRST_RUN_ROW_IDS = void 0;
|
|
14
|
+
exports.createInitialRows = createInitialRows;
|
|
15
|
+
exports.derivePrimaryButtonLabel = derivePrimaryButtonLabel;
|
|
16
|
+
exports.FIRST_RUN_ROW_IDS = [
|
|
17
|
+
'node',
|
|
18
|
+
'git',
|
|
19
|
+
'agent',
|
|
20
|
+
'fraim',
|
|
21
|
+
];
|
|
22
|
+
exports.FIRST_RUN_PROMPT = 'Onboard this project';
|
|
23
|
+
exports.FIRST_RUN_RESOURCES_URL = 'https://fraimworks.ai/resources.html';
|
|
24
|
+
/**
|
|
25
|
+
* Recommended-agent priority order (R2.2).
|
|
26
|
+
* Tie-break: Claude Code first, then Codex, then Gemini.
|
|
27
|
+
*/
|
|
28
|
+
exports.FIRST_RUN_AGENT_OPTIONS = [
|
|
29
|
+
{
|
|
30
|
+
id: 'claude-code',
|
|
31
|
+
label: 'Claude Code',
|
|
32
|
+
detectAliases: ['claude-code', 'claude code', 'claude'],
|
|
33
|
+
loginCommand: 'claude',
|
|
34
|
+
launchCommand: 'claude',
|
|
35
|
+
installPackage: '@anthropic-ai/claude-code',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'codex',
|
|
39
|
+
label: 'Codex',
|
|
40
|
+
detectAliases: ['codex'],
|
|
41
|
+
loginCommand: 'codex login',
|
|
42
|
+
launchCommand: 'codex',
|
|
43
|
+
installPackage: '@openai/codex',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'gemini-cli',
|
|
47
|
+
label: 'Gemini CLI',
|
|
48
|
+
detectAliases: ['gemini-cli', 'gemini cli', 'gemini'],
|
|
49
|
+
loginCommand: 'gemini',
|
|
50
|
+
launchCommand: 'gemini',
|
|
51
|
+
installPackage: '@google/gemini-cli',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'copilot-cli',
|
|
55
|
+
label: 'GitHub Copilot CLI',
|
|
56
|
+
detectAliases: ['copilot', 'copilot-cli', 'github copilot cli'],
|
|
57
|
+
loginCommand: 'copilot login',
|
|
58
|
+
launchCommand: 'copilot',
|
|
59
|
+
installPackage: '@github/copilot',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* The canonical row set, in display order. Each row starts in `pending`;
|
|
64
|
+
* detection updates statuses on session load.
|
|
65
|
+
*/
|
|
66
|
+
function createInitialRows() {
|
|
67
|
+
return [
|
|
68
|
+
{ id: 'node', label: 'Node.js', status: 'pending', verb: "we'll install" },
|
|
69
|
+
{ id: 'git', label: 'git', status: 'pending', verb: "we'll install", optional: true },
|
|
70
|
+
{ id: 'agent', label: 'Locally installed AI agents', status: 'pending', verb: "we'll detect local AI agents" },
|
|
71
|
+
{ id: 'fraim', label: 'FRAIM', status: 'pending', verb: "we'll set up FRAIM" },
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* State-derived primary-button label per R2.0. NEVER audience-derived.
|
|
76
|
+
*
|
|
77
|
+
* - "Get Started": every row is ok, OR every row is ok-or-manual-required
|
|
78
|
+
* (skip-path: user explicitly handled remaining steps via Skip-and-continue).
|
|
79
|
+
* Clicking "Get Started" shows the user-type selection screen (#367).
|
|
80
|
+
* - "Continue": at least one row is ok and there is still wizard work to do.
|
|
81
|
+
* - "Set up FRAIM": no rows ok yet.
|
|
82
|
+
*/
|
|
83
|
+
function derivePrimaryButtonLabel(rows) {
|
|
84
|
+
// Optional rows (e.g. git) never block wizard progression.
|
|
85
|
+
const required = rows.filter((row) => !row.optional);
|
|
86
|
+
const allOk = required.every((row) => row.status === 'ok');
|
|
87
|
+
if (allOk)
|
|
88
|
+
return 'Get Started';
|
|
89
|
+
// Skip-path: every required row is ok-or-manual-required — nothing left for the wizard.
|
|
90
|
+
if (required.some((row) => row.status === 'ok'))
|
|
91
|
+
return 'Continue';
|
|
92
|
+
return 'Set up FRAIM';
|
|
93
|
+
}
|