@ashx-j/lunr 0.2.24 → 0.2.25

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/gateway/adapters/discord.d.ts.map +1 -1
  3. package/dist/gateway/adapters/discord.js +2 -7
  4. package/dist/gateway/adapters/discord.js.map +1 -1
  5. package/dist/gateway/adapters/telegram.d.ts.map +1 -1
  6. package/dist/gateway/adapters/telegram.js +7 -0
  7. package/dist/gateway/adapters/telegram.js.map +1 -1
  8. package/dist/gateway/agent-bridge.d.ts +4 -1
  9. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  10. package/dist/gateway/agent-bridge.js +57 -17
  11. package/dist/gateway/agent-bridge.js.map +1 -1
  12. package/dist/gateway/buttons.d.ts.map +1 -1
  13. package/dist/gateway/buttons.js +29 -19
  14. package/dist/gateway/buttons.js.map +1 -1
  15. package/dist/gateway/commands.d.ts +2 -0
  16. package/dist/gateway/commands.d.ts.map +1 -1
  17. package/dist/gateway/commands.js +39 -18
  18. package/dist/gateway/commands.js.map +1 -1
  19. package/dist/gateway/config.d.ts +1 -0
  20. package/dist/gateway/config.d.ts.map +1 -1
  21. package/dist/gateway/config.js +2 -0
  22. package/dist/gateway/config.js.map +1 -1
  23. package/dist/gateway/cron.d.ts +1 -1
  24. package/dist/gateway/cron.d.ts.map +1 -1
  25. package/dist/gateway/cron.js +2 -2
  26. package/dist/gateway/cron.js.map +1 -1
  27. package/dist/gateway/index.d.ts.map +1 -1
  28. package/dist/gateway/index.js +26 -26
  29. package/dist/gateway/index.js.map +1 -1
  30. package/dist/gateway/mobile-commands.d.ts.map +1 -1
  31. package/dist/gateway/mobile-commands.js +3 -3
  32. package/dist/gateway/mobile-commands.js.map +1 -1
  33. package/dist/gateway/presenter.d.ts +12 -1
  34. package/dist/gateway/presenter.d.ts.map +1 -1
  35. package/dist/gateway/presenter.js +146 -48
  36. package/dist/gateway/presenter.js.map +1 -1
  37. package/dist/gateway/router.d.ts +5 -5
  38. package/dist/gateway/router.d.ts.map +1 -1
  39. package/dist/gateway/router.js +95 -41
  40. package/dist/gateway/router.js.map +1 -1
  41. package/dist/gateway/service.d.ts.map +1 -1
  42. package/dist/gateway/service.js +13 -4
  43. package/dist/gateway/service.js.map +1 -1
  44. package/dist/gateway/setup.d.ts +5 -0
  45. package/dist/gateway/setup.d.ts.map +1 -1
  46. package/dist/gateway/setup.js +98 -42
  47. package/dist/gateway/setup.js.map +1 -1
  48. package/dist/gateway/stream.d.ts +6 -5
  49. package/dist/gateway/stream.d.ts.map +1 -1
  50. package/dist/gateway/stream.js +24 -11
  51. package/dist/gateway/stream.js.map +1 -1
  52. package/dist/node-runtime/{agent-session-services-IEF7LQWD.js → agent-session-services-YXXFCP5J.js} +7 -6
  53. package/dist/node-runtime/{builtin-extensions-EBAWTH4M.js → builtin-extensions-ZLAX7TUH.js} +1 -1
  54. package/dist/node-runtime/{chunk-OF7PWMG4.js → chunk-2HHLJWGD.js} +3 -87
  55. package/dist/node-runtime/{chunk-VWCE2HSZ.js → chunk-2HMIRK7W.js} +4 -4
  56. package/dist/node-runtime/{chunk-K5AGHH57.js → chunk-3XPLBUEM.js} +1 -1
  57. package/dist/node-runtime/{chunk-RORY2HMJ.js → chunk-6XL67VVY.js} +1 -1
  58. package/dist/node-runtime/chunk-7S3T6YEA.js +97 -0
  59. package/dist/node-runtime/{chunk-R3K2F5GI.js → chunk-B4HP6ZWM.js} +2 -0
  60. package/dist/node-runtime/{chunk-YVI6L6LU.js → chunk-E4D3IGP2.js} +38 -22
  61. package/dist/node-runtime/{chunk-5Q3EHOHI.js → chunk-IJIFHRTB.js} +14 -12
  62. package/dist/node-runtime/{chunk-Y73WPE6S.js → chunk-KWIYUUZK.js} +63 -37
  63. package/dist/node-runtime/{chunk-F4FLTQKV.js → chunk-LQP3RB67.js} +184 -1527
  64. package/dist/node-runtime/{chunk-FU6OODRZ.js → chunk-NRKGMJYU.js} +1 -1
  65. package/dist/node-runtime/{chunk-IGV54Q5V.js → chunk-QTU6AKNJ.js} +128 -63
  66. package/dist/node-runtime/{chunk-L3LX4SNR.js → chunk-RC7DMV45.js} +5 -5
  67. package/dist/node-runtime/{chunk-UWPJL22I.js → chunk-SLVOAQOY.js} +15 -4
  68. package/dist/node-runtime/{chunk-WHLRAGUQ.js → chunk-TORR5XBV.js} +6 -6
  69. package/dist/node-runtime/{chunk-UP2XBVH5.js → chunk-UOEDI7RP.js} +1 -1
  70. package/dist/node-runtime/{chunk-DE2JB6J5.js → chunk-VQFSUJJG.js} +2 -2
  71. package/dist/node-runtime/{chunk-QM43FEAE.js → chunk-WF7PGWRZ.js} +3 -3
  72. package/dist/node-runtime/chunk-YCUZEGJG.js +1361 -0
  73. package/dist/node-runtime/{command-7BG7VVTS.js → command-6WDOYKS6.js} +5 -5
  74. package/dist/node-runtime/{commands-QFDKNQTL.js → commands-QC6VUZJQ.js} +5 -5
  75. package/dist/node-runtime/{config-RORQUQFG.js → config-D4QUPAHX.js} +1 -1
  76. package/dist/node-runtime/{gateway-TWR3N65G.js → gateway-INAYBF4Q.js} +134 -78
  77. package/dist/node-runtime/index.js +15 -12
  78. package/dist/node-runtime/{install-cli-VAP6KBFN.js → install-cli-KL4UDDAD.js} +3 -3
  79. package/dist/node-runtime/{interactive-mode-6J5VZ2TT.js → interactive-mode-Z5M7WYYZ.js} +8 -7
  80. package/dist/node-runtime/{lunr-browser-J5ZZYVGM.js → lunr-browser-IR7TL3J7.js} +3 -3
  81. package/dist/node-runtime/{lunr-cron-CXOL37AL.js → lunr-cron-LWIZJM2N.js} +5 -5
  82. package/dist/node-runtime/main.js +12 -10
  83. package/dist/node-runtime/{mobile-commands-VTBM5DSZ.js → mobile-commands-J7OJEKFO.js} +4 -4
  84. package/dist/node-runtime/{package-manager-cli-ZLW5DZXF.js → package-manager-cli-VYZM5WHN.js} +10 -7
  85. package/dist/node-runtime/{pi-subagents-XPCLLKIM.js → pi-subagents-DNY7EAIG.js} +6 -6
  86. package/dist/node-runtime/rpc-entry.js +12 -10
  87. package/dist/node-runtime/{runtime-bridges-QYXUX2TG.js → runtime-bridges-VRDDR73P.js} +5 -5
  88. package/dist/node-runtime/{service-ISSP6VHE.js → service-MDQIPQ26.js} +1 -1
  89. package/dist/node-runtime/{settings-manager-WCQDUVQB.js → settings-manager-J4SPQPDJ.js} +4 -4
  90. package/dist/node-runtime/setup-CQ2NLVK5.js +214 -0
  91. package/dist/node-runtime/{subagent-executor-OTNISDQU.js → subagent-executor-KZZQGAKV.js} +5 -5
  92. package/dist/node-runtime/{update-cli-VBR2777K.js → update-cli-GTFJC7XK.js} +4 -4
  93. package/docs/features.md +4 -4
  94. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  95. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  96. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  97. package/examples/extensions/gondolin/package-lock.json +2 -2
  98. package/examples/extensions/gondolin/package.json +1 -1
  99. package/examples/extensions/sandbox/package-lock.json +2 -2
  100. package/examples/extensions/sandbox/package.json +1 -1
  101. package/examples/extensions/with-deps/package-lock.json +2 -2
  102. package/examples/extensions/with-deps/package.json +1 -1
  103. package/package.json +4 -4
  104. package/dist/node-runtime/setup-YRBKH2LD.js +0 -155
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/gateway/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,qBAAqB;IACrC,4FAA4F;IAC5F,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS9D;AAED,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,YAAY,OAAO,EAAE,qBAAqB,EAIzC;IAED,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAQxB;IAED,qFAAqF;IAC/E,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAIhC;IAED,8EAA8E;IAC9E,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED,iFAAiF;IACjF,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,0EAAwE;IACxE,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,QAAQ;YAIF,KAAK;CAcnB","sourcesContent":["/**\n * lunR: gateway streaming — throttled progressive replies.\n *\n * StreamConsumer accumulates assistant text deltas and mirrors them into a\n * single chat message: the first flush sends a new message, later flushes\n * edit it. Flushes happen at most every intervalMs and only once at least\n * `threshold` new chars have accumulated. finalize() forces a last flush and\n * returns the full text — the caller splits THAT via text.ts for final\n * delivery; the streaming preview itself is truncated to maxPreview-3 + \"…\".\n *\n * applySilenceFilter() recognizes the [SILENT] / NO_REPLY markers (whole\n * response, or first/last line) and returns null when nothing deliverable\n * remains.\n */\n\nexport interface StreamConsumerOptions {\n\t/** Send the initial message; resolves with the platform message id (null = send failed). */\n\tsendInitial: (text: string) => Promise<string | null>;\n\tedit: (messageId: string, text: string) => Promise<void>;\n\tintervalMs: number;\n\tthreshold: number;\n\t/** Platform max message length; the streaming preview is truncated to fit. Default 4096. */\n\tmaxPreview?: number;\n\t/** Clock injection for tests. */\n\tnow?: () => number;\n}\n\nconst SILENCE_MARKERS = new Set([\"[SILENT]\", \"NO_REPLY\"]);\n\nfunction isMarkerLine(line: string): boolean {\n\treturn SILENCE_MARKERS.has(line.trim());\n}\n\n/**\n * Null when the text is just a silence marker (whole, first-line, or\n * last-line); otherwise the text with a leading/trailing marker line\n * stripped (null when nothing else remains).\n */\nexport function applySilenceFilter(text: string): string | null {\n\tconst trimmed = text.trim();\n\tif (trimmed.length === 0) return null;\n\tif (isMarkerLine(trimmed)) return null;\n\tconst lines = trimmed.split(\"\\n\");\n\tif (lines.length > 0 && isMarkerLine(lines[0])) lines.shift();\n\tif (lines.length > 0 && isMarkerLine(lines[lines.length - 1])) lines.pop();\n\tconst result = lines.join(\"\\n\").trim();\n\treturn result.length > 0 ? result : null;\n}\n\nexport class StreamConsumer {\n\tprivate readonly options: StreamConsumerOptions;\n\tprivate buffer = \"\";\n\tprivate flushedLen = 0;\n\tprivate messageId: string | null = null;\n\tprivate lastFlushAt = 0;\n\tprivate chain: Promise<void> = Promise.resolve();\n\tprivate readonly maxPreview: number;\n\tprivate readonly now: () => number;\n\n\tconstructor(options: StreamConsumerOptions) {\n\t\tthis.options = options;\n\t\tthis.maxPreview = options.maxPreview ?? 4096;\n\t\tthis.now = options.now ?? (() => Date.now());\n\t}\n\n\t/** Accumulate a delta; schedule a flush when both gates (chars, interval) pass. */\n\tpush(delta: string): void {\n\t\tthis.buffer += delta;\n\t\tif (\n\t\t\tthis.buffer.length - this.flushedLen >= this.options.threshold &&\n\t\t\tthis.now() - this.lastFlushAt >= this.options.intervalMs\n\t\t) {\n\t\t\tthis.schedule();\n\t\t}\n\t}\n\n\t/** Force a final flush; resolves with the FULL text (caller splits for delivery). */\n\tasync finalize(): Promise<string> {\n\t\tthis.schedule(true);\n\t\tawait this.chain;\n\t\treturn this.buffer;\n\t}\n\n\t/** The platform message id of the preview, when one was successfully sent. */\n\tget sentMessageId(): string | null {\n\t\treturn this.messageId;\n\t}\n\n\t/** True when the preview had to truncate the full text (buffer > maxPreview). */\n\tget truncated(): boolean {\n\t\treturn this.buffer.length > this.maxPreview;\n\t}\n\n\t/** The streaming preview: full text truncated to maxPreview-3 + \"…\". */\n\tprivate preview(): string {\n\t\tif (this.buffer.length <= this.maxPreview) return this.buffer;\n\t\tlet cut = this.maxPreview - 3;\n\t\t// Don't truncate between surrogate halves.\n\t\tconst code = this.buffer.charCodeAt(cut - 1);\n\t\tif (code >= 0xd800 && code <= 0xdbff) cut -= 1;\n\t\treturn `${this.buffer.slice(0, cut)}…`;\n\t}\n\n\tprivate schedule(force = false): void {\n\t\tthis.chain = this.chain.then(() => this.flush(force)).catch(() => {});\n\t}\n\n\tprivate async flush(force: boolean): Promise<void> {\n\t\tconst hasNew = this.buffer.length > this.flushedLen;\n\t\tif (!hasNew) return;\n\t\tif (!force && this.messageId !== null && this.now() - this.lastFlushAt < this.options.intervalMs) return;\n\t\tconst text = this.preview();\n\t\tif (this.messageId === null) {\n\t\t\tthis.messageId = await this.options.sendInitial(text);\n\t\t\tif (this.messageId === null) return; // send failed; retry on the next flush\n\t\t} else {\n\t\t\tawait this.options.edit(this.messageId, text);\n\t\t}\n\t\tthis.flushedLen = this.buffer.length;\n\t\tthis.lastFlushAt = this.now();\n\t}\n}\n"]}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/gateway/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,qBAAqB;IACrC,4FAA4F;IAC5F,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS9D;AAED,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,YAAY,OAAO,EAAE,qBAAqB,EAIzC;IAED,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAQxB;IAED,uEAAuE;IACjE,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAGhC;IAED,8EAA8E;IAC9E,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED,iFAAiF;IACjF,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,uEAAuE;IACvE,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,QAAQ;YAIF,KAAK;CAiBnB","sourcesContent":["/**\n * lunR: gateway streaming — throttled progressive replies.\n *\n * StreamConsumer accumulates assistant text deltas and mirrors them into a\n * single chat message: the first flush sends a new message, later flushes\n * edit it. Flushes happen at most every intervalMs and only once at least\n * `threshold` new chars have accumulated. finalize() waits for pending previews;\n * the router owns final delivery. Previews omit silence-marker lines and truncate\n * at maxPreview-3 + \"…\".\n *\n * applySilenceFilter() recognizes the [SILENT] / NO_REPLY markers (whole\n * response, or first/last line) and returns null when nothing deliverable\n * remains.\n */\n\nexport interface StreamConsumerOptions {\n\t/** Send the initial message; resolves with the platform message id (null = send failed). */\n\tsendInitial: (text: string) => Promise<string | null>;\n\tedit: (messageId: string, text: string) => Promise<void>;\n\tintervalMs: number;\n\tthreshold: number;\n\t/** Platform max message length; the streaming preview is truncated to fit. Default 4096. */\n\tmaxPreview?: number;\n\t/** Clock injection for tests. */\n\tnow?: () => number;\n}\n\nconst SILENCE_MARKERS = new Set([\"[SILENT]\", \"NO_REPLY\"]);\n\nfunction isMarkerLine(line: string): boolean {\n\treturn SILENCE_MARKERS.has(line.trim());\n}\n\n/**\n * Null when the text is just a silence marker (whole, first-line, or\n * last-line); otherwise the text with a leading/trailing marker line\n * stripped (null when nothing else remains).\n */\nexport function applySilenceFilter(text: string): string | null {\n\tconst trimmed = text.trim();\n\tif (trimmed.length === 0) return null;\n\tif (isMarkerLine(trimmed)) return null;\n\tconst lines = trimmed.split(\"\\n\");\n\tif (lines.length > 0 && isMarkerLine(lines[0])) lines.shift();\n\tif (lines.length > 0 && isMarkerLine(lines[lines.length - 1])) lines.pop();\n\tconst result = lines.join(\"\\n\").trim();\n\treturn result.length > 0 ? result : null;\n}\n\nexport class StreamConsumer {\n\tprivate readonly options: StreamConsumerOptions;\n\tprivate buffer = \"\";\n\tprivate flushedLen = 0;\n\tprivate lastPreview = \"\";\n\tprivate messageId: string | null = null;\n\tprivate lastFlushAt = 0;\n\tprivate chain: Promise<void> = Promise.resolve();\n\tprivate readonly maxPreview: number;\n\tprivate readonly now: () => number;\n\n\tconstructor(options: StreamConsumerOptions) {\n\t\tthis.options = options;\n\t\tthis.maxPreview = options.maxPreview ?? 4096;\n\t\tthis.now = options.now ?? (() => Date.now());\n\t}\n\n\t/** Accumulate a delta; schedule a flush when both gates (chars, interval) pass. */\n\tpush(delta: string): void {\n\t\tthis.buffer += delta;\n\t\tif (\n\t\t\tthis.buffer.length - this.flushedLen >= this.options.threshold &&\n\t\t\tthis.now() - this.lastFlushAt >= this.options.intervalMs\n\t\t) {\n\t\t\tthis.schedule();\n\t\t}\n\t}\n\n\t/** Wait for pending previews; final delivery belongs to the router. */\n\tasync finalize(): Promise<string> {\n\t\tawait this.chain;\n\t\treturn this.buffer;\n\t}\n\n\t/** The platform message id of the preview, when one was successfully sent. */\n\tget sentMessageId(): string | null {\n\t\treturn this.messageId;\n\t}\n\n\t/** True when the preview had to truncate the full text (buffer > maxPreview). */\n\tget truncated(): boolean {\n\t\treturn this.buffer.length > this.maxPreview;\n\t}\n\n\t/** Preview visible text without a trailing silence-marker fragment. */\n\tprivate preview(): string {\n\t\tconst lines = this.buffer.split(\"\\n\");\n\t\tconst last = lines.at(-1)?.trim() ?? \"\";\n\t\tif (SILENCE_MARKERS.has(last) || (last && [...SILENCE_MARKERS].some((marker) => marker.startsWith(last))))\n\t\t\tlines.pop();\n\t\tif (lines.length && isMarkerLine(lines[0])) lines.shift();\n\t\tconst visible = lines.join(\"\\n\").trim();\n\t\tif (!visible) return \"\";\n\t\tif (visible.length <= this.maxPreview) return visible;\n\t\tlet cut = this.maxPreview - 3;\n\t\t// Don't truncate between surrogate halves.\n\t\tconst code = visible.charCodeAt(cut - 1);\n\t\tif (code >= 0xd800 && code <= 0xdbff) cut -= 1;\n\t\treturn `${visible.slice(0, cut)}…`;\n\t}\n\n\tprivate schedule(force = false): void {\n\t\tthis.chain = this.chain.then(() => this.flush(force)).catch(() => {});\n\t}\n\n\tprivate async flush(force: boolean): Promise<void> {\n\t\tconst hasNew = this.buffer.length > this.flushedLen;\n\t\tif (!hasNew) return;\n\t\tif (!force && this.messageId !== null && this.now() - this.lastFlushAt < this.options.intervalMs) return;\n\t\tconst text = this.preview();\n\t\tif (!text || text === this.lastPreview) return;\n\t\tconst length = this.buffer.length;\n\t\tif (this.messageId === null) {\n\t\t\tthis.messageId = await this.options.sendInitial(text);\n\t\t\tif (this.messageId === null) return; // send failed; retry on the next flush\n\t\t} else {\n\t\t\tawait this.options.edit(this.messageId, text);\n\t\t}\n\t\tthis.flushedLen = length;\n\t\tthis.lastPreview = text;\n\t\tthis.lastFlushAt = this.now();\n\t}\n}\n"]}
@@ -4,9 +4,9 @@
4
4
  * StreamConsumer accumulates assistant text deltas and mirrors them into a
