@ctrl-spc/cli 1.1.15 → 1.2.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.
Files changed (42) hide show
  1. package/dist/agents.js +54 -0
  2. package/dist/collision.js +44 -0
  3. package/dist/commands/fetch.js +213 -0
  4. package/dist/commands/init.js +457 -0
  5. package/dist/commands/link.js +152 -0
  6. package/dist/commands/login.js +332 -0
  7. package/dist/commands/logout.js +14 -0
  8. package/dist/commands/run.js +622 -0
  9. package/dist/config.js +68 -0
  10. package/dist/env.js +5 -0
  11. package/dist/git.js +70 -0
  12. package/dist/index.js +56 -72
  13. package/dist/mcp.js +1732 -0
  14. package/dist/presence.js +62 -0
  15. package/dist/prompt.js +47 -0
  16. package/dist/protocol.js +117 -0
  17. package/dist/skills.js +148 -0
  18. package/dist/supabase.js +40 -10
  19. package/dist/topology.js +602 -0
  20. package/package.json +25 -23
  21. package/bin/ctrl-spc.js +0 -6
  22. package/dist/auth.js +0 -172
  23. package/dist/controlRequests.js +0 -164
  24. package/dist/daemon.js +0 -152
  25. package/dist/devices.js +0 -64
  26. package/dist/flagAssembler.js +0 -29
  27. package/dist/init.js +0 -79
  28. package/dist/launchd.js +0 -154
  29. package/dist/lifecycle.js +0 -37
  30. package/dist/mcpConfig.js +0 -12
  31. package/dist/repo.js +0 -52
  32. package/dist/session.js +0 -58
  33. package/dist/setup.js +0 -60
  34. package/dist/spawner.js +0 -140
  35. package/dist/windows-service.js +0 -142
  36. package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.d.ts +0 -24
  37. package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.js +0 -32
  38. package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.d.ts +0 -2
  39. package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.js +0 -21
  40. package/node_modules/@ctrl-spc/mcp-server/dist/index.d.ts +0 -1
  41. package/node_modules/@ctrl-spc/mcp-server/dist/index.js +0 -22
  42. package/node_modules/@ctrl-spc/mcp-server/package.json +0 -38
