codeharbor 0.1.1 → 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/.env.example ADDED
@@ -0,0 +1,78 @@
1
+ MATRIX_HOMESERVER=https://matrix.biglone.tech
2
+ MATRIX_USER_ID=@codeharbor-bot:example.com
3
+ MATRIX_ACCESS_TOKEN=
4
+
5
+ # Optional explicit trigger in groups; can be empty to disable prefix trigger.
6
+ MATRIX_COMMAND_PREFIX=!code
7
+
8
+ CODEX_BIN=codex
9
+ CODEX_MODEL=
10
+ CODEX_WORKDIR=/Users/biglone/workspace
11
+ CODEX_DANGEROUS_BYPASS=false
12
+ CODEX_EXEC_TIMEOUT_MS=600000
13
+ CODEX_SANDBOX_MODE=
14
+ CODEX_APPROVAL_POLICY=
15
+ # Optional extra CLI args, space separated (example: --search --no-alt-screen)
16
+ CODEX_EXTRA_ARGS=
17
+ # Optional JSON object for additional environment variables passed to codex child process.
18
+ CODEX_EXTRA_ENV_JSON=
19
+
20
+ # SQLite state database path.
21
+ STATE_DB_PATH=data/state.db
22
+ # Legacy JSON path for one-time migration import.
23
+ STATE_PATH=data/state.json
24
+
25
+ MAX_PROCESSED_EVENTS_PER_SESSION=200
26
+ MAX_SESSION_AGE_DAYS=30
27
+ MAX_SESSIONS=5000
28
+ REPLY_CHUNK_SIZE=3500
29
+
30
+ MATRIX_PROGRESS_UPDATES=true
31
+ MATRIX_PROGRESS_MIN_INTERVAL_MS=2500
32
+ MATRIX_TYPING_TIMEOUT_MS=10000
33
+ SESSION_ACTIVE_WINDOW_MINUTES=20
34
+
35
+ # Group trigger defaults.
36
+ GROUP_TRIGGER_ALLOW_MENTION=true
37
+ GROUP_TRIGGER_ALLOW_REPLY=true
38
+ GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW=true
39
+ GROUP_TRIGGER_ALLOW_PREFIX=true
40
+
41
+ # Optional room-level trigger overrides (JSON object).
42
+ # ROOM_TRIGGER_POLICY_JSON={"!room:example.com":{"allowMention":true,"allowReply":true,"allowActiveWindow":false,"allowPrefix":false}}
43
+ ROOM_TRIGGER_POLICY_JSON=
44
+
45
+ # Rate limiting / anti-abuse.
46
+ RATE_LIMIT_WINDOW_SECONDS=60
47
+ RATE_LIMIT_MAX_REQUESTS_PER_USER=20
48
+ RATE_LIMIT_MAX_REQUESTS_PER_ROOM=120
49
+ RATE_LIMIT_MAX_CONCURRENT_GLOBAL=8
50
+ RATE_LIMIT_MAX_CONCURRENT_PER_USER=1
51
+ RATE_LIMIT_MAX_CONCURRENT_PER_ROOM=4
52
+
53
+ # CLI compatibility mode (IM shell approximation of codex CLI).
54
+ CLI_COMPAT_MODE=false
55
+ CLI_COMPAT_PASSTHROUGH_EVENTS=true
56
+ CLI_COMPAT_PRESERVE_WHITESPACE=true
57
+ CLI_COMPAT_DISABLE_REPLY_CHUNK_SPLIT=false
58
+ CLI_COMPAT_PROGRESS_THROTTLE_MS=300
59
+ CLI_COMPAT_FETCH_MEDIA=true
60
+ # Optional JSONL output path for executed prompt recording (for replay benchmarking).
61
+ CLI_COMPAT_RECORD_PATH=
62
+
63
+ DOCTOR_HTTP_TIMEOUT_MS=10000
64
+
65
+ # Admin API server (for config UI/backend).
66
+ ADMIN_BIND_HOST=127.0.0.1
67
+ ADMIN_PORT=8787
68
+ # Token protection for /api/admin/* endpoints.
69
+ # Strongly recommended for any non-localhost access.
70
+ # Required when exposing admin via reverse proxy/tunnel/public domain.
71
+ ADMIN_TOKEN=
72
+ # Optional IP allowlist (comma-separated, for example: 127.0.0.1,192.168.1.10).
73
+ ADMIN_IP_ALLOWLIST=
74
+ # Optional browser origin allowlist for CORS (comma-separated).
75
+ # Example: https://admin.example.com,https://ops.example.com
76
+ ADMIN_ALLOWED_ORIGINS=
77
+
78
+ LOG_LEVEL=info
package/README.md CHANGED
@@ -49,6 +49,44 @@ Install globally from npm (after publish):
49
49
  npm install -g codeharbor
