@osfactory/har 0.13.1 → 0.14.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/dist/index.js +204 -34
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23129,6 +23129,22 @@ var AgentSessionUsageSchema = external_exports.object({
|
|
|
23129
23129
|
var SyncUsageInputSchema = external_exports.object({
|
|
23130
23130
|
usage: external_exports.array(AgentSessionUsageSchema)
|
|
23131
23131
|
});
|
|
23132
|
+
var AgentSessionEventSchema = external_exports.object({
|
|
23133
|
+
sessionKey: external_exports.string().min(1),
|
|
23134
|
+
agentId: external_exports.number().int().min(HAR_AGENT_SLOT_MIN),
|
|
23135
|
+
agentTool: AgentToolSchema,
|
|
23136
|
+
eventName: external_exports.string().min(1),
|
|
23137
|
+
sequence: external_exports.number().int().nonnegative().default(0),
|
|
23138
|
+
timestamp: external_exports.string(),
|
|
23139
|
+
attributes: external_exports.record(external_exports.unknown()).optional(),
|
|
23140
|
+
promptText: external_exports.string().nullable().optional(),
|
|
23141
|
+
responseText: external_exports.string().nullable().optional(),
|
|
23142
|
+
rawTruncated: external_exports.string().nullable().optional(),
|
|
23143
|
+
source: external_exports.enum(["otel", "harvest"]).default("otel")
|
|
23144
|
+
});
|
|
23145
|
+
var SyncSessionEventsInputSchema = external_exports.object({
|
|
23146
|
+
events: external_exports.array(AgentSessionEventSchema)
|
|
23147
|
+
});
|
|
23132
23148
|
|
|
23133
23149
|
// src/harness/manifest.ts
|
|
23134
23150
|
var GENERATOR_VERSION = "0.4.0";
|
|
@@ -29915,6 +29931,18 @@ function createRemoteExecutor(apiUrl, apiKey) {
|
|
|
29915
29931
|
var fs33 = __toESM(require("fs"));
|
|
29916
29932
|
var os9 = __toESM(require("os"));
|
|
29917
29933
|
var path33 = __toESM(require("path"));
|
|
29934
|
+
var DEFAULT_SIGNALS_ON = {
|
|
29935
|
+
metrics: true,
|
|
29936
|
+
logs: true,
|
|
29937
|
+
prompts: false,
|
|
29938
|
+
traces: false
|
|
29939
|
+
};
|
|
29940
|
+
var DEFAULT_SIGNALS_OFF = {
|
|
29941
|
+
metrics: false,
|
|
29942
|
+
logs: false,
|
|
29943
|
+
prompts: false,
|
|
29944
|
+
traces: false
|
|
29945
|
+
};
|
|
29918
29946
|
function getPreferencePath() {
|
|
29919
29947
|
if (process.env.HAR_TELEMETRY_CONFIG_PATH) {
|
|
29920
29948
|
return path33.resolve(process.env.HAR_TELEMETRY_CONFIG_PATH);
|
|
@@ -29928,24 +29956,41 @@ function parseEnvOverride(raw) {
|
|
|
29928
29956
|
if (["1", "true", "on", "yes", "enabled"].includes(normalized)) return true;
|
|
29929
29957
|
return void 0;
|
|
29930
29958
|
}
|
|
29959
|
+
function normalizeSignals(enabled, raw) {
|
|
29960
|
+
if (!enabled) return { ...DEFAULT_SIGNALS_OFF };
|
|
29961
|
+
return {
|
|
29962
|
+
metrics: raw?.metrics !== false,
|
|
29963
|
+
logs: raw?.logs !== false,
|
|
29964
|
+
prompts: raw?.prompts === true,
|
|
29965
|
+
traces: raw?.traces === true
|
|
29966
|
+
};
|
|
29967
|
+
}
|
|
29931
29968
|
function readTelemetryPreference() {
|
|
29932
29969
|
const preferencePath = getPreferencePath();
|
|
29933
29970
|
try {
|
|
29934
29971
|
if (!fs33.existsSync(preferencePath)) {
|
|
29935
|
-
return { enabled: true };
|
|
29972
|
+
return { enabled: true, signals: { ...DEFAULT_SIGNALS_ON } };
|
|
29936
29973
|
}
|
|
29937
29974
|
const parsed = JSON.parse(fs33.readFileSync(preferencePath, "utf8"));
|
|
29975
|
+
const enabled = parsed.enabled !== false;
|
|
29938
29976
|
return {
|
|
29939
|
-
enabled
|
|
29977
|
+
enabled,
|
|
29978
|
+
signals: normalizeSignals(enabled, parsed.signals),
|
|
29940
29979
|
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : void 0
|
|
29941
29980
|
};
|
|
29942
29981
|
} catch {
|
|
29943
|
-
return { enabled: true };
|
|
29982
|
+
return { enabled: true, signals: { ...DEFAULT_SIGNALS_ON } };
|
|
29944
29983
|
}
|
|
29945
29984
|
}
|
|
29946
|
-
function writeTelemetryPreference(enabled) {
|
|
29985
|
+
function writeTelemetryPreference(enabled, signals) {
|
|
29986
|
+
const current = readTelemetryPreference();
|
|
29987
|
+
const nextSignals = normalizeSignals(enabled, {
|
|
29988
|
+
...current.signals,
|
|
29989
|
+
...signals
|
|
29990
|
+
});
|
|
29947
29991
|
const preference = {
|
|
29948
29992
|
enabled,
|
|
29993
|
+
signals: enabled ? nextSignals : { ...DEFAULT_SIGNALS_OFF },
|
|
29949
29994
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29950
29995
|
};
|
|
29951
29996
|
const preferencePath = getPreferencePath();
|
|
@@ -29958,12 +30003,18 @@ function isTelemetryEnabled() {
|
|
|
29958
30003
|
if (envOverride !== void 0) return envOverride;
|
|
29959
30004
|
return readTelemetryPreference().enabled;
|
|
29960
30005
|
}
|
|
30006
|
+
function getTelemetrySignals() {
|
|
30007
|
+
if (!isTelemetryEnabled()) return { ...DEFAULT_SIGNALS_OFF };
|
|
30008
|
+
return readTelemetryPreference().signals;
|
|
30009
|
+
}
|
|
29961
30010
|
function getTelemetryPreferencePath() {
|
|
29962
30011
|
return getPreferencePath();
|
|
29963
30012
|
}
|
|
29964
30013
|
var TELEMETRY_SIGNALS = [
|
|
29965
|
-
"
|
|
29966
|
-
"
|
|
30014
|
+
"Metrics (default): tokens and estimated USD cost via OTEL metrics",
|
|
30015
|
+
"Logs/events (default): session events (tool calls, api_request) without prompt bodies",
|
|
30016
|
+
"Prompts (opt-in): user/assistant text via OTEL_LOG_USER_PROMPTS \u2014 har telemetry on --prompts",
|
|
30017
|
+
"Traces (opt-in): thin span ingest when CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1",
|
|
29967
30018
|
"Fallback: har control sync harvests local Claude/Codex session files when OTEL is missing"
|
|
29968
30019
|
];
|
|
29969
30020
|
|
|
@@ -30006,13 +30057,31 @@ function buildTelemetryEnvBlock(attrs, options) {
|
|
|
30006
30057
|
];
|
|
30007
30058
|
const injectOtel = isTelemetryEnabled() && options?.otelReady !== false;
|
|
30008
30059
|
if (injectOtel) {
|
|
30060
|
+
const signals = getTelemetrySignals();
|
|
30009
30061
|
lines.push(
|
|
30010
30062
|
"# HAR telemetry \u2192 Mission Control OTLP (disable: har telemetry off)",
|
|
30011
30063
|
"CLAUDE_CODE_ENABLE_TELEMETRY=1",
|
|
30012
|
-
"OTEL_METRICS_EXPORTER=otlp",
|
|
30013
30064
|
"OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
|
|
30014
30065
|
`OTEL_EXPORTER_OTLP_ENDPOINT=${apiUrl}/api/otel`
|
|
30015
30066
|
);
|
|
30067
|
+
if (signals.metrics) {
|
|
30068
|
+
lines.push("OTEL_METRICS_EXPORTER=otlp");
|
|
30069
|
+
}
|
|
30070
|
+
if (signals.logs) {
|
|
30071
|
+
lines.push("OTEL_LOGS_EXPORTER=otlp");
|
|
30072
|
+
}
|
|
30073
|
+
if (signals.prompts) {
|
|
30074
|
+
lines.push(
|
|
30075
|
+
"OTEL_LOG_USER_PROMPTS=1",
|
|
30076
|
+
"OTEL_LOG_ASSISTANT_RESPONSES=1"
|
|
30077
|
+
);
|
|
30078
|
+
}
|
|
30079
|
+
if (signals.traces) {
|
|
30080
|
+
lines.push(
|
|
30081
|
+
"OTEL_TRACES_EXPORTER=otlp",
|
|
30082
|
+
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1"
|
|
30083
|
+
);
|
|
30084
|
+
}
|
|
30016
30085
|
}
|
|
30017
30086
|
return lines.join("\n") + "\n";
|
|
30018
30087
|
}
|
|
@@ -30111,19 +30180,12 @@ function extractClaudeUsageFromRecords(records) {
|
|
|
30111
30180
|
}
|
|
30112
30181
|
return { tokensInput, tokensOutput, tokensCacheRead, tokensCacheCreation, costUsd };
|
|
30113
30182
|
}
|
|
30114
|
-
function
|
|
30183
|
+
function findMatchingClaudeTranscripts(slot) {
|
|
30115
30184
|
const targets = [slot.workDir, slot.worktreePath].filter(Boolean);
|
|
30116
|
-
if (targets.length === 0) return
|
|
30185
|
+
if (targets.length === 0) return [];
|
|
30117
30186
|
const root = claudeProjectsRoot();
|
|
30118
|
-
if (!fs35.existsSync(root)) return
|
|
30119
|
-
const
|
|
30120
|
-
branch: slot.branch,
|
|
30121
|
-
agentId: slot.agentId,
|
|
30122
|
-
suffix: slot.suffix,
|
|
30123
|
-
createdAt: slot.sessionCreatedAt
|
|
30124
|
-
});
|
|
30125
|
-
let best = null;
|
|
30126
|
-
let bestMtime = 0;
|
|
30187
|
+
if (!fs35.existsSync(root)) return [];
|
|
30188
|
+
const matches = [];
|
|
30127
30189
|
for (const entry of fs35.readdirSync(root, { withFileTypes: true })) {
|
|
30128
30190
|
if (!entry.isDirectory()) continue;
|
|
30129
30191
|
const projectDir = path35.join(root, entry.name);
|
|
@@ -30143,16 +30205,70 @@ function harvestClaudeUsage(slot) {
|
|
|
30143
30205
|
}
|
|
30144
30206
|
}
|
|
30145
30207
|
if (!cwdHit) continue;
|
|
30146
|
-
|
|
30147
|
-
if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
|
|
30148
|
-
continue;
|
|
30149
|
-
}
|
|
30150
|
-
if (stat.mtimeMs >= bestMtime) {
|
|
30151
|
-
bestMtime = stat.mtimeMs;
|
|
30152
|
-
best = usage2;
|
|
30153
|
-
}
|
|
30208
|
+
matches.push({ filePath, records, mtimeMs: stat.mtimeMs });
|
|
30154
30209
|
}
|
|
30155
30210
|
}
|
|
30211
|
+
return matches.sort((a2, b2) => b2.mtimeMs - a2.mtimeMs);
|
|
30212
|
+
}
|
|
30213
|
+
function extractPromptEvents(records, sessionKey, slot) {
|
|
30214
|
+
const events = [];
|
|
30215
|
+
let sequence = 0;
|
|
30216
|
+
for (const record2 of records) {
|
|
30217
|
+
if (!record2 || typeof record2 !== "object") continue;
|
|
30218
|
+
const payload = record2;
|
|
30219
|
+
const typ = String(payload.type ?? "");
|
|
30220
|
+
const message = payload.message;
|
|
30221
|
+
const role = String(message?.role ?? payload.role ?? "");
|
|
30222
|
+
let text = null;
|
|
30223
|
+
const content = message?.content ?? payload.content;
|
|
30224
|
+
if (typeof content === "string") text = content;
|
|
30225
|
+
else if (Array.isArray(content)) {
|
|
30226
|
+
text = content.map((part) => {
|
|
30227
|
+
if (typeof part === "string") return part;
|
|
30228
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
30229
|
+
return part.text;
|
|
30230
|
+
}
|
|
30231
|
+
return "";
|
|
30232
|
+
}).filter(Boolean).join("\n");
|
|
30233
|
+
}
|
|
30234
|
+
if (!text || text.trim().length === 0) continue;
|
|
30235
|
+
const isUser = typ === "user" || role === "user" || typ === "human" || Boolean(payload.userType);
|
|
30236
|
+
const isAssistant = typ === "assistant" || role === "assistant";
|
|
30237
|
+
if (!isUser && !isAssistant) continue;
|
|
30238
|
+
sequence += 1;
|
|
30239
|
+
const timestamp = typeof payload.timestamp === "string" ? payload.timestamp : typeof payload.ts === "string" ? payload.ts : (/* @__PURE__ */ new Date()).toISOString();
|
|
30240
|
+
events.push({
|
|
30241
|
+
sessionKey,
|
|
30242
|
+
agentId: slot.agentId,
|
|
30243
|
+
agentTool: "claude_code",
|
|
30244
|
+
eventName: isUser ? "claude_code.user_prompt" : "claude_code.assistant_response",
|
|
30245
|
+
sequence,
|
|
30246
|
+
timestamp,
|
|
30247
|
+
promptText: isUser ? text.slice(0, 8e3) : null,
|
|
30248
|
+
responseText: isAssistant ? text.slice(0, 8e3) : null,
|
|
30249
|
+
rawTruncated: text.slice(0, 4e3),
|
|
30250
|
+
source: "harvest"
|
|
30251
|
+
});
|
|
30252
|
+
}
|
|
30253
|
+
return events;
|
|
30254
|
+
}
|
|
30255
|
+
function harvestClaudeUsage(slot) {
|
|
30256
|
+
const matches = findMatchingClaudeTranscripts(slot);
|
|
30257
|
+
const sessionKey = buildSessionKey({
|
|
30258
|
+
branch: slot.branch,
|
|
30259
|
+
agentId: slot.agentId,
|
|
30260
|
+
suffix: slot.suffix,
|
|
30261
|
+
createdAt: slot.sessionCreatedAt
|
|
30262
|
+
});
|
|
30263
|
+
let best = null;
|
|
30264
|
+
for (const match of matches) {
|
|
30265
|
+
const usage2 = extractClaudeUsageFromRecords(match.records);
|
|
30266
|
+
if (usage2.tokensInput + usage2.tokensOutput + usage2.tokensCacheRead === 0 && (usage2.costUsd == null || usage2.costUsd === 0)) {
|
|
30267
|
+
continue;
|
|
30268
|
+
}
|
|
30269
|
+
best = usage2;
|
|
30270
|
+
break;
|
|
30271
|
+
}
|
|
30156
30272
|
if (!best) return null;
|
|
30157
30273
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
30158
30274
|
const tokensTotal = best.tokensInput + best.tokensOutput + best.tokensCacheRead + best.tokensCacheCreation;
|
|
@@ -30174,6 +30290,18 @@ function harvestClaudeUsage(slot) {
|
|
|
30174
30290
|
lastSeenAt: now
|
|
30175
30291
|
};
|
|
30176
30292
|
}
|
|
30293
|
+
function harvestClaudeEvents(slot) {
|
|
30294
|
+
if (!getTelemetrySignals().prompts) return [];
|
|
30295
|
+
const matches = findMatchingClaudeTranscripts(slot);
|
|
30296
|
+
if (matches.length === 0) return [];
|
|
30297
|
+
const sessionKey = buildSessionKey({
|
|
30298
|
+
branch: slot.branch,
|
|
30299
|
+
agentId: slot.agentId,
|
|
30300
|
+
suffix: slot.suffix,
|
|
30301
|
+
createdAt: slot.sessionCreatedAt
|
|
30302
|
+
});
|
|
30303
|
+
return extractPromptEvents(matches[0].records, sessionKey, slot).slice(-40);
|
|
30304
|
+
}
|
|
30177
30305
|
|
|
30178
30306
|
// src/core/usage-harvest/codex.ts
|
|
30179
30307
|
var fs36 = __toESM(require("fs"));
|
|
@@ -30300,6 +30428,9 @@ function harvestUsageForSlot(slot) {
|
|
|
30300
30428
|
if (codex) out.push(codex);
|
|
30301
30429
|
return out;
|
|
30302
30430
|
}
|
|
30431
|
+
function harvestEventsForSlot(slot) {
|
|
30432
|
+
return harvestClaudeEvents(slot);
|
|
30433
|
+
}
|
|
30303
30434
|
|
|
30304
30435
|
// src/core/control-sync.ts
|
|
30305
30436
|
async function postJson(url, body, dryRun) {
|
|
@@ -30454,6 +30585,21 @@ async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun) {
|
|
|
30454
30585
|
const usageBody = SyncUsageInputSchema.parse({ usage: usage2 });
|
|
30455
30586
|
await postJson(`${apiUrl}/api/repos/${repoId}/usage`, usageBody, dryRun);
|
|
30456
30587
|
}
|
|
30588
|
+
const events = status.slots.flatMap(
|
|
30589
|
+
(slot) => harvestEventsForSlot({
|
|
30590
|
+
agentId: slot.agentId,
|
|
30591
|
+
workDir: slot.workDir,
|
|
30592
|
+
worktreePath: slot.worktreePath,
|
|
30593
|
+
branch: slot.branch,
|
|
30594
|
+
suffix: slot.suffix,
|
|
30595
|
+
sessionCreatedAt: slot.sessionCreatedAt,
|
|
30596
|
+
repoPath
|
|
30597
|
+
})
|
|
30598
|
+
);
|
|
30599
|
+
if (events.length > 0) {
|
|
30600
|
+
const eventsBody = SyncSessionEventsInputSchema.parse({ events });
|
|
30601
|
+
await postJson(`${apiUrl}/api/repos/${repoId}/events`, eventsBody, dryRun);
|
|
30602
|
+
}
|
|
30457
30603
|
} catch (err) {
|
|
30458
30604
|
const message = err instanceof Error ? err.message : String(err);
|
|
30459
30605
|
process.stderr.write(`[har control] usage harvest skipped: ${message}
|
|
@@ -40151,18 +40297,20 @@ function printSignals() {
|
|
|
40151
40297
|
async function handleStatus3(argv) {
|
|
40152
40298
|
const preference = readTelemetryPreference();
|
|
40153
40299
|
const enabled = isTelemetryEnabled();
|
|
40300
|
+
const signals = getTelemetrySignals();
|
|
40154
40301
|
const apiUrl = getControlApiUrl();
|
|
40155
40302
|
const reachable = enabled ? await isControlApiReachable(apiUrl) : false;
|
|
40156
40303
|
const payload = {
|
|
40157
40304
|
enabled,
|
|
40158
40305
|
preferenceEnabled: preference.enabled,
|
|
40306
|
+
signals,
|
|
40159
40307
|
preferencePath: getTelemetryPreferencePath(),
|
|
40160
40308
|
updatedAt: preference.updatedAt ?? null,
|
|
40161
40309
|
envOverride: process.env.HAR_TELEMETRY ?? null,
|
|
40162
40310
|
controlApiUrl: apiUrl,
|
|
40163
40311
|
controlReachable: reachable,
|
|
40164
40312
|
otelEndpoint: `${apiUrl.replace(/\/$/, "")}/api/otel`,
|
|
40165
|
-
|
|
40313
|
+
signalDescriptions: [...TELEMETRY_SIGNALS]
|
|
40166
40314
|
};
|
|
40167
40315
|
if (argv.json) {
|
|
40168
40316
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
@@ -40172,21 +40320,33 @@ async function handleStatus3(argv) {
|
|
|
40172
40320
|
header("har telemetry status");
|
|
40173
40321
|
info(`Enabled: ${enabled ? "yes" : "no"}${process.env.HAR_TELEMETRY ? " (HAR_TELEMETRY override)" : ""}`);
|
|
40174
40322
|
info(`Preference: ${getTelemetryPreferencePath()}${preference.updatedAt ? ` (updated ${preference.updatedAt})` : " (default on)"}`);
|
|
40323
|
+
info(
|
|
40324
|
+
`Signals: metrics=${signals.metrics} logs=${signals.logs} prompts=${signals.prompts} traces=${signals.traces}`
|
|
40325
|
+
);
|
|
40175
40326
|
info(`Mission Control: ${apiUrl} (${reachable ? "reachable" : "not reachable"})`);
|
|
40176
40327
|
info(`OTLP endpoint: ${payload.otelEndpoint}`);
|
|
40177
40328
|
printSignals();
|
|
40178
40329
|
}
|
|
40179
|
-
async function handleOn() {
|
|
40330
|
+
async function handleOn(argv) {
|
|
40180
40331
|
header("har telemetry on");
|
|
40181
|
-
writeTelemetryPreference(true
|
|
40332
|
+
const preference = writeTelemetryPreference(true, {
|
|
40333
|
+
prompts: argv.prompts === true ? true : void 0,
|
|
40334
|
+
traces: argv.traces === true ? true : void 0
|
|
40335
|
+
});
|
|
40182
40336
|
success("Telemetry enabled (opt-out). Agent usage will be stored in local Mission Control.");
|
|
40337
|
+
info(
|
|
40338
|
+
`Signals: metrics=${preference.signals.metrics} logs=${preference.signals.logs} prompts=${preference.signals.prompts} traces=${preference.signals.traces}`
|
|
40339
|
+
);
|
|
40340
|
+
if (preference.signals.prompts) {
|
|
40341
|
+
warn("Prompt/response text will leave the agent machine for local Mission Control storage.");
|
|
40342
|
+
}
|
|
40183
40343
|
printSignals();
|
|
40184
40344
|
const result = await ensureTelemetryInfrastructure({ startIfNeeded: true });
|
|
40185
40345
|
if (result.message) success(result.message);
|
|
40186
40346
|
if (result.warning) warn(result.warning);
|
|
40187
40347
|
if (result.otelReady) {
|
|
40188
40348
|
info(`OTLP ingest: ${result.apiUrl.replace(/\/$/, "")}/api/otel`);
|
|
40189
|
-
info("Usage appears under Mission Control \u2192 Worktrees. Disable: har telemetry off");
|
|
40349
|
+
info("Usage appears under Mission Control \u2192 Worktrees / Usage. Disable: har telemetry off");
|
|
40190
40350
|
}
|
|
40191
40351
|
}
|
|
40192
40352
|
async function handleOff() {
|
|
@@ -40296,10 +40456,20 @@ var telemetryCommand = {
|
|
|
40296
40456
|
).command(
|
|
40297
40457
|
"on",
|
|
40298
40458
|
"Enable telemetry (default) and ensure Mission Control is running",
|
|
40299
|
-
() => {
|
|
40300
|
-
|
|
40301
|
-
|
|
40302
|
-
|
|
40459
|
+
(y2) => y2.option("prompts", {
|
|
40460
|
+
type: "boolean",
|
|
40461
|
+
default: false,
|
|
40462
|
+
describe: "Opt in to shipping user/assistant prompt text to Mission Control"
|
|
40463
|
+
}).option("traces", {
|
|
40464
|
+
type: "boolean",
|
|
40465
|
+
default: false,
|
|
40466
|
+
describe: "Opt in to thin OTEL traces ingest (Claude beta)"
|
|
40467
|
+
}),
|
|
40468
|
+
(argv) => {
|
|
40469
|
+
void handleOn({
|
|
40470
|
+
prompts: argv.prompts,
|
|
40471
|
+
traces: argv.traces
|
|
40472
|
+
});
|
|
40303
40473
|
}
|
|
40304
40474
|
).command(
|
|
40305
40475
|
"off",
|