work-ally 0.2.0-alpha.1

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 (172) hide show
  1. package/AGENTS.md +110 -0
  2. package/DASHBOARD.md +160 -0
  3. package/PRODUCT.md +113 -0
  4. package/README.md +403 -0
  5. package/ally.sh +171 -0
  6. package/bridge/src/approval-rules.ts +360 -0
  7. package/bridge/src/channel-delivery.ts +207 -0
  8. package/bridge/src/channel-types.ts +22 -0
  9. package/bridge/src/channels/fake/adapter.ts +31 -0
  10. package/bridge/src/channels/feishu/adapter.ts +411 -0
  11. package/bridge/src/channels/feishu/approvals.ts +6 -0
  12. package/bridge/src/channels/feishu/formatter.ts +276 -0
  13. package/bridge/src/channels/feishu/normalize.ts +368 -0
  14. package/bridge/src/codex-config.ts +52 -0
  15. package/bridge/src/config.ts +240 -0
  16. package/bridge/src/fake-runtime-client.ts +505 -0
  17. package/bridge/src/handoff-service.ts +494 -0
  18. package/bridge/src/logger.ts +194 -0
  19. package/bridge/src/memory-digest.ts +186 -0
  20. package/bridge/src/receiver-approval-autonomy.ts +158 -0
  21. package/bridge/src/receiver-control-core.ts +140 -0
  22. package/bridge/src/receiver-control-work-session.ts +218 -0
  23. package/bridge/src/receiver-control.ts +83 -0
  24. package/bridge/src/receiver-delivery.ts +136 -0
  25. package/bridge/src/receiver-helpers.ts +96 -0
  26. package/bridge/src/receiver-human-gate.ts +333 -0
  27. package/bridge/src/receiver-inbound-preflight.ts +162 -0
  28. package/bridge/src/receiver-recovery.ts +236 -0
  29. package/bridge/src/receiver-runtime-callbacks.ts +367 -0
  30. package/bridge/src/receiver-runtime-policy.ts +132 -0
  31. package/bridge/src/receiver-runtime-state.ts +124 -0
  32. package/bridge/src/receiver-support-actions.ts +189 -0
  33. package/bridge/src/receiver-thread-start.ts +57 -0
  34. package/bridge/src/receiver-turn-coordination.ts +94 -0
  35. package/bridge/src/receiver-turn-execution.ts +257 -0
  36. package/bridge/src/receiver-turn-failure.ts +143 -0
  37. package/bridge/src/receiver-turn-result.ts +185 -0
  38. package/bridge/src/receiver-turn-steer.ts +70 -0
  39. package/bridge/src/receiver-work-session.ts +76 -0
  40. package/bridge/src/receiver.ts +329 -0
  41. package/bridge/src/router.ts +62 -0
  42. package/bridge/src/runtime-client-agent-messages.ts +150 -0
  43. package/bridge/src/runtime-client-message-dispatch.ts +176 -0
  44. package/bridge/src/runtime-client-protocol.ts +411 -0
  45. package/bridge/src/runtime-client-request-ops.ts +56 -0
  46. package/bridge/src/runtime-client-run-turn.ts +158 -0
  47. package/bridge/src/runtime-client-thread-ops.ts +270 -0
  48. package/bridge/src/runtime-client-transport.ts +309 -0
  49. package/bridge/src/runtime-client-turn-poll.ts +224 -0
  50. package/bridge/src/runtime-client-turn-read.ts +185 -0
  51. package/bridge/src/runtime-client-turn-state.ts +105 -0
  52. package/bridge/src/runtime-client.ts +344 -0
  53. package/bridge/src/runtime-user-input.ts +403 -0
  54. package/bridge/src/scheduler.ts +239 -0
  55. package/bridge/src/server-handoff-command.ts +364 -0
  56. package/bridge/src/server-main.ts +80 -0
  57. package/bridge/src/server-routine-command.ts +60 -0
  58. package/bridge/src/server-routine-execution.ts +222 -0
  59. package/bridge/src/server-runtime-app-support.ts +107 -0
  60. package/bridge/src/server-runtime-app.ts +238 -0
  61. package/bridge/src/server-thread-sync-command.ts +63 -0
  62. package/bridge/src/server.ts +17 -0
  63. package/bridge/src/session-store-delivery.ts +220 -0
  64. package/bridge/src/session-store-human-gate.ts +380 -0
  65. package/bridge/src/session-store-inbound-acceptance.ts +66 -0
  66. package/bridge/src/session-store-meta.ts +134 -0
  67. package/bridge/src/session-store-turn-ledger.ts +272 -0
  68. package/bridge/src/session-store.ts +380 -0
  69. package/bridge/src/system-notify.ts +220 -0
  70. package/bridge/src/thread-sync.ts +200 -0
  71. package/bridge/src/translator.ts +494 -0
  72. package/bridge/src/types.ts +289 -0
  73. package/bridge/src/utils.ts +104 -0
  74. package/bridge/src/work-session-store.ts +471 -0
  75. package/docs/.gitkeep +0 -0
  76. package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
  77. package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
  78. package/docs/completed/README.md +21 -0
  79. package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
  80. package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
  81. package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
  82. package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
  83. package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
  84. package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
  85. package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
  86. package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
  87. package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
  88. package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
  89. package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
  90. package/docs/developer-workflow.md +49 -0
  91. package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
  92. package/docs/implementation/test-coverage-map.md +363 -0
  93. package/docs/implementation/work-ally-implementation-guide.md +790 -0
  94. package/docs/issues/README.md +10 -0
  95. package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
  96. package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
  97. package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
  98. package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
  99. package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
  100. package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
  101. package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
  102. package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
  103. package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
  104. package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
  105. package/docs/manual-acceptance.md +127 -0
  106. package/docs/ops-runbook.md +44 -0
  107. package/docs/planning/FEATURE-memory-system.md +748 -0
  108. package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
  109. package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
  110. package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
  111. package/docs/planning/SPEC-assistant-rename.md +610 -0
  112. package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
  113. package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
  114. package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
  115. package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
  116. package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
  117. package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
  118. package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
  119. package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
  120. package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
  121. package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
  122. package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
  123. package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
  124. package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
  125. package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
  126. package/docs/planning/SPEC-stable-archive-contract.md +456 -0
  127. package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
  128. package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
  129. package/docs/planning/ally-next.md +1278 -0
  130. package/docs/planning/assistant-workbench-spec.md +725 -0
  131. package/docs/planning/product-workbench.md +283 -0
  132. package/docs/product-onboarding.md +227 -0
  133. package/docs/product-spec-standard.md +528 -0
  134. package/docs/troubleshooting.md +45 -0
  135. package/docs/user-quickstart.md +46 -0
  136. package/internal/dispatch.sh +95 -0
  137. package/internal/lib/common.sh +1450 -0
  138. package/internal/modules/assistant/manage.sh +1312 -0
  139. package/internal/modules/bootstrap/setup.sh +144 -0
  140. package/internal/modules/config/init-env.sh +10 -0
  141. package/internal/modules/global/manage.sh +154 -0
  142. package/internal/modules/handoff/manage.sh +54 -0
  143. package/internal/modules/mcp/manage.sh +83 -0
  144. package/internal/modules/ops/logs.sh +76 -0
  145. package/internal/modules/routines/manage.sh +55 -0
  146. package/internal/modules/runtime/assistant-autosave.sh +26 -0
  147. package/internal/modules/runtime/restart.sh +6 -0
  148. package/internal/modules/runtime/start.sh +283 -0
  149. package/internal/modules/runtime/status.sh +194 -0
  150. package/internal/modules/runtime/stop.sh +55 -0
  151. package/internal/modules/runtime/supervisor.sh +216 -0
  152. package/internal/modules/runtime/update.sh +26 -0
  153. package/package.json +41 -0
  154. package/runtime/config/.gitkeep +0 -0
  155. package/runtime/host/.gitkeep +0 -0
  156. package/runtime/host/healthcheck-codex-app-server.ts +22 -0
  157. package/runtime/host/ping-pong-codex-app-server.ts +66 -0
  158. package/runtime/host/probe-codex-app-server.ts +115 -0
  159. package/skills/archive-reader/SKILL.md +9 -0
  160. package/skills/feishu-production-debug/SKILL.md +37 -0
  161. package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
  162. package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
  163. package/skills/issue-to-spec-triage/SKILL.md +44 -0
  164. package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
  165. package/skills/memory-digest/SKILL.md +9 -0
  166. package/skills/post-implementation-closure/SKILL.md +39 -0
  167. package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
  168. package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
  169. package/skills/product-spec/SKILL.md +244 -0
  170. package/templates/env.example +5 -0
  171. package/templates/routines/nightly-memory-digest.yaml +10 -0
  172. package/templates/workspace/AGENTS.md +26 -0
