@ctrl-spc/cs 0.7.1 → 0.7.2
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 +1 -1
- package/dist/codex-home.js +10 -6
- package/dist/config.js +17 -0
- package/dist/index.js +93 -12
- package/dist/mcp.js +15 -6
- package/dist/panel3/answer.js +12 -12
- package/dist/panel3/checkout.js +522 -2
- package/dist/panel3/cli.js +49 -62
- package/dist/panel3/client.js +53 -16
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/prompt.js +146 -3
- package/dist/panel3/run.js +638 -71
- package/dist/panel3/say.js +10 -10
- package/dist/panel3/show.js +195 -22
- package/dist/panel3/spawn.js +5 -2
- package/dist/panel3/tools.js +326 -5
- package/dist/presence.js +8 -0
- package/dist/skills.js +165 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ cs Open the Companion app (the front door)
|
|
|
43
43
|
cs open Open the Companion app in your browser
|
|
44
44
|
cs login Sign in from the terminal and link this computer
|
|
45
45
|
cs start Come online now, no window (used by auto-start)
|
|
46
|
-
cs status Show
|
|
46
|
+
cs status Show the whole setup state and the one next step to take
|
|
47
47
|
cs autostart on Come online automatically at login
|
|
48
48
|
cs autostart off Stop coming online at login
|
|
49
49
|
cs logout Sign this computer out
|
package/dist/codex-home.js
CHANGED
|
@@ -195,11 +195,15 @@ export function codexRunConfigToml(server, runTodoId, runtimePlatform = process.
|
|
|
195
195
|
* codex worker reached for it instead of this product's `dispatch` tool;
|
|
196
196
|
* no level 3 run was ever started through the record.
|
|
197
197
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* the
|
|
201
|
-
*
|
|
202
|
-
*
|
|
198
|
+
* `[agents] enabled = false` is the ONLY key that removes it, and it is
|
|
199
|
+
* MEASURED, never read off documentation. `features.multi_agent = false` is
|
|
200
|
+
* the trap: it parses, it reads like the answer, and the tool is still there.
|
|
201
|
+
* Re-measured 2026-08-25 by spawning codex with each config and asking it to
|
|
202
|
+
* delegate: with `multi_agent = false` the stream still carries
|
|
203
|
+
* `collab_tool_call`, and the agent then quietly did the work itself; with
|
|
204
|
+
* `[agents] enabled = false` the tool is gone and the run starts normally.
|
|
205
|
+
* Checked on 0.149.0-alpha.4.1 (macOS) and 0.148.0-alpha.8 (Windows), so the
|
|
206
|
+
* claim that a newer runtime rejects this key does not hold on either.
|
|
203
207
|
*
|
|
204
208
|
* v2's claude workers are deliberately still granted `Task` (see
|
|
205
209
|
* `orchestrator.ts`), so this is an EXPLICIT argument rather than a change to
|
|
@@ -228,8 +232,8 @@ subagentsEnabled = true, persistentPanelOwner = false) {
|
|
|
228
232
|
/* The account's own connectors, which no config.toml grants and no argv
|
|
229
233
|
removes. See the block comment above. */
|
|
230
234
|
'apps = false',
|
|
231
|
-
...(!subagentsEnabled ? ['multi_agent = false'] : []),
|
|
232
235
|
'',
|
|
236
|
+
...(!subagentsEnabled ? ['[agents]', 'enabled = false', ''] : []),
|
|
233
237
|
];
|
|
234
238
|
/* Native Windows must name its sandbox implementation. Without this block,
|
|
235
239
|
codex silently resolves a requested workspace-write run as read-only and
|
package/dist/config.js
CHANGED
|
@@ -234,6 +234,23 @@ export function companionToken() {
|
|
|
234
234
|
export function mcpToken() {
|
|
235
235
|
return persistentToken('mcp-token');
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The stored mcp token, or null when this install has never minted one. Unlike
|
|
239
|
+
* mcpToken(), it never creates the file — a read-only command (`cs status`)
|
|
240
|
+
* must not mint a token as a side effect of reporting state, and "no token" is
|
|
241
|
+
* itself the honest answer to whether anything could be registered yet.
|
|
242
|
+
*/
|
|
243
|
+
export function readMcpToken() {
|
|
244
|
+
const path = filePath('mcp-token');
|
|
245
|
+
if (!existsSync(path))
|
|
246
|
+
return null;
|
|
247
|
+
try {
|
|
248
|
+
return readFileSync(path, 'utf8').trim() || null;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
237
254
|
function persistentToken(name) {
|
|
238
255
|
const path = filePath(name);
|
|
239
256
|
if (existsSync(path)) {
|
package/dist/index.js
CHANGED
|
@@ -4,33 +4,106 @@ import { runDaemon } from './daemon.js';
|
|
|
4
4
|
import { openCompanion } from './companion.js';
|
|
5
5
|
import { autostartOn, autostartOff } from './autostart.js';
|
|
6
6
|
import { detectAgents } from './agents.js';
|
|
7
|
-
import { getMachineIdentity, clearSession } from './config.js';
|
|
8
|
-
import { getClient
|
|
7
|
+
import { getMachineIdentity, clearSession, readSession } from './config.js';
|
|
8
|
+
import { getClient } from './supabase.js';
|
|
9
|
+
import { foreignToolsServerAlive, claudeRegisteredOnDisk, codexRegisteredOnDisk } from './mcp.js';
|
|
10
|
+
import { panelCommand } from './panel3/cli.js';
|
|
9
11
|
const HELP = `cs — CTRL+SPC
|
|
10
12
|
|
|
11
13
|
cs Open the Companion app (the front door)
|
|
12
14
|
cs open Open the Companion app in your browser
|
|
13
15
|
cs login Sign in from the terminal and link this computer
|
|
14
16
|
cs start Come online and answer cards, no window (used by auto-start)
|
|
15
|
-
cs status Show
|
|
17
|
+
cs status Show the whole setup state and the one next step to take
|
|
16
18
|
cs autostart on Come online automatically at login
|
|
17
19
|
cs autostart off Stop coming online at login
|
|
18
20
|
cs logout Sign this computer out
|
|
21
|
+
|
|
22
|
+
cs say "<text>" Start a card and put your message on it
|
|
23
|
+
cs say --project <p> "<text>" Start it filed under that project (id or name)
|
|
24
|
+
cs say --card <id> "<text>" Add a message to a card you already have
|
|
25
|
+
cs answer <id> "<text>" Answer a question a card is waiting on you for
|
|
26
|
+
cs show Every card
|
|
27
|
+
cs show <id> One card in full, or one run with its report
|
|
28
|
+
|
|
19
29
|
cs help Show this help
|
|
20
30
|
`;
|
|
31
|
+
/**
|
|
32
|
+
* The whole setup state, and the ONE next step to take, for an agent — the
|
|
33
|
+
* reader of this command in every story is Claude Code or Codex, not a person
|
|
34
|
+
* at a prompt, so every step below addresses the agent and names the user only
|
|
35
|
+
* for sign-in, which is the one step only the user can take.
|
|
36
|
+
*
|
|
37
|
+
* A non-zero exit means "not fully set up", NEVER "a command failed". A machine
|
|
38
|
+
* mid-setup exits 1 while everything is working correctly.
|
|
39
|
+
*/
|
|
21
40
|
async function status() {
|
|
22
41
|
const id = getMachineIdentity();
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
const installed = detectAgents();
|
|
43
|
+
/* Signed-out is a FILE question, not a network one. getClient() throws
|
|
44
|
+
NotLoggedIn whenever setSession fails FOR ANY REASON, including a network
|
|
45
|
+
failure (supabase.ts), so trusting it would tell a signed-in user on bad
|
|
46
|
+
wifi to go and sign in again — the confident-wrong instruction this feature
|
|
47
|
+
exists to kill. readSession() returning null is the only true "signed out". */
|
|
48
|
+
const stored = readSession();
|
|
49
|
+
let signedIn = false;
|
|
50
|
+
let email = null;
|
|
51
|
+
let sessionProblem = null;
|
|
52
|
+
if (stored) {
|
|
53
|
+
try {
|
|
54
|
+
const client = await getClient();
|
|
55
|
+
const { data } = await client.auth.getUser();
|
|
56
|
+
email = data.user?.email ?? null;
|
|
57
|
+
signedIn = true;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
/* The REASON only. getClient()'s NotLoggedIn message ends with its own
|
|
61
|
+
"Run `cs login` again.", which would print twice in the step below. */
|
|
62
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
63
|
+
sessionProblem = raw.replace(/\s*Run `cs login` again\.?\s*$/, '').replace(/\.$/, '');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const serving = await foreignToolsServerAlive();
|
|
67
|
+
/* Gated on `serving` because a plain shutdown DELIBERATELY leaves the agent
|
|
68
|
+
config entries in place (presence.ts), so an ungated read prints
|
|
69
|
+
"registered" against a daemon that is not there. */
|
|
70
|
+
const registered = {
|
|
71
|
+
claude: serving && claudeRegisteredOnDisk(),
|
|
72
|
+
codex: serving && codexRegisteredOnDisk(),
|
|
73
|
+
};
|
|
74
|
+
console.log(`Computer: ${id.name}`);
|
|
75
|
+
console.log(`Signed in: ${signedIn ? (email ?? 'unknown') : stored ? 'saved sign-in unusable' : 'Not signed in'}`);
|
|
76
|
+
console.log(`Daemon: ${serving ? 'running' : 'not running'}`);
|
|
77
|
+
console.log(`Agents: ${installed.length ? installed.join(', ') : 'none installed'}`);
|
|
78
|
+
for (const agent of installed) {
|
|
79
|
+
console.log(` ${agent.padEnd(8)} ${registered[agent] ? 'registered' : 'not registered'}`);
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
const unregistered = installed.filter((a) => !registered[a]);
|
|
83
|
+
if (!stored) {
|
|
84
|
+
console.log('Nobody is signed in. Ask the user to run `cs login` themselves: it opens a');
|
|
85
|
+
console.log('browser and waits up to five minutes, so do not run it yourself. They sign in');
|
|
86
|
+
console.log('with the same email and password they use on ctrl-spc.com. Then run `cs status`');
|
|
87
|
+
console.log('again.');
|
|
88
|
+
}
|
|
89
|
+
else if (!signedIn) {
|
|
90
|
+
console.log(`The saved sign-in could not be used: ${sessionProblem ?? 'unknown'}. Ask the user to run`);
|
|
91
|
+
console.log('`cs login` again.');
|
|
92
|
+
}
|
|
93
|
+
else if (!serving) {
|
|
94
|
+
console.log('Nothing is serving the tools. `cs start` never exits on its own, so start it in');
|
|
95
|
+
console.log('the background and leave it running, then run `cs status` again.');
|
|
96
|
+
}
|
|
97
|
+
else if (unregistered.length) {
|
|
98
|
+
console.log(`The tools are being served but are not registered into ${unregistered.join(' or ')}. Restart the`);
|
|
99
|
+
console.log('background daemon, then run `cs status` again.');
|
|
30
100
|
}
|
|
31
|
-
|
|
32
|
-
console.log(
|
|
101
|
+
else {
|
|
102
|
+
console.log('Tools are registered. If this agent cannot see them, restart it. An agent picks');
|
|
103
|
+
console.log('up MCP servers only when it starts.');
|
|
104
|
+
return;
|
|
33
105
|
}
|
|
106
|
+
process.exitCode = 1;
|
|
34
107
|
}
|
|
35
108
|
async function main() {
|
|
36
109
|
const cmd = process.argv[2];
|
|
@@ -44,6 +117,14 @@ async function main() {
|
|
|
44
117
|
case 'logout':
|
|
45
118
|
console.log(clearSession() ? 'Signed out.' : 'Was not signed in.');
|
|
46
119
|
return;
|
|
120
|
+
/* THE PANEL'S OWN COMMANDS, ROUTED WHOLE. The person types one CLI, so the
|
|
121
|
+
card commands are `cs` subcommands; the argument handling stays inside
|
|
122
|
+
`panel3/`, which is the one import this file makes into it (named in
|
|
123
|
+
`test/panel3-isolation.contract.test.mjs`). */
|
|
124
|
+
case 'say':
|
|
125
|
+
case 'answer':
|
|
126
|
+
case 'show':
|
|
127
|
+
return panelCommand(process.argv.slice(2));
|
|
47
128
|
case 'autostart':
|
|
48
129
|
if (arg === 'on')
|
|
49
130
|
return autostartOn();
|
package/dist/mcp.js
CHANGED
|
@@ -13,7 +13,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
import { TOOLS_SERVER_PORT, SESSION_TTL_MS } from './env.js';
|
|
15
15
|
import { agentPath } from './agents.js';
|
|
16
|
-
import { mcpToken, readSession } from './config.js';
|
|
16
|
+
import { mcpToken, readMcpToken, readSession } from './config.js';
|
|
17
17
|
import { readPngScreenshot, } from './screenshots.js';
|
|
18
18
|
/* 18c Slice 7 correction: the product captures the PNG itself, because a
|
|
19
19
|
spawned worker has no reachable way to run a capture command. The whole
|
|
@@ -13135,7 +13135,7 @@ let foreignProbeCache = null;
|
|
|
13135
13135
|
/** Whether ANOTHER process's ctrl-spc tools server answers /health on the fixed
|
|
13136
13136
|
* port. Never throws; refusals, timeouts, and non-ctrl-spc answers all read as
|
|
13137
13137
|
* "not alive" (→ the badge keeps today's failed semantics). */
|
|
13138
|
-
async function foreignToolsServerAlive(fetchImpl) {
|
|
13138
|
+
export async function foreignToolsServerAlive(fetchImpl = fetch) {
|
|
13139
13139
|
const now = Date.now();
|
|
13140
13140
|
if (foreignProbeCache &&
|
|
13141
13141
|
foreignProbeCache.fetchImpl === fetchImpl &&
|
|
@@ -13162,14 +13162,20 @@ async function foreignToolsServerAlive(fetchImpl) {
|
|
|
13162
13162
|
* `claude mcp add --scope user` writes) carries a `ctrl-spc` entry whose url
|
|
13163
13163
|
* matches the CURRENT mcp token and tools port. A stale entry from an old
|
|
13164
13164
|
* token or port does not count — it would 401 against the live server. */
|
|
13165
|
-
function claudeRegisteredOnDisk() {
|
|
13165
|
+
export function claudeRegisteredOnDisk() {
|
|
13166
13166
|
try {
|
|
13167
|
+
// readMcpToken, never mcpToken: `cs status` must not mint a token as a side
|
|
13168
|
+
// effect of a read. No token on disk means this install never launched, so
|
|
13169
|
+
// nothing can be registered against it either way.
|
|
13170
|
+
const token = readMcpToken();
|
|
13171
|
+
if (!token)
|
|
13172
|
+
return false;
|
|
13167
13173
|
const raw = readFileSync(join(homedir(), '.claude.json'), 'utf8');
|
|
13168
13174
|
const parsed = JSON.parse(raw);
|
|
13169
13175
|
const url = parsed?.mcpServers?.['ctrl-spc']?.url;
|
|
13170
13176
|
return (typeof url === 'string' &&
|
|
13171
13177
|
url.includes(`127.0.0.1:${TOOLS_SERVER_PORT}/mcp`) &&
|
|
13172
|
-
url.includes(`token=${
|
|
13178
|
+
url.includes(`token=${token}`));
|
|
13173
13179
|
}
|
|
13174
13180
|
catch {
|
|
13175
13181
|
return false;
|
|
@@ -13260,11 +13266,14 @@ export function ensureCodexMcpApproval(path = codexConfigPath()) {
|
|
|
13260
13266
|
* written (current token + port). A substring check on purpose: it holds
|
|
13261
13267
|
* regardless of how Codex quotes/normalizes the TOML around it, and the
|
|
13262
13268
|
* tokened url is unique to this install. */
|
|
13263
|
-
function codexRegisteredOnDisk() {
|
|
13269
|
+
export function codexRegisteredOnDisk() {
|
|
13264
13270
|
try {
|
|
13271
|
+
const token = readMcpToken(); // see claudeRegisteredOnDisk: never mint here
|
|
13272
|
+
if (!token)
|
|
13273
|
+
return false;
|
|
13265
13274
|
const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
13266
13275
|
const raw = readFileSync(join(codexHome, 'config.toml'), 'utf8');
|
|
13267
|
-
return raw.includes(`http://127.0.0.1:${TOOLS_SERVER_PORT}/mcp?token=${
|
|
13276
|
+
return raw.includes(`http://127.0.0.1:${TOOLS_SERVER_PORT}/mcp?token=${token}`);
|
|
13268
13277
|
}
|
|
13269
13278
|
catch {
|
|
13270
13279
|
return false;
|
package/dist/panel3/answer.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ═══ AGENT PANEL v3: `
|
|
2
|
+
* ═══ AGENT PANEL v3: `cs answer`, the person settling a question. ═══
|
|
3
3
|
*
|
|
4
4
|
* THIS FILE BELONGS TO AGENT PANEL v3. Nothing outside `src/panel3/` may
|
|
5
5
|
* import it.
|
|
6
6
|
*
|
|
7
7
|
* ---------------------------------------------------------------------------
|
|
8
|
-
* ═══ IT IS `
|
|
8
|
+
* ═══ IT IS `cs say`'s SIBLING, AND IT IS A SEPARATE COMMAND ON PURPOSE. ═══
|
|
9
9
|
*
|
|
10
10
|
* Both are the person putting something on the record and returning, with no
|
|
11
11
|
* spawn to wait for. They differ in what the record does with it, and that
|
|
12
12
|
* difference is exactly why they are not one command: a message is new work, and
|
|
13
13
|
* an answer is the thing one particular piece of stopped work was waiting for.
|
|
14
|
-
* `
|
|
14
|
+
* `cs say --card <id> "the answer"` would put the words on the card as a fresh
|
|
15
15
|
* message, leave the question open, and leave the run that asked it stopped
|
|
16
16
|
* forever — which is the fenced-off answer ux.md spends a section on, arrived at
|
|
17
17
|
* by the shortest route.
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
// `import type` loses its v3 header in the published `dist/`.
|
|
49
49
|
import { at, out, returned, signedInClient } from './client.js';
|
|
50
50
|
import { ASK_CONTENT_COLUMNS, withAskContent } from './show.js';
|
|
51
|
-
const USAGE = 'usage:
|
|
51
|
+
const USAGE = 'usage: cs answer <question-id> "<text>"';
|
|
52
52
|
/**
|
|
53
|
-
* The two arguments, and a second bare word refused rather than joined: `
|
|
53
|
+
* The two arguments, and a second bare word refused rather than joined: `cs
|
|
54
54
|
* answer <id> yes it is` is an answer the shell already split, and gluing it back
|
|
55
55
|
* together would be this command guessing at what the person typed. The same rule
|
|
56
|
-
* `
|
|
56
|
+
* `cs say` applies, for the same reason.
|
|
57
57
|
*/
|
|
58
58
|
function parseArgs(args) {
|
|
59
59
|
const words = args.filter((a) => !a.startsWith('--'));
|
|
@@ -81,7 +81,7 @@ function parseArgs(args) {
|
|
|
81
81
|
*
|
|
82
82
|
* ═══ AND THE WAITING ONE IS ONLY SAID WHEN IT IS TRUE. ═══ It used to say the
|
|
83
83
|
* question "may yet be settled without troubling you" whatever the record said —
|
|
84
|
-
* including for a question `
|
|
84
|
+
* including for a question `cs show` had already called stalled, because the
|
|
85
85
|
* agent it went to had ended without dealing with it. Two surfaces, opposite
|
|
86
86
|
* claims, and the one the person can act on was the one that was wrong. Those
|
|
87
87
|
* are now answerable, so the only refusal left here is the honest one: it has
|
|
@@ -94,15 +94,15 @@ async function whyNot(client, askId) {
|
|
|
94
94
|
.eq('id', askId), 'read', `question ${askId}`));
|
|
95
95
|
const ask = asks[0];
|
|
96
96
|
if (!ask) {
|
|
97
|
-
return `there is no question with id ${askId}. \`
|
|
97
|
+
return `there is no question with id ${askId}. \`cs show\` lists the cards and the questions on them.`;
|
|
98
98
|
}
|
|
99
99
|
if (ask.answered_at) {
|
|
100
100
|
return (`that question was already answered ${at(ask.answered_at)}, with:\n ${ask.answer ?? '(nothing)'}\n`
|
|
101
101
|
+ 'Answering it again would rewrite a decision the work has already been started on. Send a '
|
|
102
|
-
+ `message instead:
|
|
102
|
+
+ `message instead: cs say --card ${ask.card_id} "<text>"`);
|
|
103
103
|
}
|
|
104
104
|
return ('that question has not reached you: it is with the work it came from, which has not had its go '
|
|
105
|
-
+ 'at it yet and may settle it without troubling you. `
|
|
105
|
+
+ 'at it yet and may settle it without troubling you. `cs show` says who has it, and it becomes '
|
|
106
106
|
+ 'yours to answer if they end without dealing with it.');
|
|
107
107
|
}
|
|
108
108
|
export async function answer(args) {
|
|
@@ -151,7 +151,7 @@ export function answeredLines(askId, cardId, state) {
|
|
|
151
151
|
`card ${cardId} in hand again`,
|
|
152
152
|
'',
|
|
153
153
|
' Whoever was waiting on this is started again with it, wherever they had got to.',
|
|
154
|
-
`
|
|
154
|
+
` cs show ${cardId}`,
|
|
155
155
|
];
|
|
156
156
|
}
|
|
157
157
|
return [
|
|
@@ -161,6 +161,6 @@ export function answeredLines(askId, cardId, state) {
|
|
|
161
161
|
' NOBODY IS BEING STARTED FOR THIS. The answer is on the record and nothing is waiting for',
|
|
162
162
|
' it, because the work that asked was stopped. Send a message if you want this card picked',
|
|
163
163
|
' up again:',
|
|
164
|
-
`
|
|
164
|
+
` cs say --card ${cardId} "<text>"`,
|
|
165
165
|
];
|
|
166
166
|
}
|