fraim-framework 2.0.122 → 2.0.123
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/dist/src/ai-hub/catalog.js +131 -0
- package/dist/src/ai-hub/desktop-main.js +111 -0
- package/dist/src/ai-hub/hosts.js +241 -0
- package/dist/src/ai-hub/preferences.js +55 -0
- package/dist/src/ai-hub/server.js +307 -0
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/cli/commands/hub.js +96 -0
- package/dist/src/cli/commands/setup.js +5 -5
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/user-level-sync.js +59 -0
- package/dist/src/core/quality-evidence.js +4 -0
- package/package.json +150 -146
- package/public/ai-hub/index.html +130 -0
- package/public/ai-hub/script.js +374 -0
- package/public/ai-hub/styles.css +568 -0
|
@@ -0,0 +1,131 @@
|
|
|
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.summarizeProject = summarizeProject;
|
|
7
|
+
exports.discoverEmployeeJobs = discoverEmployeeJobs;
|
|
8
|
+
exports.discoverManagerTemplates = discoverManagerTemplates;
|
|
9
|
+
exports.getAiHubCategories = getAiHubCategories;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
13
|
+
const EMPLOYEE_CATEGORIES = {
|
|
14
|
+
marketing: {
|
|
15
|
+
id: 'marketing',
|
|
16
|
+
label: 'Marketing',
|
|
17
|
+
description: 'Content, campaign, and creative production jobs.',
|
|
18
|
+
},
|
|
19
|
+
'go-to-market': {
|
|
20
|
+
id: 'go-to-market',
|
|
21
|
+
label: 'GTM',
|
|
22
|
+
description: 'Launch, distribution, and growth execution jobs.',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const MANAGER_GROUPS = {
|
|
26
|
+
coaching: 'Coaching',
|
|
27
|
+
delegation: 'Delegation',
|
|
28
|
+
};
|
|
29
|
+
const sectionValue = (content, heading) => {
|
|
30
|
+
const match = content.match(new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?:\\r?\\n## |\\r?\\n---|$)`));
|
|
31
|
+
if (!match)
|
|
32
|
+
return [];
|
|
33
|
+
return match[1]
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map((line) => line.replace(/^[-*]\s*/, '').trim());
|
|
38
|
+
};
|
|
39
|
+
const humanizeJobName = (jobName) => jobName
|
|
40
|
+
.split('-')
|
|
41
|
+
.map((part) => (part.length > 0 ? `${part[0].toUpperCase()}${part.slice(1)}` : part))
|
|
42
|
+
.join(' ');
|
|
43
|
+
const toPosix = (value) => value.replace(/\\/g, '/');
|
|
44
|
+
function parseJobStub(filePath, categoryId, projectPath) {
|
|
45
|
+
const content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
46
|
+
const fileName = path_1.default.basename(filePath, '.md');
|
|
47
|
+
const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
|
|
48
|
+
const outcome = sectionValue(content, 'Outcome');
|
|
49
|
+
return {
|
|
50
|
+
id: fileName,
|
|
51
|
+
title: humanizeJobName(fileName),
|
|
52
|
+
categoryId,
|
|
53
|
+
categoryLabel: EMPLOYEE_CATEGORIES[categoryId].label,
|
|
54
|
+
intent,
|
|
55
|
+
outcome,
|
|
56
|
+
stubPath: toPosix(path_1.default.relative(projectPath, filePath)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function parseManagerStub(filePath, groupId, projectPath) {
|
|
60
|
+
const content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
61
|
+
const fileName = path_1.default.basename(filePath, '.md');
|
|
62
|
+
const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
|
|
63
|
+
return {
|
|
64
|
+
id: fileName,
|
|
65
|
+
title: humanizeJobName(fileName),
|
|
66
|
+
groupId,
|
|
67
|
+
groupLabel: MANAGER_GROUPS[groupId],
|
|
68
|
+
intent,
|
|
69
|
+
stubPath: toPosix(path_1.default.relative(projectPath, filePath)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const readMarkdownFiles = (dirPath) => {
|
|
73
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
74
|
+
return [];
|
|
75
|
+
return fs_1.default
|
|
76
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
77
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
78
|
+
.map((entry) => path_1.default.join(dirPath, entry.name))
|
|
79
|
+
.sort((a, b) => a.localeCompare(b));
|
|
80
|
+
};
|
|
81
|
+
function summarizeProject(projectPath) {
|
|
82
|
+
if (!projectPath) {
|
|
83
|
+
return {
|
|
84
|
+
path: '',
|
|
85
|
+
exists: false,
|
|
86
|
+
hasFraim: false,
|
|
87
|
+
message: 'Choose a project path to load FRAIM jobs.',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!fs_1.default.existsSync(projectPath)) {
|
|
91
|
+
return {
|
|
92
|
+
path: projectPath,
|
|
93
|
+
exists: false,
|
|
94
|
+
hasFraim: false,
|
|
95
|
+
message: 'Project path does not exist on this machine.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
|
|
99
|
+
if (!fs_1.default.existsSync(fraimDir)) {
|
|
100
|
+
return {
|
|
101
|
+
path: projectPath,
|
|
102
|
+
exists: true,
|
|
103
|
+
hasFraim: false,
|
|
104
|
+
message: 'This folder does not contain a local fraim/ directory.',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
path: projectPath,
|
|
109
|
+
exists: true,
|
|
110
|
+
hasFraim: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function discoverEmployeeJobs(projectPath) {
|
|
114
|
+
const project = summarizeProject(projectPath);
|
|
115
|
+
if (!project.exists || !project.hasFraim)
|
|
116
|
+
return [];
|
|
117
|
+
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
|
|
118
|
+
return Object.keys(EMPLOYEE_CATEGORIES)
|
|
119
|
+
.flatMap((categoryId) => readMarkdownFiles(path_1.default.join(fraimDir, 'ai-employee', 'jobs', categoryId)).map((filePath) => parseJobStub(filePath, categoryId, projectPath)));
|
|
120
|
+
}
|
|
121
|
+
function discoverManagerTemplates(projectPath) {
|
|
122
|
+
const project = summarizeProject(projectPath);
|
|
123
|
+
if (!project.exists || !project.hasFraim)
|
|
124
|
+
return [];
|
|
125
|
+
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
|
|
126
|
+
return Object.keys(MANAGER_GROUPS)
|
|
127
|
+
.flatMap((groupId) => readMarkdownFiles(path_1.default.join(fraimDir, 'ai-manager', 'jobs', groupId)).map((filePath) => parseManagerStub(filePath, groupId, projectPath)));
|
|
128
|
+
}
|
|
129
|
+
function getAiHubCategories() {
|
|
130
|
+
return Object.values(EMPLOYEE_CATEGORIES);
|
|
131
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.launchDesktopShell = launchDesktopShell;
|
|
4
|
+
const electron_1 = require("electron");
|
|
5
|
+
const server_1 = require("./server");
|
|
6
|
+
let server = null;
|
|
7
|
+
let mainWindow = null;
|
|
8
|
+
let stopping = null;
|
|
9
|
+
function preferredWindowSize() {
|
|
10
|
+
const primaryDisplay = electron_1.screen.getPrimaryDisplay();
|
|
11
|
+
const workArea = primaryDisplay.workAreaSize;
|
|
12
|
+
return {
|
|
13
|
+
width: Math.max(1440, Math.min(1680, workArea.width)),
|
|
14
|
+
height: Math.max(980, Math.min(1180, workArea.height)),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
let projectPath = process.cwd();
|
|
19
|
+
let preferredPort = 43091;
|
|
20
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
21
|
+
const value = argv[index];
|
|
22
|
+
if (value === '--project-path' && argv[index + 1]) {
|
|
23
|
+
projectPath = argv[index + 1];
|
|
24
|
+
index += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (value === '--port' && argv[index + 1]) {
|
|
28
|
+
preferredPort = Number(argv[index + 1]) || preferredPort;
|
|
29
|
+
index += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { projectPath, preferredPort };
|
|
33
|
+
}
|
|
34
|
+
async function stopServer() {
|
|
35
|
+
if (!server)
|
|
36
|
+
return;
|
|
37
|
+
await server.stop();
|
|
38
|
+
server = null;
|
|
39
|
+
}
|
|
40
|
+
function stopServerOnce() {
|
|
41
|
+
if (!stopping) {
|
|
42
|
+
stopping = stopServer().finally(() => {
|
|
43
|
+
stopping = null;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return stopping;
|
|
47
|
+
}
|
|
48
|
+
async function createWindow(url) {
|
|
49
|
+
const { width, height } = preferredWindowSize();
|
|
50
|
+
mainWindow = new electron_1.BrowserWindow({
|
|
51
|
+
title: 'Visa AI Hub',
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
minWidth: 1400,
|
|
55
|
+
minHeight: 960,
|
|
56
|
+
useContentSize: true,
|
|
57
|
+
autoHideMenuBar: true,
|
|
58
|
+
backgroundColor: '#f2f5fb',
|
|
59
|
+
show: false,
|
|
60
|
+
webPreferences: {
|
|
61
|
+
contextIsolation: true,
|
|
62
|
+
nodeIntegration: false,
|
|
63
|
+
sandbox: true,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
electron_1.Menu.setApplicationMenu(null);
|
|
67
|
+
mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
|
|
68
|
+
void electron_1.shell.openExternal(targetUrl);
|
|
69
|
+
return { action: 'deny' };
|
|
70
|
+
});
|
|
71
|
+
mainWindow.once('ready-to-show', () => {
|
|
72
|
+
mainWindow?.show();
|
|
73
|
+
});
|
|
74
|
+
mainWindow.on('closed', () => {
|
|
75
|
+
mainWindow = null;
|
|
76
|
+
});
|
|
77
|
+
await mainWindow.loadURL(url);
|
|
78
|
+
mainWindow.webContents.setZoomFactor(1);
|
|
79
|
+
}
|
|
80
|
+
async function launchDesktopShell(options) {
|
|
81
|
+
const port = await (0, server_1.findAvailablePort)(options.preferredPort);
|
|
82
|
+
server = new server_1.AiHubServer({ projectPath: options.projectPath });
|
|
83
|
+
await server.start(port);
|
|
84
|
+
await createWindow(`http://127.0.0.1:${port}/ai-hub/`);
|
|
85
|
+
}
|
|
86
|
+
async function bootstrap() {
|
|
87
|
+
const options = parseArgs(process.argv.slice(2));
|
|
88
|
+
await electron_1.app.whenReady();
|
|
89
|
+
electron_1.app.setName('Visa AI Hub');
|
|
90
|
+
electron_1.app.on('activate', () => {
|
|
91
|
+
if (electron_1.BrowserWindow.getAllWindows().length === 0) {
|
|
92
|
+
void launchDesktopShell(options);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
electron_1.app.on('before-quit', () => {
|
|
96
|
+
void stopServerOnce();
|
|
97
|
+
});
|
|
98
|
+
electron_1.app.on('window-all-closed', () => {
|
|
99
|
+
void stopServerOnce().finally(() => {
|
|
100
|
+
electron_1.app.quit();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
await launchDesktopShell(options);
|
|
104
|
+
}
|
|
105
|
+
if (process.versions.electron && process.type !== 'renderer') {
|
|
106
|
+
bootstrap().catch(async (error) => {
|
|
107
|
+
console.error(error instanceof Error ? error.message : error);
|
|
108
|
+
await stopServerOnce();
|
|
109
|
+
electron_1.app.exit(1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHubEvent = exports.createHubMessage = exports.FakeHostRuntime = exports.CliHostRuntime = void 0;
|
|
4
|
+
exports.detectEmployees = detectEmployees;
|
|
5
|
+
exports.buildStartPlan = buildStartPlan;
|
|
6
|
+
exports.buildContinuePlan = buildContinuePlan;
|
|
7
|
+
exports.parseHostLine = parseHostLine;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const EMPLOYEE_LABELS = {
|
|
11
|
+
codex: 'Codex',
|
|
12
|
+
claude: 'Claude Code',
|
|
13
|
+
};
|
|
14
|
+
const executableName = (command) => command;
|
|
15
|
+
function quoteWindowsArg(value) {
|
|
16
|
+
if (value.length === 0) {
|
|
17
|
+
return '""';
|
|
18
|
+
}
|
|
19
|
+
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
|
|
20
|
+
}
|
|
21
|
+
function escapeWindowsArg(value) {
|
|
22
|
+
return /[\s"]/u.test(value) ? quoteWindowsArg(value) : value;
|
|
23
|
+
}
|
|
24
|
+
function resolveHostInvocation(plan) {
|
|
25
|
+
if (process.platform !== 'win32') {
|
|
26
|
+
return plan;
|
|
27
|
+
}
|
|
28
|
+
const [command, ...args] = [plan.command, ...plan.args];
|
|
29
|
+
return {
|
|
30
|
+
command: 'cmd.exe',
|
|
31
|
+
args: ['/d', '/s', '/c', [command, ...args.map(escapeWindowsArg)].join(' ')],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const availableByVersionProbe = (command) => {
|
|
35
|
+
const invocation = resolveHostInvocation({ command, args: ['--version'] });
|
|
36
|
+
const result = (0, child_process_1.spawnSync)(invocation.command, invocation.args, {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
});
|
|
39
|
+
return result.status === 0;
|
|
40
|
+
};
|
|
41
|
+
function detectEmployees() {
|
|
42
|
+
return Object.keys(EMPLOYEE_LABELS).map((id) => {
|
|
43
|
+
const available = id === 'codex'
|
|
44
|
+
? availableByVersionProbe(executableName('codex'))
|
|
45
|
+
: availableByVersionProbe(executableName('claude'));
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
label: EMPLOYEE_LABELS[id],
|
|
49
|
+
available,
|
|
50
|
+
detail: available ? 'Installed and ready on this machine.' : 'CLI not detected on this machine.',
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function buildStartPlan(hostId, message) {
|
|
55
|
+
if (hostId === 'codex') {
|
|
56
|
+
return {
|
|
57
|
+
command: executableName('codex'),
|
|
58
|
+
args: ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'],
|
|
59
|
+
stdin: message,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
command: executableName('claude'),
|
|
64
|
+
args: ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'],
|
|
65
|
+
stdin: message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function buildContinuePlan(hostId, sessionId, message) {
|
|
69
|
+
if (hostId === 'codex') {
|
|
70
|
+
return {
|
|
71
|
+
command: executableName('codex'),
|
|
72
|
+
args: ['exec', 'resume', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', sessionId],
|
|
73
|
+
stdin: message,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
command: executableName('claude'),
|
|
78
|
+
args: ['-p', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions', '-r', sessionId],
|
|
79
|
+
stdin: message,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseHostLine(hostId, line) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed)
|
|
85
|
+
return {};
|
|
86
|
+
if (hostId === 'codex') {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(trimmed);
|
|
89
|
+
if (parsed.type === 'thread.started' && parsed.thread_id) {
|
|
90
|
+
return { sessionId: parsed.thread_id, raw: trimmed };
|
|
91
|
+
}
|
|
92
|
+
if (parsed.type === 'item.completed' && parsed.item?.type === 'agent_message' && parsed.item.text) {
|
|
93
|
+
return { message: parsed.item.text, raw: trimmed };
|
|
94
|
+
}
|
|
95
|
+
return { raw: trimmed };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { raw: trimmed };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(trimmed);
|
|
103
|
+
if (parsed.type === 'system' && parsed.session_id) {
|
|
104
|
+
return { sessionId: parsed.session_id, raw: trimmed };
|
|
105
|
+
}
|
|
106
|
+
if (parsed.type === 'assistant') {
|
|
107
|
+
const text = parsed.message?.content?.find((entry) => entry.type === 'text')?.text;
|
|
108
|
+
if (text) {
|
|
109
|
+
return { message: text, sessionId: parsed.session_id, raw: trimmed };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (parsed.type === 'result' && parsed.result) {
|
|
113
|
+
return { message: parsed.result, sessionId: parsed.session_id, raw: trimmed };
|
|
114
|
+
}
|
|
115
|
+
return { raw: trimmed };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return { raw: trimmed };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function wireHostProcess(hostId, child, handlers) {
|
|
122
|
+
const wire = (buffer, channel) => {
|
|
123
|
+
let pending = '';
|
|
124
|
+
buffer.on('data', (chunk) => {
|
|
125
|
+
pending += chunk.toString();
|
|
126
|
+
const lines = pending.split(/\r?\n/);
|
|
127
|
+
pending = lines.pop() || '';
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const parsed = parseHostLine(hostId, line);
|
|
130
|
+
if (parsed.message || parsed.sessionId || parsed.raw) {
|
|
131
|
+
handlers.onEvent(parsed, channel);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
buffer.on('end', () => {
|
|
136
|
+
if (pending.trim().length > 0) {
|
|
137
|
+
const parsed = parseHostLine(hostId, pending);
|
|
138
|
+
if (parsed.message || parsed.sessionId || parsed.raw) {
|
|
139
|
+
handlers.onEvent(parsed, channel);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
wire(child.stdout, 'stdout');
|
|
145
|
+
wire(child.stderr, 'stderr');
|
|
146
|
+
child.on('close', (code) => handlers.onExit(code));
|
|
147
|
+
return child;
|
|
148
|
+
}
|
|
149
|
+
function spawnHostProcess(hostId, plan, projectPath, handlers) {
|
|
150
|
+
const invocation = resolveHostInvocation(plan);
|
|
151
|
+
const child = (0, child_process_1.spawn)(invocation.command, invocation.args, {
|
|
152
|
+
cwd: projectPath,
|
|
153
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
154
|
+
env: process.env,
|
|
155
|
+
});
|
|
156
|
+
if (typeof plan.stdin === 'string') {
|
|
157
|
+
child.stdin.write(plan.stdin);
|
|
158
|
+
}
|
|
159
|
+
child.stdin.end();
|
|
160
|
+
return wireHostProcess(hostId, child, handlers);
|
|
161
|
+
}
|
|
162
|
+
class CliHostRuntime {
|
|
163
|
+
detectEmployees() {
|
|
164
|
+
return detectEmployees();
|
|
165
|
+
}
|
|
166
|
+
startRun(hostId, projectPath, message, handlers) {
|
|
167
|
+
return spawnHostProcess(hostId, buildStartPlan(hostId, message), projectPath, handlers);
|
|
168
|
+
}
|
|
169
|
+
continueRun(hostId, projectPath, sessionId, message, handlers) {
|
|
170
|
+
return spawnHostProcess(hostId, buildContinuePlan(hostId, sessionId, message), projectPath, handlers);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.CliHostRuntime = CliHostRuntime;
|
|
174
|
+
class FakeHostRuntime {
|
|
175
|
+
constructor() {
|
|
176
|
+
this.employees = [
|
|
177
|
+
{ id: 'codex', label: 'Codex', available: true, detail: 'Test double employee.' },
|
|
178
|
+
{ id: 'claude', label: 'Claude Code', available: true, detail: 'Test double employee.' },
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
detectEmployees() {
|
|
182
|
+
return this.employees;
|
|
183
|
+
}
|
|
184
|
+
startRun(hostId, _projectPath, message, handlers) {
|
|
185
|
+
return this.fakeProcess(hostId, `Started ${hostId}: ${message}`, handlers);
|
|
186
|
+
}
|
|
187
|
+
continueRun(hostId, _projectPath, sessionId, message, handlers) {
|
|
188
|
+
return this.fakeProcess(hostId, `Resumed ${hostId} session ${sessionId}: ${message}`, handlers);
|
|
189
|
+
}
|
|
190
|
+
fakeProcess(_hostId, text, handlers) {
|
|
191
|
+
handlers.onEvent({ sessionId: (0, crypto_1.randomUUID)(), raw: 'fake-session-start' }, 'system');
|
|
192
|
+
handlers.onEvent({ message: text, raw: text }, 'stdout');
|
|
193
|
+
setTimeout(() => handlers.onExit(0), 25);
|
|
194
|
+
return {
|
|
195
|
+
stdout: process.stdout,
|
|
196
|
+
stderr: process.stderr,
|
|
197
|
+
stdin: process.stdin,
|
|
198
|
+
kill: () => true,
|
|
199
|
+
on: () => ({}),
|
|
200
|
+
once: () => ({}),
|
|
201
|
+
emit: () => true,
|
|
202
|
+
addListener: () => ({}),
|
|
203
|
+
removeListener: () => ({}),
|
|
204
|
+
removeAllListeners: () => ({}),
|
|
205
|
+
setMaxListeners: () => ({}),
|
|
206
|
+
getMaxListeners: () => 0,
|
|
207
|
+
listeners: () => [],
|
|
208
|
+
rawListeners: () => [],
|
|
209
|
+
listenerCount: () => 0,
|
|
210
|
+
prependListener: () => ({}),
|
|
211
|
+
prependOnceListener: () => ({}),
|
|
212
|
+
eventNames: () => [],
|
|
213
|
+
pid: 0,
|
|
214
|
+
connected: false,
|
|
215
|
+
disconnect: () => undefined,
|
|
216
|
+
exitCode: 0,
|
|
217
|
+
killed: false,
|
|
218
|
+
signalCode: null,
|
|
219
|
+
spawnargs: [],
|
|
220
|
+
spawnfile: '',
|
|
221
|
+
stdio: [],
|
|
222
|
+
unref: () => undefined,
|
|
223
|
+
ref: () => undefined,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
exports.FakeHostRuntime = FakeHostRuntime;
|
|
228
|
+
const createHubMessage = (role, text) => ({
|
|
229
|
+
id: (0, crypto_1.randomUUID)(),
|
|
230
|
+
role,
|
|
231
|
+
text,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
});
|
|
234
|
+
exports.createHubMessage = createHubMessage;
|
|
235
|
+
const createHubEvent = (channel, text) => ({
|
|
236
|
+
id: (0, crypto_1.randomUUID)(),
|
|
237
|
+
channel,
|
|
238
|
+
text,
|
|
239
|
+
createdAt: new Date().toISOString(),
|
|
240
|
+
});
|
|
241
|
+
exports.createHubEvent = createHubEvent;
|
|
@@ -0,0 +1,55 @@
|
|
|
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.AiHubPreferencesStore = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
10
|
+
const DEFAULT_CATEGORY = 'marketing';
|
|
11
|
+
const DEFAULT_EMPLOYEE = 'codex';
|
|
12
|
+
const defaultPreferences = (projectPath) => ({
|
|
13
|
+
projectPath,
|
|
14
|
+
employeeId: DEFAULT_EMPLOYEE,
|
|
15
|
+
categoryId: DEFAULT_CATEGORY,
|
|
16
|
+
recentJobIds: [],
|
|
17
|
+
});
|
|
18
|
+
class AiHubPreferencesStore {
|
|
19
|
+
constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-state.json')) {
|
|
20
|
+
this.stateFilePath = stateFilePath;
|
|
21
|
+
}
|
|
22
|
+
load(projectPath) {
|
|
23
|
+
if (!fs_1.default.existsSync(this.stateFilePath)) {
|
|
24
|
+
return defaultPreferences(projectPath);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
28
|
+
return {
|
|
29
|
+
projectPath: raw.projectPath || projectPath,
|
|
30
|
+
employeeId: raw.employeeId === 'claude' ? 'claude' : DEFAULT_EMPLOYEE,
|
|
31
|
+
categoryId: raw.categoryId === 'go-to-market' ? 'go-to-market' : DEFAULT_CATEGORY,
|
|
32
|
+
recentJobIds: Array.isArray(raw.recentJobIds) ? raw.recentJobIds.filter((value) => typeof value === 'string') : [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return defaultPreferences(projectPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
save(preferences) {
|
|
40
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
|
|
41
|
+
fs_1.default.writeFileSync(this.stateFilePath, JSON.stringify(preferences, null, 2));
|
|
42
|
+
}
|
|
43
|
+
remember(preferences, jobId) {
|
|
44
|
+
const recentJobIds = jobId
|
|
45
|
+
? [jobId, ...preferences.recentJobIds.filter((value) => value !== jobId)].slice(0, 8)
|
|
46
|
+
: preferences.recentJobIds.slice(0, 8);
|
|
47
|
+
const next = {
|
|
48
|
+
...preferences,
|
|
49
|
+
recentJobIds,
|
|
50
|
+
};
|
|
51
|
+
this.save(next);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.AiHubPreferencesStore = AiHubPreferencesStore;
|