clickroom 0.3.3 → 0.3.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.
- package/cli/agents.js +2 -2
- package/cli/gaps.js +17 -7
- package/cli/note.js +2 -2
- package/cli/process.js +3 -3
- package/cli/run.js +24 -0
- package/package.json +1 -1
package/cli/agents.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { execFile } from 'node:child_process';
|
|
5
4
|
import { HOME, ROOT } from './config.js';
|
|
5
|
+
import { execCmd } from './run.js';
|
|
6
6
|
|
|
7
7
|
// `npx clickroom` also connects the coding agents on the computer to clickroom's feedback (the MCP
|
|
8
8
|
// server, `clickroom mcp`, signed in with this computer's key): Claude Code for the project being
|
|
@@ -24,7 +24,7 @@ const SERVER = ['npx', '-y', VERSION ? `clickroom@${VERSION}` : 'clickroom', 'mc
|
|
|
24
24
|
const DONE = path.join(HOME, 'agents.json');
|
|
25
25
|
|
|
26
26
|
const run = (cmd, args, cwd) =>
|
|
27
|
-
new Promise((resolve) =>
|
|
27
|
+
new Promise((resolve) => execCmd(cmd, args, { cwd, timeout: 20000 }, (err, stdout) => resolve({ ok: !err, missing: err?.code === 'ENOENT', out: String(stdout || '') })));
|
|
28
28
|
|
|
29
29
|
const AGENTS = [
|
|
30
30
|
{
|
package/cli/gaps.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// "Find gaps with Claude" (Clarify): the project's clickroom terminal runs Claude Code headless in
|
|
2
2
|
// the project folder. It may only read files and ask through clickroom's MCP server, so each gap it
|
|
3
3
|
// finds lands in Clarify as a draft, live, while nothing in the folder changes.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
4
6
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
+
import { ROOT, credentials, isTeammate } from './config.js';
|
|
8
|
+
import { onPath, spawnCmd } from './run.js';
|
|
7
9
|
import { call } from './client.js';
|
|
8
10
|
import { gapsPrompt } from '../worker/handoff.js';
|
|
9
11
|
|
|
@@ -11,25 +13,33 @@ const TOOLS = ['Read', 'Glob', 'Grep', 'mcp__clickroom__clickroom_ask', 'mcp__cl
|
|
|
11
13
|
const NEVER = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'WebFetch', 'WebSearch'];
|
|
12
14
|
const LIMIT = 10 * 60 * 1000; // a run that hangs is stopped
|
|
13
15
|
|
|
14
|
-
export const hasClaude = () =>
|
|
16
|
+
export const hasClaude = () => onPath('claude');
|
|
17
|
+
|
|
18
|
+
// Whose computer this is, for the review: the run is asked for from someone else's browser.
|
|
19
|
+
const computer = () => (isTeammate() && credentials()?.name ? `${credentials().name}’s computer` : 'the computer');
|
|
15
20
|
|
|
16
21
|
const questionCount = async (name) =>
|
|
17
22
|
(await call('GET', `/__ps/api/projects/${encodeURIComponent(name)}/threads`)).threads.filter((t) => t.kind === 'question').length;
|
|
18
23
|
|
|
19
24
|
// Resolves to the number of questions it added (drafts), or throws.
|
|
20
25
|
export async function findGaps(name, root) {
|
|
21
|
-
if (!hasClaude()) throw new Error(
|
|
26
|
+
if (!hasClaude()) throw new Error(`Claude Code (\`claude\`) isn’t installed on ${computer()}, which runs \`clickroom\` for this project.`);
|
|
22
27
|
const before = await questionCount(name);
|
|
23
|
-
|
|
24
|
-
const
|
|
28
|
+
// The MCP config as a file and the prompt through stdin: both survive Windows' shell.
|
|
29
|
+
const mcp = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'clickroom-gaps-')), 'mcp.json');
|
|
30
|
+
fs.writeFileSync(mcp, JSON.stringify({ mcpServers: { clickroom: { command: process.execPath, args: [path.join(ROOT, 'bin', 'clickroom.js'), 'mcp'] } } }));
|
|
31
|
+
const args = ['-p', '--mcp-config', mcp, '--strict-mcp-config', '--tools', 'Read,Glob,Grep', '--allowedTools', TOOLS.join(','), '--disallowedTools', NEVER.join(','), '--no-session-persistence'];
|
|
25
32
|
await new Promise((resolve, reject) => {
|
|
26
|
-
const child =
|
|
33
|
+
const child = spawnCmd('claude', args, { cwd: root, stdio: ['pipe', 'ignore', 'pipe'] });
|
|
34
|
+
child.stdin.on('error', () => {});
|
|
35
|
+
child.stdin.end(gapsPrompt(name));
|
|
27
36
|
let err = '';
|
|
28
37
|
child.stderr.on('data', (d) => (err = (err + d).slice(-2000)));
|
|
29
38
|
const timer = setTimeout(() => child.kill('SIGTERM'), LIMIT);
|
|
30
39
|
child.on('error', reject);
|
|
31
40
|
child.on('exit', (code) => {
|
|
32
41
|
clearTimeout(timer);
|
|
42
|
+
fs.rmSync(path.dirname(mcp), { recursive: true, force: true });
|
|
33
43
|
code === 0 ? resolve() : reject(new Error(err.trim().split('\n').pop() || `claude exited with ${code}`));
|
|
34
44
|
});
|
|
35
45
|
});
|
package/cli/note.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnCmd } from './run.js';
|
|
2
2
|
import { call } from './client.js';
|
|
3
3
|
import { changedSource, folderChanges } from './changes.js';
|
|
4
4
|
|
|
@@ -55,7 +55,7 @@ function askClaude(input) {
|
|
|
55
55
|
let out = '';
|
|
56
56
|
let child;
|
|
57
57
|
try {
|
|
58
|
-
child =
|
|
58
|
+
child = spawnCmd('claude', ['-p', '--model', 'haiku', '--tools', '', '--no-session-persistence', '--setting-sources', '', '--system-prompt', PROMPT], { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
59
59
|
} catch {
|
|
60
60
|
return resolve(null);
|
|
61
61
|
}
|
package/cli/process.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// answers to questions), writes one brief into the project and starts Claude Code on it.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { onPath, spawnCmd } from './run.js';
|
|
6
6
|
import { call } from './client.js';
|
|
7
7
|
import { buildBrief } from '../worker/handoff.js';
|
|
8
8
|
|
|
@@ -33,14 +33,14 @@ export function writeBrief(root, text) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function hasClaude() {
|
|
36
|
-
return
|
|
36
|
+
return onPath('claude');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Interactive Claude Code in the project, so the person watches and approves the changes.
|
|
40
40
|
export function startClaude(root, name) {
|
|
41
41
|
const prompt = `Process the clickroom feedback in ${BRIEF} for project "${name}". Read it first, then work through every item.`;
|
|
42
42
|
return new Promise((resolve, reject) => {
|
|
43
|
-
const child =
|
|
43
|
+
const child = spawnCmd('claude', [prompt], { cwd: root, stdio: 'inherit' });
|
|
44
44
|
child.on('error', reject);
|
|
45
45
|
child.on('exit', (code) => resolve(code ?? 0));
|
|
46
46
|
});
|
package/cli/run.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn, execFile, spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
// Starting the coding agents' commands (claude, codex) on every OS. On Windows they are often
|
|
4
|
+
// npm's `.cmd` launchers, which Node only starts through the shell (cmd.exe): there each argument
|
|
5
|
+
// is quoted, double quotes become single ones and line breaks spaces (cmd can't take them), so
|
|
6
|
+
// anything long or multi-line goes in through stdin or a file instead.
|
|
7
|
+
|
|
8
|
+
const WIN = process.platform === 'win32';
|
|
9
|
+
const quote = (a) => {
|
|
10
|
+
const s = String(a).replace(/\r?\n/g, ' ').replace(/"/g, "'");
|
|
11
|
+
return s === '' || /[\s&|<>^()%!,;=]/.test(s) ? `"${s}"` : s;
|
|
12
|
+
};
|
|
13
|
+
const shellArgs = (args) => (WIN ? args.map(quote) : args);
|
|
14
|
+
|
|
15
|
+
// Is the command there? (On Windows the shell can't tell us: a missing command just fails.)
|
|
16
|
+
export const onPath = (cmd) => spawnSync(WIN ? 'where' : 'which', [cmd], { stdio: 'ignore', windowsHide: true }).status === 0;
|
|
17
|
+
|
|
18
|
+
export const spawnCmd = (cmd, args, opts = {}) => spawn(cmd, shellArgs(args), { windowsHide: true, ...opts, shell: WIN });
|
|
19
|
+
|
|
20
|
+
// execFile's callback, with `missing` when the command isn't installed (ENOENT elsewhere).
|
|
21
|
+
export function execCmd(cmd, args, opts, cb) {
|
|
22
|
+
if (WIN && !onPath(cmd)) return cb(Object.assign(new Error(`${cmd} not found`), { code: 'ENOENT' }), '');
|
|
23
|
+
return execFile(cmd, shellArgs(args), { windowsHide: true, ...opts, shell: WIN }, cb);
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clickroom",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Share prototype builds for review: pins, region comments, threads, versions, and agent hand-off (MCP). Teammates push builds to their team’s clickroom with `clickroom login`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|