5
5
  * single chat message: the first flush sends a new message, later flushes
6
6
  * edit it. Flushes happen at most every intervalMs and only once at least
7
- * `threshold` new chars have accumulated. finalize() forces a last flush and
8
- * returns the full text — the caller splits THAT via text.ts for final
9
- * delivery; the streaming preview itself is truncated to maxPreview-3 + "…".
7
+ * `threshold` new chars have accumulated. finalize() waits for pending previews;
8
+ * the router owns final delivery. Previews omit silence-marker lines and truncate
9
+ * at maxPreview-3 + "…".
10
10
  *
11
11
  * applySilenceFilter() recognizes the [SILENT] / NO_REPLY markers (whole
12
12
  * response, or first/last line) and returns null when nothing deliverable
@@ -39,6 +39,7 @@ export class StreamConsumer {
39
39
  options;
40
40
  buffer = "";
41
41
  flushedLen = 0;
42
+ lastPreview = "";
42
43
  messageId = null;
43
44
  lastFlushAt = 0;
44
45
  chain = Promise.resolve();
@@ -57,9 +58,8 @@ export class StreamConsumer {
57
58
  this.schedule();
58
59
  }
59
60
  }
60
- /** Force a final flush; resolves with the FULL text (caller splits for delivery). */
61
+ /** Wait for pending previews; final delivery belongs to the router. */
61
62
  async finalize() {
62
- this.schedule(true);
63
63
  await this.chain;
64
64
  return this.buffer;
65
65
  }
