gamealgo-cli 0.3.6 → 0.3.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.
- package/dist/client.js +24 -0
- package/dist/commands/dynamic-config.js +71 -0
- package/dist/commands/events.js +25 -1
- package/dist/commands/report.js +99 -21
- package/dist/main.js +5 -0
- package/dist/report-csv.js +87 -0
- package/package.json +1 -1
- package/skills/gamealgo/SKILL.md +63 -1
package/dist/client.js
CHANGED
|
@@ -133,6 +133,21 @@ export class GameAlgoAdminClient {
|
|
|
133
133
|
async getScriptVersion(name, versionId) {
|
|
134
134
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/scripts/${encodeURIComponent(name)}/versions/${encodeURIComponent(versionId)}`);
|
|
135
135
|
}
|
|
136
|
+
async listDynamicConfigs() {
|
|
137
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs`);
|
|
138
|
+
}
|
|
139
|
+
async getDynamicConfig(key) {
|
|
140
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}`);
|
|
141
|
+
}
|
|
142
|
+
async putDynamicConfig(key, body) {
|
|
143
|
+
return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}`, body);
|
|
144
|
+
}
|
|
145
|
+
async queueDynamicConfigRun(key) {
|
|
146
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}/run`, {});
|
|
147
|
+
}
|
|
148
|
+
async listDynamicConfigRuns(key, limit = 50) {
|
|
149
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/dynamic-configs/${encodeURIComponent(key)}/runs?limit=${encodeURIComponent(String(limit))}`);
|
|
150
|
+
}
|
|
136
151
|
async getReportPack() {
|
|
137
152
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack`);
|
|
138
153
|
}
|
|
@@ -173,6 +188,15 @@ export class GameAlgoAdminClient {
|
|
|
173
188
|
async deprecateEventCatalog(eventType) {
|
|
174
189
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/deprecate`, {});
|
|
175
190
|
}
|
|
191
|
+
async blockEventCatalog(eventType, reason) {
|
|
192
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/block`, { reason });
|
|
193
|
+
}
|
|
194
|
+
async activateEventCatalog(eventType, reason, confirmEventType) {
|
|
195
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/events/catalog/${encodeURIComponent(eventType)}/activate`, {
|
|
196
|
+
reason,
|
|
197
|
+
confirmEventType,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
176
200
|
async getIntegrationPlan(environment, platform) {
|
|
177
201
|
const params = new URLSearchParams({ environment, platform });
|
|
178
202
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/integration/plan?${params.toString()}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { parseFlags } from "../args.js";
|
|
4
|
+
import { printResult } from "../output.js";
|
|
5
|
+
import { contentTypeForFileName, objectValue, readJsonFile, sanitizeRemoteFileName } from "../resources.js";
|
|
6
|
+
import { announceTargetGame } from "../target.js";
|
|
7
|
+
import { optionalString } from "../utils.js";
|
|
8
|
+
export async function handleDynamicConfig(client, args, global) {
|
|
9
|
+
const sub = args.shift();
|
|
10
|
+
if (sub === "list") {
|
|
11
|
+
parseFlags(args);
|
|
12
|
+
await printResult(await client.listDynamicConfigs(), global);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (sub === "get") {
|
|
16
|
+
const key = args.shift();
|
|
17
|
+
if (!key)
|
|
18
|
+
throw new Error("usage: gamealgo dynamic-config get <key>");
|
|
19
|
+
parseFlags(args);
|
|
20
|
+
await printResult(await client.getDynamicConfig(key), global);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (sub === "runs") {
|
|
24
|
+
const key = args.shift();
|
|
25
|
+
if (!key)
|
|
26
|
+
throw new Error("usage: gamealgo dynamic-config runs <key> [--limit 50]");
|
|
27
|
+
const flags = parseFlags(args);
|
|
28
|
+
await printResult(await client.listDynamicConfigRuns(key, Number(flags.limit || 50)), global);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (sub === "run") {
|
|
32
|
+
const key = args.shift();
|
|
33
|
+
if (!key)
|
|
34
|
+
throw new Error("usage: gamealgo dynamic-config run <key>");
|
|
35
|
+
parseFlags(args);
|
|
36
|
+
await announceTargetGame(client);
|
|
37
|
+
await printResult(await client.queueDynamicConfigRun(key), global);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "publish") {
|
|
41
|
+
const definitionPath = args.shift();
|
|
42
|
+
if (!definitionPath) {
|
|
43
|
+
throw new Error("usage: gamealgo dynamic-config publish <definition.json> [--script transform.js] [--message text]");
|
|
44
|
+
}
|
|
45
|
+
const flags = parseFlags(args);
|
|
46
|
+
const definition = objectValue(await readJsonFile(definitionPath));
|
|
47
|
+
if (!definition)
|
|
48
|
+
throw new Error("dynamic config definition must be a JSON object");
|
|
49
|
+
const key = optionalString(definition.key);
|
|
50
|
+
if (!key)
|
|
51
|
+
throw new Error("dynamic config definition.key is required");
|
|
52
|
+
const body = { ...definition };
|
|
53
|
+
delete body.key;
|
|
54
|
+
const scriptPath = optionalString(flags.script);
|
|
55
|
+
await announceTargetGame(client);
|
|
56
|
+
if (scriptPath) {
|
|
57
|
+
const scriptName = sanitizeRemoteFileName(optionalString(definition.scriptName) || basename(scriptPath));
|
|
58
|
+
const content = await readFile(scriptPath, "utf8");
|
|
59
|
+
const created = await client.createScriptVersion(scriptName, content, contentTypeForFileName(scriptName), optionalString(flags.message));
|
|
60
|
+
body.scriptVersionId = created.scriptVersion.versionId;
|
|
61
|
+
}
|
|
62
|
+
delete body.scriptName;
|
|
63
|
+
if (!optionalString(body.scriptVersionId)) {
|
|
64
|
+
throw new Error("definition.scriptVersionId or --script is required");
|
|
65
|
+
}
|
|
66
|
+
const response = await client.putDynamicConfig(key, body);
|
|
67
|
+
await printResult({ ok: true, ...response }, global);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("usage: gamealgo dynamic-config <list|get|publish|run|runs>");
|
|
71
|
+
}
|
package/dist/commands/events.js
CHANGED
|
@@ -41,8 +41,32 @@ export async function handleEvents(client, args, global) {
|
|
|
41
41
|
await writeEventCatalogOutput(await client.deprecateEventCatalog(eventType), flags, global);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
+
if (sub === "block") {
|
|
45
|
+
const eventType = args.shift();
|
|
46
|
+
const reason = optionalString(flags.reason);
|
|
47
|
+
if (!eventType || args.length || !reason)
|
|
48
|
+
throw new Error("usage: gamealgo events block <eventType> --reason <reason> [--yes]");
|
|
49
|
+
if (!flags.yes && !(await confirm(`Block event ${eventType}? New occurrences will be dropped before Kafka.`)))
|
|
50
|
+
throw new Error("Cancelled");
|
|
51
|
+
await announceTargetGame(client);
|
|
52
|
+
await writeEventCatalogOutput(await client.blockEventCatalog(eventType, reason), flags, global);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (sub === "activate") {
|
|
56
|
+
const eventType = args.shift();
|
|
57
|
+
const reason = optionalString(flags.reason);
|
|
58
|
+
const confirmation = optionalString(flags.confirm);
|
|
59
|
+
if (!eventType || args.length || !reason || !confirmation) {
|
|
60
|
+
throw new Error("usage: gamealgo events activate <eventType> --reason <reason> --confirm <eventType>");
|
|
61
|
+
}
|
|
62
|
+
if (confirmation !== eventType)
|
|
63
|
+
throw new Error("--confirm must exactly match eventType");
|
|
64
|
+
await announceTargetGame(client);
|
|
65
|
+
await writeEventCatalogOutput(await client.activateEventCatalog(eventType, reason, confirmation), flags, global);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
44
68
|
if (sub !== "count")
|
|
45
|
-
throw new Error("usage: gamealgo events <list|get|apply|deprecate|count>");
|
|
69
|
+
throw new Error("usage: gamealgo events <list|get|apply|deprecate|block|activate|count>");
|
|
46
70
|
const timeoutMs = reportTimeoutMs(flags);
|
|
47
71
|
const startedAt = Date.now();
|
|
48
72
|
const stopProgress = startProgress("Querying event counts", timeoutMs);
|
package/dist/commands/report.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { extname, join } from "node:path";
|
|
1
2
|
import { parseFlags, parseSelectorFlags } from "../args.js";
|
|
2
3
|
import { formatDuration, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
|
|
4
|
+
import { reportDashboardCsv, reportDashboardCsvExports } from "../report-csv.js";
|
|
3
5
|
import { objectValue, readJsonFile, writeTextFile } from "../resources.js";
|
|
4
6
|
import { announceTargetGame } from "../target.js";
|
|
5
7
|
import { optionalString } from "../utils.js";
|
|
@@ -43,7 +45,7 @@ export async function handleReport(client, args, global) {
|
|
|
43
45
|
const startDate = String(flags.from || flags.start || flags.startDate || "");
|
|
44
46
|
const endDate = String(flags.to || flags.end || flags.endDate || "");
|
|
45
47
|
if (!startDate || !endDate)
|
|
46
|
-
throw new Error("usage: gamealgo report result --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v]");
|
|
48
|
+
throw new Error("usage: gamealgo report result --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--format json|csv] [--out file-or-directory]");
|
|
47
49
|
const timeoutMs = reportTimeoutMs(flags);
|
|
48
50
|
const platform = await resolveCliReportPlatform(client, flags.platform, selectors.platform, startDate, endDate);
|
|
49
51
|
const startedAt = Date.now();
|
|
@@ -70,17 +72,7 @@ export async function handleReport(client, args, global) {
|
|
|
70
72
|
const elapsedMs = Date.now() - startedAt;
|
|
71
73
|
const outputValue = withCliMeta(response, { elapsedMs, timeoutMs });
|
|
72
74
|
process.stderr.write(`Report query finished in ${formatDuration(elapsedMs)}.\n`);
|
|
73
|
-
|
|
74
|
-
await writeTextFile(String(flags.out), JSON.stringify(outputValue, null, 2) + "\n");
|
|
75
|
-
await printResult({
|
|
76
|
-
ok: true,
|
|
77
|
-
out: String(flags.out),
|
|
78
|
-
results: Array.isArray(outputValue.results) ? outputValue.results.length : 0,
|
|
79
|
-
elapsedMs,
|
|
80
|
-
}, global);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
await printResult(outputValue, global);
|
|
75
|
+
await writeReportQueryOutput(outputValue, flags, global, elapsedMs);
|
|
84
76
|
return;
|
|
85
77
|
}
|
|
86
78
|
if (sub === "preview") {
|
|
@@ -90,7 +82,7 @@ export async function handleReport(client, args, global) {
|
|
|
90
82
|
const startDate = String(flags.from || flags.start || flags.startDate || "");
|
|
91
83
|
const endDate = String(flags.to || flags.end || flags.endDate || "");
|
|
92
84
|
if (!filePath || !startDate || !endDate) {
|
|
93
|
-
throw new Error("usage: gamealgo report preview --pack report-pack.json --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v]");
|
|
85
|
+
throw new Error("usage: gamealgo report preview --pack report-pack.json --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--format json|csv] [--out file-or-directory]");
|
|
94
86
|
}
|
|
95
87
|
const content = await readJsonFile(filePath);
|
|
96
88
|
const timeoutMs = reportTimeoutMs(flags);
|
|
@@ -119,20 +111,106 @@ export async function handleReport(client, args, global) {
|
|
|
119
111
|
const elapsedMs = Date.now() - startedAt;
|
|
120
112
|
const outputValue = withCliMeta(response, { elapsedMs, timeoutMs });
|
|
121
113
|
process.stderr.write(`Report preview finished in ${formatDuration(elapsedMs)}.\n`);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
114
|
+
await writeReportQueryOutput(outputValue, flags, global, elapsedMs);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw new Error("usage: gamealgo report <pull|validate|publish|manifest|result|preview>");
|
|
118
|
+
}
|
|
119
|
+
export function reportOutputFormat(value) {
|
|
120
|
+
if (value === undefined || value === null || value === "")
|
|
121
|
+
return "json";
|
|
122
|
+
if (typeof value !== "string")
|
|
123
|
+
throw new Error("--format requires json or csv");
|
|
124
|
+
const normalized = value.trim().toLowerCase();
|
|
125
|
+
if (normalized === "json" || normalized === "csv")
|
|
126
|
+
return normalized;
|
|
127
|
+
throw new Error("--format must be json or csv");
|
|
128
|
+
}
|
|
129
|
+
async function writeReportQueryOutput(outputValue, flags, global, elapsedMs) {
|
|
130
|
+
const format = reportOutputFormat(flags.format);
|
|
131
|
+
const out = optionalString(flags.out);
|
|
132
|
+
if (flags.out === true)
|
|
133
|
+
throw new Error("--out requires a file path");
|
|
134
|
+
if (format === "csv") {
|
|
135
|
+
if (!out && global.json)
|
|
136
|
+
throw new Error("--format csv cannot be combined with --json unless --out is provided");
|
|
137
|
+
if (out) {
|
|
138
|
+
const exports = reportDashboardCsvExports(outputValue);
|
|
139
|
+
const files = reportCsvOutputFiles(exports, out);
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
warnTruncatedCsv(file.exported);
|
|
142
|
+
await writeTextFile(file.out, file.exported.content);
|
|
143
|
+
}
|
|
144
|
+
const first = files[0];
|
|
145
|
+
const summary = files.length === 1 ? {
|
|
125
146
|
ok: true,
|
|
126
|
-
out:
|
|
127
|
-
|
|
147
|
+
out: first.out,
|
|
148
|
+
format,
|
|
149
|
+
reportId: first.exported.reportId,
|
|
150
|
+
chartId: first.exported.chartId,
|
|
151
|
+
chartTitle: first.exported.chartTitle,
|
|
152
|
+
rows: first.exported.rowCount,
|
|
153
|
+
columns: first.exported.columns.length,
|
|
154
|
+
truncated: first.exported.truncated,
|
|
128
155
|
elapsedMs,
|
|
129
|
-
}
|
|
156
|
+
} : {
|
|
157
|
+
ok: true,
|
|
158
|
+
out,
|
|
159
|
+
format,
|
|
160
|
+
results: files.length,
|
|
161
|
+
files: files.map((file) => ({
|
|
162
|
+
out: file.out,
|
|
163
|
+
reportId: file.exported.reportId,
|
|
164
|
+
chartId: file.exported.chartId,
|
|
165
|
+
chartTitle: file.exported.chartTitle,
|
|
166
|
+
rows: file.exported.rowCount,
|
|
167
|
+
columns: file.exported.columns.length,
|
|
168
|
+
truncated: file.exported.truncated,
|
|
169
|
+
})),
|
|
170
|
+
elapsedMs,
|
|
171
|
+
};
|
|
172
|
+
await printResult(summary, global);
|
|
130
173
|
return;
|
|
131
174
|
}
|
|
132
|
-
|
|
175
|
+
const exported = reportDashboardCsv(outputValue);
|
|
176
|
+
warnTruncatedCsv(exported);
|
|
177
|
+
process.stdout.write(exported.content);
|
|
133
178
|
return;
|
|
134
179
|
}
|
|
135
|
-
|
|
180
|
+
if (out) {
|
|
181
|
+
await writeTextFile(out, JSON.stringify(outputValue, null, 2) + "\n");
|
|
182
|
+
await printResult({
|
|
183
|
+
ok: true,
|
|
184
|
+
out,
|
|
185
|
+
results: Array.isArray(outputValue.results) ? outputValue.results.length : 0,
|
|
186
|
+
elapsedMs,
|
|
187
|
+
}, global);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await printResult(outputValue, global);
|
|
191
|
+
}
|
|
192
|
+
function reportCsvOutputFiles(exports, out) {
|
|
193
|
+
if (exports.length === 1)
|
|
194
|
+
return [{ out, exported: exports[0] }];
|
|
195
|
+
if (extname(out).toLowerCase() === ".csv") {
|
|
196
|
+
throw new Error(`CSV export matched ${exports.length} charts; use --chart-id to select one chart or pass a directory to --out`);
|
|
197
|
+
}
|
|
198
|
+
return exports.map((exported, index) => ({
|
|
199
|
+
out: join(out, `${String(index + 1).padStart(2, "0")}-${reportCsvBaseName(exported, index)}.csv`),
|
|
200
|
+
exported,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
function reportCsvBaseName(exported, index) {
|
|
204
|
+
const candidate = exported.chartId || exported.reportId || `report-${index + 1}`;
|
|
205
|
+
return candidate
|
|
206
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-")
|
|
207
|
+
.replace(/^-+|-+$/g, "")
|
|
208
|
+
|| `report-${index + 1}`;
|
|
209
|
+
}
|
|
210
|
+
function warnTruncatedCsv(exported) {
|
|
211
|
+
if (!exported.truncated)
|
|
212
|
+
return;
|
|
213
|
+
process.stderr.write(`Warning: CSV contains a truncated report result${exported.rowLimit ? ` (limit ${exported.rowLimit})` : ""}.\n`);
|
|
136
214
|
}
|
|
137
215
|
export async function resolveCliReportPlatform(client, flagValue, selectorValue, startDate, endDate) {
|
|
138
216
|
const flagPlatform = optionalCliReportPlatform(flagValue, "--platform");
|
package/dist/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { handleFileResource } from "./commands/resources.js";
|
|
|
12
12
|
import { handleSkill } from "./commands/skill.js";
|
|
13
13
|
import { loadPackageMetadata } from "./config.js";
|
|
14
14
|
import { handleDocs, handleHelp } from "./docs.js";
|
|
15
|
+
import { handleDynamicConfig } from "./commands/dynamic-config.js";
|
|
15
16
|
import { printResult } from "./output.js";
|
|
16
17
|
export async function main() {
|
|
17
18
|
const args = process.argv.slice(2);
|
|
@@ -76,6 +77,10 @@ export async function main() {
|
|
|
76
77
|
await handleFileResource(client, args, global, "config");
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
80
|
+
if (command === "dynamic-config") {
|
|
81
|
+
await handleDynamicConfig(client, args, global);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
79
84
|
if (command === "report") {
|
|
80
85
|
await handleReport(client, args, global);
|
|
81
86
|
return;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function reportDashboardCsv(value) {
|
|
2
|
+
const exports = reportDashboardCsvExports(value);
|
|
3
|
+
if (exports.length !== 1) {
|
|
4
|
+
throw new Error(`CSV output to stdout or one file requires exactly one matching chart, but the query returned ${exports.length}`);
|
|
5
|
+
}
|
|
6
|
+
return exports[0];
|
|
7
|
+
}
|
|
8
|
+
export function reportDashboardCsvExports(value) {
|
|
9
|
+
const dashboard = recordValue(value);
|
|
10
|
+
const results = Array.isArray(dashboard?.results) ? dashboard.results : [];
|
|
11
|
+
if (!results.length)
|
|
12
|
+
throw new Error("CSV export did not receive any matching chart results");
|
|
13
|
+
return results.map(reportResultCsv);
|
|
14
|
+
}
|
|
15
|
+
function reportResultCsv(value) {
|
|
16
|
+
const result = recordValue(value);
|
|
17
|
+
if (!result)
|
|
18
|
+
throw new Error("CSV export received an invalid report result");
|
|
19
|
+
if (typeof result.error === "string" && result.error.trim()) {
|
|
20
|
+
throw new Error(`cannot export failed report result: ${result.error.trim()}`);
|
|
21
|
+
}
|
|
22
|
+
const rows = Array.isArray(result.rows)
|
|
23
|
+
? result.rows.map((row) => recordValue(row) || {})
|
|
24
|
+
: [];
|
|
25
|
+
const declaredColumns = Array.isArray(result.columns)
|
|
26
|
+
? result.columns.filter((column) => typeof column === "string" && Boolean(column.trim()))
|
|
27
|
+
: [];
|
|
28
|
+
const columns = uniqueStrings(declaredColumns.length ? declaredColumns : rows.flatMap((row) => Object.keys(row)));
|
|
29
|
+
if (!columns.length) {
|
|
30
|
+
throw new Error("CSV export result is not ready because the Server returned no columns; retry with --refresh or wait for the background refresh");
|
|
31
|
+
}
|
|
32
|
+
const lines = [
|
|
33
|
+
columns.map(csvCell).join(","),
|
|
34
|
+
...rows.map((row) => columns.map((column) => csvCell(row[column])).join(",")),
|
|
35
|
+
];
|
|
36
|
+
const chart = recordValue(result.chart);
|
|
37
|
+
const rowLimit = positiveIntegerOrUndefined(result.rowLimit);
|
|
38
|
+
return {
|
|
39
|
+
content: `\uFEFF${lines.join("\r\n")}\r\n`,
|
|
40
|
+
columns,
|
|
41
|
+
rowCount: rows.length,
|
|
42
|
+
reportId: optionalString(result.reportId),
|
|
43
|
+
chartId: optionalString(chart?.id),
|
|
44
|
+
chartTitle: optionalString(chart?.title),
|
|
45
|
+
truncated: result.truncated === true,
|
|
46
|
+
...(rowLimit ? { rowLimit } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function csvCell(value) {
|
|
50
|
+
let text;
|
|
51
|
+
if (value === null || value === undefined)
|
|
52
|
+
text = "";
|
|
53
|
+
else if (typeof value === "string")
|
|
54
|
+
text = protectSpreadsheetFormula(value);
|
|
55
|
+
else if (typeof value === "number")
|
|
56
|
+
text = Number.isFinite(value) ? String(value) : "";
|
|
57
|
+
else if (typeof value === "boolean" || typeof value === "bigint")
|
|
58
|
+
text = String(value);
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
text = JSON.stringify(value) ?? "";
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
text = String(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (/[",\r\n]/.test(text) || /^\s|\s$/.test(text))
|
|
68
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
function protectSpreadsheetFormula(value) {
|
|
72
|
+
return /^\s*[=+\-@]/.test(value) ? `'${value}` : value;
|
|
73
|
+
}
|
|
74
|
+
function recordValue(value) {
|
|
75
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
76
|
+
? value
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
function uniqueStrings(values) {
|
|
80
|
+
return [...new Set(values)];
|
|
81
|
+
}
|
|
82
|
+
function optionalString(value) {
|
|
83
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
84
|
+
}
|
|
85
|
+
function positiveIntegerOrUndefined(value) {
|
|
86
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
87
|
+
}
|
package/package.json
CHANGED
package/skills/gamealgo/SKILL.md
CHANGED
|
@@ -37,7 +37,7 @@ Use each topic's section list as the router. Read the whole topic only when the
|
|
|
37
37
|
- Calling or changing a concrete public SDK function: additionally run `gamealgo docs api <function> --platform <platform>` for each affected function. Never infer one platform's API from another.
|
|
38
38
|
- Adding or changing events: additionally run `gamealgo events list --days 30 --json` before defining fields or names.
|
|
39
39
|
- Querying an existing dashboard or report: read `gamealgo docs reports --section report-query` plus the section that defines the relevant metric or template. Creating, changing, previewing, or publishing Report Packs, charts, report SQL, or business metrics: list the `reports` sections and read every section affected by the change before interpreting data or editing definitions.
|
|
40
|
-
- Creating or changing remote configuration, strategies, experiments, variants, objectives, rollout, or assessment: list the `experiments` sections and read every section affected by the change; also load the integration plan and exact API documentation when client code changes.
|
|
40
|
+
- Creating or changing remote configuration, dynamic config pipelines, strategies, experiments, variants, objectives, rollout, or assessment: list the `experiments` sections and read every section affected by the change; also load the integration plan and exact API documentation when client code changes.
|
|
41
41
|
- Adjust or marketing attribution work: load the selected optimization task when one applies, read every topic in its `docs`, and load the integration plan and exact identifier/attribution APIs for the target platform.
|
|
42
42
|
|
|
43
43
|
If work crosses domains, combine their requirements. For example, an advertising event used by an experiment report requires the integration plan, event inventory, SDK API documentation, relevant report sections, and relevant experiment sections. Read a section once per turn unless the task expands or the Server documentation version changes.
|
|
@@ -53,6 +53,18 @@ For initial integration or business dashboard work, standard dashboards are only
|
|
|
53
53
|
5. Before publishing reports, remote configuration, scripts, strategies, experiments, rollout changes, or other externally visible mutations, show the relevant diff and impact unless the user has already explicitly authorized that exact operation.
|
|
54
54
|
6. After execution, verify with real configuration retrieval, accepted non-debug events, report results, or experiment state as appropriate. A code diff or successful request alone is not business verification.
|
|
55
55
|
|
|
56
|
+
## Keep Production Analysis Off The Developer Workstation
|
|
57
|
+
|
|
58
|
+
For production telemetry investigations, execute partition-scale scans and aggregations with managed Alibaba Cloud compute such as DataWorks/MaxCompute or Kyuubi/Spark, and return only the small aggregated result needed for diagnosis.
|
|
59
|
+
|
|
60
|
+
The built-in domestic Data Bridge Paimon backend is not a distributed SQL pushdown engine: PyPaimon plans Paimon splits, reads OSS data into an Arrow table inside the Bridge pod, and only then executes the SQL with DuckDB. The current runner also does not translate SQL predicates into `read_builder.with_filter(...)`, so SQL `dt`, `game`, or `platform` filters do not guarantee Paimon split pruning. Do not use this backend for production raw/enriched partition scans or describe it as Paimon aggregating inside the storage service. Limit it to tightly bounded summary-table reads or emergency diagnostics until the query path is backed by managed compute.
|
|
61
|
+
|
|
62
|
+
- Never use the local PyPaimon/DuckDB launcher to calculate production counts, DAU, hourly volume, event coverage, or source-to-enriched reconciliation. That launcher materializes selected Paimon partition data on the local machine even when the submitted SQL contains only aggregates.
|
|
63
|
+
- Do not pull production detail rows or whole partitions to the local workspace and aggregate them afterward.
|
|
64
|
+
- Put partition filters, game filters, grouping, distinct counts, joins, and same-elapsed-window comparisons in the SQL sent to the managed query engine, and verify its execution plan actually prunes partitions and columns.
|
|
65
|
+
- Local Paimon access is limited to catalog/schema/partition metadata and a deliberately tiny `LIMIT` sample when the schema cannot otherwise be established.
|
|
66
|
+
- If no managed query path is available, stop and give the developer a bounded SQL query to run. Do not silently fall back to a local or Bridge-Pod partition scan.
|
|
67
|
+
|
|
56
68
|
Reading documentation does not grant permission to mutate production. CLI and Server validation, scopes, project binding, and explicit approval requirements still apply.
|
|
57
69
|
|
|
58
70
|
## Integration Details
|
|
@@ -69,6 +81,39 @@ Reuse events with the same business meaning, register new fields with their real
|
|
|
69
81
|
|
|
70
82
|
For every game, prefer stable, human-readable Chinese values for business dimensions that will appear directly in report legends, axes, selectors, or tables. This includes values such as level or stage type, game mode, placement, item name, and business outcome or failure reason. Keep event names, field names, IDs, SQL aliases, and protocol-defined enums in their required technical format. Do not rename values already used by a live game solely to translate them, because that would split one historical dimension into multiple report values.
|
|
71
83
|
|
|
84
|
+
## Bound Custom Event Volume
|
|
85
|
+
|
|
86
|
+
Before adding or changing a custom event, prefer a protocol-standard event or an existing custom event with the same meaning. Keep the event type stable and put variable values in payload fields; never generate event types from item IDs, levels, timestamps, user input, or other runtime values.
|
|
87
|
+
|
|
88
|
+
Inspect every call site for render/update loops, timers, polling, transport retries, duplicate lifecycle callbacks, and re-entrant listeners. State the trigger boundary and estimate the maximum calls per user, session, context, and active user-day. Use the game's active-user estimate to describe the expected daily volume when it is available.
|
|
89
|
+
|
|
90
|
+
- Standard SDK semantic events are governed by their protocol semantics. Do not disguise custom telemetry as a standard event to bypass safeguards.
|
|
91
|
+
- The SDK limits custom events to 1,000 occurrences per `context × eventType`, 5,000 total occurrences per context, and 100 distinct custom event types per context.
|
|
92
|
+
- A custom event with an unbounded trigger or an expected rate above 1,000 calls per context requires explicit developer review before implementation. Explain the expected volume, storage and processing impact, and the guard that makes the trigger bounded.
|
|
93
|
+
- Never add client-side retries around `track`, rename an event after rejection, remove its custom-event prefix, declare it standard, raise a safety limit, or call a lower-level transport to bypass a quota.
|
|
94
|
+
- Treat an SDK `event_guard` diagnostic as evidence of a telemetry bug or unexpectedly high-frequency design. Find and fix the emission path before asking to change a limit.
|
|
95
|
+
|
|
96
|
+
Before delivery, report the estimated bound, normal-flow tests, the limiting loop or callback, and behavior once the local quota is reached.
|
|
97
|
+
|
|
98
|
+
## Treat Dynamic Config As A Production Publisher
|
|
99
|
+
|
|
100
|
+
A dynamic config pipeline is Server-side automation that turns aggregated Report Pack inputs into a live config file. It does not authorize SDK changes, detailed production-data downloads, or live publication by itself.
|
|
101
|
+
|
|
102
|
+
- Define bounded aggregate inputs with Report Pack syntax. Keep filters, joins, aggregation, and data reduction in the managed query rather than passing event detail to JavaScript.
|
|
103
|
+
- Keep transforms synchronous and deterministic. They may use only the supplied datasets, previous output, and scheduled metadata; never depend on network, filesystem, environment variables, wall-clock APIs, randomness, async functions, or Promise output.
|
|
104
|
+
- The output file name and content type belong to the reviewed pipeline definition. The script may return only file content and JSON diagnostics.
|
|
105
|
+
- Before publishing or materially updating an active pipeline, show the target game, input reports and windows, complete script diff, output file and content type, schedule, expected behavior, failure behavior, and user-facing or operational risks. Obtain explicit approval for that exact publication unless the user already gave it after seeing those details.
|
|
106
|
+
- Prefer a disabled pipeline or a local script test while details are still under discussion. After an authorized publish, trigger one run, inspect the persisted run result and generated file version, and confirm that an unchanged second run is hash-deduplicated before treating the automation as verified.
|
|
107
|
+
- Do not raise the 1 MiB script/input/output caps, the 5,000-row per-dataset cap, the 5-second execution cap, or the minimum 60-minute schedule to bypass a failing design. Reduce data in the Report Pack or simplify the transform.
|
|
108
|
+
|
|
109
|
+
Use `gamealgo dynamic-config list|get|publish|run|runs` for lifecycle operations. Dynamic config output continues through the ordinary SDK config-file delivery mechanism, so do not change client SDK code merely to consume a generated file that already uses the existing config API.
|
|
110
|
+
|
|
111
|
+
## Respect Blocked Events
|
|
112
|
+
|
|
113
|
+
A blocked event is a persistent ingestion safety state. You may inspect its diagnostics, analyze data read-only, fix the client call site, and run local validation, but you must not reactivate it, register an equivalent replacement event, declare it standard, switch credentials, call a lower-level transport, or otherwise bypass the block without fresh explicit approval. Applying ordinary catalog metadata must not reactivate a blocked or deprecated event.
|
|
114
|
+
|
|
115
|
+
Before asking to reactivate an event, tell the developer why it was blocked, the faulty call path, the implemented fix, the previous and expected event rate per context and active user-day, the validation performed, and the remaining risk. Only an unambiguous instruction given after that disclosure authorizes the exact `blocked` to `active` transition. Use the dedicated CLI activation command with its required reason and exact event-name confirmation, then verify both the catalog state and newly observed event health. Do not edit the database or repeatedly reactivate an event that becomes blocked again without a new diagnosis and approval.
|
|
116
|
+
|
|
72
117
|
## Initialize At The Earliest Safe Lifecycle Point
|
|
73
118
|
|
|
74
119
|
Initialize GameAlgo at the beginning of the earliest runtime lifecycle hook where its minimal required dependencies are available. Do not place it after save loading, scene creation, asset loading, UI construction, ad initialization, or a broad game-module bootstrap merely for organizational convenience. Any unhandled failure before the call would otherwise exclude an opened user from telemetry, and an SDK initialization watchdog cannot report a failure when the SDK was never started.
|
|
@@ -100,6 +145,23 @@ When client code implements an experiment or remote Strategy, define a stable, t
|
|
|
100
145
|
|
|
101
146
|
Before delivery, inspect changed client code for Variant or assignment-identity access and verify none of it reaches business conditionals. Test active control and treatment configs, missing configuration, and the post-promotion shape where the Variant becomes `default` while the promoted config remains active; unchanged parameters must produce unchanged game behavior regardless of the Variant name.
|
|
102
147
|
|
|
148
|
+
## Require Explicit Approval Before Publishing Experiments
|
|
149
|
+
|
|
150
|
+
Treat publishing a live experiment as a two-step workflow. Before approval, limit work to read-only analysis, experiment ideas, and local drafts. Do not publish an experiment-specific Strategy or Strategy version, create or start a Run, allocate traffic, or otherwise mutate live experiment state.
|
|
151
|
+
|
|
152
|
+
Before asking for approval, tell the developer:
|
|
153
|
+
|
|
154
|
+
- the target game and environment, eligible audience, and exclusions;
|
|
155
|
+
- the hypothesis, primary metric, and decision criterion;
|
|
156
|
+
- the exact control and treatment differences, including parameter values or scripts;
|
|
157
|
+
- the traffic allocation and planned duration, sample size, or review checkpoint;
|
|
158
|
+
- the expected benefit and realistic product, revenue, retention, user-experience, and technical risks that apply; and
|
|
159
|
+
- the stop-loss conditions and rollback or cancellation plan.
|
|
160
|
+
|
|
161
|
+
Then explicitly ask whether to publish that exact experiment. Only an unambiguous confirmation given after this disclosure authorizes publication. Earlier broad instructions such as “优化一下”, “做个实验”, or “按你判断”, and authorization for code, report, or configuration work do not authorize publishing an experiment. If a material detail changes after approval—including the target game, audience, variants, parameters or scripts, traffic, duration, objective, decision criterion, or risk profile—disclose the change and obtain fresh approval before publishing.
|
|
162
|
+
|
|
163
|
+
After an authorized publication, retrieve and report the actual Strategy and Run IDs, status, scope, and rollback path. Do not silently create another experiment, Run, round, or rollout. A direct request to pause, cancel, or roll back an existing experiment is sufficient authority for that protective action and must not be blocked by this publication gate.
|
|
164
|
+
|
|
103
165
|
## Protect D0 Monetization In Maker Experiments
|
|
104
166
|
|
|
105
167
|
For TapTap Maker and other Maker games, do not autonomously design, create, or start an experiment that removes all ads from a user's first local day, delays the first ad until D1 or later, or otherwise moves the game's core D0 ad inventory to a later day. Many Maker games lose a large share of users before D1, so this is a direct D0 revenue-risk decision rather than an ordinary ad-timing adjustment. A broad request to optimize ads is not approval for this design.
|