icoa-cli 2.19.33 → 2.19.34
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/commands/ai4ctf.js +3 -1
- package/dist/commands/ctf4ai-demo.js +3 -1
- package/dist/commands/exam.js +19 -0
- package/dist/lib/log-sync.js +33 -15
- package/dist/repl.js +14 -0
- package/package.json +1 -1
package/dist/commands/ai4ctf.js
CHANGED
|
@@ -88,6 +88,9 @@ function showDemoHint(tier) {
|
|
|
88
88
|
export async function handleChatMessage(input) {
|
|
89
89
|
if (!chatSession)
|
|
90
90
|
return 'exit';
|
|
91
|
+
// Capture every input (including special commands like hint/submit/exit)
|
|
92
|
+
// so the full flow shows up in session.log even before any early return.
|
|
93
|
+
logCommand(`ai4ctf: ${input}`);
|
|
91
94
|
// Scripted demo hints — intercept before the AI chat so that typing
|
|
92
95
|
// `hint a` / `hint b` / `hint c` behaves like a real competition command
|
|
93
96
|
// instead of becoming a generic AI chat turn.
|
|
@@ -237,7 +240,6 @@ export async function handleChatMessage(input) {
|
|
|
237
240
|
console.log();
|
|
238
241
|
return 'exit';
|
|
239
242
|
}
|
|
240
|
-
logCommand(`ai4ctf: ${input}`);
|
|
241
243
|
console.log(chalk.gray(` ${t('ai4ctfThinking')}`));
|
|
242
244
|
try {
|
|
243
245
|
const response = await chatSession.sendMessage(input);
|
|
@@ -89,6 +89,9 @@ export function isCtf4aiActive() {
|
|
|
89
89
|
export async function handleCtf4aiMessage(input) {
|
|
90
90
|
if (!ctf4aiSession)
|
|
91
91
|
return 'exit';
|
|
92
|
+
// Capture every input (including special commands) for the audit trail
|
|
93
|
+
// before any early-return branches.
|
|
94
|
+
logCommand(`ctf4ai: ${input}`);
|
|
92
95
|
if (input === 'exit' || input === 'back' || input === 'quit') {
|
|
93
96
|
ctf4aiActive = false;
|
|
94
97
|
ctf4aiSession = null;
|
|
@@ -118,7 +121,6 @@ export async function handleCtf4aiMessage(input) {
|
|
|
118
121
|
printDemoReport(false, ctf4aiTokens);
|
|
119
122
|
return 'exit';
|
|
120
123
|
}
|
|
121
|
-
logCommand(`ctf4ai: ${input}`);
|
|
122
124
|
try {
|
|
123
125
|
console.log(chalk.gray(` ${t('ctf4aiThinking')}`));
|
|
124
126
|
const { text, tokensUsed } = await ctf4aiSession.sendMessage(input);
|
package/dist/commands/exam.js
CHANGED
|
@@ -9,6 +9,23 @@ import { printSuccess, printError, printWarning, printInfo, printTable, printHea
|
|
|
9
9
|
import { t } from '../lib/i18n.js';
|
|
10
10
|
import { getDeviceFingerprint } from '../lib/access.js';
|
|
11
11
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
// Fire-and-forget event POST to /api/icoa/demo-stats. Used for demo-flow
|
|
13
|
+
// choice points (demo enter, retry, back) where we want a server-side
|
|
14
|
+
// timestamp of the user's decision but have nothing more to report.
|
|
15
|
+
function reportDemoEvent(type, extra = {}) {
|
|
16
|
+
const config = getConfig();
|
|
17
|
+
fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
type,
|
|
22
|
+
lang: config.language || 'en',
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
...extra,
|
|
25
|
+
}),
|
|
26
|
+
signal: AbortSignal.timeout(5000),
|
|
27
|
+
}).catch(() => { });
|
|
28
|
+
}
|
|
12
29
|
/**
|
|
13
30
|
* Skippable intro animation for first-time demo users.
|
|
14
31
|
* Resolves immediately on any keypress. Max 30s auto-advance.
|
|
@@ -1077,6 +1094,7 @@ export function registerExamCommand(program) {
|
|
|
1077
1094
|
.description('Try a free practice exam (no account needed)')
|
|
1078
1095
|
.action(async () => {
|
|
1079
1096
|
logCommand('exam demo');
|
|
1097
|
+
reportDemoEvent('demo-enter');
|
|
1080
1098
|
const { pickDemoQuestions, getLocalizedDemoSession, DEMO_PICK_SIZE, DEMO_POOL_SIZE } = await import('../lib/demo-exam.js');
|
|
1081
1099
|
// First-time intro animation (skippable)
|
|
1082
1100
|
const cfg = getConfig();
|
|
@@ -1150,6 +1168,7 @@ export function registerExamCommand(program) {
|
|
|
1150
1168
|
.description('Retry only the questions you got wrong last demo attempt')
|
|
1151
1169
|
.action(async () => {
|
|
1152
1170
|
logCommand('exam demo-retry');
|
|
1171
|
+
reportDemoEvent('post-report-retry');
|
|
1153
1172
|
const { getLocalizedDemoSession } = await import('../lib/demo-exam.js');
|
|
1154
1173
|
const retryQueue = getRetryQueue();
|
|
1155
1174
|
if (!retryQueue || retryQueue.length === 0) {
|
package/dist/lib/log-sync.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getIcoaDir, getConfig } from './config.js';
|
|
4
|
-
|
|
4
|
+
import { getDeviceFingerprint } from './access.js';
|
|
5
|
+
// Every 30s, post new lines from ~/.icoa/session.log to the server audit
|
|
6
|
+
// endpoint. Two auth modes:
|
|
7
|
+
//
|
|
8
|
+
// 1. Logged-in user (CTFd token present) → Authorization: Token <token>,
|
|
9
|
+
// stored on the server as /opt/CTFd/audit/user-<userId>.jsonl
|
|
10
|
+
// 2. Anonymous demo user (no token) → X-Device-Fingerprint: <hash>,
|
|
11
|
+
// stored as /opt/CTFd/audit/demo-<fingerprint>.jsonl
|
|
12
|
+
//
|
|
13
|
+
// The demo path is what lets us observe the full prompt/command history of
|
|
14
|
+
// users who never log in (the main demo audience).
|
|
15
|
+
const SYNC_INTERVAL = 30_000;
|
|
16
|
+
const DEFAULT_SERVER = 'https://practice.icoa2026.au';
|
|
5
17
|
const SYNC_STATE_FILE = () => join(getIcoaDir(), 'sync-state.json');
|
|
6
18
|
let syncTimer = null;
|
|
7
19
|
function getSyncState() {
|
|
@@ -20,25 +32,37 @@ function saveSyncState(state) {
|
|
|
20
32
|
}
|
|
21
33
|
async function syncLogs() {
|
|
22
34
|
const config = getConfig();
|
|
23
|
-
|
|
24
|
-
return; // Not connected, skip
|
|
35
|
+
const serverUrl = config.ctfdUrl || DEFAULT_SERVER;
|
|
25
36
|
const logPath = join(getIcoaDir(), 'session.log');
|
|
26
37
|
if (!existsSync(logPath))
|
|
27
38
|
return;
|
|
28
39
|
const state = getSyncState();
|
|
29
40
|
const allLines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
30
|
-
// Only send new lines since last sync
|
|
31
41
|
const newLines = allLines.slice(state.lastSyncedLine);
|
|
32
42
|
if (newLines.length === 0)
|
|
33
43
|
return;
|
|
44
|
+
// Pick auth mode: token for logged-in users, device fingerprint for anon.
|
|
45
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
46
|
+
let identity;
|
|
47
|
+
if (config.token) {
|
|
48
|
+
headers['Authorization'] = `Token ${config.token}`;
|
|
49
|
+
identity = `user:${config.userId ?? 'unknown'}`;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const fp = config.deviceFingerprint || getDeviceFingerprint();
|
|
53
|
+
headers['X-Device-Fingerprint'] = fp;
|
|
54
|
+
identity = `demo:${fp.slice(0, 12)}`;
|
|
55
|
+
}
|
|
34
56
|
const payload = {
|
|
57
|
+
identity,
|
|
35
58
|
userId: config.userId,
|
|
36
59
|
userName: config.userName,
|
|
37
60
|
teamId: config.teamId,
|
|
38
61
|
sessionId: config.sessionId,
|
|
39
|
-
deviceFingerprint: config.deviceFingerprint,
|
|
62
|
+
deviceFingerprint: config.deviceFingerprint || getDeviceFingerprint(),
|
|
63
|
+
lang: config.language || 'en',
|
|
40
64
|
timestamp: new Date().toISOString(),
|
|
41
|
-
entries: newLines.map(line => {
|
|
65
|
+
entries: newLines.map((line) => {
|
|
42
66
|
try {
|
|
43
67
|
return JSON.parse(line);
|
|
44
68
|
}
|
|
@@ -48,16 +72,12 @@ async function syncLogs() {
|
|
|
48
72
|
}),
|
|
49
73
|
};
|
|
50
74
|
try {
|
|
51
|
-
|
|
52
|
-
const url = new URL('/api/icoa/audit', config.ctfdUrl).href;
|
|
75
|
+
const url = new URL('/api/icoa/audit', serverUrl).href;
|
|
53
76
|
const res = await fetch(url, {
|
|
54
77
|
method: 'POST',
|
|
55
|
-
headers
|
|
56
|
-
'Content-Type': 'application/json',
|
|
57
|
-
'Authorization': `Token ${config.token}`,
|
|
58
|
-
},
|
|
78
|
+
headers,
|
|
59
79
|
body: JSON.stringify(payload),
|
|
60
|
-
signal: AbortSignal.timeout(10_000),
|
|
80
|
+
signal: AbortSignal.timeout(10_000),
|
|
61
81
|
});
|
|
62
82
|
if (res.ok) {
|
|
63
83
|
state.lastSyncedLine = allLines.length;
|
|
@@ -77,9 +97,7 @@ async function syncLogs() {
|
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
export function startLogSync() {
|
|
80
|
-
// Initial sync after 5 seconds
|
|
81
100
|
setTimeout(() => syncLogs(), 5_000);
|
|
82
|
-
// Then every 30 seconds
|
|
83
101
|
syncTimer = setInterval(() => syncLogs(), SYNC_INTERVAL);
|
|
84
102
|
}
|
|
85
103
|
export function stopLogSync() {
|
package/dist/repl.js
CHANGED
|
@@ -360,6 +360,20 @@ export async function startRepl(program, resumeMode) {
|
|
|
360
360
|
console.log();
|
|
361
361
|
}
|
|
362
362
|
else {
|
|
363
|
+
// No active exam — typically this is the "post-report back" choice
|
|
364
|
+
// after the user has finished the 3-stage demo flow. Report it as a
|
|
365
|
+
// distinct event so the admin can count retry vs back decisions.
|
|
366
|
+
const cfg = getConfig();
|
|
367
|
+
fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
headers: { 'Content-Type': 'application/json' },
|
|
370
|
+
body: JSON.stringify({
|
|
371
|
+
type: 'post-report-back',
|
|
372
|
+
lang: cfg.language || 'en',
|
|
373
|
+
timestamp: new Date().toISOString(),
|
|
374
|
+
}),
|
|
375
|
+
signal: AbortSignal.timeout(5000),
|
|
376
|
+
}).catch(() => { });
|
|
363
377
|
console.log(chalk.gray(' Already at main menu.'));
|
|
364
378
|
}
|
|
365
379
|
rl.prompt();
|