50
50
  ```
51
51
 
52
+ Linux one-command install (creates `/opt/codeharbor`, sets ownership, installs latest package):
53
+
54
+ ```bash
55
+ curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/install-linux.sh | bash
56
+ ```
57
+
58
+ Linux easy mode (install + write `.env` + enable/start systemd in one run):
59
+
60
+ ```bash
61
+ curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/install-linux-easy.sh | bash -s -- \
62
+ --matrix-homeserver https://matrix.example.com \
63
+ --matrix-user-id @bot:example.com \
64
+ --matrix-access-token 'your-token'
65
+ ```
66
+
67
+ Enable Admin service at install time:
68
+
69
+ ```bash
70
+ curl -fsSL https://raw.githubusercontent.com/biglone/CodeHarbor/main/scripts/install-linux-easy.sh | bash -s -- \
71
+ --matrix-homeserver https://matrix.example.com \
72
+ --matrix-user-id @bot:example.com \
73
+ --matrix-access-token 'your-token' \
74
+ --enable-admin-service \
75
+ --admin-token 'replace-with-strong-token'
76
+ ```
77
+
78
+ Run local script with custom options:
79
+
80
+ ```bash
81
+ ./scripts/install-linux.sh --app-dir /srv/codeharbor --package codeharbor@0.1.1 --init
82
+ ```
83
+
84
+ Runtime home behavior:
85
+
86
+ - By default, all `codeharbor` commands use `/opt/codeharbor` for `.env` and relative data paths.
87
+ - No manual `cd /opt/codeharbor` is required after installation.
88
+ - To use a custom runtime directory, set `CODEHARBOR_HOME` (for example `export CODEHARBOR_HOME=/srv/codeharbor`).
89
+
52
90
  Install directly from GitHub:
53
91
 
54
92
  ```bash
@@ -133,6 +171,7 @@ npm install
133
171
  2. Configure environment:
134
172
 
135
173
  ```bash
174
+ export CODEHARBOR_HOME="$(pwd)"
136
175
  codeharbor init
137
176
  ```
138
177
 
@@ -145,6 +184,7 @@ Required values:
145
184
  3. Run in dev mode:
146
185
 
147
186
  ```bash
187
+ export CODEHARBOR_HOME="$(pwd)"
148
188
  npm run dev
149
189
  ```
150
190
 
@@ -152,6 +192,7 @@ npm run dev
152
192
 
153
193
  ```bash
154
194
  npm run build
195
+ export CODEHARBOR_HOME="$(pwd)"
155
196
  node dist/cli.js start
156
197
  ```
157
198
 
@@ -176,6 +217,8 @@ It documents:
176
217
  - `codeharbor admin serve`: start admin UI + config API server
177
218
  - `codeharbor config export`: export current config snapshot as JSON
178
219
  - `codeharbor config import <file>`: import config snapshot JSON (supports `--dry-run`)
