tina4-nodejs 3.13.131 → 3.13.132

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.
@@ -4063,33 +4063,17 @@ var init_engine = __esm({
4063
4063
  i++;
4064
4064
  }
4065
4065
  }
4066
- handleCache(tokens, start2, context) {
4067
- const [content] = stripTag(tokens[start2][1]);
4068
- const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4069
- const cacheKey = m ? m[1] : "default";
4070
- const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4071
- sweepExpiredCache(this.fragmentCache);
4072
- const cached = this.fragmentCache.get(cacheKey);
4073
- if (cached) {
4074
- const [htmlContent, expiresAt] = cached;
4075
- if (Date.now() < expiresAt) {
4076
- let i2 = start2 + 1;
4077
- let depth2 = 0;
4078
- while (i2 < tokens.length) {
4079
- if (tokens[i2][0] === "BLOCK") {
4080
- const [tagContent] = stripTag(tokens[i2][1]);
4081
- const tag = tagContent.split(/\s+/)[0] || "";
4082
- if (tag === "cache") depth2++;
4083
- else if (tag === "endcache") {
4084
- if (depth2 === 0) return [htmlContent, i2 + 1];
4085
- depth2--;
4086
- }
4087
- }
4088
- i2++;
4089
- }
4090
- return [htmlContent, i2];
4091
- }
4092
- }
4066
+ /**
4067
+ * Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
4068
+ * starting from the token after the opening tag (start + 1). Nested same-tag
4069
+ * blocks are kept in the body and balanced by depth; the matching closing tag is
4070
+ * consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
4071
+ *
4072
+ * canNest guards the open-tag count: handleSetBlock passes it so the inline
4073
+ * {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
4074
+ * only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
4075
+ */
4076
+ collectBlockBody(tokens, start2, openTag, closeTag, canNest) {
4093
4077
  const bodyTokens = [];
4094
4078
  let i = start2 + 1;
4095
4079
  let depth = 0;
@@ -4097,10 +4081,10 @@ var init_engine = __esm({
4097
4081
  if (tokens[i][0] === "BLOCK") {
4098
4082
  const [tagContent] = stripTag(tokens[i][1]);
4099
4083
  const tag = tagContent.split(/\s+/)[0] || "";
4100
- if (tag === "cache") {
4084
+ if (tag === openTag && (canNest ? canNest(tagContent) : true)) {
4101
4085
  depth++;
4102
4086
  bodyTokens.push(tokens[i]);
4103
- } else if (tag === "endcache") {
4087
+ } else if (tag === closeTag) {
4104
4088
  if (depth === 0) {
4105
4089
  i++;
4106
4090
  break;
@@ -4115,6 +4099,36 @@ var init_engine = __esm({
4115
4099
  }
4116
4100
  i++;
4117
4101
  }
4102
+ return [bodyTokens, i];
4103
+ }
4104
+ handleCache(tokens, start2, context) {
4105
+ const [content] = stripTag(tokens[start2][1]);
4106
+ const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4107
+ const cacheKey = m ? m[1] : "default";
4108
+ const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4109
+ sweepExpiredCache(this.fragmentCache);
4110
+ const cached = this.fragmentCache.get(cacheKey);
4111
+ if (cached) {
4112
+ const [htmlContent, expiresAt] = cached;
4113
+ if (Date.now() < expiresAt) {
4114
+ let i2 = start2 + 1;
4115
+ let depth = 0;
4116
+ while (i2 < tokens.length) {
4117
+ if (tokens[i2][0] === "BLOCK") {
4118
+ const [tagContent] = stripTag(tokens[i2][1]);
4119
+ const tag = tagContent.split(/\s+/)[0] || "";
4120
+ if (tag === "cache") depth++;
4121
+ else if (tag === "endcache") {
4122
+ if (depth === 0) return [htmlContent, i2 + 1];
4123
+ depth--;
4124
+ }
4125
+ }
4126
+ i2++;
4127
+ }
4128
+ return [htmlContent, i2];
4129
+ }
4130
+ }
4131
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "cache", "endcache");
4118
4132
  const rendered = this.renderTokens([...bodyTokens], context);
4119
4133
  capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
4120
4134
  this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
@@ -4279,62 +4293,20 @@ var init_engine = __esm({
4279
4293
  handleSetBlock(tokens, start2, context) {
4280
4294
  const [content] = stripTag(tokens[start2][1]);
4281
4295
  const name = (content.split(/\s+/)[1] || "").trim();
4282
- const bodyTokens = [];
4283
- let i = start2 + 1;
4284
- let depth = 0;
4285
- while (i < tokens.length) {
4286
- if (tokens[i][0] === "BLOCK") {
4287
- const [tagContent] = stripTag(tokens[i][1]);
4288
- const tag = tagContent.split(/\s+/)[0] || "";
4289
- if (tag === "set" && !tagContent.includes("=")) {
4290
- depth++;
4291
- bodyTokens.push(tokens[i]);
4292
- } else if (tag === "endset") {
4293
- if (depth === 0) {
4294
- i++;
4295
- break;
4296
- }
4297
- depth--;
4298
- bodyTokens.push(tokens[i]);
4299
- } else {
4300
- bodyTokens.push(tokens[i]);
4301
- }
4302
- } else {
4303
- bodyTokens.push(tokens[i]);
4304
- }
4305
- i++;
4306
- }
4296
+ const [bodyTokens, i] = this.collectBlockBody(
4297
+ tokens,
4298
+ start2,
4299
+ "set",
4300
+ "endset",
4301
+ (tagContent) => !tagContent.includes("=")
4302
+ );
4307
4303
  if (name) {
4308
4304
  context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
4309
4305
  }
4310
4306
  return i;
4311
4307
  }
4312
4308
  handleSpaceless(tokens, start2, context) {
4313
- const bodyTokens = [];
4314
- let i = start2 + 1;
4315
- let depth = 0;
4316
- while (i < tokens.length) {
4317
- if (tokens[i][0] === "BLOCK") {
4318
- const [tagContent] = stripTag(tokens[i][1]);
4319
- const tag = tagContent.split(/\s+/)[0] || "";
4320
- if (tag === "spaceless") {
4321
- depth++;
4322
- bodyTokens.push(tokens[i]);
4323
- } else if (tag === "endspaceless") {
4324
- if (depth === 0) {
4325
- i++;
4326
- break;
4327
- }
4328
- depth--;
4329
- bodyTokens.push(tokens[i]);
4330
- } else {
4331
- bodyTokens.push(tokens[i]);
4332
- }
4333
- } else {
4334
- bodyTokens.push(tokens[i]);
4335
- }
4336
- i++;
4337
- }
4309
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "spaceless", "endspaceless");
4338
4310
  let rendered = this.renderTokens([...bodyTokens], context);
4339
4311
  rendered = rendered.replace(/>\s+</g, "><");
4340
4312
  return [rendered, i];
@@ -4343,31 +4315,7 @@ var init_engine = __esm({
4343
4315
  const [content] = stripTag(tokens[start2][1]);
4344
4316
  const modeMatch = content.match(/^autoescape\s+(false|true)/);
4345
4317
  const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
4346
- const bodyTokens = [];
4347
- let i = start2 + 1;
4348
- let depth = 0;
4349
- while (i < tokens.length) {
4350
- if (tokens[i][0] === "BLOCK") {
4351
- const [tagContent] = stripTag(tokens[i][1]);
4352
- const tag = tagContent.split(/\s+/)[0] || "";
4353
- if (tag === "autoescape") {
4354
- depth++;
4355
- bodyTokens.push(tokens[i]);
4356
- } else if (tag === "endautoescape") {
4357
- if (depth === 0) {
4358
- i++;
4359
- break;
4360
- }
4361
- depth--;
4362
- bodyTokens.push(tokens[i]);
4363
- } else {
4364
- bodyTokens.push(tokens[i]);
4365
- }
4366
- } else {
4367
- bodyTokens.push(tokens[i]);
4368
- }
4369
- i++;
4370
- }
4318
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "autoescape", "endautoescape");
4371
4319
  if (!autoEscapeOn) {
4372
4320
  const oldAutoEscape = this._autoEscape;
4373
4321
  this._autoEscape = false;
@@ -5721,6 +5669,99 @@ var init_databaseResult = __esm({
5721
5669
  }
5722
5670
  });
5723
5671
 
5672
+ // ../orm/src/modelCollection.ts
5673
+ var ModelCollection;
5674
+ var init_modelCollection = __esm({
5675
+ "../orm/src/modelCollection.ts"() {
5676
+ "use strict";
5677
+ ModelCollection = class extends Array {
5678
+ /** Total rows matching the query's filter (ignores limit/offset). */
5679
+ _total = 0;
5680
+ /** The SQL limit that produced this page. */
5681
+ _limit = 0;
5682
+ /** The SQL offset that produced this page. */
5683
+ _offset = 0;
5684
+ /**
5685
+ * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
5686
+ * `Array`, never another `ModelCollection`. This is what defuses the Array-
5687
+ * subclass constructor/species trap: the engine never calls this constructor
5688
+ * with a length during those operations.
5689
+ */
5690
+ static get [Symbol.species]() {
5691
+ return Array;
5692
+ }
5693
+ /**
5694
+ * @param items the page of hydrated model instances (or the length, when the
5695
+ * engine constructs a derived array — defended against here).
5696
+ * @param total total rows matching the query's filter (ignores limit/offset).
5697
+ * @param limit the SQL limit that produced this page.
5698
+ * @param offset the SQL offset that produced this page.
5699
+ */
5700
+ constructor(items, total = 0, limit = 0, offset = 0) {
5701
+ super();
5702
+ if (typeof items === "number") {
5703
+ this.length = items;
5704
+ return;
5705
+ }
5706
+ if (items) {
5707
+ for (let i = 0; i < items.length; i++) {
5708
+ this[i] = items[i];
5709
+ }
5710
+ }
5711
+ this._total = Math.trunc(total) || 0;
5712
+ this._limit = Math.trunc(limit) || 0;
5713
+ this._offset = Math.trunc(offset) || 0;
5714
+ }
5715
+ /**
5716
+ * Total rows matching the query's filter, ignoring limit/offset.
5717
+ *
5718
+ * This is the whole point of the collection: the page slice you are iterating
5719
+ * is capped by `limit`, but this number is the full count of matching rows —
5720
+ * what a pager needs to render "page 3 of 13".
5721
+ */
5722
+ getTotalRecords() {
5723
+ return this._total;
5724
+ }
5725
+ /**
5726
+ * The canonical pagination envelope — seven snake_case keys, identical to
5727
+ * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
5728
+ * `toPaginate()` / `to_paginate()`.
5729
+ *
5730
+ * records the page's rows as plain objects (never re-sliced)
5731
+ * total getTotalRecords() — the true total for the filter
5732
+ * page floor(offset / per_page) + 1
5733
+ * per_page the query's limit
5734
+ * total_pages ceil(total / per_page)
5735
+ * limit the SQL limit actually applied
5736
+ * offset the SQL offset actually applied
5737
+ *
5738
+ * `records` are model dicts (via `toDict()`, the same serialisation the
5739
+ * framework applies to a model in a JSON response), so the JSON a client sees
5740
+ * matches `DatabaseResult` exactly — the result is uniform whether the route
5741
+ * returned a raw `db.fetch()` or an ORM query.
5742
+ */
5743
+ toPaginate() {
5744
+ const perPage = this._limit > 0 ? this._limit : this.length;
5745
+ const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
5746
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
5747
+ const records = this.map((model) => {
5748
+ const m = model;
5749
+ return m && typeof m.toDict === "function" ? m.toDict() : model;
5750
+ });
5751
+ return {
5752
+ records,
5753
+ total: this._total,
5754
+ page,
5755
+ per_page: perPage,
5756
+ total_pages: totalPages,
5757
+ limit: perPage,
5758
+ offset: this._offset
5759
+ };
5760
+ }
5761
+ };
5762
+ }
5763
+ });
5764
+
5724
5765
  // ../orm/src/databaseUrl.ts
5725
5766
  import { inspect } from "node:util";
5726
5767
  function stripOneSlash(path8) {
@@ -14069,6 +14110,7 @@ var init_baseModel = __esm({
14069
14110
  "../orm/src/baseModel.ts"() {
14070
14111
  "use strict";
14071
14112
  init_database();
14113
+ init_modelCollection();
14072
14114
  init_validation();
14073
14115
  init_queryBuilder();
14074
14116
  init_sqlite();
@@ -14304,6 +14346,30 @@ var init_baseModel = __esm({
14304
14346
  static getPkColumn() {
14305
14347
  return this.getDbColumn(this.getPkField());
14306
14348
  }
14349
+ /**
14350
+ * Shared read tail for the collection-returning finders (where / all / select
14351
+ * / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
14352
+ * makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
14353
+ * the rows into model instances, and returns a ModelCollection carrying the
14354
+ * total (ADR-0064).
14355
+ *
14356
+ * The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
14357
+ * already runs; the ORM used to discard it. ZERO extra queries beyond that one
14358
+ * probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
14359
+ * limit/offset to the page, and the probe wraps the un-limited SQL so it counts
14360
+ * the WHOLE filtered set, not the page.
14361
+ */
14362
+ static async _collect(sql, params, limit, offset, include) {
14363
+ const db = this.getDb();
14364
+ const rows = await adapterFetch(db, sql, params, limit, offset);
14365
+ const data = Array.isArray(rows) ? rows : rows?.data ?? [];
14366
+ const total = await probeTotal(db, sql, params, limit) ?? data.length;
14367
+ const instances = data.map((row) => new this(row));
14368
+ if (include) {
14369
+ await this._eagerLoad(instances, include);
14370
+ }
14371
+ return new ModelCollection(instances, total, limit, offset);
14372
+ }
14307
14373
  /**
14308
14374
  * Find a record by primary key.
14309
14375
  * @param id Primary key value.
@@ -14356,7 +14422,6 @@ var init_baseModel = __esm({
14356
14422
  );
14357
14423
  }
14358
14424
  const lim = typeof limit === "number" ? limit : 100;
14359
- const db = ModelClass.getDb();
14360
14425
  const conditions = [];
14361
14426
  const params = [];
14362
14427
  if (filter) {
@@ -14376,16 +14441,7 @@ var init_baseModel = __esm({
14376
14441
  if (orderBy) {
14377
14442
  sql += ` ORDER BY ${orderBy}`;
14378
14443
  }
14379
- const rows = await adapterFetch(db, sql, params, lim, offset);
14380
- const data = rows?.data ?? rows;
14381
- const instances = (Array.isArray(data) ? data : []).map((row) => {
14382
- const inst = new this(row);
14383
- return inst;
14384
- });
14385
- if (include) {
14386
- await ModelClass._eagerLoad(instances, include);
14387
- }
14388
- return instances;
14444
+ return ModelClass._collect(sql, params, lim, offset, include);
14389
14445
  }
14390
14446
  /**
14391
14447
  * Load a record into this instance via selectOne.
@@ -14453,7 +14509,6 @@ var init_baseModel = __esm({
14453
14509
  */
14454
14510
  static async all(limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14455
14511
  const ModelClass = this;
14456
- const db = ModelClass.getDb();
14457
14512
  const conditions = [];
14458
14513
  if (ModelClass.softDelete) {
14459
14514
  conditions.push("is_deleted = 0");
@@ -14463,13 +14518,8 @@ var init_baseModel = __esm({
14463
14518
  }
14464
14519
  const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
14465
14520
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14466
- const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14467
- const rows = await adapterQuery(db, sql, []);
14468
- const instances = rows.map((row) => new ModelClass(row));
14469
- if (include) {
14470
- await ModelClass._eagerLoad(instances, include);
14471
- }
14472
- return instances;
14521
+ const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
14522
+ return ModelClass._collect(sql, [], limit, offset, include);
14473
14523
  }
14474
14524
  /**
14475
14525
  * Query records with a WHERE clause.
@@ -14484,7 +14534,6 @@ var init_baseModel = __esm({
14484
14534
  */
14485
14535
  static async where(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14486
14536
  const ModelClass = this;
14487
- const db = ModelClass.getDb();
14488
14537
  const parts = [];
14489
14538
  if (ModelClass.softDelete) {
14490
14539
  parts.push("is_deleted = 0");
@@ -14494,13 +14543,8 @@ var init_baseModel = __esm({
14494
14543
  }
14495
14544
  parts.push(`(${conditions})`);
14496
14545
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14497
- const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14498
- const rows = await adapterQuery(db, sql, params);
14499
- const instances = rows.map((row) => new ModelClass(row));
14500
- if (include) {
14501
- await ModelClass._eagerLoad(instances, include);
14502
- }
14503
- return instances;
14546
+ const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
14547
+ return ModelClass._collect(sql, params, limit, offset, include);
14504
14548
  }
14505
14549
  /**
14506
14550
  * Save this instance (insert or update). Returns this on success (fluent
@@ -14980,10 +15024,7 @@ var init_baseModel = __esm({
14980
15024
  */
14981
15025
  static async select(sql, params, limit = DEFAULT_ROW_CAP, offset = 0) {
14982
15026
  const ModelClass = this;
14983
- const db = ModelClass.getDb();
14984
- const paged = SQLTranslator.appendLimit(sql, limit, offset);
14985
- const rows = await adapterQuery(db, paged, params);
14986
- return rows.map((row) => new ModelClass(row));
15027
+ return ModelClass._collect(sql, params, limit, offset);
14987
15028
  }
14988
15029
  static async selectOne(sql, params, include) {
14989
15030
  const ModelClass = this;
@@ -15055,7 +15096,6 @@ var init_baseModel = __esm({
15055
15096
  */
15056
15097
  static async withTrashed(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0) {
15057
15098
  const ModelClass = this;
15058
- const db = ModelClass.getDb();
15059
15099
  const parts = [];
15060
15100
  if (ModelClass.tableFilter) {
15061
15101
  parts.push(ModelClass.tableFilter);
@@ -15067,14 +15107,7 @@ var init_baseModel = __esm({
15067
15107
  if (parts.length > 0) {
15068
15108
  sql += ` WHERE ${parts.join(" AND ")}`;
15069
15109
  }
15070
- if (limit !== void 0) {
15071
- sql += ` LIMIT ${limit}`;
15072
- }
15073
- if (offset !== void 0) {
15074
- sql += ` OFFSET ${offset}`;
15075
- }
15076
- const rows = await adapterQuery(db, sql, params);
15077
- return rows.map((row) => new ModelClass(row));
15110
+ return ModelClass._collect(sql, params, limit, offset);
15078
15111
  }
15079
15112
  /**
15080
15113
  * Count records matching conditions (respects soft delete and table filter).
@@ -18413,6 +18446,7 @@ __export(src_exports, {
18413
18446
  InvalidId: () => InvalidId,
18414
18447
  LocalStorage: () => LocalStorage,
18415
18448
  Migration: () => Migration,
18449
+ ModelCollection: () => ModelCollection,
18416
18450
  MongodbAdapter: () => MongodbAdapter,
18417
18451
  MssqlAdapter: () => MssqlAdapter,
18418
18452
  MysqlAdapter: () => MysqlAdapter,
@@ -18517,6 +18551,7 @@ var init_src = __esm({
18517
18551
  "use strict";
18518
18552
  init_types();
18519
18553
  init_databaseResult();
18554
+ init_modelCollection();
18520
18555
  init_database();
18521
18556
  init_database();
18522
18557
  init_databaseUrl();
@@ -35243,7 +35278,7 @@ function toolbarJs() {
35243
35278
  connect();
35244
35279
  })();`;
35245
35280
  }
35246
- var cpuCount, DEV_SAFE_METHODS, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, handleChat, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, handleExecute, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
35281
+ var cpuCount, DEV_SAFE_METHODS, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
35247
35282
  var init_devAdmin = __esm({
35248
35283
  "src/devAdmin.ts"() {
35249
35284
  "use strict";
@@ -35581,9 +35616,6 @@ var init_devAdmin = __esm({
35581
35616
  { method: "POST", pattern: "/__dev/api/websockets/disconnect", handler: handleWebsocketsDisconnect },
35582
35617
  // Tools
35583
35618
  { method: "POST", pattern: "/__dev/api/tool", handler: handleTool },
35584
- // Chat — proxies to Rust agent /chat (SSE passthrough). Forwards
35585
- // active_file and any other body keys verbatim. See proxyToSupervisor.
35586
- { method: "POST", pattern: "/__dev/api/chat", handler: handleChat },
35587
35619
  // Threads — proxies to Rust agent /threads. Mirrors Python's
35588
35620
  // _api_threads + _api_threads_sub.
35589
35621
  { method: "GET", pattern: "/__dev/api/threads", handler: handleThreads },
@@ -35662,8 +35694,6 @@ var init_devAdmin = __esm({
35662
35694
  { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
35663
35695
  { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
35664
35696
  { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
35665
- // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
35666
- { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
35667
35697
  // Framework-grounding MCP token config — self-contained (.env upsert)
35668
35698
  { method: "GET", pattern: "/__dev/api/grounding/status", handler: handleGroundingStatus },
35669
35699
  { method: "POST", pattern: "/__dev/api/grounding/token", handler: handleGroundingToken },
@@ -36233,9 +36263,6 @@ var init_devAdmin = __esm({
36233
36263
  }
36234
36264
  res.json({ tool, status: "executed", message: `Tool '${tool}' executed (stub)`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
36235
36265
  };
36236
- handleChat = async (req2, res) => {
36237
- await proxyToSupervisor(req2, res, "/chat");
36238
- };
36239
36266
  DEFAULT_MCP_URL = "https://mcp.tina4.com";
36240
36267
  handleGroundingStatus = async (_req, res) => {
36241
36268
  const token = resolveDevEnvVar("TINA4_MCP_TOKEN");
@@ -36523,35 +36550,6 @@ var init_devAdmin = __esm({
36523
36550
  501
36524
36551
  );
36525
36552
  };
36526
- handleExecute = async (req2, res) => {
36527
- const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
36528
- const agentUrl = `http://127.0.0.1:${port + 2e3}/execute`;
36529
- try {
36530
- const upstream = await fetch(agentUrl, {
36531
- method: "POST",
36532
- headers: { "Content-Type": "application/json" },
36533
- body: JSON.stringify(req2.body ?? {})
36534
- });
36535
- if (!upstream.body) {
36536
- res.json({ error: "agent returned no body" }, 502);
36537
- return;
36538
- }
36539
- res.raw.writeHead(upstream.status || 200, {
36540
- "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
36541
- "Cache-Control": "no-cache",
36542
- Connection: "keep-alive"
36543
- });
36544
- const reader = upstream.body.getReader();
36545
- while (true) {
36546
- const { done, value } = await reader.read();
36547
- if (done) break;
36548
- if (value) res.raw.write(Buffer.from(value));
36549
- }
36550
- res.raw.end();
36551
- } catch (e) {
36552
- res.json({ error: `agent unreachable at ${agentUrl}: ${e.message}` }, 502);
36553
- }
36554
- };
36555
36553
  DEV_FILES_IGNORED = /* @__PURE__ */ new Set([
36556
36554
  "__pycache__",
36557
36555
  "node_modules",
@@ -39029,7 +39027,9 @@ function injectIntoHtml(ctx, devToolbar, html) {
39029
39027
  // Suppress the live reloader on the AI/stable port (data-reload="0"); the
39030
39028
  // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
39031
39029
  // suppressReload flag.
39032
- reload: !ctx.isAiPortRequest
39030
+ // Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
39031
+ // itself gently, so the toolbar's full-page reloader must not fire there.
39032
+ reload: !ctx.isAiPortRequest && !ctx.pathname.startsWith("/__dev")
39033
39033
  };
39034
39034
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
39035
39035
  }
@@ -39284,6 +39284,51 @@ async function runDispatch(ctx, rawReq, rawRes) {
39284
39284
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
39285
39285
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
39286
39286
  }
39287
+ function loopbackBindHosts(host) {
39288
+ const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
39289
+ switch (normalized) {
39290
+ case "localhost":
39291
+ return ["127.0.0.1", "::1"];
39292
+ case "127.0.0.1":
39293
+ case "0.0.0.0":
39294
+ return ["::1"];
39295
+ case "::1":
39296
+ case "::":
39297
+ return ["127.0.0.1"];
39298
+ default:
39299
+ return [];
39300
+ }
39301
+ }
39302
+ function startLoopbackSiblings(port, host, dispatch) {
39303
+ const siblingHosts = loopbackBindHosts(host);
39304
+ if (siblingHosts.length === 0) return Promise.resolve([]);
39305
+ return Promise.all(
39306
+ siblingHosts.map(
39307
+ (siblingHost) => new Promise((resolveSibling) => {
39308
+ const sibling = createServer2(dispatch);
39309
+ let settled = false;
39310
+ sibling.on("error", (err) => {
39311
+ const expected = err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT";
39312
+ if (!expected) {
39313
+ Log.debug(
39314
+ `Loopback sibling ${siblingHost}:${port} not bound (${err.code ?? err.message}) \u2014 skipped, primary already serves`
39315
+ );
39316
+ }
39317
+ if (!settled) {
39318
+ settled = true;
39319
+ resolveSibling(null);
39320
+ }
39321
+ });
39322
+ sibling.listen(port, siblingHost, () => {
39323
+ settled = true;
39324
+ resolveSibling(sibling);
39325
+ });
39326
+ })
39327
+ )
39328
+ ).then(
39329
+ (results) => results.filter((s) => s !== null)
39330
+ );
39331
+ }
39287
39332
  async function startServer(config) {
39288
39333
  loadEnv(".env.local");
39289
39334
  loadEnv();
@@ -39552,8 +39597,9 @@ ${reset2}
39552
39597
  }
39553
39598
  });
39554
39599
  return new Promise((resolvePromise) => {
39555
- server.listen(port, host, () => {
39600
+ server.listen(port, host, async () => {
39556
39601
  if (!cluster.isWorker) writePidfile(port);
39602
+ const siblingServers = await startLoopbackSiblings(port, host, dispatch);
39557
39603
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
39558
39604
  const isDebug = isTruthy(process.env.TINA4_DEBUG);
39559
39605
  const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
@@ -39616,14 +39662,22 @@ ${reset2}
39616
39662
  }
39617
39663
  let shuttingDown = false;
39618
39664
  const closeListeners = () => new Promise((done) => {
39619
- let pending = aiServer ? 2 : 1;
39665
+ let pending = 1 + (aiServer ? 1 : 0) + siblingServers.length;
39620
39666
  const one = () => {
39621
39667
  if (--pending === 0) done();
39622
39668
  };
39623
39669
  server.close(one);
39624
39670
  if (aiServer) aiServer.close(one);
39671
+ for (const sibling of siblingServers) {
39672
+ try {
39673
+ sibling.close(one);
39674
+ } catch {
39675
+ one();
39676
+ }
39677
+ }
39625
39678
  server.closeIdleConnections();
39626
39679
  aiServer?.closeIdleConnections();
39680
+ for (const sibling of siblingServers) sibling.closeIdleConnections();
39627
39681
  });
39628
39682
  const gracefulShutdown = async (signal) => {
39629
39683
  if (shuttingDown) return;
@@ -39651,6 +39705,7 @@ ${reset2}
39651
39705
  );
39652
39706
  server.closeAllConnections();
39653
39707
  aiServer?.closeAllConnections();
39708
+ for (const sibling of siblingServers) sibling.closeAllConnections();
39654
39709
  }
39655
39710
  try {
39656
39711
  const orm = await Promise.resolve().then(() => (init_src(), src_exports));
@@ -39678,6 +39733,12 @@ ${reset2}
39678
39733
  stopAllBackgroundTasks();
39679
39734
  if (aiServer) aiServer.close();
39680
39735
  server.close();
39736
+ for (const sibling of siblingServers) {
39737
+ try {
39738
+ sibling.close();
39739
+ } catch {
39740
+ }
39741
+ }
39681
39742
  Promise.resolve().then(() => (init_src(), src_exports)).then((orm) => orm.closeDatabase()).catch(() => {
39682
39743
  });
39683
39744
  },
@@ -46863,6 +46924,7 @@ __export(index_exports, {
46863
46924
  isValidSessionId: () => isValidSessionId,
46864
46925
  kafkaSecurityConfig: () => kafkaSecurityConfig,
46865
46926
  loadEnv: () => loadEnv,
46927
+ loopbackBindHosts: () => loopbackBindHosts,
46866
46928
  makeCaseInsensitiveHeaders: () => makeCaseInsensitiveHeaders,
46867
46929
  matchCronField: () => matchCronField,
46868
46930
  matchesCron: () => matchesCron,
@@ -47232,6 +47294,7 @@ export {
47232
47294
  isValidSessionId,
47233
47295
  kafkaSecurityConfig,
47234
47296
  loadEnv,
47297
+ loopbackBindHosts,
47235
47298
  makeCaseInsensitiveHeaders,
47236
47299
  matchCronField,
47237
47300
  matchesCron,