@@ -71,16 +71,25 @@ export class StreamConsumer {
71
71
  get truncated() {
72
72
  return this.buffer.length > this.maxPreview;
73
73
  }
74
- /** The streaming preview: full text truncated to maxPreview-3 + "…". */
74
+ /** Preview visible text without a trailing silence-marker fragment. */
75
75
  preview() {
76
- if (this.buffer.length <= this.maxPreview)
77
- return this.buffer;
76
+ const lines = this.buffer.split("\n");
77
+ const last = lines.at(-1)?.trim() ?? "";
78
+ if (SILENCE_MARKERS.has(last) || (last && [...SILENCE_MARKERS].some((marker) => marker.startsWith(last))))
79
+ lines.pop();
80
+ if (lines.length && isMarkerLine(lines[0]))
81
+ lines.shift();
82
+ const visible = lines.join("\n").trim();
83
+ if (!visible)
84
+ return "";
85
+ if (visible.length <= this.maxPreview)
86
+ return visible;
78
87
  let cut = this.maxPreview - 3;
79
88
  // Don't truncate between surrogate halves.
80
- const code = this.buffer.charCodeAt(cut - 1);
89
+ const code = visible.charCodeAt(cut - 1);
81
90
  if (code >= 0xd800 && code <= 0xdbff)
82
91
  cut -= 1;
83
- return `${this.buffer.slice(0, cut)}…`;
92
+ return `${visible.slice(0, cut)}…`;
84
93
  }
85
94
  schedule(force = false) {
86
95
  this.chain = this.chain.then(() => this.flush(force)).catch(() => { });
@@ -92,6 +101,9 @@ export class StreamConsumer {
92
101
  if (!force && this.messageId !== null && this.now() - this.lastFlushAt < this.options.intervalMs)
93
102
  return;
94
103
  const text = this.preview();
104
+ if (!text || text === this.lastPreview)
105
+ return;
106
+ const length = this.buffer.length;
95
107
  if (this.messageId === null) {
96
108
  this.messageId = await this.options.sendInitial(text);
97
109
  if (this.messageId === null)
@@ -100,7 +112,8 @@ export class StreamConsumer {
100
112
  else {
101
113
  await this.options.edit(this.messageId, text);
102
114
  }
103
- this.flushedLen = this.buffer.length;
115
+ this.flushedLen = length;
116
+ this.lastPreview = text;
104
117
  this.lastFlushAt = this.now();
105
118
  }
106
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/gateway/stream.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway streaming — throttled progressive replies.\n *\n * StreamConsumer accumulates assistant text deltas and mirrors them into a\n * single chat message: the first flush sends a new message, later flushes\n * edit it. Flushes happen at most every intervalMs and only once at least\n * `threshold` new chars have accumulated. finalize() forces a last flush and\n * returns the full text — the caller splits THAT via text.ts for final\n * delivery; the streaming preview itself is truncated to maxPreview-3 + \"…\".\n *\n * applySilenceFilter() recognizes the [SILENT] / NO_REPLY markers (whole\n * response, or first/last line) and returns null when nothing deliverable\n * remains.\n */\n\nexport interface StreamConsumerOptions {\n\t/** Send the initial message; resolves with the platform message id (null = send failed). */\n\tsendInitial: (text: string) => Promise<string | null>;\n\tedit: (messageId: string, text: string) => Promise<void>;\n\tintervalMs: number;\n\tthreshold: number;\n\t/** Platform max message length; the streaming preview is truncated to fit. Default 4096. */\n\tmaxPreview?: number;\n\t/** Clock injection for tests. */\n\tnow?: () => number;\n}\n\nconst SILENCE_MARKERS = new Set([\"[SILENT]\", \"NO_REPLY\"]);\n\nfunction isMarkerLine(line: string): boolean {\n\treturn SILENCE_MARKERS.has(line.trim());\n}\n\n/**\n * Null when the text is just a silence marker (whole, first-line, or\n * last-line); otherwise the text with a leading/trailing marker line\n * stripped (null when nothing else remains).\n */\nexport function applySilenceFilter(text: string): string | null {\n\tconst trimmed = text.trim();\n\tif (trimmed.length === 0) return null;\n\tif (isMarkerLine(trimmed)) return null;\n\tconst lines = trimmed.split(\"\\n\");\n\tif (lines.length > 0 && isMarkerLine(lines[0])) lines.shift();\n\tif (lines.length > 0 && isMarkerLine(lines[lines.length - 1])) lines.pop();\n\tconst result = lines.join(\"\\n\").trim();\n\treturn result.length > 0 ? result : null;\n}\n\nexport class StreamConsumer {\n\tprivate readonly options: StreamConsumerOptions;\n\tprivate buffer = \"\";\n\tprivate flushedLen = 0;\n\tprivate messageId: string | null = null;\n\tprivate lastFlushAt = 0;\n\tprivate chain: Promise<void> = Promise.resolve();\n\tprivate readonly maxPreview: number;\n\tprivate readonly now: () => number;\n\n\tconstructor(options: StreamConsumerOptions) {\n\t\tthis.options = options;\n\t\tthis.maxPreview = options.maxPreview ?? 4096;\n\t\tthis.now = options.now ?? (() => Date.now());\n\t}\n\n\t/** Accumulate a delta; schedule a flush when both gates (chars, interval) pass. */\n\tpush(delta: string): void {\n\t\tthis.buffer += delta;\n\t\tif (\n\t\t\tthis.buffer.length - this.flushedLen >= this.options.threshold &&\n\t\t\tthis.now() - this.lastFlushAt >= this.options.intervalMs\n\t\t) {\n\t\t\tthis.schedule();\n\t\t}\n\t}\n\n\t/** Force a final flush; resolves with the FULL text (caller splits for delivery). */\n\tasync finalize(): Promise<string> {\n\t\tthis.schedule(true);\n\t\tawait this.chain;\n\t\treturn this.buffer;\n\t}\n\n\t/** The platform message id of the preview, when one was successfully sent. */\n\tget sentMessageId(): string | null {\n\t\treturn this.messageId;\n\t}\n\n\t/** True when the preview had to truncate the full text (buffer > maxPreview). */\n\tget truncated(): boolean {\n\t\treturn this.buffer.length > this.maxPreview;\n\t}\n\n\t/** The streaming preview: full text truncated to maxPreview-3 + \"…\". */\n\tprivate preview(): string {\n\t\tif (this.buffer.length <= this.maxPreview) return this.buffer;\n\t\tlet cut = this.maxPreview - 3;\n\t\t// Don't truncate between surrogate halves.\n\t\tconst code = this.buffer.charCodeAt(cut - 1);\n\t\tif (code >= 0xd800 && code <= 0xdbff) cut -= 1;\n\t\treturn `${this.buffer.slice(0, cut)}…`;\n\t}\n\n\tprivate schedule(force = false): void {\n\t\tthis.chain = this.chain.then(() => this.flush(force)).catch(() => {});\n\t}\n\n\tprivate async flush(force: boolean): Promise<void> {\n\t\tconst hasNew = this.buffer.length > this.flushedLen;\n\t\tif (!hasNew) return;\n\t\tif (!force && this.messageId !== null && this.now() - this.lastFlushAt < this.options.intervalMs) return;\n\t\tconst text = this.preview();\n\t\tif (this.messageId === null) {\n\t\t\tthis.messageId = await this.options.sendInitial(text);\n\t\t\tif (this.messageId === null) return; // send failed; retry on the next flush\n\t\t} else {\n\t\t\tawait this.options.edit(this.messageId, text);\n\t\t}\n\t\tthis.flushedLen = this.buffer.length;\n\t\tthis.lastFlushAt = this.now();\n\t}\n}\n"]}
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/gateway/stream.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway streaming — throttled progressive replies.\n *\n * StreamConsumer accumulates assistant text deltas and mirrors them into a\n * single chat message: the first flush sends a new message, later flushes\n * edit it. Flushes happen at most every intervalMs and only once at least\n * `threshold` new chars have accumulated. finalize() waits for pending previews;\n * the router owns final delivery. Previews omit silence-marker lines and truncate\n * at maxPreview-3 + \"…\".\n *\n * applySilenceFilter() recognizes the [SILENT] / NO_REPLY markers (whole\n * response, or first/last line) and returns null when nothing deliverable\n * remains.\n */\n\nexport interface StreamConsumerOptions {\n\t/** Send the initial message; resolves with the platform message id (null = send failed). */\n\tsendInitial: (text: string) => Promise<string | null>;\n\tedit: (messageId: string, text: string) => Promise<void>;\n\tintervalMs: number;\n\tthreshold: number;\n\t/** Platform max message length; the streaming preview is truncated to fit. Default 4096. */\n\tmaxPreview?: number;\n\t/** Clock injection for tests. */\n\tnow?: () => number;\n}\n\nconst SILENCE_MARKERS = new Set([\"[SILENT]\", \"NO_REPLY\"]);\n\nfunction isMarkerLine(line: string): boolean {\n\treturn SILENCE_MARKERS.has(line.trim());\n}\n\n/**\n * Null when the text is just a silence marker (whole, first-line, or\n * last-line); otherwise the text with a leading/trailing marker line\n * stripped (null when nothing else remains).\n */\nexport function applySilenceFilter(text: string): string | null {\n\tconst trimmed = text.trim();\n\tif (trimmed.length === 0) return null;\n\tif (isMarkerLine(trimmed)) return null;\n\tconst lines = trimmed.split(\"\\n\");\n\tif (lines.length > 0 && isMarkerLine(lines[0])) lines.shift();\n\tif (lines.length > 0 && isMarkerLine(lines[lines.length - 1])) lines.pop();\n\tconst result = lines.join(\"\\n\").trim();\n\treturn result.length > 0 ? result : null;\n}\n\nexport class StreamConsumer {\n\tprivate readonly options: StreamConsumerOptions;\n\tprivate buffer = \"\";\n\tprivate flushedLen = 0;\n\tprivate lastPreview = \"\";\n\tprivate messageId: string | null = null;\n\tprivate lastFlushAt = 0;\n\tprivate chain: Promise<void> = Promise.resolve();\n\tprivate readonly maxPreview: number;\n\tprivate readonly now: () => number;\n\n\tconstructor(options: StreamConsumerOptions) {\n\t\tthis.options = options;\n\t\tthis.maxPreview = options.maxPreview ?? 4096;\n\t\tthis.now = options.now ?? (() => Date.now());\n\t}\n\n\t/** Accumulate a delta; schedule a flush when both gates (chars, interval) pass. */\n\tpush(delta: string): void {\n\t\tthis.buffer += delta;\n\t\tif (\n\t\t\tthis.buffer.length - this.flushedLen >= this.options.threshold &&\n\t\t\tthis.now() - this.lastFlushAt >= this.options.intervalMs\n\t\t) {\n\t\t\tthis.schedule();\n\t\t}\n\t}\n\n\t/** Wait for pending previews; final delivery belongs to the router. */\n\tasync finalize(): Promise<string> {\n\t\tawait this.chain;\n\t\treturn this.buffer;\n\t}\n\n\t/** The platform message id of the preview, when one was successfully sent. */\n\tget sentMessageId(): string | null {\n\t\treturn this.messageId;\n\t}\n\n\t/** True when the preview had to truncate the full text (buffer > maxPreview). */\n\tget truncated(): boolean {\n\t\treturn this.buffer.length > this.maxPreview;\n\t}\n\n\t/** Preview visible text without a trailing silence-marker fragment. */\n\tprivate preview(): string {\n\t\tconst lines = this.buffer.split(\"\\n\");\n\t\tconst last = lines.at(-1)?.trim() ?? \"\";\n\t\tif (SILENCE_MARKERS.has(last) || (last && [...SILENCE_MARKERS].some((marker) => marker.startsWith(last))))\n\t\t\tlines.pop();\n\t\tif (lines.length && isMarkerLine(lines[0])) lines.shift();\n\t\tconst visible = lines.join(\"\\n\").trim();\n\t\tif (!visible) return \"\";\n\t\tif (visible.length <= this.maxPreview) return visible;\n\t\tlet cut = this.maxPreview - 3;\n\t\t// Don't truncate between surrogate halves.\n\t\tconst code = visible.charCodeAt(cut - 1);\n\t\tif (code >= 0xd800 && code <= 0xdbff) cut -= 1;\n\t\treturn `${visible.slice(0, cut)}…`;\n\t}\n\n\tprivate schedule(force = false): void {\n\t\tthis.chain = this.chain.then(() => this.flush(force)).catch(() => {});\n\t}\n\n\tprivate async flush(force: boolean): Promise<void> {\n\t\tconst hasNew = this.buffer.length > this.flushedLen;\n\t\tif (!hasNew) return;\n\t\tif (!force && this.messageId !== null && this.now() - this.lastFlushAt < this.options.intervalMs) return;\n\t\tconst text = this.preview();\n\t\tif (!text || text === this.lastPreview) return;\n\t\tconst length = this.buffer.length;\n\t\tif (this.messageId === null) {\n\t\t\tthis.messageId = await this.options.sendInitial(text);\n\t\t\tif (this.messageId === null) return; // send failed; retry on the next flush\n\t\t} else {\n\t\t\tawait this.options.edit(this.messageId, text);\n\t\t}\n\t\tthis.flushedLen = length;\n\t\tthis.lastPreview = text;\n\t\tthis.lastFlushAt = this.now();\n\t}\n}\n"]}
@@ -2,9 +2,10 @@ import { createRequire as __lunrRequire } from "node:module"; const require = __
2
2
  import {
3
3
  createAgentSessionFromServices,
4
4
  createAgentSessionServices
5
- } from "./chunk-DE2JB6J5.js";
5
+ } from "./chunk-VQFSUJJG.js";
6
6
  import "./chunk-CCCN2UCX.js";
