@sympoies/dsh-telegram 0.0.0-bootstrap.0 → 0.6.0

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 (194) hide show
  1. package/LICENSE +1 -2
  2. package/NOTICE +19 -0
  3. package/README.md +589 -2
  4. package/cordis.patch.yml +14 -0
  5. package/lib/access.d.ts +76 -0
  6. package/lib/access.d.ts.map +1 -0
  7. package/lib/access.js +137 -0
  8. package/lib/access.js.map +1 -0
  9. package/lib/client.js +835 -0
  10. package/lib/commands.d.ts +51 -0
  11. package/lib/commands.d.ts.map +1 -0
  12. package/lib/commands.js +83 -0
  13. package/lib/commands.js.map +1 -0
  14. package/lib/config.d.ts +125 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +97 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/diagnostics.d.ts +58 -0
  19. package/lib/diagnostics.d.ts.map +1 -0
  20. package/lib/diagnostics.js +59 -0
  21. package/lib/diagnostics.js.map +1 -0
  22. package/lib/failures.d.ts +71 -0
  23. package/lib/failures.d.ts.map +1 -0
  24. package/lib/failures.js +139 -0
  25. package/lib/failures.js.map +1 -0
  26. package/lib/harness/host.d.ts +106 -0
  27. package/lib/harness/host.d.ts.map +1 -0
  28. package/lib/harness/host.js +142 -0
  29. package/lib/harness/host.js.map +1 -0
  30. package/lib/harness/message.d.ts +45 -0
  31. package/lib/harness/message.d.ts.map +1 -0
  32. package/lib/harness/message.js +28 -0
  33. package/lib/harness/message.js.map +1 -0
  34. package/lib/harness/model-selection.d.ts +82 -0
  35. package/lib/harness/model-selection.d.ts.map +1 -0
  36. package/lib/harness/model-selection.js +103 -0
  37. package/lib/harness/model-selection.js.map +1 -0
  38. package/lib/harness/questions-seam.d.ts +34 -0
  39. package/lib/harness/questions-seam.d.ts.map +1 -0
  40. package/lib/harness/questions-seam.js +45 -0
  41. package/lib/harness/questions-seam.js.map +1 -0
  42. package/lib/harness/types.d.ts +108 -0
  43. package/lib/harness/types.d.ts.map +1 -0
  44. package/lib/harness/types.js +23 -0
  45. package/lib/harness/types.js.map +1 -0
  46. package/lib/index.d.ts +65 -0
  47. package/lib/index.d.ts.map +1 -0
  48. package/lib/index.js +977 -0
  49. package/lib/index.js.map +1 -0
  50. package/lib/interact/approvals.d.ts +59 -0
  51. package/lib/interact/approvals.d.ts.map +1 -0
  52. package/lib/interact/approvals.js +132 -0
  53. package/lib/interact/approvals.js.map +1 -0
  54. package/lib/interact/pending.d.ts +56 -0
  55. package/lib/interact/pending.d.ts.map +1 -0
  56. package/lib/interact/pending.js +89 -0
  57. package/lib/interact/pending.js.map +1 -0
  58. package/lib/interact/questions.d.ts +100 -0
  59. package/lib/interact/questions.d.ts.map +1 -0
  60. package/lib/interact/questions.js +255 -0
  61. package/lib/interact/questions.js.map +1 -0
  62. package/lib/interact/surface.d.ts +33 -0
  63. package/lib/interact/surface.d.ts.map +1 -0
  64. package/lib/interact/surface.js +39 -0
  65. package/lib/interact/surface.js.map +1 -0
  66. package/lib/interact/text-capture.d.ts +38 -0
  67. package/lib/interact/text-capture.d.ts.map +1 -0
  68. package/lib/interact/text-capture.js +75 -0
  69. package/lib/interact/text-capture.js.map +1 -0
  70. package/lib/media/collect.d.ts +157 -0
  71. package/lib/media/collect.d.ts.map +1 -0
  72. package/lib/media/collect.js +235 -0
  73. package/lib/media/collect.js.map +1 -0
  74. package/lib/media/extractor.d.ts +117 -0
  75. package/lib/media/extractor.d.ts.map +1 -0
  76. package/lib/media/extractor.js +207 -0
  77. package/lib/media/extractor.js.map +1 -0
  78. package/lib/media/intake.d.ts +57 -0
  79. package/lib/media/intake.d.ts.map +1 -0
  80. package/lib/media/intake.js +115 -0
  81. package/lib/media/intake.js.map +1 -0
  82. package/lib/media/limits.d.ts +69 -0
  83. package/lib/media/limits.d.ts.map +1 -0
  84. package/lib/media/limits.js +97 -0
  85. package/lib/media/limits.js.map +1 -0
  86. package/lib/media/ocr.d.ts +66 -0
  87. package/lib/media/ocr.d.ts.map +1 -0
  88. package/lib/media/ocr.js +131 -0
  89. package/lib/media/ocr.js.map +1 -0
  90. package/lib/media/screenshot.d.ts +57 -0
  91. package/lib/media/screenshot.d.ts.map +1 -0
  92. package/lib/media/screenshot.js +101 -0
  93. package/lib/media/screenshot.js.map +1 -0
  94. package/lib/media/vision.d.ts +60 -0
  95. package/lib/media/vision.d.ts.map +1 -0
  96. package/lib/media/vision.js +74 -0
  97. package/lib/media/vision.js.map +1 -0
  98. package/lib/render/clamp.d.ts +34 -0
  99. package/lib/render/clamp.d.ts.map +1 -0
  100. package/lib/render/clamp.js +65 -0
  101. package/lib/render/clamp.js.map +1 -0
  102. package/lib/render/escape.d.ts +15 -0
  103. package/lib/render/escape.d.ts.map +1 -0
  104. package/lib/render/escape.js +17 -0
  105. package/lib/render/escape.js.map +1 -0
  106. package/lib/reply/activity.d.ts +31 -0
  107. package/lib/reply/activity.d.ts.map +1 -0
  108. package/lib/reply/activity.js +88 -0
  109. package/lib/reply/activity.js.map +1 -0
  110. package/lib/reply/rich-stream.d.ts +173 -0
  111. package/lib/reply/rich-stream.d.ts.map +1 -0
  112. package/lib/reply/rich-stream.js +286 -0
  113. package/lib/reply/rich-stream.js.map +1 -0
  114. package/lib/reply/split-markdown.d.ts +21 -0
  115. package/lib/reply/split-markdown.d.ts.map +1 -0
  116. package/lib/reply/split-markdown.js +87 -0
  117. package/lib/reply/split-markdown.js.map +1 -0
  118. package/lib/reply/turn-bridge.d.ts +143 -0
  119. package/lib/reply/turn-bridge.d.ts.map +1 -0
  120. package/lib/reply/turn-bridge.js +182 -0
  121. package/lib/reply/turn-bridge.js.map +1 -0
  122. package/lib/router.d.ts +415 -0
  123. package/lib/router.d.ts.map +1 -0
  124. package/lib/router.js +663 -0
  125. package/lib/router.js.map +1 -0
  126. package/lib/secrets.d.ts +30 -0
  127. package/lib/secrets.d.ts.map +1 -0
  128. package/lib/secrets.js +51 -0
  129. package/lib/secrets.js.map +1 -0
  130. package/lib/session/bindings.d.ts +81 -0
  131. package/lib/session/bindings.d.ts.map +1 -0
  132. package/lib/session/bindings.js +170 -0
  133. package/lib/session/bindings.js.map +1 -0
  134. package/lib/session/history.d.ts +50 -0
  135. package/lib/session/history.d.ts.map +1 -0
  136. package/lib/session/history.js +127 -0
  137. package/lib/session/history.js.map +1 -0
  138. package/lib/session/models.d.ts +99 -0
  139. package/lib/session/models.d.ts.map +1 -0
  140. package/lib/session/models.js +123 -0
  141. package/lib/session/models.js.map +1 -0
  142. package/lib/session/permission.d.ts +83 -0
  143. package/lib/session/permission.d.ts.map +1 -0
  144. package/lib/session/permission.js +107 -0
  145. package/lib/session/permission.js.map +1 -0
  146. package/lib/session/picker.d.ts +49 -0
  147. package/lib/session/picker.d.ts.map +1 -0
  148. package/lib/session/picker.js +107 -0
  149. package/lib/session/picker.js.map +1 -0
  150. package/lib/session/preferences.d.ts +55 -0
  151. package/lib/session/preferences.d.ts.map +1 -0
  152. package/lib/session/preferences.js +102 -0
  153. package/lib/session/preferences.js.map +1 -0
  154. package/lib/session/recovery.d.ts +66 -0
  155. package/lib/session/recovery.d.ts.map +1 -0
  156. package/lib/session/recovery.js +112 -0
  157. package/lib/session/recovery.js.map +1 -0
  158. package/lib/session/runner.d.ts +223 -0
  159. package/lib/session/runner.d.ts.map +1 -0
  160. package/lib/session/runner.js +260 -0
  161. package/lib/session/runner.js.map +1 -0
  162. package/lib/session/workspaces.d.ts +28 -0
  163. package/lib/session/workspaces.d.ts.map +1 -0
  164. package/lib/session/workspaces.js +53 -0
  165. package/lib/session/workspaces.js.map +1 -0
  166. package/lib/telegram/addressing.d.ts +39 -0
  167. package/lib/telegram/addressing.d.ts.map +1 -0
  168. package/lib/telegram/addressing.js +81 -0
  169. package/lib/telegram/addressing.js.map +1 -0
  170. package/lib/telegram/albums.d.ts +46 -0
  171. package/lib/telegram/albums.d.ts.map +1 -0
  172. package/lib/telegram/albums.js +104 -0
  173. package/lib/telegram/albums.js.map +1 -0
  174. package/lib/telegram/api.d.ts +238 -0
  175. package/lib/telegram/api.d.ts.map +1 -0
  176. package/lib/telegram/api.js +518 -0
  177. package/lib/telegram/api.js.map +1 -0
  178. package/lib/telegram/poller.d.ts +59 -0
  179. package/lib/telegram/poller.d.ts.map +1 -0
  180. package/lib/telegram/poller.js +94 -0
  181. package/lib/telegram/poller.js.map +1 -0
  182. package/lib/telegram/types.d.ts +130 -0
  183. package/lib/telegram/types.d.ts.map +1 -0
  184. package/lib/telegram/types.js +9 -0
  185. package/lib/telegram/types.js.map +1 -0
  186. package/lib/telegram/typing.d.ts +47 -0
  187. package/lib/telegram/typing.d.ts.map +1 -0
  188. package/lib/telegram/typing.js +91 -0
  189. package/lib/telegram/typing.js.map +1 -0
  190. package/lib/versions.d.ts +63 -0
  191. package/lib/versions.d.ts.map +1 -0
  192. package/lib/versions.js +135 -0
  193. package/lib/versions.js.map +1 -0
  194. package/package.json +94 -10
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Choosing which model a Telegram conversation talks to.
3
+ *
4
+ * The web UI has a model picker; a phone had nothing, so every conversation
5
+ * ran on whatever the deployment happened to default to. That is the wrong
6
+ * place to be stuck: the reason to reach for a bot from a phone is often that
7
+ * the question is small and a cheaper model would do, or that it is hard and a
8
+ * better one is worth the wait.
9
+ *
10
+ * The choice is per conversation and durable, so it survives `/new` and a
11
+ * restart. It is applied per turn rather than at session creation, because the
12
+ * harness reads a mutable selection while assembling each step — which is what
13
+ * lets a model change take effect on the very next message.
14
+ */
15
+ import type { ModelRoute } from '../harness/model-selection.js';
16
+ /** One reasoning effort a model offers. */
17
+ export interface EffortOption {
18
+ readonly id: string;
19
+ readonly name?: string;
20
+ }
21
+ /** What a model says about its own reasoning. */
22
+ export interface ReasoningInfo {
23
+ readonly efforts: readonly EffortOption[];
24
+ readonly defaultEffort?: string;
25
+ }
26
+ /** One model a provider advertises. */
27
+ export interface CatalogModel {
28
+ readonly id: string;
29
+ readonly name?: string;
30
+ }
31
+ /** One configured provider and what it offers. */
32
+ export interface CatalogProvider {
33
+ readonly id: string;
34
+ readonly name?: string;
35
+ readonly models: readonly CatalogModel[];
36
+ }
37
+ /** The `ctx.llm` surface this needs. */
38
+ export interface ProviderCatalog {
39
+ listProviders(): readonly {
40
+ id: string;
41
+ name?: string;
42
+ }[];
43
+ listModels(provider: string): Promise<readonly CatalogModel[]>;
44
+ resolveModelInfo(provider: string, model: string): Promise<{
45
+ reasoning?: ReasoningInfo;
46
+ }>;
47
+ }
48
+ /**
49
+ * The efforts a route offers, and which one it uses by default.
50
+ *
51
+ * Read from the model rather than fixed here: `low`/`medium`/`high` is one
52
+ * provider's vocabulary, not everyone's, and offering an effort a model does
53
+ * not have would fail the turn rather than the command.
54
+ *
55
+ * @param catalog - the harness llm service.
56
+ * @param route - the model to ask about.
57
+ * @returns its reasoning options, or undefined when it has none or cannot say.
58
+ */
59
+ export declare function effortsFor(catalog: ProviderCatalog, route: ModelRoute | undefined): Promise<ReasoningInfo | undefined>;
60
+ /**
61
+ * Find the effort a user's words name.
62
+ *
63
+ * @param input - what followed `/effort`.
64
+ * @param efforts - what the current model offers.
65
+ * @returns the exact effort id, or undefined when nothing matches.
66
+ */
67
+ export declare function matchEffort(input: string, efforts: readonly EffortOption[]): string | undefined;
68
+ /**
69
+ * Every configured provider with its models, for a message that lists them.
70
+ *
71
+ * A provider whose catalog cannot be read is skipped rather than failing the
72
+ * listing: one misconfigured key should not hide every other model.
73
+ *
74
+ * @param catalog - the harness llm service.
75
+ */
76
+ export declare function listCatalog(catalog: ProviderCatalog): Promise<CatalogProvider[]>;
77
+ /**
78
+ * Find the route a user's words name.
79
+ *
80
+ * Accepts the full `provider/model`, and a bare model id when exactly one
81
+ * provider offers it — which is what anyone types first, and is unambiguous
82
+ * often enough to be worth supporting. A bare id offered by several providers
83
+ * is reported as ambiguous rather than guessed at.
84
+ *
85
+ * @param input - what followed `/model`.
86
+ * @param providers - the configured catalog.
87
+ */
88
+ export declare function matchRoute(input: string, providers: readonly CatalogProvider[]): {
89
+ readonly kind: 'route';
90
+ readonly route: ModelRoute;
91
+ } | {
92
+ readonly kind: 'ambiguous';
93
+ readonly candidates: readonly string[];
94
+ } | {
95
+ readonly kind: 'unknown';
96
+ };
97
+ /** Render a route the way it is typed back in. */
98
+ export declare function formatRoute(route: ModelRoute | undefined): string;
99
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/session/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAG/D,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;CACzC;AAED,wCAAwC;AACxC,MAAM,WAAW,eAAe;IAC9B,aAAa,IAAI,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzD,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAAA;IAC9D,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;CAC1F;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,UAAU,GAAG,SAAS,GAC5B,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAWpC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAetF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,SAAS,eAAe,EAAE,GAEnC;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACtE;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CA4B/B;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAEjE"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Choosing which model a Telegram conversation talks to.
3
+ *
4
+ * The web UI has a model picker; a phone had nothing, so every conversation
5
+ * ran on whatever the deployment happened to default to. That is the wrong
6
+ * place to be stuck: the reason to reach for a bot from a phone is often that
7
+ * the question is small and a cheaper model would do, or that it is hard and a
8
+ * better one is worth the wait.
9
+ *
10
+ * The choice is per conversation and durable, so it survives `/new` and a
11
+ * restart. It is applied per turn rather than at session creation, because the
12
+ * harness reads a mutable selection while assembling each step — which is what
13
+ * lets a model change take effect on the very next message.
14
+ */
15
+ import { parseRoute } from '../harness/model-selection.js';
16
+ /**
17
+ * The efforts a route offers, and which one it uses by default.
18
+ *
19
+ * Read from the model rather than fixed here: `low`/`medium`/`high` is one
20
+ * provider's vocabulary, not everyone's, and offering an effort a model does
21
+ * not have would fail the turn rather than the command.
22
+ *
23
+ * @param catalog - the harness llm service.
24
+ * @param route - the model to ask about.
25
+ * @returns its reasoning options, or undefined when it has none or cannot say.
26
+ */
27
+ export async function effortsFor(catalog, route) {
28
+ if (!route)
29
+ return undefined;
30
+ try {
31
+ const info = await catalog.resolveModelInfo(route.provider, route.model);
32
+ return info.reasoning !== undefined && info.reasoning.efforts.length > 0
33
+ ? info.reasoning
34
+ : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ /**
41
+ * Find the effort a user's words name.
42
+ *
43
+ * @param input - what followed `/effort`.
44
+ * @param efforts - what the current model offers.
45
+ * @returns the exact effort id, or undefined when nothing matches.
46
+ */
47
+ export function matchEffort(input, efforts) {
48
+ const wanted = input.trim().toLowerCase();
49
+ if (wanted === '')
50
+ return undefined;
51
+ const exact = efforts.find((effort) => effort.id.toLowerCase() === wanted);
52
+ if (exact)
53
+ return exact.id;
54
+ // A display name is what a person reads off the list, so it is what they
55
+ // type back. Accepted only while it names one effort.
56
+ const named = efforts.filter((effort) => (effort.name ?? '').toLowerCase() === wanted);
57
+ return named.length === 1 ? named[0].id : undefined;
58
+ }
59
+ /**
60
+ * Every configured provider with its models, for a message that lists them.
61
+ *
62
+ * A provider whose catalog cannot be read is skipped rather than failing the
63
+ * listing: one misconfigured key should not hide every other model.
64
+ *
65
+ * @param catalog - the harness llm service.
66
+ */
67
+ export async function listCatalog(catalog) {
68
+ const providers = catalog.listProviders();
69
+ const groups = await Promise.all(providers.map(async (provider) => {
70
+ try {
71
+ const models = await catalog.listModels(provider.id);
72
+ return { id: provider.id, ...(provider.name ? { name: provider.name } : {}), models };
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }));
78
+ return groups.filter((group) => group !== undefined && group.models.length > 0);
79
+ }
80
+ /**
81
+ * Find the route a user's words name.
82
+ *
83
+ * Accepts the full `provider/model`, and a bare model id when exactly one
84
+ * provider offers it — which is what anyone types first, and is unambiguous
85
+ * often enough to be worth supporting. A bare id offered by several providers
86
+ * is reported as ambiguous rather than guessed at.
87
+ *
88
+ * @param input - what followed `/model`.
89
+ * @param providers - the configured catalog.
90
+ */
91
+ export function matchRoute(input, providers) {
92
+ const wanted = input.trim();
93
+ if (wanted === '')
94
+ return { kind: 'unknown' };
95
+ const qualified = parseRoute(wanted);
96
+ if (qualified) {
97
+ const provider = providers.find((entry) => entry.id === qualified.provider);
98
+ const known = provider?.models.some((model) => model.id === qualified.model);
99
+ // An unlisted route is still taken when its provider is configured: a
100
+ // catalog can lag behind a model the provider actually serves, and
101
+ // refusing would make this stricter than the web UI's own picker.
102
+ if (provider)
103
+ return { kind: 'route', route: qualified };
104
+ if (known)
105
+ return { kind: 'route', route: qualified };
106
+ }
107
+ const matches = providers.flatMap((provider) => provider.models
108
+ .filter((model) => model.id.toLowerCase() === wanted.toLowerCase())
109
+ .map((model) => `${provider.id}/${model.id}`));
110
+ if (matches.length === 1) {
111
+ const route = parseRoute(matches[0]);
112
+ if (route)
113
+ return { kind: 'route', route };
114
+ }
115
+ if (matches.length > 1)
116
+ return { kind: 'ambiguous', candidates: matches };
117
+ return { kind: 'unknown' };
118
+ }
119
+ /** Render a route the way it is typed back in. */
120
+ export function formatRoute(route) {
121
+ return route === undefined ? 'the deployment default' : `${route.provider}/${route.model}`;
122
+ }
123
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/session/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAkC1D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAwB,EACxB,KAA6B;IAE7B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAE5B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;QACxE,OAAO,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YACtE,CAAC,CAAC,IAAI,CAAC,SAAS;YAChB,CAAC,CAAC,SAAS,CAAA;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,KAAa,EACb,OAAgC;IAEhC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACzC,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IAEnC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAA;IAC1E,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC,EAAE,CAAA;IAE1B,yEAAyE;IACzE,sDAAsD;IACtD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAA;IACtF,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAE,KAAK,CAAC,CAAC,CAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACvE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAwB;IACxD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,EAAE,CAAA;IAEzC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC/B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;YACpD,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;QACvF,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC,CAAC,CACH,CAAA;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAA4B,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAC3G,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CACxB,KAAa,EACb,SAAqC;IAKrC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC3B,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAE7C,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;IACpC,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,QAAQ,CAAC,CAAA;QAC3E,MAAM,KAAK,GAAG,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,KAAK,CAAC,CAAA;QAC5E,sEAAsE;QACtE,mEAAmE;QACnE,kEAAkE;QAClE,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;QACxD,IAAI,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;IACvD,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAC7C,QAAQ,CAAC,MAAM;SACZ,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC;SAClE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE,EAAE,CAAC,CAChD,CAAA;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAW,CAAC,CAAA;QAC9C,IAAI,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAA;IAC5C,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,CAAA;IAEzE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;AAC5B,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,KAA6B;IACvD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,KAAK,EAAE,CAAA;AAC5F,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The permission preset a Telegram conversation runs under.
3
+ *
4
+ * A deployment picks one default for everything it runs, and that choice is
5
+ * usually made for the surface the operator sits in front of: the web UI is
6
+ * loopback-only, with a person watching what the agent does. A Telegram bot is
7
+ * not that. It is reachable from anywhere, gated by a list of user ids, and
8
+ * read on a phone — so the same `danger-full-access` that is merely convenient
9
+ * at a desk is a different proposition over a chat app.
10
+ *
11
+ * This lets the two differ. It is deliberately not a second place to author
12
+ * presets: the names come from the deployment's own table, and all this does
13
+ * is choose among them for conversations that arrive over Telegram.
14
+ *
15
+ * It also decides whether the approval buttons work at all. Under a preset
16
+ * whose approval policy is `never` nothing ever asks, so the buttons this
17
+ * plugin registers can never appear — choosing one that asks is what turns
18
+ * them on.
19
+ */
20
+ import type { ChatTarget } from '../interact/surface.js';
21
+ import type { Logger } from '../harness/types.js';
22
+ /** The deployment's preset table, narrowed to what this needs. */
23
+ export interface PermissionPresetsLike {
24
+ /** Every preset name the deployment defines. */
25
+ readonly names: readonly string[];
26
+ /** Switch one session onto a preset; an unknown name throws. */
27
+ set(session: unknown, name: string): void;
28
+ }
29
+ /** Looking up a live session by id. */
30
+ export interface SessionLookup {
31
+ get(sessionId: string): unknown;
32
+ }
33
+ /** Construction options. */
34
+ export interface PermissionControlOptions {
35
+ readonly presets?: PermissionPresetsLike;
36
+ readonly sessions?: SessionLookup;
37
+ /**
38
+ * The preset this conversation should run under; empty or undefined leaves
39
+ * the deployment default. Read per session so a change lands on the next
40
+ * conversation rather than the next restart.
41
+ */
42
+ readonly preset: (target: ChatTarget) => string | undefined;
43
+ readonly logger?: Logger;
44
+ }
45
+ /**
46
+ * Find the preset a user's words name.
47
+ *
48
+ * Matched loosely against the deployment's own table rather than against a
49
+ * fixed list: the names are the deployment's to choose, and `full access`,
50
+ * `full-access` and `danger-full-access` are all the same request from
51
+ * somebody typing on a phone.
52
+ *
53
+ * @param input - what the user typed.
54
+ * @param names - every preset the deployment defines.
55
+ * @returns the exact table key, or undefined when nothing matches.
56
+ */
57
+ export declare function matchPreset(input: string, names: readonly string[]): string | undefined;
58
+ export declare class PermissionControl {
59
+ private readonly options;
60
+ private readonly logger;
61
+ /** Names already reported as unknown, so one typo is not logged per turn. */
62
+ private readonly complained;
63
+ constructor(options: PermissionControlOptions);
64
+ /** Whether this deployment can be told to use a different preset. */
65
+ get available(): boolean;
66
+ /** Every preset name available, for a message that has to name them. */
67
+ get names(): readonly string[];
68
+ /**
69
+ * Put one session on the configured preset.
70
+ *
71
+ * Applied after the session exists rather than at creation: the harness pins
72
+ * an initial permission before publishing, and switching afterwards is the
73
+ * supported path — it is what the web UI's own picker does mid-session.
74
+ *
75
+ * Failure is never fatal. A session that keeps the deployment default is
76
+ * exactly what an unconfigured deployment gets, so a bad name costs a log
77
+ * line rather than the user's message.
78
+ *
79
+ * @param sessionId - the session to switch.
80
+ */
81
+ apply(target: ChatTarget, sessionId: string): void;
82
+ }
83
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../src/session/permission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAGjD,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,gEAAgE;IAChE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C;AAED,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;CAChC;AAED,4BAA4B;AAC5B,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAA;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,CAAA;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAWvF;AAOD,qBAAa,iBAAiB;IAKhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;gBAElB,OAAO,EAAE,wBAAwB;IAI9D,qEAAqE;IACrE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,wEAAwE;IACxE,IAAI,KAAK,IAAI,SAAS,MAAM,EAAE,CAE7B;IAED;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;CA8BnD"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The permission preset a Telegram conversation runs under.
3
+ *
4
+ * A deployment picks one default for everything it runs, and that choice is
5
+ * usually made for the surface the operator sits in front of: the web UI is
6
+ * loopback-only, with a person watching what the agent does. A Telegram bot is
7
+ * not that. It is reachable from anywhere, gated by a list of user ids, and
8
+ * read on a phone — so the same `danger-full-access` that is merely convenient
9
+ * at a desk is a different proposition over a chat app.
10
+ *
11
+ * This lets the two differ. It is deliberately not a second place to author
12
+ * presets: the names come from the deployment's own table, and all this does
13
+ * is choose among them for conversations that arrive over Telegram.
14
+ *
15
+ * It also decides whether the approval buttons work at all. Under a preset
16
+ * whose approval policy is `never` nothing ever asks, so the buttons this
17
+ * plugin registers can never appear — choosing one that asks is what turns
18
+ * them on.
19
+ */
20
+ import { SILENT_LOGGER } from '../harness/types.js';
21
+ /**
22
+ * Find the preset a user's words name.
23
+ *
24
+ * Matched loosely against the deployment's own table rather than against a
25
+ * fixed list: the names are the deployment's to choose, and `full access`,
26
+ * `full-access` and `danger-full-access` are all the same request from
27
+ * somebody typing on a phone.
28
+ *
29
+ * @param input - what the user typed.
30
+ * @param names - every preset the deployment defines.
31
+ * @returns the exact table key, or undefined when nothing matches.
32
+ */
33
+ export function matchPreset(input, names) {
34
+ const wanted = normalize(input);
35
+ if (wanted === '')
36
+ return undefined;
37
+ const exact = names.find((name) => normalize(name) === wanted);
38
+ if (exact !== undefined)
39
+ return exact;
40
+ // A shorthand is accepted only while it names ONE preset. `read` picking
41
+ // `read-only` is helpful; a prefix shared by two would be a coin toss.
42
+ const partial = names.filter((name) => normalize(name).includes(wanted));
43
+ return partial.length === 1 ? partial[0] : undefined;
44
+ }
45
+ /** Compare names the way somebody types them, not the way they are stored. */
46
+ function normalize(text) {
47
+ return text.trim().toLowerCase().replace(/[\s_-]+/g, '');
48
+ }
49
+ export class PermissionControl {
50
+ options;
51
+ logger;
52
+ /** Names already reported as unknown, so one typo is not logged per turn. */
53
+ complained = new Set();
54
+ constructor(options) {
55
+ this.options = options;
56
+ this.logger = options.logger ?? SILENT_LOGGER;
57
+ }
58
+ /** Whether this deployment can be told to use a different preset. */
59
+ get available() {
60
+ return this.options.presets !== undefined && this.options.sessions !== undefined;
61
+ }
62
+ /** Every preset name available, for a message that has to name them. */
63
+ get names() {
64
+ return this.options.presets?.names ?? [];
65
+ }
66
+ /**
67
+ * Put one session on the configured preset.
68
+ *
69
+ * Applied after the session exists rather than at creation: the harness pins
70
+ * an initial permission before publishing, and switching afterwards is the
71
+ * supported path — it is what the web UI's own picker does mid-session.
72
+ *
73
+ * Failure is never fatal. A session that keeps the deployment default is
74
+ * exactly what an unconfigured deployment gets, so a bad name costs a log
75
+ * line rather than the user's message.
76
+ *
77
+ * @param sessionId - the session to switch.
78
+ */
79
+ apply(target, sessionId) {
80
+ const wanted = this.options.preset(target)?.trim();
81
+ if (!wanted)
82
+ return;
83
+ const { presets, sessions } = this.options;
84
+ if (!presets || !sessions)
85
+ return;
86
+ if (!presets.names.includes(wanted)) {
87
+ if (!this.complained.has(wanted)) {
88
+ this.complained.add(wanted);
89
+ this.logger.warn(`[dsh-telegram] unknown permission preset "${wanted}" ` +
90
+ `(known: ${presets.names.join(', ') || 'none'}); keeping the deployment default`);
91
+ }
92
+ return;
93
+ }
94
+ const session = sessions.get(sessionId);
95
+ if (session === undefined) {
96
+ this.logger.debug(`[dsh-telegram] session '${sessionId}' is not live; permission unchanged`);
97
+ return;
98
+ }
99
+ try {
100
+ presets.set(session, wanted);
101
+ }
102
+ catch (error) {
103
+ this.logger.warn(`[dsh-telegram] could not apply permission preset "${wanted}"`, error);
104
+ }
105
+ }
106
+ }
107
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../../src/session/permission.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Picking up a conversation that `/new` left behind.
3
+ *
4
+ * The harness keeps every session's log, but the binding naming the current
5
+ * one is replaced, so from a phone `/new` is a one-way door: someone who
6
+ * starts a fresh conversation to ask one quick thing loses the one they were
7
+ * in the middle of.
8
+ *
9
+ * Offered as buttons rather than as `/resume 3`, for the same reason the
10
+ * recovery offer is: the list is already on screen, and asking someone to read
11
+ * an index off it and type a number back is asking them to do a computer's
12
+ * job. It also means there is nothing to mistype.
13
+ */
14
+ import type { ChatSurface, ChatTarget } from '../interact/surface.js';
15
+ import type { PendingRegistry } from '../interact/pending.js';
16
+ import type { Logger } from '../harness/types.js';
17
+ import type { ChatHistory } from './history.js';
18
+ /** Everything the picker needs from the rest of the plugin. */
19
+ export interface SessionPickerOptions {
20
+ readonly surface: ChatSurface;
21
+ readonly pending: PendingRegistry<unknown>;
22
+ readonly history: ChatHistory;
23
+ /** The session this conversation is on now, so it is not offered to itself. */
24
+ readonly currentSession: (target: ChatTarget) => string | undefined;
25
+ /** Point the conversation at an existing session. */
26
+ readonly adopt: (target: ChatTarget, sessionId: string) => Promise<void>;
27
+ readonly logger?: Logger;
28
+ }
29
+ export declare class SessionPicker {
30
+ private readonly options;
31
+ private readonly logger;
32
+ constructor(options: SessionPickerOptions);
33
+ /**
34
+ * Offer this chat's earlier conversations.
35
+ *
36
+ * @param target - the conversation asking.
37
+ */
38
+ offer(target: ChatTarget): Promise<void>;
39
+ /**
40
+ * Route one button press.
41
+ *
42
+ * @param data - raw `callback_data` from the update.
43
+ * @returns whether the press belonged to an open picker.
44
+ */
45
+ handleCallback(data: string | undefined): boolean;
46
+ /** Post one message, swallowing delivery failures. */
47
+ private say;
48
+ }
49
+ //# sourceMappingURL=picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/session/picker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAGjD,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,cAAc,CAAA;AAQ5D,+DAA+D;AAC/D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,CAAA;IAC1C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,GAAG,SAAS,CAAA;IACnE,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,qBAAa,aAAa;IAGZ,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;gBAEF,OAAO,EAAE,oBAAoB;IAI1D;;;;OAIG;IACG,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAuD9C;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO;IAajD,sDAAsD;YACxC,GAAG;CAWlB"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Picking up a conversation that `/new` left behind.
3
+ *
4
+ * The harness keeps every session's log, but the binding naming the current
5
+ * one is replaced, so from a phone `/new` is a one-way door: someone who
6
+ * starts a fresh conversation to ask one quick thing loses the one they were
7
+ * in the middle of.
8
+ *
9
+ * Offered as buttons rather than as `/resume 3`, for the same reason the
10
+ * recovery offer is: the list is already on screen, and asking someone to read
11
+ * an index off it and type a number back is asking them to do a computer's
12
+ * job. It also means there is nothing to mistype.
13
+ */
14
+ import { escapeHtml } from '../render/escape.js';
15
+ import { SILENT_LOGGER } from '../harness/types.js';
16
+ /** Prefix marking callback data as belonging to a session picker. */
17
+ const KIND = 's';
18
+ /** How many to offer; Telegram keyboards get unusable long before this. */
19
+ const OFFER = 8;
20
+ export class SessionPicker {
21
+ options;
22
+ logger;
23
+ constructor(options) {
24
+ this.options = options;
25
+ this.logger = options.logger ?? SILENT_LOGGER;
26
+ }
27
+ /**
28
+ * Offer this chat's earlier conversations.
29
+ *
30
+ * @param target - the conversation asking.
31
+ */
32
+ async offer(target) {
33
+ const current = this.options.currentSession(target);
34
+ const past = this.options
35
+ .history.forChat(target)
36
+ .filter((entry) => entry.sessionId !== current)
37
+ .slice(0, OFFER);
38
+ if (past.length === 0) {
39
+ await this.say(target, current === undefined
40
+ ? 'No conversations yet — send a message to start one.'
41
+ : 'This is the only conversation in this chat so far.');
42
+ return;
43
+ }
44
+ const waiter = this.options.pending.open({});
45
+ const keyboard = past.map((entry, index) => [
46
+ {
47
+ text: labelOf(entry),
48
+ callbackData: `${KIND}:${waiter.token}:${index}`,
49
+ },
50
+ ]);
51
+ await this.say(target, [
52
+ '<b>Earlier conversations</b>',
53
+ '',
54
+ 'Pick one to carry on with. The one you are in now stays where it is.',
55
+ ].join('\n'), keyboard);
56
+ const pressed = await waiter.promise;
57
+ if (typeof pressed !== 'string')
58
+ return;
59
+ const index = Number.parseInt(pressed, 10);
60
+ const chosen = past[index];
61
+ if (!chosen)
62
+ return;
63
+ try {
64
+ await this.options.adopt(target, chosen.sessionId);
65
+ await this.say(target, `↩️ Back in <b>${escapeHtml(labelOf(chosen))}</b>.\n\n` +
66
+ `<code>${escapeHtml(chosen.cwd)}</code>`);
67
+ }
68
+ catch (error) {
69
+ this.logger.error('[dsh-telegram] could not adopt an earlier session', error);
70
+ await this.say(target, '⚠️ That conversation could not be reopened.');
71
+ }
72
+ }
73
+ /**
74
+ * Route one button press.
75
+ *
76
+ * @param data - raw `callback_data` from the update.
77
+ * @returns whether the press belonged to an open picker.
78
+ */
79
+ handleCallback(data) {
80
+ if (data === undefined)
81
+ return false;
82
+ const parts = data.split(':');
83
+ if (parts.length !== 3 || parts[0] !== KIND)
84
+ return false;
85
+ const token = parts[1];
86
+ const index = parts[2];
87
+ if (token === '' || !/^\d+$/.test(index))
88
+ return false;
89
+ return this.options.pending.settle(token, index);
90
+ }
91
+ /** Post one message, swallowing delivery failures. */
92
+ async say(target, html, keyboard) {
93
+ try {
94
+ await this.options.surface.send(target, html, keyboard);
95
+ }
96
+ catch (error) {
97
+ this.logger.warn('[dsh-telegram] could not offer the session list', error);
98
+ }
99
+ }
100
+ }
101
+ /** A button label: what the conversation opened with, or when it started. */
102
+ function labelOf(entry) {
103
+ if (entry.label !== undefined && entry.label !== '')
104
+ return entry.label;
105
+ return new Date(entry.startedAt).toISOString().slice(0, 16).replace('T', ' ');
106
+ }
107
+ //# sourceMappingURL=picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker.js","sourceRoot":"","sources":["../../src/session/picker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * One durable string a conversation chose for itself.
3
+ *
4
+ * Two things need this and they are the same shape: the directory a chat works
5
+ * in, and the model it talks to. Both are per conversation, both must outlive
6
+ * `/new` — a preference discarded with the session would silently snap back to
7
+ * the deployment default on the next message — and both must outlive a
8
+ * restart.
9
+ *
10
+ * Kept apart from the session binding for exactly that reason: a binding is
11
+ * one conversation with the agent and dies with `/new`, while a preference
12
+ * belongs to the CHAT.
13
+ */
14
+ import type { ChatTarget } from '../interact/surface.js';
15
+ /** Construction options. */
16
+ export interface ChatPreferenceOptions {
17
+ /**
18
+ * Whether a stored value is still usable.
19
+ *
20
+ * Applied on read rather than only on write, because the file outlives the
21
+ * process that wrote it: a path that was absolute, or a model that was
22
+ * configured, may be neither by the time it is read back.
23
+ */
24
+ readonly accept: (value: string) => boolean;
25
+ }
26
+ export declare class ChatPreferences {
27
+ private readonly file;
28
+ private readonly accept;
29
+ private values;
30
+ private constructor();
31
+ /**
32
+ * Load the store, or start empty when there is nothing readable yet.
33
+ *
34
+ * A corrupt file costs the conversation its preference, which one command
35
+ * restores; refusing to start would cost the whole bot.
36
+ *
37
+ * @param file - absolute path to the JSON document.
38
+ * @param options - how to tell a usable value from one to drop.
39
+ */
40
+ static open(file: string, options: ChatPreferenceOptions): Promise<ChatPreferences>;
41
+ /** What this conversation chose, if it chose anything. */
42
+ forChat(target: ChatTarget): string | undefined;
43
+ /**
44
+ * Remember a conversation's choice.
45
+ *
46
+ * @param target - the conversation.
47
+ * @param value - the value, already checked by the caller.
48
+ */
49
+ set(target: ChatTarget, value: string): Promise<void>;
50
+ /** Forget it, returning the conversation to the deployment default. */
51
+ clear(target: ChatTarget): Promise<void>;
52
+ /** Write the document atomically, so a crash cannot leave a half file. */
53
+ private persist;
54
+ }
55
+ //# sourceMappingURL=preferences.d.ts.map