product-console-companion 0.1.0 → 0.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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Fynch Consulting
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fynch Consulting
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,96 +1,132 @@
1
- # product-console-companion
2
-
3
- A local-first BYOS companion for the [Fynch Product Console](../README.md). Reads code from
4
- your own machine, so:
5
-
6
- - code never leaves your machine — only a small structured verdict (matches/differs + a short
7
- explanation) is ever sent back;
8
- - there's no server-side truncation on what gets read (`server/drift.ts`'s
9
- `MAX_FILE_CHARS`/`MAX_TREE_PATHS` don't apply here at all);
10
- - AI spend bills against your own key/subscription, not the workspace's shared/BYOK credential.
11
-
12
- Covers every AI call the hosted Console makes that reads your code — six task kinds as of this
13
- release (`verify_module` — "Check against code" on a module card; `generate_spec_batch` —
14
- writing full specs during setup; `propose_skeleton` and `propose_new_capabilities` — the two
15
- shallow whole-repo scans; `design_prompt` — the free-text export; `analyse_shipment` — the
16
- passive per-commit drift check behind the GitHub webhook and the "Check GitHub" button), two
17
- backends, chosen per connection at `connect` time:
18
-
19
- - **`api-key`** (default) — no CLI dependency. Calls Anthropic directly with your own API key
20
- (`api-key-caller.ts`), running its own two-pass file-select-then-judge, mirroring
21
- `server/drift.ts`'s `verifyModuleAgainstCode` but reading local files unbounded.
22
- - **`cli`** — shells out to an already-authenticated `claude` CLI (`claude-cli-caller.ts`) for
23
- full agentic Read/Grep/Glob depth in one call — the model does its own exploring instead of a
24
- separate file-select pass. Bills against whatever the CLI is authenticated with (a Claude
25
- subscription via `claude setup-token`, or `ANTHROPIC_API_KEY` if the CLI has that set). Runs
26
- against a disposable, read-only git worktree (`sandbox.ts`) as defense-in-depth on top of
27
- `--restricted --tools "Read,Grep,Glob"`, which should already make a write/exec structurally
28
- impossible on its own.
29
-
30
- ## Setup
31
-
32
- ```bash
33
- npx product-console-companion connect \
34
- --url https://console.fynchconsulting.com \
35
- --token <publicId>.<secret> \
36
- --connection <connectionId> \
37
- --path /path/to/your/local/checkout \
38
- --backend api-key # or: --backend cli
39
- ```
40
-
41
- Get `--token`/`--connection` from the Console's **Companion** button (generates a token and a
42
- ready-to-paste version of this command) — see `src/CompanionModal.tsx` in the parent repo.
43
- Running `connect` again for a different `--connection`/`--path` (same `--url`/`--token`) adds a
44
- second connection to the same local config; one companion process can serve several repos, each
45
- with its own backend choice.
46
-
47
- For `--backend api-key`: `export ANTHROPIC_API_KEY=sk-ant-...` (get one at console.anthropic.com).
48
- For `--backend cli`: install and authenticate the CLI first — `npm install -g
49
- @anthropic-ai/claude-code`, then `claude` (interactive login) or set `ANTHROPIC_API_KEY` for it.
50
- `connect` checks `claude --version` up front and fails fast if it's missing.
51
-
52
- Then, in the Console's **Manage connected repos**, switch that repo's execution to "On your
53
- machine, via companion". Finally, to actually serve tasks:
54
-
55
- ```bash
56
- npx product-console-companion run
57
- ```
58
-
59
- Polls each configured connection every few seconds until Ctrl+C. Nothing runs while this isn't
60
- running — see the parent plan's Section 7 for why that's deliberate (goal 4: no AI spend while
61
- you're not around to notice).
62
-
63
- ## What's NOT yet implemented (be aware before relying on this)
64
-
65
- - **Per-task budget is not enforced client-side for the `api-key` backend.** Every dispatched
66
- task carries a `maxBudgetUsd` (visible in the `run` log line); `api-key-caller.ts` does not
67
- track spend against it or abort a call that exceeds it. The `cli` backend passes it straight
68
- through as `claude`'s own `--max-budget-usd`, which the CLI does enforce (confirmed
69
- empirically: it stops and reports `terminal_reason: 'budget_exhausted'`).
70
- - **No wall-clock timeout for the `api-key` backend** — a hung request blocks that connection's
71
- polling loop. The `cli` backend has one (10 minutes, independent of the budget cap — a dollar
72
- ceiling doesn't stop a network-stall hang).
73
- - **Git-fetch-if-missing is `analyse_shipment`-only.** Every other task kind reads whatever HEAD
74
- currently is in `--path` (or, for `cli`, a worktree pinned to that HEAD) — correct for those,
75
- since they check the *current* state of your code, not a specific historical commit.
76
- `analyse_shipment` is different: it's about one specific commit a webhook or the hosted poller
77
- told the server about, which the server never fetches for you. It verifies that commit is
78
- actually present locally first, attempts one `git fetch` (your own git credentials — never
79
- anything from the hosted server) if it isn't, and reports back distinctly (`sha_not_local`)
80
- rather than silently analysing the wrong tree state if it still can't find it.
81
- - **`cli` backend's sandbox worktree isolates writes, not reads** — `--add-dir` points at the
82
- disposable worktree, so a read sees exactly what's committed at HEAD, including anything
83
- `.gitignore`d being invisible (same as `git ls-files`'s own behavior). Uncommitted local
84
- changes in your real checkout are never visible to the CLI backend — worth knowing if you're
85
- expecting a check to see work-in-progress edits. (`analyse_shipment`'s own diff read is the one
86
- exception: it happens against your real checkout, via `git show`, before the sandbox is even
87
- created — the model itself still only ever sees the sandboxed copy.)
88
-
89
- ## Local development (inside the parent repo)
90
-
91
- ```bash
92
- cd companion
93
- npm install
94
- npm run dev -- connect --url http://localhost:3101 --token <publicId>.<secret> --connection <connectionId> --path <repo path> --backend api-key
95
- npm run dev -- run
96
- ```
1
+ # product-console-companion
2
+
3
+ A local-first BYOS companion for the [Fynch Product Console](../README.md). Reads code from
4
+ your own machine, so:
5
+
6
+ - code never leaves your machine — only a small structured verdict (matches/differs + a short
7
+ explanation) is ever sent back;
8
+ - there's no server-side truncation on what gets read (`server/drift.ts`'s
9
+ `MAX_FILE_CHARS`/`MAX_TREE_PATHS` don't apply here at all);
10
+ - AI spend bills against your own key/subscription, not the workspace's shared/BYOK credential.
11
+
12
+ Covers every AI call the hosted Console makes that reads your code — six task kinds as of this
13
+ release (`verify_module` — "Check against code" on a module card; `generate_spec_batch` —
14
+ writing full specs during setup; `propose_skeleton` and `propose_new_capabilities` — the two
15
+ shallow whole-repo scans; `design_prompt` — the free-text export; `analyse_shipment` — the
16
+ passive per-commit drift check behind the GitHub webhook and the "Check GitHub" button), two
17
+ backends, chosen per connection at `connect` time:
18
+
19
+ - **`api-key`** (default) — no CLI dependency. Calls Anthropic directly with your own API key
20
+ (`api-key-caller.ts`), running its own two-pass file-select-then-judge, mirroring
21
+ `server/drift.ts`'s `verifyModuleAgainstCode` but reading local files unbounded.
22
+ - **`cli`** — shells out to an already-authenticated `claude` CLI (`claude-cli-caller.ts`) for
23
+ full agentic Read/Grep/Glob depth in one call — the model does its own exploring instead of a
24
+ separate file-select pass. Bills against whatever the CLI is authenticated with (a Claude
25
+ subscription via `claude setup-token`, or `ANTHROPIC_API_KEY` if the CLI has that set). Runs
26
+ against a disposable, read-only git worktree (`sandbox.ts`) as defense-in-depth on top of
27
+ `--restricted --tools "Read,Grep,Glob"`, which should already make a write/exec structurally
28
+ impossible on its own.
29
+
30
+ ## Setup
31
+
32
+ The Console's **Companion** button generates a ready-to-paste command per connected project —
33
+ that's the intended way to get `--token`/`--connection`/`--path` right (see
34
+ `src/CompanionModal.tsx` in the parent repo, which also has an inline toggle to switch that
35
+ connection's execution to "on your machine" right next to the command it activates). Written out,
36
+ the shape is:
37
+
38
+ ```bash
39
+ npx product-console-companion connect \
40
+ --url https://console.fynchconsulting.com \
41
+ --token <publicId>.<secret> \
42
+ --connection <connectionId> \
43
+ --path /path/to/your/local/checkout \
44
+ --backend api-key \
45
+ --api-key sk-ant-... # optional — see below
46
+ ```
47
+
48
+ `--path` defaults to the current directory if omitted — running `connect` from inside your
49
+ project's own folder works with no `--path` at all. `connect` **pairs and immediately starts
50
+ serving tasks in the same command** (equivalent to also running `run`, below) — pass `--no-run`
51
+ if you're pairing a second/third project first and want to start one shared `run` for all of them
52
+ afterward instead.
53
+
54
+ Running `connect` again for a different `--connection`/`--path` (same `--url`/`--token`) adds a
55
+ second connection to the same local config; one companion process can serve several repos, each
56
+ with its own backend choice.
57
+
58
+ **`--backend api-key`** (default) needs an Anthropic API key (get one free at
59
+ console.anthropic.com) — either pass it inline with `--api-key sk-ant-...` (saved to this
60
+ package's own local config file, `~/.product-console-companion/config.json`, permissions locked
61
+ to your user only) or set `ANTHROPIC_API_KEY` in your environment yourself; the inline flag takes
62
+ priority if both are present.
63
+
64
+ **`--backend cli`** needs [Claude Code](https://claude.com/claude-code) installed and you logged
65
+ into it first — `npm install -g @anthropic-ai/claude-code`, then run `claude` and follow the login
66
+ prompt. `connect` checks `claude --version` up front and fails fast if it's missing. Bills against
67
+ your Claude subscription, not a separate API key.
68
+
69
+ Once it's running:
70
+
71
+ ```bash
72
+ npx product-console-companion run
73
+ ```
74
+
75
+ (this is what `connect` already ran for you, unless you passed `--no-run`) polls each configured
76
+ connection every few seconds until Ctrl+C. Nothing runs while this isn't running — see the parent
77
+ plan's Section 7 for why that's deliberate (goal 4: no AI spend while you're not around to
78
+ notice).
79
+
80
+ ## What's NOT yet implemented (be aware before relying on this)
81
+
82
+ - **Per-task budget is not enforced client-side for the `api-key` backend.** Every dispatched
83
+ task carries a `maxBudgetUsd` (visible in the `run` log line); `api-key-caller.ts` does not
84
+ track spend against it or abort a call that exceeds it. The `cli` backend passes it straight
85
+ through as `claude`'s own `--max-budget-usd`, which the CLI does enforce (confirmed
86
+ empirically: it stops and reports `terminal_reason: 'budget_exhausted'`).
87
+ - **No wall-clock timeout for the `api-key` backend** — a hung request blocks that connection's
88
+ polling loop. The `cli` backend has one (10 minutes, independent of the budget cap — a dollar
89
+ ceiling doesn't stop a network-stall hang).
90
+ - **Git-fetch-if-missing is `analyse_shipment`-only.** Every other task kind reads whatever HEAD
91
+ currently is in `--path` (or, for `cli`, a worktree pinned to that HEAD) — correct for those,
92
+ since they check the *current* state of your code, not a specific historical commit.
93
+ `analyse_shipment` is different: it's about one specific commit a webhook or the hosted poller
94
+ told the server about, which the server never fetches for you. It verifies that commit is
95
+ actually present locally first, attempts one `git fetch` (your own git credentials — never
96
+ anything from the hosted server) if it isn't, and reports back distinctly (`sha_not_local`)
97
+ rather than silently analysing the wrong tree state if it still can't find it.
98
+ - **`cli` backend's sandbox worktree isolates writes, not reads** — `--add-dir` points at the
99
+ disposable worktree, so a read sees exactly what's committed at HEAD, including anything
100
+ `.gitignore`d being invisible (same as `git ls-files`'s own behavior). Uncommitted local
101
+ changes in your real checkout are never visible to the CLI backend — worth knowing if you're
102
+ expecting a check to see work-in-progress edits. (`analyse_shipment`'s own diff read is the one
103
+ exception: it happens against your real checkout, via `git show`, before the sandbox is even
104
+ created — the model itself still only ever sees the sandboxed copy.)
105
+
106
+ ## Local development (inside the parent repo)
107
+
108
+ ```bash
109
+ cd companion
110
+ npm install
111
+ npm run dev -- connect --url http://localhost:3101 --token <publicId>.<secret> --connection <connectionId> --path <repo path> --backend api-key
112
+ npm run dev -- run
113
+ ```
114
+
115
+ ## Publishing a new version
116
+
117
+ Via GitHub Actions Trusted Publishing (`.github/workflows/publish-companion.yml`) — no npm token
118
+ generated, pasted, or stored for this, ever. One-time setup on npmjs.com (already done as of this
119
+ writing): on the `product-console-companion` package's own page, **Settings → Trusted
120
+ publishing → GitHub Actions**, pointing at this repo (`fynchconsulting/product-console`) and this
121
+ exact workflow filename (`publish-companion.yml`), no environment.
122
+
123
+ To actually publish once that's configured: bump the version in `package.json` in a normal PR,
124
+ merge it, then either
125
+
126
+ ```bash
127
+ git tag companion-v<the new version> && git push --tags
128
+ ```
129
+
130
+ or trigger the workflow manually (Actions tab, or `gh workflow run publish-companion.yml`). Either
131
+ way it publishes whatever's currently in `package.json` at that point — there's no separate
132
+ "which version" input to get wrong.
@@ -47,16 +47,16 @@ export async function runVerifyModuleTask(client, task, repoPath) {
47
47
  // Pass 1: which files implement this module? Reuses the module-spec text the server already
48
48
  // wrote into spec.volatileText (buildVerifyModuleTaskSpec) as context — it already contains
49
49
  // the full spec, so this doesn't need to re-derive it.
50
- const selectPrompt = `${spec.volatileText}
51
-
52
- # The repository file tree (${allFiles.length} files)
53
-
54
- ${allFiles.join('\n')}
55
-
56
- # Before judging
57
-
58
- First, list the file paths (verbatim from the tree above, up to ${MAX_CANDIDATE_FILES}) most
59
- likely to implement this module's behaviour. Prefer application source over tests, config, and
50
+ const selectPrompt = `${spec.volatileText}
51
+
52
+ # The repository file tree (${allFiles.length} files)
53
+
54
+ ${allFiles.join('\n')}
55
+
56
+ # Before judging
57
+
58
+ First, list the file paths (verbatim from the tree above, up to ${MAX_CANDIDATE_FILES}) most
59
+ likely to implement this module's behaviour. Prefer application source over tests, config, and
60
60
  generated files. Empty list if nothing plausibly implements it.`;
61
61
  const selectStream = client.messages.stream({
62
62
  model: spec.model,
@@ -163,16 +163,16 @@ export async function runGenerateSpecBatchTask(client, task, repoPath) {
163
163
  const allFiles = await listRepoFiles(repoPath);
164
164
  console.log(` scanning ${allFiles.length} files in ${repoPath} for ${moduleIds.length} module(s)…`);
165
165
  // Pass 1: which files relate to each module in this batch?
166
- const selectPrompt = `${spec.volatileText}
167
-
168
- # The repository file tree (${allFiles.length} files)
169
-
170
- ${allFiles.join('\n')}
171
-
172
- # Before writing specs
173
-
174
- For EACH module above (by id: ${moduleIds.join(', ')}), list up to ${MAX_FILES_PER_MODULE_IN_BATCH}
175
- file paths (verbatim from the tree above) most likely to implement it. One entry per module id,
166
+ const selectPrompt = `${spec.volatileText}
167
+
168
+ # The repository file tree (${allFiles.length} files)
169
+
170
+ ${allFiles.join('\n')}
171
+
172
+ # Before writing specs
173
+
174
+ For EACH module above (by id: ${moduleIds.join(', ')}), list up to ${MAX_FILES_PER_MODULE_IN_BATCH}
175
+ file paths (verbatim from the tree above) most likely to implement it. One entry per module id,
176
176
  in the same order as given.`;
177
177
  const selectStream = client.messages.stream({
178
178
  model: spec.model,
@@ -239,20 +239,20 @@ in the same order as given.`;
239
239
  async function runShallowRepoTask(client, spec, repoPath) {
240
240
  const repoCtx = await buildLocalRepoContext(repoPath);
241
241
  console.log(` scanning ${repoCtx.fileList.length} files, README ${repoCtx.readme ? 'found' : 'not found'}, CHANGELOG ${repoCtx.changelog ? 'found' : 'not found'}, package.json ${repoCtx.packageJson ? 'found' : 'not found'}`);
242
- const repoSection = `
243
- # Repository context (your local checkout)
244
-
245
- ## File tree (${repoCtx.fileList.length} files)
246
- ${repoCtx.fileList.join('\n')}
247
-
248
- ## README
249
- ${repoCtx.readme ?? '(none found)'}
250
-
251
- ## CHANGELOG
252
- ${repoCtx.changelog ?? '(none found)'}
253
-
254
- ## package.json
255
- ${repoCtx.packageJson ?? '(none found)'}
242
+ const repoSection = `
243
+ # Repository context (your local checkout)
244
+
245
+ ## File tree (${repoCtx.fileList.length} files)
246
+ ${repoCtx.fileList.join('\n')}
247
+
248
+ ## README
249
+ ${repoCtx.readme ?? '(none found)'}
250
+
251
+ ## CHANGELOG
252
+ ${repoCtx.changelog ?? '(none found)'}
253
+
254
+ ## package.json
255
+ ${repoCtx.packageJson ?? '(none found)'}
256
256
  `.trim();
257
257
  const content = [];
258
258
  if (spec.stableDocumentBase64) {
package/dist/cli.js CHANGED
@@ -7,13 +7,22 @@ import { CompanionClient, CompanionHttpError } from './http-client.js';
7
7
  import { assertIsRepo, currentCommit } from './local-scan.js';
8
8
  import { runAnalyseShipmentTask, runDesignPromptTask, runGenerateSpecBatchTask, runProposeNewCapabilitiesTask, runProposeSkeletonTask, runVerifyModuleTask } from './api-key-caller.js';
9
9
  import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runVerifyModuleTaskViaCli } from './claude-cli-caller.js';
10
+ /** A flag with nothing after it (end of argv, or the next token is itself another "--flag") is
11
+ * treated as a bare boolean ("true") rather than swallowing the following flag as its value —
12
+ * needed for --no-run below. Every existing "--flag value" pair still parses exactly as before. */
10
13
  function parseFlags(argv) {
11
14
  const flags = {};
12
15
  for (let i = 0; i < argv.length; i++) {
13
16
  const a = argv[i];
14
17
  if (a?.startsWith('--')) {
15
- flags[a.slice(2)] = argv[i + 1] ?? '';
16
- i++;
18
+ const next = argv[i + 1];
19
+ if (next === undefined || next.startsWith('--')) {
20
+ flags[a.slice(2)] = 'true';
21
+ }
22
+ else {
23
+ flags[a.slice(2)] = next;
24
+ i++;
25
+ }
17
26
  }
18
27
  }
19
28
  return flags;
@@ -32,10 +41,19 @@ async function cmdConnect(argv) {
32
41
  const url = f.url?.replace(/\/+$/, '');
33
42
  const token = f.token;
34
43
  const connectionId = f.connection;
35
- const repoPath = f.path ? resolve(f.path) : undefined;
44
+ // --path is optional — defaults to wherever this command is actually run from, so "open a
45
+ // terminal in your project's folder, paste this" just works without typing a path at all.
46
+ const repoPath = resolve(f.path || '.');
36
47
  const backend = (f.backend || 'api-key');
37
- if (!url || !token || !connectionId || !repoPath) {
38
- console.error('Usage: product-console-companion connect --url <origin> --token <publicId.secret> --connection <connectionId> --path <local repo path> [--backend api-key|cli]');
48
+ const apiKey = f['api-key'];
49
+ // Pairing on its own does nothing useful — see cmdRun below — so this defaults to starting
50
+ // right away, which is what someone following the app's own copy-pasted command wants: one
51
+ // paste, and it's working. --no-run is the escape hatch for pairing a second/third project
52
+ // before starting one shared `run` for all of them (see the README's own multi-connection note).
53
+ const shouldRunAfter = f['no-run'] !== 'true';
54
+ if (!url || !token || !connectionId) {
55
+ console.error('Usage: product-console-companion connect --url <your Console link> --token <publicId.secret> --connection <connectionId> ' +
56
+ '[--path <folder — defaults to the folder you\'re in right now>] [--backend api-key|cli] [--api-key <your Anthropic key>] [--no-run]');
39
57
  process.exitCode = 1;
40
58
  return;
41
59
  }
@@ -44,28 +62,41 @@ async function cmdConnect(argv) {
44
62
  process.exitCode = 1;
45
63
  return;
46
64
  }
47
- await assertIsRepo(repoPath);
65
+ try {
66
+ await assertIsRepo(repoPath);
67
+ }
68
+ catch (err) {
69
+ console.error((err instanceof Error ? err.message : String(err)) + ' Run this from inside your project\'s folder, or pass --path <folder>.');
70
+ process.exitCode = 1;
71
+ return;
72
+ }
48
73
  if (backend === 'cli') {
49
74
  const version = await checkClaudeCliAvailable();
50
75
  if (!version) {
51
- console.error('--backend cli needs the `claude` CLI on PATH and authenticated — install it first: npm install -g @anthropic-ai/claude-code');
76
+ 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.');
52
77
  process.exitCode = 1;
53
78
  return;
54
79
  }
55
- console.log(`Found claude CLI: ${version}`);
80
+ console.log(`Found Claude Code: ${version}`);
56
81
  }
57
82
  const client = new CompanionClient(url, token);
58
83
  try {
59
84
  await client.whoami();
60
85
  }
61
86
  catch (err) {
62
- console.error(`Could not validate that token against ${url}:`, err instanceof Error ? err.message : err);
87
+ console.error(`Hmm, that didn't work — could not verify your token against ${url}. Double check you copied the ` +
88
+ `WHOLE thing, including the part after the dot.`, err instanceof Error ? err.message : err);
63
89
  process.exitCode = 1;
64
90
  return;
65
91
  }
66
- addConnection({ url, token, connection: { connectionId, repoPath, backend } });
67
- console.log(`Paired (backend: ${backend}). Saved to ${configPath}.`);
68
- console.log(`Run "product-console-companion run" to start serving tasks for this (and any other configured) connection.`);
92
+ addConnection({ url, token, connection: { connectionId, repoPath, backend }, apiKey });
93
+ console.log(`Connected! This computer will now read this project's code and run AI calls for it. (Saved to ${configPath}.)`);
94
+ if (!shouldRunAfter) {
95
+ console.log('Run "product-console-companion run" whenever you\'re ready to start.');
96
+ return;
97
+ }
98
+ console.log('Starting now — Ctrl+C any time to stop.\n');
99
+ await cmdRun();
69
100
  }
70
101
  /** How often an idle connection is re-polled when nothing was claimed — a short client-side
71
102
  * poll, not a server-held long-poll (see the plan's own reasoning: this app has already been
@@ -125,7 +156,7 @@ async function runOneTask(anthropic, backend, client, repoPath, task) {
125
156
  async function cmdRun() {
126
157
  const config = loadConfig();
127
158
  if (!config || config.connections.length === 0) {
128
- console.error(`No connections configured — ${configPath} is missing or empty. Run "connect" first.`);
159
+ console.error(`Nothing set up yet — ${configPath} is missing or empty. Run "connect" first.`);
129
160
  process.exitCode = 1;
130
161
  return;
131
162
  }
@@ -133,34 +164,40 @@ async function cmdRun() {
133
164
  // configured to run, not every prerequisite either backend could ever need.
134
165
  const needsApiKey = config.connections.some((c) => c.backend !== 'cli');
135
166
  const needsCli = config.connections.some((c) => c.backend === 'cli');
136
- const apiKey = process.env.ANTHROPIC_API_KEY;
167
+ // config.apiKey (set via `connect --api-key ...`) takes priority over the environment variable
168
+ // — the easier path for someone who'd rather paste their key once into a command than learn
169
+ // what an environment variable is and how to set one on their OS. Both are supported; neither
170
+ // is required unless a configured connection actually needs it.
171
+ const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
137
172
  if (needsApiKey && !apiKey) {
138
- console.error('ANTHROPIC_API_KEY is not set — the api-key backend calls the model directly with your own key. Get one at console.anthropic.com and set it in your environment.');
173
+ console.error('No Anthropic API key found. Get one free at console.anthropic.com, then either run ' +
174
+ '"connect" again adding --api-key <your key>, or set it yourself: ' +
175
+ '"set ANTHROPIC_API_KEY=<your key>" (Windows) / "export ANTHROPIC_API_KEY=<your key>" (Mac/Linux), then run "run" again.');
139
176
  process.exitCode = 1;
140
177
  return;
141
178
  }
142
179
  if (needsCli) {
143
180
  const version = await checkClaudeCliAvailable();
144
181
  if (!version) {
145
- console.error('The cli backend needs the `claude` CLI on PATH and authenticated — install it first: npm install -g @anthropic-ai/claude-code');
182
+ 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.');
146
183
  process.exitCode = 1;
147
184
  return;
148
185
  }
149
186
  }
150
187
  for (const c of config.connections) {
151
188
  await assertIsRepo(c.repoPath).catch((err) => {
152
- console.error(`Warning: ${c.connectionId} → ${c.repoPath}:`, err instanceof Error ? err.message : err);
189
+ console.error(`Warning: can't find ${c.connectionId}'s project folder (${c.repoPath}):`, err instanceof Error ? err.message : err);
153
190
  });
154
191
  }
155
192
  const anthropic = needsApiKey ? new Anthropic({ apiKey }) : null;
156
193
  const client = new CompanionClient(config.url, config.token);
157
194
  await client.whoami();
158
- console.log(`Connected to ${config.url}. Serving ${config.connections.length} connection${config.connections.length === 1 ? '' : 's'}:`);
195
+ console.log(`Connected to ${config.url}. Watching ${config.connections.length} project${config.connections.length === 1 ? '' : 's'}:`);
159
196
  for (const c of config.connections) {
160
197
  const sha = await currentCommit(c.repoPath).catch(() => '(unknown)');
161
198
  console.log(` - ${c.connectionId} → ${c.repoPath} @ ${sha.slice(0, 7)} (${c.backend})`);
162
199
  }
163
- console.log('Polling for tasks — Ctrl+C to stop.');
200
+ console.log('Waiting for work — leave this running. Ctrl+C any time to stop.');
164
201
  let stopping = false;
165
202
  process.on('SIGINT', () => {
166
203
  if (stopping)
package/dist/config.js CHANGED
@@ -20,12 +20,14 @@ function save(config) {
20
20
  }
21
21
  /** Adds (or replaces, if this exact connectionId is already configured) one connection —
22
22
  * url/token are shared across every connection this companion serves; only mismatched values
23
- * overwrite them, so re-running `connect` for a second repo with the SAME url/token is additive. */
23
+ * overwrite them, so re-running `connect` for a second repo with the SAME url/token is additive.
24
+ * apiKey works the same way: passing one saves/updates it; omitting it on a later `connect`
25
+ * call leaves whatever was already saved untouched, rather than blowing it away. */
24
26
  export function addConnection(args) {
25
27
  const existing = loadConfig();
26
28
  const connections = (existing?.connections ?? []).filter((c) => c.connectionId !== args.connection.connectionId);
27
29
  connections.push(args.connection);
28
- const config = { url: args.url, token: args.token, connections };
30
+ const config = { url: args.url, token: args.token, connections, apiKey: args.apiKey || existing?.apiKey };
29
31
  save(config);
30
32
  return config;
31
33
  }
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "product-console-companion",
3
- "version": "0.1.0",
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
- "type": "module",
6
- "license": "MIT",
7
- "bin": {
8
- "product-console-companion": "dist/cli.js"
9
- },
10
- "files": [
11
- "dist"
12
- ],
13
- "scripts": {
14
- "build": "tsc -b",
15
- "dev": "tsx src/cli.ts"
16
- },
17
- "keywords": [
18
- "cli",
19
- "ai",
20
- "anthropic",
21
- "claude",
22
- "companion",
23
- "byok",
24
- "byos",
25
- "product-console"
26
- ],
27
- "homepage": "https://console.fynchconsulting.com",
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/fynchconsulting/product-console.git",
31
- "directory": "companion"
32
- },
33
- "dependencies": {
34
- "@anthropic-ai/sdk": "^0.111.0",
35
- "cross-spawn": "^7.0.6"
36
- },
37
- "devDependencies": {
38
- "@types/cross-spawn": "^6.0.6",
39
- "@types/node": "^24.10.1",
40
- "tsx": "^4.20.6",
41
- "typescript": "^5.9.3"
42
- },
43
- "engines": {
44
- "node": ">=20"
45
- }
46
- }
1
+ {
2
+ "name": "product-console-companion",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "product-console-companion": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -b",
15
+ "dev": "tsx src/cli.ts"
16
+ },
17
+ "keywords": [
18
+ "cli",
19
+ "ai",
20
+ "anthropic",
21
+ "claude",
22
+ "companion",
23
+ "byok",
24
+ "byos",
25
+ "product-console"
26
+ ],
27
+ "homepage": "https://console.fynchconsulting.com",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/fynchconsulting/product-console.git",
31
+ "directory": "companion"
32
+ },
33
+ "dependencies": {
34
+ "@anthropic-ai/sdk": "^0.111.0",
35
+ "cross-spawn": "^7.0.6"
36
+ },
37
+ "devDependencies": {
38
+ "@types/cross-spawn": "^6.0.6",
39
+ "@types/node": "^24.10.1",
40
+ "tsx": "^4.20.6",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=20"
45
+ }
46
+ }