trackqa 0.1.1 → 0.1.3
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 +13 -9
- package/dist/src/lib/mcpOnboarding.js +1 -104
- package/dist/tools/trackqa-connect/src/cli.js +22 -6
- package/dist/tools/trackqa-connect/src/clients/claude.js +3 -1
- package/dist/tools/trackqa-connect/src/clients/codex-verify.js +1 -1
- package/dist/tools/trackqa-connect/src/clients/codex.js +3 -1
- package/dist/tools/trackqa-connect/src/clients/gemini.js +5 -2
- package/dist/tools/trackqa-connect/src/clients/vscode.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,19 +7,23 @@ credentials. The connector does not install or authenticate the AI client itself
|
|
|
7
7
|
|
|
8
8
|
## Package status and installation
|
|
9
9
|
|
|
10
|
-
**trackqa@0.1.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
**trackqa@0.1.2** was published and independently verified on November 5, 2026.
|
|
11
|
+
It corrected default Codex profile and application-data validation, but separate
|
|
12
|
+
executable checks still reject user-installed clients when launched from the home.
|
|
13
|
+
The local **v0.1.3 candidate** corrects those executable checks and Gemini's default
|
|
14
|
+
home validation, and recognizes Gemini's bundled npm entry alongside its older
|
|
15
|
+
layout. Existing installation allowlists, workspace boundaries and compatibility
|
|
16
|
+
checks remain enforced. **v0.1.3 is not published.** Existing published artifacts
|
|
17
|
+
are retained unchanged; experimental client validation limits are unchanged.
|
|
14
18
|
|
|
15
19
|
To verify an approved local tarball in a disposable directory:
|
|
16
20
|
|
|
17
21
|
```powershell
|
|
18
|
-
npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.
|
|
22
|
+
npm install --ignore-scripts --no-audit --no-fund <absolute-path-to-trackqa-0.1.3.tgz>
|
|
19
23
|
npm exec --offline -- trackqa --help
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
The official hosted workflow is:
|
|
23
27
|
|
|
24
28
|
```powershell
|
|
25
29
|
npx trackqa connect
|
|
@@ -29,9 +33,9 @@ npx trackqa status --client codex
|
|
|
29
33
|
npx trackqa disconnect --client codex --project <project-UUID>
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
Until v0.1.
|
|
33
|
-
|
|
34
|
-
through a local tarball installation
|
|
36
|
+
The published version is v0.1.2. Until v0.1.3 is published, run from your coding
|
|
37
|
+
project folder instead of the user home to avoid these location rejections.
|
|
38
|
+
Validate v0.1.3 through a disposable local tarball installation.
|
|
35
39
|
Bare `connect` prompts for a client; `--client codex` is optional. No client is
|
|
36
40
|
selected by default.
|
|
37
41
|
|
|
@@ -7,12 +7,7 @@ export const mcpTools = [
|
|
|
7
7
|
{ name: 'trackqa_get_issue', description: 'Read an issue in that project.' },
|
|
8
8
|
{ name: 'trackqa_submit_finding', description: 'Submit a potential bug for human QA/Owner review.' },
|
|
9
9
|
];
|
|
10
|
-
|
|
11
|
-
codex: 'https://developers.openai.com/codex/mcp/',
|
|
12
|
-
claude: 'https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcpjson',
|
|
13
|
-
};
|
|
14
|
-
// These builders never accept a token. UUID-derived identifiers avoid project
|
|
15
|
-
// name/key injection and keep credentials for different projects independent.
|
|
10
|
+
// UUID-derived identifiers keep project configurations independent.
|
|
16
11
|
export function mcpSetup(supabaseUrl, projectId) {
|
|
17
12
|
let url;
|
|
18
13
|
try {
|
|
@@ -29,61 +24,6 @@ export function mcpSetup(supabaseUrl, projectId) {
|
|
|
29
24
|
return { endpoint: `${url.origin}/functions/v1/trackqa-mcp`, serverName: `trackqa_${id}`,
|
|
30
25
|
envVar: `TRACKQA_MCP_${id.toUpperCase()}_TOKEN`, projectId: projectId.toLowerCase() };
|
|
31
26
|
}
|
|
32
|
-
function verifySetup(setup) {
|
|
33
|
-
try {
|
|
34
|
-
const expected = mcpSetup(new URL(setup.endpoint).origin, setup.projectId);
|
|
35
|
-
if (setup.endpoint === expected.endpoint && setup.serverName === expected.serverName && setup.envVar === expected.envVar)
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
catch { /* Return only a fixed configuration error. */ }
|
|
39
|
-
throw new Error('Client setup is unavailable.');
|
|
40
|
-
}
|
|
41
|
-
export function clientConfig(setup, client) {
|
|
42
|
-
verifySetup(setup);
|
|
43
|
-
if (!['codex', 'claude', 'generic'].includes(client))
|
|
44
|
-
throw new Error('Client setup is unavailable.');
|
|
45
|
-
if (client === 'generic')
|
|
46
|
-
return null; // There is no universal client config schema.
|
|
47
|
-
if (client === 'claude')
|
|
48
|
-
return JSON.stringify({ mcpServers: { [setup.serverName]: {
|
|
49
|
-
type: 'http', url: setup.endpoint, headers: { Authorization: 'Bearer ${' + setup.envVar + '}' },
|
|
50
|
-
} } }, null, 2);
|
|
51
|
-
return `[mcp_servers.${setup.serverName}]\nurl = ${JSON.stringify(setup.endpoint)}\nbearer_token_env_var = ${JSON.stringify(setup.envVar)}\nenabled_tools = [${mcpTools.map(tool => JSON.stringify(tool.name)).join(', ')}]`;
|
|
52
|
-
}
|
|
53
|
-
export function clientLaunch(setup, client, terminal) {
|
|
54
|
-
verifySetup(setup);
|
|
55
|
-
// Only fixed executable names and validated variable identifiers enter code.
|
|
56
|
-
if (!/^TRACKQA_MCP_[0-9A-F]{32}_TOKEN$/.test(setup.envVar)
|
|
57
|
-
|| !['codex', 'claude'].includes(client) || !['powershell', 'bash'].includes(terminal)) {
|
|
58
|
-
throw new Error('Client setup is unavailable.');
|
|
59
|
-
}
|
|
60
|
-
const variable = setup.envVar;
|
|
61
|
-
if (terminal === 'bash')
|
|
62
|
-
return `(
|
|
63
|
-
set +x
|
|
64
|
-
trap 'unset ${variable}' EXIT
|
|
65
|
-
if ! read -r -s -p 'TrackQA token (hidden): ' ${variable}; then exit 1; fi
|
|
66
|
-
printf '\\n'
|
|
67
|
-
if [[ ! "$${variable}" =~ ^tqm_[0-9a-f]{64}$ ]]; then printf 'Invalid TrackQA token format.\\n' >&2; exit 1; fi
|
|
68
|
-
export ${variable}
|
|
69
|
-
${client}
|
|
70
|
-
)`;
|
|
71
|
-
return `& {
|
|
72
|
-
$ErrorActionPreference = 'Stop'
|
|
73
|
-
$secureToken = Read-Host 'TrackQA token (hidden)' -AsSecureString
|
|
74
|
-
$tokenPointer = [IntPtr]::Zero
|
|
75
|
-
try {
|
|
76
|
-
$tokenPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)
|
|
77
|
-
$env:${variable} = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPointer)
|
|
78
|
-
if ($env:${variable} -notmatch '^tqm_[0-9a-f]{64}$') { throw 'Invalid TrackQA token format.' }
|
|
79
|
-
${client}
|
|
80
|
-
} finally {
|
|
81
|
-
Remove-Item Env:${variable} -ErrorAction SilentlyContinue
|
|
82
|
-
if ($tokenPointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPointer) }
|
|
83
|
-
$secureToken.Dispose()
|
|
84
|
-
}
|
|
85
|
-
}`;
|
|
86
|
-
}
|
|
87
27
|
export function connectionStatus(connection, enabled, archived, memberPresent, now) {
|
|
88
28
|
if (connection.revoked_at)
|
|
89
29
|
return 'Revoked';
|
|
@@ -98,46 +38,3 @@ export function connectionStatus(connection, enabled, archived, memberPresent, n
|
|
|
98
38
|
return 'Project disabled';
|
|
99
39
|
return 'Ready to connect';
|
|
100
40
|
}
|
|
101
|
-
export function watchTokenReveal(clear, documentTarget, windowTarget) {
|
|
102
|
-
const hide = () => { if (documentTarget.hidden)
|
|
103
|
-
clear(); };
|
|
104
|
-
const timer = windowTarget.setTimeout(clear, 120_000);
|
|
105
|
-
documentTarget.addEventListener('visibilitychange', hide);
|
|
106
|
-
windowTarget.addEventListener('pagehide', clear);
|
|
107
|
-
return () => {
|
|
108
|
-
windowTarget.clearTimeout(timer);
|
|
109
|
-
documentTarget.removeEventListener('visibilitychange', hide);
|
|
110
|
-
windowTarget.removeEventListener('pagehide', clear);
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
const issuanceError = () => new Error('Token issuance could not be completed. Refresh connections and revoke any new entry before trying again.');
|
|
114
|
-
// The existing human-authenticated RPC remains authoritative. No retry, cache,
|
|
115
|
-
// persistence, diagnostics, or token-bearing return value is introduced here.
|
|
116
|
-
export async function issueConnectionToken(call, scope, label, reveal) {
|
|
117
|
-
let rows = null;
|
|
118
|
-
try {
|
|
119
|
-
if (!scope.isOwner || !scope.enabled || scope.archived || !uuid.test(scope.projectId)
|
|
120
|
-
|| !uuid.test(scope.userId) || !scope.memberIds.includes(scope.userId)
|
|
121
|
-
|| !label.trim() || label.trim().length > 120)
|
|
122
|
-
throw issuanceError();
|
|
123
|
-
const result = await call({ p_project_id: scope.projectId, p_user_id: scope.userId, p_client_label: label.trim() });
|
|
124
|
-
rows = result.data;
|
|
125
|
-
if (result.error || !Array.isArray(rows) || rows.length !== 1 || !uuid.test(rows[0]?.connection_id)
|
|
126
|
-
|| !/^tqm_[0-9a-f]{64}$/.test(rows[0]?.access_token))
|
|
127
|
-
throw issuanceError();
|
|
128
|
-
const connectionId = rows[0].connection_id;
|
|
129
|
-
const displayed = reveal({ connectionId, token: rows[0].access_token });
|
|
130
|
-
return { connectionId, displayed };
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
throw issuanceError();
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
// Discard the SDK response's one-time secret after handing it to the local
|
|
137
|
-
// reveal dialog. It must never enter connection metadata or a query cache.
|
|
138
|
-
if (Array.isArray(rows))
|
|
139
|
-
for (const row of rows)
|
|
140
|
-
if (row && typeof row === 'object')
|
|
141
|
-
row.access_token = '';
|
|
142
|
-
}
|
|
143
|
-
}
|
|
@@ -16,7 +16,7 @@ import { adapterLabel } from "./clients/adapter.js";
|
|
|
16
16
|
import { ConnectorError, display, parseArgs, requireValue } from "./core.js";
|
|
17
17
|
import { Store } from "./state.js";
|
|
18
18
|
import { connect, disconnect, status } from "./workflow.js";
|
|
19
|
-
export const help = `TrackQA Connect 0.1.
|
|
19
|
+
export const help = `TrackQA Connect 0.1.3 - Windows, Owner-operated MVP release candidate
|
|
20
20
|
Usage after installing the approved package or local tarball:
|
|
21
21
|
trackqa connect --client codex
|
|
22
22
|
trackqa status --client codex
|
|
@@ -39,21 +39,37 @@ Advanced: TRACKQA_PUBLIC_KEY or VITE_SUPABASE_ANON_KEY overrides the public key
|
|
|
39
39
|
No bearer token is saved. The existing 90-day token expiry is unchanged; session cleanup attempts and independently verifies revocation.
|
|
40
40
|
Status is local-only: it does not authorize or test a live MCP connection.
|
|
41
41
|
Disconnect may require fresh Google Owner sign-in. Never pass credentials as arguments.`;
|
|
42
|
+
const samePath = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
43
|
+
const under = (target, base) => {
|
|
44
|
+
const relative = path.relative(path.resolve(base).toLowerCase(), path.resolve(target).toLowerCase());
|
|
45
|
+
return relative === '' || (relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative));
|
|
46
|
+
};
|
|
47
|
+
export function isSafeConfigurationRoot(target, cwd, repo, home, defaultRoot) {
|
|
48
|
+
if (![target, cwd, repo, home, defaultRoot].every(value => path.isAbsolute(value)))
|
|
49
|
+
return false;
|
|
50
|
+
// Home is also a common shell launch directory. Exempt only the exact
|
|
51
|
+
// default root there, never a workspace-local override or the package tree.
|
|
52
|
+
return !under(target, repo) && (!under(target, cwd) || (samePath(cwd, home) && samePath(target, defaultRoot)));
|
|
53
|
+
}
|
|
42
54
|
export async function locations(env = process.env, client = 'codex') {
|
|
43
55
|
const codexHome = env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
44
56
|
if (client === 'codex')
|
|
45
57
|
requireValue(path.isAbsolute(codexHome), 'configuration_location_untrusted');
|
|
46
58
|
const canonical = await realpath(codexHome).catch(() => path.resolve(codexHome));
|
|
47
59
|
const repo = fileURLToPath(new URL('../../../', import.meta.url));
|
|
48
|
-
const
|
|
60
|
+
const cwd = await realpath(process.cwd());
|
|
61
|
+
const home = await realpath(os.homedir()).catch(() => path.resolve(os.homedir()));
|
|
49
62
|
if (client === 'codex')
|
|
50
|
-
requireValue(
|
|
63
|
+
requireValue(isSafeConfigurationRoot(canonical, cwd, repo, home, path.join(home, '.codex')), 'configuration_location_untrusted');
|
|
51
64
|
const data = process.platform === 'win32' ? env.LOCALAPPDATA : env.XDG_STATE_HOME ?? path.join(os.homedir(), '.local', 'state');
|
|
52
|
-
requireValue(data && path.isAbsolute(data)
|
|
65
|
+
requireValue(data && path.isAbsolute(data), 'recovery_unavailable');
|
|
66
|
+
const canonicalData = await realpath(data).catch(() => path.resolve(data));
|
|
67
|
+
const defaultData = process.platform === 'win32' ? path.join(home, 'AppData', 'Local') : path.join(home, '.local', 'state');
|
|
68
|
+
requireValue(isSafeConfigurationRoot(canonicalData, cwd, repo, home, defaultData), 'recovery_unavailable');
|
|
53
69
|
const geminiHome = env.GEMINI_CLI_HOME ?? os.homedir();
|
|
54
70
|
const geminiCanonical = await realpath(geminiHome).catch(() => path.resolve(geminiHome));
|
|
55
71
|
if (client === 'gemini') {
|
|
56
|
-
requireValue(path.isAbsolute(geminiHome) &&
|
|
72
|
+
requireValue(path.isAbsolute(geminiHome) && isSafeConfigurationRoot(geminiCanonical, cwd, repo, home, home), 'configuration_location_untrusted');
|
|
57
73
|
const config = path.join(geminiCanonical, '.gemini');
|
|
58
74
|
const resolved = await realpath(config).catch(() => config);
|
|
59
75
|
requireValue(resolved.toLowerCase() === config.toLowerCase(), 'configuration_location_untrusted');
|
|
@@ -217,7 +233,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
217
233
|
if (code === 'configuration_write_failed' || code === 'configuration_write_uncertain')
|
|
218
234
|
process.stdout.write('Settings replacement failed or its outcome is uncertain. Do not retry automatically. Inspect settings.json and any .trackqa-settings-recovery-*.bak in the selected Gemini home; preserve recovery metadata and compare/merge before restoring anything.\n');
|
|
219
235
|
if (code === 'client_not_found')
|
|
220
|
-
process.stdout.write('Selected client was not found in supported installation locations.
|
|
236
|
+
process.stdout.write('Selected AI client was not found in supported installation locations. Install it independently or use the matching --codex-path, --gemini-path, --claude-path or --vscode-path option for an existing supported installation. No installation or login was attempted.\n');
|
|
221
237
|
if (code === 'client_not_found' && args.includes('claude'))
|
|
222
238
|
process.stdout.write('Claude Code requires an independently installed native Windows claude.exe; npm shims and Desktop are not selected. Use --claude-path for an existing supported native installation. No login or installation was attempted.\n');
|
|
223
239
|
if (code === 'gemini_compatibility_unverified')
|
|
@@ -12,7 +12,9 @@ export function trustedClaudeLocation(file, home = os.homedir(), platform = proc
|
|
|
12
12
|
if (!paths.isAbsolute(file) || paths.basename(file).toLowerCase() !== 'claude.exe' || platform !== 'win32')
|
|
13
13
|
return false;
|
|
14
14
|
const within = (root) => paths.resolve(file).toLowerCase().startsWith(paths.resolve(root).toLowerCase() + paths.sep);
|
|
15
|
-
|
|
15
|
+
const workspace = paths.resolve(cwd).toLowerCase(), relative = paths.relative(workspace, paths.resolve(file).toLowerCase());
|
|
16
|
+
const inWorkspace = relative === '' || (relative !== '..' && !relative.startsWith('..' + paths.sep) && !paths.isAbsolute(relative));
|
|
17
|
+
if (inWorkspace && workspace !== paths.resolve(home).toLowerCase())
|
|
16
18
|
return false;
|
|
17
19
|
const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
|
|
18
20
|
return [paths.join(home, '.local', 'bin'), paths.join(home, '.local', 'share', 'claude', 'versions'),
|
|
@@ -63,7 +63,7 @@ export async function verifyCodex(executable, codexHome, setup, token, signal, s
|
|
|
63
63
|
child.stdin.write(JSON.stringify({ id, method, params }) + '\n');
|
|
64
64
|
});
|
|
65
65
|
try {
|
|
66
|
-
await rpc('initialize', { clientInfo: { name: 'trackqa_connect', version: '0.1.
|
|
66
|
+
await rpc('initialize', { clientInfo: { name: 'trackqa_connect', version: '0.1.3' } });
|
|
67
67
|
child.stdin.write(JSON.stringify({ method: 'initialized', params: {} }) + '\n');
|
|
68
68
|
const config = await rpc('config/read');
|
|
69
69
|
const servers = config?.config?.mcp_servers;
|
|
@@ -9,7 +9,9 @@ export function trustedLocation(file, home = os.homedir(), platform = process.pl
|
|
|
9
9
|
const paths = platform === 'win32' ? path.win32 : path.posix;
|
|
10
10
|
const normalized = paths.resolve(file).toLowerCase();
|
|
11
11
|
const within = (root) => normalized.startsWith(paths.resolve(root).toLowerCase() + paths.sep);
|
|
12
|
-
|
|
12
|
+
const workspace = paths.resolve(cwd).toLowerCase(), relative = paths.relative(workspace, normalized);
|
|
13
|
+
const inWorkspace = relative === '' || (relative !== '..' && !relative.startsWith('..' + paths.sep) && !paths.isAbsolute(relative));
|
|
14
|
+
if (!paths.isAbsolute(file) || (inWorkspace && workspace !== paths.resolve(home).toLowerCase()))
|
|
13
15
|
return false;
|
|
14
16
|
if (platform === 'win32') {
|
|
15
17
|
if (paths.basename(file).toLowerCase() !== 'codex.exe')
|
|
@@ -16,7 +16,9 @@ export function trustedGeminiLocation(file, home = os.homedir(), platform = proc
|
|
|
16
16
|
return false;
|
|
17
17
|
const normalized = paths.resolve(file).toLowerCase();
|
|
18
18
|
const inside = (root) => normalized.startsWith(paths.resolve(root).toLowerCase() + paths.sep);
|
|
19
|
-
|
|
19
|
+
const workspace = paths.resolve(cwd).toLowerCase(), relative = paths.relative(workspace, normalized);
|
|
20
|
+
const inWorkspace = relative === '' || (relative !== '..' && !relative.startsWith('..' + paths.sep) && !paths.isAbsolute(relative));
|
|
21
|
+
if (inWorkspace && workspace !== paths.resolve(home).toLowerCase())
|
|
20
22
|
return false;
|
|
21
23
|
const roots = platform === 'win32' ? [paths.join(home, 'AppData', 'Roaming', 'npm', 'node_modules', '@google', 'gemini-cli'),
|
|
22
24
|
paths.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'nodejs', 'node_modules', '@google', 'gemini-cli')]
|
|
@@ -187,7 +189,8 @@ export async function probeGemini(exe, run = capture) {
|
|
|
187
189
|
}
|
|
188
190
|
export async function detectGemini(explicit, source = process.env, run = capture, inspect = checkGeminiInstallation) {
|
|
189
191
|
const prefixes = [...new Set([...(source.PATH ?? '').split(path.delimiter), source.APPDATA ? path.join(source.APPDATA, 'npm') : '', path.join(source.ProgramFiles ?? 'C:\\Program Files', 'nodejs')])].filter(folder => path.isAbsolute(folder));
|
|
190
|
-
const candidates = explicit ? [explicit] : prefixes.
|
|
192
|
+
const candidates = explicit ? [explicit] : prefixes.flatMap(folder => ['dist/index.js', 'bundle/gemini.js']
|
|
193
|
+
.map(bin => path.join(folder, 'node_modules', '@google', 'gemini-cli', bin)));
|
|
191
194
|
let incompatible = false;
|
|
192
195
|
for (const file of candidates) {
|
|
193
196
|
let exe;
|
|
@@ -10,7 +10,9 @@ export function trustedVSCodeLocation(file, home = os.homedir(), platform = proc
|
|
|
10
10
|
if (platform !== 'win32')
|
|
11
11
|
return false;
|
|
12
12
|
const paths = path.win32, resolved = paths.resolve(file).toLowerCase();
|
|
13
|
-
|
|
13
|
+
const workspace = paths.resolve(cwd).toLowerCase(), relative = paths.relative(workspace, resolved);
|
|
14
|
+
const inWorkspace = relative === '' || (relative !== '..' && !relative.startsWith('..' + paths.sep) && !paths.isAbsolute(relative));
|
|
15
|
+
if (!paths.isAbsolute(file) || paths.basename(file).toLowerCase() !== 'code.exe' || (inWorkspace && workspace !== paths.resolve(home).toLowerCase()))
|
|
14
16
|
return false;
|
|
15
17
|
return [paths.join(home, 'AppData', 'Local', 'Programs', 'Microsoft VS Code', 'Code.exe'),
|
|
16
18
|
paths.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Microsoft VS Code', 'Code.exe')].some(candidate => paths.resolve(candidate).toLowerCase() === resolved);
|