@cooked-ham/hamgoose 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/bin/hamgoose.mjs +254 -151
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,14 +15,15 @@ dependency-free launcher so the install works from the Node world too. It finds
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
npx
|
|
18
|
+
# either globally …
|
|
19
|
+
npm i -g @cooked-ham/hamgoose
|
|
20
|
+
hamgoose register # install the Python package + register with Goose
|
|
21
|
+
hamgoose help # show all commands
|
|
22
|
+
|
|
23
|
+
# …or ad-hoc via npx
|
|
24
|
+
npx @cooked-ham/hamgoose install # install the Python package (idempotent)
|
|
25
|
+
npx @cooked-ham/hamgoose register # install + register with Goose's config
|
|
26
|
+
npx -y @cooked-ham/hamgoose # run the MCP stdio server (what Goose spawns)
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
## Use it as a Goose extension
|
|
@@ -34,8 +35,10 @@ npx -y @cooked-ham/hamgoose
|
|
|
34
35
|
npx -y @cooked-ham/hamgoose
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
(If you did `npm i -g @cooked-ham/hamgoose`, the command is simply `hamgoose`.)
|
|
39
|
+
|
|
37
40
|
…or the equivalent `extensions:` entry in `config.yaml`. Then in any repo:
|
|
38
|
-
`goose` → `/start_mission
|
|
41
|
+
`goose` → `/prompts` (list) or `/prompt start_mission` (guided setup).
|
|
39
42
|
|
|
40
43
|
Equivalent channels: `pip install git+https://github.com/cooked-ham/hamgoose.git`
|
|
41
44
|
or, once on PyPI, `pip install hamgoose` / `uvx hamgoose`.
|
package/bin/hamgoose.mjs
CHANGED
|
@@ -1,152 +1,255 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* @cooked-ham/hamgoose — npm launcher for the hamgoose Goose extension.
|
|
4
|
-
*
|
|
5
|
-
* hamgoose itself is a Python stdio MCP server (github.com/cooked-ham/hamgoose).
|
|
6
|
-
* This package is a thin, dependency-free launcher with one job: make
|
|
7
|
-
* `npx @cooked-ham/hamgoose`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (base
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
const
|
|
152
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @cooked-ham/hamgoose — npm launcher for the hamgoose Goose extension.
|
|
4
|
+
*
|
|
5
|
+
* hamgoose itself is a Python stdio MCP server (github.com/cooked-ham/hamgoose).
|
|
6
|
+
* This package is a thin, dependency-free launcher with one job: make
|
|
7
|
+
* `npx @cooked-ham/hamgoose` (or a globally installed `hamgoose`) just work.
|
|
8
|
+
* It never contains mission logic.
|
|
9
|
+
*
|
|
10
|
+
* hamgoose run the MCP stdio server (what Goose spawns)
|
|
11
|
+
* hamgoose install install the Python package (idempotent)
|
|
12
|
+
* hamgoose register install + register with Goose
|
|
13
|
+
* hamgoose unregister remove from Goose's config
|
|
14
|
+
* hamgoose help show help
|
|
15
|
+
* hamgoose --version print version
|
|
16
|
+
*
|
|
17
|
+
* Design notes (fix for 0.1.0 "infinite wall" bug):
|
|
18
|
+
* - The npm bin is named `hamgoose`, the SAME as the Python console script.
|
|
19
|
+
* Resolving "the server" via `where hamgoose` therefore finds THIS launcher's
|
|
20
|
+
* own global shim first and spawns itself forever. So the preferred
|
|
21
|
+
* resolution is `python -m hamgoose` (verified by importing the module with
|
|
22
|
+
* a known interpreter — PATH is never consulted), and any PATH hit that
|
|
23
|
+
* resolves back to this package is rejected (isOurOwn()).
|
|
24
|
+
* - On Windows we spawn a single quoted command line with an EMPTY args array.
|
|
25
|
+
* shell:true + args is deprecated (node DEP0190) and unsafe.
|
|
26
|
+
*/
|
|
27
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
28
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
29
|
+
import process from "node:process";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { createRequire } from "node:module";
|
|
32
|
+
|
|
33
|
+
const REPO = "https://github.com/cooked-ham/hamgoose.git";
|
|
34
|
+
const WIN = process.platform === "win32";
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const VERSION = require("../package.json").version;
|
|
37
|
+
// This launcher's own package dir (…/node_modules/@cooked-ham/hamgoose, or the
|
|
38
|
+
// repo's npm/ dir when run from a checkout). Anything resolving inside here is
|
|
39
|
+
// us, not the Python server.
|
|
40
|
+
const PKG_ROOT = path.dirname(require.resolve("../package.json"));
|
|
41
|
+
|
|
42
|
+
/** Quote one token for cmd.exe. */
|
|
43
|
+
function q(token) {
|
|
44
|
+
const t = String(token);
|
|
45
|
+
return /[ \t"]/.test(t) ? '"' + t.replace(/"/g, '""') + '"' : t;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run a command, capture output.
|
|
50
|
+
* Windows: build ONE quoted command line and pass no args (shell:true + args
|
|
51
|
+
* triggers DEP0190 and is unsafe). POSIX: plain exec with args, no shell.
|
|
52
|
+
*/
|
|
53
|
+
function run(cmd, args = [], inherit = false) {
|
|
54
|
+
const stdio = inherit ? "inherit" : "pipe";
|
|
55
|
+
const r = WIN
|
|
56
|
+
? spawnSync([cmd, ...args].map(q).join(" "), [], { encoding: "utf8", shell: true, stdio })
|
|
57
|
+
: spawnSync(cmd, args, { encoding: "utf8", stdio });
|
|
58
|
+
return { code: r.status ?? 1, out: ((r.stdout || "") + (r.stderr || "")).trim() };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Spawn the server the same safe way. sync=true returns a SpawnSync result. */
|
|
62
|
+
function launch(cmdArgs, extra = [], sync = false, env = process.env) {
|
|
63
|
+
const all = [cmdArgs[0], ...cmdArgs[1], ...extra];
|
|
64
|
+
if (WIN) {
|
|
65
|
+
const line = all.map(q).join(" ");
|
|
66
|
+
return sync
|
|
67
|
+
? spawnSync(line, [], { stdio: "inherit", shell: true, env })
|
|
68
|
+
: spawn(line, [], { stdio: "inherit", shell: true, env });
|
|
69
|
+
}
|
|
70
|
+
return sync
|
|
71
|
+
? spawnSync(all[0], all.slice(1), { stdio: "inherit", env })
|
|
72
|
+
: spawn(all[0], all.slice(1), { stdio: "inherit", env });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True if p points at THIS launcher (npm shim / our mjs), not the Python server. */
|
|
76
|
+
function isOurOwn(p) {
|
|
77
|
+
try {
|
|
78
|
+
let rp = path.resolve(p);
|
|
79
|
+
try { rp = realpathSync(rp); } catch { /* keep resolved path */ }
|
|
80
|
+
if (rp.startsWith(PKG_ROOT + path.sep)) return true; // inside this package
|
|
81
|
+
const base = path.basename(rp).toLowerCase();
|
|
82
|
+
if (base === "hamgoose.cmd" || base === "hamgoose.ps1") return true; // npm shims
|
|
83
|
+
if (!path.extname(base)) { // bare `hamgoose` script
|
|
84
|
+
try {
|
|
85
|
+
const txt = readFileSync(rp, "utf8");
|
|
86
|
+
if (txt.length < 8192 && /hamgoose\.mjs|@cooked-ham\/hamgoose/.test(txt)) return true;
|
|
87
|
+
} catch { /* unreadable → not ours */ }
|
|
88
|
+
}
|
|
89
|
+
} catch { /* never treat a failure as "not ours" in a way that matters */ }
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function which(name) {
|
|
94
|
+
const r = WIN
|
|
95
|
+
? run("where", [name])
|
|
96
|
+
: run("sh", ["-c", `command -v ${name}`]);
|
|
97
|
+
if (r.code !== 0) return null;
|
|
98
|
+
// Skip hits that are this launcher's own bin — the npm global `hamgoose`
|
|
99
|
+
// shim shadows the Python `hamgoose` console script (same name!).
|
|
100
|
+
const line = r.out.split(/\r?\n/).map((s) => s.trim()).find((l) => l && !isOurOwn(l));
|
|
101
|
+
return line || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pythonCandidates() {
|
|
105
|
+
return WIN ? ["py -3", "python", "python3"] : ["python3", "python"];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Find a Python >= 3.11 command string, or null. */
|
|
109
|
+
function findPython() {
|
|
110
|
+
for (const py of pythonCandidates()) {
|
|
111
|
+
const ok = run(py, [
|
|
112
|
+
"-c",
|
|
113
|
+
"import sys; sys.exit(0 if sys.version_info >= (3,11) else 1)",
|
|
114
|
+
]);
|
|
115
|
+
if (ok.code === 0) return py;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** pip-install the Python package (idempotent). Returns success bool. */
|
|
121
|
+
function pipInstall(py) {
|
|
122
|
+
console.log(`Installing hamgoose from ${REPO} …`);
|
|
123
|
+
let r = run(py, ["-m", "pip", "install", "--user", REPO], true);
|
|
124
|
+
if (r.code !== 0 && /externally managed/i.test(r.out)) {
|
|
125
|
+
console.log("Retrying with --break-system-packages (PEP 668)…");
|
|
126
|
+
r = run(py, ["-m", "pip", "install", "--user", "--break-system-packages", REPO], true);
|
|
127
|
+
}
|
|
128
|
+
if (r.code !== 0) {
|
|
129
|
+
console.error("\nInstall failed. Manual fallback:\n " +
|
|
130
|
+
` ${py} -m pip install ${REPO}\n` +
|
|
131
|
+
" (git must be on PATH for the git+ URL; see the repo README for a zip fallback)");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Locate how to run the Python hamgoose server. Returns [cmd, args] or null.
|
|
139
|
+
*
|
|
140
|
+
* Order matters: the interpreter-checked `py -m hamgoose` first, because the
|
|
141
|
+
* PATH fallback can never see the npm global shim that shadows the name.
|
|
142
|
+
*/
|
|
143
|
+
function resolveServer(py) {
|
|
144
|
+
// 1) The interpreter we found can import hamgoose → `py -m hamgoose`.
|
|
145
|
+
if (py && run(py, ["-c", "import hamgoose"]).code === 0) return [py, ["-m", "hamgoose"]];
|
|
146
|
+
// 2) A hamgoose console script on PATH that is not this launcher.
|
|
147
|
+
const onPath = which("hamgoose");
|
|
148
|
+
if (onPath && !isOurOwn(onPath)) return [onPath, []];
|
|
149
|
+
// 3) pip --user Scripts/bin fallback.
|
|
150
|
+
if (py) {
|
|
151
|
+
const base = run(py, ["-m", "site", "--user-base"]);
|
|
152
|
+
if (base.code === 0) {
|
|
153
|
+
const p = WIN
|
|
154
|
+
? path.join(base.out.trim(), "Scripts", "hamgoose.exe")
|
|
155
|
+
: path.join(base.out.trim(), "bin", "hamgoose");
|
|
156
|
+
if (existsSync(p)) return [p, []];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function describe(cmdArgs) {
|
|
163
|
+
return cmdArgs.length > 1 ? `${cmdArgs[0]} ${cmdArgs[1].join(" ")}` : cmdArgs[0];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Ensure the Python package is installed. Returns [cmd, args] or null. */
|
|
167
|
+
function ensureInstalled(quiet = false) {
|
|
168
|
+
const py = findPython();
|
|
169
|
+
let server = resolveServer(py);
|
|
170
|
+
if (server) {
|
|
171
|
+
if (!quiet) console.log(`hamgoose already installed (${describe(server)})`);
|
|
172
|
+
return server;
|
|
173
|
+
}
|
|
174
|
+
if (!py) {
|
|
175
|
+
console.error("Python 3.11+ not found. Install one (python.org or `uv`) and retry —\n" +
|
|
176
|
+
" or: uv python install 3.12 && uv tool install git+" + REPO);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!pipInstall(py)) return null;
|
|
180
|
+
server = resolveServer(py);
|
|
181
|
+
if (!quiet) console.log(server ? `Installed: ${describe(server)}` : "Installed (rerun, or check your Python install).");
|
|
182
|
+
return server;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printHelp() {
|
|
186
|
+
console.log(`hamgoose npm launcher v${VERSION}
|
|
187
|
+
Runs the Python hamgoose server (github.com/cooked-ham/hamgoose).
|
|
188
|
+
|
|
189
|
+
Usage (after \`npm i -g @cooked-ham/hamgoose\`, or via \`npx -y @cooked-ham/hamgoose\`):
|
|
190
|
+
hamgoose in a terminal: one-shot setup (install + register)
|
|
191
|
+
piped/stdio (what Goose spawns): run the MCP server
|
|
192
|
+
hamgoose install install the Python package (idempotent)
|
|
193
|
+
hamgoose register install + register with Goose's config.yaml
|
|
194
|
+
hamgoose unregister remove hamgoose from Goose's config.yaml
|
|
195
|
+
hamgoose help show this message
|
|
196
|
+
hamgoose --version print version
|
|
197
|
+
|
|
198
|
+
Use with Goose: Add Extension (STDIO), Name "hamgoose", Command:
|
|
199
|
+
hamgoose
|
|
200
|
+
|
|
201
|
+
In any repo: run \`goose\`, then type \`/mission <goal>\` (or just ask for a
|
|
202
|
+
mission in plain English). Manage with \`hamgoose unregister\` or
|
|
203
|
+
\`goose configure\` -> Extensions.`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [, , ...args] = process.argv;
|
|
207
|
+
const first = args[0];
|
|
208
|
+
|
|
209
|
+
if (first === "help" || first === "--help" || first === "-h") {
|
|
210
|
+
printHelp();
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
if (first === "--version" || first === "-v") {
|
|
214
|
+
console.log(VERSION);
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
if (first === "install") {
|
|
218
|
+
process.exit(ensureInstalled(false) ? 0 : 1);
|
|
219
|
+
}
|
|
220
|
+
if (first === "register" || first === "add" || first === "unregister" || first === "remove") {
|
|
221
|
+
const server = ensureInstalled(false);
|
|
222
|
+
if (!server) process.exit(1);
|
|
223
|
+
const pyCmd = first === "add" ? "register" : first === "remove" ? "unregister" : first;
|
|
224
|
+
console.log(pyCmd === "register" ? "Registering with Goose…" : "Updating Goose's config…");
|
|
225
|
+
const r = launch(server, [pyCmd, ...args.slice(1)], true);
|
|
226
|
+
process.exit(r.status ?? 1);
|
|
227
|
+
}
|
|
228
|
+
if (first) {
|
|
229
|
+
// Anything else is NOT stdio-server mode: don't spawn anything.
|
|
230
|
+
console.error(`Unknown command: ${first}\n\n`);
|
|
231
|
+
printHelp();
|
|
232
|
+
process.exit(2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// No args, real terminal: one-shot interactive setup (install + register).
|
|
236
|
+
// Goose spawns us with piped stdio, which falls through to server mode below.
|
|
237
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
238
|
+
console.log(`hamgoose v${VERSION} — setting up (idempotent, safe to rerun)`);
|
|
239
|
+
const setup = ensureInstalled(false);
|
|
240
|
+
if (!setup) process.exit(1);
|
|
241
|
+
console.log("Registering with Goose…");
|
|
242
|
+
const r = launch(setup, ["register"], true);
|
|
243
|
+
process.exit(r.status ?? 1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// No args + non-TTY: stdio server mode (Goose's extension command).
|
|
247
|
+
if (process.env.HAMGOOSE_LAUNCHER) {
|
|
248
|
+
console.error("error: hamgoose launcher recursion guard tripped — refusing to spawn. " +
|
|
249
|
+
"Check that `where hamgoose` / `which hamgoose` is not resolving to this launcher.");
|
|
250
|
+
process.exit(3);
|
|
251
|
+
}
|
|
252
|
+
const server = ensureInstalled(true);
|
|
253
|
+
if (!server) process.exit(1);
|
|
254
|
+
const child = launch(server, [], false, { ...process.env, HAMGOOSE_LAUNCHER: "1" });
|
|
255
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cooked-ham/hamgoose",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "npm launcher for hamgoose — Factory-Droid-style Mission orchestration extension for Goose (goal → plan → approve → isolated workers → validated code)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goose",
|