220
+ - `scripts/install-linux.sh`: Linux bootstrap installer (creates runtime dir + installs npm package)
221
+ - `scripts/install-linux-easy.sh`: one-shot Linux install + config + systemd auto-start
179
222
  - `scripts/backup-config.sh`: export timestamped snapshot and keep latest N backups
180
223
  - `scripts/install-backup-timer.sh`: install/update user-level systemd timer for automatic backups
181
224
  - `npm run test:e2e`: run Admin UI end-to-end tests (Playwright)
package/dist/cli.js CHANGED
@@ -24,6 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
+ var import_node_fs9 = __toESM(require("fs"));
28
+ var import_node_path11 = __toESM(require("path"));
27
29
  var import_commander = require("commander");
28
30
 
29
31
  // src/app.ts
@@ -45,12 +47,9 @@ var import_dotenv = __toESM(require("dotenv"));
45
47
  async function runInitCommand(options = {}) {
46
48
  const cwd = options.cwd ?? process.cwd();
47
49
  const envPath = import_node_path.default.resolve(cwd, ".env");
48
- const templatePath = import_node_path.default.resolve(cwd, ".env.example");
50
+ const templatePath = resolveInitTemplatePath(cwd);
49
51
  const input = options.input ?? process.stdin;
50
52
  const output = options.output ?? process.stdout;
51
- if (!import_node_fs.default.existsSync(templatePath)) {
52
- throw new Error(`Cannot find template file: ${templatePath}`);
53
- }
54
53
  const templateContent = import_node_fs.default.readFileSync(templatePath, "utf8");
55
54
  const existingContent = import_node_fs.default.existsSync(envPath) ? import_node_fs.default.readFileSync(envPath, "utf8") : "";
56
55
  const existingValues = existingContent ? import_dotenv.default.parse(existingContent) : {};
@@ -142,6 +141,18 @@ async function runInitCommand(options = {}) {
142
141
  rl.close();
143
142
  }
144
143
  }
