hanoman 0.1.37 → 0.1.39

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.37",
3
- "sha": "c2be030",
4
- "builtAt": "2026-08-14T16:26:48.760Z"
2
+ "version": "0.1.39",
3
+ "sha": "b5ac8a8",
4
+ "builtAt": "2026-08-15T01:27:37.357Z"
5
5
  }
package/dist/cli.js CHANGED
@@ -5650,6 +5650,7 @@ var init_api = __esm({
5650
5650
  // SPEC-233 · drop commit isolasi
5651
5651
  fsBrowse: (path) => `${API}/fs/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`,
5652
5652
  terminalSessions: `${API}/terminal/sessions`,
5653
+ terminalWorkspace: `${API}/terminal/workspace`,
5653
5654
  // SPEC-742 · ADR-0116 · pembersihan worktree yang masih tertunda sesudah sesi ditutup. Di bawah
5654
5655
  // prefix /terminal supaya ikut capability `sessions` yang sudah ada (cermin sessionHistory).
5655
5656
  terminalCleanups: `${API}/terminal/cleanups`,
@@ -7121,6 +7122,70 @@ var init_changelog = __esm({
7121
7122
  }
7122
7123
  });
7123
7124
 
7125
+ // ../shared/src/terminal-workspace.ts
7126
+ var zSessionId, zTerminalGroup, zTerminalWorkspaceV1, zTerminalWorkspaceWrite, zTerminalWorkspaceSnapshot;
7127
+ var init_terminal_workspace = __esm({
7128
+ "../shared/src/terminal-workspace.ts"() {
7129
+ "use strict";
7130
+ init_zod();
7131
+ zSessionId = external_exports.string().trim().min(1).max(256);
7132
+ zTerminalGroup = external_exports.object({
7133
+ id: external_exports.string().trim().min(1).max(128),
7134
+ name: external_exports.string().trim().min(1).max(80),
7135
+ layout: external_exports.object({
7136
+ rows: external_exports.number().int().min(1).max(12),
7137
+ cols: external_exports.number().int().min(1).max(12),
7138
+ cells: external_exports.array(zSessionId.nullable()).max(144)
7139
+ }).strict().superRefine((layout, ctx) => {
7140
+ if (layout.cells.length !== layout.rows * layout.cols) {
7141
+ ctx.addIssue({
7142
+ code: external_exports.ZodIssueCode.custom,
7143
+ path: ["cells"],
7144
+ message: "cells must match rows \xD7 cols"
7145
+ });
7146
+ }
7147
+ })
7148
+ }).strict();
7149
+ zTerminalWorkspaceV1 = external_exports.object({
7150
+ version: external_exports.literal(1),
7151
+ groups: external_exports.array(zTerminalGroup).min(1).max(24)
7152
+ }).strict().superRefine((workspace, ctx) => {
7153
+ const groupIds = /* @__PURE__ */ new Set();
7154
+ const sessionIds = /* @__PURE__ */ new Set();
7155
+ workspace.groups.forEach((group, groupIndex) => {
7156
+ if (groupIds.has(group.id)) {
7157
+ ctx.addIssue({
7158
+ code: external_exports.ZodIssueCode.custom,
7159
+ path: ["groups", groupIndex, "id"],
7160
+ message: "group id must be unique"
7161
+ });
7162
+ }
7163
+ groupIds.add(group.id);
7164
+ group.layout.cells.forEach((sessionId, cellIndex) => {
7165
+ if (sessionId === null) return;
7166
+ if (sessionIds.has(sessionId)) {
7167
+ ctx.addIssue({
7168
+ code: external_exports.ZodIssueCode.custom,
7169
+ path: ["groups", groupIndex, "layout", "cells", cellIndex],
7170
+ message: "sessionId must be unique across the workspace"
7171
+ });
7172
+ }
7173
+ sessionIds.add(sessionId);
7174
+ });
7175
+ });
7176
+ });
7177
+ zTerminalWorkspaceWrite = external_exports.object({
7178
+ baseRevision: external_exports.number().int().min(0),
7179
+ workspace: zTerminalWorkspaceV1
7180
+ }).strict();
7181
+ zTerminalWorkspaceSnapshot = external_exports.object({
7182
+ workspace: zTerminalWorkspaceV1.nullable(),
7183
+ revision: external_exports.number().int().min(0),
7184
+ updatedAt: external_exports.string().datetime().nullable()
7185
+ }).strict();
7186
+ }
7187
+ });
7188
+
7124
7189
  // ../shared/src/index.ts
7125
7190
  var init_src = __esm({
7126
7191
  "../shared/src/index.ts"() {
@@ -7152,6 +7217,7 @@ var init_src = __esm({
7152
7217
  init_auto_merge();
7153
7218
  init_changelog();
7154
7219
  init_cron_expr();
7220
+ init_terminal_workspace();
7155
7221
  }
7156
7222
  });
7157
7223
 
package/dist/server.js CHANGED
@@ -6092,6 +6092,7 @@ var init_api = __esm({
6092
6092
  // SPEC-233 · drop commit isolasi
6093
6093
  fsBrowse: (path2) => `${API}/fs/browse${path2 ? `?path=${encodeURIComponent(path2)}` : ""}`,
6094
6094
  terminalSessions: `${API}/terminal/sessions`,
6095
+ terminalWorkspace: `${API}/terminal/workspace`,
6095
6096
  // SPEC-742 · ADR-0116 · pembersihan worktree yang masih tertunda sesudah sesi ditutup. Di bawah
6096
6097
  // prefix /terminal supaya ikut capability `sessions` yang sudah ada (cermin sessionHistory).
6097
6098
  terminalCleanups: `${API}/terminal/cleanups`,
@@ -7701,6 +7702,70 @@ var init_changelog = __esm({
7701
7702
  }
7702
7703
  });
7703
7704
 
7705
+ // ../shared/src/terminal-workspace.ts
7706
+ var zSessionId, zTerminalGroup, zTerminalWorkspaceV1, zTerminalWorkspaceWrite, zTerminalWorkspaceSnapshot;
7707
+ var init_terminal_workspace = __esm({
7708
+ "../shared/src/terminal-workspace.ts"() {
7709
+ "use strict";
7710
+ init_zod();
7711
+ zSessionId = external_exports.string().trim().min(1).max(256);
7712
+ zTerminalGroup = external_exports.object({
7713
+ id: external_exports.string().trim().min(1).max(128),
7714
+ name: external_exports.string().trim().min(1).max(80),
7715
+ layout: external_exports.object({
7716
+ rows: external_exports.number().int().min(1).max(12),
7717
+ cols: external_exports.number().int().min(1).max(12),
7718
+ cells: external_exports.array(zSessionId.nullable()).max(144)
7719
+ }).strict().superRefine((layout, ctx) => {
7720
+ if (layout.cells.length !== layout.rows * layout.cols) {
7721
+ ctx.addIssue({
7722
+ code: external_exports.ZodIssueCode.custom,
7723
+ path: ["cells"],
7724
+ message: "cells must match rows \xD7 cols"
7725
+ });
7726
+ }
7727
+ })
7728
+ }).strict();
7729
+ zTerminalWorkspaceV1 = external_exports.object({
7730
+ version: external_exports.literal(1),
7731
+ groups: external_exports.array(zTerminalGroup).min(1).max(24)
7732
+ }).strict().superRefine((workspace, ctx) => {
7733
+ const groupIds = /* @__PURE__ */ new Set();
7734
+ const sessionIds = /* @__PURE__ */ new Set();
7735
+ workspace.groups.forEach((group2, groupIndex) => {
7736
+ if (groupIds.has(group2.id)) {
7737
+ ctx.addIssue({
7738
+ code: external_exports.ZodIssueCode.custom,
7739
+ path: ["groups", groupIndex, "id"],
7740
+ message: "group id must be unique"
7741
+ });
7742
+ }
7743
+ groupIds.add(group2.id);
7744
+ group2.layout.cells.forEach((sessionId2, cellIndex) => {
7745
+ if (sessionId2 === null) return;
7746
+ if (sessionIds.has(sessionId2)) {
7747
+ ctx.addIssue({
7748
+ code: external_exports.ZodIssueCode.custom,
7749
+ path: ["groups", groupIndex, "layout", "cells", cellIndex],
7750
+ message: "sessionId must be unique across the workspace"
7751
+ });
7752
+ }
7753
+ sessionIds.add(sessionId2);
7754
+ });
7755
+ });
7756
+ });
7757
+ zTerminalWorkspaceWrite = external_exports.object({
7758
+ baseRevision: external_exports.number().int().min(0),
7759
+ workspace: zTerminalWorkspaceV1
7760
+ }).strict();
7761
+ zTerminalWorkspaceSnapshot = external_exports.object({
7762
+ workspace: zTerminalWorkspaceV1.nullable(),
7763
+ revision: external_exports.number().int().min(0),
7764
+ updatedAt: external_exports.string().datetime().nullable()
7765
+ }).strict();
7766
+ }
7767
+ });
7768
+
7704
7769
  // ../shared/src/index.ts
7705
7770
  var init_src = __esm({
7706
7771
  "../shared/src/index.ts"() {
@@ -7732,6 +7797,7 @@ var init_src = __esm({
7732
7797
  init_auto_merge();
7733
7798
  init_changelog();
7734
7799
  init_cron_expr();
7800
+ init_terminal_workspace();
7735
7801
  }
7736
7802
  });
7737
7803
 
@@ -30237,6 +30303,7 @@ async function recordSessionResult(input) {
30237
30303
  // src/routes/terminal.ts
30238
30304
  init_stage_machine();
30239
30305
  init_pty();
30306
+ var TERMINAL_WS_MESSAGES_PER_MINUTE = 6e3;
30240
30307
  async function advanceStage(specId, repoDir, sessionId2, flow, worktree) {
30241
30308
  const next = stageForRun(readPhases(phaseFilePath(repoDir, sessionId2), flow), worktree, specId);
30242
30309
  if (!next) return;
@@ -30580,7 +30647,7 @@ async function terminal_default(app2, opts) {
30580
30647
  socket2.close(1008, "connection limit");
30581
30648
  return;
30582
30649
  }
30583
- const guard = new WsMessageGuard();
30650
+ const guard = new WsMessageGuard({ perWindow: TERMINAL_WS_MESSAGES_PER_MINUTE });
30584
30651
  const client = { send: (m) => socket2.send(m), close: () => socket2.close() };
30585
30652
  attach(id, client);
30586
30653
  socket2.on("message", async (raw2) => {
@@ -30617,6 +30684,93 @@ async function terminal_default(app2, opts) {
30617
30684
  });
30618
30685
  }
30619
30686
 
30687
+ // src/routes/terminal-workspace.ts
30688
+ init_src();
30689
+
30690
+ // src/services/terminal-workspace.ts
30691
+ init_src();
30692
+ init_db();
30693
+ import "@prisma/client";
30694
+ var InvalidStoredTerminalWorkspaceError = class extends Error {
30695
+ constructor() {
30696
+ super("stored terminal workspace is invalid");
30697
+ }
30698
+ };
30699
+ async function readTerminalWorkspace(userId) {
30700
+ const user = await prisma.user.findUniqueOrThrow({
30701
+ where: { id: userId },
30702
+ select: {
30703
+ terminalWorkspace: true,
30704
+ terminalWorkspaceRevision: true,
30705
+ terminalWorkspaceUpdatedAt: true
30706
+ }
30707
+ });
30708
+ if (user.terminalWorkspace === null) {
30709
+ return {
30710
+ workspace: null,
30711
+ revision: user.terminalWorkspaceRevision,
30712
+ updatedAt: user.terminalWorkspaceUpdatedAt?.toISOString() ?? null
30713
+ };
30714
+ }
30715
+ const parsed = zTerminalWorkspaceV1.safeParse(user.terminalWorkspace);
30716
+ if (!parsed.success) throw new InvalidStoredTerminalWorkspaceError();
30717
+ return {
30718
+ workspace: parsed.data,
30719
+ revision: user.terminalWorkspaceRevision,
30720
+ updatedAt: user.terminalWorkspaceUpdatedAt?.toISOString() ?? null
30721
+ };
30722
+ }
30723
+ async function writeTerminalWorkspace(userId, input) {
30724
+ await readTerminalWorkspace(userId);
30725
+ const updatedAt = /* @__PURE__ */ new Date();
30726
+ const changed = await prisma.user.updateMany({
30727
+ where: { id: userId, terminalWorkspaceRevision: input.baseRevision },
30728
+ data: {
30729
+ terminalWorkspace: input.workspace,
30730
+ terminalWorkspaceRevision: { increment: 1 },
30731
+ terminalWorkspaceUpdatedAt: updatedAt
30732
+ }
30733
+ });
30734
+ if (changed.count === 1) {
30735
+ return {
30736
+ ok: true,
30737
+ current: {
30738
+ workspace: input.workspace,
30739
+ revision: input.baseRevision + 1,
30740
+ updatedAt: updatedAt.toISOString()
30741
+ }
30742
+ };
30743
+ }
30744
+ return { ok: false, current: await readTerminalWorkspace(userId) };
30745
+ }
30746
+
30747
+ // src/routes/terminal-workspace.ts
30748
+ var storedInvalid = (reply) => reply.code(422).send({ error: "stored terminal workspace is invalid" });
30749
+ async function terminalWorkspaceRoutes(app2) {
30750
+ app2.get("/terminal/workspace", async (request, reply) => {
30751
+ try {
30752
+ return await readTerminalWorkspace(request.user.id);
30753
+ } catch (error) {
30754
+ if (error instanceof InvalidStoredTerminalWorkspaceError) return storedInvalid(reply);
30755
+ throw error;
30756
+ }
30757
+ });
30758
+ app2.put("/terminal/workspace", async (request, reply) => {
30759
+ const parsed = zTerminalWorkspaceWrite.safeParse(request.body);
30760
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
30761
+ try {
30762
+ const result = await writeTerminalWorkspace(request.user.id, parsed.data);
30763
+ if (!result.ok) {
30764
+ return reply.code(409).send({ code: "revision-conflict", current: result.current });
30765
+ }
30766
+ return result.current;
30767
+ } catch (error) {
30768
+ if (error instanceof InvalidStoredTerminalWorkspaceError) return storedInvalid(reply);
30769
+ throw error;
30770
+ }
30771
+ });
30772
+ }
30773
+
30620
30774
  // src/routes/vps.ts
30621
30775
  init_db();
30622
30776
  init_src();
@@ -44585,6 +44739,7 @@ function capabilityForRoute(method, path2) {
44585
44739
  if (top === "vps") return rw("vps");
44586
44740
  if (top === "prds") return rw("docs");
44587
44741
  if (top === "terminal") {
44742
+ if (seg[1] === "workspace") return "COOKIE_ONLY";
44588
44743
  if (seg[seg.length - 1] === "ws") return "sessions:write";
44589
44744
  return rw("sessions");
44590
44745
  }
@@ -44851,6 +45006,7 @@ function buildApp({ requireAuth = true, agentDocFile, env = process.env } = {})
44851
45006
  await api.register(fs_default);
44852
45007
  const wsOptions = { allowedOrigins: wsControlOrigins(env) };
44853
45008
  await api.register(terminal_default, wsOptions);
45009
+ await api.register(terminalWorkspaceRoutes);
44854
45010
  await api.register(vps_default);
44855
45011
  await api.register(limits);
44856
45012
  await api.register(update);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanoman",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "Orchestrator + dashboard workflow docs-driven untuk sesi Claude Code / Codex",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,4 @@
1
+ -- SPEC-786 · workspace Terminal kanonik per akun admin, LOCAL-only.
2
+ ALTER TABLE "User" ADD COLUMN "terminalWorkspace" JSONB;
3
+ ALTER TABLE "User" ADD COLUMN "terminalWorkspaceRevision" INTEGER NOT NULL DEFAULT 0;
4
+ ALTER TABLE "User" ADD COLUMN "terminalWorkspaceUpdatedAt" DATETIME;
@@ -207,6 +207,11 @@ model User {
207
207
  // cookie yang sudah terbit tetap hidup sampai 7 hari (pencabutan yang tak mencabut).
208
208
  disabled Boolean @default(false)
209
209
  createdAt DateTime @default(now())
210
+ // SPEC-786 · LOCAL-only. Mapping Terminal is state operator per akun pada instance ini;
211
+ // fullscreen, focus, dan pilihan grup aktif tetap di browser dan tidak masuk JSON ini.
212
+ terminalWorkspace Json?
213
+ terminalWorkspaceRevision Int @default(0)
214
+ terminalWorkspaceUpdatedAt DateTime?
210
215
  sessions Session[]
211
216
  deviceTokens DeviceToken[]
212
217
  projectAccess ClientProjectAccess[]