hanoman 0.1.47 → 0.1.50

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
@@ -84,13 +84,55 @@ Sesudah `hanoman update`, instance yang berjalan perlu di-restart (mis. `systemc
84
84
 
85
85
  | Env | Default | Untuk apa |
86
86
  |---|---|---|
87
- | `HANOMAN_HOME` | `~/.hanoman` | DB SQLite, key SSH, transkrip sesi |
87
+ | `HANOMAN_HOME` | `~/.hanoman` | akar seluruh state hanoman lihat [Isi `$HANOMAN_HOME`](#isi-hanoman_home) |
88
88
  | `HANOMAN_DATABASE_URL` | — | berkas DB khusus hanoman; hanya URL `file:` (nilai lain **melempar**) |
89
89
  | `DATABASE_URL` | `file:$HANOMAN_HOME/hanoman.db` | dipakai bila ber-`file:`; nilai lain **diabaikan** dengan peringatan |
90
+ | `HANOMAN_TRANSCRIPT_DIR` | `$HANOMAN_HOME/transcripts` | transkrip sesi yang sudah ditutup |
91
+ | `HANOMAN_UPLOAD_DIR` | `$HANOMAN_HOME/uploads` | lampiran tiket & lampiran gambar sesi terminal |
92
+ | `HANOMAN_SSH_KEY_DIR` | `$HANOMAN_HOME` | keypair identitas hanoman untuk VPS |
90
93
  | `PORT` / `HOST` | `8787` / `127.0.0.1` | alamat bind |
91
94
  | `HANOMAN_CLAUDE_BIN` / `HANOMAN_CODEX_BIN` | `claude` / `codex` | biner agen |
92
95
  | `HANOMAN_TMUX_SOCKET` | `hanoman` | socket tmux terpisah dari milikmu |
93
96
 
97
+ <a id="isi-hanoman_home"></a>
98
+
99
+ ## Isi `$HANOMAN_HOME` — satu batas backup/restore
100
+
101
+ Tanpa override, **seluruh** state hanoman hidup di bawah satu direktori. Itulah batas yang harus
102
+ disalin: memindahkan DB saja meninggalkan metadata yang menunjuk byte yang tak ada (SPEC-846).
103
+
104
+ | Path | Isi | Hilang berarti |
105
+ |---|---|---|
106
+ | `hanoman.db` (+ `-wal`/`-shm`) | seluruh state aplikasi | semuanya |
107
+ | `secret.key` | kunci AES untuk RuntimeConfig & webhook secret | credential terenkripsi tak bisa dibuka |
108
+ | `id_ed25519` + `.pub` | identitas SSH hanoman ke VPS | akses VPS mati; `authorized_keys` tak lagi cocok |
109
+ | `transcripts/` | transkrip sesi yang ditutup | `hasTranscript` benar, isinya tak terbaca |
110
+ | `uploads/` | lampiran tiket, byte source-map lama | lampiran 404 walau metadata utuh |
111
+ | `uploads/terminal/<sessionId>/` | lampiran gambar sesi terminal | path di prompt agen menunjuk berkas hilang |
112
+ | `setup.token` | token bootstrap admin pertama (one-time, 15 menit) | hanya relevan sebelum admin pertama |
113
+ | `agent-token` | agent token yang dibaca `hanoman mcp` | klien MCP lokal kehilangan autentikasi |
114
+
115
+ ```bash
116
+ # Backup — SQLite disalin konsisten, sisanya berkas biasa.
117
+ sqlite3 "$HANOMAN_HOME/hanoman.db" ".backup '/backup/hanoman.db'"
118
+ tar -C "$HANOMAN_HOME" -czf /backup/hanoman-files.tgz \
119
+ secret.key id_ed25519 id_ed25519.pub transcripts uploads
120
+
121
+ # Restore di host lain — umask dulu, isinya memuat kunci privat.
122
+ umask 077
123
+ install -d -m 0700 "$HANOMAN_HOME"
124
+ cp /backup/hanoman.db "$HANOMAN_HOME/hanoman.db"
125
+ tar -C "$HANOMAN_HOME" -xzf /backup/hanoman-files.tgz
126
+ hanoman doctor # memverifikasi tiap path data efektif + izin tulisnya
127
+ ```
128
+
129
+ `setup.token` dan `agent-token` sengaja **tidak** ikut: yang pertama one-time dan lahir ulang
130
+ sendiri bila DB belum punya user, yang kedua sebaiknya diterbitkan ulang di host baru.
131
+
132
+ Mengeset `HANOMAN_TRANSCRIPT_DIR`, `HANOMAN_UPLOAD_DIR`, atau `HANOMAN_SSH_KEY_DIR` memindahkan
133
+ bagian itu ke luar batas ini — sah, tapi backup-nya menjadi tanggung jawabmu. `hanoman doctor`
134
+ mencetak path efektifnya supaya tak perlu ditebak.
135
+
94
136
  ## Bind & TLS
95
137
 
96
138
  Default `127.0.0.1:8787`. hanoman punya auth, tapi cookie sesinya `Secure` — set
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.47",
3
- "sha": "e703b80",
4
- "builtAt": "2026-08-19T07:28:27.280Z"
2
+ "version": "0.1.50",
3
+ "sha": "30fe53e3",
4
+ "builtAt": "2026-08-20T04:25:22.099Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -5064,6 +5064,10 @@ var init_dto = __esm({
5064
5064
  cwd: external_exports.string(),
5065
5065
  startedAt: external_exports.string(),
5066
5066
  endedAt: external_exports.string().nullable(),
5067
+ // SPEC-844 · ADR-0125 · `string` longgar, bukan enum: baris lama null dan nilai asing dari
5068
+ // instance lebih baru tak boleh melempar di boundary — `sessionOutcome()` yang menafsirkannya.
5069
+ endedReason: external_exports.string().nullable(),
5070
+ reconciledAt: external_exports.string().nullable(),
5067
5071
  exitCode: external_exports.number().nullable(),
5068
5072
  transcriptBytes: external_exports.number().nullable()
5069
5073
  });
@@ -5918,6 +5922,13 @@ var init_session_kind = __esm({
5918
5922
  }
5919
5923
  });
5920
5924
 
5925
+ // ../shared/src/session-end.ts
5926
+ var init_session_end = __esm({
5927
+ "../shared/src/session-end.ts"() {
5928
+ "use strict";
5929
+ }
5930
+ });
5931
+
5921
5932
  // ../shared/src/config.ts
5922
5933
  var zHanomanConfig;
5923
5934
  var init_config = __esm({
@@ -6291,6 +6302,8 @@ var init_webhook = __esm({
6291
6302
  "branch",
6292
6303
  "startedAt",
6293
6304
  "endedAt",
6305
+ "endedReason",
6306
+ "reconciledAt",
6294
6307
  "exitCode"
6295
6308
  ],
6296
6309
  projectIdField: "projectId",
@@ -6299,9 +6312,9 @@ var init_webhook = __esm({
6299
6312
  },
6300
6313
  derived: [{
6301
6314
  type: "session.ended",
6302
- label: "Sesi selesai atau gagal",
6315
+ label: "Sesi selesai, gagal, atau terputus",
6303
6316
  changed: ["endedAt"],
6304
- when: "Sesi ditutup dan endedAt terisi. exitCode bukan 0 berarti pane mati gagal; null berarti tak terbaca, misalnya tmux mati di luar hanoman."
6317
+ when: 'Sesi berakhir dan endedAt terisi. endedReason memberi tahu CARANYA: "closed" = hanoman menutupnya, jadi exitCode berlaku (null = agen masih hidup saat ditutup, 0 = keluar bersih, selain itu gagal); "reconciled" = panenya sudah lenyap saat hanoman menyala lagi (reboot, kill-server, host mati) sehingga hasilnya TAK DIKETAHUI, exitCode selalu null, dan reconciledAt menyebut kapan hanoman menemukannya \u2014 endedAt di kasus itu batas BAWAH, bukan waktu berakhir sebenarnya. null = baris lahir sebelum SPEC-844.'
6305
6318
  }],
6306
6319
  sample: {
6307
6320
  id: "6f0c1c1e-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
@@ -6317,6 +6330,8 @@ var init_webhook = __esm({
6317
6330
  branch: "hanoman/spec-481",
6318
6331
  startedAt: "2026-08-01T02:12:31.000Z",
6319
6332
  endedAt: "2026-08-01T09:44:02.000Z",
6333
+ endedReason: "closed",
6334
+ reconciledAt: null,
6320
6335
  exitCode: 0
6321
6336
  }
6322
6337
  },
@@ -7251,6 +7266,7 @@ var init_src = __esm({
7251
7266
  init_portal();
7252
7267
  init_prd_status();
7253
7268
  init_session_kind();
7269
+ init_session_end();
7254
7270
  init_config();
7255
7271
  init_config_registry();
7256
7272
  init_semver();
@@ -7443,6 +7459,16 @@ function resolveHome(env = process.env, home = homedir()) {
7443
7459
  const v = env.HANOMAN_HOME?.trim();
7444
7460
  return v ? v : join(home, ".hanoman");
7445
7461
  }
7462
+ function resolveDataDirs(env = process.env, home = homedir()) {
7463
+ const root = resolveHome(env, home);
7464
+ const dir = (key, name) => env[key]?.trim() || join(root, name);
7465
+ return {
7466
+ home: root,
7467
+ transcripts: dir("HANOMAN_TRANSCRIPT_DIR", "transcripts"),
7468
+ uploads: dir("HANOMAN_UPLOAD_DIR", "uploads"),
7469
+ sshKeys: env.HANOMAN_SSH_KEY_DIR?.trim() || root
7470
+ };
7471
+ }
7446
7472
  function schemeOf(raw) {
7447
7473
  return raw.split("://")[0] ?? "?";
7448
7474
  }
@@ -7701,10 +7727,10 @@ function ensureSpawnHelpersExecutable(paths2, ops) {
7701
7727
  }
7702
7728
  return fixed;
7703
7729
  }
7704
- function repairSpawnHelper(resolve4, notify) {
7730
+ function repairSpawnHelper(resolve5, notify) {
7705
7731
  let ptyDir;
7706
7732
  try {
7707
- ptyDir = dirname3(resolve4("node-pty/package.json"));
7733
+ ptyDir = dirname3(resolve5("node-pty/package.json"));
7708
7734
  } catch {
7709
7735
  return [];
7710
7736
  }
@@ -8106,7 +8132,7 @@ __export(doctor_exports, {
8106
8132
  });
8107
8133
  import { execFileSync as execFileSync3 } from "node:child_process";
8108
8134
  import { accessSync, constants, existsSync as existsSync4 } from "node:fs";
8109
- import { dirname as dirname5 } from "node:path";
8135
+ import { dirname as dirname5, resolve as resolve4 } from "node:path";
8110
8136
  function doctorReport(p) {
8111
8137
  const major = Number(/^v?(\d+)/.exec(p.node)?.[1] ?? 0);
8112
8138
  const rows = [
@@ -8121,7 +8147,13 @@ function doctorReport(p) {
8121
8147
  text: p.gh ? `gh ${p.gh}` : "gh \u2014 tak ada (tarik issue akan lewat HTTP + GITHUB_TOKEN)",
8122
8148
  fatal: false
8123
8149
  },
8124
- { mark: p.homeWritable ? "\u2713" : "\u2717", text: `data dir ${p.homeWritable ? "bisa ditulis" : "TAK bisa ditulis"}`, fatal: !p.homeWritable },
8150
+ // Direktori turunan lahir saat dipakai, jadi izin tulis yang belum ada di situ adalah
8151
+ // peringatan — bukan alasan menyatakan hanoman tak bisa menjalankan sesi. Home tetap fatal.
8152
+ ...p.dirs.map((d) => ({
8153
+ mark: d.writable ? "\u2713" : d.fatal ? "\u2717" : "!",
8154
+ text: `${d.label} ${d.path}${d.writable ? "" : " \u2014 TAK bisa ditulis"}`,
8155
+ fatal: d.fatal && !d.writable
8156
+ })),
8125
8157
  { mark: p.web ? "\u2713" : "!", text: p.web ? "aset dashboard ada" : "aset dashboard tak ada \u2014 API jalan, dashboard tidak", fatal: false },
8126
8158
  { mark: "\xB7", text: `db ${p.db}`, fatal: false },
8127
8159
  {
@@ -8144,6 +8176,21 @@ function doctorReport(p) {
8144
8176
  }
8145
8177
  return { lines: rows.map((r) => ` ${r.mark} ${r.text}`), ok: !rows.some((r) => r.fatal) };
8146
8178
  }
8179
+ function writable(path) {
8180
+ for (let p = resolve4(path); ; ) {
8181
+ if (existsSync4(p)) {
8182
+ try {
8183
+ accessSync(p, constants.W_OK);
8184
+ return true;
8185
+ } catch {
8186
+ return false;
8187
+ }
8188
+ }
8189
+ const up = dirname5(p);
8190
+ if (up === p) return false;
8191
+ p = up;
8192
+ }
8193
+ }
8147
8194
  function version(bin, args) {
8148
8195
  try {
8149
8196
  return execFileSync3(bin, args, { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n")[0] ?? null;
@@ -8162,13 +8209,13 @@ async function doctor(_argv, ctx) {
8162
8209
  `);
