codexmobile-relay / scripts /lark-cli-guard.mjs
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
5.66 kB
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
const args = process.argv.slice(2);
const realCli = process.env.CODEXMOBILE_REAL_LARK_CLI || 'lark-cli';
const stateDir = process.env.CODEXMOBILE_LARK_GUARD_STATE_DIR || process.cwd();
const turnId = process.env.CODEXMOBILE_TURN_ID || process.env.CODEXMOBILE_SESSION_ID || 'unknown-turn';
function argValue(flag) {
const index = args.indexOf(flag);
if (index < 0 || index + 1 >= args.length) {
return '';
}
return String(args[index + 1] || '').trim();
}
function isSlidesCreate() {
return args[0] === 'slides' && args[1] === '+create';
}
function stateKey() {
const title = argValue('--title') || '(untitled)';
return `${turnId}:${title.toLowerCase()}`;
}
function redacted(value) {
return String(value || '')
.replace(/"appSecret"\s*:\s*"[^"]+"/gi, '"appSecret":"****"')
.replace(/"access[_-]?token"\s*:\s*"[^"]+"/gi, '"accessToken":"****"')
.replace(/"refresh[_-]?token"\s*:\s*"[^"]+"/gi, '"refreshToken":"****"')
.replace(/\b(u|ur|t)-[A-Za-z0-9._-]{20,}\b/g, '$1-[hidden]')
.replace(/sk-[A-Za-z0-9._-]+/g, 'sk-[hidden]');
}
async function readState(filePath) {
try {
const raw = await fs.readFile(filePath, 'utf8');
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
async function writeState(filePath, state) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf8');
}
function isPreCreateValidationFailure(text) {
return /--slides invalid json|invalid json, must be an array|unknown flag|required flag|auth|permission|forbidden|unauthorized|no user logged in|network|econn|timeout/i.test(text);
}
async function maybeBlockDuplicate(stateFile) {
if (!isSlidesCreate()) {
return false;
}
const state = await readState(stateFile);
const key = stateKey();
const existing = state[key];
if (!existing?.attemptedAt) {
return false;
}
const title = argValue('--title') || '(untitled)';
const hint = {
ok: false,
error: {
type: 'codexmobile_duplicate_slides_create_blocked',
message: `This turn already ran slides +create for "${title}". Do not create another PPT. Reuse the previous xml_presentation_id and repair/append pages on the same presentation.`,
previous: {
title,
attemptedAt: existing.attemptedAt,
xmlPresentationId: existing.xmlPresentationId || '',
output: existing.output || ''
},
nextStep: 'Run lark-cli slides xml_presentations get, then use xml_presentation.slide.create or +replace-slide on the existing PPT.'
}
};
process.stderr.write(`${JSON.stringify(hint, null, 2)}\n`);
process.exitCode = 24;
return true;
}
function extractPresentationId(text) {
const value = String(text || '');
const jsonMatch = value.match(/"xml_presentation_id"\s*:\s*"([^"]+)"/i);
if (jsonMatch) {
return jsonMatch[1];
}
const urlMatch = value.match(/\/slides\/([A-Za-z0-9_-]+)/i);
return urlMatch?.[1] || '';
}
async function recordCreateAttempt(stateFile, stdout, stderr, code) {
if (!isSlidesCreate()) {
return;
}
const combined = `${stdout}\n${stderr}`;
const xmlPresentationId = extractPresentationId(combined);
if (code !== 0 && !xmlPresentationId && isPreCreateValidationFailure(combined)) {
return;
}
const state = await readState(stateFile);
const key = stateKey();
state[key] = {
attemptedAt: new Date().toISOString(),
title: argValue('--title') || '(untitled)',
xmlPresentationId,
output: redacted(combined).slice(-1600)
};
await writeState(stateFile, state);
}
function larkCliSpawnCommand() {
if (process.platform === 'win32' && /\.cmd$|\.bat$/i.test(realCli)) {
const commandLine = ['call', windowsCmdQuote(realCli), ...args.map(windowsCmdQuote)].join(' ');
return {
command: process.env.ComSpec || 'cmd.exe',
args: ['/d', '/c', commandLine],
windowsVerbatimArguments: true
};
}
return {
command: realCli,
args,
windowsVerbatimArguments: false
};
}
function windowsCmdQuote(value) {
return `"${String(value || '').replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
}
async function main() {
const stateFile = path.join(stateDir, 'lark-slides-create-guard.json');
if (await maybeBlockDuplicate(stateFile)) {
return;
}
const cli = larkCliSpawnCommand();
const child = spawn(cli.command, cli.args, {
cwd: process.cwd(),
env: {
...process.env,
CODEXMOBILE_LARK_GUARD_CHILD: '1'
},
shell: false,
windowsHide: true,
windowsVerbatimArguments: cli.windowsVerbatimArguments
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk) => {
const text = chunk.toString('utf8');
stdout += text;
});
child.stderr?.on('data', (chunk) => {
const text = chunk.toString('utf8');
stderr += text;
});
child.on('error', (error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});
child.on('close', async (code) => {
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
await recordCreateAttempt(stateFile, stdout, stderr, code || 0).catch((error) => {
process.stderr.write(`[codexmobile-lark-guard] failed to record state: ${error.message}\n`);
});
process.exitCode = code || 0;
});
}
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});