@@ -0,0 +1,289 @@
1
+ export type ApprovalKind = 'command' | 'file_change';
2
+ export type ApprovalDecision = 'accept' | 'acceptForSession' | 'decline' | 'cancel';
3
+ export type RuntimeProgressKind = 'agent_message' | 'plan' | 'command' | 'context_compaction';
4
+ export interface RuntimeProgressMeta {
5
+ kind?: RuntimeProgressKind;
6
+ phase?: 'commentary' | 'final_answer' | 'unknown' | null;
7
+ turnId?: string | null;
8
+ itemId?: string | null;
9
+ }
10
+ export type RuntimeThreadActiveFlag = 'waitingOnApproval' | 'waitingOnUserInput';
11
+ export type RuntimeThreadStatusType = 'notLoaded' | 'idle' | 'systemError' | 'active';
12
+ export type PendingUserInputKind = 'questionnaire' | 'mcp_elicitation';
13
+
14
+ export interface RuntimeThreadStatus {
15
+ type: RuntimeThreadStatusType;
16
+ activeFlags: RuntimeThreadActiveFlag[];
17
+ }
18
+
19
+ export interface RuntimeErrorDetails extends Error {
20
+ code?: string | number;
21
+ data?: unknown;
22
+ }
23
+
24
+ export interface ConversationRef {
25
+ channel: string;
26
+ conversationId: string;
27
+ userId?: string | null;
28
+ chatType?: string | null;
29
+ }
30
+
31
+ export interface InboundAttachment {
32
+ kind: 'image' | 'file' | 'audio' | 'video';
33
+ path?: string;
34
+ token?: string;
35
+ name?: string;
36
+ }
37
+
38
+ export interface InboundMessage {
39
+ messageId: string;
40
+ conversationRef: ConversationRef;
41
+ senderId: string;
42
+ senderName?: string | null;
43
+ text: string;
44
+ sourceMessageType?: string | null;
45
+ replyToMessageId?: string | null;
46
+ rootMessageId?: string | null;
47
+ replyToText?: string | null;
48
+ attachments?: InboundAttachment[];
49
+ receivedAt: string;
50
+ raw?: unknown;
51
+ }
52
+
53
+ export interface SessionMeta {
54
+ conversationRef: ConversationRef;
55
+ sessionKey: string;
56
+ createdAt: string;
57
+ updatedAt: string;
58
+ threadId: string | null;
59
+ activeTurnId: string | null;
60
+ lastTurnId: string | null;
61
+ lastMessageId: string | null;
62
+ lastPromptPreview: string | null;
63
+ lastReplyPreview: string | null;
64
+ pendingApprovalIds: string[];
65
+ pendingUserInputRequestId: string | null;
66
+ }
67
+
68
+ export type WorkSessionOrigin = 'work_ally' | 'codex_cli_attach';
69
+ export type WorkSessionActiveSurface = 'work_ally_channel' | 'official_codex_cli' | 'idle' | 'closed';
70
+ export type WorkSessionOwnershipSource =
71
+ | 'explicit_channel_takeover'
72
+ | 'explicit_codex_launch'
73
+ | 'observed_external_activity'
74
+ | 'manual_attach'
75
+ | 'new_session';
76
+ export type WorkSessionResumeCapability = 'ready' | 'not_ready';
77
+ export type WorkSessionCliResumeRefType = 'session_id' | 'thread_name' | 'unknown' | 'null';
78
+
79
+ export interface WorkSessionMeta {
80
+ workSessionId: string;
81
+ threadHandle: string;
82
+ assistantName: string;
83
+ assistantCodexHome: string | null;
84
+ workspaceRoot: string;
85
+ runtime: 'codex';
86
+ runtimeThreadId: string;
87
+ cliResumeRef: string | null;
88
+ cliResumeRefType: WorkSessionCliResumeRefType;
89
+ threadName: string | null;
90
+ origin: WorkSessionOrigin;
91
+ deliveryChannel: string | null;
92
+ deliveryConversationKey: string | null;
93
+ activeSurface: WorkSessionActiveSurface;
94
+ ownershipSource: WorkSessionOwnershipSource;
95
+ resumeCapability: WorkSessionResumeCapability;
96
+ lastRuntimeActivityAt: string | null;
97
+ lastSurfaceSwitchAt: string;
98
+ createdAt: string;
99
+ updatedAt: string;
100
+ archivedAt: string | null;
101
+ lastTurnId?: string | null;
102
+ lastPromptPreview?: string | null;
103
+ lastReplyPreview?: string | null;
104
+ lastHandoffExportAt?: string | null;
105
+ lastAttachAt?: string | null;
106
+ }
107
+
108
+ export interface WorkSessionAssistantIndex {
109
+ assistantName: string;
110
+ activeWorkSessionId: string | null;
111
+ updatedAt: string;
112
+ }
113
+
114
+ export interface WorkSessionDeliveryIndex {
115
+ deliveryChannel: string;
116
+ deliveryConversationKey: string;
117
+ activeWorkSessionId: string | null;
118
+ updatedAt: string;
119
+ }
120
+
121
+ export interface UserInputOption {
122
+ label: string;
123
+ description: string;
124
+ }
125
+
126
+ export interface UserInputQuestion {
127
+ id: string;
128
+ header: string;
129
+ question: string;
130
+ isOther?: boolean;
131
+ isSecret?: boolean;
132
+ options?: UserInputOption[] | null;
133
+ }
134
+
135
+ export interface McpElicitationRequest {
136
+ serverName: string;
137
+ mode: 'form' | 'url';
138
+ message: string;
139
+ requestedSchema?: unknown;
140
+ url?: string | null;
141
+ elicitationId?: string | null;
142
+ }
143
+
144
+ export interface UserInputRequest {
145
+ requestId: string | number;
146
+ threadId: string;
147
+ turnId: string;
148
+ itemId: string | null;
149
+ messageId: string;
150
+ kind?: PendingUserInputKind;
151
+ questions: UserInputQuestion[];
152
+ elicitation?: McpElicitationRequest | null;
153
+ }
154
+
155
+ export interface PendingUserInputRecord extends UserInputRequest {
156
+ createdAt: string;
157
+ status: 'pending';
158
+ visible: boolean;
159
+ }
160
+
161
+ export interface InboundReceipt {
162
+ messageId: string;
163
+ status: 'processing' | 'completed';
164
+ createdAt: string;
165
+ updatedAt: string;
166
+ outcome?: 'success' | 'failed';
167
+ }
168
+
169
+ export interface ApprovalRecord {
170
+ approvalId: string;
171
+ kind: ApprovalKind;
172
+ requestId: string | number;
173
+ turnId: string;
174
+ threadId: string;
175
+ messageId: string;
176
+ command?: string | null;
177
+ reason?: string | null;
178
+ grantRoot?: string | null;
179
+ cwd?: string | null;
180
+ createdAt: string;
181
+ status: 'pending' | 'accepted' | 'declined' | 'cancelled';
182
+ visible: boolean;
183
+ }
184
+
185
+ export type TurnExecutionStatus =
186
+ | 'running'
187
+ | 'completed'
188
+ | 'failed'
189
+ | 'interrupted';
190
+
191
+ export type TurnDeliveryStatus = 'not_ready' | 'pending' | 'delivered' | 'suppressed' | 'delivery_unavailable';
192
+ export type InboundAcceptanceStatus = 'received' | TurnExecutionStatus;
193
+
194
+ export interface InboundAcceptanceRecord {
195
+ messageId: string;
196
+ threadId: string | null;
197
+ turnId: string | null;
198
+ createdAt: string;
199
+ updatedAt: string;
200
+ acceptedAt: string | null;
201
+ executionStatus: InboundAcceptanceStatus;
202
+ }
203
+
204
+ export interface TurnRecord {
205
+ turnId: string;
206
+ threadId: string;
207
+ status: string;
208
+ prompt: string;
209
+ reply: string;
210
+ startedAt: string;
211
+ completedAt: string;
212
+ error?: string | null;
213
+ }
214
+
215
+ export interface TurnLedgerRecord {
216
+ turnId: string;
217
+ threadId: string;
218
+ messageId: string | null;
219
+ promptPreview: string | null;
220
+ createdAt: string;
221
+ updatedAt: string;
222
+ executionStatus: TurnExecutionStatus;
223
+ replyPreview: string | null;
224
+ error: string | null;
225
+ deliveryStatus: TurnDeliveryStatus;
226
+ deliveredAt: string | null;
227
+ suppressedByMessageId: string | null;
228
+ suppressedByTurnId: string | null;
229
+ suppressionReason: string | null;
230
+ }
231
+
232
+ export interface RuntimeTurnResult {
233
+ threadId: string;
234
+ turnId: string;
235
+ status: string;
236
+ reply: string;
237
+ error?: string | null;
238
+ }
239
+
240
+ export interface OutboundDeliveryRecord {
241
+ sourceKey: string;
242
+ threadId: string;
243
+ turnId: string;
244
+ conversationRef: ConversationRef;
245
+ envelopeType: 'final_reply';
246
+ payloadText: string;
247
+ payloadDigest: string;
248
+ createdAt: string;
249
+ updatedAt: string;
250
+ status: 'pending' | 'delivered';
251
+ deliveredAt: string | null;
252
+ }
253
+
254
+ export type ConnectionLifecycleState = 'disconnected' | 'reconnecting' | 'reconnected';
255
+ export type ConnectionLifecycleSource = 'runtime' | 'feishu';
256
+
257
+ export interface ConnectionLifecycleEvent {
258
+ source: ConnectionLifecycleSource;
259
+ state: ConnectionLifecycleState;
260
+ reason?: string | null;
261
+ attempt?: number | null;
262
+ nextRetryDelayMs?: number | null;
263
+ hasActiveTurn?: boolean | null;
264
+ }
265
+
266
+ export interface ApprovalRequest {
267
+ approvalId: string;
268
+ kind: ApprovalKind;
269
+ requestId: string | number;
270
+ threadId: string;
271
+ turnId: string;
272
+ messageId: string;
273
+ command?: string | null;
274
+ reason?: string | null;
275
+ grantRoot?: string | null;
276
+ cwd?: string | null;
277
+ boundaryReason?: string | null;
278
+ autonomyDecision?: {
279
+ action: 'auto_approve';
280
+ reason: string;
281
+ policy: string;
282
+ } | null;
283
+ }
284
+
285
+ export interface ServerRequestResolvedEvent {
286
+ requestId: string | number;
287
+ threadId: string | null;
288
+ turnId: string | null;
289
+ }
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import crypto from 'node:crypto';
5
+
6
+ export function ensureDir(target: string): string {
7
+ fs.mkdirSync(target, { recursive: true });
8
+ return target;
9
+ }
10
+
11
+ export function readJsonFile(filePath: string, fallback = null) {
12
+ if (!fs.existsSync(filePath)) {
13
+ return fallback;
14
+ }
15
+ try {
16
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
17
+ } catch {
18
+ return fallback;
19
+ }
20
+ }
21
+
22
+ export function writeJsonFile(filePath: string, value: unknown): void {
23
+ ensureDir(path.dirname(filePath));
24
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
25
+ }
26
+
27
+ export function appendNdjson(filePath: string, value: unknown): void {
28
+ ensureDir(path.dirname(filePath));
29
+ fs.appendFileSync(filePath, `${JSON.stringify(value)}\n`, 'utf8');
30
+ }
31
+
32
+ export function nowIso(): string {
33
+ return new Date().toISOString();
34
+ }
35
+
36
+ export function resolveTimeZone(explicit?: string | null): string {
37
+ const detected = explicit || Intl.DateTimeFormat().resolvedOptions().timeZone;
38
+ return detected || 'UTC';
39
+ }
40
+
41
+ export function todayStamp(date = new Date(), timeZone?: string): string {
42
+ const zone = resolveTimeZone(timeZone);
43
+ const parts = new Intl.DateTimeFormat('en-CA', {
44
+ timeZone: zone,
45
+ year: 'numeric',
46
+ month: '2-digit',
47
+ day: '2-digit',
48
+ }).formatToParts(date);
49
+ const values = Object.fromEntries(parts.filter((item) => item.type !== 'literal').map((item) => [item.type, item.value]));
50
+ return `${values.year}-${values.month}-${values.day}`;
51
+ }
52
+
53
+ export function sanitizeSegment(input: string): string {
54
+ return input.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120) || 'default';
55
+ }
56
+
57
+ export function conversationKey(channel: string, conversationId: string): string {
58
+ return `${sanitizeSegment(channel)}--${sanitizeSegment(conversationId)}`;
59
+ }
60
+
61
+ export function loadDotEnv(filePath: string): Record<string, string> {
62
+ const result: Record<string, string> = {};
63
+ if (!fs.existsSync(filePath)) {
64
+ return result;
65
+ }
66
+ const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
67
+ for (const line of lines) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed || trimmed.startsWith('#')) continue;
70
+ const eq = trimmed.indexOf('=');
71
+ if (eq < 0) continue;
72
+ const key = trimmed.slice(0, eq).trim();
73
+ let value = trimmed.slice(eq + 1).trim();
74
+ const commentIndex = value.search(/\s+#/);
75
+ if (commentIndex >= 0) {
76
+ value = value.slice(0, commentIndex).trim();
77
+ }
78
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
79
+ value = value.slice(1, -1);
80
+ }
81
+ result[key] = value;
82
+ }
83
+ return result;
84
+ }
85
+
86
+ export function mergeEnv(values: Record<string, string>): void {
87
+ for (const [key, value] of Object.entries(values)) {
88
+ if (!(key in process.env)) {
89
+ process.env[key] = value;
90
+ }
91
+ }
92
+ }
93
+
94
+ export function randomId(prefix: string): string {
95
+ return `${prefix}_${crypto.randomUUID()}`;
96
+ }
97
+
98
+ export function sleep(ms: number): Promise<void> {
99
+ return new Promise((resolve) => setTimeout(resolve, ms));
100
+ }
101
+
102
+ export function tempDir(prefix: string): string {
103
+ return fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
104
+ }