trackqa 0.0.0-stage → 0.1.0
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 +161 -2
- package/dist/src/lib/mcpOnboarding.js +143 -0
- package/dist/tools/mcp-pilot/google-pkce.mjs +244 -0
- package/dist/tools/trackqa-connect/src/auth.js +153 -0
- package/dist/tools/trackqa-connect/src/cli.js +246 -0
- package/dist/tools/trackqa-connect/src/clients/adapter.js +3 -0
- package/dist/tools/trackqa-connect/src/clients/claude-session.js +78 -0
- package/dist/tools/trackqa-connect/src/clients/claude.js +113 -0
- package/dist/tools/trackqa-connect/src/clients/codex-verify.js +110 -0
- package/dist/tools/trackqa-connect/src/clients/codex.js +191 -0
- package/dist/tools/trackqa-connect/src/clients/gemini-config.js +141 -0
- package/dist/tools/trackqa-connect/src/clients/gemini-verify.js +14 -0
- package/dist/tools/trackqa-connect/src/clients/gemini-windows.js +134 -0
- package/dist/tools/trackqa-connect/src/clients/gemini.js +249 -0
- package/dist/tools/trackqa-connect/src/clients/vscode-copilot-lifecycle.js +97 -0
- package/dist/tools/trackqa-connect/src/clients/vscode-copilot-session.js +115 -0
- package/dist/tools/trackqa-connect/src/clients/vscode-copilot.js +59 -0
- package/dist/tools/trackqa-connect/src/clients/vscode-process.js +123 -0
- package/dist/tools/trackqa-connect/src/clients/vscode-session.js +145 -0
- package/dist/tools/trackqa-connect/src/clients/vscode.js +123 -0
- package/dist/tools/trackqa-connect/src/core.js +87 -0
- package/dist/tools/trackqa-connect/src/process.js +76 -0
- package/dist/tools/trackqa-connect/src/state.js +80 -0
- package/dist/tools/trackqa-connect/src/testing/copilot-fixture-scope.js +103 -0
- package/dist/tools/trackqa-connect/src/workflow.js +241 -0
- package/package.json +43 -4
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { parseEnv } from 'node:util';
|
|
4
|
+
import { googlePkceSignIn, GooglePkceError } from '../../mcp-pilot/google-pkce.mjs';
|
|
5
|
+
import { boundedJson, ConnectorError, requireValue, SUPABASE_URL, UUID } from "./core.js";
|
|
6
|
+
import { fixtureMembershipRoute, fixtureRequestAllowed, fixtureScopeState, observeFixtureResponse } from "./testing/copilot-fixture-scope.js";
|
|
7
|
+
export function isPublicKey(key) {
|
|
8
|
+
if (/^sb_publishable_[A-Za-z0-9_-]+$/.test(key))
|
|
9
|
+
return true;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(Buffer.from(key.split('.')[1], 'base64url').toString()).role === 'anon';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function publicKey(env = process.env) {
|
|
18
|
+
let value = env.TRACKQA_PUBLIC_KEY ?? env.VITE_SUPABASE_ANON_KEY;
|
|
19
|
+
// Local development only: read the existing app's public key, never load its
|
|
20
|
+
// other variables into process.env or pass them to the coding client.
|
|
21
|
+
if (!value) {
|
|
22
|
+
for (const file of ['.env.local', '.env']) {
|
|
23
|
+
try {
|
|
24
|
+
const text = await readFile(new URL('../../../' + file, import.meta.url), 'utf8');
|
|
25
|
+
const line = text.split(/\r?\n/).find(line => /^\s*VITE_SUPABASE_ANON_KEY\s*=/.test(line));
|
|
26
|
+
if (line)
|
|
27
|
+
value = parseEnv(line).VITE_SUPABASE_ANON_KEY;
|
|
28
|
+
if (value)
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
catch { /* No secret or filesystem error details leave this function. */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
requireValue(value && value.length <= 8192 && isPublicKey(value), 'public_key_required');
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
// The CLI receives only a checked human API closure. Raw Auth tokens and the SDK
|
|
38
|
+
// client never reach the client adapter or the recovery store.
|
|
39
|
+
export async function withOwner(options, action) {
|
|
40
|
+
requireValue(isPublicKey(options.key), 'public_key_required');
|
|
41
|
+
if (options.fixtureScope)
|
|
42
|
+
fixtureScopeState(options.fixtureScope);
|
|
43
|
+
const posts = new Set();
|
|
44
|
+
const send = options.fetch ?? fetch;
|
|
45
|
+
const authFetch = async (input, init = {}) => {
|
|
46
|
+
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
|
|
47
|
+
const method = init.method ?? 'GET';
|
|
48
|
+
const allowed = method === 'GET' ? url.pathname === '/auth/v1/user' && !url.search
|
|
49
|
+
: method === 'POST' && ((url.pathname === '/auth/v1/token' && url.search === '?grant_type=pkce')
|
|
50
|
+
|| (url.pathname === '/auth/v1/logout' && url.search === '?scope=local'));
|
|
51
|
+
requireValue(url.origin === SUPABASE_URL && allowed, 'authentication_failed');
|
|
52
|
+
if (method === 'POST') {
|
|
53
|
+
requireValue(!posts.has(url.pathname), 'authentication_failed');
|
|
54
|
+
posts.add(url.pathname);
|
|
55
|
+
}
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await send(url.href, { ...init, signal: AbortSignal.timeout(10_000), redirect: 'error', credentials: 'omit', cache: 'no-store' });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new ConnectorError('authentication_failed');
|
|
62
|
+
}
|
|
63
|
+
// Bound error responses too; only the SDK sees their bodies, never output.
|
|
64
|
+
if (response.status === 204)
|
|
65
|
+
return new Response(null, { status: 204 });
|
|
66
|
+
try {
|
|
67
|
+
const data = await boundedJson(new Response(response.body, { status: 200 }), 65_536);
|
|
68
|
+
return Response.json(data, { status: response.status, headers: response.headers });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new ConnectorError('authentication_failed');
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await response.body?.cancel().catch(() => { });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const memory = new Map();
|
|
78
|
+
const client = (options.makeClient ?? createClient)(SUPABASE_URL, options.key, {
|
|
79
|
+
auth: { flowType: 'pkce', persistSession: false, autoRefreshToken: false, detectSessionInUrl: false, debug: false,
|
|
80
|
+
storage: { getItem: (key) => memory.get(key) ?? null, setItem: (key, value) => { memory.set(key, value); }, removeItem: (key) => { memory.delete(key); } } },
|
|
81
|
+
db: { retry: false }, global: { fetch: authFetch },
|
|
82
|
+
});
|
|
83
|
+
let login;
|
|
84
|
+
let authenticated = false;
|
|
85
|
+
let result;
|
|
86
|
+
let failure;
|
|
87
|
+
let access = '';
|
|
88
|
+
try {
|
|
89
|
+
requireValue(!options.signal?.aborted, 'interrupted');
|
|
90
|
+
login = await (options.signIn ?? googlePkceSignIn)(client, { signal: options.signal });
|
|
91
|
+
authenticated = Boolean(login?.data?.session);
|
|
92
|
+
requireValue(authenticated && !login?.error, 'authentication_failed');
|
|
93
|
+
const verified = await client.auth.getUser();
|
|
94
|
+
const user = verified.data?.user;
|
|
95
|
+
requireValue(!verified.error && user && UUID.test(user.id) && user.id === login?.data?.user?.id, 'identity_unverified');
|
|
96
|
+
requireValue(!user.factors?.some(factor => factor.status === 'verified'), 'mfa_unsupported');
|
|
97
|
+
access = login.data.session.access_token;
|
|
98
|
+
const api = async (route, method = 'GET', body) => {
|
|
99
|
+
// Fixed project endpoint; no arbitrary targets, redirects or privileged key.
|
|
100
|
+
const url = new URL(route, SUPABASE_URL);
|
|
101
|
+
requireValue(url.origin === SUPABASE_URL && !url.username && !url.password && route.startsWith('/rest/v1/'), 'request_failed');
|
|
102
|
+
const resource = url.pathname.slice('/rest/v1/'.length);
|
|
103
|
+
requireValue(options.fixtureScope ? fixtureRequestAllowed(options.fixtureScope, user.id, route, method, body) : method === 'GET' ? ['profiles', 'projects', 'project_members', 'mcp_connections'].includes(resource)
|
|
104
|
+
: method === 'PATCH' ? resource === 'projects' && !!url.searchParams.get('id') && JSON.stringify(body) === '{"mcp_enabled":true}'
|
|
105
|
+
: ['rpc/issue_mcp_connection_token', 'rpc/revoke_mcp_connection'].includes(resource), 'request_failed');
|
|
106
|
+
try {
|
|
107
|
+
const result = await boundedJson(await send(url.href, { method, headers: { apikey: options.key, Authorization: `Bearer ${access}`, 'Content-Type': 'application/json', Prefer: 'return=representation' },
|
|
108
|
+
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(10_000), redirect: 'error', cache: 'no-store', credentials: 'omit' }));
|
|
109
|
+
if (options.fixtureScope)
|
|
110
|
+
observeFixtureResponse(options.fixtureScope, user.id, route, result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new ConnectorError('request_failed');
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const profile = await api(`/rest/v1/profiles?id=eq.${user.id}&select=id&limit=2`);
|
|
118
|
+
requireValue(Array.isArray(profile) && profile.length === 1 && profile[0].id === user.id, 'profile_required');
|
|
119
|
+
const memberships = await api(options.fixtureScope ? fixtureMembershipRoute(user.id) : `/rest/v1/project_members?user_id=eq.${user.id}&role=eq.OWNER&select=project_id,role,project:projects(id,name,key,mcp_enabled,archived)&limit=201`);
|
|
120
|
+
requireValue(Array.isArray(memberships) && memberships.length <= 200, 'project_selection_unavailable');
|
|
121
|
+
const projects = memberships.filter(row => row.role === 'OWNER' && UUID.test(row.project_id) && row.project?.id === row.project_id
|
|
122
|
+
&& row.project.archived === false && typeof row.project.mcp_enabled === 'boolean' && typeof row.project.name === 'string' && typeof row.project.key === 'string')
|
|
123
|
+
.map(row => row.project);
|
|
124
|
+
requireValue(!options.signal?.aborted, 'interrupted');
|
|
125
|
+
result = await action({ userId: user.id, api, projects });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failure = error instanceof ConnectorError ? error : error instanceof GooglePkceError ? new ConnectorError(error.code) : new ConnectorError('authentication_failed');
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
access = '';
|
|
132
|
+
if (authenticated) {
|
|
133
|
+
try {
|
|
134
|
+
if ((await client.auth.signOut({ scope: 'local' })).error)
|
|
135
|
+
failure ??= new ConnectorError('signout_unconfirmed');
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
failure ??= new ConnectorError('signout_unconfirmed');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (login?.data.session) {
|
|
142
|
+
login.data.session.access_token = '';
|
|
143
|
+
login.data.session.refresh_token = '';
|
|
144
|
+
login.data.session.provider_token = null;
|
|
145
|
+
login.data.session.provider_refresh_token = null;
|
|
146
|
+
}
|
|
147
|
+
login = undefined;
|
|
148
|
+
memory.clear(); // Includes any verifier retained after an incomplete exchange.
|
|
149
|
+
}
|
|
150
|
+
if (failure)
|
|
151
|
+
throw failure;
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { realpath } from 'node:fs/promises';
|
|
7
|
+
import { publicKey, withOwner } from "./auth.js";
|
|
8
|
+
import { codexAdapter, detectCodex } from "./clients/codex.js";
|
|
9
|
+
import { geminiAdapter, detectGemini } from "./clients/gemini.js";
|
|
10
|
+
import { claudeAdapter, detectClaude } from "./clients/claude.js";
|
|
11
|
+
import { vscodeAdapter, detectVSCode } from "./clients/vscode.js";
|
|
12
|
+
import { vscodeCopilotAdapter } from "./clients/vscode-copilot.js";
|
|
13
|
+
import { copilotRecoveryHome, freshCopilotHome } from "./clients/vscode-copilot-session.js";
|
|
14
|
+
import { recordHome } from "./state.js";
|
|
15
|
+
import { adapterLabel } from "./clients/adapter.js";
|
|
16
|
+
import { ConnectorError, display, parseArgs, requireValue } from "./core.js";
|
|
17
|
+
import { Store } from "./state.js";
|
|
18
|
+
import { connect, disconnect, status } from "./workflow.js";
|
|
19
|
+
export const help = `TrackQA Connect 0.1.0 - Windows, Owner-operated MVP release candidate
|
|
20
|
+
Usage after installing the approved package or local tarball:
|
|
21
|
+
trackqa connect --client codex
|
|
22
|
+
trackqa status --client codex
|
|
23
|
+
trackqa disconnect --client codex
|
|
24
|
+
Intended registry workflow after publication: npx trackqa connect
|
|
25
|
+
Codex CLI: MVP-supported candidate; Windows hosted acceptance passed.
|
|
26
|
+
Gemini CLI and Claude Code: experimental; hosted validation deferred.
|
|
27
|
+
VS Code Local (--client vscode): experimental; hosted read-only acceptance passed.
|
|
28
|
+
VS Code Copilot (--client vscode-copilot): experimental; hosted read-only acceptance passed; ordinary connect remains guarded before authentication.
|
|
29
|
+
Antigravity: deferred; no adapter.
|
|
30
|
+
Optional: --project <project UUID> --codex-path <trusted absolute native executable>
|
|
31
|
+
Gemini optional: --gemini-path <installed @google/gemini-cli package bin .js>
|
|
32
|
+
Claude optional: --claude-path <trusted absolute native claude.exe>
|
|
33
|
+
VS Code optional: --vscode-path <trusted installed absolute Code.exe>; supported family 1.143.x.
|
|
34
|
+
Claude connect only: --claude-only excludes other MCP servers for this session (opt-in).
|
|
35
|
+
No client is selected by default. AI-client accounts are separate from TrackQA sign-in.
|
|
36
|
+
Requires Node >=22.19, Windows, a free localhost callback port 5173 and Google Owner authorization; MFA accounts are unsupported.
|
|
37
|
+
Set TRACKQA_PUBLIC_KEY to the deployment's public anon/publishable key. Never supply a secret or service-role key.
|
|
38
|
+
No bearer token is saved. The existing 90-day token expiry is unchanged; session cleanup attempts and independently verifies revocation.
|
|
39
|
+
Status is local-only: it does not authorize or test a live MCP connection.
|
|
40
|
+
Disconnect may require fresh Google Owner sign-in. Never pass credentials as arguments.`;
|
|
41
|
+
export async function locations(env = process.env, client = 'codex') {
|
|
42
|
+
const codexHome = env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
43
|
+
if (client === 'codex')
|
|
44
|
+
requireValue(path.isAbsolute(codexHome), 'configuration_location_untrusted');
|
|
45
|
+
const canonical = await realpath(codexHome).catch(() => path.resolve(codexHome));
|
|
46
|
+
const repo = fileURLToPath(new URL('../../../', import.meta.url));
|
|
47
|
+
const under = (target, base) => target.toLowerCase() === base.toLowerCase() || target.toLowerCase().startsWith(path.resolve(base).toLowerCase() + path.sep);
|
|
48
|
+
if (client === 'codex')
|
|
49
|
+
requireValue(!under(canonical, process.cwd()) && !under(canonical, repo), 'configuration_location_untrusted');
|
|
50
|
+
const data = process.platform === 'win32' ? env.LOCALAPPDATA : env.XDG_STATE_HOME ?? path.join(os.homedir(), '.local', 'state');
|
|
51
|
+
requireValue(data && path.isAbsolute(data) && !under(data, repo) && !under(data, process.cwd()), 'recovery_unavailable');
|
|
52
|
+
const geminiHome = env.GEMINI_CLI_HOME ?? os.homedir();
|
|
53
|
+
const geminiCanonical = await realpath(geminiHome).catch(() => path.resolve(geminiHome));
|
|
54
|
+
if (client === 'gemini') {
|
|
55
|
+
requireValue(path.isAbsolute(geminiHome) && !under(geminiCanonical, repo) && !under(geminiCanonical, process.cwd()), 'configuration_location_untrusted');
|
|
56
|
+
const config = path.join(geminiCanonical, '.gemini');
|
|
57
|
+
const resolved = await realpath(config).catch(() => config);
|
|
58
|
+
requireValue(resolved.toLowerCase() === config.toLowerCase(), 'configuration_location_untrusted');
|
|
59
|
+
}
|
|
60
|
+
return { codexHome: canonical, geminiHome: geminiCanonical, claudeHome: path.join(data, 'TrackQA-Connect', 'claude-sessions'), vscodeHome: path.join(data, 'TrackQA-Connect', 'vscode-sessions'), vscodeCopilotHome: path.join(data, 'TrackQA-Connect', 'vscode-copilot-sessions'), store: new Store(path.join(data, 'TrackQA-Connect')) };
|
|
61
|
+
}
|
|
62
|
+
const codes = new Set(`usage configuration_busy configuration_write_failed configuration_write_uncertain gemini_auto_update_enabled gemini_compatibility_unverified client_unsupported client_foreground_unsupported client_not_found executable_untrusted executable_changed public_key_required authentication_failed callback_port_in_use callback_unavailable callback_timeout callback_rejected oauth_denied oauth_failed browser_open_failed interrupted identity_unverified profile_required mfa_unsupported owner_required no_eligible_owner_projects project_unavailable project_selection_unavailable request_failed response_invalid response_limit signout_unconfirmed configuration_unreadable configuration_conflict configuration_changed configuration_home_mismatch configuration_location_untrusted installation_unconfirmed removal_unconfirmed recovery_invalid recovery_unavailable existing_connection_recover_first connector_busy_or_stale_lock enablement_unconfirmed issuance_unconfirmed token_invalid cleanup_unconfirmed no_managed_connections client_command_failed client_command_timeout client_response_limit client_launch_failed client_exit_failed verification_initialization_failed verification_discovery_failed verification_connection_status_failed verification_project_scope_failed`.split(' '));
|
|
63
|
+
export function safeCode(error) {
|
|
64
|
+
if (error instanceof ConnectorError && ['copilot_runtime_unverified', 'copilot_process_unconfirmed'].includes(error.code))
|
|
65
|
+
return error.code;
|
|
66
|
+
if (error instanceof ConnectorError && error.code === 'vscode_process_unconfirmed')
|
|
67
|
+
return error.code;
|
|
68
|
+
if (error instanceof ConnectorError && codes.has(error.code))
|
|
69
|
+
return error.code;
|
|
70
|
+
if (error instanceof Error && error.message === 'connector_busy_or_stale_lock')
|
|
71
|
+
return error.message;
|
|
72
|
+
return 'operation_failed';
|
|
73
|
+
}
|
|
74
|
+
export async function main(args = process.argv.slice(2)) {
|
|
75
|
+
// SDK diagnostics must never leak credential-bearing error objects. Only
|
|
76
|
+
// our fixed messages go to stdout. The foreground client owns its terminal.
|
|
77
|
+
const previousConsole = { ...console };
|
|
78
|
+
for (const name of ['log', 'warn', 'error', 'debug', 'info'])
|
|
79
|
+
console[name] = () => { };
|
|
80
|
+
let prompt;
|
|
81
|
+
let unlock;
|
|
82
|
+
const stop = new AbortController();
|
|
83
|
+
const interrupt = () => stop.abort();
|
|
84
|
+
process.on('SIGINT', interrupt);
|
|
85
|
+
process.on('SIGTERM', interrupt);
|
|
86
|
+
try {
|
|
87
|
+
const options = parseArgs(args);
|
|
88
|
+
if (options.command === 'help') {
|
|
89
|
+
process.stdout.write(help + '\n');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const ui = {
|
|
93
|
+
say(message) { process.stdout.write(message + '\n'); },
|
|
94
|
+
async confirm(message) {
|
|
95
|
+
requireValue(process.stdin.isTTY && process.stdout.isTTY, 'usage');
|
|
96
|
+
requireValue(!stop.signal.aborted, 'interrupted');
|
|
97
|
+
prompt ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
98
|
+
return (await prompt.question(message + ' [y/N] ', { signal: stop.signal })).trim().toLowerCase() === 'y';
|
|
99
|
+
},
|
|
100
|
+
async choose(message, choices) {
|
|
101
|
+
requireValue(process.stdin.isTTY && process.stdout.isTTY, 'usage');
|
|
102
|
+
requireValue(!stop.signal.aborted, 'interrupted');
|
|
103
|
+
ui.say(message + '\n' + choices.map((choice, index) => `${index + 1}. ${display(choice)}`).join('\n'));
|
|
104
|
+
prompt ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
const input = (await prompt.question('Selection (no default): ', { signal: stop.signal })).trim();
|
|
106
|
+
requireValue(/^[1-9][0-9]*$/.test(input) && Number(input) <= choices.length, 'usage');
|
|
107
|
+
return Number(input) - 1;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const client = options.client ?? ['codex', 'gemini', 'claude', 'vscode', 'vscode-copilot'][await ui.choose('Select a client (see validation limits)', ['Codex CLI (MVP-supported candidate)', 'Gemini CLI (experimental; hosted validation deferred)', 'Claude Code (experimental; hosted validation deferred)', 'VS Code Local (experimental; manual activation)', 'VS Code Copilot (experimental; hosted reads passed; connect guarded)'])];
|
|
111
|
+
const paths = await locations(process.env, client);
|
|
112
|
+
const copilotControls = {
|
|
113
|
+
report: ui.say,
|
|
114
|
+
async requestDisconnect(signal) {
|
|
115
|
+
requireValue(process.stdin.isTTY && process.stdout.isTTY, 'usage');
|
|
116
|
+
prompt ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
117
|
+
while (!signal.aborted) {
|
|
118
|
+
const input = await prompt.question('Type disconnect to revoke TrackQA access: ', { signal });
|
|
119
|
+
if (input.trim().toLowerCase() === 'disconnect')
|
|
120
|
+
return;
|
|
121
|
+
ui.say('Enter disconnect to end TrackQA access. No input is logged.');
|
|
122
|
+
}
|
|
123
|
+
throw new ConnectorError('interrupted');
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
let copilotHome = paths.vscodeCopilotHome;
|
|
127
|
+
if (client === 'vscode-copilot') {
|
|
128
|
+
const records = (await paths.store.read()).filter(row => row.client === client && (!options.projectId || row.projectId === options.projectId));
|
|
129
|
+
if (options.command === 'connect') {
|
|
130
|
+
requireValue(!(await paths.store.read()).some(row => row.client === client), 'existing_connection_recover_first');
|
|
131
|
+
copilotHome = freshCopilotHome(paths.vscodeCopilotHome);
|
|
132
|
+
}
|
|
133
|
+
else if (options.command === 'status') {
|
|
134
|
+
const exe = await detectVSCode(options.executable);
|
|
135
|
+
if (!records.length) {
|
|
136
|
+
await status(paths.store, vscodeCopilotAdapter(exe, copilotHome, copilotControls), ui, options.projectId);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const record of records)
|
|
140
|
+
await status(paths.store, vscodeCopilotAdapter(exe, copilotRecoveryHome(paths.vscodeCopilotHome, recordHome(record)), copilotControls), ui, record.projectId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
requireValue(records.length, 'no_managed_connections');
|
|
145
|
+
const record = records.length === 1 ? records[0] : records[await ui.choose('Choose a connection to remove', records.map(row => row.projectId))];
|
|
146
|
+
options.projectId = record.projectId;
|
|
147
|
+
copilotHome = copilotRecoveryHome(paths.vscodeCopilotHome, recordHome(record));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const adapter = client === 'vscode-copilot'
|
|
151
|
+
? vscodeCopilotAdapter(await detectVSCode(options.executable), copilotHome, copilotControls)
|
|
152
|
+
: client === 'vscode'
|
|
153
|
+
? vscodeAdapter(await detectVSCode(options.executable), paths.vscodeHome)
|
|
154
|
+
: client === 'claude'
|
|
155
|
+
? claudeAdapter(await detectClaude(options.executable), paths.claudeHome, process.env, options.claudeOnly)
|
|
156
|
+
: client === 'gemini'
|
|
157
|
+
? geminiAdapter(await detectGemini(options.executable), paths.geminiHome)
|
|
158
|
+
: codexAdapter(await detectCodex(paths.codexHome, options.executable), paths.codexHome);
|
|
159
|
+
const executable = adapter.executable;
|
|
160
|
+
const label = adapterLabel(adapter);
|
|
161
|
+
if (options.command === 'connect')
|
|
162
|
+
requireValue(executable.foregroundMode !== 'unsupported', 'client_foreground_unsupported');
|
|
163
|
+
ui.say(`${label} detected: ${executable.version}. Recovery metadata: ${display(paths.store.root)}`);
|
|
164
|
+
if (options.command === 'connect')
|
|
165
|
+
ui.say(client === 'vscode-copilot'
|
|
166
|
+
? 'Copilot hosted read-only acceptance passed through the fixture-pinned test entry. Ordinary connect remains guarded; no Owner sign-in or issuance is authorized here.'
|
|
167
|
+
: client === 'vscode'
|
|
168
|
+
? 'Isolated VS Code Local window; native MCP activation is manual. Close the entire isolated instance for cleanup. Do not use Copilot or provider harnesses. No model account authentication is automated.'
|
|
169
|
+
: `Foreground launch: ${executable.foregroundMode === 'no-daemon' ? '--no-daemon (supported by this executable; no shared background server)' : 'direct (no --no-daemon flag advertised; interactive startup is not preverified)'}.`);
|
|
170
|
+
if (options.command === 'status') {
|
|
171
|
+
await status(paths.store, adapter, ui, options.projectId);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
requireValue(process.platform === 'win32', 'client_unsupported');
|
|
175
|
+
if (options.command === 'connect')
|
|
176
|
+
await adapter.prepare?.();
|
|
177
|
+
unlock = await paths.store.lock();
|
|
178
|
+
const authenticate = async (action) => {
|
|
179
|
+
requireValue(!stop.signal.aborted, 'interrupted');
|
|
180
|
+
ui.say('Complete Google sign-in in your system browser. No password or token will be requested in this terminal.');
|
|
181
|
+
return withOwner({ key: await publicKey(), signal: stop.signal }, action);
|
|
182
|
+
};
|
|
183
|
+
if (options.command === 'connect') {
|
|
184
|
+
if (!await ui.confirm('Continue with Google Owner sign-in?'))
|
|
185
|
+
return;
|
|
186
|
+
await authenticate(owner => connect({ owner, adapter, store: paths.store, ui: {
|
|
187
|
+
...ui,
|
|
188
|
+
// Close readline before handing stdin to the foreground TUI, otherwise
|
|
189
|
+
// it may consume Codex keystrokes. Future prompts reopen it as needed.
|
|
190
|
+
say(message) { if (/^Launching (?:Codex|Gemini|Claude Code|VS Code Local)\./.test(message)) {
|
|
191
|
+
prompt?.close();
|
|
192
|
+
prompt = undefined;
|
|
193
|
+
} ui.say(message); },
|
|
194
|
+
}, projectId: options.projectId, signal: stop.signal }));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await disconnect(paths.store, adapter, ui, async (action) => { await authenticate(action); }, options.projectId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
const code = stop.signal.aborted ? 'interrupted' : safeCode(error);
|
|
203
|
+
if (code === 'copilot_runtime_unverified')
|
|
204
|
+
process.stdout.write('Copilot hosted read-only acceptance passed in the fixture-pinned test. Ordinary connect remains guarded. Launch, Owner authentication and issuance remain blocked here.\n');
|
|
205
|
+
process.stdout.write(`TrackQA connector stopped: ${code}. No credentials are included in this diagnostic.\n`);
|
|
206
|
+
if (code === 'cleanup_unconfirmed' || code === 'interrupted')
|
|
207
|
+
process.stdout.write('Run status and disconnect to inspect nonsecret recovery metadata. An Owner can revoke access in Project Settings > AI Integrations > Manage Connections. Do not assume remote access was removed.\n');
|
|
208
|
+
if (code === 'configuration_changed')
|
|
209
|
+
process.stdout.write('The client entry changed after installation. It was preserved. Review only that server entry before removing it manually.\n');
|
|
210
|
+
if (code === 'vscode_process_unconfirmed')
|
|
211
|
+
process.stdout.write('VS Code process termination is unconfirmed. Close only the TrackQA Local instance, then run disconnect --client vscode using the same project. Keep process.json and recovery metadata; do not delete them to bypass the check. New unrelated Code processes can also require a conservative refusal. No process was force-killed.\n');
|
|
212
|
+
if (code === 'configuration_busy')
|
|
213
|
+
process.stdout.write('Settings are locked by another writer or a stale .trackqa-settings.lock. Close other configuration writers. Remove only a stale lock after confirming its recorded helper PID has exited.\n');
|
|
214
|
+
if (code === 'configuration_write_failed' || code === 'configuration_write_uncertain')
|
|
215
|
+
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');
|
|
216
|
+
if (code === 'client_not_found')
|
|
217
|
+
process.stdout.write('Selected client was not found in supported installation locations. No installation was attempted. For Gemini supply the existing @google/gemini-cli bin with --gemini-path; agy.exe is not Gemini CLI.\n');
|
|
218
|
+
if (code === 'client_not_found' && args.includes('claude'))
|
|
219
|
+
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');
|
|
220
|
+
if (code === 'gemini_compatibility_unverified')
|
|
221
|
+
process.stdout.write('Gemini capability or isolated JSON editor checks failed. Native add/remove are never used on real settings. No Owner sign-in or token issuance was attempted.\n');
|
|
222
|
+
if (code === 'gemini_auto_update_enabled')
|
|
223
|
+
process.stdout.write('Disable general.enableAutoUpdate using Gemini settings before this connector launches it. Workspace/system overrides must not re-enable it. The connector did not change your preferences or upgrade the client.\n');
|
|
224
|
+
if (code === 'client_foreground_unsupported')
|
|
225
|
+
process.stdout.write('This executable supports MCP configuration but has no verified interactive foreground launch mode. Select an existing complete Codex CLI using --codex-path. No installation or interactive retry was attempted; status/disconnect remain available.\n');
|
|
226
|
+
if (code === 'client_launch_failed' || code === 'client_exit_failed')
|
|
227
|
+
process.stdout.write('Client foreground startup or exit failed; MCP verification is separate from TUI availability. Review the client diagnostic above and the cleanup result. No automatic relaunch was attempted.\n');
|
|
228
|
+
if (code === 'connector_busy_or_stale_lock')
|
|
229
|
+
process.stdout.write('Another session may be running. Inspect connector.lock in the displayed recovery folder. Remove only that lock after independently confirming its recorded process has exited.\n');
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
prompt?.close();
|
|
233
|
+
try {
|
|
234
|
+
await unlock?.();
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
process.stdout.write('Recovery lock cleanup unconfirmed; inspect nonsecret connector.lock before another attempt.\n');
|
|
239
|
+
}
|
|
240
|
+
process.removeListener('SIGINT', interrupt);
|
|
241
|
+
process.removeListener('SIGTERM', interrupt);
|
|
242
|
+
Object.assign(console, previousConsole);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
246
|
+
await main();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { lstat, mkdir, open, realpath, rmdir, unlink } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ConnectorError, requireValue, setupFor, UUID } from "../core.js";
|
|
4
|
+
import { fingerprint } from "./codex.js";
|
|
5
|
+
import { commitWindowsSettings } from "./gemini-windows.js";
|
|
6
|
+
// Reuse the existing private Windows file writer, not Gemini's settings editor.
|
|
7
|
+
// These files live only in the connector recovery directory, never Claude's home.
|
|
8
|
+
export const claudeEntry = (setup) => ({ type: 'http', url: setup.endpoint, headers: { Authorization: `Bearer \${${setup.envVar}}` } });
|
|
9
|
+
export function claudeSessionPath(home, setup) {
|
|
10
|
+
requireValue(path.isAbsolute(home) && UUID.test(setup.projectId)
|
|
11
|
+
&& fingerprint(setup) === fingerprint(setupFor(setup.projectId, 'claude')), 'configuration_location_untrusted');
|
|
12
|
+
return path.join(home, setup.projectId, 'settings.json');
|
|
13
|
+
}
|
|
14
|
+
const payload = (setup) => Buffer.from(JSON.stringify({ mcpServers: { [setup.serverName]: claudeEntry(setup) } }, null, 2) + '\n');
|
|
15
|
+
export const matchesClaudeEntry = (value, setup) => !!value && fingerprint(value) === fingerprint(claudeEntry(setup));
|
|
16
|
+
async function directory(home, setup, create = false) {
|
|
17
|
+
const file = claudeSessionPath(home, setup);
|
|
18
|
+
for (const dir of [home, path.dirname(file)]) {
|
|
19
|
+
if (create)
|
|
20
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
21
|
+
try {
|
|
22
|
+
const info = await lstat(dir);
|
|
23
|
+
requireValue(info.isDirectory() && !info.isSymbolicLink()
|
|
24
|
+
&& (await realpath(dir)).toLowerCase() === path.resolve(dir).toLowerCase(), 'configuration_location_untrusted');
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code !== 'ENOENT')
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
export async function readClaudeSession(home, setup) {
|
|
34
|
+
const file = await directory(home, setup);
|
|
35
|
+
try {
|
|
36
|
+
const info = await lstat(file);
|
|
37
|
+
requireValue(info.isFile() && !info.isSymbolicLink() && info.nlink === 1 && info.size <= 4096, 'configuration_changed');
|
|
38
|
+
const handle = await open(file, 'r');
|
|
39
|
+
try {
|
|
40
|
+
const actual = await handle.stat();
|
|
41
|
+
requireValue(actual.ino === info.ino && actual.dev === info.dev && actual.size === info.size, 'configuration_changed');
|
|
42
|
+
const bytes = await handle.readFile();
|
|
43
|
+
// Exact connector-generated bytes only: no additional servers, JSONC,
|
|
44
|
+
// duplicate keys, credentials or externally changed entries are accepted.
|
|
45
|
+
requireValue(bytes.equals(payload(setup)), 'configuration_changed');
|
|
46
|
+
return claudeEntry(setup);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await handle.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === 'ENOENT')
|
|
54
|
+
return null;
|
|
55
|
+
if (error instanceof ConnectorError)
|
|
56
|
+
throw error;
|
|
57
|
+
throw new ConnectorError('configuration_unreadable');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function installClaudeSession(home, setup, commit = commitWindowsSettings) {
|
|
61
|
+
requireValue(await readClaudeSession(home, setup) === null, 'configuration_conflict');
|
|
62
|
+
const file = await directory(home, setup, true);
|
|
63
|
+
await commit(file, { exists: false, hash: null }, payload(setup));
|
|
64
|
+
requireValue(await readClaudeSession(home, setup) !== null, 'installation_unconfirmed');
|
|
65
|
+
return fingerprint(claudeEntry(setup));
|
|
66
|
+
}
|
|
67
|
+
export async function removeClaudeSession(home, setup, hash) {
|
|
68
|
+
const entry = await readClaudeSession(home, setup);
|
|
69
|
+
if (entry === null)
|
|
70
|
+
return;
|
|
71
|
+
// A crash after file creation but before journal acknowledgement is recoverable
|
|
72
|
+
// from the exact token-free payload in this owned, project-specific location.
|
|
73
|
+
requireValue(hash === null || fingerprint(entry) === hash, 'configuration_changed');
|
|
74
|
+
const file = claudeSessionPath(home, setup);
|
|
75
|
+
await unlink(file);
|
|
76
|
+
await rmdir(path.dirname(file)); // Never recursively delete an unexpected file.
|
|
77
|
+
requireValue(await readClaudeSession(home, setup) === null, 'removal_unconfirmed');
|
|
78
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { capture, foreground } from "../process.js";
|
|
6
|
+
import { ConnectorError, requireValue, TOKEN, tools } from "../core.js";
|
|
7
|
+
import { commitWindowsSettings } from "./gemini-windows.js";
|
|
8
|
+
import { claudeSessionPath, installClaudeSession, matchesClaudeEntry, readClaudeSession, removeClaudeSession } from "./claude-session.js";
|
|
9
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export function trustedClaudeLocation(file, home = os.homedir(), platform = process.platform, cwd = process.cwd()) {
|
|
11
|
+
const paths = platform === 'win32' ? path.win32 : path.posix;
|
|
12
|
+
if (!paths.isAbsolute(file) || paths.basename(file).toLowerCase() !== 'claude.exe' || platform !== 'win32')
|
|
13
|
+
return false;
|
|
14
|
+
const within = (root) => paths.resolve(file).toLowerCase().startsWith(paths.resolve(root).toLowerCase() + paths.sep);
|
|
15
|
+
if (within(cwd) || paths.resolve(file).toLowerCase() === paths.resolve(cwd).toLowerCase())
|
|
16
|
+
return false;
|
|
17
|
+
const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
|
|
18
|
+
return [paths.join(home, '.local', 'bin'), paths.join(home, '.local', 'share', 'claude', 'versions'),
|
|
19
|
+
paths.join(programFiles, 'ClaudeCode')].some(within)
|
|
20
|
+
|| (within(paths.join(programFiles, 'WinGet', 'Packages'))
|
|
21
|
+
&& /^Anthropic\.ClaudeCode_[A-Za-z0-9._-]+[\\/]/.test(paths.relative(paths.join(programFiles, 'WinGet', 'Packages'), file)));
|
|
22
|
+
}
|
|
23
|
+
export async function checkClaudeExecutable(file) {
|
|
24
|
+
requireValue(trustedClaudeLocation(file), 'executable_untrusted');
|
|
25
|
+
const canonical = await realpath(file);
|
|
26
|
+
requireValue(trustedClaudeLocation(canonical), 'executable_untrusted');
|
|
27
|
+
const info = await lstat(canonical);
|
|
28
|
+
requireValue(info.isFile() && !info.isSymbolicLink(), 'executable_untrusted');
|
|
29
|
+
return { path: canonical, sha256: sha(await readFile(canonical)) };
|
|
30
|
+
}
|
|
31
|
+
export function claudeEnvironment(source = process.env) {
|
|
32
|
+
const env = { DISABLE_AUTOUPDATER: '1', DISABLE_UPDATES: '1' };
|
|
33
|
+
for (const key of ['PATH', 'PATHEXT', 'SystemRoot', 'SystemDrive', 'WINDIR', 'COMSPEC', 'USERPROFILE', 'HOME', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP',
|
|
34
|
+
'TERM', 'COLORTERM', 'LANG', 'LC_ALL', 'HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy',
|
|
35
|
+
'NODE_USE_SYSTEM_CA', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'CLAUDE_CONFIG_DIR', 'CLAUDE_CODE_GIT_BASH_PATH']) {
|
|
36
|
+
if (source[key] !== undefined)
|
|
37
|
+
env[key] = source[key];
|
|
38
|
+
}
|
|
39
|
+
// Claude manages its own account cache. No ambient provider keys, Owner Auth,
|
|
40
|
+
// MCP tokens, debug destinations, NODE_OPTIONS or TLS-disable settings pass.
|
|
41
|
+
return env;
|
|
42
|
+
}
|
|
43
|
+
export async function detectClaude(explicit, source = process.env, run = capture, inspect = checkClaudeExecutable) {
|
|
44
|
+
const candidates = explicit ? [explicit] : [...new Set([
|
|
45
|
+
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
|
|
46
|
+
...(source.PATH ?? '').split(path.delimiter).filter(folder => path.isAbsolute(folder)).map(folder => path.join(folder, 'claude.exe')),
|
|
47
|
+
])];
|
|
48
|
+
let incompatible = false;
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
let exe;
|
|
51
|
+
try {
|
|
52
|
+
exe = await inspect(candidate);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
if (explicit)
|
|
56
|
+
throw new ConnectorError('executable_untrusted');
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Help/version only, private disposable config, no native add/remove or login.
|
|
60
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'trackqa-claude-probe-'));
|
|
61
|
+
try {
|
|
62
|
+
const env = { ...claudeEnvironment(source), CLAUDE_CONFIG_DIR: root };
|
|
63
|
+
const version = /^(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)\s+\(Claude Code\)$/.exec((await run(exe.path, ['--version'], env, undefined, root)).trim())?.[1];
|
|
64
|
+
requireValue(version, 'client_unsupported');
|
|
65
|
+
const help = await run(exe.path, ['--help'], env, undefined, root);
|
|
66
|
+
requireValue(help.includes('--mcp-config'), 'client_unsupported');
|
|
67
|
+
requireValue(sha(await readFile(exe.path)) === exe.sha256, 'executable_changed');
|
|
68
|
+
return { ...exe, version, appServer: false, foregroundMode: 'direct', strictMcp: help.includes('--strict-mcp-config') };
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (explicit || (error instanceof ConnectorError && error.code === 'executable_changed'))
|
|
72
|
+
throw error;
|
|
73
|
+
incompatible = true;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await rm(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new ConnectorError(incompatible ? 'client_unsupported' : 'client_not_found');
|
|
80
|
+
}
|
|
81
|
+
export const claudeLaunchArgs = (file, strict = false) => ['--mcp-config', file, ...(strict ? ['--strict-mcp-config'] : [])];
|
|
82
|
+
export const claudeVerification = () => ({ authorized: null, verified: false, code: 'manual_verification_required' });
|
|
83
|
+
export function claudeInstructions(setup) {
|
|
84
|
+
return `Claude Code native header expansion, discovery and authenticated reads are not yet verified. Claude account sign-in is separate from TrackQA. Use /mcp to check this server and exactly these five tools: ${tools.join(', ')}. Call only trackqa_connection_status and trackqa_list_projects; confirm only project ${setup.projectId}. Do not submit a finding. Exit with /exit for revocation and cleanup. Manual observations do not change the connector's unverified result.`;
|
|
85
|
+
}
|
|
86
|
+
export function claudeAdapter(exe, sessionHome, source = process.env, strict = false, launchForeground = foreground, commit = commitWindowsSettings) {
|
|
87
|
+
requireValue(!strict || exe.strictMcp, 'client_unsupported');
|
|
88
|
+
const entry = (setup) => readClaudeSession(sessionHome, setup);
|
|
89
|
+
const integrity = async () => requireValue(await realpath(exe.path) === exe.path && sha(await readFile(exe.path)) === exe.sha256, 'executable_changed');
|
|
90
|
+
return {
|
|
91
|
+
client: 'claude', label: 'Claude Code', configurationHome: sessionHome, executable: exe, entry, matches: matchesClaudeEntry,
|
|
92
|
+
installPreview: setup => `Session-only private token-free file: ${claudeSessionPath(sessionHome, setup)}; --mcp-config${strict ? ' --strict-mcp-config (explicit TrackQA-only session: excludes other configured MCP servers for this session)' : ' (other MCP configuration is preserved and may also load)'}`,
|
|
93
|
+
manualInstructions: claudeInstructions,
|
|
94
|
+
async install(setup) { await integrity(); return installClaudeSession(sessionHome, setup, commit); },
|
|
95
|
+
async remove(setup, hash) { await removeClaudeSession(sessionHome, setup, hash); },
|
|
96
|
+
async verify() { return claudeVerification(); },
|
|
97
|
+
async launch(setup, token, signal) {
|
|
98
|
+
requireValue(TOKEN.test(token), 'token_invalid');
|
|
99
|
+
requireValue(await entry(setup) !== null, 'configuration_changed');
|
|
100
|
+
await integrity();
|
|
101
|
+
const env = { ...claudeEnvironment(source), [setup.envVar]: token };
|
|
102
|
+
token = '';
|
|
103
|
+
try {
|
|
104
|
+
const running = launchForeground(exe.path, env, signal, undefined, claudeLaunchArgs(claudeSessionPath(sessionHome, setup), strict));
|
|
105
|
+
delete env[setup.envVar];
|
|
106
|
+
await running;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
delete env[setup.envVar];
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|