@karma42/agent 0.1.0-beta.3 → 0.1.0-beta.4

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.
@@ -1,214 +0,0 @@
1
- import { join, dirname, relative, isAbsolute, resolve } from 'node:path';
2
- import { lstatSync, realpathSync, unlinkSync } from 'node:fs';
3
- import { makePlan, providerName, MODES, COPY_VERSION, VERSION } from './plan.mjs';
4
- import { defaultHome, canonicalPath, ensureHome, connectionDir, privateDir, readPrivate, writePrivate, load, save, binding, withLock, list, lstatExists } from './state.mjs';
5
- import { system } from './system.mjs';
6
-
7
- export const RUNTIME_POLICY = 'existing-service-managed-v2';
8
- const TOKEN = /^[\w-]{43}$/;
9
- export const normalizeRepo = repo => repo.replace(/\.git\/?$/, '').replace(/\/$/, '');
10
- function safeFolder(folder) {
11
- if (typeof folder !== 'string' || !isAbsolute(folder) || folder.length > 2048 || /[\x00-\x1f\x7f-\x9f]/.test(folder)) throw new Error('Choose a valid absolute checkout folder.');
12
- return resolve(folder);
13
- }
14
- function inside(root, path) {
15
- const part = relative(root, path);
16
- return part === '' || (!part.startsWith('../') && part !== '..' && !isAbsolute(part));
17
- }
18
- function checkoutPath(dir, value) {
19
- const folder = safeFolder(value);
20
- const state = join(dir, 'agent-state');
21
- const actualFolder = canonicalPath(folder);
22
- if (inside(actualFolder, dir) || inside(state, actualFolder) || inside(actualFolder, state)) throw new Error('Checkout and private connection state must be separate. Choose a sibling checkout folder.');
23
- return folder;
24
- }
25
- function validateRecord(record, id) {
26
- if (record?.schema !== 1 || record.id !== id || !/^[\w-]{1,64}$/.test(record.agentName ?? '') || !/^[\w-]{1,200}$/.test(record.agentId ?? '') || id.split('/')[1] !== record.agentId) throw new Error('Invalid connection record. Restore the private record or reconnect.');
27
- makePlan({ provider: record.provider, mode: record.mode, projectUrl: record.service, repo: record.repo, folder: record.folder });
28
- if (new URL(record.service).pathname.split('/')[2] !== id.split('/')[0]) throw new Error('Connection project identity mismatch.');
29
- if (record.runtimePolicy !== RUNTIME_POLICY) throw new Error('Unsupported runtime policy. Prepare this connection using a supported package.');
30
- return record;
31
- }
32
- export function createOperations({ home = defaultHome(), io, sys = system } = {}) {
33
- if (lstatExists(home) && lstatSync(home).isSymbolicLink()) throw new Error('Use a real private state directory without a symbolic link.');
34
- home = canonicalPath(home);
35
- const say = text => io.say(text);
36
- const stateOption = resolve(home) === resolve(defaultHome()) ? '' : ` --state-dir '${home.replaceAll("'", "'\\''")}'`;
37
- const requireInteractive = () => { if (!io.interactive) throw new Error('Use an interactive terminal. Connection and start require your explicit approval; setup codes are never command arguments.'); };
38
- const confirm = async text => {
39
- if (!await io.confirm(text)) throw new Error('Cancelled. No agent was started.');
40
- };
41
- const read = id => validateRecord(load(connectionDir(home, id)), id);
42
- const prerequisites = provider => {
43
- const report = sys.doctor(provider);
44
- if (!report.readyForPairingChecks) throw new Error(`Install or update the required local tools first: ${report.checks.filter(c => !c.ok).map(c => c.tool).join(', ')}. Run doctor for details.`);
45
- sys.verifyBundle();
46
- };
47
- async function consent(record, access) {
48
- const plan = makePlan({ provider: record.provider, mode: record.mode, projectUrl: record.service, repo: record.repo, folder: record.folder });
49
- say(`\nReview this connection\nProject: ${record.service}\nAgent: ${record.agentName}\nRepository: ${record.repo}\nCheckout: ${record.folder}\nProvider: ${record.provider}\nPermissions: ${plan.permissions.title}\n${plan.permissions.detail}\n${plan.permissions.boundary}`);
50
- say(`GitHub account: ${access.githubLogin ?? 'not signed in / not detected'}; repository write access: ${access.github ? 'checked' : 'not confirmed'}. Model sign-in: not checked.`);
51
- say('The foreground runner uses service-selected runtime releases and instructions, including compatible updates between turns. Hashes detect corruption; they are not independent publisher signatures. Package updates are a separate manual installation. Existing client trust checks remain in place. No personal client configuration is edited.');
52
- say('There is no default time, turn or spending cap. Readiness checks do not verify model sign-in; sign in with your chosen local client before starting.');
53
- for (const line of plan.acknowledgements) await confirm(line);
54
- await confirm('I understand and accept this service-managed runtime update policy.');
55
- if (record.mode === 'bypass' && await io.ask('Type BYPASS to confirm this permission choice:') !== 'BYPASS') throw new Error('Bypass not confirmed. No agent was started.');
56
- await confirm('Prepare the checkout and connection now? This may clone the repository; Git can invoke configured hooks and filters. No model will start.');
57
- return { binding: binding(record), acknowledgedAt: sys.now(), packageVersion: VERSION, copyVersion: COPY_VERSION };
58
- }
59
- function checkFolder(record, clone = false) {
60
- if (!lstatExists(record.folder)) {
61
- if (!clone) throw new Error('Checkout is missing. Run prepare before starting.');
62
- if (!lstatExists(dirname(record.folder)) || realpathSync(dirname(record.folder)) !== dirname(record.folder)) throw new Error('Choose a checkout under an existing real directory without symbolic links.');
63
- if (sys.run('git', ['clone', '--', record.repo, record.folder]) === null) throw new Error('Could not clone the repository. Check GitHub sign-in and access, then retry prepare.');
64
- }
65
- if (!lstatSync(record.folder).isDirectory() || lstatSync(record.folder).isSymbolicLink() || realpathSync(record.folder) !== record.folder) throw new Error('Use the real checkout path without symbolic links.');
66
- const top = sys.run('git', ['rev-parse', '--show-toplevel'], record.folder);
67
- const origin = sys.run('git', ['remote', 'get-url', 'origin'], record.folder);
68
- if (top !== record.folder || !origin || normalizeRepo(origin) !== normalizeRepo(record.repo)) throw new Error('Checkout must be the project repository root with the exact HTTPS GitHub origin. Existing files were preserved.');
69
- }
70
- function github(record) {
71
- const raw = sys.run('gh', ['api', 'user']);
72
- let login = null, permission = false;
73
- try { const value = JSON.parse(raw); if (/^[A-Za-z0-9-]{1,39}$/.test(value.login)) login = value.login; } catch {}
74
- const slug = new URL(normalizeRepo(record.repo)).pathname.slice(1);
75
- if (login) {
76
- try { permission = JSON.parse(sys.run('gh', ['api', `repos/${slug}`]))?.permissions?.push === true; } catch {}
77
- }
78
- return { github: permission, githubLogin: login };
79
- }
80
- async function finish(dir, record) {
81
- prerequisites(record.provider);
82
- const candidate = { ...record, folder: checkoutPath(dir, record.folder), copyVersion: COPY_VERSION, bundleHash: sys.verifyBundle().fingerprint, consent: null, phase: 'paired' };
83
- const approved = await consent(candidate, github(candidate));
84
- // Mark any older approval invalid before touching the selected checkout.
85
- save(dir, candidate);
86
- checkFolder(candidate, true);
87
- privateDir(join(dir, 'agent-state'));
88
- const state = join(dir, 'agent-state', candidate.agentName); privateDir(state);
89
- const bundleHash = sys.installBundle(state);
90
- const check = { runner: true, folder: true, ...github(candidate) };
91
- const token = readPrivate(join(state, 'token')).trim();
92
- if (!TOKEN.test(token)) throw new Error('Invalid stored credential. Reconnect in My agents.');
93
- await sys.request(candidate.service, 'readiness', check, token);
94
- const prepared = { ...candidate, phase: 'prepared', consent: approved, bundleHash, readiness: { ...check, checkedAt: sys.now() } };
95
- save(dir, prepared);
96
- say(`\nConnection prepared; runner not started.\nConnection ID: ${prepared.id}\nGitHub: ${check.github ? `write access checked for ${check.githubLogin}` : 'action required — sign in with gh and accept the owner’s repository invitation, then prepare again.'}\nStart separately: karma42-agent start --connection ${prepared.id}${stateOption}`);
97
- return prepared;
98
- }
99
- async function pair(service, expected) {
100
- await confirm('Exchange a one-time setup code from this project and save its private credential? This rotates the existing connection credential. Stop any previous runner first. No model will start.');
101
- const code = await io.secret('Paste the setup code from My agents (hidden):');
102
- if (!TOKEN.test(code)) throw new Error('Use the 43-character setup code from My agents.');
103
- const response = await sys.request(service, 'exchange', { code });
104
- if (!TOKEN.test(response?.token ?? '') || !/^[\w-]{1,200}$/.test(response.agentId ?? '') || !/^[\w-]{1,64}$/.test(response.agentName ?? '')) throw new Error('Unexpected pairing response. Generate a fresh code in My agents.');
105
- // Validate authoritative repository before displaying it or using it in a subprocess.
106
- const plan = makePlan({ provider: expected.provider, mode: expected.mode, projectUrl: service, repo: response.repoUrl, folder: expected.folder ?? '/placeholder' });
107
- const id = `${new URL(service).pathname.split('/')[2]}/${response.agentId}`;
108
- const dir = connectionDir(home, id);
109
- // Persist a private recovery receipt before attempting preparation: never lose a valid token
110
- // on a clone failure, cancelled consent, or a reconnect identity mismatch.
111
- if (expected.folder) checkoutPath(dir, expected.folder);
112
- const matches = !expected.id || (id === expected.id && response.agentName === expected.agentName && normalizeRepo(plan.repo) === normalizeRepo(expected.repo));
113
- const receipt = { schema: 1, id, service, agentId: response.agentId, agentName: response.agentName, repo: plan.repo, folder: safeFolder(matches && expected.folder ? expected.folder : join(dir, 'workspace')), provider: expected.provider, mode: expected.mode, copyVersion: COPY_VERSION, runtimePolicy: RUNTIME_POLICY, token: response.token, pairedAt: sys.now() };
114
- writePrivate(join(dir, 'pending-pair.json'), receipt);
115
- if (!matches) throw new Error(`That code belongs to a different connection or repository. Existing checkout and saved credential were preserved. A private recovery receipt was saved for connection ${id}; run prepare for that connection ID to review and recover it without another exchange.`);
116
- return { id, dir, receipt };
117
- }
118
- async function applyPair(pairing, options, locked = false) {
119
- const { dir, id, receipt } = pairing;
120
- const action = async () => {
121
- validateRecord(receipt, id);
122
- if (!TOKEN.test(receipt.token ?? '')) throw new Error('Invalid recovery receipt. Generate a new setup code.');
123
- let previous = null;
124
- if (lstatExists(join(dir, 'connection.json'))) previous = read(id);
125
- if (previous && (previous.service !== receipt.service || previous.agentName !== receipt.agentName || normalizeRepo(previous.repo) !== normalizeRepo(receipt.repo))) throw new Error('This connection has a different saved identity. Private recovery receipt saved; existing state preserved.');
126
- const folder = checkoutPath(dir, options.folder ?? previous?.folder ?? receipt.folder);
127
- privateDir(join(dir, 'agent-state'));
128
- const state = join(dir, 'agent-state', receipt.agentName); privateDir(state);
129
- const { token, ...metadata } = receipt;
130
- const record = { ...metadata, folder, provider: options.provider ?? receipt.provider, mode: options.mode ?? receipt.mode, phase: 'paired', consent: null };
131
- // Readiness and approval are invalidated when the server credential rotates.
132
- save(dir, record);
133
- writePrivate(join(state, 'token'), `${token}\n`);
134
- writePrivate(join(state, 'service.json'), { service: receipt.service });
135
- unlinkSync(join(dir, 'pending-pair.json'));
136
- return finish(dir, record);
137
- };
138
- return locked ? action() : withLock(dir, action);
139
- }
140
- return {
141
- async connect(options) {
142
- requireInteractive(); ensureHome(home);
143
- const provider = providerName(options.provider);
144
- const mode = options.mode;
145
- // Use shared URL/permission validators before the first service request.
146
- const plan = makePlan({ provider, mode, projectUrl: options['project-url'], repo: 'https://github.com/placeholder/project', folder: options.folder ?? '/placeholder' });
147
- if (options.folder && (inside(canonicalPath(safeFolder(options.folder)), home) || inside(home, canonicalPath(safeFolder(options.folder))))) throw new Error('Choose a custom checkout outside private connection storage; it must not contain that storage either.');
148
- prerequisites(provider);
149
- return withLock(home, async () => applyPair(await pair(plan.projectUrl, { provider, mode, folder: options.folder }), { provider, mode, folder: options.folder }));
150
- },
151
- async prepare(options) {
152
- requireInteractive();
153
- const dir = connectionDir(home, options.connection);
154
- return withLock(dir, async () => {
155
- if (lstatExists(join(dir, 'pending-pair.json'))) {
156
- say('A previous pairing was interrupted. Reviewing its saved private credential; no new code will be consumed.');
157
- return applyPair({ dir, id: options.connection, receipt: readPrivate(join(dir, 'pending-pair.json'), true) }, {}, true);
158
- }
159
- return finish(dir, read(options.connection));
160
- });
161
- },
162
- async permissions(options) {
163
- requireInteractive();
164
- const dir = connectionDir(home, options.connection);
165
- return withLock(dir, async () => {
166
- const record = read(options.connection);
167
- return finish(dir, { ...record, provider: options.provider ? providerName(options.provider) : record.provider, mode: options.mode });
168
- });
169
- },
170
- async reconnect(options) {
171
- requireInteractive();
172
- const dir = connectionDir(home, options.connection);
173
- return withLock(home, () => withLock(dir, async () => {
174
- const record = read(options.connection);
175
- prerequisites(record.provider);
176
- return applyPair(await pair(record.service, record), record, true);
177
- }));
178
- },
179
- async start(options) {
180
- requireInteractive();
181
- const dir = connectionDir(home, options.connection);
182
- return withLock(dir, async () => {
183
- const record = read(options.connection);
184
- if (lstatExists(join(dir, 'pending-pair.json'))) throw new Error('A saved pairing needs recovery. Run prepare before starting.');
185
- if (record.phase !== 'prepared' || record.copyVersion !== COPY_VERSION || record.consent?.binding !== binding(record)) throw new Error('This connection needs preparation or renewed permission approval. Run prepare first.');
186
- prerequisites(record.provider);
187
- if (sys.verifyInstalled(join(dir, 'agent-state', record.agentName)) !== record.bundleHash) throw new Error('The packaged runtime changed. Run prepare to renew approval.');
188
- checkFolder(record);
189
- const check = { runner: true, folder: true, ...github(record) };
190
- const token = readPrivate(join(dir, 'agent-state', record.agentName, 'token')).trim();
191
- if (!TOKEN.test(token)) throw new Error('Invalid stored credential. Reconnect in My agents.');
192
- await sys.request(record.service, 'readiness', check, token);
193
- say(`\nStart ${record.agentName}\nRepository: ${record.repo}\nCheckout: ${record.folder}\nProvider: ${record.provider}\nPermissions: ${MODES[record.provider][record.mode].title}\n${MODES[record.provider][record.mode].detail}`);
194
- say(`GitHub account: ${check.githubLogin ?? 'not signed in / not detected'}; repository write access: ${check.github ? 'checked now' : 'not confirmed'}. Model sign-in is checked by the local client when work starts.`);
195
- if (!check.github) await confirm('GitHub write access is not confirmed. Coding and publishing may fail. Start with this limitation?');
196
- await confirm('Start this agent now? An active project can begin model work immediately and consume your usage. A preparing or paused project waits. Keep this terminal open; Ctrl+C stops the runner. Completed actions are not undone.');
197
- const started = { ...record, readiness: { ...check, checkedAt: sys.now() }, lastLocalStartAt: sys.now(), lastLocalExit: null };
198
- save(dir, started);
199
- const code = await sys.launch(dir, started);
200
- save(dir, { ...started, lastLocalExit: { at: sys.now(), code } });
201
- say('Foreground runner exited. Check My agents for service-observed contact and activity.');
202
- return code;
203
- });
204
- },
205
- status(options = {}) {
206
- const records = options.connection ? list(home).filter(r => r.id === options.connection).map(r => validateRecord(r, r.id)) : list(home).map(r => validateRecord(r, r.id));
207
- if (options.connection && !records.length) throw new Error('No saved connection with this ID. Run status to list connections.');
208
- const pending = record => lstatExists(join(connectionDir(home, record.id), 'pending-pair.json'));
209
- const result = records.map(record => ({ id: record.id, agent: record.agentName, provider: record.provider, permissions: record.mode, service: record.service, repository: record.repo, folder: record.folder, preparation: record.phase, pairingPending: pending(record), approvalCurrent: !pending(record) && record.phase === 'prepared' && record.copyVersion === COPY_VERSION && record.consent?.binding === binding(record), github: record.readiness ?? null, localOperationLock: lstatExists(join(connectionDir(home, record.id), 'operation.lock')), lastLocalStartAt: record.lastLocalStartAt ?? null, lastLocalExit: record.lastLocalExit ?? null, serverContact: 'not checked', modelSignIn: 'not checked' }));
210
- say(options.json ? JSON.stringify(result, null, 2) : result.length ? result.map(r => `${r.agent} (${r.provider}, ${r.permissions})\n${r.id}\nPreparation: ${r.pairingPending ? 'pairing recovery needed — run prepare' : r.preparation}; current approval: ${r.approvalCurrent ? 'yes' : 'no'}\nLocal operation lock: ${r.localOperationLock ? 'present — this does not prove a live process' : 'absent'}\nGitHub check: ${r.github?.github ? 'write access checked' : 'not confirmed'}${r.github ? ` at ${r.github.checkedAt}` : ''}\nServer contact and model sign-in: not checked. Open My agents for observed activity.`).join('\n\n') : 'No saved package connections. Legacy setup connections are preserved separately.');
211
- return result;
212
- },
213
- };
214
- }
package/src/doctor.mjs DELETED
@@ -1,28 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { providerName } from './plan.mjs';
3
-
4
- function atLeast(actual, minimum) {
5
- const a = actual.split('.').map(Number), b = minimum.split('.').map(Number);
6
- for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; }
7
- return true;
8
- }
9
- export function probeVersion(command) {
10
- const result = spawnSync(command, ['--version'], { encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024, shell: false, windowsHide: true });
11
- if (result.error || result.status !== 0) return null;
12
- return result.stdout;
13
- }
14
- export function doctor(provider, probe = probeVersion, nodeVersion = process.versions.node) {
15
- providerName(provider);
16
- const spec = [
17
- ['git', /git version (\d+\.\d+\.\d+)/, null],
18
- ['gh', /gh version (\d+\.\d+\.\d+)/, null],
19
- provider === 'claude' ? ['claude', /(?:^|\n)(\d+\.\d+\.\d+)/, '2.1.259'] : ['codex', /codex-cli (\d+\.\d+\.\d+)/, '0.160.0'],
20
- ];
21
- const checks = [{ tool: 'node', version: nodeVersion, ok: /^\d+\.\d+\.\d+$/.test(nodeVersion) && atLeast(nodeVersion, '22.0.0'), minimum: '22.0.0' }];
22
- for (const [tool, pattern, minimum] of spec) {
23
- const output = probe(tool);
24
- const version = typeof output === 'string' ? pattern.exec(output)?.[1] ?? null : null;
25
- checks.push({ tool, version, minimum, ok: !!version && (!minimum || atLeast(version, minimum)) });
26
- }
27
- return { checks, readyForPairingChecks: checks.every(c => c.ok), authenticated: 'not checked', repositoryAccess: 'not checked', executionReady: false };
28
- }
package/src/plan.mjs DELETED
@@ -1,88 +0,0 @@
1
- import { isAbsolute } from 'node:path';
2
-
3
- export const VERSION = '0.1.0-beta.3';
4
- export const COPY_VERSION = '2026-10-09.2';
5
- // These descriptions reflect pilot/agent-kit/providers.mjs. They are a proposal
6
- // for the onboarding vocabulary, not a change to the current adapter settings.
7
- export const MODES = {
8
- claude: {
9
- guarded: {
10
- title: 'Guarded automation',
11
- detail: 'Claude can edit files and run the configured Git, GitHub and project commands without asking. Commands needing additional approval are refused by this foreground runner. It cannot pause to ask you for each action.',
12
- boundary: 'The command rules are not a system sandbox. Repository scripts, hooks and tools can access files and credentials available to your operating-system account.',
13
- },
14
- bypass: {
15
- title: 'Bypass permissions',
16
- detail: 'Claude skips its normal permission checks and may read, edit or delete files and execute potentially destructive commands without asking.',
17
- boundary: 'Use an isolated or disposable execution environment. A dedicated repository folder alone does not isolate your machine or its credentials.',
18
- },
19
- },
20
- codex: {
21
- guarded: {
22
- title: 'Guarded automation',
23
- detail: 'Codex uses a workspace-write sandbox with automatic approval review. Some escalated commands can be approved to run outside that sandbox. This runner does not ask you to approve every action.',
24
- boundary: 'Automatic approval is not human review. You must supervise the run and review the resulting changes.',
25
- },
26
- bypass: {
27
- title: 'Full access / bypass permissions',
28
- detail: 'This adapter disables the Codex sandbox and approval review. Commands run with the access available to your operating-system account without per-action prompts.',
29
- boundary: 'Use an isolated or disposable execution environment. A dedicated repository folder alone does not isolate your machine or its credentials.',
30
- },
31
- },
32
- };
33
-
34
- function clean(value, label) {
35
- if (typeof value !== 'string' || !value || value.length > 2048 || /[\x00-\x1f\x7f-\x9f]/.test(value)) throw new Error(`Provide a valid ${label}.`);
36
- return value;
37
- }
38
- export function providerName(value) {
39
- if (!Object.hasOwn(MODES, value ?? '')) throw new Error('Choose --provider claude or codex.');
40
- return value;
41
- }
42
- export function makePlan(input) {
43
- const provider = providerName(input.provider);
44
- if (!Object.hasOwn(MODES[provider], input.mode ?? '')) throw new Error('Choose --mode guarded or bypass explicitly.');
45
- const raw = clean(input.projectUrl, 'project URL');
46
- let url;
47
- try { url = new URL(raw); } catch { throw new Error('Use a Karma42 project service URL.'); }
48
- const local = ['localhost', '127.0.0.1'].includes(url.hostname);
49
- if (!['https:', 'http:'].includes(url.protocol) || (url.protocol === 'http:' && !local)
50
- || url.username || url.password || url.search || url.hash || !/^\/p\/[a-f0-9]{64}\/?$/.test(url.pathname)) {
51
- throw new Error('Use an HTTPS project service URL ending in /p/PROJECT_ID, without credentials or query parameters. HTTP is allowed only on loopback for local previews.');
52
- }
53
- const repo = clean(input.repo, 'GitHub repository URL');
54
- if (!/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/?$/.test(repo)) throw new Error('Use a GitHub repository URL without credentials, query parameters or fragments.');
55
- if (new URL(repo).pathname.split('/').filter(Boolean).length !== 2 || /\/(?:\.|\.\.)(?:\/|$)/.test(repo)) throw new Error('Use a valid owner and repository in the GitHub URL.');
56
- const folder = clean(input.folder, 'absolute checkout folder');
57
- if (!isAbsolute(folder)) throw new Error('Choose an absolute checkout folder.');
58
- return {
59
- draft: true, verified: false, version: VERSION, copyVersion: COPY_VERSION,
60
- projectUrl: url.href.replace(/\/$/, ''), repo: repo.replace(/\/$/, ''), folder, provider, mode: input.mode,
61
- permissions: MODES[provider][input.mode],
62
- acknowledgements: [
63
- 'I understand that this agent can change files, run commands, use my granted GitHub access and consume my model usage.',
64
- 'I will monitor its activity, review changes and stop it if it acts unexpectedly.',
65
- ...(input.mode === 'bypass' ? ['I understand that bypass skips per-action permission checks. I have chosen an isolated or disposable environment appropriate for this risk.'] : []),
66
- ],
67
- checks: { account: 'not checked', membership: 'not checked', repositoryAccess: 'not checked', folder: 'not checked', connection: 'not created', runner: 'not started' },
68
- };
69
- }
70
- export function renderPlan(plan) {
71
- return [
72
- 'Karma42 — onboarding preview',
73
- 'DRAFT: this is a walkthrough. Nothing is connected and no consent is recorded.',
74
- '', '1. Review the proposed connection',
75
- `Project service: ${plan.projectUrl}`, `Repository (unverified): ${plan.repo}`, `Folder (unverified): ${plan.folder}`,
76
- `Provider: ${plan.provider}`, `Permissions: ${plan.permissions.title}`,
77
- '', '2. Understand the permission choice', plan.permissions.detail, plan.permissions.boundary,
78
- 'Karma42 membership and GitHub permissions still apply. This choice does not grant additional access.',
79
- '', '3. Acknowledge before preparing the connection',
80
- ...plan.acknowledgements.map(line => `[ ] ${line}`),
81
- 'Proposed action: Prepare connection. Account, project and repository must first be checked against the service.',
82
- '', '4. Start separately when ready',
83
- 'Proposed confirmation: Start this agent now? If the project is active, it can begin work immediately and consume model usage. If the project is preparing or paused, it waits.',
84
- 'Keep the foreground terminal open to monitor activity. Ctrl+C stops the runner. Previously completed actions are not undone.',
85
- 'The runner will report model usage when available; this is not a measured price or a spending cap.',
86
- '', 'Preview result: not connected; not started. Connect separately in an interactive terminal.',
87
- ].join('\n');
88
- }
@@ -1,17 +0,0 @@
1
- // Separate process keeps ambient PILOT_* overrides out of the canonical supervisor.
2
- import { join } from 'node:path';
3
- import { load, binding } from './state.mjs';
4
- import { COPY_VERSION } from './plan.mjs';
5
- import { supervise } from '../runtime/launcher.mjs';
6
- const dir = process.argv[2];
7
- const record = load(dir);
8
- if (record.phase !== 'prepared' || record.copyVersion !== COPY_VERSION || record.consent?.binding !== binding(record)) throw new Error('Prepare this connection and confirm its permissions before starting.');
9
- process.exitCode = await supervise({
10
- service: record.service, agentName: record.agentName, repoDir: record.folder, home: join(dir, 'agent-state'),
11
- env: {
12
- PILOT_PROVIDER: record.provider,
13
- PILOT_PERMISSION_MODE: record.mode === 'bypass' ? 'bypassPermissions' : 'acceptEdits',
14
- PILOT_MAX_TURNS_PER_HOUR: '0', PILOT_TURN_SECONDS: '0', PILOT_TURN_BUDGET_USD: '0',
15
- PILOT_WORK_INTERVAL_MINUTES: '15',
16
- },
17
- });
package/src/terminal.mjs DELETED
@@ -1,33 +0,0 @@
1
- import { createInterface } from 'node:readline/promises';
2
- export function terminal(input = process.stdin, output = process.stdout) {
3
- return {
4
- interactive: !!input.isTTY && !!output.isTTY,
5
- say: text => output.write(`${text}\n`),
6
- async ask(text) {
7
- const rl = createInterface({ input, output });
8
- try { return (await rl.question(`${text} `)).trim(); } finally { rl.close(); }
9
- },
10
- async confirm(text) { return (await this.ask(`${text} [yes/no]`)).toLowerCase() === 'yes'; },
11
- secret(text) {
12
- if (!input.isTTY || !output.isTTY) throw new Error('Enter the setup code in an interactive terminal.');
13
- return new Promise((resolve, reject) => {
14
- const wasRaw = input.isRaw; let value = '';
15
- input.setRawMode(true); input.resume(); output.write(`${text} `);
16
- const finish = (error) => {
17
- input.off('data', onData); input.off('end', onEnd); input.setRawMode(wasRaw); input.pause(); output.write('\n');
18
- if (error) reject(error); else resolve(value);
19
- };
20
- const onEnd = () => finish(new Error('Code entry cancelled.'));
21
- const onData = chunk => {
22
- for (const char of chunk.toString('utf8')) {
23
- if (char === '\x03' || char === '\x04') { finish(new Error('Code entry cancelled.')); return; }
24
- if (char === '\r' || char === '\n') { finish(); return; }
25
- if (char === '\x7f' || char === '\b') value = value.slice(0, -1);
26
- else if (/[\w-]/.test(char) && value.length < 100) value += char;
27
- }
28
- };
29
- input.on('data', onData); input.once('end', onEnd);
30
- });
31
- },
32
- };
33
- }