@amityco/foundry-mcp 0.1.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.
- package/README.md +50 -0
- package/dist/server.js +106 -0
- package/dist/tools/docs.js +255 -0
- package/dist/tools/harness.js +246 -0
- package/dist/tools/integration.js +544 -0
- package/dist/tools/project.js +571 -0
- package/dist/tools/resolve.js +193 -0
- package/dist/tools/sensors.js +185 -0
- package/dist/types.js +31 -0
- package/package.json +55 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
4
|
+
import { detectCommandSensors } from "./harness.js";
|
|
5
|
+
import { inspectProject } from "./project.js";
|
|
6
|
+
export const planIntegrationTool = {
|
|
7
|
+
name: "plan_integration",
|
|
8
|
+
description: "Create a grounded, evidence-backed implementation packet before an AI coding agent edits a customer project.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
repoPath: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Absolute or relative path to the customer repository root.",
|
|
15
|
+
},
|
|
16
|
+
request: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Natural-language integration request.",
|
|
19
|
+
},
|
|
20
|
+
surfacePath: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Optional app/workspace path inside repoPath, such as apps/web or apps/mobile.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["repoPath", "request"],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
async call(input) {
|
|
29
|
+
const args = objectInput(input);
|
|
30
|
+
const repoPath = stringField(args, "repoPath");
|
|
31
|
+
const request = stringField(args, "request");
|
|
32
|
+
return textResult(await buildIntegrationPlan(repoPath, request, optionalStringField(args, "surfacePath")));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
async function buildIntegrationPlan(repoPath, request, surfacePath) {
|
|
36
|
+
const repoRoot = path.resolve(repoPath);
|
|
37
|
+
const inspection = await inspectProject(repoRoot, surfacePath);
|
|
38
|
+
const root = inspection.effectiveRoot;
|
|
39
|
+
const outcome = classifyOutcome(request);
|
|
40
|
+
const platform = preferredPlatform(inspection.platforms);
|
|
41
|
+
const supportLevel = supportFor(outcome, platform);
|
|
42
|
+
const sensors = await detectCommandSensors(root, inspection.platforms);
|
|
43
|
+
return {
|
|
44
|
+
outcome,
|
|
45
|
+
platform,
|
|
46
|
+
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
47
|
+
availableSurfaces: inspection.surfaces,
|
|
48
|
+
supportLevel,
|
|
49
|
+
intent: intentFor(request, outcome),
|
|
50
|
+
intake: intakeFor(request, outcome, inspection.designSignals),
|
|
51
|
+
docs: docsFor(outcome, platform),
|
|
52
|
+
requiredInputs: requiredInputsFor(request, outcome, platform, inspection.designSignals),
|
|
53
|
+
targetFiles: await targetFilesFor(root, outcome, platform, inspection.designSignals),
|
|
54
|
+
implementationRules: implementationRulesFor(request, outcome, platform),
|
|
55
|
+
implementationSteps: implementationStepsFor(outcome, platform, inspection.designSignals),
|
|
56
|
+
validation: validationFor(outcome, platform),
|
|
57
|
+
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
58
|
+
stopConditions: stopConditionsFor(request, outcome, platform, inspection.platforms, inspection.designSignals, inspection.surfaces, surfacePath),
|
|
59
|
+
evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function classifyOutcome(request) {
|
|
63
|
+
const normalized = request.toLowerCase();
|
|
64
|
+
if (/\b(push|notification|firebase|fcm|apns)\b/.test(normalized)) {
|
|
65
|
+
return "setup-push";
|
|
66
|
+
}
|
|
67
|
+
if (/\b(live object|live objects|live collection|live collections|realtime collection|real-time collection|observe|observer|subscribe|subscription|unsubscribe|live update|live updates)\b/.test(normalized)) {
|
|
68
|
+
return "setup-live-data";
|
|
69
|
+
}
|
|
70
|
+
if (/\b(social feature|social features|feed|timeline|post list|news feed)\b/.test(normalized)) {
|
|
71
|
+
return "add-feed";
|
|
72
|
+
}
|
|
73
|
+
if (/\b(error|broken|crash|not working|fail|timeout|401|403)\b/.test(normalized)) {
|
|
74
|
+
return "troubleshoot";
|
|
75
|
+
}
|
|
76
|
+
if (/\b(validate|check|correct|setup right|initiali[sz])\b/.test(normalized)) {
|
|
77
|
+
return "validate-setup";
|
|
78
|
+
}
|
|
79
|
+
if (/\b(setup|set up|install|integrate|wire|configure)\b/.test(normalized)) {
|
|
80
|
+
return "setup-sdk";
|
|
81
|
+
}
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
function intentFor(request, outcome) {
|
|
85
|
+
const broadSocialRequest = /\b(nice|social features|social feature|engagement|community experience)\b/i.test(request);
|
|
86
|
+
const designRequest = /\bdesign token|design tokens|theme|same design|design system|brand/i.test(request);
|
|
87
|
+
return {
|
|
88
|
+
rawRequest: request,
|
|
89
|
+
interpretation: interpretationFor(outcome),
|
|
90
|
+
ambiguity: broadSocialRequest || designRequest ? "high" : "medium",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function interpretationFor(outcome) {
|
|
94
|
+
if (outcome === "add-feed") {
|
|
95
|
+
return "Add a social feed or social content surface using the customer app's existing design system.";
|
|
96
|
+
}
|
|
97
|
+
if (outcome === "setup-push") {
|
|
98
|
+
return "Set up push notifications with platform credentials, user permission/token handling, device registration after login, and unregister on logout or user switch.";
|
|
99
|
+
}
|
|
100
|
+
if (outcome === "setup-live-data") {
|
|
101
|
+
return "Set up Live Object or Live Collection observation with loading/error states, granular updates, and cleanup when the view/component lifecycle ends.";
|
|
102
|
+
}
|
|
103
|
+
return `Implement ${outcome}.`;
|
|
104
|
+
}
|
|
105
|
+
function intakeFor(request, outcome, designSignals) {
|
|
106
|
+
const broadSocialRequest = /\b(nice|social features|social feature|engagement|community experience)\b/i.test(request);
|
|
107
|
+
const mentionsDesign = /\bdesign token|design tokens|theme|same design|design system|brand/i.test(request);
|
|
108
|
+
const questions = [];
|
|
109
|
+
if (outcome === "add-feed" && broadSocialRequest) {
|
|
110
|
+
questions.push({
|
|
111
|
+
id: "feature_surface",
|
|
112
|
+
question: "Which social surface should be implemented first?",
|
|
113
|
+
why: "The request is broad, so the coding agent needs one product surface before it can choose SDK docs, UI states, and target files.",
|
|
114
|
+
required: true,
|
|
115
|
+
blocksImplementationWhenMissing: true,
|
|
116
|
+
options: ["feed", "profile", "community", "chat", "notifications"],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (outcome === "add-feed") {
|
|
120
|
+
questions.push({
|
|
121
|
+
id: "feed_scope",
|
|
122
|
+
question: "What scope should the first feed use?",
|
|
123
|
+
why: "Feed APIs and query shape depend on whether the content is global, user-specific, or community-specific.",
|
|
124
|
+
required: true,
|
|
125
|
+
blocksImplementationWhenMissing: true,
|
|
126
|
+
options: ["global", "user", "community"],
|
|
127
|
+
});
|
|
128
|
+
questions.push({
|
|
129
|
+
id: "target_screen_or_route",
|
|
130
|
+
question: "Which screen, route, or component should receive the social UI?",
|
|
131
|
+
why: "Foundry can identify platform files, but the host app owns navigation and product placement.",
|
|
132
|
+
required: true,
|
|
133
|
+
blocksImplementationWhenMissing: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (outcome === "setup-push") {
|
|
137
|
+
questions.push({
|
|
138
|
+
id: "push_platform",
|
|
139
|
+
question: "Which push platform and credentials are already configured for this app?",
|
|
140
|
+
why: "Device registration only works after platform push setup is complete in the app and social.plus console.",
|
|
141
|
+
required: true,
|
|
142
|
+
blocksImplementationWhenMissing: true,
|
|
143
|
+
options: ["FCM", "APNS", "web push", "cross-platform FCM/APNS"],
|
|
144
|
+
});
|
|
145
|
+
questions.push({
|
|
146
|
+
id: "device_token_source",
|
|
147
|
+
question: "Where does the host app request notification permission and obtain the device token?",
|
|
148
|
+
why: "The social.plus SDK registers a token; the host app remains responsible for permission prompts and token creation.",
|
|
149
|
+
required: true,
|
|
150
|
+
blocksImplementationWhenMissing: true,
|
|
151
|
+
});
|
|
152
|
+
questions.push({
|
|
153
|
+
id: "logout_user_switch_flow",
|
|
154
|
+
question: "Which logout or user-switch flow should unregister the device token?",
|
|
155
|
+
why: "A registered device is associated with one user at a time, so cleanup prevents notifications going to the wrong user.",
|
|
156
|
+
required: true,
|
|
157
|
+
blocksImplementationWhenMissing: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (outcome === "setup-live-data") {
|
|
161
|
+
questions.push({
|
|
162
|
+
id: "live_data_shape",
|
|
163
|
+
question: "Is the UI observing one object or a collection?",
|
|
164
|
+
why: "Live Objects and Live Collections have different callback shapes, update semantics, and UI handling.",
|
|
165
|
+
required: true,
|
|
166
|
+
blocksImplementationWhenMissing: true,
|
|
167
|
+
options: ["single object", "collection"],
|
|
168
|
+
});
|
|
169
|
+
questions.push({
|
|
170
|
+
id: "live_data_domain",
|
|
171
|
+
question: "Which domain should be observed?",
|
|
172
|
+
why: "The docs and SDK entrypoints differ for posts, comments, messages, channels, users, and communities.",
|
|
173
|
+
required: true,
|
|
174
|
+
blocksImplementationWhenMissing: true,
|
|
175
|
+
options: ["posts", "comments", "messages", "channels", "users", "communities", "stories"],
|
|
176
|
+
});
|
|
177
|
+
questions.push({
|
|
178
|
+
id: "lifecycle_owner",
|
|
179
|
+
question: "Which component, screen, ViewModel, or controller owns the subscription lifecycle?",
|
|
180
|
+
why: "Observers must be cleaned up when the UI lifecycle ends to avoid duplicate subscriptions and memory leaks.",
|
|
181
|
+
required: true,
|
|
182
|
+
blocksImplementationWhenMissing: true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (mentionsDesign && designSignals.length === 0) {
|
|
186
|
+
questions.push({
|
|
187
|
+
id: "design_source",
|
|
188
|
+
question: "Where are the app's design tokens, theme, or reusable UI components defined?",
|
|
189
|
+
why: "The user asked to match the existing design, and no local design source was detected.",
|
|
190
|
+
required: true,
|
|
191
|
+
blocksImplementationWhenMissing: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (mentionsDesign && designSignals.length > 0) {
|
|
195
|
+
questions.push({
|
|
196
|
+
id: "confirm_design_source",
|
|
197
|
+
question: `Should the social UI use the detected design source(s): ${designSignals.map((signal) => signal.file).join(", ")}?`,
|
|
198
|
+
why: "Foundry found likely design evidence, but the user or host agent should confirm it is the right source before UI edits.",
|
|
199
|
+
required: true,
|
|
200
|
+
blocksImplementationWhenMissing: false,
|
|
201
|
+
options: ["yes", "use another source"],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
status: questions.some((question) => question.blocksImplementationWhenMissing) ? "needs-clarification" : "ready",
|
|
206
|
+
questions,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function preferredPlatform(platforms) {
|
|
210
|
+
const order = ["flutter", "android", "typescript", "react-native", "ios"];
|
|
211
|
+
return order.find((platform) => platforms.includes(platform)) ?? platforms[0] ?? "unknown";
|
|
212
|
+
}
|
|
213
|
+
function supportFor(outcome, platform) {
|
|
214
|
+
if (outcome === "unknown" || platform === "unknown") {
|
|
215
|
+
return "unsupported";
|
|
216
|
+
}
|
|
217
|
+
if (platform === "ios") {
|
|
218
|
+
return "guided";
|
|
219
|
+
}
|
|
220
|
+
if (["android", "flutter", "typescript", "react-native"].includes(platform)) {
|
|
221
|
+
return "supported";
|
|
222
|
+
}
|
|
223
|
+
return "guided";
|
|
224
|
+
}
|
|
225
|
+
function docsFor(outcome, platform) {
|
|
226
|
+
const docs = [];
|
|
227
|
+
if (outcome === "setup-sdk" || outcome === "validate-setup") {
|
|
228
|
+
docs.push(platformQuickStart(platform));
|
|
229
|
+
docs.push({
|
|
230
|
+
path: "social-plus-sdk/getting-started/authentication",
|
|
231
|
+
reason: "Canonical login, session, and authentication ordering guidance.",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (outcome === "setup-push") {
|
|
235
|
+
docs.push({
|
|
236
|
+
path: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/overview",
|
|
237
|
+
reason: "Push notification architecture, delivery options, and cross-platform setup overview.",
|
|
238
|
+
});
|
|
239
|
+
docs.push({
|
|
240
|
+
path: pushSetupPath(platform),
|
|
241
|
+
reason: "Canonical push notification setup for the detected platform.",
|
|
242
|
+
});
|
|
243
|
+
docs.push({
|
|
244
|
+
path: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device",
|
|
245
|
+
reason: "Device registration and logout/user-switch cleanup guidance.",
|
|
246
|
+
});
|
|
247
|
+
docs.push({
|
|
248
|
+
path: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/overview",
|
|
249
|
+
reason: "Notification preference settings for user/channel/community controls.",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (outcome === "add-feed" || outcome === "setup-live-data") {
|
|
253
|
+
if (outcome === "add-feed") {
|
|
254
|
+
docs.push({
|
|
255
|
+
path: "social-plus-sdk/social/posts",
|
|
256
|
+
reason: "Canonical social post/feed concepts and API entrypoint.",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
docs.push({
|
|
260
|
+
path: "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
|
|
261
|
+
reason: "Live Object and Live Collection lifecycle, snapshots, loading/error states, and cleanup guidance.",
|
|
262
|
+
});
|
|
263
|
+
docs.push({
|
|
264
|
+
path: liveDataPlatformPath(platform),
|
|
265
|
+
reason: "Platform-specific Live Object and Live Collection implementation patterns.",
|
|
266
|
+
});
|
|
267
|
+
docs.push({
|
|
268
|
+
path: "social-plus-sdk/core-concepts/realtime-communication/realtime-events/overview",
|
|
269
|
+
reason: "Real-time event subscription context for live data updates.",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (outcome === "troubleshoot") {
|
|
273
|
+
docs.push({
|
|
274
|
+
path: "social-plus-sdk/getting-started/authentication",
|
|
275
|
+
reason: "Authentication and session issues are common setup failure causes.",
|
|
276
|
+
});
|
|
277
|
+
docs.push({
|
|
278
|
+
path: "social-plus-sdk/core-concepts/foundation/logging",
|
|
279
|
+
reason: "Logging and diagnostics guidance for debugging integration failures.",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return docs.filter((doc) => doc.path !== "unknown");
|
|
283
|
+
}
|
|
284
|
+
function platformQuickStart(platform) {
|
|
285
|
+
const paths = {
|
|
286
|
+
android: "social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start",
|
|
287
|
+
flutter: "social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start",
|
|
288
|
+
ios: "social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start",
|
|
289
|
+
typescript: "social-plus-sdk/getting-started/platform-setup/web/web-quick-start",
|
|
290
|
+
"react-native": "social-plus-sdk/getting-started/platform-setup/web/web-quick-start",
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
path: paths[platform] ?? "unknown",
|
|
294
|
+
reason: "Canonical SDK setup guidance for the detected platform.",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function pushSetupPath(platform) {
|
|
298
|
+
const paths = {
|
|
299
|
+
android: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/android-setup",
|
|
300
|
+
flutter: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/flutter-setup",
|
|
301
|
+
ios: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/ios-setup",
|
|
302
|
+
typescript: "social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/web-setup",
|
|
303
|
+
"react-native": "social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/react-native-setup",
|
|
304
|
+
};
|
|
305
|
+
return paths[platform] ?? "unknown";
|
|
306
|
+
}
|
|
307
|
+
function liveDataPlatformPath(platform) {
|
|
308
|
+
const paths = {
|
|
309
|
+
android: "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/android",
|
|
310
|
+
flutter: "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter",
|
|
311
|
+
ios: "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/ios",
|
|
312
|
+
typescript: "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript",
|
|
313
|
+
"react-native": "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript",
|
|
314
|
+
};
|
|
315
|
+
return paths[platform] ?? "unknown";
|
|
316
|
+
}
|
|
317
|
+
function requiredInputsFor(request, outcome, platform, designSignals) {
|
|
318
|
+
const inputs = ["social.plus API key", "social.plus region: US, EU, or SG", "user identity source for login"];
|
|
319
|
+
const mentionsDesign = /\bdesign token|design tokens|theme|same design|design system|brand/i.test(request);
|
|
320
|
+
const broadSocialRequest = /\b(nice|social features|social feature|engagement|community experience)\b/i.test(request);
|
|
321
|
+
const hasDesignSignal = designSignals.length > 0;
|
|
322
|
+
if (outcome === "setup-push") {
|
|
323
|
+
inputs.push("customer-owned Firebase/APNS/Web Push configuration", "device token source from the host app", "notification permission request flow", "device registration and logout/user-switch behavior");
|
|
324
|
+
}
|
|
325
|
+
if (outcome === "add-feed" || outcome === "setup-live-data") {
|
|
326
|
+
inputs.push("live data shape: single object or collection", "lifecycle owner for subscribe/unsubscribe cleanup");
|
|
327
|
+
}
|
|
328
|
+
if (outcome === "add-feed") {
|
|
329
|
+
inputs.push("feature choice: feed, profile, community, chat, notifications, or another social surface", "feed scope: global, user, or community", "target screen or route for the feed UI");
|
|
330
|
+
}
|
|
331
|
+
if (outcome === "setup-live-data") {
|
|
332
|
+
inputs.push("observed domain: posts, comments, messages, channels, users, communities, or stories", "query/filter/sort and pagination requirements", "UI handling for loading, empty, error, insertion, deletion, and modification states");
|
|
333
|
+
}
|
|
334
|
+
if ((mentionsDesign || broadSocialRequest) && hasDesignSignal) {
|
|
335
|
+
inputs.push(`confirm detected design source(s): ${designSignals.map((signal) => signal.file).join(", ")}`);
|
|
336
|
+
}
|
|
337
|
+
if ((mentionsDesign || broadSocialRequest) && !hasDesignSignal) {
|
|
338
|
+
inputs.push("design token or theme source file", "UI framework/design system used by the host app");
|
|
339
|
+
}
|
|
340
|
+
if (platform === "android") {
|
|
341
|
+
inputs.push("Android application module path");
|
|
342
|
+
}
|
|
343
|
+
if (platform === "ios") {
|
|
344
|
+
inputs.push("iOS dependency manager: Swift Package Manager or CocoaPods");
|
|
345
|
+
}
|
|
346
|
+
return inputs;
|
|
347
|
+
}
|
|
348
|
+
async function targetFilesFor(root, outcome, platform, designSignals) {
|
|
349
|
+
const designTargets = outcome === "add-feed" ? designSignals.map((signal) => ({
|
|
350
|
+
path: signal.file,
|
|
351
|
+
operation: "Reuse existing design tokens, theme values, component conventions, spacing, typography, and colors for the social UI.",
|
|
352
|
+
confidence: "high",
|
|
353
|
+
evidence: signal.reason,
|
|
354
|
+
})) : [];
|
|
355
|
+
const pushTargets = outcome === "setup-push" ? [
|
|
356
|
+
{ path: "push token/permission flow", operation: "Request permission, obtain the platform device token, and handle token refresh.", confidence: "low", evidence: "Required device token source from the host app." },
|
|
357
|
+
{ path: "login/auth flow", operation: "Register the device token only after social.plus login succeeds.", confidence: "low", evidence: "Device registration requires an authenticated client." },
|
|
358
|
+
{ path: "logout/user-switch flow", operation: "Unregister the device token before logout or user switch completes.", confidence: "low", evidence: "Device association is user-specific." },
|
|
359
|
+
] : [];
|
|
360
|
+
const liveTargets = outcome === "setup-live-data" ? [
|
|
361
|
+
{ path: "target screen/component/controller", operation: "Observe the Live Object or Live Collection and render loading, empty, error, and data states.", confidence: "low", evidence: "Required lifecycle owner and target UI." },
|
|
362
|
+
{ path: "data source/repository module", operation: "Encapsulate query/get calls and expose a cleanup-aware subscription API.", confidence: "low", evidence: "Live Objects and Collections require lifecycle cleanup." },
|
|
363
|
+
] : [];
|
|
364
|
+
if (platform === "android") {
|
|
365
|
+
return [
|
|
366
|
+
await target(root, ["app/build.gradle.kts", "app/build.gradle"], "Add or verify the social.plus Android SDK dependency.", "Detected Android app Gradle module.", "medium"),
|
|
367
|
+
await target(root, ["app/src/main/AndroidManifest.xml"], "Verify permissions and Application class wiring.", "Detected default Android manifest path.", "medium"),
|
|
368
|
+
{ path: "Application class", operation: "Initialize social.plus once at app startup.", confidence: "low", evidence: "Required by Android setup lifecycle rule; exact file must be detected by the coding agent." },
|
|
369
|
+
{ path: "login/auth flow", operation: "Call social.plus login after user identity is known.", confidence: "low", evidence: "Required user identity source." },
|
|
370
|
+
...pushTargets,
|
|
371
|
+
...liveTargets,
|
|
372
|
+
...designTargets,
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
if (platform === "flutter") {
|
|
376
|
+
return [
|
|
377
|
+
await target(root, ["pubspec.yaml"], "Add or verify the Flutter SDK dependency.", "Detected Flutter package manifest.", "high"),
|
|
378
|
+
await target(root, ["lib/main.dart"], "Initialize Flutter binding and SDK before runApp.", "Conventional Flutter app entrypoint.", "medium"),
|
|
379
|
+
{ path: "login/auth flow", operation: "Call AmityCoreClient.login after user identity is known.", confidence: "low", evidence: "Required user identity source." },
|
|
380
|
+
...pushTargets,
|
|
381
|
+
...liveTargets,
|
|
382
|
+
...designTargets,
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
if (platform === "typescript" || platform === "react-native") {
|
|
386
|
+
return [
|
|
387
|
+
await target(root, ["package.json"], "Add or verify the TypeScript SDK dependency.", "Detected JavaScript package manifest.", "high"),
|
|
388
|
+
{ path: "client initialization module", operation: "Create the social.plus client with API key and region.", confidence: "low", evidence: "Validator rule: typescript.client.create." },
|
|
389
|
+
{ path: "login/auth flow", operation: "Await login after user identity is known and before live subscriptions.", confidence: "low", evidence: "Required user identity source." },
|
|
390
|
+
...pushTargets,
|
|
391
|
+
...liveTargets,
|
|
392
|
+
...designTargets,
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
if (platform === "ios") {
|
|
396
|
+
return [
|
|
397
|
+
await target(root, ["Package.swift", "Podfile"], "Add or verify the iOS SDK dependency.", "Detected iOS dependency manifest if present.", "medium"),
|
|
398
|
+
{ path: "app initialization flow", operation: "Initialize AmityClient with API key and region.", confidence: "low", evidence: "iOS quick-start docs." },
|
|
399
|
+
{ path: "login/auth flow", operation: "Call login after user identity is known and handle session renewal.", confidence: "low", evidence: "Authentication docs." },
|
|
400
|
+
...pushTargets,
|
|
401
|
+
...liveTargets,
|
|
402
|
+
...designTargets,
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
return [{ path: "unknown", operation: "No safe target files can be selected until platform is detected.", confidence: "low", evidence: "No platform signal." }];
|
|
406
|
+
}
|
|
407
|
+
async function target(root, candidates, operation, evidence, fallbackConfidence) {
|
|
408
|
+
for (const candidate of candidates) {
|
|
409
|
+
if (await exists(path.join(root, candidate))) {
|
|
410
|
+
return { path: candidate, operation, confidence: "high", evidence };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { path: candidates[0], operation, confidence: fallbackConfidence, evidence: `${evidence} File was not found at the conventional path; verify before editing.` };
|
|
414
|
+
}
|
|
415
|
+
function implementationRulesFor(request, outcome, platform) {
|
|
416
|
+
const rules = [
|
|
417
|
+
"Do not invent API keys, app IDs, regions, user IDs, Firebase files, APNS keys, or server-issued auth tokens.",
|
|
418
|
+
"Do not silently default the social.plus region; ask for US, EU, or SG if it is not known.",
|
|
419
|
+
"Do not make SDK calls before setup and login ordering is established.",
|
|
420
|
+
"Every code edit should be tied to a target file and validation step from this plan.",
|
|
421
|
+
];
|
|
422
|
+
const mentionsDesign = /\bdesign token|design tokens|theme|same design|design system|brand/i.test(request);
|
|
423
|
+
if (platform === "android") {
|
|
424
|
+
rules.push("Do not initialize the SDK from an Activity, Fragment, or composable lifecycle when Application startup is available.");
|
|
425
|
+
}
|
|
426
|
+
if (platform === "flutter") {
|
|
427
|
+
rules.push("Call WidgetsFlutterBinding.ensureInitialized before async SDK setup in main().");
|
|
428
|
+
}
|
|
429
|
+
if (platform === "react-native") {
|
|
430
|
+
rules.push("Do not create the client inside a remounting React component body.");
|
|
431
|
+
}
|
|
432
|
+
if (outcome === "setup-push") {
|
|
433
|
+
rules.push("Do not fabricate Firebase/APNS/Web Push configuration. Use customer-provided push credentials and files only.");
|
|
434
|
+
rules.push("Do not register a device token before social.plus login succeeds.");
|
|
435
|
+
rules.push("Always unregister the device token on logout or user switch so a device is not tied to the wrong user.");
|
|
436
|
+
rules.push("Do not implement platform permission prompts or token creation unless the host app's push library and ownership are identified.");
|
|
437
|
+
}
|
|
438
|
+
if (outcome === "setup-live-data") {
|
|
439
|
+
rules.push("Do not replace Live Object or Live Collection observation with polling unless the docs or user explicitly require it.");
|
|
440
|
+
rules.push("Always clean up observers/subscriptions when the owning component, screen, ViewModel, or controller is destroyed.");
|
|
441
|
+
rules.push("Handle loading and error states from the live data API before rendering data as ready.");
|
|
442
|
+
rules.push("For collections, use insertion/deletion/modification changes for targeted UI updates instead of assuming a full list replacement.");
|
|
443
|
+
}
|
|
444
|
+
if (mentionsDesign) {
|
|
445
|
+
rules.push("Do not create a new visual system. Inspect and reuse the host app's existing design tokens, theme files, components, spacing, typography, and color conventions.");
|
|
446
|
+
}
|
|
447
|
+
return rules;
|
|
448
|
+
}
|
|
449
|
+
function implementationStepsFor(outcome, platform, designSignals) {
|
|
450
|
+
if (outcome === "setup-sdk") {
|
|
451
|
+
return [
|
|
452
|
+
{ step: "Verify or add the platform SDK dependency.", evidence: [platformQuickStart(platform).path, "targetFiles.dependency"] },
|
|
453
|
+
{ step: "Initialize the social.plus client exactly once with API key and explicit region.", evidence: [platformQuickStart(platform).path, "requiredInputs.social.plus API key", "requiredInputs.social.plus region"] },
|
|
454
|
+
{ step: "Wire login after user identity is known and before social.plus API queries/subscriptions.", evidence: ["social-plus-sdk/getting-started/authentication", "requiredInputs.user identity source for login"] },
|
|
455
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
if (outcome === "setup-push") {
|
|
459
|
+
return [
|
|
460
|
+
{ step: "Verify SDK setup and login are already correct before push work.", evidence: ["validate_setup", "social-plus-sdk/getting-started/authentication"] },
|
|
461
|
+
{ step: "Confirm platform push credentials and app-side permission/token ownership.", evidence: ["requiredInputs.customer-owned Firebase/APNS/Web Push configuration", "requiredInputs.device token source from the host app", pushSetupPath(platform)] },
|
|
462
|
+
{ step: "Wire platform push config using only customer-provided Firebase/APNS/Web Push material.", evidence: [pushSetupPath(platform), "requiredInputs.customer-owned Firebase/APNS/Web Push configuration"] },
|
|
463
|
+
{ step: "Register device after login and unregister on logout/user switch.", evidence: ["social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device"] },
|
|
464
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
if (outcome === "add-feed") {
|
|
468
|
+
const designEvidence = designSignals.length > 0 ? designSignals.map((signal) => signal.file) : ["requiredInputs.design token or theme source file"];
|
|
469
|
+
return [
|
|
470
|
+
{ step: "Confirm the target route/screen and feed scope before writing code.", evidence: ["requiredInputs.feed scope", "requiredInputs.target screen or route"] },
|
|
471
|
+
{ step: "Fetch the canonical social/feed docs and use platform-appropriate live collection patterns.", evidence: ["social-plus-sdk/social/posts", "social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview"] },
|
|
472
|
+
{ step: "Reuse the host app's existing visual system for the social surface.", evidence: designEvidence },
|
|
473
|
+
{ step: "Implement loading, empty, error, and data states.", evidence: ["implementationRules.file-specific edits"] },
|
|
474
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
475
|
+
];
|
|
476
|
+
}
|
|
477
|
+
if (outcome === "setup-live-data") {
|
|
478
|
+
return [
|
|
479
|
+
{ step: "Confirm whether the UI needs a Live Object or Live Collection and which domain it observes.", evidence: ["requiredInputs.live data shape", "requiredInputs.observed domain"] },
|
|
480
|
+
{ step: "Fetch Live Objects/Collections docs plus platform-specific implementation guidance.", evidence: ["social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview", liveDataPlatformPath(platform)] },
|
|
481
|
+
{ step: "Implement the subscription in the owning lifecycle scope and render loading, empty, error, and data states.", evidence: ["requiredInputs.lifecycle owner for subscribe/unsubscribe cleanup", "implementationRules.loading-error-states"] },
|
|
482
|
+
{ step: "For collections, apply insertion, deletion, and modification changes without unnecessary full-list rerenders.", evidence: ["social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview"] },
|
|
483
|
+
{ step: "Clean up the observer/subscription when the owner unmounts, disposes, or disappears.", evidence: ["implementationRules.cleanup", "validate_setup"] },
|
|
484
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
return [{ step: "Gather more evidence before implementation.", evidence: ["stopConditions", "search_docs", "inspect_project"] }];
|
|
488
|
+
}
|
|
489
|
+
function validationFor(outcome, platform) {
|
|
490
|
+
const validation = ["validate_setup", "run_sensors"];
|
|
491
|
+
if (outcome === "setup-sdk") {
|
|
492
|
+
validation.push(`${platform}.setup.present`, `${platform}.login.present`, `${platform}.region.explicit`);
|
|
493
|
+
}
|
|
494
|
+
if (outcome === "setup-push") {
|
|
495
|
+
validation.push("push permission and token source identified", "device registration after login", "unregister on logout/user switch", `${platform}.push.unregister.present`);
|
|
496
|
+
}
|
|
497
|
+
if (outcome === "setup-live-data") {
|
|
498
|
+
validation.push("live data owner identified", "loading and error states handled", "subscription cleanup on lifecycle end", `${platform}.live.cleanup`);
|
|
499
|
+
}
|
|
500
|
+
return validation;
|
|
501
|
+
}
|
|
502
|
+
function stopConditionsFor(request, outcome, platform, platforms, designSignals, surfaces, surfacePath) {
|
|
503
|
+
const mentionsDesign = /\bdesign token|design tokens|theme|same design|design system|brand/i.test(request);
|
|
504
|
+
const broadSocialRequest = /\b(nice|social features|social feature|engagement|community experience)\b/i.test(request);
|
|
505
|
+
const stops = [
|
|
506
|
+
"Required customer secrets or credentials are missing.",
|
|
507
|
+
"The target file is ambiguous or missing and no safe conventional location is detected.",
|
|
508
|
+
"Docs lookup does not return the canonical page named in this plan.",
|
|
509
|
+
];
|
|
510
|
+
if (platform === "unknown" || outcome === "unknown") {
|
|
511
|
+
stops.push("The request or platform is unsupported; do not implement until clarified.");
|
|
512
|
+
}
|
|
513
|
+
if (platforms.length > 1 && !surfacePath) {
|
|
514
|
+
stops.push(`Multiple platform signals detected (${platforms.join(", ")}); confirm which app surface should be modified.`);
|
|
515
|
+
}
|
|
516
|
+
if (surfaces.length > 1 && !surfacePath) {
|
|
517
|
+
stops.push(`Multiple app surfaces detected (${surfaces.map((surface) => surface.path).join(", ")}); call this tool again with surfacePath set to the target app surface.`);
|
|
518
|
+
}
|
|
519
|
+
if (outcome === "setup-push") {
|
|
520
|
+
stops.push("Firebase/APNS/Web Push configuration is not present or not provided by the customer.");
|
|
521
|
+
stops.push("The host app's notification permission and device-token source is unknown.");
|
|
522
|
+
stops.push("No logout or user-switch cleanup point is identified for unregistering device tokens.");
|
|
523
|
+
}
|
|
524
|
+
if (outcome === "setup-live-data") {
|
|
525
|
+
stops.push("The request does not identify whether the UI needs a Live Object or Live Collection.");
|
|
526
|
+
stops.push("The lifecycle owner for subscription cleanup is unknown.");
|
|
527
|
+
}
|
|
528
|
+
if (broadSocialRequest) {
|
|
529
|
+
stops.push("The requested social feature is too broad; confirm the first feature surface before implementing.");
|
|
530
|
+
}
|
|
531
|
+
if (mentionsDesign && designSignals.length === 0) {
|
|
532
|
+
stops.push("No design token, theme, or component source has been identified from the customer repo.");
|
|
533
|
+
}
|
|
534
|
+
return stops;
|
|
535
|
+
}
|
|
536
|
+
async function exists(filePath) {
|
|
537
|
+
try {
|
|
538
|
+
await access(filePath);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
}
|