7
- import "./chunk-F4FLTQKV.js";
7
+ import "./chunk-YCUZEGJG.js";
8
+ import "./chunk-LQP3RB67.js";
8
9
  import "./chunk-OBMWP3I5.js";
9
10
  import "./chunk-DVGWF6A2.js";
10
11
  import "./chunk-QFWG2RVL.js";
@@ -13,9 +14,9 @@ import "./chunk-53YJI7JU.js";
13
14
  import "./chunk-5ZQXRPMC.js";
14
15
  import "./chunk-AD6VXAO7.js";
15
16
  import "./chunk-KJ6DWS3K.js";
16
- import "./chunk-RORY2HMJ.js";
17
- import "./chunk-UP2XBVH5.js";
18
- import "./chunk-VWCE2HSZ.js";
17
+ import "./chunk-6XL67VVY.js";
18
+ import "./chunk-UOEDI7RP.js";
19
+ import "./chunk-2HMIRK7W.js";
19
20
  import "./chunk-URD2SLIT.js";
20
21
  import "./chunk-GYIXXQAO.js";
21
22
  import "./chunk-GJ2RDBRL.js";
@@ -35,7 +36,7 @@ import "./chunk-UAEOTO52.js";
35
36
  import "./chunk-RQ2Q5IYR.js";
