vibe-coding-master 0.2.6 → 0.2.7

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.
@@ -0,0 +1,214 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
4
+ const MAX_DEDUPE_IDS = 1000;
5
+ export function createGatewaySettingsService(deps) {
6
+ const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "gateway", "settings.json");
7
+ const auditPath = deps.auditPath ?? path.join(homedir(), ".vcm", "gateway", "audit.jsonl");
8
+ const now = deps.now ?? (() => new Date().toISOString());
9
+ let cachedSettings = null;
10
+ async function loadSettings() {
11
+ if (cachedSettings) {
12
+ return cachedSettings;
13
+ }
14
+ if (!(await deps.fs.pathExists(settingsPath))) {
15
+ cachedSettings = normalizeSettings({}, now());
16
+ await saveSettings(cachedSettings);
17
+ return cachedSettings;
18
+ }
19
+ cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now());
20
+ return cachedSettings;
21
+ }
22
+ async function saveSettings(settings) {
23
+ cachedSettings = normalizeSettings(settings, now());
24
+ await deps.fs.writeJsonAtomic(settingsPath, cachedSettings);
25
+ return cachedSettings;
26
+ }
27
+ return {
28
+ loadSettings,
29
+ async updateSettings(input) {
30
+ const current = await loadSettings();
31
+ return saveSettings({
32
+ ...current,
33
+ enabled: input.enabled ?? current.enabled,
34
+ translationEnabled: input.translationEnabled ?? current.translationEnabled,
35
+ currentProjectId: input.currentProjectId !== undefined ? normalizeNullableString(input.currentProjectId) : current.currentProjectId,
36
+ currentTaskSlug: input.currentTaskSlug !== undefined ? normalizeNullableString(input.currentTaskSlug) : current.currentTaskSlug,
37
+ updatedAt: now()
38
+ });
39
+ },
40
+ saveSettings,
41
+ async resetBinding() {
42
+ const current = await loadSettings();
43
+ return saveSettings({
44
+ ...current,
45
+ enabled: false,
46
+ binding: createDefaultBinding(),
47
+ dedupe: {
48
+ recentInboundMessageIds: []
49
+ },
50
+ pushCursors: {},
51
+ lastPollStatus: {
52
+ state: "idle"
53
+ },
54
+ lastMessageStatus: null,
55
+ updatedAt: now()
56
+ });
57
+ },
58
+ expose(settings, running = false) {
59
+ return {
60
+ version: 1,
61
+ enabled: settings.enabled,
62
+ running,
63
+ channel: settings.channel,
64
+ translationEnabled: settings.translationEnabled,
65
+ currentProjectId: settings.currentProjectId,
66
+ currentTaskSlug: settings.currentTaskSlug,
67
+ binding: {
68
+ accountId: settings.binding.accountId,
69
+ baseUrl: settings.binding.baseUrl,
70
+ boundUserId: settings.binding.boundUserId,
71
+ loginUserId: settings.binding.loginUserId,
72
+ tokenConfigured: Boolean(settings.binding.token)
73
+ },
74
+ pendingConfirmations: settings.pendingConfirmations,
75
+ lastPollStatus: settings.lastPollStatus,
76
+ lastMessageStatus: settings.lastMessageStatus,
77
+ updatedAt: settings.updatedAt
78
+ };
79
+ },
80
+ getSettingsPath() {
81
+ return settingsPath;
82
+ },
83
+ getAuditPath() {
84
+ return auditPath;
85
+ }
86
+ };
87
+ }
88
+ export function normalizeSettings(input, timestamp) {
89
+ const bindingInput = isObject(input.binding) ? input.binding : {};
90
+ const dedupeInput = isObject(input.dedupe) ? input.dedupe : {};
91
+ const pendingInput = isObject(input.pendingConfirmations)
92
+ ? input.pendingConfirmations
93
+ : {};
94
+ const pushCursors = isObject(input.pushCursors) ? input.pushCursors : {};
95
+ return {
96
+ version: 1,
97
+ enabled: input.enabled === true,
98
+ channel: "weixin-ilink",
99
+ translationEnabled: input.translationEnabled !== false,
100
+ currentProjectId: normalizeNullableString(input.currentProjectId),
101
+ currentTaskSlug: normalizeNullableString(input.currentTaskSlug),
102
+ binding: {
103
+ ...createDefaultBinding(),
104
+ accountId: normalizeNullableString(bindingInput.accountId),
105
+ baseUrl: normalizeBaseUrl(bindingInput.baseUrl),
106
+ boundUserId: normalizeNullableString(bindingInput.boundUserId),
107
+ loginUserId: normalizeNullableString(bindingInput.loginUserId),
108
+ token: normalizeNullableString(bindingInput.token),
109
+ getUpdatesBuf: typeof bindingInput.getUpdatesBuf === "string" ? bindingInput.getUpdatesBuf : "",
110
+ contextTokens: isObject(bindingInput.contextTokens)
111
+ ? normalizeStringRecord(bindingInput.contextTokens)
112
+ : {}
113
+ },
114
+ dedupe: {
115
+ recentInboundMessageIds: Array.isArray(dedupeInput.recentInboundMessageIds)
116
+ ? dedupeInput.recentInboundMessageIds.filter((value) => typeof value === "string").slice(-MAX_DEDUPE_IDS)
117
+ : []
118
+ },
119
+ pendingConfirmations: {
120
+ closeTask: normalizeCloseTaskConfirmation(pendingInput.closeTask)
121
+ },
122
+ pushCursors: normalizePushCursors(pushCursors),
123
+ lastPollStatus: normalizePollStatus(input.lastPollStatus),
124
+ lastMessageStatus: normalizeMessageStatus(input.lastMessageStatus),
125
+ updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : timestamp
126
+ };
127
+ }
128
+ function createDefaultBinding() {
129
+ return {
130
+ accountId: null,
131
+ baseUrl: DEFAULT_BASE_URL,
132
+ boundUserId: null,
133
+ loginUserId: null,
134
+ token: null,
135
+ getUpdatesBuf: "",
136
+ contextTokens: {}
137
+ };
138
+ }
139
+ function normalizeCloseTaskConfirmation(input) {
140
+ if (!isObject(input)) {
141
+ return null;
142
+ }
143
+ const taskSlug = normalizeNullableString(input.taskSlug);
144
+ const createdAt = normalizeNullableString(input.createdAt);
145
+ const expiresAt = normalizeNullableString(input.expiresAt);
146
+ if (!taskSlug || !createdAt || !expiresAt) {
147
+ return null;
148
+ }
149
+ return { taskSlug, createdAt, expiresAt };
150
+ }
151
+ function normalizePushCursors(input) {
152
+ const out = {};
153
+ for (const [key, value] of Object.entries(input)) {
154
+ if (!isObject(value)) {
155
+ continue;
156
+ }
157
+ out[key] = {
158
+ lastTranscriptEventId: normalizeNullableString(value.lastTranscriptEventId),
159
+ lastTranscriptTimestamp: normalizeNullableString(value.lastTranscriptTimestamp)
160
+ };
161
+ }
162
+ return out;
163
+ }
164
+ function normalizePollStatus(input) {
165
+ if (!isObject(input)) {
166
+ return { state: "idle" };
167
+ }
168
+ const state = input.state === "running" || input.state === "error" || input.state === "expired"
169
+ ? input.state
170
+ : "idle";
171
+ return {
172
+ state,
173
+ checkedAt: normalizeNullableString(input.checkedAt) ?? undefined,
174
+ error: normalizeNullableString(input.error) ?? undefined
175
+ };
176
+ }
177
+ function normalizeMessageStatus(input) {
178
+ if (!isObject(input)) {
179
+ return null;
180
+ }
181
+ return {
182
+ checkedAt: normalizeNullableString(input.checkedAt) ?? undefined,
183
+ direction: input.direction === "inbound" || input.direction === "outbound" ? input.direction : undefined,
184
+ command: normalizeNullableString(input.command) ?? undefined,
185
+ result: input.result === "ok" || input.result === "ignored" || input.result === "error" ? input.result : undefined,
186
+ preview: normalizeNullableString(input.preview) ?? undefined,
187
+ error: normalizeNullableString(input.error) ?? undefined
188
+ };
189
+ }
190
+ function normalizeBaseUrl(input) {
191
+ const raw = typeof input === "string" ? input.trim() : "";
192
+ if (!raw) {
193
+ return DEFAULT_BASE_URL;
194
+ }
195
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
196
+ return raw.replace(/\/+$/, "");
197
+ }
198
+ return `https://${raw.replace(/\/+$/, "")}`;
199
+ }
200
+ function normalizeNullableString(input) {
201
+ return typeof input === "string" && input.trim() ? input.trim() : null;
202
+ }
203
+ function normalizeStringRecord(input) {
204
+ const out = {};
205
+ for (const [key, value] of Object.entries(input)) {
206
+ if (typeof value === "string") {
207
+ out[key] = value;
208
+ }
209
+ }
210
+ return out;
211
+ }
212
+ function isObject(value) {
213
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
214
+ }
@@ -14,6 +14,11 @@ import { createHarnessService, createScriptFixedHarnessInstaller } from "./servi
14
14
  import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