package/dist/auth.js DELETED
@@ -1,172 +0,0 @@
1
- import http from 'node:http';
2
- import { randomUUID } from 'node:crypto';
3
- import { createSupabaseClient, SUPABASE_ANON_KEY, SUPABASE_FUNCTIONS_URL } from './supabase.js';
4
- import { getOrCreateMachineId, saveMachineState } from './session.js';
5
- const CALLBACK_HOST = '127.0.0.1';
6
- const CALLBACK_PORT = 54321;
7
- const CALLBACK_PATH = '/callback';
8
- const TOKEN_PATH = '/token';
9
- const AUTH_TIMEOUT_MS = 10 * 60 * 1000;
10
- const SIGN_IN_EXPIRED_MESSAGE = 'That sign-in link expired. Run `ctrl-spc setup` again to get a fresh one.';
11
- const SEND_EMAIL_FAILED_MESSAGE = "Couldn't send the sign-in email. Check your connection and try again.";
12
- export function buildRedirectUrl(state) {
13
- const url = new URL(`http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`);
14
- url.searchParams.set('state', state);
15
- return url.toString();
16
- }
17
- export function buildCallbackHtml(state) {
18
- return `<!doctype html>
19
- <html lang="en">
20
- <head><meta charset="utf-8"><title>Control Space</title></head>
21
- <body>
22
- <p>Connecting...</p>
23
- <script>
24
- const hash = new URLSearchParams(window.location.hash.slice(1));
25
- const body = {
26
- access_token: hash.get('access_token'),
27
- refresh_token: hash.get('refresh_token'),
28
- state: '${escapeJsString(state)}',
29
- };
30
- if (!body.access_token || !body.refresh_token) {
31
- document.body.textContent = '${escapeJsString(SIGN_IN_EXPIRED_MESSAGE)}';
32
- } else {
33
- fetch('/token', {
34
- method: 'POST',
35
- headers: { 'Content-Type': 'application/json' },
36
- body: JSON.stringify(body),
37
- }).then((res) => {
38
- document.body.textContent = res.ok
39
- ? 'Connected - you can close this tab.'
40
- : '${escapeJsString(SIGN_IN_EXPIRED_MESSAGE)}';
41
- });
42
- }
43
- </script>
44
- </body>
45
- </html>`;
46
- }
47
- export function isValidTokenCallback(body, expectedState) {
48
- return typeof body.state === 'string' && body.state === expectedState;
49
- }
50
- export async function requestCliLoginEmail(email, redirectTo, fetchImpl = fetch) {
51
- const response = await fetchImpl(`${SUPABASE_FUNCTIONS_URL}/request-cli-login`, {
52
- method: 'POST',
53
- headers: {
54
- apikey: SUPABASE_ANON_KEY,
55
- Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
56
- 'Content-Type': 'application/json',
57
- },
58
- body: JSON.stringify({ email, redirectTo }),
59
- });
60
- if (response.ok)
61
- return;
62
- const message = await readFunctionError(response);
63
- throw new Error(message ?? SEND_EMAIL_FAILED_MESSAGE);
64
- }
65
- export async function authenticate(email) {
66
- const client = createSupabaseClient();
67
- const state = randomUUID();
68
- const redirectUrl = buildRedirectUrl(state);
69
- return new Promise((resolve, reject) => {
70
- let settled = false;
71
- let timeout;
72
- const server = http.createServer(async (req, res) => {
73
- const url = new URL(req.url ?? '/', redirectUrl);
74
- if (req.method === 'GET' && url.pathname === CALLBACK_PATH) {
75
- if (url.searchParams.get('state') !== state) {
76
- res.writeHead(400, { 'Content-Type': 'text/plain' });
77
- res.end(SIGN_IN_EXPIRED_MESSAGE);
78
- return;
79
- }
80
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
81
- res.end(buildCallbackHtml(state));
82
- return;
83
- }
84
- if (req.method === 'POST' && url.pathname === TOKEN_PATH) {
85
- const body = await readJson(req);
86
- if (!isValidTokenCallback(body, state)) {
87
- res.writeHead(400);
88
- res.end();
89
- return;
90
- }
91
- const accessToken = typeof body.access_token === 'string' ? body.access_token : null;
92
- const refreshToken = typeof body.refresh_token === 'string' ? body.refresh_token : null;
93
- if (!accessToken || !refreshToken) {
94
- res.writeHead(400);
95
- res.end();
96
- return;
97
- }
98
- const { error } = await client.auth.setSession({
99
- access_token: accessToken,
100
- refresh_token: refreshToken,
101
- });
102
- if (error) {
103
- res.writeHead(400);
104
- res.end();
105
- settle(() => reject(new Error(SIGN_IN_EXPIRED_MESSAGE)));
106
- return;
107
- }
108
- const machineId = getOrCreateMachineId();
109
- const machineState = { machineId, accessToken, refreshToken };
110
- saveMachineState(machineState);
111
- res.writeHead(200);
112
- res.end();
113
- settle(() => resolve(machineState));
114
- return;
115
- }
116
- res.writeHead(404);
117
- res.end();
118
- });
119
- timeout = setTimeout(() => {
120
- settle(() => reject(new Error(SIGN_IN_EXPIRED_MESSAGE)));
121
- }, AUTH_TIMEOUT_MS);
122
- server.on('error', () => {
123
- settle(() => reject(new Error(SEND_EMAIL_FAILED_MESSAGE)));
124
- });
125
- server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
126
- try {
127
- await requestCliLoginEmail(email, redirectUrl);
128
- }
129
- catch (error) {
130
- settle(() => reject(error instanceof Error ? error : new Error(SEND_EMAIL_FAILED_MESSAGE)));
131
- }
132
- });
133
- function settle(done) {
134
- if (settled)
135
- return;
136
- settled = true;
137
- clearTimeout(timeout);
138
- server.close(() => done());
139
- }
140
- });
141
- }
142
- async function readFunctionError(response) {
143
- try {
144
- const body = await response.json();
145
- return typeof body.error === 'string' ? body.error : null;
146
- }
147
- catch {
148
- return null;
149
- }
150
- }
151
- function readJson(req) {
152
- return new Promise((resolve) => {
153
- let body = '';
154
- req.on('data', (chunk) => {
155
- body += chunk.toString();
156
- if (body.length > 10_000)
157
- req.destroy();
158
- });
159
- req.on('end', () => {
160
- try {
161
- resolve(JSON.parse(body));
162
- }
163
- catch {
164
- resolve({});
165
- }
166
- });
167
- req.on('error', () => resolve({}));
168
- });
169
- }
170
- function escapeJsString(value) {
171
- return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
172
- }
@@ -1,164 +0,0 @@
1
- import { assembleCommand, PING_PROMPT } from './flagAssembler.js';
2
- const STOP_NOT_OWNED_MESSAGE = 'This machine can only stop agent processes it started and still owns.';
3
- export function makeControlRequestProcessor(deps) {
4
- const timeoutMs = deps.pingTimeoutMs ?? 60_000;
5
- async function resolveLocalPath(projectId) {
6
- const { data } = await deps.session.client
7
- .from('project_links')
8
- .select('value')
9
- .eq('project_id', projectId)
10
- .eq('kind', 'path')
11
- .limit(1);
12
- return data?.[0]?.value ?? null;
13
- }
14
- async function resolveMachineLabel() {
15
- const { data } = await deps.session.client
16
- .from('devices')
17
- .select('label')
18
- .eq('id', deps.session.machineId)
19
- .single();
20
- return (data?.label ?? 'This machine').trim() || 'This machine';
21
- }
22
- async function mark(row, patch) {
23
- await deps.session.client.from('agent_control_requests').update(patch).eq('id', row.id);
24
- }
25
- function completedPatch(status, errorMessage) {
26
- return {
27
- status,
28
- ...(errorMessage ? { error_message: errorMessage } : {}),
29
- completed_at: new Date(deps.now()).toISOString(),
30
- };
31
- }
32
- async function collectStdout(handle) {
33
- return new Promise((resolve, reject) => {
34
- let stdout = '';
35
- let settled = false;
36
- const timer = setTimeout(() => {
37
- handle.kill();
38
- finish(() => reject(new Error('Provider timed out before replying.')));
39
- }, timeoutMs);
40
- const finish = (settle) => {
41
- if (settled)
42
- return;
43
- settled = true;
44
- clearTimeout(timer);
45
- settle();
46
- };
47
- handle.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
48
- handle.stdout.on('end', () => finish(() => resolve(stdout)));
49
- handle.stdout.on('error', (err) => finish(() => reject(err)));
50
- });
51
- }
52
- async function findSession(row) {
53
- let query = deps.session.client
54
- .from('sessions')
55
- .select('id, project_id, device_id, agent_type, model, effort, permission_mode, allowed_tools')
56
- .eq('project_id', row.project_id)
57
- .is('ended_at', null);
58
- if (row.session_id)
59
- query = query.eq('id', row.session_id);
60
- const { data, error } = await query.order('created_at', { ascending: false }).limit(1);
61
- if (error)
62
- throw new Error(`Session lookup failed: ${error.message}`);
63
- return ((data ?? [])[0] ?? null);
64
- }
65
- async function insertMessage(row, sessionId, role, kind, content) {
66
- const { data, error } = await deps.session.client.from('agent_messages').insert({
67
- project_id: row.project_id,
68
- session_id: sessionId,
69
- role,
70
- kind,
71
- content,
72
- }).select('id').single();
73
- if (error)
74
- throw new Error(`Message insert failed: ${error.message}`);
75
- return data?.id ?? null;
76
- }
77
- async function processPing(row, session) {
78
- if (!session) {
79
- await mark(row, completedPatch('failed', 'No active agent session was found.'));
80
- return;
81
- }
82
- const cwd = await resolveLocalPath(row.project_id);
83
- if (!cwd) {
84
- await mark(row, completedPatch('failed', 'This project is not linked to a local folder on this machine.'));
85
- return;
86
- }
87
- const { cmd, args } = assembleCommand(session, PING_PROMPT);
88
- const handle = deps.spawnAgent(cmd, args, cwd);
89
- const stdout = await collectStdout(handle);
90
- const machineLabel = await resolveMachineLabel();
91
- const responseId = await insertMessage(row, session.id, 'agent', 'ping', `${machineLabel} responded: ${cleanAgentText(stdout)}`);
92
- await mark(row, { ...completedPatch('succeeded'), response_message_id: responseId });
93
- }
94
- async function processStop(row, session) {
95
- const owned = row.session_id
96
- ? deps.ownedSessions.get(row.session_id)
97
- : [...deps.ownedSessions.values()].find((entry) => entry.projectId === row.project_id);
98
- if (!owned) {
99
- if (session) {
100
- const endedAt = new Date(deps.now()).toISOString();
101
- await deps.session.client.from('sessions').update({
102
- connection_state: 'disconnected',
103
- ended_at: endedAt,
104
- exit_code: null,
105
- stop_reason: 'daemon_restarted',
106
- }).eq('id', session.id);
107
- await insertMessage(row, session.id, 'system', 'system', 'This agent was no longer owned by the current machine helper, so Control Space marked it disconnected. Start a fresh agent to continue.');
108
- await mark(row, { status: 'succeeded', completed_at: endedAt });
109
- return;
110
- }
111
- await mark(row, completedPatch('failed', STOP_NOT_OWNED_MESSAGE));
112
- return;
113
- }
114
- owned.handle.kill();
115
- deps.ownedSessions.delete(owned.sessionId);
116
- const endedAt = new Date(deps.now()).toISOString();
117
- await deps.session.client.from('sessions').update({
118
- connection_state: 'disconnected',
119
- ended_at: endedAt,
120
- exit_code: null,
121
- stop_reason: 'stopped_by_request',
122
- }).eq('id', owned.sessionId);
123
- await insertMessage(row, owned.sessionId, 'system', 'system', 'Agent stopped.');
124
- await mark(row, { status: 'succeeded', completed_at: endedAt });
125
- }
126
- async function processOne(row) {
127
- const session = await findSession(row);
128
- if (session?.device_id && session.device_id !== deps.session.machineId)
129
- return;
130
- await mark(row, { status: 'processing' });
131
- if (row.action === 'ping') {
132
- await processPing(row, session);
133
- return;
134
- }
135
- if (row.action === 'stop') {
136
- await processStop(row, session);
137
- return;
138
- }
139
- await mark(row, completedPatch('failed', 'This control request is not supported by this version of the daemon.'));
140
- }
141
- async function processPending() {
142
- const { data, error } = await deps.session.client
143
- .from('agent_control_requests')
144
- .select('id, project_id, session_id, action')
145
- .eq('status', 'pending');
146
- if (error) {
147
- deps.log(`agent_control_requests scan failed: ${error.message}`);
148
- return;
149
- }
150
- for (const row of (data ?? [])) {
151
- try {
152
- await processOne(row);
153
- }
154
- catch (err) {
155
- deps.log(err instanceof Error ? err.message : `control request failed for ${row.id}`);
156
- await mark(row, completedPatch('failed', err instanceof Error ? err.message : 'Control request failed.'));
157
- }
158
- }
159
- }
160
- return { processPending };
161
- }
162
- function cleanAgentText(value) {
163
- return value.replace(/\s+/g, ' ').trim().slice(0, 4000);
164
- }
package/dist/daemon.js DELETED
@@ -1,152 +0,0 @@
1
- import { createSessionClient, heartbeat, setDeviceStatus } from './devices.js';
2
- import { loadMachineState } from './session.js';
3
- import { makeSpawner, nodeSpawnAgent } from './spawner.js';
4
- import { makeControlRequestProcessor } from './controlRequests.js';
5
- export const HEARTBEAT_INTERVAL_MS = 30_000;
6
- export const WORK_SCAN_INTERVAL_MS = 2_000;
7
- const SESSION_RETRY_INTERVAL_MS = 5_000;
8
- export async function heartbeatOnce(session, deps) {
9
- try {
10
- await deps.heartbeat(session);
11
- return session;
12
- }
13
- catch (err) {
14
- deps.log(err instanceof Error ? err.message : 'Heartbeat failed');
15
- // A re-run of `setup` may have rotated the token on disk; rebuild and let the next beat retry.
16
- try {
17
- return await deps.createSession();
18
- }
19
- catch {
20
- return session;
21
- }
22
- }
23
- }
24
- export async function runSpawnScan(spawner, log) {
25
- try {
26
- await spawner.processPending();
27
- }
28
- catch (err) {
29
- log(err instanceof Error ? err.message : 'Spawn scan failed');
30
- }
31
- }
32
- export async function runControlScan(processor, log) {
33
- try {
34
- await processor.processPending();
35
- }
36
- catch (err) {
37
- log(err instanceof Error ? err.message : 'Control request scan failed');
38
- }
39
- }
40
- export async function markRecoveredSessionsDisconnected(session, now, log) {
41
- const { data, error } = await session.client
42
- .from('sessions')
43
- .select('id, project_id')
44
- .eq('device_id', session.machineId)
45
- .eq('connection_state', 'connected')
46
- .is('ended_at', null);
47
- if (error) {
48
- log(`Session recovery scan failed: ${error.message}`);
49
- return;
50
- }
51
- const rows = (data ?? []);
52
- if (rows.length === 0)
53
- return;
54
- const endedAt = new Date(now()).toISOString();
55
- const ids = rows.map((row) => row.id);
56
- const { error: updateError } = await session.client
57
- .from('sessions')
58
- .update({
59
- connection_state: 'disconnected',
60
- ended_at: endedAt,
61
- exit_code: null,
62
- stop_reason: 'daemon_restarted',
63
- })
64
- .in('id', ids);
65
- if (updateError) {
66
- log(`Session recovery update failed: ${updateError.message}`);
67
- return;
68
- }
69
- const messages = rows.map((row) => ({
70
- project_id: row.project_id,
71
- session_id: row.id,
72
- role: 'system',
73
- kind: 'system',
74
- content: 'This session was no longer owned by the current machine helper, so Control Space marked it disconnected. Start a fresh agent to continue.',
75
- }));
76
- const { error: messageError } = await session.client.from('agent_messages').insert(messages);
77
- if (messageError)
78
- log(`Session recovery message insert failed: ${messageError.message}`);
79
- }
80
- export async function touchOwnedSessions(session, ownedSessions, now, log) {
81
- const ids = [...ownedSessions.keys()];
82
- if (ids.length === 0)
83
- return;
84
- const { error } = await session.client
85
- .from('sessions')
86
- .update({ last_seen_at: new Date(now()).toISOString() })
87
- .in('id', ids)
88
- .is('ended_at', null);
89
- if (error)
90
- log(`Session heartbeat failed: ${error.message}`);
91
- }
92
- export async function runDaemon() {
93
- const deps = {
94
- createSession: createSessionClient,
95
- heartbeat,
96
- log: (msg) => process.stderr.write(`${msg}\n`),
97
- };
98
- let session = await acquireSession(deps);
99
- let shuttingDown = false;
100
- const ownedSessions = new Map();
101
- const shutdown = async () => {
102
- if (shuttingDown)
103
- return;
104
- shuttingDown = true;
105
- try {
106
- await setDeviceStatus(session, 'disconnected');
107
- }
108
- catch {
109
- // Best effort: a hard kill is covered by heartbeat staleness.
110
- }
111
- process.exit(0);
112
- };
113
- process.once('SIGINT', () => void shutdown());
114
- process.once('SIGTERM', () => void shutdown());
115
- session = await heartbeatOnce(session, deps);
116
- await markRecoveredSessionsDisconnected(session, () => Date.now(), deps.log);
117
- let lastHeartbeatAt = Date.now();
118
- let spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
119
- let controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
120
- await runSpawnScan(spawner, deps.log);
121
- await runControlScan(controls, deps.log);
122
- while (true) {
123
- await sleep(WORK_SCAN_INTERVAL_MS);
124
- const now = Date.now();
125
- if (now - lastHeartbeatAt >= HEARTBEAT_INTERVAL_MS) {
126
- session = await heartbeatOnce(session, deps);
127
- await touchOwnedSessions(session, ownedSessions, () => now, deps.log);
128
- lastHeartbeatAt = now;
129
- }
130
- spawner = makeSpawner({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
131
- controls = makeControlRequestProcessor({ session, spawnAgent: nodeSpawnAgent, ownedSessions, now: () => Date.now(), log: deps.log });
132
- await runSpawnScan(spawner, deps.log);
133
- await runControlScan(controls, deps.log);
134
- }
135
- }
136
- async function acquireSession(deps) {
137
- while (true) {
138
- while (!loadMachineState()) {
139
- await sleep(SESSION_RETRY_INTERVAL_MS);
140
- }
141
- try {
142
- return await deps.createSession();
143
- }
144
- catch (err) {
145
- deps.log(err instanceof Error ? err.message : 'Session unavailable');
146
- await sleep(SESSION_RETRY_INTERVAL_MS);
147
- }
148
- }
149
- }
150
- function sleep(ms) {
151
- return new Promise((resolve) => setTimeout(resolve, ms));
152
- }
package/dist/devices.js DELETED
@@ -1,64 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { createSupabaseClient } from './supabase.js';
3
- import { loadMachineState, saveMachineState } from './session.js';
4
- export const NOT_CONNECTED_MESSAGE = 'This machine is not connected. Run `ctrl-spc setup` first.';
5
- export const SESSION_EXPIRED_MESSAGE = 'Your machine session expired. Run `ctrl-spc setup` to reconnect.';
6
- const REGISTER_FAILED_MESSAGE = "Connected to your account but couldn't register this machine. Check your connection and try again.";
7
- export function hashToken(token) {
8
- return createHash('sha256').update(token).digest('hex');
9
- }
10
- export function sessionToState(machineId, session) {
11
- return { machineId, accessToken: session.access_token, refreshToken: session.refresh_token };
12
- }
13
- export async function createSessionClient(deps = {}) {
14
- const loadState = deps.loadState ?? loadMachineState;
15
- const saveState = deps.saveState ?? saveMachineState;
16
- const createClient = deps.createClient ?? createSupabaseClient;
17
- const state = loadState();
18
- if (!state)
19
- throw new Error(NOT_CONNECTED_MESSAGE);
20
- const client = createClient();
21
- const machineId = state.machineId;
22
- client.auth.onAuthStateChange((_event, session) => {
23
- if (session)
24
- saveState(sessionToState(machineId, session));
25
- });
26
- const { data, error } = await client.auth.setSession({
27
- access_token: state.accessToken,
28
- refresh_token: state.refreshToken,
29
- });
30
- if (error || !data.session)
31
- throw new Error(SESSION_EXPIRED_MESSAGE);
32
- return { client, machineId };
33
- }
34
- export async function heartbeat(session) {
35
- const { error } = await session.client
36
- .from('devices')
37
- .update({ last_seen_at: new Date().toISOString(), status: 'connected' })
38
- .eq('id', session.machineId);
39
- if (error)
40
- throw new Error(`Heartbeat failed: ${error.message}`);
41
- }
42
- export async function setDeviceStatus(session, status) {
43
- const { error } = await session.client.from('devices').update({ status }).eq('id', session.machineId);
44
- if (error)
45
- throw new Error(`Update failed: ${error.message}`);
46
- }
47
- export async function upsertDevice(session, label, loadState = loadMachineState) {
48
- const { data, error: userError } = await session.client.auth.getUser();
49
- if (userError || !data.user)
50
- throw new Error(REGISTER_FAILED_MESSAGE);
51
- const state = loadState();
52
- if (!state)
53
- throw new Error(NOT_CONNECTED_MESSAGE);
54
- const { error } = await session.client.from('devices').upsert({
55
- id: session.machineId,
56
- owner_id: data.user.id,
57
- label,
58
- token_hash: hashToken(state.refreshToken),
59
- status: 'connected',
60
- last_seen_at: new Date().toISOString(),
61
- }, { onConflict: 'id' });
62
- if (error)
63
- throw new Error(REGISTER_FAILED_MESSAGE);
64
- }
@@ -1,29 +0,0 @@
1
- export const READINESS_REPLY = 'Ready to get to work';
2
- export const READINESS_PROMPT = `Respond with exactly this phrase and nothing else: ${READINESS_REPLY}`;
3
- export const PING_PROMPT = 'Reply with one short sentence confirming you are still responsive to Control Space.';
4
- // Translates the launch-config columns into the agent's headless CLI argv.
5
- // Null columns are omitted so the CLI falls back to its own default.
6
- export function assembleCommand(req, prompt = READINESS_PROMPT) {
7
- if (req.agent_type === 'codex') {
8
- const args = ['--ask-for-approval', 'on-request', 'exec'];
9
- if (req.model)
10
- args.push('--model', req.model);
11
- args.push('--sandbox', req.permission_mode ?? 'read-only');
12
- if (req.effort)
13
- args.push('-c', `model_reasoning_effort=${req.effort}`);
14
- args.push('--color', 'never');
15
- args.push(prompt);
16
- return { cmd: 'codex', args };
17
- }
18
- // Default: Claude Code.
19
- const args = ['-p', prompt];
20
- if (req.model)
21
- args.push('--model', req.model);
22
- if (req.effort)
23
- args.push('--effort', req.effort);
24
- args.push('--permission-mode', req.permission_mode ?? 'plan');
25
- if (req.allowed_tools && req.allowed_tools.length > 0) {
26
- args.push('--allowedTools', req.allowed_tools.join(','));
27
- }
28
- return { cmd: 'claude', args };
29
- }
package/dist/init.js DELETED
@@ -1,79 +0,0 @@
1
- import { stdin as input, stdout as output } from 'node:process';
2
- import * as readline from 'node:readline/promises';
3
- import { claimProject as claimWithServer } from '@ctrl-spc/mcp-server/claimResolver';
4
- import { createSupabaseClient } from './supabase.js';
5
- import { loadMachineState, saveMachineState } from './session.js';
6
- import { detectMonorepoProjects, detectRepo, parseProjectSelection } from './repo.js';
7
- import { writeMcpConfig as writeConfig } from './mcpConfig.js';
8
- export async function runInitForProjects(options) {
9
- const messages = [];
10
- for (const project of options.projects) {
11
- const result = await options.claimProject({
12
- name: project.name,
13
- remoteUrl: options.repo.normalizedRemoteUrl,
14
- localPath: project.path,
15
- fingerprint: options.repo.fingerprint,
16
- orgId: options.orgId,
17
- isMonorepoSubproject: project.path !== options.repo.root,
18
- });
19
- if (!result.ok) {
20
- messages.push(result.message);
21
- continue;
22
- }
23
- messages.push(`Project registered: ${result.projectName}`);
24
- }
25
- options.writeMcpConfig(options.repo.root);
26
- return messages;
27
- }
28
- export async function runInit(argv, cwd = process.cwd()) {
29
- const orgFlagIndex = argv.indexOf('--org');
30
- const personal = argv.includes('--personal');
31
- const orgId = orgFlagIndex >= 0 ? argv[orgFlagIndex + 1] : null;
32
- if (!personal && !orgId)
33
- throw new Error('Run init from the copied Add-project prompt so Control Space knows which org to use.');
34
- const repo = detectRepo(cwd);
35
- const detected = detectMonorepoProjects(repo.root);
36
- const projects = detected.length <= 1 ? detected : await askWhichProjects(detected);
37
- if (projects.length === 0) {
38
- console.log('No projects registered.');
39
- return;
40
- }
41
- const state = loadMachineState();
42
- if (!state)
43
- throw new Error('Run `ctrl-spc setup` first so Control Space can act as you.');
44
- const client = createSupabaseClient();
45
- const { data, error } = await client.auth.setSession({
46
- access_token: state.accessToken,
47
- refresh_token: state.refreshToken,
48
- });
49
- if (error || !data.session)
50
- throw new Error('Your machine session has expired. Run `ctrl-spc setup` again.');
51
- if (data.session.access_token !== state.accessToken ||
52
- data.session.refresh_token !== state.refreshToken) {
53
- state.accessToken = data.session.access_token;
54
- state.refreshToken = data.session.refresh_token;
55
- saveMachineState(state);
56
- }
57
- const messages = await runInitForProjects({
58
- repo,
59
- projects,
60
- orgId,
61
- claimProject: (claimInput) => claimWithServer(client, claimInput),
62
- writeMcpConfig: writeConfig,
63
- });
64
- for (const message of messages)
65
- console.log(message);
66
- console.log('MCP config written: .mcp.json');
67
- }
68
- async function askWhichProjects(projects) {
69
- console.log('Detected multiple sub-projects:');
70
- projects.forEach((project, index) => console.log(`${index + 1}. ${project.name} - ${project.path}`));
71
- const rl = readline.createInterface({ input, output });
72
- try {
73
- const answer = await rl.question('Which should Control Space register? Enter numbers separated by commas: ');
74
- return parseProjectSelection(answer, projects);
75
- }
76
- finally {
77
- rl.close();
78
- }
79
- }