tina4-nodejs 3.13.130 → 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.
@@ -4064,33 +4064,17 @@ var init_engine = __esm({
4064
4064
  i++;
4065
4065
  }
4066
4066
  }
4067
- handleCache(tokens, start2, context) {
4068
- const [content] = stripTag(tokens[start2][1]);
4069
- const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4070
- const cacheKey = m ? m[1] : "default";
4071
- const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4072
- sweepExpiredCache(this.fragmentCache);
4073
- const cached = this.fragmentCache.get(cacheKey);
4074
- if (cached) {
4075
- const [htmlContent, expiresAt] = cached;
4076
- if (Date.now() < expiresAt) {
4077
- let i2 = start2 + 1;
4078
- let depth2 = 0;
4079
- while (i2 < tokens.length) {
4080
- if (tokens[i2][0] === "BLOCK") {
4081
- const [tagContent] = stripTag(tokens[i2][1]);
4082
- const tag = tagContent.split(/\s+/)[0] || "";
4083
- if (tag === "cache") depth2++;
4084
- else if (tag === "endcache") {
4085
- if (depth2 === 0) return [htmlContent, i2 + 1];
4086
- depth2--;
4087
- }
4088
- }
4089
- i2++;
4090
- }
4091
- return [htmlContent, i2];
4092
- }
4093
- }
4067
+ /**
4068
+ * Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
4069
+ * starting from the token after the opening tag (start + 1). Nested same-tag
4070
+ * blocks are kept in the body and balanced by depth; the matching closing tag is
4071
+ * consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
4072
+ *
4073
+ * canNest guards the open-tag count: handleSetBlock passes it so the inline
4074
+ * {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
4075
+ * only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
4076
+ */
4077
+ collectBlockBody(tokens, start2, openTag, closeTag, canNest) {
4094
4078
  const bodyTokens = [];
4095
4079
  let i = start2 + 1;
4096
4080
  let depth = 0;
@@ -4098,10 +4082,10 @@ var init_engine = __esm({
4098
4082
  if (tokens[i][0] === "BLOCK") {
4099
4083
  const [tagContent] = stripTag(tokens[i][1]);
4100
4084
  const tag = tagContent.split(/\s+/)[0] || "";
4101
- if (tag === "cache") {
4085
+ if (tag === openTag && (canNest ? canNest(tagContent) : true)) {
4102
4086
  depth++;
4103
4087
  bodyTokens.push(tokens[i]);
4104
- } else if (tag === "endcache") {
4088
+ } else if (tag === closeTag) {
4105
4089
  if (depth === 0) {
4106
4090
  i++;
4107
4091
  break;
@@ -4116,6 +4100,36 @@ var init_engine = __esm({
4116
4100
  }
4117
4101
  i++;
4118
4102
  }
4103
+ return [bodyTokens, i];
4104
+ }
4105
+ handleCache(tokens, start2, context) {
4106
+ const [content] = stripTag(tokens[start2][1]);
4107
+ const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4108
+ const cacheKey = m ? m[1] : "default";
4109
+ const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4110
+ sweepExpiredCache(this.fragmentCache);
4111
+ const cached = this.fragmentCache.get(cacheKey);
4112
+ if (cached) {
4113
+ const [htmlContent, expiresAt] = cached;
4114
+ if (Date.now() < expiresAt) {
4115
+ let i2 = start2 + 1;
4116
+ let depth = 0;
4117
+ while (i2 < tokens.length) {
4118
+ if (tokens[i2][0] === "BLOCK") {
4119
+ const [tagContent] = stripTag(tokens[i2][1]);
4120
+ const tag = tagContent.split(/\s+/)[0] || "";
4121
+ if (tag === "cache") depth++;
4122
+ else if (tag === "endcache") {
4123
+ if (depth === 0) return [htmlContent, i2 + 1];
4124
+ depth--;
4125
+ }
4126
+ }
4127
+ i2++;
4128
+ }
4129
+ return [htmlContent, i2];
4130
+ }
4131
+ }
4132
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "cache", "endcache");
4119
4133
  const rendered = this.renderTokens([...bodyTokens], context);
4120
4134
  capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
4121
4135
  this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
@@ -4280,62 +4294,20 @@ var init_engine = __esm({
4280
4294
  handleSetBlock(tokens, start2, context) {
4281
4295
  const [content] = stripTag(tokens[start2][1]);
4282
4296
  const name = (content.split(/\s+/)[1] || "").trim();
4283
- const bodyTokens = [];
4284
- let i = start2 + 1;
4285
- let depth = 0;
4286
- while (i < tokens.length) {
4287
- if (tokens[i][0] === "BLOCK") {
4288
- const [tagContent] = stripTag(tokens[i][1]);
4289
- const tag = tagContent.split(/\s+/)[0] || "";
4290
- if (tag === "set" && !tagContent.includes("=")) {
4291
- depth++;
4292
- bodyTokens.push(tokens[i]);
4293
- } else if (tag === "endset") {
4294
- if (depth === 0) {
4295
- i++;
4296
- break;
4297
- }
4298
- depth--;
4299
- bodyTokens.push(tokens[i]);
4300
- } else {
4301
- bodyTokens.push(tokens[i]);
4302
- }
4303
- } else {
4304
- bodyTokens.push(tokens[i]);
4305
- }
4306
- i++;
4307
- }
4297
+ const [bodyTokens, i] = this.collectBlockBody(
4298
+ tokens,
4299
+ start2,
4300
+ "set",
4301
+ "endset",
4302
+ (tagContent) => !tagContent.includes("=")
4303
+ );
4308
4304
  if (name) {
4309
4305
  context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
4310
4306
  }
4311
4307
  return i;
4312
4308
  }
4313
4309
  handleSpaceless(tokens, start2, context) {
4314
- const bodyTokens = [];
4315
- let i = start2 + 1;
4316
- let depth = 0;
4317
- while (i < tokens.length) {
4318
- if (tokens[i][0] === "BLOCK") {
4319
- const [tagContent] = stripTag(tokens[i][1]);
4320
- const tag = tagContent.split(/\s+/)[0] || "";
4321
- if (tag === "spaceless") {
4322
- depth++;
4323
- bodyTokens.push(tokens[i]);
4324
- } else if (tag === "endspaceless") {
4325
- if (depth === 0) {
4326
- i++;
4327
- break;
4328
- }
4329
- depth--;
4330
- bodyTokens.push(tokens[i]);
4331
- } else {
4332
- bodyTokens.push(tokens[i]);
4333
- }
4334
- } else {
4335
- bodyTokens.push(tokens[i]);
4336
- }
4337
- i++;
4338
- }
4310
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "spaceless", "endspaceless");
4339
4311
  let rendered = this.renderTokens([...bodyTokens], context);
4340
4312
  rendered = rendered.replace(/>\s+</g, "><");
4341
4313
  return [rendered, i];
@@ -4344,31 +4316,7 @@ var init_engine = __esm({
4344
4316
  const [content] = stripTag(tokens[start2][1]);
4345
4317
  const modeMatch = content.match(/^autoescape\s+(false|true)/);
4346
4318
  const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
4347
- const bodyTokens = [];
4348
- let i = start2 + 1;
4349
- let depth = 0;
4350
- while (i < tokens.length) {
4351
- if (tokens[i][0] === "BLOCK") {
4352
- const [tagContent] = stripTag(tokens[i][1]);
4353
- const tag = tagContent.split(/\s+/)[0] || "";
4354
- if (tag === "autoescape") {
4355
- depth++;
4356
- bodyTokens.push(tokens[i]);
4357
- } else if (tag === "endautoescape") {
4358
- if (depth === 0) {
4359
- i++;
4360
- break;
4361
- }
4362
- depth--;
4363
- bodyTokens.push(tokens[i]);
4364
- } else {
4365
- bodyTokens.push(tokens[i]);
4366
- }
4367
- } else {
4368
- bodyTokens.push(tokens[i]);
4369
- }
4370
- i++;
4371
- }
4319
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "autoescape", "endautoescape");
4372
4320
  if (!autoEscapeOn) {
4373
4321
  const oldAutoEscape = this._autoEscape;
4374
4322
  this._autoEscape = false;
@@ -5722,6 +5670,99 @@ var init_databaseResult = __esm({
5722
5670
  }
5723
5671
  });
5724
5672
 
5673
+ // ../orm/src/modelCollection.ts
5674
+ var ModelCollection;
5675
+ var init_modelCollection = __esm({
5676
+ "../orm/src/modelCollection.ts"() {
5677
+ "use strict";
5678
+ ModelCollection = class extends Array {
5679
+ /** Total rows matching the query's filter (ignores limit/offset). */
5680
+ _total = 0;
5681
+ /** The SQL limit that produced this page. */
5682
+ _limit = 0;
5683
+ /** The SQL offset that produced this page. */
5684
+ _offset = 0;
5685
+ /**
5686
+ * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
5687
+ * `Array`, never another `ModelCollection`. This is what defuses the Array-
5688
+ * subclass constructor/species trap: the engine never calls this constructor
5689
+ * with a length during those operations.
5690
+ */
5691
+ static get [Symbol.species]() {
5692
+ return Array;
5693
+ }
5694
+ /**
5695
+ * @param items the page of hydrated model instances (or the length, when the
5696
+ * engine constructs a derived array — defended against here).
5697
+ * @param total total rows matching the query's filter (ignores limit/offset).
5698
+ * @param limit the SQL limit that produced this page.
5699
+ * @param offset the SQL offset that produced this page.
5700
+ */
5701
+ constructor(items, total = 0, limit = 0, offset = 0) {
5702
+ super();
5703
+ if (typeof items === "number") {
5704
+ this.length = items;
5705
+ return;
5706
+ }
5707
+ if (items) {
5708
+ for (let i = 0; i < items.length; i++) {
5709
+ this[i] = items[i];
5710
+ }
5711
+ }
5712
+ this._total = Math.trunc(total) || 0;
5713
+ this._limit = Math.trunc(limit) || 0;
5714
+ this._offset = Math.trunc(offset) || 0;
5715
+ }
5716
+ /**
5717
+ * Total rows matching the query's filter, ignoring limit/offset.
5718
+ *
5719
+ * This is the whole point of the collection: the page slice you are iterating
5720
+ * is capped by `limit`, but this number is the full count of matching rows —
5721
+ * what a pager needs to render "page 3 of 13".
5722
+ */
5723
+ getTotalRecords() {
5724
+ return this._total;
5725
+ }
5726
+ /**
5727
+ * The canonical pagination envelope — seven snake_case keys, identical to
5728
+ * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
5729
+ * `toPaginate()` / `to_paginate()`.
5730
+ *
5731
+ * records the page's rows as plain objects (never re-sliced)
5732
+ * total getTotalRecords() — the true total for the filter
5733
+ * page floor(offset / per_page) + 1
5734
+ * per_page the query's limit
5735
+ * total_pages ceil(total / per_page)
5736
+ * limit the SQL limit actually applied
5737
+ * offset the SQL offset actually applied
5738
+ *
5739
+ * `records` are model dicts (via `toDict()`, the same serialisation the
5740
+ * framework applies to a model in a JSON response), so the JSON a client sees
5741
+ * matches `DatabaseResult` exactly — the result is uniform whether the route
5742
+ * returned a raw `db.fetch()` or an ORM query.
5743
+ */
5744
+ toPaginate() {
5745
+ const perPage = this._limit > 0 ? this._limit : this.length;
5746
+ const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
5747
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
5748
+ const records = this.map((model) => {
5749
+ const m = model;
5750
+ return m && typeof m.toDict === "function" ? m.toDict() : model;
5751
+ });
5752
+ return {
5753
+ records,
5754
+ total: this._total,
5755
+ page,
5756
+ per_page: perPage,
5757
+ total_pages: totalPages,
5758
+ limit: perPage,
5759
+ offset: this._offset
5760
+ };
5761
+ }
5762
+ };
5763
+ }
5764
+ });
5765
+
5725
5766
  // ../orm/src/databaseUrl.ts
5726
5767
  import { inspect } from "node:util";
5727
5768
  function stripOneSlash(path8) {
@@ -14070,6 +14111,7 @@ var init_baseModel = __esm({
14070
14111
  "../orm/src/baseModel.ts"() {
14071
14112
  "use strict";
14072
14113
  init_database();
14114
+ init_modelCollection();
14073
14115
  init_validation();
14074
14116
  init_queryBuilder();
14075
14117
  init_sqlite();
@@ -14305,6 +14347,30 @@ var init_baseModel = __esm({
14305
14347
  static getPkColumn() {
14306
14348
  return this.getDbColumn(this.getPkField());
14307
14349
  }
14350
+ /**
14351
+ * Shared read tail for the collection-returning finders (where / all / select
14352
+ * / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
14353
+ * makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
14354
+ * the rows into model instances, and returns a ModelCollection carrying the
14355
+ * total (ADR-0064).
14356
+ *
14357
+ * The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
14358
+ * already runs; the ORM used to discard it. ZERO extra queries beyond that one
14359
+ * probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
14360
+ * limit/offset to the page, and the probe wraps the un-limited SQL so it counts
14361
+ * the WHOLE filtered set, not the page.
14362
+ */
14363
+ static async _collect(sql, params, limit, offset, include) {
14364
+ const db = this.getDb();
14365
+ const rows = await adapterFetch(db, sql, params, limit, offset);
14366
+ const data = Array.isArray(rows) ? rows : rows?.data ?? [];
14367
+ const total = await probeTotal(db, sql, params, limit) ?? data.length;
14368
+ const instances = data.map((row) => new this(row));
14369
+ if (include) {
14370
+ await this._eagerLoad(instances, include);
14371
+ }
14372
+ return new ModelCollection(instances, total, limit, offset);
14373
+ }
14308
14374
  /**
14309
14375
  * Find a record by primary key.
14310
14376
  * @param id Primary key value.
@@ -14357,7 +14423,6 @@ var init_baseModel = __esm({
14357
14423
  );
14358
14424
  }
14359
14425
  const lim = typeof limit === "number" ? limit : 100;
14360
- const db = ModelClass.getDb();
14361
14426
  const conditions = [];
14362
14427
  const params = [];
14363
14428
  if (filter) {
@@ -14377,16 +14442,7 @@ var init_baseModel = __esm({
14377
14442
  if (orderBy) {
14378
14443
  sql += ` ORDER BY ${orderBy}`;
14379
14444
  }
14380
- const rows = await adapterFetch(db, sql, params, lim, offset);
14381
- const data = rows?.data ?? rows;
14382
- const instances = (Array.isArray(data) ? data : []).map((row) => {
14383
- const inst = new this(row);
14384
- return inst;
14385
- });
14386
- if (include) {
14387
- await ModelClass._eagerLoad(instances, include);
14388
- }
14389
- return instances;
14445
+ return ModelClass._collect(sql, params, lim, offset, include);
14390
14446
  }
14391
14447
  /**
14392
14448
  * Load a record into this instance via selectOne.
@@ -14454,7 +14510,6 @@ var init_baseModel = __esm({
14454
14510
  */
14455
14511
  static async all(limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14456
14512
  const ModelClass = this;
14457
- const db = ModelClass.getDb();
14458
14513
  const conditions = [];
14459
14514
  if (ModelClass.softDelete) {
14460
14515
  conditions.push("is_deleted = 0");
@@ -14464,13 +14519,8 @@ var init_baseModel = __esm({
14464
14519
  }
14465
14520
  const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
14466
14521
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14467
- const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14468
- const rows = await adapterQuery(db, sql, []);
14469
- const instances = rows.map((row) => new ModelClass(row));
14470
- if (include) {
14471
- await ModelClass._eagerLoad(instances, include);
14472
- }
14473
- return instances;
14522
+ const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
14523
+ return ModelClass._collect(sql, [], limit, offset, include);
14474
14524
  }
14475
14525
  /**
14476
14526
  * Query records with a WHERE clause.
@@ -14485,7 +14535,6 @@ var init_baseModel = __esm({
14485
14535
  */
14486
14536
  static async where(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14487
14537
  const ModelClass = this;
14488
- const db = ModelClass.getDb();
14489
14538
  const parts = [];
14490
14539
  if (ModelClass.softDelete) {
14491
14540
  parts.push("is_deleted = 0");
@@ -14495,13 +14544,8 @@ var init_baseModel = __esm({
14495
14544
  }
14496
14545
  parts.push(`(${conditions})`);
14497
14546
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14498
- const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14499
- const rows = await adapterQuery(db, sql, params);
14500
- const instances = rows.map((row) => new ModelClass(row));
14501
- if (include) {
14502
- await ModelClass._eagerLoad(instances, include);
14503
- }
14504
- return instances;
14547
+ const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
14548
+ return ModelClass._collect(sql, params, limit, offset, include);
14505
14549
  }
14506
14550
  /**
14507
14551
  * Save this instance (insert or update). Returns this on success (fluent
@@ -14981,10 +15025,7 @@ var init_baseModel = __esm({
14981
15025
  */
14982
15026
  static async select(sql, params, limit = DEFAULT_ROW_CAP, offset = 0) {
14983
15027
  const ModelClass = this;
14984
- const db = ModelClass.getDb();
14985
- const paged = SQLTranslator.appendLimit(sql, limit, offset);
14986
- const rows = await adapterQuery(db, paged, params);
14987
- return rows.map((row) => new ModelClass(row));
15028
+ return ModelClass._collect(sql, params, limit, offset);
14988
15029
  }
14989
15030
  static async selectOne(sql, params, include) {
14990
15031
  const ModelClass = this;
@@ -15056,7 +15097,6 @@ var init_baseModel = __esm({
15056
15097
  */
15057
15098
  static async withTrashed(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0) {
15058
15099
  const ModelClass = this;
15059
- const db = ModelClass.getDb();
15060
15100
  const parts = [];
15061
15101
  if (ModelClass.tableFilter) {
15062
15102
  parts.push(ModelClass.tableFilter);
@@ -15068,14 +15108,7 @@ var init_baseModel = __esm({
15068
15108
  if (parts.length > 0) {
15069
15109
  sql += ` WHERE ${parts.join(" AND ")}`;
15070
15110
  }
15071
- if (limit !== void 0) {
15072
- sql += ` LIMIT ${limit}`;
15073
- }
15074
- if (offset !== void 0) {
15075
- sql += ` OFFSET ${offset}`;
15076
- }
15077
- const rows = await adapterQuery(db, sql, params);
15078
- return rows.map((row) => new ModelClass(row));
15111
+ return ModelClass._collect(sql, params, limit, offset);
15079
15112
  }
15080
15113
  /**
15081
15114
  * Count records matching conditions (respects soft delete and table filter).
@@ -18414,6 +18447,7 @@ __export(src_exports, {
18414
18447
  InvalidId: () => InvalidId,
18415
18448
  LocalStorage: () => LocalStorage,
18416
18449
  Migration: () => Migration,
18450
+ ModelCollection: () => ModelCollection,
18417
18451
  MongodbAdapter: () => MongodbAdapter,
18418
18452
  MssqlAdapter: () => MssqlAdapter,
18419
18453
  MysqlAdapter: () => MysqlAdapter,
@@ -18518,6 +18552,7 @@ var init_src = __esm({
18518
18552
  "use strict";
18519
18553
  init_types();
18520
18554
  init_databaseResult();
18555
+ init_modelCollection();
18521
18556
  init_database();
18522
18557
  init_database();
18523
18558
  init_databaseUrl();
@@ -35264,7 +35299,7 @@ function toolbarJs() {
35264
35299
  connect();
35265
35300
  })();`;
35266
35301
  }
35267
- 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;
35302
+ 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;
35268
35303
  var init_devAdmin = __esm({
35269
35304
  "../core/src/devAdmin.ts"() {
35270
35305
  "use strict";
@@ -35602,9 +35637,6 @@ var init_devAdmin = __esm({
35602
35637
  { method: "POST", pattern: "/__dev/api/websockets/disconnect", handler: handleWebsocketsDisconnect },
35603
35638
  // Tools
35604
35639
  { method: "POST", pattern: "/__dev/api/tool", handler: handleTool },
35605
- // Chat — proxies to Rust agent /chat (SSE passthrough). Forwards
35606
- // active_file and any other body keys verbatim. See proxyToSupervisor.
35607
- { method: "POST", pattern: "/__dev/api/chat", handler: handleChat },
35608
35640
  // Threads — proxies to Rust agent /threads. Mirrors Python's
35609
35641
  // _api_threads + _api_threads_sub.
35610
35642
  { method: "GET", pattern: "/__dev/api/threads", handler: handleThreads },
@@ -35683,8 +35715,6 @@ var init_devAdmin = __esm({
35683
35715
  { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
35684
35716
  { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
35685
35717
  { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
35686
- // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
35687
- { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
35688
35718
  // Framework-grounding MCP token config — self-contained (.env upsert)
35689
35719
  { method: "GET", pattern: "/__dev/api/grounding/status", handler: handleGroundingStatus },
35690
35720
  { method: "POST", pattern: "/__dev/api/grounding/token", handler: handleGroundingToken },
@@ -36254,9 +36284,6 @@ var init_devAdmin = __esm({
36254
36284
  }
36255
36285
  res.json({ tool, status: "executed", message: `Tool '${tool}' executed (stub)`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
36256
36286
  };
36257
- handleChat = async (req2, res) => {
36258
- await proxyToSupervisor(req2, res, "/chat");
36259
- };
36260
36287
  DEFAULT_MCP_URL = "https://mcp.tina4.com";
36261
36288
  handleGroundingStatus = async (_req, res) => {
36262
36289
  const token = resolveDevEnvVar("TINA4_MCP_TOKEN");
@@ -36544,35 +36571,6 @@ var init_devAdmin = __esm({
36544
36571
  501
36545
36572
  );
36546
36573
  };
36547
- handleExecute = async (req2, res) => {
36548
- const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
36549
- const agentUrl = `http://127.0.0.1:${port + 2e3}/execute`;
36550
- try {
36551
- const upstream = await fetch(agentUrl, {
36552
- method: "POST",
36553
- headers: { "Content-Type": "application/json" },
36554
- body: JSON.stringify(req2.body ?? {})
36555
- });
36556
- if (!upstream.body) {
36557
- res.json({ error: "agent returned no body" }, 502);
36558
- return;
36559
- }
36560
- res.raw.writeHead(upstream.status || 200, {
36561
- "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
36562
- "Cache-Control": "no-cache",
36563
- Connection: "keep-alive"
36564
- });
36565
- const reader = upstream.body.getReader();
36566
- while (true) {
36567
- const { done, value } = await reader.read();
36568
- if (done) break;
36569
- if (value) res.raw.write(Buffer.from(value));
36570
- }
36571
- res.raw.end();
36572
- } catch (e) {
36573
- res.json({ error: `agent unreachable at ${agentUrl}: ${e.message}` }, 502);
36574
- }
36575
- };
36576
36574
  DEV_FILES_IGNORED = /* @__PURE__ */ new Set([
36577
36575
  "__pycache__",
36578
36576
  "node_modules",
@@ -39050,7 +39048,9 @@ function injectIntoHtml(ctx, devToolbar, html) {
39050
39048
  // Suppress the live reloader on the AI/stable port (data-reload="0"); the
39051
39049
  // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
39052
39050
  // suppressReload flag.
39053
- reload: !ctx.isAiPortRequest
39051
+ // Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
39052
+ // itself gently, so the toolbar's full-page reloader must not fire there.
39053
+ reload: !ctx.isAiPortRequest && !ctx.pathname.startsWith("/__dev")
39054
39054
  };
39055
39055
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
39056
39056
  }
@@ -39305,6 +39305,51 @@ async function runDispatch(ctx, rawReq, rawRes) {
39305
39305
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
39306
39306
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
39307
39307
  }
39308
+ function loopbackBindHosts(host) {
39309
+ const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
39310
+ switch (normalized) {
39311
+ case "localhost":
39312
+ return ["127.0.0.1", "::1"];
39313
+ case "127.0.0.1":
39314
+ case "0.0.0.0":
39315
+ return ["::1"];
39316
+ case "::1":
39317
+ case "::":
39318
+ return ["127.0.0.1"];
39319
+ default:
39320
+ return [];
39321
+ }
39322
+ }
39323
+ function startLoopbackSiblings(port, host, dispatch) {
39324
+ const siblingHosts = loopbackBindHosts(host);
39325
+ if (siblingHosts.length === 0) return Promise.resolve([]);
39326
+ return Promise.all(
39327
+ siblingHosts.map(
39328
+ (siblingHost) => new Promise((resolveSibling) => {
39329
+ const sibling = createServer2(dispatch);
39330
+ let settled = false;
39331
+ sibling.on("error", (err) => {
39332
+ const expected = err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT";
39333
+ if (!expected) {
39334
+ Log.debug(
39335
+ `Loopback sibling ${siblingHost}:${port} not bound (${err.code ?? err.message}) \u2014 skipped, primary already serves`
39336
+ );
39337
+ }
39338
+ if (!settled) {
39339
+ settled = true;
39340
+ resolveSibling(null);
39341
+ }
39342
+ });
39343
+ sibling.listen(port, siblingHost, () => {
39344
+ settled = true;
39345
+ resolveSibling(sibling);
39346
+ });
39347
+ })
39348
+ )
39349
+ ).then(
39350
+ (results) => results.filter((s) => s !== null)
39351
+ );
39352
+ }
39308
39353
  async function startServer(config) {
39309
39354
  loadEnv(".env.local");
39310
39355
  loadEnv();
@@ -39573,8 +39618,9 @@ ${reset2}
39573
39618
  }
39574
39619
  });
39575
39620
  return new Promise((resolvePromise) => {
39576
- server.listen(port, host, () => {
39621
+ server.listen(port, host, async () => {
39577
39622
  if (!cluster.isWorker) writePidfile(port);
39623
+ const siblingServers = await startLoopbackSiblings(port, host, dispatch);
39578
39624
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
39579
39625
  const isDebug = isTruthy(process.env.TINA4_DEBUG);
39580
39626
  const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
@@ -39637,14 +39683,22 @@ ${reset2}
39637
39683
  }
39638
39684
  let shuttingDown = false;
39639
39685
  const closeListeners = () => new Promise((done) => {
39640
- let pending = aiServer ? 2 : 1;
39686
+ let pending = 1 + (aiServer ? 1 : 0) + siblingServers.length;
39641
39687
  const one = () => {
39642
39688
  if (--pending === 0) done();
39643
39689
  };
39644
39690
  server.close(one);
39645
39691
  if (aiServer) aiServer.close(one);
39692
+ for (const sibling of siblingServers) {
39693
+ try {
39694
+ sibling.close(one);
39695
+ } catch {
39696
+ one();
39697
+ }
39698
+ }
39646
39699
  server.closeIdleConnections();
39647
39700
  aiServer?.closeIdleConnections();
39701
+ for (const sibling of siblingServers) sibling.closeIdleConnections();
39648
39702
  });
39649
39703
  const gracefulShutdown = async (signal) => {
39650
39704
  if (shuttingDown) return;
@@ -39672,6 +39726,7 @@ ${reset2}
39672
39726
  );
39673
39727
  server.closeAllConnections();
39674
39728
  aiServer?.closeAllConnections();
39729
+ for (const sibling of siblingServers) sibling.closeAllConnections();
39675
39730
  }
39676
39731
  try {
39677
39732
  const orm = await Promise.resolve().then(() => (init_src(), src_exports));
@@ -39699,6 +39754,12 @@ ${reset2}
39699
39754
  stopAllBackgroundTasks();
39700
39755
  if (aiServer) aiServer.close();
39701
39756
  server.close();
39757
+ for (const sibling of siblingServers) {
39758
+ try {
39759
+ sibling.close();
39760
+ } catch {
39761
+ }
39762
+ }
39702
39763
  Promise.resolve().then(() => (init_src(), src_exports)).then((orm) => orm.closeDatabase()).catch(() => {
39703
39764
  });
39704
39765
  },
@@ -46902,6 +46963,7 @@ __export(src_exports3, {
46902
46963
  isValidSessionId: () => isValidSessionId,
46903
46964
  kafkaSecurityConfig: () => kafkaSecurityConfig,
46904
46965
  loadEnv: () => loadEnv,
46966
+ loopbackBindHosts: () => loopbackBindHosts,
46905
46967
  makeCaseInsensitiveHeaders: () => makeCaseInsensitiveHeaders,
46906
46968
  matchCronField: () => matchCronField,
46907
46969
  matchesCron: () => matchesCron,