8163
8210
  return 1;
8164
8211
  }
8165
- const home = resolveHome(ctx.env);
8166
- let homeWritable = false;
8167
- try {
8168
- accessSync(existsSync4(home) ? home : dirname5(home), constants.W_OK);
8169
- homeWritable = true;
8170
- } catch {
8171
- }
8212
+ const d = resolveDataDirs(ctx.env);
8213
+ const dirs = [
8214
+ { label: "data dir", path: d.home, writable: writable(d.home), fatal: true },
8215
+ { label: "transkrip", path: d.transcripts, writable: writable(d.transcripts), fatal: false },
8216
+ { label: "upload", path: d.uploads, writable: writable(d.uploads), fatal: false },
8217
+ { label: "key SSH", path: d.sshKeys, writable: writable(d.sshKeys), fatal: false }
8218
+ ];
8172
8219
  const claude = version(ctx.env.HANOMAN_CLAUDE_BIN ?? "claude", ["--version"]);
8173
8220
  const codex = version(ctx.env.HANOMAN_CODEX_BIN ?? "codex", ["--version"]);
8174
8221
  const method = METHODS[DEFAULT_METHOD];
@@ -8198,7 +8245,7 @@ async function doctor(_argv, ctx) {
8198
8245
  claude,
8199
8246
  codex,
8200
8247
  gh: version(ctx.env.HANOMAN_GH_BIN ?? "gh", ["--version"]),
8201
- homeWritable,
8248
+ dirs,
8202
8249
  web: layout.web !== null,
8203
8250
  db,
8204
8251
  methods,
@@ -17588,14 +17635,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
17588
17635
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
17589
17636
  }
