opencode-agenthub 0.1.0 → 0.1.1

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 CHANGED
@@ -1,8 +1,11 @@
1
1
  # opencode-agenthub
2
2
 
3
- > **Alpha.** Requires Node >= 18.0.0. macOS and Linux are primary targets. Windows users should use WSL 2 for the best experience; native Windows remains best-effort in alpha.
3
+ [![npm version](https://img.shields.io/npm/v/opencode-agenthub.svg)](https://www.npmjs.com/package/opencode-agenthub)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
5
 
5
- `opencode-agenthub` is a lightweight framework and CLI for organizing your AI agents, skills, prompts, and workspace runtime setup into one consistent structure.
6
+ > Requires Node >= 18.0.0. Supports macOS and Linux directly. Windows users should use WSL 2 for the best experience; native Windows support remains best-effort alpha.
7
+
8
+ `opencode-agenthub` is a control plane and CLI for organizing, composing, and activating OpenCode agents, skills, profiles, bundles, and workspace runtime setup.
6
9
 
7
10
  The npm package name is `opencode-agenthub`. The CLI command is `agenthub`. `opencode-agenthub` also works as a compatibility alias.
8
11
 
@@ -9,7 +9,7 @@
9
9
  "name": "auto",
10
10
  "mode": "primary",
11
11
  "hidden": false,
12
- "model": "github-copilot/claude-sonnet-4.5",
12
+ "model": "",
13
13
  "description": "Default coding agent with OMO-style intent detection and native build/edit/test workflow",
14
14
  "permission": {
15
15
  "*": "allow"
@@ -46,10 +46,12 @@ import {
46
46
  } from "./settings.js";
47
47
  import {
48
48
  displayHomeConfigPath,
49
+ interactivePromptResetSequence,
49
50
  resolvePythonCommand,
50
51
  shouldChmod,
51
52
  shouldOfferEnvrc,
52
53
  spawnOptions,
54
+ stripTerminalControlInput,
53
55
  windowsStartupNotice
54
56
  } from "./platform.js";
55
57
  const cliCommand = "agenthub";
@@ -1186,10 +1188,23 @@ const maybeConfigureEnvrc = async (workspace, configRoot) => {
1186
1188
  rl.close();
1187
1189
  }
1188
1190
  };
1189
- const createPromptInterface = () => readline.createInterface({ input: process.stdin, output: process.stdout });
1191
+ const createPromptInterface = () => {
1192
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1193
+ if (interactive) {
1194
+ const resetSequence = interactivePromptResetSequence();
1195
+ if (resetSequence) process.stdout.write(resetSequence);
1196
+ }
1197
+ return readline.createInterface({
1198
+ input: process.stdin,
1199
+ output: process.stdout,
1200
+ terminal: interactive
1201
+ });
1202
+ };
1203
+ const askPrompt = async (rl, question) => stripTerminalControlInput(await rl.question(question));
1190
1204
  const promptRequired = async (rl, question, defaultValue) => {
1191
1205
  while (true) {
1192
- const answer = await rl.question(
1206
+ const answer = await askPrompt(
1207
+ rl,
1193
1208
  defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
1194
1209
  );
1195
1210
  const value = normalizeOptional(answer) || defaultValue;
@@ -1198,14 +1213,16 @@ const promptRequired = async (rl, question, defaultValue) => {
1198
1213
  }
1199
1214
  };
1200
1215
  const promptOptional = async (rl, question, defaultValue) => {
1201
- const answer = await rl.question(
1216
+ const answer = await askPrompt(
1217
+ rl,
1202
1218
  defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
1203
1219
  );
1204
1220
  return normalizeOptional(answer) || defaultValue;
1205
1221
  };
1206
1222
  const promptCsv = async (rl, question, defaultValues = []) => {
1207
1223
  const defaultValue = defaultValues.join(", ");
1208
- const answer = await rl.question(
1224
+ const answer = await askPrompt(
1225
+ rl,
1209
1226
  defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
1210
1227
  );
1211
1228
  return normalizeCsv(answer || defaultValue);
@@ -1213,7 +1230,7 @@ const promptCsv = async (rl, question, defaultValues = []) => {
1213
1230
  const promptBoolean = async (rl, question, defaultValue) => {
1214
1231
  const suffix = defaultValue ? "[Y/n]" : "[y/N]";
1215
1232
  while (true) {
1216
- const answer = (await rl.question(`${question} ${suffix}: `)).trim().toLowerCase();
1233
+ const answer = (await askPrompt(rl, `${question} ${suffix}: `)).trim().toLowerCase();
1217
1234
  if (!answer) return defaultValue;
1218
1235
  if (answer === "y" || answer === "yes") return true;
1219
1236
  if (answer === "n" || answer === "no") return false;
@@ -1223,7 +1240,7 @@ const promptBoolean = async (rl, question, defaultValue) => {
1223
1240
  const promptChoice = async (rl, question, choices, defaultValue) => {
1224
1241
  const label = `${question} [${choices.join("/")}] (${defaultValue})`;
1225
1242
  while (true) {
1226
- const answer = (await rl.question(`${label}: `)).trim().toLowerCase();
1243
+ const answer = (await askPrompt(rl, `${label}: `)).trim().toLowerCase();
1227
1244
  if (!answer) return defaultValue;
1228
1245
  const match = choices.find((choice) => choice === answer);
1229
1246
  if (match) return match;
@@ -1238,7 +1255,10 @@ const promptIndexedChoice = async (rl, question, choices, defaultValue) => {
1238
1255
  });
1239
1256
  const defaultIndex = Math.max(choices.indexOf(defaultValue), 0) + 1;
1240
1257
  while (true) {
1241
- const answer = (await rl.question(`${question} [1-${choices.length}] (${defaultIndex}): `)).trim().toLowerCase();
1258
+ const answer = (await askPrompt(
1259
+ rl,
1260
+ `${question} [1-${choices.length}] (${defaultIndex}): `
1261
+ )).trim().toLowerCase();
1242
1262
  if (!answer) return defaultValue;
1243
1263
  const numeric = Number(answer);
1244
1264
  if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
@@ -1522,7 +1542,8 @@ const createSkillDefinition = async (root, name, reservedOk = false) => {
1522
1542
  };
1523
1543
  const readProfileDefinition = async (root, name) => readJsonIfExists(path.join(root, "profiles", `${name}.json`));
1524
1544
  const promptRecord = async (rl, question) => {
1525
- const answer = await rl.question(
1545
+ const answer = await askPrompt(
1546
+ rl,
1526
1547
  `${question} (comma-separated key=value, blank to skip): `
1527
1548
  );
1528
1549
  const entries = normalizeCsv(answer);
@@ -5,6 +5,20 @@ const shouldChmod = (win) => !isWindows(win);
5
5
  const shouldOfferEnvrc = (win) => !isWindows(win);
6
6
  const resolvePythonCommand = (win) => isWindows(win) ? "python" : "python3";
7
7
  const spawnOptions = (win) => isWindows(win) ? { shell: true } : {};
8
+ const csiSequencePattern = /\u001b\[[0-?]*[ -/]*[@-~]/g;
9
+ const oscSequencePattern = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
10
+ const singleEscapePattern = /\u001b[@-_]/g;
11
+ const controlCharacterPattern = /[\u0000-\u001f\u007f]/g;
12
+ const stripTerminalControlInput = (value) => value.replace(oscSequencePattern, "").replace(csiSequencePattern, "").replace(singleEscapePattern, "").replace(controlCharacterPattern, "");
13
+ const interactivePromptResetSequence = (win = detectWindows()) => isWindows(win) ? [
14
+ "\x1B[?1000l",
15
+ "\x1B[?1001l",
16
+ "\x1B[?1002l",
17
+ "\x1B[?1003l",
18
+ "\x1B[?1005l",
19
+ "\x1B[?1006l",
20
+ "\x1B[?1015l"
21
+ ].join("") : "";
8
22
  const generateRunScript = () => `#!/usr/bin/env bash
9
23
  set -euo pipefail
10
24
 
@@ -37,12 +51,14 @@ export {
37
51
  displayHomeConfigPath,
38
52
  generateRunCmd,
39
53
  generateRunScript,
54
+ interactivePromptResetSequence,
40
55
  isWindows,
41
56
  resolveHomeConfigRoot,
42
57
  resolvePythonCommand,
43
58
  shouldChmod,
44
59
  shouldOfferEnvrc,
45
60
  spawnOptions,
61
+ stripTerminalControlInput,
46
62
  symlinkType,
47
63
  windowsStartupNotice
48
64
  };
@@ -132,7 +132,7 @@ async function createBundleForSoul(targetRoot, soulName, options = {}) {
132
132
  agent: {
133
133
  name: options.agentName ?? soulName,
134
134
  mode: options.mode ?? "primary",
135
- model: options.model || "github-copilot/claude-sonnet-4.5",
135
+ model: options.model ?? "",
136
136
  description: "Auto-generated bundle for imported soul"
137
137
  }
138
138
  };
@@ -149,9 +149,9 @@ async function createBundleFlow(rl, targetRoot) {
149
149
  const modeInput = await rl.question("Mode [primary/subagent, default: primary]: ");
150
150
  const mode = modeInput.trim() === "subagent" ? "subagent" : "primary";
151
151
  const modelInput = await rl.question(
152
- "Model [default: github-copilot/claude-sonnet-4.5]: "
152
+ "Model [default: none]: "
153
153
  );
154
- const model = modelInput.trim() || "github-copilot/claude-sonnet-4.5";
154
+ const model = modelInput.trim();
155
155
  const result = await createBundleForSoul(targetRoot, soulName.trim(), {
156
156
  agentName: finalAgentName,
157
157
  mode,
@@ -9,12 +9,22 @@ import tempfile
9
9
  from pathlib import Path
10
10
 
11
11
 
12
- def resolve_agenthub_bin() -> str:
12
+ def wrap_executable(command: str) -> list[str]:
13
+ path = Path(command)
14
+ suffix = path.suffix.lower()
15
+ if suffix == ".js":
16
+ return ["node", command]
17
+ if os.name == "nt" and suffix in {".cmd", ".bat"}:
18
+ return ["cmd", "/c", command]
19
+ return [command]
20
+
21
+
22
+ def resolve_agenthub_command() -> list[str]:
13
23
  if os.getenv("OPENCODE_AGENTHUB_BIN"):
14
- return os.environ["OPENCODE_AGENTHUB_BIN"]
24
+ return wrap_executable(os.environ["OPENCODE_AGENTHUB_BIN"])
15
25
  found = shutil.which("opencode-agenthub")
16
26
  if found:
17
- return found
27
+ return wrap_executable(found)
18
28
  # Portable repo-local fallback: when running from src/skills/hr-support/bin/
19
29
  # inside the source tree, try <repo-root>/bin/opencode-agenthub
20
30
  this_file = Path(__file__).resolve()
@@ -28,13 +38,19 @@ def resolve_agenthub_bin() -> str:
28
38
  and parts[-4] == "skills"
29
39
  and parts[-5] == "src"
30
40
  ):
41
+ repo_bin_cmd = this_file.parents[4] / "bin" / "opencode-agenthub.cmd"
42
+ if os.name == "nt" and repo_bin_cmd.exists():
43
+ return ["cmd", "/c", str(repo_bin_cmd)]
31
44
  repo_bin = this_file.parents[4] / "bin" / "opencode-agenthub"
32
- if repo_bin.exists():
33
- return str(repo_bin)
45
+ if os.name != "nt" and repo_bin.exists():
46
+ return [str(repo_bin)]
47
+ repo_dist = this_file.parents[4] / "dist" / "composer" / "opencode-profile.js"
48
+ if repo_dist.exists():
49
+ return ["node", str(repo_dist)]
34
50
  raise SystemExit(
35
51
  "Could not locate opencode-agenthub.\n"
36
52
  " Set OPENCODE_AGENTHUB_BIN to the full path, or add opencode-agenthub to PATH.\n"
37
- " When running from source, ensure bin/opencode-agenthub exists in the repo root."
53
+ " When running from source, ensure bin/opencode-agenthub (or .cmd on Windows) exists, or build dist/composer/opencode-profile.js in the repo root."
38
54
  )
39
55
 
40
56
 
@@ -223,7 +239,6 @@ def main() -> int:
223
239
  "Usage: validate_staged_package.py <stage-package-root|agenthub-home-root>"
224
240
  )
225
241
 
226
- agenthub_bin = resolve_agenthub_bin()
227
242
  import_root = resolve_import_root(sys.argv[1])
228
243
  workspace_root = Path.cwd()
229
244
  validate_bundle_metadata(import_root)
@@ -235,6 +250,9 @@ def main() -> int:
235
250
  if not profiles:
236
251
  raise SystemExit("No profiles found in staged import root.")
237
252
 
253
+ # Integration smoke phase below requires a runnable agenthub binary.
254
+ agenthub_cmd = resolve_agenthub_command()
255
+
238
256
  with tempfile.TemporaryDirectory(prefix="agenthub-stage-validate-") as temp_dir:
239
257
  temp_root = Path(temp_dir)
240
258
  temp_home = temp_root / "home"
@@ -243,7 +261,7 @@ def main() -> int:
243
261
 
244
262
  run(
245
263
  [
246
- agenthub_bin,
264
+ *agenthub_cmd,
247
265
  "setup",
248
266
  "minimal",
249
267
  "--target-root",
@@ -252,7 +270,7 @@ def main() -> int:
252
270
  )
253
271
  run(
254
272
  [
255
- agenthub_bin,
273
+ *agenthub_cmd,
256
274
  "hub-import",
257
275
  "--source",
258
276
  str(import_root),
@@ -268,7 +286,7 @@ def main() -> int:
268
286
  for profile in profiles:
269
287
  run(
270
288
  [
271
- agenthub_bin,
289
+ *agenthub_cmd,
272
290
  "run",
273
291
  profile,
274
292
  "--workspace",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-agenthub",
3
- "version": "0.1.0",
4
- "description": "Systematic control plane for opencode agents, skills, profiles, and shared runtime assets.",
3
+ "version": "0.1.1",
4
+ "description": "A control plane for organizing, composing, and activating OpenCode agents, skills, profiles, and bundles.",
5
5
  "type": "module",
6
6
  "main": "./dist/plugins/opencode-agenthub.js",
7
7
  "exports": {
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  "build": "node scripts/build.mjs",
17
17
  "test:smoke": "bun test test/smoke-*.test.ts",
18
- "prepublishOnly": "npm run build"
18
+ "prepack": "npm run build"
19
19
  },
20
20
  "files": [
21
21
  "LICENSE",