144
+ function resolveInitTemplatePath(cwd) {
145
+ const candidates = [
146
+ import_node_path.default.resolve(cwd, ".env.example"),
147
+ import_node_path.default.resolve(__dirname, "..", ".env.example")
148
+ ];
149
+ for (const candidate of candidates) {
150
+ if (import_node_fs.default.existsSync(candidate)) {
151
+ return candidate;
152
+ }
153
+ }
154
+ throw new Error(`Cannot find template file. Tried: ${candidates.join(", ")}`);
155
+ }
145
156
  function applyEnvOverrides(template, overrides) {
146
157
  const lines = template.split(/\r?\n/);
147
158
  const seen = /* @__PURE__ */ new Set();
@@ -4160,7 +4171,6 @@ var import_node_fs6 = __toESM(require("fs"));
4160
4171
  var import_node_path7 = __toESM(require("path"));
4161
4172
  var import_dotenv2 = __toESM(require("dotenv"));
4162
4173
  var import_zod = require("zod");
4163
- import_dotenv2.default.config();
4164
4174
  var configSchema = import_zod.z.object({
4165
4175
  MATRIX_HOMESERVER: import_zod.z.string().url(),
4166
4176
  MATRIX_USER_ID: import_zod.z.string().min(1),
@@ -4168,7 +4178,7 @@ var configSchema = import_zod.z.object({
4168
4178
  MATRIX_COMMAND_PREFIX: import_zod.z.string().default("!code"),
4169
4179
  CODEX_BIN: import_zod.z.string().default("codex"),
4170
4180
  CODEX_MODEL: import_zod.z.string().optional(),
4171
- CODEX_WORKDIR: import_zod.z.string().default(process.cwd()),
4181
+ CODEX_WORKDIR: import_zod.z.string().default("."),
4172
4182
  CODEX_DANGEROUS_BYPASS: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
4173
4183
  CODEX_EXEC_TIMEOUT_MS: import_zod.z.string().default("600000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
4174
4184
  CODEX_SANDBOX_MODE: import_zod.z.string().optional(),
@@ -4266,6 +4276,13 @@ var configSchema = import_zod.z.object({
4266
4276
  adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
4267
4277
  logLevel: v.LOG_LEVEL
4268
4278
  }));
4279
+ function loadEnvFromFile(filePath = import_node_path7.default.resolve(process.cwd(), ".env"), env = process.env) {
4280
+ import_dotenv2.default.config({
4281
+ path: filePath,
4282
+ processEnv: env,
4283
+ quiet: true
4284
+ });
4285
+ }
4269
4286
  function loadConfig(env = process.env) {
4270
4287
  const parsed = configSchema.safeParse(env);
4271
4288
  if (!parsed.success) {
@@ -4793,7 +4810,7 @@ async function runStartupPreflight(options = {}) {
4793
4810
  code: "missing_dotenv",
4794
4811
  check: ".env",
4795
4812
  message: `No .env file found at ${envPath}.`,
4796
- fix: "cp .env.example .env && codeharbor init"
4813
+ fix: 'Run "codeharbor init" to create baseline config.'
4797
4814
  });
4798
4815
  }
4799
4816
  for (const key of REQUIRED_ENV_KEYS) {
@@ -4893,14 +4910,29 @@ function readEnv(env, key) {
4893
4910
  return env[key]?.trim() ?? "";
4894
4911
  }
4895
4912
 
4913
+ // src/runtime-home.ts
4914
+ var import_node_path10 = __toESM(require("path"));
4915
+ var DEFAULT_RUNTIME_HOME = "/opt/codeharbor";
4916
+ var RUNTIME_HOME_ENV_KEY = "CODEHARBOR_HOME";
4917
+ function resolveRuntimeHome(env = process.env) {
4918
+ const configured = env[RUNTIME_HOME_ENV_KEY]?.trim();
4919
+ if (configured) {
4920
+ return import_node_path10.default.resolve(configured);
4921
+ }
4922
+ return DEFAULT_RUNTIME_HOME;
4923
+ }
4924
+
4896
4925
  // src/cli.ts
4926
+ var runtimeHome = null;
4897
4927
  var program = new import_commander.Command();
4898
4928
  program.name("codeharbor").description("Instant-messaging bridge for Codex CLI sessions").version("0.1.0");
4899
4929
  program.command("init").description("Create or update .env via guided prompts").option("-f, --force", "overwrite existing .env without confirmation").action(async (options) => {
4900
- await runInitCommand({ force: options.force ?? false });
4930
+ const home = ensureRuntimeHomeOrExit();
4931
+ await runInitCommand({ force: options.force ?? false, cwd: home });
4901
4932
  });
4902
4933
  program.command("start").description("Start CodeHarbor service").action(async () => {
4903
- const config = await loadConfigWithPreflight("start");
4934
+ const home = ensureRuntimeHomeOrExit();
4935
+ const config = await loadConfigWithPreflight("start", home);
4904
4936
  if (!config) {
4905
4937
  process.exitCode = 1;
4906
4938
  return;
@@ -4919,7 +4951,8 @@ program.command("start").description("Start CodeHarbor service").action(async ()
4919
4951
  });
4920
4952
  });
4921
4953
  program.command("doctor").description("Check codex and matrix connectivity").action(async () => {
4922
- const config = await loadConfigWithPreflight("doctor");
4954
+ const home = ensureRuntimeHomeOrExit();
4955
+ const config = await loadConfigWithPreflight("doctor", home);
4923
4956
  if (!config) {
4924
4957
  process.exitCode = 1;
4925
4958
  return;
@@ -4932,6 +4965,7 @@ admin.command("serve").description("Start admin config API server").option("--ho
4932
4965
  "--allow-insecure-no-token",
4933
4966
  "allow serving admin API without ADMIN_TOKEN on non-loopback host (not recommended)"
4934
4967
  ).action(async (options) => {
4968
+ ensureRuntimeHomeOrExit();
4935
4969
  const config = loadConfig();
4936
4970
  const host = options.host?.trim() || config.adminBindHost;
4937
4971
  const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
@@ -4962,7 +4996,8 @@ admin.command("serve").description("Start admin config API server").option("--ho
4962
4996
  });
4963
4997
  configCommand.command("export").description("Export config snapshot as JSON").option("-o, --output <path>", "write snapshot to file instead of stdout").action(async (options) => {
4964
4998
  try {
4965
- await runConfigExportCommand({ outputPath: options.output });
4999
+ const home = ensureRuntimeHomeOrExit();
5000
+ await runConfigExportCommand({ outputPath: options.output, cwd: home });
4966
5001
  } catch (error) {
4967
5002
  process.stderr.write(`Config export failed: ${formatError3(error)}
4968
5003
  `);
@@ -4971,9 +5006,11 @@ configCommand.command("export").description("Export config snapshot as JSON").op
4971
5006
  });
4972
5007
  configCommand.command("import").description("Import config snapshot from JSON").argument("<file>", "snapshot file path").option("--dry-run", "validate snapshot without writing changes").action(async (file, options) => {
4973
5008
  try {
5009
+ const home = ensureRuntimeHomeOrExit();
4974
5010
  await runConfigImportCommand({
4975
5011
  filePath: file,
4976
- dryRun: options.dryRun ?? false
5012
+ dryRun: options.dryRun ?? false,
5013
+ cwd: home
4977
5014
  });
4978
5015
  } catch (error) {
4979
5016
  process.stderr.write(`Config import failed: ${formatError3(error)}
@@ -4985,8 +5022,8 @@ if (process.argv.length <= 2) {
4985
5022
  process.argv.push("start");
4986
5023
  }
4987
5024
  void program.parseAsync(process.argv);
4988
- async function loadConfigWithPreflight(commandName) {
4989
- const preflight = await runStartupPreflight();
5025
+ async function loadConfigWithPreflight(commandName, runtimeHomePath) {
5026
+ const preflight = await runStartupPreflight({ cwd: runtimeHomePath });
4990
5027
  if (preflight.issues.length > 0) {
4991
5028
  const report = formatPreflightReport(preflight, commandName);
4992
5029
  if (preflight.ok) {
@@ -5006,6 +5043,31 @@ async function loadConfigWithPreflight(commandName) {
5006
5043
  return null;
5007
5044
  }
5008
5045
  }
5046
+ function ensureRuntimeHomeOrExit() {
5047
+ if (runtimeHome) {
5048
+ return runtimeHome;
5049
+ }
5050
+ const home = resolveRuntimeHome();
5051
+ try {
5052
+ import_node_fs9.default.mkdirSync(home, { recursive: true });
5053
+ } catch (error) {
5054
+ const message = error instanceof Error ? error.message : String(error);
5055
+ process.stderr.write(`Runtime setup failed: cannot create ${home}. ${message}
5056
+ `);
5057
+ process.exit(1);
5058
+ }
5059
+ try {
5060
+ process.chdir(home);
5061
+ } catch (error) {
5062
+ const message = error instanceof Error ? error.message : String(error);
5063
+ process.stderr.write(`Runtime setup failed: cannot switch to ${home}. ${message}
5064
+ `);
5065
+ process.exit(1);
5066
+ }
5067
+ loadEnvFromFile(import_node_path11.default.resolve(home, ".env"));
5068
+ runtimeHome = home;
5069
+ return runtimeHome;
5070
+ }
5009
5071
  function parsePortOption(raw, fallback) {
5010
5072
  const parsed = Number.parseInt(raw, 10);
5011
5073
  if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharbor",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Instant-messaging bridge for Codex CLI sessions",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli.js",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ ".env.example",
20
21
  "README.md",
21
22
  "LICENSE"
22
23
  ],