36
37
  import "./chunk-PVCDKGR6.js";
37
38
  import "./chunk-OR4L3IB2.js";
38
- import "./chunk-R3K2F5GI.js";
39
+ import "./chunk-B4HP6ZWM.js";
39
40
  import "./chunk-E24EO34J.js";
40
41
  import "./chunk-GS42T6MX.js";
41
42
  import "./chunk-RDDAZOMU.js";
@@ -6,7 +6,7 @@ import {
6
6
  lightBuiltinExtensions,
7
7
  loadAllBuiltinExtensions,
8
8
  loadDeferredBuiltinExtensions
9
- } from "./chunk-QM43FEAE.js";
9
+ } from "./chunk-WF7PGWRZ.js";
10
10
  import "./chunk-PL6JKXWI.js";
11
11
  import "./chunk-5ZH3ZFIZ.js";
12
12
  import "./chunk-CCCN2UCX.js";
@@ -4,17 +4,10 @@ import {
4
4
  } from "./chunk-YPHAIRZT.js";
5
5
  import {
6
6
  DefaultPackageManager
7
- } from "./chunk-F4FLTQKV.js";
7
+ } from "./chunk-LQP3RB67.js";
8
8
  import {
9
9
  SettingsManager
10
- } from "./chunk-RORY2HMJ.js";
11
- import {
12
- getProjectTrustOptions,
13
- hasTrustRequiringProjectResources
14
- } from "./chunk-HTFRRDSF.js";
15
- import {
16
- emitProjectTrustEvent
17
- } from "./chunk-GJ2RDBRL.js";
10
+ } from "./chunk-6XL67VVY.js";
18
11
  import {
19
12
  KeybindingsManager
20
13
  } from "./chunk-5EFIU3OM.js";
