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,368 @@
1
+ import type { InboundMessage } from '../../types.ts';
2
+
3
+ export interface FeishuEventSummary {
4
+ messageId: string;
5
+ conversationId: string;
6
+ chatType: string;
7
+ messageType: string;
8
+ senderType: string;
9
+ openId: string;
10
+ userId: string;
11
+ unionId: string;
12
+ }
13
+
14
+ interface CollectTextOptions {
15
+ includeMediaPlaceholders?: boolean;
16
+ parseJsonStrings?: boolean;
17
+ }
18
+
19
+ const DEFAULT_COLLECT_OPTIONS: Required<CollectTextOptions> = {
20
+ includeMediaPlaceholders: true,
21
+ parseJsonStrings: false,
22
+ };
23
+
24
+ const MERGE_FORWARD_COLLECT_OPTIONS: Required<CollectTextOptions> = {
25
+ includeMediaPlaceholders: false,
26
+ parseJsonStrings: true,
27
+ };
28
+
29
+ function parsePayload(content: string): unknown {
30
+ try {
31
+ return JSON.parse(content || '{}');
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ function looksLikeJson(value: string): boolean {
38
+ const trimmed = value.trim();
39
+ return (trimmed.startsWith('{') && trimmed.endsWith('}'))
40
+ || (trimmed.startsWith('[') && trimmed.endsWith(']'));
41
+ }
42
+
43
+ function asRecord(value: unknown): Record<string, unknown> | null {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
45
+ return null;
46
+ }
47
+ return value as Record<string, unknown>;
48
+ }
49
+
50
+ function firstNonEmptyString(...values: unknown[]): string {
51
+ for (const value of values) {
52
+ if (typeof value === 'string' && value.trim()) {
53
+ return value.trim();
54
+ }
55
+ }
56
+ return '';
57
+ }
58
+
59
+ function pushSegment(segments: string[], value: string): void {
60
+ const trimmed = value.trim();
61
+ if (!trimmed) {
62
+ return;
63
+ }
64
+ if (segments[segments.length - 1] === trimmed) {
65
+ return;
66
+ }
67
+ segments.push(trimmed);
68
+ }
69
+
70
+ function collectRichTextSegments(value: unknown, options: CollectTextOptions = DEFAULT_COLLECT_OPTIONS): string[] {
71
+ const resolvedOptions = {
72
+ ...DEFAULT_COLLECT_OPTIONS,
73
+ ...options,
74
+ };
75
+
76
+ if (!value) {
77
+ return [];
78
+ }
79
+ if (typeof value === 'string') {
80
+ if (resolvedOptions.parseJsonStrings && looksLikeJson(value)) {
81
+ return collectRichTextSegments(parsePayload(value), resolvedOptions);
82
+ }
83
+ const trimmed = value.trim();
84
+ return trimmed ? [trimmed] : [];
85
+ }
86
+ if (Array.isArray(value)) {
87
+ const segments: string[] = [];
88
+ for (const item of value) {
89
+ for (const segment of collectRichTextSegments(item, resolvedOptions)) {
90
+ pushSegment(segments, segment);
91
+ }
92
+ }
93
+ return segments;
94
+ }
95
+ if (typeof value !== 'object') {
96
+ return [];
97
+ }
98
+
99
+ const record = value as Record<string, unknown>;
100
+ if (record.post && typeof record.post === 'object') {
101
+ return collectRichTextSegments(record.post, resolvedOptions);
102
+ }
103
+
104
+ const tag = typeof record.tag === 'string' ? record.tag : '';
105
+ if (tag === 'text') {
106
+ return collectRichTextSegments(record.text, resolvedOptions);
107
+ }
108
+ if (tag === 'md' || tag === 'markdown' || tag === 'lark_md') {
109
+ return collectRichTextSegments(record.text ?? record.content, resolvedOptions);
110
+ }
111
+ if (tag === 'a') {
112
+ const segments: string[] = [];
113
+ for (const part of collectRichTextSegments(record.text, resolvedOptions)) {
114
+ pushSegment(segments, part);
115
+ }
116
+ if (typeof record.href === 'string') {
117
+ pushSegment(segments, record.href);
118
+ }
119
+ return segments;
120
+ }
121
+ if (tag === 'at') {
122
+ if (typeof record.user_name === 'string' && record.user_name.trim()) {
123
+ return [`@${record.user_name.trim()}`];
124
+ }
125
+ if (typeof record.user_id === 'string' && record.user_id.trim()) {
126
+ return [`@${record.user_id.trim()}`];
127
+ }
128
+ return collectRichTextSegments(record.text, resolvedOptions);
129
+ }
130
+ if (tag === 'img') {
131
+ return resolvedOptions.includeMediaPlaceholders ? ['[image]'] : [];
132
+ }
133
+ if (tag === 'media') {
134
+ return resolvedOptions.includeMediaPlaceholders ? ['[media]'] : [];
135
+ }
136
+ if (tag === 'emotion') {
137
+ if (typeof record.emoji_type === 'string' && record.emoji_type.trim()) {
138
+ return [`:${record.emoji_type.trim()}:`];
139
+ }
140
+ return ['[emotion]'];
141
+ }
142
+
143
+ const segments: string[] = [];
144
+ for (const part of collectRichTextSegments(record.title, resolvedOptions)) {
145
+ pushSegment(segments, part);
146
+ }
147
+ for (const part of collectRichTextSegments(record.text, resolvedOptions)) {
148
+ pushSegment(segments, part);
149
+ }
150
+ if (typeof record.href === 'string') {
151
+ pushSegment(segments, record.href);
152
+ }
153
+ if (typeof record.url === 'string') {
154
+ pushSegment(segments, record.url);
155
+ }
156
+ for (const part of collectRichTextSegments(record.content, resolvedOptions)) {
157
+ pushSegment(segments, part);
158
+ }
159
+ for (const part of collectRichTextSegments(record.elements, resolvedOptions)) {
160
+ pushSegment(segments, part);
161
+ }
162
+
163
+ for (const [key, nested] of Object.entries(record)) {
164
+ if (['post', 'title', 'text', 'content', 'elements', 'tag', 'href', 'url'].includes(key)) {
165
+ continue;
166
+ }
167
+ for (const part of collectRichTextSegments(nested, resolvedOptions)) {
168
+ pushSegment(segments, part);
169
+ }
170
+ }
171
+ return segments;
172
+ }
173
+
174
+ function pushCollectedSegments(target: string[], value: unknown, options: CollectTextOptions): void {
175
+ for (const segment of collectRichTextSegments(value, options)) {
176
+ pushSegment(target, segment);
177
+ }
178
+ }
179
+
180
+ function extractStructuredTextFromValue(value: unknown, messageType = 'text', options: CollectTextOptions = DEFAULT_COLLECT_OPTIONS): string {
181
+ if (typeof value === 'string') {
182
+ const payload = parsePayload(value) as Record<string, unknown>;
183
+ const text = typeof payload.text === 'string'
184
+ ? payload.text
185
+ : typeof payload.content === 'string'
186
+ ? payload.content
187
+ : '';
188
+
189
+ if (messageType === 'interactive') {
190
+ const interactive = collectRichTextSegments(payload.elements ?? payload, options);
191
+ return interactive.join('\n').trim() || text.trim();
192
+ }
193
+
194
+ if (text.trim()) {
195
+ return text.trim();
196
+ }
197
+ return collectRichTextSegments(payload, options).join('\n').trim();
198
+ }
199
+
200
+ if (messageType === 'interactive') {
201
+ return collectRichTextSegments(value, options).join('\n').trim();
202
+ }
203
+ return collectRichTextSegments(value, options).join('\n').trim();
204
+ }
205
+
206
+ function extractMergeForwardItemText(value: unknown): string {
207
+ const item = asRecord(value);
208
+ if (!item) {
209
+ return collectRichTextSegments(value, MERGE_FORWARD_COLLECT_OPTIONS).join('\n').trim();
210
+ }
211
+
212
+ const body = asRecord(item.body);
213
+ const sender = firstNonEmptyString(
214
+ item.sender_name,
215
+ item.senderName,
216
+ body?.sender_name,
217
+ body?.senderName,
218
+ asRecord(item.sender)?.sender_name,
219
+ asRecord(item.sender)?.name,
220
+ asRecord(item.from)?.sender_name,
221
+ asRecord(item.from)?.name,
222
+ );
223
+ const messageType = firstNonEmptyString(
224
+ item.msg_type,
225
+ item.message_type,
226
+ body?.msg_type,
227
+ body?.message_type,
228
+ ) || 'text';
229
+
230
+ const text = extractStructuredTextFromValue(
231
+ body?.content ?? item.content ?? body?.elements ?? item.elements ?? body?.text ?? item.text,
232
+ messageType,
233
+ MERGE_FORWARD_COLLECT_OPTIONS,
234
+ );
235
+ if (text) {
236
+ return sender ? `${sender}: ${text}` : text;
237
+ }
238
+
239
+ const fallbackSegments: string[] = [];
240
+ pushCollectedSegments(fallbackSegments, item.title, MERGE_FORWARD_COLLECT_OPTIONS);
241
+ pushCollectedSegments(fallbackSegments, item.preview, MERGE_FORWARD_COLLECT_OPTIONS);
242
+ pushCollectedSegments(fallbackSegments, item.summary, MERGE_FORWARD_COLLECT_OPTIONS);
243
+ return fallbackSegments.join('\n').trim();
244
+ }
245
+
246
+ function extractMergeForwardText(value: unknown): string {
247
+ const payload = typeof value === 'string' ? parsePayload(value) : value;
248
+ const record = asRecord(payload);
249
+ if (!record) {
250
+ return collectRichTextSegments(payload, MERGE_FORWARD_COLLECT_OPTIONS).join('\n').trim();
251
+ }
252
+
253
+ const segments: string[] = [];
254
+ pushCollectedSegments(segments, record.title, MERGE_FORWARD_COLLECT_OPTIONS);
255
+ pushCollectedSegments(segments, record.summary, MERGE_FORWARD_COLLECT_OPTIONS);
256
+ pushCollectedSegments(segments, record.preview, MERGE_FORWARD_COLLECT_OPTIONS);
257
+ pushCollectedSegments(segments, record.preview_list, MERGE_FORWARD_COLLECT_OPTIONS);
258
+
259
+ const collections = [record.message_list, record.messages, record.items, record.messageList, record.messageListData];
260
+ for (const collection of collections) {
261
+ if (!Array.isArray(collection)) {
262
+ continue;
263
+ }
264
+ for (const item of collection) {
265
+ const itemText = extractMergeForwardItemText(item);
266
+ if (itemText) {
267
+ pushSegment(segments, itemText);
268
+ }
269
+ }
270
+ }
271
+
272
+ if (segments.length > 0) {
273
+ return segments.join('\n').trim();
274
+ }
275
+
276
+ return extractStructuredTextFromValue(
277
+ record.content ?? record.elements ?? record.post ?? record.preview,
278
+ 'text',
279
+ MERGE_FORWARD_COLLECT_OPTIONS,
280
+ );
281
+ }
282
+
283
+ function unwrapEventEnvelope(event: any): any {
284
+ if (event?.event && typeof event.event === 'object') {
285
+ return event.event;
286
+ }
287
+ return event;
288
+ }
289
+
290
+ function readMessage(event: any): any {
291
+ return unwrapEventEnvelope(event)?.message;
292
+ }
293
+
294
+ function readSender(event: any): any {
295
+ return unwrapEventEnvelope(event)?.sender;
296
+ }
297
+
298
+ export function summarizeFeishuEvent(event: any): FeishuEventSummary {
299
+ const message = readMessage(event);
300
+ const sender = readSender(event);
301
+ const senderId = sender?.sender_id || {};
302
+ return {
303
+ messageId: message?.message_id || '',
304
+ conversationId: message?.chat_id || '',
305
+ chatType: message?.chat_type || '',
306
+ messageType: message?.message_type || '',
307
+ senderType: sender?.sender_type || '',
308
+ openId: senderId?.open_id || '',
309
+ userId: senderId?.user_id || '',
310
+ unionId: senderId?.union_id || '',
311
+ };
312
+ }
313
+
314
+ export function extractTextFromPayload(content: string, messageType = 'text'): string {
315
+ if (messageType === 'merge_forward') {
316
+ return extractMergeForwardText(content);
317
+ }
318
+ return extractStructuredTextFromValue(content, messageType, DEFAULT_COLLECT_OPTIONS);
319
+ }
320
+
321
+ export function isSupportedChatType(chatType: string | undefined): boolean {
322
+ return chatType === 'p2p' || chatType === 'single' || chatType === 'group';
323
+ }
324
+
325
+ export function extractFeishuMessageId(event: any): string {
326
+ return readMessage(event)?.message_id || '';
327
+ }
328
+
329
+ export function normalizeFeishuEvent(event: any, allowedUserIds: string[]): InboundMessage | null {
330
+ const message = readMessage(event);
331
+ const sender = readSender(event);
332
+ const senderId = sender?.sender_id?.open_id || '';
333
+ const messageId = message?.message_id || '';
334
+ const conversationId = message?.chat_id || '';
335
+ const messageType = message?.message_type || 'text';
336
+
337
+ if (sender?.sender_type === 'bot') {
338
+ return null;
339
+ }
340
+ if (!senderId || !messageId || !conversationId) {
341
+ return null;
342
+ }
343
+ if (!isSupportedChatType(message.chat_type)) {
344
+ return null;
345
+ }
346
+ if (allowedUserIds.length > 0 && !allowedUserIds.includes(senderId)) {
347
+ return null;
348
+ }
349
+
350
+ return {
351
+ messageId,
352
+ conversationRef: {
353
+ channel: 'feishu',
354
+ conversationId,
355
+ userId: senderId,
356
+ chatType: message.chat_type,
357
+ },
358
+ senderId,
359
+ senderName: sender?.sender_name || null,
360
+ text: extractTextFromPayload(message.content || '{}', messageType),
361
+ sourceMessageType: messageType,
362
+ replyToMessageId: message?.parent_id || null,
363
+ rootMessageId: message?.root_id || null,
364
+ replyToText: null,
365
+ receivedAt: new Date().toISOString(),
366
+ raw: event,
367
+ };
368
+ }
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ function resolveCodexHome(): string {
6
+ const explicit = process.env.CODEX_HOME;
7
+ if (explicit && explicit.trim()) {
8
+ return explicit.trim();
9
+ }
10
+ return path.join(os.homedir(), '.codex');
11
+ }
12
+
13
+ function redactLine(line: string): string {
14
+ const sensitivePattern = /(secret|token|api[_-]?key|password|credential|bearer|authorization)/i;
15
+ if (!line.includes('=') || !sensitivePattern.test(line)) {
16
+ return line;
17
+ }
18
+ const index = line.indexOf('=');
19
+ const key = line.slice(0, index + 1);
20
+ return `${key} "[REDACTED]"`;
21
+ }
22
+
23
+ function trimContent(content: string, maxLines = 200): string {
24
+ const lines = content.split(/\r?\n/);
25
+ if (lines.length <= maxLines) {
26
+ return content.trimEnd();
27
+ }
28
+ const kept = lines.slice(0, maxLines).join('\n').trimEnd();
29
+ return `${kept}\n... (truncated, total ${lines.length} lines)`;
30
+ }
31
+
32
+ export function loadCodexConfigPreview(): string {
33
+ const codexHome = resolveCodexHome();
34
+ const configPath = path.join(codexHome, 'config.toml');
35
+ if (!fs.existsSync(configPath)) {
36
+ return [
37
+ '未找到 Codex 配置文件。',
38
+ `path: ${configPath}`,
39
+ '如果你还没配置过 Codex,这是正常的。',
40
+ ].join('\n');
41
+ }
42
+
43
+ const raw = fs.readFileSync(configPath, 'utf8');
44
+ const sanitized = trimContent(raw.split(/\r?\n/).map((line) => redactLine(line)).join('\n'));
45
+ return [
46
+ '当前仅做只读查看;敏感字段已脱敏。',
47
+ `path: ${configPath}`,
48
+ '```toml',
49
+ sanitized,
50
+ '```',
51
+ ].join('\n');
52
+ }
@@ -0,0 +1,240 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, loadDotEnv, mergeEnv, resolveTimeZone } from './utils.ts';
3
+
4
+
5
+ export type CodexApprovalPolicy = 'untrusted' | 'on-failure' | 'on-request' | 'never';
6
+ export type CodexSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
7
+
8
+ export interface McpToolApprovalRule {
9
+ serverName: string;
10
+ toolName: string;
11
+ }
12
+
13
+ export interface WorkAllyConfig {
14
+ workspaceRoot: string;
15
+ stateDir: string;
16
+ implementationDir: string;
17
+ assistant: {
18
+ mode: 'assistant' | 'native';
19
+ name?: string;
20
+ assistantHome?: string;
21
+ codexHome?: string;
22
+ };
23
+ envFile: string;
24
+ runtime: {
25
+ mode: 'codex' | 'fake';
26
+ approvalPolicy: CodexApprovalPolicy;
27
+ sandboxMode: CodexSandboxMode;
28
+ mcpToolApprovalRules: McpToolApprovalRule[];
29
+ };
30
+ timezone: string;
31
+ channel: {
32
+ impl: 'feishu' | 'fake';
33
+ allowedUserIds: string[];
34
+ };
35
+ progressHeartbeatSeconds: number;
36
+ feishu: {
37
+ appId?: string;
38
+ appSecret?: string;
39
+ reactEmoji: string;
40
+ };
41
+ defaultPushTarget?: string;
42
+ logging: {
43
+ retentionDays: number;
44
+ };
45
+ paths: {
46
+ runtimeDir: string;
47
+ logsDir: string;
48
+ sessionsDir: string;
49
+ workSessionsDir: string;
50
+ runsDir: string;
51
+ cacheDir: string;
52
+ archiveDir: string;
53
+ dailyMemoryDir: string;
54
+ longTermMemoryDir: string;
55
+ routinesDir: string;
56
+ projectRoutinesDir: string;
57
+ conversationsDir: string;
58
+ assistantHome?: string;
59
+ };
60
+ }
61
+
62
+
63
+ const DEFAULT_MCP_TOOL_APPROVAL_ALLOWLIST = 'feishu:*';
64
+ const APPROVAL_POLICIES = new Set<CodexApprovalPolicy>(['untrusted', 'on-failure', 'on-request', 'never']);
65
+ const SANDBOX_MODES = new Set<CodexSandboxMode>(['read-only', 'workspace-write', 'danger-full-access']);
66
+
67
+ function parseList(value: string | undefined): string[] {
68
+ return (value ?? '')
69
+ .split(',')
70
+ .map((item) => item.trim())
71
+ .filter(Boolean);
72
+ }
73
+
74
+ function parseApprovalPolicy(value: string | undefined): CodexApprovalPolicy {
75
+ const normalized = (value || 'on-request').trim() as CodexApprovalPolicy;
76
+ if (!APPROVAL_POLICIES.has(normalized)) {
77
+ throw new Error(`Invalid WORK_ALLY_CODEX_APPROVAL_POLICY: ${value}`);
78
+ }
79
+ return normalized;
80
+ }
81
+
82
+ function parseSandboxMode(value: string | undefined): CodexSandboxMode {
83
+ const normalized = (value || 'workspace-write').trim() as CodexSandboxMode;
84
+ if (!SANDBOX_MODES.has(normalized)) {
85
+ throw new Error(`Invalid WORK_ALLY_CODEX_SANDBOX_MODE: ${value}`);
86
+ }
87
+ return normalized;
88
+ }
89
+
90
+ function normalizeRulePart(value: string): string {
91
+ return value.trim().toLowerCase();
92
+ }
93
+
94
+ export function parseMcpToolApprovalRules(value: string | undefined): McpToolApprovalRule[] {
95
+ const raw = (value ?? DEFAULT_MCP_TOOL_APPROVAL_ALLOWLIST).trim();
96
+ if (!raw) {
97
+ return parseMcpToolApprovalRules(DEFAULT_MCP_TOOL_APPROVAL_ALLOWLIST);
98
+ }
99
+
100
+ if (['none', 'manual', '0'].includes(raw.toLowerCase())) {
101
+ return [];
102
+ }
103
+
104
+ return raw
105
+ .split(',')
106
+ .map((item) => item.trim())
107
+ .filter(Boolean)
108
+ .map((item) => {
109
+ const separator = item.indexOf(':');
110
+ const serverName = normalizeRulePart(separator === -1 ? item : item.slice(0, separator));
111
+ const toolName = normalizeRulePart(separator === -1 ? '*' : item.slice(separator + 1));
112
+ if (!serverName || !toolName) {
113
+ throw new Error(`Invalid WORK_ALLY_MCP_TOOL_APPROVAL_ALLOWLIST entry: ${item}`);
114
+ }
115
+ return { serverName, toolName };
116
+ });
117
+ }
118
+
119
+ export function formatMcpToolApprovalRules(rules: McpToolApprovalRule[]): string {
120
+ if (!rules.length) {
121
+ return 'manual all';
122
+ }
123
+ return `allow ${rules.map((rule) => `${rule.serverName}:${rule.toolName}`).join(', ')}; manual others`;
124
+ }
125
+
126
+ export function validateConfig(config: WorkAllyConfig): void {
127
+ new Intl.DateTimeFormat('en-US', { timeZone: config.timezone }).format(new Date());
128
+
129
+ if (!APPROVAL_POLICIES.has(config.runtime.approvalPolicy)) {
130
+ throw new Error(`Invalid WORK_ALLY_CODEX_APPROVAL_POLICY: ${config.runtime.approvalPolicy}`);
131
+ }
132
+
133
+ if (!SANDBOX_MODES.has(config.runtime.sandboxMode)) {
134
+ throw new Error(`Invalid WORK_ALLY_CODEX_SANDBOX_MODE: ${config.runtime.sandboxMode}`);
135
+ }
136
+
137
+ if (!Array.isArray(config.runtime.mcpToolApprovalRules)) {
138
+ throw new Error('Invalid WORK_ALLY_MCP_TOOL_APPROVAL_ALLOWLIST');
139
+ }
140
+
141
+ for (const rule of config.runtime.mcpToolApprovalRules) {
142
+ if (!rule?.serverName || !rule?.toolName) {
143
+ throw new Error('Invalid WORK_ALLY_MCP_TOOL_APPROVAL_ALLOWLIST');
144
+ }
145
+ }
146
+
147
+ if (config.channel.impl === 'feishu') {
148
+ if (!config.feishu.appId) {
149
+ throw new Error('FEISHU_APP_ID is required');
150
+ }
151
+ if (!config.feishu.appSecret) {
152
+ throw new Error('FEISHU_APP_SECRET is required');
153
+ }
154
+ }
155
+ }
156
+
157
+ export function loadConfig(): WorkAllyConfig {
158
+ const workspaceRoot = path.resolve(process.env.WORK_ALLY_WORKSPACE_ROOT || process.cwd());
159
+ const assistantHomeRaw = process.env.WORK_ALLY_ASSISTANT_HOME;
160
+ const defaultStateDir = assistantHomeRaw
161
+ ? path.join(assistantHomeRaw, '.system')
162
+ : path.join(workspaceRoot, '.work-ally');
163
+ const stateDir = path.resolve(process.env.WORK_ALLY_STATE_DIR || defaultStateDir);
164
+ const implementationDir = path.resolve(process.env.WORK_ALLY_IMPLEMENTATION_DIR || process.cwd());
165
+ const envFile = path.resolve(process.env.WORK_ALLY_ENV_FILE || path.join(stateDir, assistantHomeRaw ? 'config.env' : '.env'));
166
+ mergeEnv(loadDotEnv(envFile));
167
+
168
+ const assistantHome = assistantHomeRaw || undefined;
169
+ const stateRoot = stateDir;
170
+ const paths = assistantHome
171
+ ? {
172
+ runtimeDir: ensureDir(path.join(stateRoot, 'runtime')),
173
+ logsDir: ensureDir(path.join(stateRoot, 'logs')),
174
+ sessionsDir: ensureDir(path.join(stateRoot, 'runtime', 'sessions')),
175
+ workSessionsDir: ensureDir(path.join(stateRoot, 'runtime', 'work-sessions')),
176
+ runsDir: ensureDir(path.join(stateRoot, 'runs')),
177
+ cacheDir: ensureDir(path.join(stateRoot, 'cache')),
178
+ archiveDir: ensureDir(path.join(stateRoot, 'archive')),
179
+ dailyMemoryDir: ensureDir(path.join(assistantHome, 'journal')),
180
+ longTermMemoryDir: ensureDir(assistantHome),
181
+ routinesDir: ensureDir(path.join(stateRoot, 'routines')),
182
+ projectRoutinesDir: ensureDir(path.join(workspaceRoot, 'routines')),
183
+ conversationsDir: ensureDir(path.join(assistantHome, 'conversations')),
184
+ assistantHome,
185
+ }
186
+ : {
187
+ runtimeDir: ensureDir(path.join(stateRoot, 'runtime')),
188
+ logsDir: ensureDir(path.join(stateRoot, 'logs')),
189
+ sessionsDir: ensureDir(path.join(stateRoot, 'sessions')),
190
+ workSessionsDir: ensureDir(path.join(stateRoot, 'work-sessions')),
191
+ runsDir: ensureDir(path.join(stateRoot, 'runs')),
192
+ cacheDir: ensureDir(path.join(stateRoot, 'cache')),
193
+ archiveDir: ensureDir(path.join(workspaceRoot, 'memory/archive')),
194
+ dailyMemoryDir: ensureDir(path.join(workspaceRoot, 'memory/daily')),
195
+ longTermMemoryDir: ensureDir(path.join(workspaceRoot, 'memory/long-term')),
196
+ routinesDir: ensureDir(path.join(workspaceRoot, 'routines')),
197
+ projectRoutinesDir: ensureDir(path.join(workspaceRoot, 'routines')),
198
+ conversationsDir: ensureDir(path.join(stateRoot, 'sessions')),
199
+ assistantHome: undefined,
200
+ };
201
+
202
+
203
+ const config: WorkAllyConfig = {
204
+ workspaceRoot,
205
+ stateDir,
206
+ implementationDir,
207
+ assistant: {
208
+ mode: ((process.env.WORK_ALLY_ASSISTANT_MODE as 'assistant' | 'native') || (assistantHome ? 'assistant' : 'native')),
209
+ name: process.env.WORK_ALLY_ASSISTANT_NAME || undefined,
210
+ assistantHome,
211
+ codexHome: process.env.WORK_ALLY_ASSISTANT_CODEX_HOME || process.env.CODEX_HOME || undefined,
212
+ },
213
+ envFile,
214
+ runtime: {
215
+ mode: (process.env.WORK_ALLY_RUNTIME_MODE as 'codex' | 'fake') || 'codex',
216
+ approvalPolicy: parseApprovalPolicy(process.env.WORK_ALLY_CODEX_APPROVAL_POLICY),
217
+ sandboxMode: parseSandboxMode(process.env.WORK_ALLY_CODEX_SANDBOX_MODE),
218
+ mcpToolApprovalRules: parseMcpToolApprovalRules(process.env.WORK_ALLY_MCP_TOOL_APPROVAL_ALLOWLIST),
219
+ },
220
+ timezone: resolveTimeZone(process.env.WORK_ALLY_TIMEZONE),
221
+ channel: {
222
+ impl: (process.env.WORK_ALLY_CHANNEL_IMPL as 'feishu' | 'fake') || 'feishu',
223
+ allowedUserIds: parseList(process.env.WORK_ALLY_ALLOWED_USER_IDS),
224
+ },
225
+ progressHeartbeatSeconds: Number(process.env.WORK_ALLY_PROGRESS_HEARTBEAT_SECONDS || '60'),
226
+ feishu: {
227
+ appId: process.env.FEISHU_APP_ID,
228
+ appSecret: process.env.FEISHU_APP_SECRET,
229
+ reactEmoji: process.env.FEISHU_REACT_EMOJI || 'Typing',
230
+ },
231
+ defaultPushTarget: process.env.WORK_ALLY_DEFAULT_PUSH_TARGET || undefined,
232
+ logging: {
233
+ retentionDays: Number(process.env.WORK_ALLY_LOG_RETENTION_DAYS || '3'),
234
+ },
235
+ paths,
236
+ };
237
+
238
+ validateConfig(config);
239
+ return config;
240
+ }