17590
17637
  function sleep(ms, signal) {
17591
- return new Promise((resolve4, reject) => {
17638
+ return new Promise((resolve5, reject) => {
17592
17639
  if (signal?.aborted) {
17593
17640
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
17594
17641
  return;
17595
17642
  }
17596
17643
  const timer = setTimeout(() => {
17597
17644
  signal?.removeEventListener("abort", onAbort);
17598
- resolve4();
17645
+ resolve5();
17599
17646
  }, ms);
17600
17647
  const onAbort = () => {
17601
17648
  clearTimeout(timer);
@@ -19406,7 +19453,7 @@ var init_src_CX2iR2pK = __esm({
19406
19453
  const flowStartedAt = Date.now();
19407
19454
  let onAbort;
19408
19455
  let cleanupMessageId;
19409
- return new Promise((resolve4, reject) => {
19456
+ return new Promise((resolve5, reject) => {
19410
19457
  const earlyReject = (error2) => {
19411
19458
  reject(error2);
19412
19459
  };
@@ -19474,7 +19521,7 @@ var init_src_CX2iR2pK = __esm({
19474
19521
  }
19475
19522
  if (decoded.kind === "invalid") return reject(decoded.error);
19476
19523
  if (decoded.kind === "input_required") {
19477
- if (options?.allowInputRequired === true) return resolve4(manualInputRequiredValue(decoded));
19524
+ if (options?.allowInputRequired === true) return resolve5(manualInputRequiredValue(decoded));
19478
19525
  const flow = {
19479
19526
  codec,
19480
19527
  request,
@@ -19486,11 +19533,11 @@ var init_src_CX2iR2pK = __esm({
19486
19533
  params
19487
19534
  }, resultSchema, legOptions)
19488
19535
  };
19489
- return resolve4(this._resolveNonCompleteResult(decoded, flow));
19536
+ return resolve5(this._resolveNonCompleteResult(decoded, flow));
19490
19537
  }
19491
19538
  const result = decoded.result;
19492
19539
  validateStandardSchema(resultSchema, result).then((parseResult) => {
19493
- if (parseResult.success) resolve4(parseResult.data);
19540
+ if (parseResult.success) resolve5(parseResult.data);
19494
19541
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
19495
19542
  }, reject);
19496
19543
  });
@@ -22306,7 +22353,7 @@ var init_ajvProvider_CEoC_sr = __esm({
22306
22353
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
22307
22354
  const schOrFunc = root.refs[ref];
22308
22355
  if (schOrFunc) return schOrFunc;
22309
- let _sch = resolve4.call(this, root, ref);
22356
+ let _sch = resolve5.call(this, root, ref);
22310
22357
  if (_sch === void 0) {
22311
22358
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
22312
22359
  const { schemaId } = this.opts;
@@ -22332,7 +22379,7 @@ var init_ajvProvider_CEoC_sr = __esm({
22332
22379
  function sameSchemaEnv(s1, s2) {
22333
22380
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
22334
22381
  }
22335
- function resolve4(root, ref) {
22382
+ function resolve5(root, ref) {
22336
22383
  let sch;
22337
22384
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
22338
22385
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -22782,7 +22829,7 @@ var init_ajvProvider_CEoC_sr = __esm({
22782
22829
  else if (typeof uri === "object") uri = parse4(serialize(uri, options), options);
22783
22830
  return uri;
22784
22831
  }
22785
- function resolve4(baseURI, relativeURI, options) {
22832
+ function resolve5(baseURI, relativeURI, options) {
22786
22833
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
22787
22834
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
22788
22835
  schemelessOptions.skipEscape = true;
@@ -22956,7 +23003,7 @@ var init_ajvProvider_CEoC_sr = __esm({
22956
23003
  const fastUri = {
22957
23004
  SCHEMES,
22958
23005
  normalize,
22959
- resolve: resolve4,
23006
+ resolve: resolve5,
22960
23007
  resolveComponent,
22961
23008
  equal,
22962
23009
  serialize,
@@ -28648,7 +28695,7 @@ var init_stdio = __esm({
28648
28695
  }
28649
28696
  send(message) {
28650
28697
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
28651
- return new Promise((resolve4, reject) => {
28698
+ return new Promise((resolve5, reject) => {
28652
28699
  const json = serializeMessage(message);
28653
28700
  let settled = false;
28654
28701
  const onError = (error2) => {
@@ -28663,14 +28710,14 @@ var init_stdio = __esm({
28663
28710
  settled = true;
28664
28711
  this._stdout.off("error", onError);
28665
28712
  this._stdout.off("drain", onDrain);
28666
- resolve4();
28713
+ resolve5();
28667
28714
  };
28668
28715
  this._stdout.once("error", onError);
28669
28716
  if (this._stdout.write(json)) {
28670
28717
  if (settled) return;
28671
28718
  settled = true;
28672
28719
  this._stdout.off("error", onError);
28673
- resolve4();
28720
+ resolve5();
28674
28721
  } else if (!settled) this._stdout.once("drain", onDrain);
28675
28722
  });
28676
28723
  }
@@ -28724,14 +28771,14 @@ var init_stdio = __esm({
28724
28771
  */
28725
28772
  async whenRequestsAnswered(timeoutMs) {
28726
28773
  if (this._closed || this._pendingRequests.size === 0) return true;
28727
- return await new Promise((resolve4) => {
28774
+ return await new Promise((resolve5) => {
28728
28775
  const waiter = () => {
28729
28776
  clearTimeout(timer);
28730
- resolve4(true);
28777
+ resolve5(true);
28731
28778
  };
28732
28779
  const timer = setTimeout(() => {
28733
28780
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
28734
- resolve4(false);
28781
+ resolve5(false);
28735
28782
  }, timeoutMs);
28736
28783
  this._drainWaiters.push(waiter);
28737
28784
  });
@@ -29008,10 +29055,10 @@ var init_server = __esm({
29008
29055
  // src/commands/mcp.ts
29009
29056
  var mcp_exports = {};
29010
29057
  __export(mcp_exports, {
29058
+ agentTokenPath: () => agentTokenPath,
29011
29059
  default: () => mcp
29012
29060
  });
29013
29061
  import { readFileSync as readFileSync2 } from "node:fs";
29014
- import { homedir as homedir3 } from "node:os";
29015
29062
  import { join as join6 } from "node:path";
29016
29063
  async function mcp(argv, ctx) {
29017
29064
  const cfg = resolveMcpConfig(argv, ctx.env, () => tokenFile(ctx.env));
@@ -29026,26 +29073,27 @@ async function mcp(argv, ctx) {
29026
29073
  await handle.close();
29027
29074
  return 0;
29028
29075
  }
29029
- await new Promise((resolve4) => {
29030
- process.stdin.once("close", resolve4);
29031
- process.stdin.once("end", resolve4);
29076
+ await new Promise((resolve5) => {
29077
+ process.stdin.once("close", resolve5);
29078
+ process.stdin.once("end", resolve5);
29032
29079
  });
29033
29080
  return 0;
29034
29081
  }
29035
- var tokenFile;
29082
+ var agentTokenPath, tokenFile;
29036
29083
  var init_mcp2 = __esm({
29037
29084
  "src/commands/mcp.ts"() {
29038
29085
  "use strict";
29086
+ init_src2();
29039
29087
  init_stdio();
29040
29088
  init_router();
29041
29089
  init_config2();
29042
29090
  init_client();
29043
29091
  init_server();
29044
29092
  init_redact();
29093
+ agentTokenPath = (env) => join6(resolveHome(env), "agent-token");
29045
29094
  tokenFile = (env) => {
29046
- const home = env.HANOMAN_HOME ?? join6(homedir3(), ".hanoman");
29047
29095
  try {
29048
- return readFileSync2(join6(home, "agent-token"), "utf8");
29096
+ return readFileSync2(agentTokenPath(env), "utf8");
29049
29097
  } catch {
29050
29098
  return null;
29051
29099
  }