editmamei 1.3.0 → 1.5.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 +10 -9
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/help.js +1 -1
- package/dist/core/server.js +112 -15
- package/dist/core/tool-groups.js +4 -3
- package/dist/core/tool-registry.js +3 -1
- package/dist/core/tool-tiers.js +2 -1
- package/dist/install-channel.js +52 -2
- package/dist/kernel/kernel.js +1 -0
- package/dist/kernel/module-lifecycle.js +44 -17
- package/dist/modules/ce/index.js +7 -0
- package/dist/perception/facets.js +4 -3
- package/dist/perception/region-scorer.js +1 -1
- package/dist/perception/scene-model.js +1 -1
- package/dist/perception/select-recipes.js +14 -3
- package/dist/platform/launch-readiness.js +25 -0
- package/dist/platform/macos-runner.js +18 -8
- package/dist/platform/script-queue.js +5 -19
- package/dist/platform/windows-runner.js +18 -8
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/activity.js +149 -0
- package/dist/telemetry/client.js +125 -8
- package/dist/telemetry/events.js +48 -3
- package/dist/tools/detection-tools.js +4 -4
- package/dist/tools/document-tools.js +23 -1
- package/dist/tools/preview-tools.js +1 -1
- package/dist/tools/scene-tools.js +8 -6
- package/dist/tools/selection-tools.js +155 -109
- package/dist/tools/sequence-tools.js +435 -0
- package/dist/update/check.js +24 -10
- package/dist/utils/operation-timeouts.js +92 -0
- package/dist/utils/run-script.js +25 -1
- package/dist/utils/session-log.js +18 -4
- package/dist/utils/tool-budget-context.js +14 -0
- package/dist/utils/xmp-crs.js +682 -0
- package/dist/version.js +1 -1
- package/package.json +10 -4
|
@@ -41,7 +41,7 @@ export function estimateHorizon(rowMeans, docHeight, pick, mainSubjectBox, prima
|
|
|
41
41
|
const split = pick.bright_fraction;
|
|
42
42
|
const splitConfidence = Math.max(0, Math.min(1, 1 - Math.abs(0.5 - split) * 2)) * 0.6 + 0.2;
|
|
43
43
|
if (!rowMeans || rowMeans.length === 0) {
|
|
44
|
-
return {
|
|
44
|
+
return { detected: false, reason: 'no-row-profile' };
|
|
45
45
|
}
|
|
46
46
|
const R = rowMeans.length;
|
|
47
47
|
let crossRow = -1;
|
|
@@ -52,14 +52,15 @@ export function estimateHorizon(rowMeans, docHeight, pick, mainSubjectBox, prima
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
if (crossRow <= 0) {
|
|
55
|
-
return {
|
|
55
|
+
return { detected: false, reason: 'no-luminance-crossing' };
|
|
56
56
|
}
|
|
57
57
|
const y = Math.round((crossRow / R) * docHeight);
|
|
58
58
|
if ((mainSubjectBox && isDominantSubjectCrossing(y, docHeight, mainSubjectBox)) ||
|
|
59
59
|
(primaryFaceBox && isPortraitFaceCrossing(y, docHeight, primaryFaceBox))) {
|
|
60
|
-
return {
|
|
60
|
+
return { detected: false, reason: 'dominant-subject' };
|
|
61
61
|
}
|
|
62
62
|
return {
|
|
63
|
+
detected: true,
|
|
63
64
|
y,
|
|
64
65
|
placement: y / docHeight,
|
|
65
66
|
confidence: Math.max(0, Math.min(1, splitConfidence)),
|
|
@@ -49,7 +49,7 @@ export function buildRegionSignals(info, scene) {
|
|
|
49
49
|
centroidY = (bounds.top + bounds.bottom) / 2 / docH;
|
|
50
50
|
touchesBottom = bounds.bottom >= docH * 0.97;
|
|
51
51
|
touchesTop = bounds.top <= docH * 0.03;
|
|
52
|
-
if (scene.horizonConfidence > 0) {
|
|
52
|
+
if (scene.horizonConfidence > 0 && scene.horizonY !== null) {
|
|
53
53
|
lowerEdgeAlign = 1 - Math.min(1, Math.abs(bounds.bottom - scene.horizonY) / docH);
|
|
54
54
|
upperEdgeAlign = 1 - Math.min(1, Math.abs(bounds.top - scene.horizonY) / docH);
|
|
55
55
|
}
|
|
@@ -142,7 +142,7 @@ export async function buildSceneModel(connection, snippet, client, opts = {}) {
|
|
|
142
142
|
recipe: { kind: 'posterize_region', levels: 3, sample: 'below_horizon' },
|
|
143
143
|
},
|
|
144
144
|
];
|
|
145
|
-
const composition = computeComposition(mainBox, allBoxes, docW, docH, horizon.placement);
|
|
145
|
+
const composition = computeComposition(mainBox, allBoxes, docW, docH, horizon.detected ? horizon.placement : null);
|
|
146
146
|
const backends = {
|
|
147
147
|
faces: det.result.backends.faces,
|
|
148
148
|
objects: det.result.backends.objects,
|
|
@@ -58,8 +58,8 @@ function sceneCtx(model) {
|
|
|
58
58
|
return {
|
|
59
59
|
docW: model.doc.width,
|
|
60
60
|
docH: model.doc.height,
|
|
61
|
-
horizonY: model.horizon.y,
|
|
62
|
-
horizonConfidence: model.horizon.confidence,
|
|
61
|
+
horizonY: model.horizon.detected ? model.horizon.y : null,
|
|
62
|
+
horizonConfidence: model.horizon.detected ? model.horizon.confidence : 0,
|
|
63
63
|
indoorObjectCount,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
@@ -490,7 +490,18 @@ async function resolveFaceFeature(connection, snippet, target, feature, opts) {
|
|
|
490
490
|
};
|
|
491
491
|
}
|
|
492
492
|
async function resolveAboveHorizon(connection, snippet, model, composition) {
|
|
493
|
-
const
|
|
493
|
+
const horizon = model.horizon;
|
|
494
|
+
if (!horizon.detected) {
|
|
495
|
+
return {
|
|
496
|
+
target: 'above_horizon',
|
|
497
|
+
method: 'none',
|
|
498
|
+
passed: false,
|
|
499
|
+
confidence: 0,
|
|
500
|
+
reasons: [`no horizon was measured in this frame (${horizon.reason})`],
|
|
501
|
+
detail: { horizon_detected: false, horizon_reason: horizon.reason },
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const y = Math.max(1, Math.min(model.doc.height, horizon.y));
|
|
494
505
|
const result = (await runScript(connection, await snippet.build('selectRectangle', {
|
|
495
506
|
left: 0,
|
|
496
507
|
top: 0,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DEFAULT_POLL_INTERVAL_MS = 250;
|
|
2
|
+
export const LAUNCH_READY_MAX_WAIT_MS = 5_000;
|
|
3
|
+
function defaultSleep(ms) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
setTimeout(resolve, ms).unref();
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export async function waitForLaunchReady(isReady, options = {}) {
|
|
9
|
+
const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
10
|
+
const maxWaitMs = options.maxWaitMs ?? LAUNCH_READY_MAX_WAIT_MS;
|
|
11
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
12
|
+
const isAborted = options.isAborted ?? (() => false);
|
|
13
|
+
const now = options.now ?? Date.now;
|
|
14
|
+
const deadline = now() + maxWaitMs;
|
|
15
|
+
while (now() < deadline) {
|
|
16
|
+
if (isAborted())
|
|
17
|
+
return false;
|
|
18
|
+
if (await isReady())
|
|
19
|
+
return true;
|
|
20
|
+
if (now() >= deadline)
|
|
21
|
+
break;
|
|
22
|
+
await sleep(intervalMs);
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
@@ -2,12 +2,14 @@ import { exec, spawn } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
4
|
import { TempDir } from '../utils/temp.js';
|
|
5
|
+
import { DEFAULT_SCRIPT_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
5
6
|
import { ScriptQueue } from './script-queue.js';
|
|
6
7
|
import { runChildWithTimeout } from './run-child.js';
|
|
7
8
|
import { decodeScriptResult } from './script-result.js';
|
|
9
|
+
import { waitForLaunchReady } from './launch-readiness.js';
|
|
8
10
|
const execAsync = promisify(exec);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
+
const LAUNCH_PROBE_TIMEOUT_MS = 2_000;
|
|
12
|
+
const IS_RUNNING_EXEC_TIMEOUT_MS = 3_000;
|
|
11
13
|
const UNSAFE_IN_APPLESCRIPT_LITERAL = /["\\\r\n\t]/;
|
|
12
14
|
export class MacOSScriptRunner {
|
|
13
15
|
logger = new Logger('MacOSScriptRunner');
|
|
@@ -69,7 +71,9 @@ end timeout`;
|
|
|
69
71
|
}
|
|
70
72
|
async isRunning() {
|
|
71
73
|
try {
|
|
72
|
-
const { stdout } = await execAsync('pgrep -f "Adobe Photoshop"'
|
|
74
|
+
const { stdout } = await execAsync('pgrep -f "Adobe Photoshop"', {
|
|
75
|
+
timeout: IS_RUNNING_EXEC_TIMEOUT_MS,
|
|
76
|
+
});
|
|
73
77
|
return stdout.trim().length > 0;
|
|
74
78
|
}
|
|
75
79
|
catch {
|
|
@@ -81,14 +85,20 @@ end timeout`;
|
|
|
81
85
|
this.logger.info('Launching Photoshop', executablePath);
|
|
82
86
|
const child = spawn('open', ['-a', executablePath], { detached: true, stdio: 'ignore' });
|
|
83
87
|
child.unref();
|
|
84
|
-
|
|
85
|
-
grace.unref();
|
|
86
|
-
resolve();
|
|
87
|
-
}, LAUNCH_GRACE_MS);
|
|
88
|
+
let aborted = false;
|
|
88
89
|
child.on('error', (error) => {
|
|
89
|
-
|
|
90
|
+
aborted = true;
|
|
90
91
|
reject(new Error(`Could not launch Photoshop at ${executablePath}: ${error.message}`));
|
|
91
92
|
});
|
|
93
|
+
const probe = () => this.run("'pong';", LAUNCH_PROBE_TIMEOUT_MS)
|
|
94
|
+
.then(() => true)
|
|
95
|
+
.catch(() => false);
|
|
96
|
+
waitForLaunchReady(probe, { isAborted: () => aborted })
|
|
97
|
+
.catch(() => false)
|
|
98
|
+
.then(() => {
|
|
99
|
+
if (!aborted)
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
92
102
|
});
|
|
93
103
|
}
|
|
94
104
|
}
|
|
@@ -8,23 +8,7 @@ export class ScriptQueue {
|
|
|
8
8
|
}
|
|
9
9
|
enqueue(run, timeout) {
|
|
10
10
|
return new Promise((resolve, reject) => {
|
|
11
|
-
|
|
12
|
-
run,
|
|
13
|
-
resolve,
|
|
14
|
-
reject,
|
|
15
|
-
cancelled: false,
|
|
16
|
-
settled: false,
|
|
17
|
-
timeoutId: null,
|
|
18
|
-
};
|
|
19
|
-
task.timeoutId = setTimeout(() => {
|
|
20
|
-
if (task.settled)
|
|
21
|
-
return;
|
|
22
|
-
task.cancelled = true;
|
|
23
|
-
task.settled = true;
|
|
24
|
-
task.timeoutId = null;
|
|
25
|
-
reject(new Error('Script execution timeout'));
|
|
26
|
-
}, timeout + QUEUE_SLACK_MS);
|
|
27
|
-
this.queue.push(task);
|
|
11
|
+
this.queue.push({ run, timeout, resolve, reject, settled: false, timeoutId: null });
|
|
28
12
|
void this.processQueue();
|
|
29
13
|
});
|
|
30
14
|
}
|
|
@@ -45,8 +29,10 @@ export class ScriptQueue {
|
|
|
45
29
|
try {
|
|
46
30
|
while (this.queue.length > 0) {
|
|
47
31
|
const task = this.queue.shift();
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
task.timeoutId = setTimeout(() => {
|
|
33
|
+
this.settleTask(task, () => task.reject(new Error('Script execution timeout')));
|
|
34
|
+
}, task.timeout + QUEUE_SLACK_MS);
|
|
35
|
+
task.timeoutId.unref?.();
|
|
50
36
|
try {
|
|
51
37
|
const result = await task.run();
|
|
52
38
|
this.settleTask(task, () => task.resolve(result));
|
|
@@ -2,12 +2,14 @@ import { exec, spawn } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
4
|
import { TempDir } from '../utils/temp.js';
|
|
5
|
+
import { DEFAULT_SCRIPT_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
5
6
|
import { ScriptQueue } from './script-queue.js';
|
|
6
7
|
import { runChildWithTimeout } from './run-child.js';
|
|
7
8
|
import { decodeScriptResult } from './script-result.js';
|
|
9
|
+
import { waitForLaunchReady } from './launch-readiness.js';
|
|
8
10
|
const execAsync = promisify(exec);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
+
const LAUNCH_PROBE_TIMEOUT_MS = 2_000;
|
|
12
|
+
const IS_RUNNING_EXEC_TIMEOUT_MS = 3_000;
|
|
11
13
|
export class WindowsScriptRunner {
|
|
12
14
|
logger = new Logger('WindowsScriptRunner');
|
|
13
15
|
queue = new ScriptQueue(this.logger);
|
|
@@ -59,7 +61,9 @@ End If
|
|
|
59
61
|
}
|
|
60
62
|
async isRunning() {
|
|
61
63
|
try {
|
|
62
|
-
const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"'
|
|
64
|
+
const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"', {
|
|
65
|
+
timeout: IS_RUNNING_EXEC_TIMEOUT_MS,
|
|
66
|
+
});
|
|
63
67
|
return stdout.toLowerCase().includes('photoshop.exe');
|
|
64
68
|
}
|
|
65
69
|
catch {
|
|
@@ -71,14 +75,20 @@ End If
|
|
|
71
75
|
this.logger.info('Launching Photoshop', executablePath);
|
|
72
76
|
const child = spawn(executablePath, [], { detached: true, stdio: 'ignore' });
|
|
73
77
|
child.unref();
|
|
74
|
-
|
|
75
|
-
grace.unref();
|
|
76
|
-
resolve();
|
|
77
|
-
}, LAUNCH_GRACE_MS);
|
|
78
|
+
let aborted = false;
|
|
78
79
|
child.on('error', (error) => {
|
|
79
|
-
|
|
80
|
+
aborted = true;
|
|
80
81
|
reject(new Error(`Could not launch Photoshop at ${executablePath}: ${error.message}`));
|
|
81
82
|
});
|
|
83
|
+
const probe = () => this.run("'pong';", LAUNCH_PROBE_TIMEOUT_MS)
|
|
84
|
+
.then(() => true)
|
|
85
|
+
.catch(() => false);
|
|
86
|
+
waitForLaunchReady(probe, { isAborted: () => aborted })
|
|
87
|
+
.catch(() => false)
|
|
88
|
+
.then(() => {
|
|
89
|
+
if (!aborted)
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
82
92
|
});
|
|
83
93
|
}
|
|
84
94
|
}
|
|
Binary file
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
2
|
+
'ps_sequence',
|
|
3
|
+
'ps_batch',
|
|
4
|
+
'ps_ping',
|
|
5
|
+
'ps_overview',
|
|
6
|
+
'ps_list_capabilities',
|
|
7
|
+
'ps_get_preview',
|
|
8
|
+
'ps_get_selection_preview',
|
|
9
|
+
'ps_get_histogram',
|
|
10
|
+
'ps_inspect',
|
|
11
|
+
'ps_read_scene',
|
|
12
|
+
'ps_compare_regions',
|
|
13
|
+
'ps_get_layer_bounds_diff',
|
|
14
|
+
'ps_template_list',
|
|
15
|
+
'ps_template_recall',
|
|
16
|
+
'ps_list_actions',
|
|
17
|
+
'ps_report_problem',
|
|
18
|
+
'ps_detect',
|
|
19
|
+
'ps_detect_landmarks',
|
|
20
|
+
'ps_document',
|
|
21
|
+
'ps_template_verify',
|
|
22
|
+
'ps_resolve_placement',
|
|
23
|
+
'ps_template_save',
|
|
24
|
+
'ps_template_delete',
|
|
25
|
+
'ps_template_create_evidence',
|
|
26
|
+
]);
|
|
27
|
+
export const KEPT_WORK_TOOLS = new Set(['ps_export', 'ps_save_psd']);
|
|
28
|
+
export const MUTATING_TOOLS = new Set([
|
|
29
|
+
'ps_play_action',
|
|
30
|
+
'ps_execute_script',
|
|
31
|
+
'ps_add_adjustment_layer',
|
|
32
|
+
'ps_apply_adjustment',
|
|
33
|
+
'ps_create_document',
|
|
34
|
+
'ps_close_document',
|
|
35
|
+
'ps_open_document',
|
|
36
|
+
'ps_develop_raw',
|
|
37
|
+
'ps_filter',
|
|
38
|
+
'ps_group',
|
|
39
|
+
'ps_clipping_mask',
|
|
40
|
+
'ps_undo',
|
|
41
|
+
'ps_redo',
|
|
42
|
+
'ps_place_image',
|
|
43
|
+
'ps_resize_image',
|
|
44
|
+
'ps_crop_document',
|
|
45
|
+
'ps_convert_image_mode',
|
|
46
|
+
'ps_move_layer_to_position',
|
|
47
|
+
'ps_convert_to_smart_object',
|
|
48
|
+
'ps_rasterize_layer',
|
|
49
|
+
'ps_set_layer',
|
|
50
|
+
'ps_duplicate_layer',
|
|
51
|
+
'ps_copy_to_new_layer',
|
|
52
|
+
'ps_merge',
|
|
53
|
+
'ps_bake_layer',
|
|
54
|
+
'ps_add_layer_style',
|
|
55
|
+
'ps_create_layer',
|
|
56
|
+
'ps_delete_layer',
|
|
57
|
+
'ps_fill_layer',
|
|
58
|
+
'ps_add_fill_layer',
|
|
59
|
+
'ps_select_layer',
|
|
60
|
+
'ps_transform_layer',
|
|
61
|
+
'ps_warp_layer',
|
|
62
|
+
'ps_warp_layer_mesh',
|
|
63
|
+
'ps_warp_layer_along',
|
|
64
|
+
'ps_warp_layer_region',
|
|
65
|
+
'ps_warp_layer_to',
|
|
66
|
+
'ps_apply_camera_raw',
|
|
67
|
+
'ps_transform_canvas',
|
|
68
|
+
'ps_guides',
|
|
69
|
+
'ps_retouch',
|
|
70
|
+
'ps_apply_brush_stroke',
|
|
71
|
+
'ps_edit_object',
|
|
72
|
+
'ps_portrait_touchup',
|
|
73
|
+
'ps_add_text_to_object',
|
|
74
|
+
'ps_select_face_feature',
|
|
75
|
+
'ps_stroke_face_contour',
|
|
76
|
+
'ps_select_by_reference',
|
|
77
|
+
'ps_select',
|
|
78
|
+
'ps_select_subject',
|
|
79
|
+
'ps_select_sky',
|
|
80
|
+
'ps_select_subject_instance',
|
|
81
|
+
'ps_select_object',
|
|
82
|
+
'ps_replace_sky',
|
|
83
|
+
'ps_modify_selection',
|
|
84
|
+
'ps_selection_channel',
|
|
85
|
+
'ps_layer_mask',
|
|
86
|
+
'ps_path',
|
|
87
|
+
'ps_vector_mask',
|
|
88
|
+
'ps_apply_image',
|
|
89
|
+
'ps_calculations',
|
|
90
|
+
'ps_shape',
|
|
91
|
+
'ps_template_apply',
|
|
92
|
+
'ps_text',
|
|
93
|
+
]);
|
|
94
|
+
export function mapClientName(name) {
|
|
95
|
+
if (name === undefined)
|
|
96
|
+
return 'other';
|
|
97
|
+
const n = name.toLowerCase();
|
|
98
|
+
if (n.includes('claude-code') || n.includes('claude_code') || n.includes('claudecode')) {
|
|
99
|
+
return 'claude_code';
|
|
100
|
+
}
|
|
101
|
+
if (n.includes('claude'))
|
|
102
|
+
return 'claude_desktop';
|
|
103
|
+
if (n.includes('cursor'))
|
|
104
|
+
return 'cursor';
|
|
105
|
+
if (n.includes('windsurf'))
|
|
106
|
+
return 'windsurf';
|
|
107
|
+
if (n.includes('vscode') || n.includes('visual studio') || n.includes('code-oss')) {
|
|
108
|
+
return 'vscode';
|
|
109
|
+
}
|
|
110
|
+
return 'other';
|
|
111
|
+
}
|
|
112
|
+
export function parseMajor(version) {
|
|
113
|
+
if (version === undefined)
|
|
114
|
+
return null;
|
|
115
|
+
const m = /^(\d+)/.exec(version.trim());
|
|
116
|
+
return m ? Number(m[1]) : null;
|
|
117
|
+
}
|
|
118
|
+
export function boundMajor(n, max) {
|
|
119
|
+
return n !== null && Number.isFinite(n) && n >= 0 && n <= max ? n : null;
|
|
120
|
+
}
|
|
121
|
+
export function nodeMajor() {
|
|
122
|
+
return parseMajor(process.versions.node);
|
|
123
|
+
}
|
|
124
|
+
export function archToken() {
|
|
125
|
+
if (process.arch === 'x64')
|
|
126
|
+
return 'x64';
|
|
127
|
+
if (process.arch === 'arm64')
|
|
128
|
+
return 'arm64';
|
|
129
|
+
return 'other';
|
|
130
|
+
}
|
|
131
|
+
export function osMajor(platform, release) {
|
|
132
|
+
if (platform === 'win32') {
|
|
133
|
+
const parts = release.split('.');
|
|
134
|
+
const major = Number(parts[0]);
|
|
135
|
+
if (!Number.isFinite(major))
|
|
136
|
+
return null;
|
|
137
|
+
const build = Number(parts[2]);
|
|
138
|
+
return Number.isFinite(build) && build >= 22000 ? 11 : major;
|
|
139
|
+
}
|
|
140
|
+
if (platform === 'darwin') {
|
|
141
|
+
const m = /^(\d+)/.exec(release);
|
|
142
|
+
if (!m)
|
|
143
|
+
return null;
|
|
144
|
+
const darwinMajor = Number(m[1]);
|
|
145
|
+
return darwinMajor <= 24 ? darwinMajor - 9 : darwinMajor + 1;
|
|
146
|
+
}
|
|
147
|
+
const m = /^(\d+)/.exec(release);
|
|
148
|
+
return m ? Number(m[1]) : null;
|
|
149
|
+
}
|
package/dist/telemetry/client.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { release } from 'node:os';
|
|
1
2
|
import { Logger } from '../utils/logger.js';
|
|
2
3
|
import { EDITION } from '../edition.js';
|
|
3
4
|
import { VERSION } from '../version.js';
|
|
4
5
|
import { resolveInstallChannel } from '../install-channel.js';
|
|
5
|
-
import {
|
|
6
|
+
import { READ_ONLY_TOOLS, KEPT_WORK_TOOLS, nodeMajor, archToken, osMajor, boundMajor, } from './activity.js';
|
|
7
|
+
import { buildClientConnected, buildDiagnosticEvent, buildModuleStatus, buildSessionStart, buildSessionSummary, buildUsageEvent, dayBucket, normalizeDayBucket, isContentSafe, PS_VERSION_UNKNOWN, } from './events.js';
|
|
6
8
|
import { sanitizeMessage, sanitizeSnippet, sanitizeStderrTail } from './sanitize.js';
|
|
7
9
|
import { httpTransport, resolveEndpoint } from './transport.js';
|
|
8
10
|
import { appendOutboxSync, clearOutbox, clearSessionState, readOutbox, readSessionState, writeSessionStateSync, } from './outbox.js';
|
|
@@ -10,6 +12,8 @@ const MAX_BATCH_SIZE = 100;
|
|
|
10
12
|
const MAX_QUEUE_SIZE = 500;
|
|
11
13
|
const DEFAULT_FLUSH_INTERVAL_MS = 5 * 60_000;
|
|
12
14
|
const SESSION_PERSIST_THROTTLE_MS = 10_000;
|
|
15
|
+
const MAX_SESSION_DURATION_S = 604_800;
|
|
16
|
+
const MAX_INSTALL_ASSET_COUNT = 100_000;
|
|
13
17
|
function isTestEnv() {
|
|
14
18
|
return process.env.VITEST !== undefined || process.env.NODE_ENV === 'test';
|
|
15
19
|
}
|
|
@@ -25,13 +29,25 @@ export class TelemetryClient {
|
|
|
25
29
|
active;
|
|
26
30
|
outboxOpts;
|
|
27
31
|
getModuleStatus;
|
|
32
|
+
resolvedModuleStatus = null;
|
|
28
33
|
queue = [];
|
|
29
34
|
timer = null;
|
|
30
35
|
shutdownPromise = null;
|
|
31
36
|
toolCallCount = 0;
|
|
32
37
|
distinctTools = new Set();
|
|
33
38
|
anyFailures = false;
|
|
39
|
+
firstCallAtMs = null;
|
|
40
|
+
lastCallAtMs = null;
|
|
41
|
+
retryCount = 0;
|
|
42
|
+
lastCallSuccess = null;
|
|
43
|
+
editsOk = 0;
|
|
44
|
+
keptWork = 0;
|
|
45
|
+
droppedEvents = 0;
|
|
46
|
+
behindLatest = null;
|
|
47
|
+
moduleUpdate = 'none';
|
|
48
|
+
installAssets = {};
|
|
34
49
|
lastSessionPersistMs = 0;
|
|
50
|
+
startDayBucket = null;
|
|
35
51
|
constructor(opts) {
|
|
36
52
|
this.settings = opts.settings;
|
|
37
53
|
this.transport = opts.transport ?? httpTransport();
|
|
@@ -60,27 +76,104 @@ export class TelemetryClient {
|
|
|
60
76
|
this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
|
|
61
77
|
this.timer.unref?.();
|
|
62
78
|
if (this.settings.telemetry.usage) {
|
|
63
|
-
|
|
64
|
-
const
|
|
79
|
+
const hostNodeMajor = boundMajor(nodeMajor(), 999);
|
|
80
|
+
const hostOsMajor = boundMajor(osMajor(process.platform, release()), 999);
|
|
81
|
+
this.enqueue(buildSessionStart(this.dims, this.now(), {
|
|
82
|
+
...(hostNodeMajor !== null ? { node_major: hostNodeMajor } : {}),
|
|
83
|
+
arch: archToken(),
|
|
84
|
+
...(hostOsMajor !== null ? { os_major: hostOsMajor } : {}),
|
|
85
|
+
}));
|
|
86
|
+
const moduleStatus = this.moduleStatus();
|
|
65
87
|
if (moduleStatus)
|
|
66
88
|
this.enqueue(buildModuleStatus(this.dims, moduleStatus, this.now()));
|
|
67
89
|
void this.flush();
|
|
68
90
|
}
|
|
69
91
|
}
|
|
92
|
+
recordClientConnected(info) {
|
|
93
|
+
if (!this.active || !this.settings.telemetry.usage)
|
|
94
|
+
return;
|
|
95
|
+
this.enqueue(buildClientConnected(this.dims, info, this.now()));
|
|
96
|
+
}
|
|
70
97
|
recordCall(call) {
|
|
71
98
|
if (!this.active || !this.settings.telemetry.usage)
|
|
72
99
|
return;
|
|
100
|
+
this.ensureStartDayBucket();
|
|
101
|
+
const nowDate = this.now();
|
|
102
|
+
const nowMs = nowDate.getTime();
|
|
73
103
|
this.toolCallCount += 1;
|
|
74
104
|
this.distinctTools.add(call.tool);
|
|
75
105
|
if (!call.success)
|
|
76
106
|
this.anyFailures = true;
|
|
77
|
-
this.
|
|
107
|
+
this.lastCallSuccess = call.success;
|
|
108
|
+
if (call.retry)
|
|
109
|
+
this.retryCount += 1;
|
|
110
|
+
if (call.success) {
|
|
111
|
+
if (!READ_ONLY_TOOLS.has(call.tool))
|
|
112
|
+
this.editsOk += 1;
|
|
113
|
+
if (KEPT_WORK_TOOLS.has(call.tool))
|
|
114
|
+
this.keptWork += 1;
|
|
115
|
+
}
|
|
116
|
+
if (this.firstCallAtMs === null)
|
|
117
|
+
this.firstCallAtMs = nowMs;
|
|
118
|
+
this.lastCallAtMs = nowMs;
|
|
119
|
+
this.enqueue(buildUsageEvent(this.dims, call, nowDate));
|
|
78
120
|
this.persistSessionStateThrottled();
|
|
79
121
|
}
|
|
122
|
+
setBehindLatest(value) {
|
|
123
|
+
this.behindLatest = value;
|
|
124
|
+
}
|
|
125
|
+
setModuleUpdate(outcome) {
|
|
126
|
+
this.moduleUpdate = outcome;
|
|
127
|
+
}
|
|
128
|
+
setInstallAssets(assets) {
|
|
129
|
+
const templatesSaved = assets.templates_saved !== undefined ? clampCount(assets.templates_saved) : null;
|
|
130
|
+
const actionSets = assets.action_sets !== undefined ? clampCount(assets.action_sets) : null;
|
|
131
|
+
this.installAssets = {
|
|
132
|
+
...this.installAssets,
|
|
133
|
+
...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
|
|
134
|
+
...(actionSets !== null ? { action_sets: actionSets } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
moduleStatus() {
|
|
138
|
+
if (this.resolvedModuleStatus === null) {
|
|
139
|
+
this.resolvedModuleStatus = { value: this.getModuleStatus() };
|
|
140
|
+
}
|
|
141
|
+
return this.resolvedModuleStatus.value;
|
|
142
|
+
}
|
|
143
|
+
durationS() {
|
|
144
|
+
if (this.firstCallAtMs === null || this.lastCallAtMs === null)
|
|
145
|
+
return undefined;
|
|
146
|
+
return Math.min(Math.max(0, Math.floor((this.lastCallAtMs - this.firstCallAtMs) / 1000)), MAX_SESSION_DURATION_S);
|
|
147
|
+
}
|
|
148
|
+
summaryFields() {
|
|
149
|
+
const duration = this.durationS();
|
|
150
|
+
const moduleStatus = this.moduleStatus();
|
|
151
|
+
return {
|
|
152
|
+
...(duration !== undefined ? { duration_s: duration } : {}),
|
|
153
|
+
retry_count: this.retryCount,
|
|
154
|
+
...(this.lastCallSuccess !== null ? { ended_after_failure: !this.lastCallSuccess } : {}),
|
|
155
|
+
edits_ok: this.editsOk,
|
|
156
|
+
kept_work: this.keptWork,
|
|
157
|
+
...(this.behindLatest !== null ? { behind_latest: this.behindLatest } : {}),
|
|
158
|
+
dropped_events: this.droppedEvents,
|
|
159
|
+
...(moduleStatus !== null ? { module_update: this.moduleUpdate } : {}),
|
|
160
|
+
...(this.installAssets.templates_saved !== undefined
|
|
161
|
+
? { templates_saved: this.installAssets.templates_saved }
|
|
162
|
+
: {}),
|
|
163
|
+
...(this.installAssets.action_sets !== undefined
|
|
164
|
+
? { action_sets: this.installAssets.action_sets }
|
|
165
|
+
: {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
80
168
|
psVersion() {
|
|
81
169
|
const v = this.dims.getPsVersion();
|
|
82
170
|
return v && v.length > 0 ? v : PS_VERSION_UNKNOWN;
|
|
83
171
|
}
|
|
172
|
+
ensureStartDayBucket() {
|
|
173
|
+
if (this.startDayBucket === null)
|
|
174
|
+
this.startDayBucket = dayBucket(this.now());
|
|
175
|
+
return this.startDayBucket;
|
|
176
|
+
}
|
|
84
177
|
persistSessionStateThrottled() {
|
|
85
178
|
const nowMs = this.now().getTime();
|
|
86
179
|
if (this.lastSessionPersistMs !== 0 &&
|
|
@@ -90,7 +183,7 @@ export class TelemetryClient {
|
|
|
90
183
|
this.lastSessionPersistMs = nowMs;
|
|
91
184
|
const state = {
|
|
92
185
|
install_id: this.dims.install_id,
|
|
93
|
-
ts_bucket:
|
|
186
|
+
ts_bucket: this.ensureStartDayBucket(),
|
|
94
187
|
editmamei_version: this.dims.editmamei_version,
|
|
95
188
|
edition: this.dims.edition,
|
|
96
189
|
platform: this.dims.platform,
|
|
@@ -98,6 +191,7 @@ export class TelemetryClient {
|
|
|
98
191
|
tool_call_count: this.toolCallCount,
|
|
99
192
|
distinct_tools: this.distinctTools.size,
|
|
100
193
|
any_failures: this.anyFailures,
|
|
194
|
+
...this.summaryFields(),
|
|
101
195
|
};
|
|
102
196
|
writeSessionStateSync(state, this.outboxOpts);
|
|
103
197
|
}
|
|
@@ -116,6 +210,9 @@ export class TelemetryClient {
|
|
|
116
210
|
error_message: sanitizeMessage(diag.error_message),
|
|
117
211
|
...(diag.snippet ? { snippet: sanitizeSnippet(diag.snippet) } : {}),
|
|
118
212
|
...(diag.stderr_tail ? { stderr_tail: sanitizeStderrTail(diag.stderr_tail) } : {}),
|
|
213
|
+
...(diag.doc_depth !== undefined ? { doc_depth: diag.doc_depth } : {}),
|
|
214
|
+
...(diag.doc_mode !== undefined ? { doc_mode: diag.doc_mode } : {}),
|
|
215
|
+
...(diag.ps_locale !== undefined ? { ps_locale: diag.ps_locale } : {}),
|
|
119
216
|
}, this.now()));
|
|
120
217
|
}
|
|
121
218
|
restampPsVersion(events) {
|
|
@@ -134,7 +231,9 @@ export class TelemetryClient {
|
|
|
134
231
|
enqueue(event) {
|
|
135
232
|
this.queue.push(event);
|
|
136
233
|
if (this.queue.length > MAX_QUEUE_SIZE) {
|
|
137
|
-
|
|
234
|
+
const excess = this.queue.length - MAX_QUEUE_SIZE;
|
|
235
|
+
this.queue.splice(0, excess);
|
|
236
|
+
this.droppedEvents += excess;
|
|
138
237
|
}
|
|
139
238
|
if (this.queue.length >= this.maxBatchSize)
|
|
140
239
|
void this.flush();
|
|
@@ -170,7 +269,8 @@ export class TelemetryClient {
|
|
|
170
269
|
tool_call_count: this.toolCallCount,
|
|
171
270
|
distinct_tools: this.distinctTools.size,
|
|
172
271
|
any_failures: this.anyFailures,
|
|
173
|
-
|
|
272
|
+
...this.summaryFields(),
|
|
273
|
+
}, this.ensureStartDayBucket(), this.now()));
|
|
174
274
|
}
|
|
175
275
|
if (this.queue.length > 0) {
|
|
176
276
|
appendOutboxSync(this.restampPsVersion(this.queue.splice(0)).filter(isContentSafe), this.outboxOpts);
|
|
@@ -221,11 +321,13 @@ export class TelemetryClient {
|
|
|
221
321
|
}
|
|
222
322
|
}
|
|
223
323
|
function summaryFromState(s) {
|
|
324
|
+
const templatesSaved = s.templates_saved !== undefined ? clampCount(s.templates_saved) : null;
|
|
325
|
+
const actionSets = s.action_sets !== undefined ? clampCount(s.action_sets) : null;
|
|
224
326
|
return {
|
|
225
327
|
v: 2,
|
|
226
328
|
type: 'session_summary',
|
|
227
329
|
install_id: s.install_id,
|
|
228
|
-
ts_bucket: s.ts_bucket,
|
|
330
|
+
ts_bucket: normalizeDayBucket(s.ts_bucket, new Date()),
|
|
229
331
|
editmamei_version: s.editmamei_version,
|
|
230
332
|
edition: s.edition,
|
|
231
333
|
platform: s.platform,
|
|
@@ -233,8 +335,23 @@ function summaryFromState(s) {
|
|
|
233
335
|
tool_call_count: s.tool_call_count,
|
|
234
336
|
distinct_tools: s.distinct_tools,
|
|
235
337
|
any_failures: s.any_failures,
|
|
338
|
+
...(s.duration_s !== undefined ? { duration_s: s.duration_s } : {}),
|
|
339
|
+
...(s.retry_count !== undefined ? { retry_count: s.retry_count } : {}),
|
|
340
|
+
...(s.ended_after_failure !== undefined ? { ended_after_failure: s.ended_after_failure } : {}),
|
|
341
|
+
...(s.edits_ok !== undefined ? { edits_ok: s.edits_ok } : {}),
|
|
342
|
+
...(s.kept_work !== undefined ? { kept_work: s.kept_work } : {}),
|
|
343
|
+
...(s.behind_latest !== undefined ? { behind_latest: s.behind_latest } : {}),
|
|
344
|
+
...(s.dropped_events !== undefined ? { dropped_events: s.dropped_events } : {}),
|
|
345
|
+
...(s.module_update !== undefined ? { module_update: s.module_update } : {}),
|
|
346
|
+
...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
|
|
347
|
+
...(actionSets !== null ? { action_sets: actionSets } : {}),
|
|
236
348
|
};
|
|
237
349
|
}
|
|
238
350
|
function errMsg(err) {
|
|
239
351
|
return err instanceof Error ? err.message : String(err);
|
|
240
352
|
}
|
|
353
|
+
function clampCount(n) {
|
|
354
|
+
if (!Number.isFinite(n))
|
|
355
|
+
return null;
|
|
356
|
+
return Math.min(Math.max(0, Math.trunc(n)), MAX_INSTALL_ASSET_COUNT);
|
|
357
|
+
}
|