fraim-hub 2.0.335 → 2.0.338
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/registry/scripts/learning/entry-format.js +27 -0
- package/dist/registry/scripts/learning/learning-domains.json +78 -0
- package/dist/src/ai-hub/attachment-store.js +35 -2
- package/dist/src/ai-hub/desktop-main.js +5 -0
- package/dist/src/ai-hub/managed-browser.js +130 -5
- package/dist/src/ai-hub/native-voice-input.js +141 -0
- package/dist/src/ai-hub/server.js +90 -33
- package/dist/src/cli/utils/pack-home.js +4 -2
- package/dist/src/config/learning-domains.js +10 -113
- package/dist/src/core/utils/local-registry-resolver.js +6 -0
- package/package.json +6 -2
- package/public/ai-hub/index.html +26 -16
- package/public/ai-hub/script.js +282 -455
- package/public/ai-hub/styles.css +47 -115
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Read the shared author's current entry format for identity and update guards.
|
|
4
|
+
function learningEntryBlocks(body) {
|
|
5
|
+
const headings = [...body.matchAll(/^#{2,6}\s+\[P-[A-Z]+\].*$/gm)];
|
|
6
|
+
return headings.map((heading, index) => {
|
|
7
|
+
const start = heading.index;
|
|
8
|
+
const next = headings[index + 1]?.index ?? body.length;
|
|
9
|
+
const ownDepth = heading[0].match(/^#+/)[0].length;
|
|
10
|
+
const afterHeading = start + heading[0].length;
|
|
11
|
+
const shallower = [...body.slice(afterHeading, next).matchAll(/^(#{1,6})\s+.*$/gm)].find(m => m[1].length <= ownDepth);
|
|
12
|
+
const limit = shallower ? afterHeading + shallower.index : next;
|
|
13
|
+
const text = body.slice(start, limit).trimEnd();
|
|
14
|
+
return { start, end: start + text.length, text, id: text.match(/^\*\*Id\*\*:\s*(L-[a-f0-9]{10})\s*$/m)?.[1] ?? null };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function learningEntryMetadata(content) {
|
|
19
|
+
const metadata = {};
|
|
20
|
+
for (const match of content.matchAll(/^\*\*(Recurrences|First recorded|First synthesized)\*\*:\s*([^\r\n]+)$/gm)) {
|
|
21
|
+
if (metadata[match[1]] !== undefined && metadata[match[1]] !== match[2]) throw new Error('entry-metadata-conflict');
|
|
22
|
+
metadata[match[1]] = match[2];
|
|
23
|
+
}
|
|
24
|
+
return metadata;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { learningEntryMetadata, learningEntryBlocks };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"mapVersion": 1,
|
|
4
|
+
"domains": [
|
|
5
|
+
"engineering",
|
|
6
|
+
"product",
|
|
7
|
+
"go-to-market",
|
|
8
|
+
"people-ops",
|
|
9
|
+
"finance",
|
|
10
|
+
"legal",
|
|
11
|
+
"compliance",
|
|
12
|
+
"customer",
|
|
13
|
+
"company-building",
|
|
14
|
+
"operations"
|
|
15
|
+
],
|
|
16
|
+
"jobDomainMap": {
|
|
17
|
+
"product-building": "engineering",
|
|
18
|
+
"dev-ops": "engineering",
|
|
19
|
+
"quality-assurance": "engineering",
|
|
20
|
+
"migration": "engineering",
|
|
21
|
+
"replicate": "engineering",
|
|
22
|
+
"reviewer": "engineering",
|
|
23
|
+
"security": "engineering",
|
|
24
|
+
"delivery-ops": "engineering",
|
|
25
|
+
"salesforce": "engineering",
|
|
26
|
+
"ai-engineering": "engineering",
|
|
27
|
+
"product-management": "product",
|
|
28
|
+
"customer-development": "product",
|
|
29
|
+
"brainstorming": "product",
|
|
30
|
+
"marketing": "go-to-market",
|
|
31
|
+
"branding": "go-to-market",
|
|
32
|
+
"sales": "go-to-market",
|
|
33
|
+
"go-to-market": "go-to-market",
|
|
34
|
+
"business-development": "go-to-market",
|
|
35
|
+
"commercial-ops": "go-to-market",
|
|
36
|
+
"people-ops": "people-ops",
|
|
37
|
+
"inclusion-ops": "people-ops",
|
|
38
|
+
"career": "people-ops",
|
|
39
|
+
"finance": "finance",
|
|
40
|
+
"invoicing": "finance",
|
|
41
|
+
"banking": "finance",
|
|
42
|
+
"legal": "legal",
|
|
43
|
+
"compliance": "compliance",
|
|
44
|
+
"customer-success": "customer",
|
|
45
|
+
"customer-communication": "customer",
|
|
46
|
+
"company-creation": "company-building",
|
|
47
|
+
"bootstrap": "company-building",
|
|
48
|
+
"fundraising": "company-building",
|
|
49
|
+
"business-ops": "operations",
|
|
50
|
+
"procurement": "operations",
|
|
51
|
+
"project-management": "operations"
|
|
52
|
+
},
|
|
53
|
+
"globalJobCategories": [
|
|
54
|
+
"manager",
|
|
55
|
+
"fraim",
|
|
56
|
+
"1-1",
|
|
57
|
+
"coaching",
|
|
58
|
+
"delegation",
|
|
59
|
+
"hiring",
|
|
60
|
+
"productivity",
|
|
61
|
+
"project-setup",
|
|
62
|
+
"setup-guardrails",
|
|
63
|
+
"team-learning",
|
|
64
|
+
"verification"
|
|
65
|
+
],
|
|
66
|
+
"domainVoice": {
|
|
67
|
+
"engineering": "Precise and technical; file paths, commands, and test names are appropriate.",
|
|
68
|
+
"product": "Product and user-outcome language; behavior and flow, not code symbols.",
|
|
69
|
+
"go-to-market": "Marketing and sales language; campaigns, funnels, messaging, audience.",
|
|
70
|
+
"people-ops": "Plain HR and recruiting language; no engineering jargon.",
|
|
71
|
+
"finance": "Finance and accounting language; figures, close, forecasts.",
|
|
72
|
+
"legal": "Contract and IP language; obligations, clauses, risk.",
|
|
73
|
+
"compliance": "Controls, evidence, and regulation language.",
|
|
74
|
+
"customer": "Customer-relationship and support language.",
|
|
75
|
+
"company-building": "Founder and operations language; setup, entity, funding.",
|
|
76
|
+
"operations": "Business-operations language; process, vendors, planning, coordination."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -30,7 +63,6 @@ exports.safeAttachmentMetaPath = safeAttachmentMetaPath;
|
|
|
30
63
|
* path its own file-read tool opens.
|
|
31
64
|
*/
|
|
32
65
|
const path_1 = __importDefault(require("path"));
|
|
33
|
-
const sharp_1 = __importDefault(require("sharp"));
|
|
34
66
|
/** Accepted upload MIME types (spec R1/R11). */
|
|
35
67
|
exports.ACCEPTED_ATTACHMENT_MIME_TYPES = new Set([
|
|
36
68
|
'image/png',
|
|
@@ -90,8 +122,9 @@ function pdfContainsScriptMarker(buffer) {
|
|
|
90
122
|
async function sanitizeAttachmentBuffer(buffer, mimeType) {
|
|
91
123
|
if (IMAGE_MIME_TYPES.has(mimeType)) {
|
|
92
124
|
try {
|
|
125
|
+
const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
|
|
93
126
|
const format = mimeType === 'image/jpeg' ? 'jpeg' : mimeType === 'image/gif' ? 'gif' : 'png';
|
|
94
|
-
return await (
|
|
127
|
+
return await sharp(buffer).toFormat(format).toBuffer();
|
|
95
128
|
}
|
|
96
129
|
catch (err) {
|
|
97
130
|
throw new AttachmentRejectedError(415, `Not a valid ${mimeType} image.`);
|
|
@@ -11,6 +11,7 @@ const electron_1 = require("electron");
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const server_1 = require("./server");
|
|
14
|
+
const native_voice_input_1 = require("./native-voice-input");
|
|
14
15
|
const cert_store_1 = require("./cert-store");
|
|
15
16
|
const office_sideload_1 = require("./office-sideload");
|
|
16
17
|
const office_setup_policy_1 = require("./office-setup-policy");
|
|
@@ -672,6 +673,10 @@ async function launchDesktopShell(options) {
|
|
|
672
673
|
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
|
|
673
674
|
},
|
|
674
675
|
restartToLatest: restartToLatestNow,
|
|
676
|
+
voiceInput: (0, native_voice_input_1.createNativeVoiceInputController)({
|
|
677
|
+
getWindow: () => mainWindow,
|
|
678
|
+
sendActionToFirstResponder: (action) => electron_1.Menu.sendActionToFirstResponder(action),
|
|
679
|
+
}),
|
|
675
680
|
hubMainDiagnostics: {
|
|
676
681
|
snapshot: () => (0, hub_main_diagnostics_1.getHubMainDiagnosticsSnapshot)(buildDesktopDebugSnapshot(runtimeId)),
|
|
677
682
|
},
|
|
@@ -110,15 +110,27 @@ function buildBrowserLaunchArgs(opts) {
|
|
|
110
110
|
* baked into any registry job or skill. Returns '' when the shared browser is not
|
|
111
111
|
* available (so the note only appears when there is something to connect to).
|
|
112
112
|
*/
|
|
113
|
-
function buildBrowserContextNote(cdpEndpoint, hubBaseUrl) {
|
|
113
|
+
function buildBrowserContextNote(cdpEndpoint, hubBaseUrl, conversationId, page) {
|
|
114
114
|
if (!cdpEndpoint || !hubBaseUrl)
|
|
115
115
|
return '';
|
|
116
|
+
const trimmedConversationId = typeof conversationId === 'string' ? conversationId.trim() : '';
|
|
117
|
+
const pageGuidance = trimmedConversationId
|
|
118
|
+
? [
|
|
119
|
+
`Browser work for this Hub conversation must use the conversation page for \`${trimmedConversationId}\`, not the globally active tab.`,
|
|
120
|
+
`Before using a browser, ensure it is running and get this conversation's page: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` with JSON body \`{"conversationId":"${trimmedConversationId}"}\` (idempotent - it reuses the running browser and this conversation's page, or relaunches/recreates them if they were closed).`,
|
|
121
|
+
page?.targetId
|
|
122
|
+
? `Current allocated page target: \`${page.targetId}\`${page.webSocketDebuggerUrl ? ` (${page.webSocketDebuggerUrl})` : ''}.`
|
|
123
|
+
: 'Use the returned `page.targetId` / `page.webSocketDebuggerUrl` from that endpoint when your browser tooling lets you choose a CDP target.',
|
|
124
|
+
]
|
|
125
|
+
: [
|
|
126
|
+
`Before using a browser, ensure it is running: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` (idempotent - it reuses the running browser, or relaunches it if it was closed).`,
|
|
127
|
+
];
|
|
116
128
|
return [
|
|
117
129
|
'',
|
|
118
|
-
'[FRAIM shared browser] A persistent shared browser is available
|
|
119
|
-
|
|
120
|
-
`Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and
|
|
121
|
-
'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in
|
|
130
|
+
'[FRAIM shared browser] A persistent shared browser is available; use it instead of launching your own.',
|
|
131
|
+
...pageGuidance,
|
|
132
|
+
`Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and use the conversation page target instead of an arbitrary existing tab.`,
|
|
133
|
+
'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in; the session persists to your next turn.',
|
|
122
134
|
].join('\n');
|
|
123
135
|
}
|
|
124
136
|
/** Default persistent profile directory — sandboxed from the user's daily browser. */
|
|
@@ -168,6 +180,7 @@ class ManagedBrowser {
|
|
|
168
180
|
this.child = null;
|
|
169
181
|
this.ownsProcess = false;
|
|
170
182
|
this.resolved = null;
|
|
183
|
+
this.conversationPages = new Map();
|
|
171
184
|
this.opts = opts;
|
|
172
185
|
this.channel = opts.channel || 'auto';
|
|
173
186
|
this.port = opts.port || DEFAULT_PORT;
|
|
@@ -180,6 +193,11 @@ class ManagedBrowser {
|
|
|
180
193
|
probe() {
|
|
181
194
|
return this.opts.probeFn ? this.opts.probeFn(this.cdpEndpoint()) : probeCdpEndpoint(this.cdpEndpoint());
|
|
182
195
|
}
|
|
196
|
+
async cdpRequest(requestPath, method = 'GET') {
|
|
197
|
+
if (this.opts.cdpRequestFn)
|
|
198
|
+
return this.opts.cdpRequestFn(requestPath, method);
|
|
199
|
+
return cdpJsonRequest(this.cdpEndpoint(), requestPath, method);
|
|
200
|
+
}
|
|
183
201
|
/** Live check — does a Chromium CDP endpoint answer on the port right now. */
|
|
184
202
|
isRunning() {
|
|
185
203
|
return this.probe();
|
|
@@ -232,6 +250,71 @@ class ManagedBrowser {
|
|
|
232
250
|
await this.waitForRunning();
|
|
233
251
|
return { endpoint: this.cdpEndpoint(), reused: false, executable: exe.path, channel: exe.channel };
|
|
234
252
|
}
|
|
253
|
+
async listPageTargets() {
|
|
254
|
+
const raw = await this.cdpRequest('/json/list');
|
|
255
|
+
if (!Array.isArray(raw))
|
|
256
|
+
return [];
|
|
257
|
+
return raw
|
|
258
|
+
.filter((target) => Boolean(target && typeof target === 'object' && typeof target.id === 'string'))
|
|
259
|
+
.filter((target) => !target.type || target.type === 'page');
|
|
260
|
+
}
|
|
261
|
+
pageFromTarget(conversationId, target) {
|
|
262
|
+
return {
|
|
263
|
+
conversationId,
|
|
264
|
+
targetId: target.id,
|
|
265
|
+
url: target.url || 'about:blank',
|
|
266
|
+
webSocketDebuggerUrl: target.webSocketDebuggerUrl || null,
|
|
267
|
+
endpoint: this.cdpEndpoint(),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async ensureConversationPage(conversationId, startUrl = 'about:blank') {
|
|
271
|
+
const id = conversationId.trim();
|
|
272
|
+
if (!id)
|
|
273
|
+
throw new Error('conversationId is required for browser page allocation.');
|
|
274
|
+
await this.start();
|
|
275
|
+
const existing = this.conversationPages.get(id);
|
|
276
|
+
const targets = await this.listPageTargets();
|
|
277
|
+
if (existing && targets.some((target) => target.id === existing.targetId)) {
|
|
278
|
+
try {
|
|
279
|
+
await this.cdpRequest(`/json/activate/${encodeURIComponent(existing.targetId)}`);
|
|
280
|
+
}
|
|
281
|
+
catch { /* best effort */ }
|
|
282
|
+
return existing;
|
|
283
|
+
}
|
|
284
|
+
const created = await this.cdpRequest(`/json/new?${encodeURIComponent(startUrl)}`, 'PUT');
|
|
285
|
+
if (!created || typeof created.id !== 'string') {
|
|
286
|
+
throw new Error('Browser did not return a page target for the conversation.');
|
|
287
|
+
}
|
|
288
|
+
const page = this.pageFromTarget(id, created);
|
|
289
|
+
this.conversationPages.set(id, page);
|
|
290
|
+
try {
|
|
291
|
+
await this.cdpRequest(`/json/activate/${encodeURIComponent(page.targetId)}`);
|
|
292
|
+
}
|
|
293
|
+
catch { /* best effort */ }
|
|
294
|
+
return page;
|
|
295
|
+
}
|
|
296
|
+
async releaseConversationPage(conversationId) {
|
|
297
|
+
const id = conversationId.trim();
|
|
298
|
+
if (!id)
|
|
299
|
+
return { released: false, conversationId: id, targetId: null };
|
|
300
|
+
const existing = this.conversationPages.get(id);
|
|
301
|
+
this.conversationPages.delete(id);
|
|
302
|
+
if (!existing)
|
|
303
|
+
return { released: false, conversationId: id, targetId: null };
|
|
304
|
+
if (await this.probe()) {
|
|
305
|
+
try {
|
|
306
|
+
await this.cdpRequest(`/json/close/${encodeURIComponent(existing.targetId)}`);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// The user may have closed the tab or browser manually. Dropping the
|
|
310
|
+
// mapping is the important cleanup invariant.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { released: true, conversationId: id, targetId: existing.targetId };
|
|
314
|
+
}
|
|
315
|
+
conversationPage(conversationId) {
|
|
316
|
+
return this.conversationPages.get(conversationId.trim()) || null;
|
|
317
|
+
}
|
|
235
318
|
async waitForRunning() {
|
|
236
319
|
const timeoutMs = this.opts.waitTimeoutMs ?? 15000;
|
|
237
320
|
const intervalMs = this.opts.waitIntervalMs ?? 250;
|
|
@@ -263,7 +346,49 @@ class ManagedBrowser {
|
|
|
263
346
|
channel: this.resolved?.channel || this.channel,
|
|
264
347
|
userDataDir: this.userDataDir,
|
|
265
348
|
executable: this.resolved?.path || null,
|
|
349
|
+
conversationPages: Array.from(this.conversationPages.values()).map((page) => ({
|
|
350
|
+
conversationId: page.conversationId,
|
|
351
|
+
targetId: page.targetId,
|
|
352
|
+
})),
|
|
266
353
|
};
|
|
267
354
|
}
|
|
268
355
|
}
|
|
269
356
|
exports.ManagedBrowser = ManagedBrowser;
|
|
357
|
+
function cdpJsonRequest(endpoint, requestPath, method) {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
let url;
|
|
360
|
+
try {
|
|
361
|
+
url = new URL(requestPath, endpoint);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
reject(error);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const req = http_1.default.request(url, { method, timeout: 3000 }, (res) => {
|
|
368
|
+
let body = '';
|
|
369
|
+
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
370
|
+
res.on('end', () => {
|
|
371
|
+
if ((res.statusCode || 500) >= 400) {
|
|
372
|
+
reject(new Error(`CDP ${method} ${requestPath} failed with HTTP ${res.statusCode}`));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!body.trim()) {
|
|
376
|
+
resolve({});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
resolve(JSON.parse(body));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
resolve(body);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
req.on('error', reject);
|
|
388
|
+
req.on('timeout', () => {
|
|
389
|
+
req.destroy();
|
|
390
|
+
reject(new Error(`CDP ${method} ${requestPath} timed out.`));
|
|
391
|
+
});
|
|
392
|
+
req.end();
|
|
393
|
+
});
|
|
394
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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.WINDOWS_VOICE_INPUT_PROGRAM = exports.NativeVoiceInputError = void 0;
|
|
7
|
+
exports.getVoiceInputCapability = getVoiceInputCapability;
|
|
8
|
+
exports.requestWindowsVoiceInput = requestWindowsVoiceInput;
|
|
9
|
+
exports.createNativeVoiceInputController = createNativeVoiceInputController;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
class NativeVoiceInputError extends Error {
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(code);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'NativeVoiceInputError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.NativeVoiceInputError = NativeVoiceInputError;
|
|
20
|
+
function getVoiceInputCapability(platform = process.platform, native = false) {
|
|
21
|
+
if (platform === 'win32') {
|
|
22
|
+
return {
|
|
23
|
+
platform: 'windows', mode: native ? 'native' : 'manual', shortcutLabel: 'Win+H',
|
|
24
|
+
guidance: 'Press Win+H to start Windows Voice Typing. If it does not appear, check Windows Settings > Privacy & security > Speech and Microphone.',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (platform === 'darwin') {
|
|
28
|
+
return {
|
|
29
|
+
platform: 'macos', mode: native ? 'native' : 'manual', shortcutLabel: 'Dictation shortcut',
|
|
30
|
+
guidance: 'Use the microphone key or your configured Dictation shortcut. If Dictation does not appear, enable it in System Settings > Keyboard > Dictation.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { platform: 'other', mode: 'unsupported', guidance: 'Voice input is available through system dictation on Windows and macOS.' };
|
|
34
|
+
}
|
|
35
|
+
// Fixed program only: no renderer or request data enters the native command.
|
|
36
|
+
// The 32-byte union and individually paced events were validated in the #1844 spike.
|
|
37
|
+
exports.WINDOWS_VOICE_INPUT_PROGRAM = String.raw `$ErrorActionPreference = 'Stop'
|
|
38
|
+
$source = @'
|
|
39
|
+
using System;
|
|
40
|
+
using System.ComponentModel;
|
|
41
|
+
using System.Runtime.InteropServices;
|
|
42
|
+
public static class FraimVoiceTyping {
|
|
43
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
44
|
+
private struct INPUT { public uint type; public InputUnion data; }
|
|
45
|
+
[StructLayout(LayoutKind.Explicit, Size = 32)]
|
|
46
|
+
private struct InputUnion { [FieldOffset(0)] public KEYBDINPUT keyboard; }
|
|
47
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
48
|
+
private struct KEYBDINPUT {
|
|
49
|
+
public ushort virtualKey; public ushort scanCode; public uint flags;
|
|
50
|
+
public uint time; public UIntPtr extraInfo;
|
|
51
|
+
}
|
|
52
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
53
|
+
private static extern uint SendInput(uint count, INPUT[] inputs, int size);
|
|
54
|
+
private static bool Emit(ushort key, uint flags) {
|
|
55
|
+
INPUT input = new INPUT { type = 1, data = new InputUnion {
|
|
56
|
+
keyboard = new KEYBDINPUT { virtualKey = key, flags = flags }
|
|
57
|
+
}};
|
|
58
|
+
return SendInput(1, new INPUT[] { input }, Marshal.SizeOf(typeof(INPUT))) == 1;
|
|
59
|
+
}
|
|
60
|
+
private static void Send(ushort key, uint flags) {
|
|
61
|
+
if (!Emit(key, flags)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
62
|
+
}
|
|
63
|
+
public static int Invoke() {
|
|
64
|
+
bool winDown = false, hDown = false;
|
|
65
|
+
try {
|
|
66
|
+
Send(0x5B, 0); winDown = true;
|
|
67
|
+
System.Threading.Thread.Sleep(40);
|
|
68
|
+
Send(0x48, 0); hDown = true;
|
|
69
|
+
System.Threading.Thread.Sleep(40);
|
|
70
|
+
Send(0x48, 2); hDown = false;
|
|
71
|
+
System.Threading.Thread.Sleep(40);
|
|
72
|
+
Send(0x5B, 2); winDown = false;
|
|
73
|
+
return 4;
|
|
74
|
+
} finally {
|
|
75
|
+
// Release each key independently even after partial emission.
|
|
76
|
+
if (hDown) Emit(0x48, 2);
|
|
77
|
+
if (winDown) Emit(0x5B, 2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
'@
|
|
82
|
+
Add-Type -TypeDefinition $source -Language CSharp
|
|
83
|
+
Write-Output ([FraimVoiceTyping]::Invoke())`;
|
|
84
|
+
function requestWindowsVoiceInput(execute = node_child_process_1.execFile) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const fail = () => reject(new NativeVoiceInputError('launch_failed'));
|
|
87
|
+
try {
|
|
88
|
+
execute(node_path_1.default.win32.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(exports.WINDOWS_VOICE_INPUT_PROGRAM, 'utf16le').toString('base64')],
|
|
89
|
+
// execFile kills only its child on timeout and calls back after close.
|
|
90
|
+
{ windowsHide: true, timeout: 5000, maxBuffer: 4096, encoding: 'utf8' }, (error, stdout) => {
|
|
91
|
+
if (error || stdout.trim() !== '4')
|
|
92
|
+
fail();
|
|
93
|
+
else
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
fail();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function createNativeVoiceInputController(options) {
|
|
103
|
+
const capability = getVoiceInputCapability(options.platform ?? process.platform, true);
|
|
104
|
+
let inFlight = false;
|
|
105
|
+
return {
|
|
106
|
+
capability,
|
|
107
|
+
async start() {
|
|
108
|
+
if (capability.mode === 'unsupported')
|
|
109
|
+
throw new NativeVoiceInputError('unsupported_platform');
|
|
110
|
+
if (inFlight)
|
|
111
|
+
throw new NativeVoiceInputError('launch_failed');
|
|
112
|
+
inFlight = true;
|
|
113
|
+
try {
|
|
114
|
+
try {
|
|
115
|
+
const win = options.getWindow();
|
|
116
|
+
if (!win || win.isDestroyed() || win.webContents.isDestroyed())
|
|
117
|
+
throw new Error('unavailable');
|
|
118
|
+
win.show();
|
|
119
|
+
win.focus();
|
|
120
|
+
win.webContents.focus();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new NativeVoiceInputError('window_unavailable');
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
if (capability.platform === 'windows')
|
|
127
|
+
await (options.requestWindows ?? requestWindowsVoiceInput)();
|
|
128
|
+
else
|
|
129
|
+
options.sendActionToFirstResponder('startDictation:');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new NativeVoiceInputError('launch_failed');
|
|
133
|
+
}
|
|
134
|
+
return { status: 'requested', capability, message: `System dictation requested. ${capability.guidance}` };
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
inFlight = false;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|