product-console-companion 0.3.0 → 0.5.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 +10 -6
- package/dist/api-key-caller.js +10 -1
- package/dist/claude-cli-caller.js +30 -18
- package/dist/cli.js +37 -31
- package/dist/config.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,12 +9,12 @@ your own machine, so:
|
|
|
9
9
|
`MAX_FILE_CHARS`/`MAX_TREE_PATHS` don't apply here at all);
|
|
10
10
|
- AI spend bills against your own key/subscription, not the workspace's shared/BYOK credential.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
Runs every AI task the hosted Console dispatches. That includes the repo-aware setup and code
|
|
13
|
+
checks as well as graph reasoning such as impact analysis, reviews, coupling maps, graph Q&A,
|
|
14
|
+
edge descriptions, and exports. Graph-reasoning tasks receive all of their context from the
|
|
15
|
+
Console and run connectionless with zero filesystem access. New server-side graph-reasoning task
|
|
16
|
+
kinds use the same safe fallback without requiring another companion release. Two backends are
|
|
17
|
+
available, chosen per connection at `connect` time:
|
|
18
18
|
|
|
19
19
|
- **`api-key`** (default) — no CLI dependency. Calls Anthropic directly with your own API key
|
|
20
20
|
(`api-key-caller.ts`), running its own two-pass file-select-then-judge, mirroring
|
|
@@ -63,6 +63,10 @@ npx product-console-companion connect --url https://console.fynchconsulting.com
|
|
|
63
63
|
|
|
64
64
|
This runs your workspace's PRD-only setup AI calls when there is no repo yet. `run` then polls
|
|
65
65
|
for workspace-level tasks in addition to any per-repo connections configured on this machine.
|
|
66
|
+
Pass `--backend cli` to use your Claude subscription for these repo-less tasks too. This mode
|
|
67
|
+
runs with zero filesystem access (stricter than the repo-scoped `cli` backend, because there is
|
|
68
|
+
nothing to read). PDF-format PRDs cannot be delivered to the `cli` backend yet; use `api-key`,
|
|
69
|
+
or re-upload the PRD as `.md`, `.txt`, or `.docx`.
|
|
66
70
|
|
|
67
71
|
**`--backend api-key`** (default) needs an Anthropic API key (get one free at
|
|
68
72
|
console.anthropic.com) — either pass it inline with `--api-key sk-ant-...` (saved to this
|
package/dist/api-key-caller.js
CHANGED
|
@@ -300,7 +300,16 @@ ${repoCtx.packageJson ?? '(none found)'}
|
|
|
300
300
|
const text = message.content.find((b) => b.type === 'text');
|
|
301
301
|
if (!text || text.type !== 'text')
|
|
302
302
|
throw new Error('No result returned.');
|
|
303
|
-
return { result: JSON.parse(text.text), filesRead: [] };
|
|
303
|
+
return { result: spec.schema ? JSON.parse(text.text) : text.text, filesRead: [] };
|
|
304
|
+
}
|
|
305
|
+
/** Forward-compatible fallback for server-created graph/spec reasoning tasks. The server sends
|
|
306
|
+
* the complete self-describing spec, so these calls never need or receive filesystem context. */
|
|
307
|
+
export async function runGenericTask(client, task, repoPath) {
|
|
308
|
+
const spec = task.payload?.spec;
|
|
309
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
310
|
+
throw new Error(`Cannot run task kind "${task.kind}": payload.spec is missing or not an object.`);
|
|
311
|
+
}
|
|
312
|
+
return runShallowRepoTask(client, spec, repoPath, true);
|
|
304
313
|
}
|
|
305
314
|
export async function runProposeSkeletonTask(client, task, repoPath) {
|
|
306
315
|
const payload = task.payload;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* during development, not guessed from --help text alone — see the Phase 2 PR description.
|
|
12
12
|
*/
|
|
13
13
|
import spawn from 'cross-spawn';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
14
15
|
import { createInterface } from 'node:readline';
|
|
15
16
|
import { createSandbox } from './sandbox.js';
|
|
16
17
|
import { appendDiffToVolatile, prepareShipmentAnalysis } from './shipment.js';
|
|
@@ -26,13 +27,18 @@ function normalizeToRepoRelative(filePath, root) {
|
|
|
26
27
|
const r = root.replace(/\\/g, '/');
|
|
27
28
|
return (p.startsWith(r) ? p.slice(r.length) : p).replace(/^\/+/, '');
|
|
28
29
|
}
|
|
29
|
-
/** The actual invocation — task-kind-agnostic.
|
|
30
|
-
* worktree of `repoPath
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
/** The actual invocation — task-kind-agnostic. Repo-scoped calls run against a disposable
|
|
31
|
+
* sandbox worktree of `repoPath`; connection-less calls grant no tools and need no sandbox.
|
|
32
|
+
* Returns whatever structured_output the model produced plus every file it actually read via
|
|
33
|
+
* the Read tool. Every exported per-kind function below is a thin wrapper over this. */
|
|
34
|
+
async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
35
|
+
if (spec.stableDocumentBase64) {
|
|
36
|
+
throw new Error("This task includes a PDF PRD, which the cli backend can't read yet. Pair with " +
|
|
37
|
+
'--backend api-key instead, or re-upload the PRD as .md/.txt/.docx.');
|
|
38
|
+
}
|
|
39
|
+
const sandbox = connectionless
|
|
40
|
+
? { path: tmpdir(), cleanup: async () => { } }
|
|
41
|
+
: await createSandbox(repoPath);
|
|
36
42
|
try {
|
|
37
43
|
const args = [
|
|
38
44
|
'-p',
|
|
@@ -43,10 +49,7 @@ async function runViaCli(spec, maxBudgetUsd, repoPath) {
|
|
|
43
49
|
'--verbose',
|
|
44
50
|
...(spec.schema ? ['--json-schema', JSON.stringify(spec.schema)] : []),
|
|
45
51
|
'--restricted',
|
|
46
|
-
'--tools',
|
|
47
|
-
'Read,Grep,Glob',
|
|
48
|
-
'--add-dir',
|
|
49
|
-
sandbox.path,
|
|
52
|
+
...(connectionless ? ['--tools', ''] : ['--tools', 'Read,Grep,Glob', '--add-dir', sandbox.path]),
|
|
50
53
|
'--model',
|
|
51
54
|
spec.model,
|
|
52
55
|
'--effort',
|
|
@@ -66,6 +69,8 @@ async function runViaCli(spec, maxBudgetUsd, repoPath) {
|
|
|
66
69
|
let stderrTail = '';
|
|
67
70
|
let timedOut = false;
|
|
68
71
|
const child = spawn('claude', args, { cwd: sandbox.path });
|
|
72
|
+
if (spec.stableText)
|
|
73
|
+
child.stdin.write(spec.stableText + '\n\n');
|
|
69
74
|
child.stdin.write(spec.volatileText);
|
|
70
75
|
child.stdin.end();
|
|
71
76
|
// stdout/stderr are only null when stdio is explicitly configured away from 'pipe' — the
|
|
@@ -130,24 +135,31 @@ async function runViaCli(spec, maxBudgetUsd, repoPath) {
|
|
|
130
135
|
await sandbox.cleanup();
|
|
131
136
|
}
|
|
132
137
|
}
|
|
138
|
+
/** Forward-compatible fallback for server-created graph/spec reasoning tasks. The server sends
|
|
139
|
+
* the complete self-describing spec, so these calls run connectionless with zero tools. */
|
|
140
|
+
export async function runGenericTaskViaCli(task, repoPath) {
|
|
141
|
+
const spec = task.payload?.spec;
|
|
142
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
143
|
+
throw new Error(`Cannot run task kind "${task.kind}": payload.spec is missing or not an object.`);
|
|
144
|
+
}
|
|
145
|
+
return runViaCli(spec, task.maxBudgetUsd, repoPath, true);
|
|
146
|
+
}
|
|
133
147
|
export async function runVerifyModuleTaskViaCli(task, repoPath) {
|
|
134
148
|
const payload = task.payload;
|
|
135
149
|
return runViaCli(payload.spec, task.maxBudgetUsd, repoPath);
|
|
136
150
|
}
|
|
137
151
|
/** moduleIds isn't needed here the way api-key-caller.ts's batch path needs it (to structure its
|
|
138
|
-
* own file-select pass) —
|
|
139
|
-
* alone, which already names every module in the batch. */
|
|
152
|
+
* own file-select pass) — spec.volatileText already names every module in the batch. */
|
|
140
153
|
export async function runGenerateSpecBatchTaskViaCli(task, repoPath) {
|
|
141
154
|
const payload = task.payload;
|
|
142
|
-
return runViaCli(payload.spec, task.maxBudgetUsd, repoPath);
|
|
155
|
+
return runViaCli(payload.spec, task.maxBudgetUsd, repoPath, payload.connectionless);
|
|
143
156
|
}
|
|
144
157
|
/** propose_skeleton and propose_new_capabilities are "shallow whole-repo" tasks (see
|
|
145
|
-
* api-key-caller.ts's own comment on the pair)
|
|
146
|
-
*
|
|
147
|
-
* itself, same as verify_module/generate_spec_batch, so these are exactly as thin as those. */
|
|
158
|
+
* api-key-caller.ts's own comment on the pair). Repo-scoped calls let the model explore via
|
|
159
|
+
* --add-dir plus Read/Grep/Glob; a connection-less skeleton call has no repo to explore. */
|
|
148
160
|
export async function runProposeSkeletonTaskViaCli(task, repoPath) {
|
|
149
161
|
const payload = task.payload;
|
|
150
|
-
return runViaCli(payload.spec, task.maxBudgetUsd, repoPath);
|
|
162
|
+
return runViaCli(payload.spec, task.maxBudgetUsd, repoPath, payload.connectionless);
|
|
151
163
|
}
|
|
152
164
|
export async function runProposeNewCapabilitiesTaskViaCli(task, repoPath) {
|
|
153
165
|
const payload = task.payload;
|
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,8 @@ import { resolve } from 'node:path';
|
|
|
5
5
|
import { addConnection, configPath, enableWorkspacePolling, loadConfig } from './config.js';
|
|
6
6
|
import { CompanionClient, CompanionHttpError } from './http-client.js';
|
|
7
7
|
import { assertIsRepo, currentCommit } from './local-scan.js';
|
|
8
|
-
import { runAnalyseShipmentTask, runDesignPromptTask, runGenerateSpecBatchTask, runProposeNewCapabilitiesTask, runProposeSkeletonTask, runVerifyModuleTask } from './api-key-caller.js';
|
|
9
|
-
import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runVerifyModuleTaskViaCli } from './claude-cli-caller.js';
|
|
8
|
+
import { runAnalyseShipmentTask, runDesignPromptTask, runGenerateSpecBatchTask, runGenericTask, runProposeNewCapabilitiesTask, runProposeSkeletonTask, runVerifyModuleTask } from './api-key-caller.js';
|
|
9
|
+
import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runGenericTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runVerifyModuleTaskViaCli } from './claude-cli-caller.js';
|
|
10
10
|
/** A flag with nothing after it (end of argv, or the next token is itself another "--flag") is
|
|
11
11
|
* treated as a bare boolean ("true") rather than swallowing the following flag as its value —
|
|
12
12
|
* needed for --no-run below. Every existing "--flag value" pair still parses exactly as before. */
|
|
@@ -42,6 +42,7 @@ async function cmdConnect(argv) {
|
|
|
42
42
|
const token = f.token;
|
|
43
43
|
const connectionId = f.connection;
|
|
44
44
|
const apiKey = f['api-key'];
|
|
45
|
+
const backend = (f.backend || 'api-key');
|
|
45
46
|
// Pairing on its own does nothing useful — see cmdRun below — so this defaults to starting
|
|
46
47
|
// right away, which is what someone following the app's own copy-pasted command wants: one
|
|
47
48
|
// paste, and it's working. --no-run is the escape hatch for pairing a second/third project
|
|
@@ -50,11 +51,25 @@ async function cmdConnect(argv) {
|
|
|
50
51
|
if (!url || !token) {
|
|
51
52
|
console.error('Usage: product-console-companion connect --url <your Console link> --token <publicId.secret> --connection <connectionId> ' +
|
|
52
53
|
'[--path <folder — defaults to the folder you\'re in right now>] [--backend api-key|cli] [--api-key <your Anthropic key>] [--no-run]\n' +
|
|
53
|
-
' or: product-console-companion connect --url <your Console link> --token <publicId.secret> [--api-key <your Anthropic key>] [--no-run]');
|
|
54
|
+
' or: product-console-companion connect --url <your Console link> --token <publicId.secret> [--backend api-key|cli] [--api-key <your Anthropic key>] [--no-run]');
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (backend !== 'api-key' && backend !== 'cli') {
|
|
59
|
+
console.error(`--backend must be "api-key" or "cli", got "${backend}"`);
|
|
54
60
|
process.exitCode = 1;
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
63
|
if (!connectionId) {
|
|
64
|
+
if (backend === 'cli') {
|
|
65
|
+
const version = await checkClaudeCliAvailable();
|
|
66
|
+
if (!version) {
|
|
67
|
+
console.error('This needs Claude Code installed and you logged into it first: npm install -g @anthropic-ai/claude-code, then run `claude` and follow the login prompt.');
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log(`Found Claude Code: ${version}`);
|
|
72
|
+
}
|
|
58
73
|
const client = new CompanionClient(url, token);
|
|
59
74
|
try {
|
|
60
75
|
await client.whoami();
|
|
@@ -65,7 +80,7 @@ async function cmdConnect(argv) {
|
|
|
65
80
|
process.exitCode = 1;
|
|
66
81
|
return;
|
|
67
82
|
}
|
|
68
|
-
enableWorkspacePolling({ url, token, apiKey });
|
|
83
|
+
enableWorkspacePolling({ url, token, apiKey, backend });
|
|
69
84
|
console.log(`Connected! This computer will now run AI calls for this workspace's setup (PRD-only) tasks. (Saved to ${configPath}.)`);
|
|
70
85
|
if (!shouldRunAfter) {
|
|
71
86
|
console.log('Run "product-console-companion run" whenever you\'re ready to start.');
|
|
@@ -78,12 +93,6 @@ async function cmdConnect(argv) {
|
|
|
78
93
|
// --path is optional — defaults to wherever this command is actually run from, so "open a
|
|
79
94
|
// terminal in your project's folder, paste this" just works without typing a path at all.
|
|
80
95
|
const repoPath = resolve(f.path || '.');
|
|
81
|
-
const backend = (f.backend || 'api-key');
|
|
82
|
-
if (backend !== 'api-key' && backend !== 'cli') {
|
|
83
|
-
console.error(`--backend must be "api-key" or "cli", got "${backend}"`);
|
|
84
|
-
process.exitCode = 1;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
96
|
try {
|
|
88
97
|
await assertIsRepo(repoPath);
|
|
89
98
|
}
|
|
@@ -127,35 +136,31 @@ const IDLE_POLL_MS = 5000;
|
|
|
127
136
|
/** How often to extend a claimed task's lease while it's in flight — comfortably inside the
|
|
128
137
|
* server's ~2-minute lease window (server/store.ts's LOCAL_TASK_LEASE_MS). */
|
|
129
138
|
const HEARTBEAT_MS = 30_000;
|
|
130
|
-
/** The
|
|
131
|
-
*
|
|
132
|
-
* (
|
|
139
|
+
/** The explicitly repo-aware task kinds, companion-side. Everything else falls through to a
|
|
140
|
+
* connectionless runner: only our server creates tasks, their ModelTaskSpec is self-describing
|
|
141
|
+
* (model, effort, maxTokens, system, stable/volatile text, optional schema), and graph reasoning
|
|
142
|
+
* has nothing legitimate to read from disk. That also makes future server kinds work without a
|
|
143
|
+
* companion release. */
|
|
133
144
|
async function runTask(task, backend, anthropic, repoPath) {
|
|
134
|
-
const connectionless = task.payload.connectionless === true;
|
|
135
|
-
const effectiveBackend = connectionless ? 'api-key' : backend;
|
|
136
|
-
if (connectionless && !anthropic) {
|
|
137
|
-
throw new Error('This is a connection-less (PRD-only) task, which needs an Anthropic API key. Re-run ' +
|
|
138
|
-
'`connect` with --api-key <key>, or set ANTHROPIC_API_KEY.');
|
|
139
|
-
}
|
|
140
145
|
if (task.kind === 'verify_module') {
|
|
141
|
-
return
|
|
146
|
+
return backend === 'cli' ? runVerifyModuleTaskViaCli(task, repoPath) : runVerifyModuleTask(anthropic, task, repoPath); // anthropic is always set for backend 'api-key' — see cmdRun
|
|
142
147
|
}
|
|
143
148
|
if (task.kind === 'generate_spec_batch') {
|
|
144
|
-
return
|
|
149
|
+
return backend === 'cli' ? runGenerateSpecBatchTaskViaCli(task, repoPath) : runGenerateSpecBatchTask(anthropic, task, repoPath);
|
|
145
150
|
}
|
|
146
151
|
if (task.kind === 'propose_skeleton') {
|
|
147
|
-
return
|
|
152
|
+
return backend === 'cli' ? runProposeSkeletonTaskViaCli(task, repoPath) : runProposeSkeletonTask(anthropic, task, repoPath);
|
|
148
153
|
}
|
|
149
154
|
if (task.kind === 'propose_new_capabilities') {
|
|
150
|
-
return
|
|
155
|
+
return backend === 'cli' ? runProposeNewCapabilitiesTaskViaCli(task, repoPath) : runProposeNewCapabilitiesTask(anthropic, task, repoPath);
|
|
151
156
|
}
|
|
152
157
|
if (task.kind === 'design_prompt') {
|
|
153
|
-
return
|
|
158
|
+
return backend === 'cli' ? runDesignPromptTaskViaCli(task, repoPath) : runDesignPromptTask(anthropic, task, repoPath);
|
|
154
159
|
}
|
|
155
160
|
if (task.kind === 'analyse_shipment') {
|
|
156
|
-
return
|
|
161
|
+
return backend === 'cli' ? runAnalyseShipmentTaskViaCli(task, repoPath) : runAnalyseShipmentTask(anthropic, task, repoPath);
|
|
157
162
|
}
|
|
158
|
-
|
|
163
|
+
return backend === 'cli' ? runGenericTaskViaCli(task, repoPath) : runGenericTask(anthropic, task, repoPath);
|
|
159
164
|
}
|
|
160
165
|
async function runOneTask(anthropic, backend, client, repoPath, task) {
|
|
161
166
|
console.log(`[${new Date().toISOString()}] claimed ${task.kind} task ${task.id} via ${backend} (budget $${task.maxBudgetUsd.toFixed(2)} — not yet enforced client-side, see README)`);
|
|
@@ -188,10 +193,11 @@ async function cmdRun() {
|
|
|
188
193
|
process.exitCode = 1;
|
|
189
194
|
return;
|
|
190
195
|
}
|
|
191
|
-
// Repo backends are chosen per connection; workspace-level tasks
|
|
196
|
+
// Repo backends are chosen per connection; workspace-level tasks have their own choice. Only
|
|
192
197
|
// require what's actually configured to run, not every prerequisite either backend could need.
|
|
193
|
-
const
|
|
194
|
-
const
|
|
198
|
+
const workspaceBackend = config.workspaceBackend ?? 'api-key';
|
|
199
|
+
const needsApiKey = (config.pollWorkspaceTasks && workspaceBackend !== 'cli') || config.connections.some((c) => c.backend !== 'cli');
|
|
200
|
+
const needsCli = (config.pollWorkspaceTasks && workspaceBackend === 'cli') || config.connections.some((c) => c.backend === 'cli');
|
|
195
201
|
// config.apiKey (set via `connect --api-key ...`) takes priority over the environment variable
|
|
196
202
|
// — the easier path for someone who'd rather paste their key once into a command than learn
|
|
197
203
|
// what an environment variable is and how to set one on their OS. Both are supported; neither
|
|
@@ -230,7 +236,7 @@ async function cmdRun() {
|
|
|
230
236
|
console.log(` - ${c.connectionId} → ${c.repoPath} @ ${sha.slice(0, 7)} (${c.backend})`);
|
|
231
237
|
}
|
|
232
238
|
if (config.pollWorkspaceTasks) {
|
|
233
|
-
console.log(
|
|
239
|
+
console.log(` - workspace-level (connection-less / PRD-only) tasks (${workspaceBackend})`);
|
|
234
240
|
}
|
|
235
241
|
console.log('Waiting for work — leave this running. Ctrl+C any time to stop.');
|
|
236
242
|
let stopping = false;
|
|
@@ -278,7 +284,7 @@ async function cmdRun() {
|
|
|
278
284
|
if (claim.task) {
|
|
279
285
|
claimedAny = true;
|
|
280
286
|
// Connection-less task kinds do not read a repo; cwd is a harmless placeholder.
|
|
281
|
-
await runOneTask(anthropic,
|
|
287
|
+
await runOneTask(anthropic, workspaceBackend, client, process.cwd(), claim.task);
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
290
|
if (!claimedAny && !stopping)
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,7 @@ export function addConnection(args) {
|
|
|
33
33
|
connections,
|
|
34
34
|
apiKey: args.apiKey || existing?.apiKey,
|
|
35
35
|
pollWorkspaceTasks: existing?.pollWorkspaceTasks,
|
|
36
|
+
workspaceBackend: existing?.workspaceBackend,
|
|
36
37
|
};
|
|
37
38
|
save(config);
|
|
38
39
|
return config;
|
|
@@ -48,6 +49,7 @@ export function enableWorkspacePolling(args) {
|
|
|
48
49
|
connections: existing?.connections ?? [],
|
|
49
50
|
apiKey: args.apiKey || existing?.apiKey,
|
|
50
51
|
pollWorkspaceTasks: true,
|
|
52
|
+
workspaceBackend: args.backend || existing?.workspaceBackend,
|
|
51
53
|
};
|
|
52
54
|
save(config);
|
|
53
55
|
return config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-console-companion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local-first BYOS companion for the Fynch Product Console — reads code and runs AI calls from your own machine instead of the hosted server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|