product-console-companion 0.1.0 → 0.3.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 +21 -21
- package/README.md +141 -96
- package/dist/api-key-caller.js +64 -39
- package/dist/cli.js +122 -31
- package/dist/config.js +25 -2
- package/dist/http-client.js +1 -1
- package/package.json +46 -46
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,141 @@
|
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
##
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
To pair this machine before any repo is connected, omit `--connection` and `--path`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx product-console-companion connect --url https://console.fynchconsulting.com --token <publicId>.<secret> [--api-key sk-ant-...]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This runs your workspace's PRD-only setup AI calls when there is no repo yet. `run` then polls
|
|
65
|
+
for workspace-level tasks in addition to any per-repo connections configured on this machine.
|
|
66
|
+
|
|
67
|
+
**`--backend api-key`** (default) needs an Anthropic API key (get one free at
|
|
68
|
+
console.anthropic.com) — either pass it inline with `--api-key sk-ant-...` (saved to this
|
|
69
|
+
package's own local config file, `~/.product-console-companion/config.json`, permissions locked
|
|
70
|
+
to your user only) or set `ANTHROPIC_API_KEY` in your environment yourself; the inline flag takes
|
|
71
|
+
priority if both are present.
|
|
72
|
+
|
|
73
|
+
**`--backend cli`** needs [Claude Code](https://claude.com/claude-code) installed and you logged
|
|
74
|
+
into it first — `npm install -g @anthropic-ai/claude-code`, then run `claude` and follow the login
|
|
75
|
+
prompt. `connect` checks `claude --version` up front and fails fast if it's missing. Bills against
|
|
76
|
+
your Claude subscription, not a separate API key.
|
|
77
|
+
|
|
78
|
+
Once it's running:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx product-console-companion run
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
(this is what `connect` already ran for you, unless you passed `--no-run`) polls each configured
|
|
85
|
+
connection every few seconds until Ctrl+C. Nothing runs while this isn't running — see the parent
|
|
86
|
+
plan's Section 7 for why that's deliberate (goal 4: no AI spend while you're not around to
|
|
87
|
+
notice).
|
|
88
|
+
|
|
89
|
+
## What's NOT yet implemented (be aware before relying on this)
|
|
90
|
+
|
|
91
|
+
- **Per-task budget is not enforced client-side for the `api-key` backend.** Every dispatched
|
|
92
|
+
task carries a `maxBudgetUsd` (visible in the `run` log line); `api-key-caller.ts` does not
|
|
93
|
+
track spend against it or abort a call that exceeds it. The `cli` backend passes it straight
|
|
94
|
+
through as `claude`'s own `--max-budget-usd`, which the CLI does enforce (confirmed
|
|
95
|
+
empirically: it stops and reports `terminal_reason: 'budget_exhausted'`).
|
|
96
|
+
- **No wall-clock timeout for the `api-key` backend** — a hung request blocks that connection's
|
|
97
|
+
polling loop. The `cli` backend has one (10 minutes, independent of the budget cap — a dollar
|
|
98
|
+
ceiling doesn't stop a network-stall hang).
|
|
99
|
+
- **Git-fetch-if-missing is `analyse_shipment`-only.** Every other task kind reads whatever HEAD
|
|
100
|
+
currently is in `--path` (or, for `cli`, a worktree pinned to that HEAD) — correct for those,
|
|
101
|
+
since they check the *current* state of your code, not a specific historical commit.
|
|
102
|
+
`analyse_shipment` is different: it's about one specific commit a webhook or the hosted poller
|
|
103
|
+
told the server about, which the server never fetches for you. It verifies that commit is
|
|
104
|
+
actually present locally first, attempts one `git fetch` (your own git credentials — never
|
|
105
|
+
anything from the hosted server) if it isn't, and reports back distinctly (`sha_not_local`)
|
|
106
|
+
rather than silently analysing the wrong tree state if it still can't find it.
|
|
107
|
+
- **`cli` backend's sandbox worktree isolates writes, not reads** — `--add-dir` points at the
|
|
108
|
+
disposable worktree, so a read sees exactly what's committed at HEAD, including anything
|
|
109
|
+
`.gitignore`d being invisible (same as `git ls-files`'s own behavior). Uncommitted local
|
|
110
|
+
changes in your real checkout are never visible to the CLI backend — worth knowing if you're
|
|
111
|
+
expecting a check to see work-in-progress edits. (`analyse_shipment`'s own diff read is the one
|
|
112
|
+
exception: it happens against your real checkout, via `git show`, before the sandbox is even
|
|
113
|
+
created — the model itself still only ever sees the sandboxed copy.)
|
|
114
|
+
|
|
115
|
+
## Local development (inside the parent repo)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd companion
|
|
119
|
+
npm install
|
|
120
|
+
npm run dev -- connect --url http://localhost:3101 --token <publicId>.<secret> --connection <connectionId> --path <repo path> --backend api-key
|
|
121
|
+
npm run dev -- run
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Publishing a new version
|
|
125
|
+
|
|
126
|
+
Via GitHub Actions Trusted Publishing (`.github/workflows/publish-companion.yml`) — no npm token
|
|
127
|
+
generated, pasted, or stored for this, ever. One-time setup on npmjs.com (already done as of this
|
|
128
|
+
writing): on the `product-console-companion` package's own page, **Settings → Trusted
|
|
129
|
+
publishing → GitHub Actions**, pointing at this repo (`fynchconsulting/product-console`) and this
|
|
130
|
+
exact workflow filename (`publish-companion.yml`), no environment.
|
|
131
|
+
|
|
132
|
+
To actually publish once that's configured: bump the version in `package.json` in a normal PR,
|
|
133
|
+
merge it, then either
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
git tag companion-v<the new version> && git push --tags
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
or trigger the workflow manually (Actions tab, or `gh workflow run publish-companion.yml`). Either
|
|
140
|
+
way it publishes whatever's currently in `package.json` at that point — there's no separate
|
|
141
|
+
"which version" input to get wrong.
|
package/dist/api-key-caller.js
CHANGED
|
@@ -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,
|
|
@@ -160,19 +160,41 @@ export async function runGenerateSpecBatchTask(client, task, repoPath) {
|
|
|
160
160
|
const payload = task.payload;
|
|
161
161
|
const spec = payload.spec;
|
|
162
162
|
const moduleIds = payload.moduleIds;
|
|
163
|
+
if (payload.connectionless) {
|
|
164
|
+
// The PRD is already in spec.stableText (stableTextForLocalDispatch); there is nothing local to read.
|
|
165
|
+
const content = [];
|
|
166
|
+
if (spec.stableText)
|
|
167
|
+
content.push({ type: 'text', text: spec.stableText, cache_control: { type: 'ephemeral' } });
|
|
168
|
+
content.push({ type: 'text', text: spec.volatileText });
|
|
169
|
+
const writeStream = client.messages.stream({
|
|
170
|
+
model: spec.model,
|
|
171
|
+
max_tokens: spec.maxTokens,
|
|
172
|
+
thinking: { type: 'adaptive' },
|
|
173
|
+
output_config: spec.schema ? { effort: spec.effort, format: { type: 'json_schema', schema: spec.schema } } : { effort: spec.effort },
|
|
174
|
+
system: spec.system,
|
|
175
|
+
messages: [{ role: 'user', content }],
|
|
176
|
+
});
|
|
177
|
+
const writeMsg = await writeStream.finalMessage();
|
|
178
|
+
if (writeMsg.stop_reason === 'refusal')
|
|
179
|
+
throw new Error('The model declined to write specs for this batch.');
|
|
180
|
+
const writeText = writeMsg.content.find((b) => b.type === 'text');
|
|
181
|
+
if (!writeText || writeText.type !== 'text')
|
|
182
|
+
throw new Error('No specs returned.');
|
|
183
|
+
return { result: JSON.parse(writeText.text), filesRead: [] };
|
|
184
|
+
}
|
|
163
185
|
const allFiles = await listRepoFiles(repoPath);
|
|
164
186
|
console.log(` scanning ${allFiles.length} files in ${repoPath} for ${moduleIds.length} module(s)…`);
|
|
165
187
|
// 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,
|
|
188
|
+
const selectPrompt = `${spec.volatileText}
|
|
189
|
+
|
|
190
|
+
# The repository file tree (${allFiles.length} files)
|
|
191
|
+
|
|
192
|
+
${allFiles.join('\n')}
|
|
193
|
+
|
|
194
|
+
# Before writing specs
|
|
195
|
+
|
|
196
|
+
For EACH module above (by id: ${moduleIds.join(', ')}), list up to ${MAX_FILES_PER_MODULE_IN_BATCH}
|
|
197
|
+
file paths (verbatim from the tree above) most likely to implement it. One entry per module id,
|
|
176
198
|
in the same order as given.`;
|
|
177
199
|
const selectStream = client.messages.stream({
|
|
178
200
|
model: spec.model,
|
|
@@ -236,31 +258,34 @@ in the same order as given.`;
|
|
|
236
258
|
* server/index.ts's completeAiJob payloads for /api/setup/skeleton and
|
|
237
259
|
* /api/module-suggestions/scan — neither includes it), so there's nothing meaningful to report.
|
|
238
260
|
*/
|
|
239
|
-
async function runShallowRepoTask(client, spec, repoPath) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
async function runShallowRepoTask(client, spec, repoPath, connectionless = false) {
|
|
262
|
+
let repoSection = '';
|
|
263
|
+
if (!connectionless) {
|
|
264
|
+
const repoCtx = await buildLocalRepoContext(repoPath);
|
|
265
|
+
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'}`);
|
|
266
|
+
repoSection = `
|
|
267
|
+
# Repository context (your local checkout)
|
|
268
|
+
|
|
269
|
+
## File tree (${repoCtx.fileList.length} files)
|
|
270
|
+
${repoCtx.fileList.join('\n')}
|
|
271
|
+
|
|
272
|
+
## README
|
|
273
|
+
${repoCtx.readme ?? '(none found)'}
|
|
274
|
+
|
|
275
|
+
## CHANGELOG
|
|
276
|
+
${repoCtx.changelog ?? '(none found)'}
|
|
277
|
+
|
|
278
|
+
## package.json
|
|
279
|
+
${repoCtx.packageJson ?? '(none found)'}
|
|
256
280
|
`.trim();
|
|
281
|
+
}
|
|
257
282
|
const content = [];
|
|
258
283
|
if (spec.stableDocumentBase64) {
|
|
259
284
|
content.push({ type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: spec.stableDocumentBase64 } });
|
|
260
285
|
}
|
|
261
286
|
if (spec.stableText)
|
|
262
287
|
content.push({ type: 'text', text: spec.stableText, cache_control: { type: 'ephemeral' } });
|
|
263
|
-
content.push({ type: 'text', text: `${spec.volatileText}\n\n${repoSection}` });
|
|
288
|
+
content.push({ type: 'text', text: connectionless ? spec.volatileText : `${spec.volatileText}\n\n${repoSection}` });
|
|
264
289
|
const stream = client.messages.stream({
|
|
265
290
|
model: spec.model,
|
|
266
291
|
max_tokens: spec.maxTokens,
|
|
@@ -279,7 +304,7 @@ ${repoCtx.packageJson ?? '(none found)'}
|
|
|
279
304
|
}
|
|
280
305
|
export async function runProposeSkeletonTask(client, task, repoPath) {
|
|
281
306
|
const payload = task.payload;
|
|
282
|
-
return runShallowRepoTask(client, payload.spec, repoPath);
|
|
307
|
+
return runShallowRepoTask(client, payload.spec, repoPath, payload.connectionless);
|
|
283
308
|
}
|
|
284
309
|
export async function runProposeNewCapabilitiesTask(client, task, repoPath) {
|
|
285
310
|
const payload = task.payload;
|
package/dist/cli.js
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
import Anthropic from '@anthropic-ai/sdk';
|
|
3
3
|
import spawn from 'cross-spawn';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
-
import { addConnection, configPath, loadConfig } from './config.js';
|
|
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
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
|
-
|
|
16
|
-
|
|
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,40 +41,84 @@ 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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
const apiKey = f['api-key'];
|
|
45
|
+
// Pairing on its own does nothing useful — see cmdRun below — so this defaults to starting
|
|
46
|
+
// right away, which is what someone following the app's own copy-pasted command wants: one
|
|
47
|
+
// paste, and it's working. --no-run is the escape hatch for pairing a second/third project
|
|
48
|
+
// before starting one shared `run` for all of them (see the README's own multi-connection note).
|
|
49
|
+
const shouldRunAfter = f['no-run'] !== 'true';
|
|
50
|
+
if (!url || !token) {
|
|
51
|
+
console.error('Usage: product-console-companion connect --url <your Console link> --token <publicId.secret> --connection <connectionId> ' +
|
|
52
|
+
'[--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]');
|
|
39
54
|
process.exitCode = 1;
|
|
40
55
|
return;
|
|
41
56
|
}
|
|
57
|
+
if (!connectionId) {
|
|
58
|
+
const client = new CompanionClient(url, token);
|
|
59
|
+
try {
|
|
60
|
+
await client.whoami();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`Hmm, that didn't work — could not verify your token against ${url}. Double check you copied the ` +
|
|
64
|
+
`WHOLE thing, including the part after the dot.`, err instanceof Error ? err.message : err);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
enableWorkspacePolling({ url, token, apiKey });
|
|
69
|
+
console.log(`Connected! This computer will now run AI calls for this workspace's setup (PRD-only) tasks. (Saved to ${configPath}.)`);
|
|
70
|
+
if (!shouldRunAfter) {
|
|
71
|
+
console.log('Run "product-console-companion run" whenever you\'re ready to start.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log('Starting now — Ctrl+C any time to stop.\n');
|
|
75
|
+
await cmdRun();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// --path is optional — defaults to wherever this command is actually run from, so "open a
|
|
79
|
+
// terminal in your project's folder, paste this" just works without typing a path at all.
|
|
80
|
+
const repoPath = resolve(f.path || '.');
|
|
81
|
+
const backend = (f.backend || 'api-key');
|
|
42
82
|
if (backend !== 'api-key' && backend !== 'cli') {
|
|
43
83
|
console.error(`--backend must be "api-key" or "cli", got "${backend}"`);
|
|
44
84
|
process.exitCode = 1;
|
|
45
85
|
return;
|
|
46
86
|
}
|
|
47
|
-
|
|
87
|
+
try {
|
|
88
|
+
await assertIsRepo(repoPath);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error((err instanceof Error ? err.message : String(err)) + ' Run this from inside your project\'s folder, or pass --path <folder>.');
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
48
95
|
if (backend === 'cli') {
|
|
49
96
|
const version = await checkClaudeCliAvailable();
|
|
50
97
|
if (!version) {
|
|
51
|
-
console.error('
|
|
98
|
+
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
99
|
process.exitCode = 1;
|
|
53
100
|
return;
|
|
54
101
|
}
|
|
55
|
-
console.log(`Found
|
|
102
|
+
console.log(`Found Claude Code: ${version}`);
|
|
56
103
|
}
|
|
57
104
|
const client = new CompanionClient(url, token);
|
|
58
105
|
try {
|
|
59
106
|
await client.whoami();
|
|
60
107
|
}
|
|
61
108
|
catch (err) {
|
|
62
|
-
console.error(`
|
|
109
|
+
console.error(`Hmm, that didn't work — could not verify your token against ${url}. Double check you copied the ` +
|
|
110
|
+
`WHOLE thing, including the part after the dot.`, err instanceof Error ? err.message : err);
|
|
63
111
|
process.exitCode = 1;
|
|
64
112
|
return;
|
|
65
113
|
}
|
|
66
|
-
addConnection({ url, token, connection: { connectionId, repoPath, backend } });
|
|
67
|
-
console.log(`
|
|
68
|
-
|
|
114
|
+
addConnection({ url, token, connection: { connectionId, repoPath, backend }, apiKey });
|
|
115
|
+
console.log(`Connected! This computer will now read this project's code and run AI calls for it. (Saved to ${configPath}.)`);
|
|
116
|
+
if (!shouldRunAfter) {
|
|
117
|
+
console.log('Run "product-console-companion run" whenever you\'re ready to start.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log('Starting now — Ctrl+C any time to stop.\n');
|
|
121
|
+
await cmdRun();
|
|
69
122
|
}
|
|
70
123
|
/** How often an idle connection is re-polled when nothing was claimed — a short client-side
|
|
71
124
|
* poll, not a server-held long-poll (see the plan's own reasoning: this app has already been
|
|
@@ -78,23 +131,29 @@ const HEARTBEAT_MS = 30_000;
|
|
|
78
131
|
* each backed by both backends. Grows by one entry per phase, same as the server's own registry
|
|
79
132
|
* (server/companion.ts's TASK_RESULT_SCHEMAS) — the two lists should always name the same kinds. */
|
|
80
133
|
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
|
+
}
|
|
81
140
|
if (task.kind === 'verify_module') {
|
|
82
|
-
return
|
|
141
|
+
return effectiveBackend === 'cli' ? runVerifyModuleTaskViaCli(task, repoPath) : runVerifyModuleTask(anthropic, task, repoPath); // anthropic is always set for backend 'api-key' — see cmdRun
|
|
83
142
|
}
|
|
84
143
|
if (task.kind === 'generate_spec_batch') {
|
|
85
|
-
return
|
|
144
|
+
return effectiveBackend === 'cli' ? runGenerateSpecBatchTaskViaCli(task, repoPath) : runGenerateSpecBatchTask(anthropic, task, repoPath);
|
|
86
145
|
}
|
|
87
146
|
if (task.kind === 'propose_skeleton') {
|
|
88
|
-
return
|
|
147
|
+
return effectiveBackend === 'cli' ? runProposeSkeletonTaskViaCli(task, repoPath) : runProposeSkeletonTask(anthropic, task, repoPath);
|
|
89
148
|
}
|
|
90
149
|
if (task.kind === 'propose_new_capabilities') {
|
|
91
|
-
return
|
|
150
|
+
return effectiveBackend === 'cli' ? runProposeNewCapabilitiesTaskViaCli(task, repoPath) : runProposeNewCapabilitiesTask(anthropic, task, repoPath);
|
|
92
151
|
}
|
|
93
152
|
if (task.kind === 'design_prompt') {
|
|
94
|
-
return
|
|
153
|
+
return effectiveBackend === 'cli' ? runDesignPromptTaskViaCli(task, repoPath) : runDesignPromptTask(anthropic, task, repoPath);
|
|
95
154
|
}
|
|
96
155
|
if (task.kind === 'analyse_shipment') {
|
|
97
|
-
return
|
|
156
|
+
return effectiveBackend === 'cli' ? runAnalyseShipmentTaskViaCli(task, repoPath) : runAnalyseShipmentTask(anthropic, task, repoPath);
|
|
98
157
|
}
|
|
99
158
|
throw new Error(`This companion doesn't know how to run task kind "${task.kind}" yet.`);
|
|
100
159
|
}
|
|
@@ -124,43 +183,56 @@ async function runOneTask(anthropic, backend, client, repoPath, task) {
|
|
|
124
183
|
}
|
|
125
184
|
async function cmdRun() {
|
|
126
185
|
const config = loadConfig();
|
|
127
|
-
if (!config || config.connections.length === 0) {
|
|
128
|
-
console.error(`
|
|
186
|
+
if (!config || (config.connections.length === 0 && !config.pollWorkspaceTasks)) {
|
|
187
|
+
console.error(`Nothing set up yet — ${configPath} is missing or empty. Run "connect" first.`);
|
|
129
188
|
process.exitCode = 1;
|
|
130
189
|
return;
|
|
131
190
|
}
|
|
132
|
-
//
|
|
133
|
-
// configured to run, not every prerequisite either backend could
|
|
134
|
-
const needsApiKey = config.connections.some((c) => c.backend !== 'cli');
|
|
191
|
+
// Repo backends are chosen per connection; workspace-level tasks always use api-key. Only
|
|
192
|
+
// require what's actually configured to run, not every prerequisite either backend could need.
|
|
193
|
+
const needsApiKey = config.pollWorkspaceTasks || config.connections.some((c) => c.backend !== 'cli');
|
|
135
194
|
const needsCli = config.connections.some((c) => c.backend === 'cli');
|
|
136
|
-
|
|
195
|
+
// config.apiKey (set via `connect --api-key ...`) takes priority over the environment variable
|
|
196
|
+
// — the easier path for someone who'd rather paste their key once into a command than learn
|
|
197
|
+
// what an environment variable is and how to set one on their OS. Both are supported; neither
|
|
198
|
+
// is required unless a configured connection actually needs it.
|
|
199
|
+
const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
137
200
|
if (needsApiKey && !apiKey) {
|
|
138
|
-
console.error('
|
|
201
|
+
console.error('No Anthropic API key found. Get one free at console.anthropic.com, then either run ' +
|
|
202
|
+
'"connect" again adding --api-key <your key>, or set it yourself: ' +
|
|
203
|
+
'"set ANTHROPIC_API_KEY=<your key>" (Windows) / "export ANTHROPIC_API_KEY=<your key>" (Mac/Linux), then run "run" again.');
|
|
139
204
|
process.exitCode = 1;
|
|
140
205
|
return;
|
|
141
206
|
}
|
|
142
207
|
if (needsCli) {
|
|
143
208
|
const version = await checkClaudeCliAvailable();
|
|
144
209
|
if (!version) {
|
|
145
|
-
console.error('
|
|
210
|
+
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
211
|
process.exitCode = 1;
|
|
147
212
|
return;
|
|
148
213
|
}
|
|
149
214
|
}
|
|
150
215
|
for (const c of config.connections) {
|
|
151
216
|
await assertIsRepo(c.repoPath).catch((err) => {
|
|
152
|
-
console.error(`Warning: ${c.connectionId}
|
|
217
|
+
console.error(`Warning: can't find ${c.connectionId}'s project folder (${c.repoPath}):`, err instanceof Error ? err.message : err);
|
|
153
218
|
});
|
|
154
219
|
}
|
|
155
220
|
const anthropic = needsApiKey ? new Anthropic({ apiKey }) : null;
|
|
156
221
|
const client = new CompanionClient(config.url, config.token);
|
|
157
222
|
await client.whoami();
|
|
158
|
-
|
|
223
|
+
const projectCount = config.connections.length;
|
|
224
|
+
const watching = config.pollWorkspaceTasks
|
|
225
|
+
? `${projectCount > 0 ? `${projectCount} project${projectCount === 1 ? '' : 's'} and ` : ''}workspace-level tasks`
|
|
226
|
+
: `${projectCount} project${projectCount === 1 ? '' : 's'}`;
|
|
227
|
+
console.log(`Connected to ${config.url}. Watching ${watching}:`);
|
|
159
228
|
for (const c of config.connections) {
|
|
160
229
|
const sha = await currentCommit(c.repoPath).catch(() => '(unknown)');
|
|
161
230
|
console.log(` - ${c.connectionId} → ${c.repoPath} @ ${sha.slice(0, 7)} (${c.backend})`);
|
|
162
231
|
}
|
|
163
|
-
|
|
232
|
+
if (config.pollWorkspaceTasks) {
|
|
233
|
+
console.log(' - workspace-level (connection-less / PRD-only) tasks');
|
|
234
|
+
}
|
|
235
|
+
console.log('Waiting for work — leave this running. Ctrl+C any time to stop.');
|
|
164
236
|
let stopping = false;
|
|
165
237
|
process.on('SIGINT', () => {
|
|
166
238
|
if (stopping)
|
|
@@ -190,6 +262,25 @@ async function cmdRun() {
|
|
|
190
262
|
claimedAny = true;
|
|
191
263
|
await runOneTask(anthropic, c.backend, client, c.repoPath, claim.task);
|
|
192
264
|
}
|
|
265
|
+
if (!stopping && config.pollWorkspaceTasks) {
|
|
266
|
+
let claim;
|
|
267
|
+
try {
|
|
268
|
+
claim = await client.claimNext();
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
if (err instanceof CompanionHttpError && err.status === 401) {
|
|
272
|
+
console.error('Token rejected (revoked?) — stopping. Pair a new token and reconnect.');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
console.error('Workspace-level poll failed:', err instanceof Error ? err.message : err);
|
|
276
|
+
claim = { task: null };
|
|
277
|
+
}
|
|
278
|
+
if (claim.task) {
|
|
279
|
+
claimedAny = true;
|
|
280
|
+
// Connection-less task kinds do not read a repo; cwd is a harmless placeholder.
|
|
281
|
+
await runOneTask(anthropic, 'api-key', client, process.cwd(), claim.task);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
193
284
|
if (!claimedAny && !stopping)
|
|
194
285
|
await new Promise((r) => setTimeout(r, IDLE_POLL_MS));
|
|
195
286
|
}
|
package/dist/config.js
CHANGED
|
@@ -20,12 +20,35 @@ 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 = {
|
|
30
|
+
const config = {
|
|
31
|
+
url: args.url,
|
|
32
|
+
token: args.token,
|
|
33
|
+
connections,
|
|
34
|
+
apiKey: args.apiKey || existing?.apiKey,
|
|
35
|
+
pollWorkspaceTasks: existing?.pollWorkspaceTasks,
|
|
36
|
+
};
|
|
37
|
+
save(config);
|
|
38
|
+
return config;
|
|
39
|
+
}
|
|
40
|
+
/** Records that this companion should also poll for workspace-level (connection-less) tasks.
|
|
41
|
+
* Additive: keeps any existing connections and apiKey; only sets url/token if not already set
|
|
42
|
+
* (or if they differ — same rule as addConnection). */
|
|
43
|
+
export function enableWorkspacePolling(args) {
|
|
44
|
+
const existing = loadConfig();
|
|
45
|
+
const config = {
|
|
46
|
+
url: args.url,
|
|
47
|
+
token: args.token,
|
|
48
|
+
connections: existing?.connections ?? [],
|
|
49
|
+
apiKey: args.apiKey || existing?.apiKey,
|
|
50
|
+
pollWorkspaceTasks: true,
|
|
51
|
+
};
|
|
29
52
|
save(config);
|
|
30
53
|
return config;
|
|
31
54
|
}
|
package/dist/http-client.js
CHANGED
|
@@ -26,7 +26,7 @@ export class CompanionClient {
|
|
|
26
26
|
return this.call('/api/companion/whoami');
|
|
27
27
|
}
|
|
28
28
|
claimNext(connectionId) {
|
|
29
|
-
return this.call(`/api/companion/tasks/next?connectionId=${encodeURIComponent(connectionId)}`);
|
|
29
|
+
return this.call(connectionId ? `/api/companion/tasks/next?connectionId=${encodeURIComponent(connectionId)}` : '/api/companion/tasks/next');
|
|
30
30
|
}
|
|
31
31
|
heartbeat(taskId) {
|
|
32
32
|
return this.call(`/api/companion/tasks/${encodeURIComponent(taskId)}/heartbeat`, { method: 'POST' });
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "product-console-companion",
|
|
3
|
-
"version": "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.3.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
|
+
}
|