@@ -213,88 +206,11 @@ async function showStartupInput(settingsManager, title, placeholder) {
213
206
  });
214
207
  }
215
208
 
216
- // packages/coding-agent/dist/cli/project-trust.js
217
- import chalk from "chalk";
218
- function createProjectTrustContext(options) {
219
- return {
220
- cwd: options.cwd,
221
- mode: options.mode === "interactive" ? "tui" : options.mode,
222
- hasUI: options.hasUI,
223
- ui: {
224
- select: async (title, selectOptions) => {
225
- if (options.hasUI && options.mode === "interactive")
226
- return showStartupSelector(options.settingsManager, title, selectOptions.map((option) => ({ label: option, value: option })));
227
- },
228
- confirm: async (title, message) => !options.hasUI || options.mode !== "interactive" ? !1 : await showStartupSelector(options.settingsManager, `${title}
229
- ${message}`, [
230
- { label: "Yes", value: !0 },
231
- { label: "No", value: !1 }
232
- ]) ?? !1,
233
- input: async (title, placeholder) => {
234
- if (options.hasUI && options.mode === "interactive")
235
- return showStartupInput(options.settingsManager, title, placeholder);
236
- },
237
- notify: (message, type = "info") => {
238
- if (options.mode !== "interactive") {
239
- let color = type === "error" ? chalk.red : type === "warning" ? chalk.yellow : chalk.cyan;
240
- console.error(color(message));
241
- }
242
- }
243
- }
244
- };
245
- }
246
-
247
- // packages/coding-agent/dist/core/project-trust.js
248
- function formatProjectTrustPrompt(cwd) {
249
- return `Trust project folder?
250
- ${cwd}
251
-
252
- This allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
253
- }
254
- async function selectProjectTrustOption(cwd, ctx) {
255
- let options = getProjectTrustOptions(cwd, { includeSessionOnly: !0 }), selected = await ctx.ui.select(formatProjectTrustPrompt(cwd), options.map((option) => option.label));
256
- return options.find((option) => option.label === selected);
257
- }
258
- function saveProjectTrustPromptResult(trustStore, result) {
259
- result.updates.length > 0 && trustStore.setMany(result.updates);
260
- }
261
- async function resolveProjectTrusted(options) {
262
- if (options.trustOverride !== void 0)
263
- return options.trustOverride;
264
- if (!hasTrustRequiringProjectResources(options.cwd))
265
- return !0;
266
- if (options.extensionsResult) {
267
- let { result, errors } = await emitProjectTrustEvent(options.extensionsResult, { type: "project_trust", cwd: options.cwd }, options.projectTrustContext);
268
- for (let error of errors)
269
- options.onExtensionError?.(`Extension "${error.extensionPath}" project_trust error: ${error.error}`);
270
- if (result) {
271
- let trusted = result.trusted === "yes";
272
- return result.remember === !0 && options.trustStore.set(options.cwd, trusted), trusted;
273
- }
274
- }
275
- let decision = options.trustStore.get(options.cwd);
276
- if (decision !== null)
277
- return decision;
278
- switch (options.defaultProjectTrust ?? "ask") {
279
- case "always":
280
- return !0;
281
- case "never":
282
- return !1;
283
- case "ask":
284
- break;
285
- }
286
- if (!options.projectTrustContext.hasUI)
287
- return !1;
288
- let selected = await selectProjectTrustOption(options.cwd, options.projectTrustContext);
289
- return selected !== void 0 ? (saveProjectTrustPromptResult(options.trustStore, selected), selected.trusted) : !1;
290
- }
291
-
292
209
  export {
293
210
  createStartupTui,
294
211
  startStartupTui,
295
212
  shouldRunFirstTimeSetup,
296
213
  showStartupSelector,
297
214
  showFirstTimeSetup,
298
- createProjectTrustContext,
299
- resolveProjectTrusted
215
+ showStartupInput
300
216
  };
@@ -2,7 +2,7 @@ import { createRequire as __lunrRequire } from "node:module"; const require = __
2
2
  import {
3
3
  loadGatewayConfig,
4
4
  saveGatewayConfig
5
- } from "./chunk-R3K2F5GI.js";
5
+ } from "./chunk-B4HP6ZWM.js";
6
6
  import {
7
7
  VERSION,
8
8
  appendDebugLog,
@@ -274,11 +274,11 @@ var chatPlatformsHandler = {
274
274
  if (cfg.telegram.token && (cfg.telegram.enabled = !0), cfg.discord.token && (cfg.discord.enabled = !0), saveGatewayConfig(cfg), ctx.next.options.autostart === !0) {
275
275
  if (!cfg.telegram.token && !cfg.discord.token)
276
276
  throw new Error("Configure a bot token with lunr gateway setup before enabling automatic startup.");
277
- await (await import("./service-ISSP6VHE.js")).configureStartup("login");
278
- } else ctx.previous?.options.autostart === !0 && await (await import("./service-ISSP6VHE.js")).configureStartup("off");
277
+ await (await import("./service-MDQIPQ26.js")).configureStartup("login");
278
+ } else ctx.previous?.options.autostart === !0 && await (await import("./service-MDQIPQ26.js")).configureStartup("off");
279
279
  },
280
280
  async disable(opts) {
281
- let service = await import("./service-ISSP6VHE.js");
281
+ let service = await import("./service-MDQIPQ26.js");
282
282
  service.loadServiceSettings().startup !== "off" && await service.configureStartup("off"), await service.stopGatewayService();
283
283
  let cfg = loadGatewayConfig();
284
284
  cfg.telegram.enabled = !1, cfg.discord.enabled = !1, opts.purgeSecrets && (delete cfg.telegram.token, delete cfg.discord.token), saveGatewayConfig(cfg);
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-YDO2F2IA.js";
14
14
  import {
15
15
  subagentCommunicationEnabled
16
- } from "./chunk-RORY2HMJ.js";
16
+ } from "./chunk-6XL67VVY.js";
17
17
  import {
18
18
  convertToLlm
19
19
  } from "./chunk-FVDOXRUD.js";
@@ -2,7 +2,7 @@ import { createRequire as __lunrRequire } from "node:module"; const require = __
2
2
  import {
3
3
  browserEnabledDefault,
4
4
  notifyBrowserEnabledChange
5
- } from "./chunk-UP2XBVH5.js";
5
+ } from "./chunk-UOEDI7RP.js";
6
6
  import {
7
7
  require_proper_lockfile
8
8
  } from "./chunk-URD2SLIT.js";
@@ -0,0 +1,97 @@
1
+ import { createRequire as __lunrRequire } from "node:module"; const require = __lunrRequire(import.meta.url);
2
+ import {
3
+ showStartupInput,
4
+ showStartupSelector
5
+ } from "./chunk-2HHLJWGD.js";
6
+ import {
7
+ getProjectTrustOptions,
8
+ hasTrustRequiringProjectResources
9
+ } from "./chunk-HTFRRDSF.js";
10
+ import {
11
+ emitProjectTrustEvent
12
+ } from "./chunk-GJ2RDBRL.js";
13
+ import {
14
+ APP_NAME,
15
+ CONFIG_DIR_NAME
16
+ } from "./chunk-FQMM2EZ2.js";
17
+
18
+ // packages/coding-agent/dist/cli/project-trust.js
19
+ import chalk from "chalk";
20
+ function createProjectTrustContext(options) {
21
+ return {
22
+ cwd: options.cwd,
23
+ mode: options.mode === "interactive" ? "tui" : options.mode,
24
+ hasUI: options.hasUI,
25
+ ui: {
26
+ select: async (title, selectOptions) => {
27
+ if (options.hasUI && options.mode === "interactive")
28
+ return showStartupSelector(options.settingsManager, title, selectOptions.map((option) => ({ label: option, value: option })));
29
+ },
30
+ confirm: async (title, message) => !options.hasUI || options.mode !== "interactive" ? !1 : await showStartupSelector(options.settingsManager, `${title}
31
+ ${message}`, [
32
+ { label: "Yes", value: !0 },
33
+ { label: "No", value: !1 }
34
+ ]) ?? !1,
35
+ input: async (title, placeholder) => {
36
+ if (options.hasUI && options.mode === "interactive")
37
+ return showStartupInput(options.settingsManager, title, placeholder);
38
+ },
39
+ notify: (message, type = "info") => {
40
+ if (options.mode !== "interactive") {
41
+ let color = type === "error" ? chalk.red : type === "warning" ? chalk.yellow : chalk.cyan;
42
+ console.error(color(message));
43
+ }
44
+ }
45
+ }
46
+ };
47
+ }
48
+
49
+ // packages/coding-agent/dist/core/project-trust.js
50
+ function formatProjectTrustPrompt(cwd) {
51
+ return `Trust project folder?
52
+ ${cwd}
53
+
54
+ This allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
55
+ }
56
+ async function selectProjectTrustOption(cwd, ctx) {
57
+ let options = getProjectTrustOptions(cwd, { includeSessionOnly: !0 }), selected = await ctx.ui.select(formatProjectTrustPrompt(cwd), options.map((option) => option.label));
58
+ return options.find((option) => option.label === selected);
59
+ }
60
+ function saveProjectTrustPromptResult(trustStore, result) {
61
+ result.updates.length > 0 && trustStore.setMany(result.updates);
62
+ }
63
+ async function resolveProjectTrusted(options) {
64
+ if (options.trustOverride !== void 0)
65
+ return options.trustOverride;
66
+ if (!hasTrustRequiringProjectResources(options.cwd))
67
+ return !0;
68
+ if (options.extensionsResult) {
69
+ let { result, errors } = await emitProjectTrustEvent(options.extensionsResult, { type: "project_trust", cwd: options.cwd }, options.projectTrustContext);
70
+ for (let error of errors)
71
+ options.onExtensionError?.(`Extension "${error.extensionPath}" project_trust error: ${error.error}`);
72
+ if (result) {
73
+ let trusted = result.trusted === "yes";
74
+ return result.remember === !0 && options.trustStore.set(options.cwd, trusted), trusted;
75
+ }
76
+ }
77
+ let decision = options.trustStore.get(options.cwd);
78
+ if (decision !== null)
79
+ return decision;
80
+ switch (options.defaultProjectTrust ?? "ask") {
81
+ case "always":
82
+ return !0;
83
+ case "never":
84
+ return !1;
85
+ case "ask":
86
+ break;
87
+ }
88
+ if (!options.projectTrustContext.hasUI)
89
+ return !1;
90
+ let selected = await selectProjectTrustOption(options.cwd, options.projectTrustContext);
91
+ return selected !== void 0 ? (saveProjectTrustPromptResult(options.trustStore, selected), selected.trusted) : !1;
92
+ }
93
+
94
+ export {
95
+ createProjectTrustContext,
96
+ resolveProjectTrusted
97
+ };
@@ -22,6 +22,7 @@ function defaultGatewayConfig() {
22
22
  return {
23
23
  owners: { telegram: [], discord: [] },
24
24
  projectRoots: [],
25
+ defaultProject: void 0,
25
26
  telegram: defaultPlatformConfig(!1),
26
27
  discord: { ...defaultPlatformConfig(!0), ignoredChannels: [], autoThread: !0 },
27
28
  groupSessionsPerUser: !0,
@@ -69,6 +70,7 @@ function sanitizeConfig(raw) {
69
70
  return {
70
71
  owners: { telegram: asStringArray(owners.telegram) ?? [], discord: asStringArray(owners.discord) ?? [] },
71
72
  projectRoots: asStringArray(r.projectRoots) ?? [],
73
+ defaultProject: typeof r.defaultProject == "string" ? r.defaultProject : void 0,
72
74
  telegram: sanitizePlatform(r.telegram, base.telegram),
73
75
  discord: sanitizeDiscord(r.discord, base.discord),
74
76
  groupSessionsPerUser: asBoolean(r.groupSessionsPerUser) ?? base.groupSessionsPerUser,
@@ -1,12 +1,14 @@
1
1
  import { createRequire as __lunrRequire } from "node:module"; const require = __lunrRequire(import.meta.url);
2
2
  import {
3
3
  FORWARDED_COMMANDS,
4
- MOBILE_COMMANDS
5
- } from "./chunk-WHLRAGUQ.js";
4
+ MOBILE_COMMANDS,
5
+ resolveWithinRoots
6
+ } from "./chunk-TORR5XBV.js";
6
7
  import {
7
8
  conversationBinding,
8
- createPicker
9
- } from "./chunk-IGV54Q5V.js";
9
+ createPicker,
10
+ isGatewayOwner
11
+ } from "./chunk-QTU6AKNJ.js";
10
12
  import {
11
13
  computeContextBreakdown
12
14
  } from "./chunk-CDVHZOOK.js";
@@ -19,6 +21,9 @@ import {
19
21
  import {
20
22
  runtimeScope
21
23
  } from "./chunk-IMLCWLPI.js";
24
+ import {
25
+ defaultGatewayConfig
26
+ } from "./chunk-B4HP6ZWM.js";
22
27
 
23
28
  // packages/coding-agent/dist/gateway/commands.js
24
29
  var MODEL_CACHE_TTL_MS = 300 * 1e3, SESSIONS_CACHE_TTL_MS = 300 * 1e3, MAX_MODEL_LIST_LINES = 15, PROVIDER_PER_PAGE = 10;
@@ -137,7 +142,14 @@ var newCommand = {
137
142
  bypassBusy: !0,
138
143
  needsSession: !1,
139
144
  async handler(ctx) {
140
- await ctx.bridge.reset(ctx.key), await ctx.reply("Session reset \u2014 next message starts a fresh session.");
145
+ await ctx.bridge.reset(ctx.key);
146
+ let cfg = ctx.cfg ?? defaultGatewayConfig(), owner = isGatewayOwner(ctx.event.source, cfg), ready = !!conversationBinding(ctx.key)?.cwd;
147
+ if (!ready && owner && cfg.defaultProject)
148
+ try {
149
+ resolveWithinRoots(cfg.defaultProject, cfg.projectRoots ?? []), ready = !0;
150
+ } catch {
151
+ }
152
+ await ctx.reply(ready ? "Session reset. Send a message to start fresh in the approved project." : owner ? "Session reset. Use /project to choose an approved project before sending a message." : "Session reset. Ask the computer's gateway owner to configure this chat's project access locally.");
141
153
  }
142
154
  }, undoCommand = {
143
155
  name: "undo",
@@ -246,7 +258,7 @@ key: ${ctx.key}`);
246
258
  }
247
259
  return {
248
260
  done: !0,
249
- text: `\u263E Model \u2192 ${exact2.provider}/${exact2.id} (thinking level re-clamped to ${session.thinkingLevel})`
261
+ text: `Model \u2192 ${exact2.provider}/${exact2.id} (thinking level re-clamped to ${session.thinkingLevel})`
250
262
  };
251
263
  }
252
264
  }, pickerOpts)).success) {
@@ -289,14 +301,16 @@ key: ${ctx.key}`);
289
301
  bypassBusy: !1,
290
302
  needsSession: !0,
291
303
  async handler(ctx) {
292
- let session = ctx.session, arg = ctx.args.trim(), currentFile = session.sessionManager?.getSessionFile();
304
+ let session = ctx.session, arg = ctx.args.trim(), currentFile = session.sessionManager?.getSessionFile(), cwd = session.sessionManager?.getCwd() ?? conversationBinding(ctx.key)?.cwd;
305
+ if (!cwd)
306
+ throw new Error("Select a project before listing sessions.");
293
307
  if (!arg) {
294
- let all = await SessionManager.list(process.cwd());
308
+ let all = await SessionManager.list(cwd);
295
309
  all.sort((a, b) => b.modified.getTime() - a.modified.getTime());
296
310
  let sessions = all.slice(0, 10);
297
311
  setCachedSessions(ctx.key, sessions);
298
312
  let items = sessions.map((s) => ({
299
- label: `${s.path === currentFile ? "\u263E " : ""}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || "(empty)"}`,
313
+ label: `${s.path === currentFile ? "(current) " : ""}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || "(empty)"}`,
300
314
  value: s.path
301
315
  }));
302
316
  if (!(await createPicker(ctx.adapter, ctx.event.source, {
@@ -309,14 +323,14 @@ key: ${ctx.key}`);
309
323
  async resolve(item) {
310
324
  await ctx.bridge.switchSession(ctx.key, item.value);
311
325
  let switched2 = await ctx.bridge.getSession(ctx.key), name2 = switched2?.sessionManager?.getSessionName() ?? "unnamed", count2 = switched2?.sessionManager?.getEntries().length ?? 0;
312
- return { done: !0, text: `\u263E Switched to "${name2}" (${count2} msgs). History continues here.` };
326
+ return { done: !0, text: `Switched to "${name2}" (${count2} msgs). History continues here.` };
313
327
  }
314
328
  }, { replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId })).success) {
315
329
  let lines = sessions.map((s, i) => {
316
- let marker = s.path === currentFile ? " \u263E" : "", label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || "(empty)";
330
+ let marker = s.path === currentFile ? " (current)" : "", label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || "(empty)";
317
331
  return `${i + 1}) ${label}${marker} \xB7 ${formatRelativeTime(s.modified)} \xB7 ${s.messageCount} msgs`;
318
332
  });
319
- await ctx.reply([`Sessions for ${process.cwd()}`, ...lines, "Use /sessions <n> or /sessions <id-prefix>"].join(`
333
+ await ctx.reply([`Sessions for ${cwd}`, ...lines, "Use /sessions <n> or /sessions <id-prefix>"].join(`
320
334
  `));
321
335
  }
322
336
  return;
@@ -329,7 +343,7 @@ key: ${ctx.key}`);
329
343
  }
330
344
  path = cached[numeric - 1].path;
331
345
  } else {
332
- let matches = (cached ?? await SessionManager.list(process.cwd())).filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));
346
+ let matches = (cached ?? await SessionManager.list(cwd)).filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));
333
347
  if (matches.length === 0) {
334
348
  await ctx.reply(`No session matches "${arg}".`);
335
349
  return;
@@ -440,7 +454,7 @@ key: ${ctx.key}`);
440
454
  title: "Pick a thinking level",
441
455
  async resolve(item) {
442
456
  let level = item.value;
443
- return session.setThinkingLevel(level), { done: !0, text: `\u263E Thinking \u2192 ${level}` };
457
+ return session.setThinkingLevel(level), { done: !0, text: `Thinking \u2192 ${level}` };
444
458
  }
445
459
  }, { replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId })).success || await ctx.reply(`Level: ${current} \u2014 available: ${levels.join(", ")}`);
446
460
  return;
@@ -468,14 +482,16 @@ key: ${ctx.key}`);
468
482
  ];
469
483
  function botCommandSpecs() {
470
484
  return [
471
- ...new Map([...CHAT_COMMANDS, ...MOBILE_COMMANDS, ...FORWARDED_COMMANDS].map((c) => [
472
- c.name,
473
- { name: c.name, description: c.description }
474
- ])).values()
485
+ ...new Map([
486
+ { name: "start", description: "Check gateway readiness" },
487
+ ...CHAT_COMMANDS,
488
+ ...MOBILE_COMMANDS,
489
+ ...FORWARDED_COMMANDS
490
+ ].map((c) => [c.name, { name: c.name, description: c.description }])).values()
475
491
  ];
476
492
  }
477
493
  function formatHelpText() {
478
- let lines = ["lunR gateway commands:"];
494
+ let lines = ["lunR gateway commands:", "/start \u2014 check gateway readiness"];
479
495
  for (let cmd of CHAT_COMMANDS) {
480
496
  let names = [cmd.name, ...cmd.aliases ?? []].map((a) => `/${a}`).join(" | ");
481
497
  lines.push(`${names} \u2014 ${cmd.description}`);
@@ -489,9 +505,9 @@ async function runChatCommand(cmd, ctx) {
489
505
  if (!cmd.bypassBusy && ctx.bridge.getStatus(ctx.key).busy)
490
506
  return await ctx.reply("\u23F3 busy \u2014 /stop first or wait"), !0;
491
507
  if (cmd.needsSession) {
492
- let session = await ctx.bridge.getSession(ctx.key);
508
+ let session = await ctx.bridge.getSession(ctx.key, cmd.name === "model");
493
509
  if (!session)
494
- return await ctx.reply("No session yet \u2014 send a message first."), !0;
510
+ return await ctx.reply("No session yet. Select an approved project with /project first."), !0;
495
511
  ctx.session = session;
496
512
  }
497
513
  try {
@@ -506,7 +522,7 @@ async function runChatCommand(cmd, ctx) {
506
522
  }
507
523
  }
508
524
  async function sendCommandReply(adapter, event, text) {
509
- await adapter.send(event.source.chatId, prefixLines(text, "\u263E "), {
525
+ await adapter.send(event.source.chatId, prefixLines(text, ""), {
510
526
  replyTo: event.messageId,
511
527
  threadId: event.source.threadId
512
528
  });