15
15
  import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
16
16
  import { createOpenAiCompatibleTranslationProvider } from "./adapters/translation-provider.js";
17
+ import { registerGatewayRoutes } from "./api/gateway-routes.js";
18
+ import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channel.js";
19
+ import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
20
+ import { createGatewayService } from "./gateway/gateway-service.js";
21
+ import { createGatewaySettingsService } from "./gateway/gateway-settings-service.js";
17
22
  import { createProjectService } from "./services/project-service.js";
18
23
  import { createSessionRegistry } from "./runtime/session-registry.js";
19
24
  import { createSessionService } from "./services/session-service.js";
@@ -90,7 +95,14 @@ export async function createServer(deps, options = {}) {
90
95
  taskService: deps.taskService,
91
96
  translationService: deps.translationService
92
97
  });
98
+ registerGatewayRoutes(app, { gatewayService: deps.gatewayService });
93
99
  registerTerminalWs(app, { runtime: deps.runtime });
100
+ app.addHook("onReady", async () => {
101
+ await deps.gatewayService.start();
102
+ });
103
+ app.addHook("onClose", async () => {
104
+ deps.gatewayService.stop();
105
+ });
94
106
  if (options.staticDir) {
95
107
  await app.register(fastifyStatic, {
96
108
  root: options.staticDir,
@@ -162,7 +174,12 @@ export function createDefaultServerDeps(options = {}) {
162
174
  sessionService,
163
175
  taskService
164
176
  });
165
- const roundService = createRoundService({ fs });
177
+ const roundService = createRoundService({
178
+ fs,
179
+ onSessionStatusChange: async ({ repoRoot, taskSlug, status }) => {
180
+ await taskService.updateTaskStatus(repoRoot, taskSlug, status);
181
+ }
182
+ });
166
183
  const transcripts = createClaudeTranscriptService();
167
184
  const translationService = createTranslationService({
168
185
  runtime,
@@ -174,6 +191,25 @@ export function createDefaultServerDeps(options = {}) {
174
191
  appSettings,
175
192
  provider: createOpenAiCompatibleTranslationProvider()
176
193
  });
194
+ const gatewaySettings = createGatewaySettingsService({ fs });
195
+ const gatewayAudit = createGatewayAuditLog({
196
+ fs,
197
+ auditPath: gatewaySettings.getAuditPath()
198
+ });
199
+ const gatewayService = createGatewayService({
200
+ fs,
201
+ settings: gatewaySettings,
202
+ audit: gatewayAudit,
203
+ channel: createWeixinIlinkChannel(),
204
+ projectService,
205
+ taskService,
206
+ sessionService,
207
+ messageService,
208
+ translationService,
209
+ roundService,
210
+ runtime,
211
+ appSettings
212
+ });
177
213
  const claudeHookService = createClaudeHookService({
178
214
  projectService,
179
215
  taskService,
@@ -181,7 +217,8 @@ export function createDefaultServerDeps(options = {}) {
181
217
  messageService,
182
218
  roundService,
183
219
  translationService,
184
- appSettings
220
+ appSettings,
221
+ gatewayService
185
222
  });
186
223
  return {
187
224
  appSettings,
@@ -196,6 +233,7 @@ export function createDefaultServerDeps(options = {}) {
196
233
  roundService,
197
234
  statusService,
198
235
  translationService,
236
+ gatewayService,
199
237
  runtime
200
238
  };
201
239
  }
@@ -43,6 +43,7 @@ export function createClaudeHookService(deps) {
43
43
  cwd: stringOrUndefined(input.event.cwd)
44
44
  });
45
45
  await deps.roundService.recordClaudeHookEvent({
46
+ repoRoot: context.project.repoRoot,
46
47
  stateRepoRoot: context.taskRepoRoot,
47
48
  stateRoot: context.config.stateRoot,
48
49
  taskSlug: input.taskSlug,
@@ -57,7 +58,7 @@ export function createClaudeHookService(deps) {
57
58
  role: input.role,
58
59
  sessionId: session.id,
59
60
  boundaryKind: "start",
60
- occurredAt: session.lastPromptSubmittedAt ?? session.updatedAt
61
+ occurredAt: session.lastTurnStartedAt ?? session.updatedAt
61
62
  });
62
63
  }
63
64
  const submitted = await deps.messageService.confirmPromptSubmitted({
@@ -112,6 +113,7 @@ export function createClaudeHookService(deps) {
112
113
  cwd: stringOrUndefined(input.event.cwd)
113
114
  });
114
115
  await deps.roundService.recordClaudeHookEvent({
116
+ repoRoot: context.project.repoRoot,
115
117
  stateRepoRoot: context.taskRepoRoot,
116
118
  stateRoot: context.config.stateRoot,
117
119
  taskSlug: input.taskSlug,
@@ -120,12 +122,12 @@ export function createClaudeHookService(deps) {
120
122
  settleGuard: async () => {
121
123
  const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
122
124
  if (pending.length === 0) {
123
- return { action: "pause" };
125
+ return { action: "stop" };
124
126
  }
125
127
  const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
126
128
  return retried.some((result) => result.delivered)
127
129
  ? { action: "continue", reason: "pending route message dispatched" }
128
- : { action: "pause" };
130
+ : { action: "stop" };
129
131
  }
130
132
  });
131
133
  if (session) {
@@ -136,9 +138,16 @@ export function createClaudeHookService(deps) {
136
138
  role: input.role,
137
139
  sessionId: session.id,
138
140
  boundaryKind: "end",
139
- occurredAt: session.lastStopAt ?? session.updatedAt
141
+ occurredAt: session.lastTurnEndedAt ?? session.updatedAt
140
142
  });
141
143
  }
144
+ if (session && input.role === "project-manager") {
145
+ void deps.gatewayService?.handlePmStop({
146
+ repoRoot: context.project.repoRoot,
147
+ taskSlug: input.taskSlug,
148
+ session
149
+ }).catch(() => undefined);
150
+ }
142
151
  const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput);
143
152
  return {
144
153
  ok: true,