editmamei 0.18.0 → 0.20.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 +1 -1
- 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/config.js +7 -0
- package/dist/cli/help.js +4 -0
- package/dist/cli/report.js +11 -0
- package/dist/cli/router.js +20 -0
- package/dist/core/server.js +33 -6
- package/dist/core/settings.js +8 -0
- package/dist/core/tool-groups.js +2 -1
- package/dist/core/tool-tiers.js +1 -0
- package/dist/diagnostics/collect.js +169 -0
- package/dist/install-channel.js +8 -0
- package/dist/modules/ce/index.js +2 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/diagnostics-tools.js +66 -0
- package/dist/update/check.js +77 -0
- package/dist/utils/log-buffer.js +41 -0
- package/dist/utils/logger.js +12 -1
- package/dist/utils/session-log-reader.js +27 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Editmamei runs on your computer and edits in your own Photoshop. No image conten
|
|
|
125
125
|
|
|
126
126
|
- **Features and pricing:** [editmamei.com](https://editmamei.com)
|
|
127
127
|
- **Install, getting started, FAQ, Pro features:** [the wiki](https://github.com/editmamei/editmamei-wiki)
|
|
128
|
-
- **Bugs and feature requests:** [the issue tracker](https://github.com/editmamei/editmamei-wiki/issues)
|
|
128
|
+
- **Bugs and feature requests:** [the issue tracker](https://github.com/editmamei/editmamei-wiki/issues). If something's broken, ask your assistant to "report a problem" (or run `editmamei report`) to drop an anonymized diagnostic bundle in your Downloads folder, then attach it to the issue.
|
|
129
129
|
- **Security:** see [editmamei.com/security](https://editmamei.com/security) (don't file security issues publicly)
|
|
130
130
|
|
|
131
131
|
## License
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/config.js
CHANGED
|
@@ -43,6 +43,13 @@ const KEYS = {
|
|
|
43
43
|
},
|
|
44
44
|
coerce: coercePath,
|
|
45
45
|
},
|
|
46
|
+
update_check: {
|
|
47
|
+
get: (s) => s.update_check,
|
|
48
|
+
set: (s, v) => {
|
|
49
|
+
s.update_check = v;
|
|
50
|
+
},
|
|
51
|
+
coerce: coerceBool,
|
|
52
|
+
},
|
|
46
53
|
};
|
|
47
54
|
function knownKeysHint() {
|
|
48
55
|
return `Known keys:\n${Object.keys(KEYS)
|
package/dist/cli/help.js
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
editmamei activate <key> Activate a Pro license on this device
|
|
11
11
|
editmamei deactivate Free this device's seat (before moving Pro to another machine)
|
|
12
12
|
editmamei license Show the current license + whether Pro is unlocked
|
|
13
|
+
editmamei report Write an anonymized diagnostic bundle to Downloads for a bug report
|
|
13
14
|
editmamei help, --help Print this help
|
|
14
15
|
|
|
15
16
|
Install options:
|
|
@@ -33,6 +34,9 @@ Config examples:
|
|
|
33
34
|
editmamei config set telemetry.usage false Opt out of anonymous usage telemetry
|
|
34
35
|
editmamei config set telemetry.diagnostics true Opt in to sanitized diagnostic detail
|
|
35
36
|
|
|
37
|
+
Report options:
|
|
38
|
+
--note "<text>" Attach a short description of the problem to the bundle
|
|
39
|
+
|
|
36
40
|
Examples:
|
|
37
41
|
npm install -g editmamei && editmamei install
|
|
38
42
|
editmamei install --photoshop-path "D:\\Adobe\\Photoshop 2025\\Photoshop.exe"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { collectDiagnostics, writeDiagnosticBundle, ISSUES_URL } from '../diagnostics/collect.js';
|
|
2
|
+
export async function runReport(opts = {}) {
|
|
3
|
+
const out = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
4
|
+
const collect = opts.collect ?? collectDiagnostics;
|
|
5
|
+
const write = opts.write ?? writeDiagnosticBundle;
|
|
6
|
+
const bundle = await collect({ note: opts.note });
|
|
7
|
+
const { path } = await write(bundle);
|
|
8
|
+
out(`\n Wrote an anonymized diagnostic bundle to:\n ${path}\n\n`);
|
|
9
|
+
out(` It contains recent logs + system info — no images, no full file paths, no tool arguments.\n`);
|
|
10
|
+
out(` Attach this file to a new issue at ${ISSUES_URL} so the maintainers can debug.\n\n`);
|
|
11
|
+
}
|
package/dist/cli/router.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runConfig } from './config.js';
|
|
|
5
5
|
import { runActivate } from './activate.js';
|
|
6
6
|
import { runDeactivate } from './deactivate.js';
|
|
7
7
|
import { runLicenseStatus } from './license.js';
|
|
8
|
+
import { runReport } from './report.js';
|
|
8
9
|
import { printHelp } from './help.js';
|
|
9
10
|
import { Logger } from '../utils/logger.js';
|
|
10
11
|
const logger = new Logger('CLI');
|
|
@@ -41,6 +42,9 @@ export async function routeCli(argv, opts = {}) {
|
|
|
41
42
|
case 'license':
|
|
42
43
|
await runLicenseStatus();
|
|
43
44
|
return { handled: true, exitCode: 0 };
|
|
45
|
+
case 'report':
|
|
46
|
+
await runReport({ note: parseNoteOpt(argv.slice(1)) });
|
|
47
|
+
return { handled: true, exitCode: 0 };
|
|
44
48
|
case 'help':
|
|
45
49
|
case '--help':
|
|
46
50
|
case '-h':
|
|
@@ -58,6 +62,22 @@ export async function routeCli(argv, opts = {}) {
|
|
|
58
62
|
return { handled: true, exitCode: 1 };
|
|
59
63
|
}
|
|
60
64
|
}
|
|
65
|
+
function parseNoteOpt(args) {
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const a = args[i];
|
|
68
|
+
if (a === '--note') {
|
|
69
|
+
const value = args[i + 1];
|
|
70
|
+
if (value && !value.startsWith('--'))
|
|
71
|
+
return value;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (a.startsWith('--note=')) {
|
|
75
|
+
const value = a.slice('--note='.length);
|
|
76
|
+
return value.length > 0 ? value : undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
61
81
|
function parseInstallOpts(args, err) {
|
|
62
82
|
const opts = {};
|
|
63
83
|
for (let i = 0; i < args.length; i++) {
|
package/dist/core/server.js
CHANGED
|
@@ -9,8 +9,9 @@ import { EDITION } from '../edition.js';
|
|
|
9
9
|
import { VERSION } from '../version.js';
|
|
10
10
|
import { Session } from './session.js';
|
|
11
11
|
import { SessionLog, classifyError } from '../utils/session-log.js';
|
|
12
|
-
import { loadSettings, applyTelemetryEnvOverrides } from './settings.js';
|
|
12
|
+
import { loadSettings, applyTelemetryEnvOverrides, applyUpdateCheckEnvOverride, } from './settings.js';
|
|
13
13
|
import { TelemetryClient } from '../telemetry/client.js';
|
|
14
|
+
import { checkForUpdate, shouldCheckForUpdate } from '../update/check.js';
|
|
14
15
|
import { join, dirname } from 'node:path';
|
|
15
16
|
import { pathToFileURL } from 'node:url';
|
|
16
17
|
import { GoSnippetClient, resolveProBinaryPath, coreBinaryName } from '../api/snippet-client.js';
|
|
@@ -49,6 +50,7 @@ export class EditmameiServer {
|
|
|
49
50
|
kernel;
|
|
50
51
|
proModule = null;
|
|
51
52
|
psVersion = null;
|
|
53
|
+
updateInfo = null;
|
|
52
54
|
snippetClient = new GoSnippetClient();
|
|
53
55
|
constructor() {
|
|
54
56
|
this.logger = new Logger('EditmameiServer');
|
|
@@ -56,13 +58,18 @@ export class EditmameiServer {
|
|
|
56
58
|
this.sessionLog = new SessionLog(this.session.getSessionId());
|
|
57
59
|
this.logger.info(`Session ${this.session.getSessionId()} → ${this.sessionLog.path}`);
|
|
58
60
|
const { settings, created } = loadSettings();
|
|
59
|
-
const effectiveSettings = applyTelemetryEnvOverrides(settings);
|
|
61
|
+
const effectiveSettings = applyUpdateCheckEnvOverride(applyTelemetryEnvOverrides(settings));
|
|
60
62
|
this.telemetry = new TelemetryClient({
|
|
61
63
|
settings: effectiveSettings,
|
|
62
64
|
getPsVersion: () => this.psVersion,
|
|
63
65
|
});
|
|
64
66
|
if (created)
|
|
65
67
|
this.logger.info(FIRST_RUN_DISCLOSURE);
|
|
68
|
+
if (shouldCheckForUpdate(effectiveSettings.update_check)) {
|
|
69
|
+
void checkForUpdate().then((info) => {
|
|
70
|
+
this.updateInfo = info;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
66
73
|
warnLogScriptOnErrorOnce(this.logger);
|
|
67
74
|
this.toolRegistry = new ToolRegistry({
|
|
68
75
|
onCall: (entry) => {
|
|
@@ -124,6 +131,16 @@ export class EditmameiServer {
|
|
|
124
131
|
items: { type: 'string' },
|
|
125
132
|
description: 'List of optional discovery signals that could not be gathered (e.g. "pingState" if the in-PS state snippet failed, "templates" if ~/.editmamei/templates was unreadable). Empty when all signals were collected cleanly. Returned defaults for any degraded field cannot be trusted as ground truth.',
|
|
126
133
|
},
|
|
134
|
+
update_available: {
|
|
135
|
+
type: ['object', 'null'],
|
|
136
|
+
description: 'Set when a newer Editmamei version is published, else null. When present, TELL THE USER: relay current → latest and how_to_update verbatim. { current, latest, channel (npm/mcpb/dev), how_to_update }. Anonymous npm-registry check at boot; opt out with `editmamei config set update_check false`.',
|
|
137
|
+
properties: {
|
|
138
|
+
current: { type: 'string' },
|
|
139
|
+
latest: { type: 'string' },
|
|
140
|
+
channel: { type: 'string' },
|
|
141
|
+
how_to_update: { type: 'string' },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
127
144
|
},
|
|
128
145
|
},
|
|
129
146
|
annotations: {
|
|
@@ -286,8 +303,10 @@ export class EditmameiServer {
|
|
|
286
303
|
const isConnected = await connection.ping();
|
|
287
304
|
if (!isConnected) {
|
|
288
305
|
return {
|
|
289
|
-
content: [
|
|
290
|
-
|
|
306
|
+
content: [
|
|
307
|
+
{ type: 'text', text: 'Failed to connect to Photoshop' + this.updateNote() },
|
|
308
|
+
],
|
|
309
|
+
structuredContent: { connected: false, update_available: this.updateInfo },
|
|
291
310
|
};
|
|
292
311
|
}
|
|
293
312
|
let version = 'Unknown';
|
|
@@ -336,7 +355,8 @@ export class EditmameiServer {
|
|
|
336
355
|
text: `Connected to Photoshop (v${version}). ` +
|
|
337
356
|
`${actionSetsCount} custom action set(s), ${userTemplates} saved template(s), ` +
|
|
338
357
|
`${openDocuments.length} open document(s)${openDocuments.length ? ': ' + openDocuments.join(', ') : ''}` +
|
|
339
|
-
`${degradedNote}
|
|
358
|
+
`${degradedNote}.` +
|
|
359
|
+
this.updateNote(),
|
|
340
360
|
},
|
|
341
361
|
],
|
|
342
362
|
structuredContent: {
|
|
@@ -346,14 +366,21 @@ export class EditmameiServer {
|
|
|
346
366
|
user_templates: userTemplates,
|
|
347
367
|
open_documents: openDocuments,
|
|
348
368
|
degraded,
|
|
369
|
+
update_available: this.updateInfo,
|
|
349
370
|
},
|
|
350
371
|
};
|
|
351
372
|
}
|
|
373
|
+
updateNote() {
|
|
374
|
+
const u = this.updateInfo;
|
|
375
|
+
if (!u)
|
|
376
|
+
return '';
|
|
377
|
+
return ` Update available: v${u.current} → v${u.latest}. ${u.how_to_update}`;
|
|
378
|
+
}
|
|
352
379
|
async start() {
|
|
353
|
-
await this.session.initialize();
|
|
354
380
|
await this.loadModules();
|
|
355
381
|
const transport = new StdioServerTransport();
|
|
356
382
|
await this.server.connect(transport);
|
|
383
|
+
void this.session.initialize().catch(() => undefined);
|
|
357
384
|
this.telemetry.start();
|
|
358
385
|
void this.telemetry.flushOutboxOnStartup();
|
|
359
386
|
this.server.onclose = () => {
|
package/dist/core/settings.js
CHANGED
|
@@ -20,6 +20,7 @@ function defaults(installId) {
|
|
|
20
20
|
telemetry: { usage: true, diagnostics: false, install_id: installId },
|
|
21
21
|
privacy: { send_previews_to_llm: true },
|
|
22
22
|
photoshop_path: null,
|
|
23
|
+
update_check: true,
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
function coerce(raw, installId) {
|
|
@@ -43,6 +44,7 @@ function coerce(raw, installId) {
|
|
|
43
44
|
: base.privacy.send_previews_to_llm,
|
|
44
45
|
},
|
|
45
46
|
photoshop_path: typeof r.photoshop_path === 'string' ? r.photoshop_path : null,
|
|
47
|
+
update_check: typeof r.update_check === 'boolean' ? r.update_check : base.update_check,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
export function loadSettings(opts = {}) {
|
|
@@ -102,6 +104,12 @@ export function applyTelemetryEnvOverrides(settings, env = process.env) {
|
|
|
102
104
|
},
|
|
103
105
|
};
|
|
104
106
|
}
|
|
107
|
+
export function applyUpdateCheckEnvOverride(settings, env = process.env) {
|
|
108
|
+
const v = parseBoolEnv(env.EDITMAMEI_UPDATE_CHECK);
|
|
109
|
+
if (v === undefined)
|
|
110
|
+
return settings;
|
|
111
|
+
return { ...settings, update_check: v };
|
|
112
|
+
}
|
|
105
113
|
export function saveSettings(settings, opts = {}) {
|
|
106
114
|
const path = settingsPath(opts);
|
|
107
115
|
const dir = dirname(path);
|
package/dist/core/tool-groups.js
CHANGED
|
@@ -2,7 +2,7 @@ export const GROUPS = {
|
|
|
2
2
|
core: {
|
|
3
3
|
id: 'core',
|
|
4
4
|
label: 'Core',
|
|
5
|
-
purpose: 'Session liveness, orientation,
|
|
5
|
+
purpose: 'Session liveness, orientation, undo/redo, and problem reporting — always available.',
|
|
6
6
|
},
|
|
7
7
|
inspect: {
|
|
8
8
|
id: 'inspect',
|
|
@@ -86,6 +86,7 @@ export const TOOL_GROUPS = {
|
|
|
86
86
|
photoshop_undo: 'core',
|
|
87
87
|
photoshop_redo: 'core',
|
|
88
88
|
photoshop_list_capabilities: 'core',
|
|
89
|
+
photoshop_report_problem: 'core',
|
|
89
90
|
photoshop_inspect: 'inspect',
|
|
90
91
|
photoshop_get_preview: 'inspect',
|
|
91
92
|
photoshop_get_histogram: 'verify',
|
package/dist/core/tool-tiers.js
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { homedir, release as osRelease, arch as osArch, platform as osPlatform } from 'node:os';
|
|
2
|
+
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { VERSION } from '../version.js';
|
|
5
|
+
import { EDITION } from '../edition.js';
|
|
6
|
+
import { loadSettings, settingsDir } from '../core/settings.js';
|
|
7
|
+
import { sanitizeMessage } from '../telemetry/sanitize.js';
|
|
8
|
+
import { sharedLogBuffer } from '../utils/log-buffer.js';
|
|
9
|
+
import { readSessionLog, listRecentSessionIds } from '../utils/session-log-reader.js';
|
|
10
|
+
import { classifyError, generateSessionId, } from '../utils/session-log.js';
|
|
11
|
+
import { detectDownloadsDir } from '../cli/downloads-dir.js';
|
|
12
|
+
export const DIAGNOSTIC_BUNDLE_SCHEMA = 1;
|
|
13
|
+
export const ISSUES_URL = 'https://github.com/editmamei/editmamei-wiki/issues';
|
|
14
|
+
const MAX_LOG_LINES = 1000;
|
|
15
|
+
const MAX_DESKTOP_LOG_LINES = 400;
|
|
16
|
+
const MAX_RECENT_SESSIONS = 3;
|
|
17
|
+
const MAX_CALLS_PER_SESSION = 500;
|
|
18
|
+
const MAX_NOTE_LEN = 1000;
|
|
19
|
+
const MAX_LINE_LEN = 4000;
|
|
20
|
+
const MAX_DESKTOP_LINE_LEN = 1000;
|
|
21
|
+
function mcpClientLabel(meta) {
|
|
22
|
+
if (!meta?.mcp_client)
|
|
23
|
+
return null;
|
|
24
|
+
return `${meta.mcp_client.name} ${meta.mcp_client.version}`;
|
|
25
|
+
}
|
|
26
|
+
export async function collectDiagnostics(opts = {}) {
|
|
27
|
+
const logBuffer = opts.logBuffer ?? sharedLogBuffer;
|
|
28
|
+
const emHome = opts.homeDir ?? settingsDir();
|
|
29
|
+
const { settings } = loadSettings(opts.homeDir ? { dir: opts.homeDir } : {});
|
|
30
|
+
const server_log = logBuffer
|
|
31
|
+
.snapshot()
|
|
32
|
+
.slice(-MAX_LOG_LINES)
|
|
33
|
+
.map((line) => sanitizeMessage(line, MAX_LINE_LEN));
|
|
34
|
+
const sessionsDir = join(emHome, 'sessions');
|
|
35
|
+
const recentIds = await listRecentSessionIds(MAX_RECENT_SESSIONS, { dir: sessionsDir });
|
|
36
|
+
const recent_sessions = [];
|
|
37
|
+
let psVersion = opts.psVersion ?? null;
|
|
38
|
+
let mcpClient = null;
|
|
39
|
+
for (const id of recentIds) {
|
|
40
|
+
const entries = await readSessionLog(id, { dir: sessionsDir });
|
|
41
|
+
const meta = [...entries].reverse().find((e) => e.type === 'meta');
|
|
42
|
+
const calls = entries.filter((e) => e.type === 'call');
|
|
43
|
+
const reduced = calls.slice(-MAX_CALLS_PER_SESSION).map((c) => ({
|
|
44
|
+
seq: c.seq,
|
|
45
|
+
ts: c.ts,
|
|
46
|
+
tool: c.tool,
|
|
47
|
+
success: c.success,
|
|
48
|
+
duration_ms: c.duration_ms,
|
|
49
|
+
error_class: c.error_class ?? (c.success ? null : classifyError(c.error)),
|
|
50
|
+
}));
|
|
51
|
+
if (meta) {
|
|
52
|
+
if (!psVersion && meta.ps_version)
|
|
53
|
+
psVersion = meta.ps_version;
|
|
54
|
+
if (!mcpClient)
|
|
55
|
+
mcpClient = mcpClientLabel(meta);
|
|
56
|
+
}
|
|
57
|
+
recent_sessions.push({
|
|
58
|
+
session_id: id,
|
|
59
|
+
editmamei_version: meta?.editmamei_version ?? null,
|
|
60
|
+
ps_version: meta?.ps_version ?? null,
|
|
61
|
+
mcp_client: mcpClientLabel(meta),
|
|
62
|
+
call_count: calls.length,
|
|
63
|
+
calls: reduced,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const desktop = await readDesktopLogTail({
|
|
67
|
+
env: opts.env,
|
|
68
|
+
overrideDir: opts.desktopLogDir,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
schema: DIAGNOSTIC_BUNDLE_SCHEMA,
|
|
72
|
+
report_id: generateSessionId(opts.now),
|
|
73
|
+
editmamei_version: VERSION,
|
|
74
|
+
edition: EDITION,
|
|
75
|
+
platform: osPlatform(),
|
|
76
|
+
os_release: osRelease(),
|
|
77
|
+
arch: osArch(),
|
|
78
|
+
node_version: process.version,
|
|
79
|
+
install_id: settings.telemetry.install_id,
|
|
80
|
+
ps_version: psVersion,
|
|
81
|
+
mcp_client: mcpClient,
|
|
82
|
+
settings: {
|
|
83
|
+
telemetry_usage: settings.telemetry.usage,
|
|
84
|
+
telemetry_diagnostics: settings.telemetry.diagnostics,
|
|
85
|
+
update_check: settings.update_check,
|
|
86
|
+
send_previews_to_llm: settings.privacy.send_previews_to_llm,
|
|
87
|
+
},
|
|
88
|
+
note: opts.note ? sanitizeMessage(opts.note, MAX_NOTE_LEN) : null,
|
|
89
|
+
server_log,
|
|
90
|
+
desktop_log: desktop.lines,
|
|
91
|
+
desktop_log_source: desktop.source,
|
|
92
|
+
recent_sessions,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function writeDiagnosticBundle(bundle, opts = {}) {
|
|
96
|
+
const dest = opts.downloadsDir ?? detectDownloadsDir(opts.env ?? process.env).path;
|
|
97
|
+
await mkdir(dest, { recursive: true });
|
|
98
|
+
const path = join(dest, `editmamei-diagnostics-${bundle.report_id}.json`);
|
|
99
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
100
|
+
await writeFile(path, json, { encoding: 'utf8', mode: 0o600 });
|
|
101
|
+
return { path, bytes: Buffer.byteLength(json, 'utf8') };
|
|
102
|
+
}
|
|
103
|
+
export function defaultDesktopLogDir(env = process.env) {
|
|
104
|
+
if (process.platform === 'darwin')
|
|
105
|
+
return join(homedir(), 'Library', 'Logs', 'Claude');
|
|
106
|
+
if (process.platform === 'win32') {
|
|
107
|
+
const appData = env.APPDATA;
|
|
108
|
+
return appData
|
|
109
|
+
? join(appData, 'Claude', 'logs')
|
|
110
|
+
: join(homedir(), 'AppData', 'Roaming', 'Claude', 'logs');
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const BASE64_RUN = /[A-Za-z0-9+/]{200,}={0,2}/g;
|
|
115
|
+
const JSONRPC_FRAME = /^(.*\bMessage (?:from|to) (?:client|server):\s*)(\{.*)$/;
|
|
116
|
+
const PAYLOAD_OBJECT = /("(?:arguments|params|result|input)"\s*:\s*)([{[][\s\S]*)$/;
|
|
117
|
+
export function sanitizeDesktopLogLine(line) {
|
|
118
|
+
const frame = JSONRPC_FRAME.exec(line);
|
|
119
|
+
if (frame) {
|
|
120
|
+
const prefix = sanitizeMessage(frame[1], MAX_LINE_LEN).replace(/\s+$/, '');
|
|
121
|
+
const body = frame[2];
|
|
122
|
+
const method = /"method"\s*:\s*"([^"]+)"/.exec(body)?.[1];
|
|
123
|
+
const id = /"id"\s*:\s*("?[\w.-]+"?)/.exec(body)?.[1];
|
|
124
|
+
const kind = method
|
|
125
|
+
? `method=${method}`
|
|
126
|
+
: /"result"/.test(body)
|
|
127
|
+
? 'result'
|
|
128
|
+
: /"error"/.test(body)
|
|
129
|
+
? 'error'
|
|
130
|
+
: 'frame';
|
|
131
|
+
return `${prefix} [${kind}${id ? ` id=${id}` : ''}, payload redacted, ${body.length} bytes]`;
|
|
132
|
+
}
|
|
133
|
+
let out = sanitizeMessage(line, MAX_LINE_LEN);
|
|
134
|
+
out = out.replace(PAYLOAD_OBJECT, '$1<redacted>');
|
|
135
|
+
out = out.replace(BASE64_RUN, '…[binary redacted]');
|
|
136
|
+
if (out.length > MAX_DESKTOP_LINE_LEN)
|
|
137
|
+
out = out.slice(0, MAX_DESKTOP_LINE_LEN) + '…[truncated]';
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
async function readDesktopLogTail(opts) {
|
|
141
|
+
const dir = opts.overrideDir ?? defaultDesktopLogDir(opts.env ?? process.env);
|
|
142
|
+
if (!dir)
|
|
143
|
+
return { lines: [], source: null };
|
|
144
|
+
let names;
|
|
145
|
+
try {
|
|
146
|
+
names = await readdir(dir);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return { lines: [], source: null };
|
|
150
|
+
}
|
|
151
|
+
const logs = names.filter((f) => f.toLowerCase().endsWith('.log'));
|
|
152
|
+
if (logs.length === 0)
|
|
153
|
+
return { lines: [], source: null };
|
|
154
|
+
const pick = logs.find((f) => /editmamei/i.test(f)) ??
|
|
155
|
+
logs.find((f) => f.toLowerCase() === 'mcp.log') ??
|
|
156
|
+
logs[0];
|
|
157
|
+
try {
|
|
158
|
+
const raw = await readFile(join(dir, pick), 'utf8');
|
|
159
|
+
const tail = raw
|
|
160
|
+
.split(/\r?\n/)
|
|
161
|
+
.filter((l) => l.length > 0)
|
|
162
|
+
.slice(-MAX_DESKTOP_LOG_LINES)
|
|
163
|
+
.map(sanitizeDesktopLogLine);
|
|
164
|
+
return { lines: tail, source: pick };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { lines: [], source: null };
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/modules/ce/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createLayerOrderingTools } from '../../tools/layer-ordering-tools.js';
|
|
|
17
17
|
import { createPreviewTools } from '../../tools/preview-tools.js';
|
|
18
18
|
import { createInspectTools } from '../../tools/inspect-tools.js';
|
|
19
19
|
import { createOverviewTools } from '../../tools/overview-tools.js';
|
|
20
|
+
import { createDiagnosticsTools } from '../../tools/diagnostics-tools.js';
|
|
20
21
|
import { createRetouchTools } from '../../tools/retouch-tools.js';
|
|
21
22
|
import { createBrushTools } from '../../tools/brush-tools.js';
|
|
22
23
|
import { createTransformCanvasTools } from '../../tools/transform-canvas-tools.js';
|
|
@@ -45,6 +46,7 @@ const ceFactories = [
|
|
|
45
46
|
createPreviewTools,
|
|
46
47
|
createInspectTools,
|
|
47
48
|
createOverviewTools,
|
|
49
|
+
createDiagnosticsTools,
|
|
48
50
|
createRetouchTools,
|
|
49
51
|
createBrushTools,
|
|
50
52
|
createTransformCanvasTools,
|
|
Binary file
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { collectDiagnostics, writeDiagnosticBundle, ISSUES_URL } from '../diagnostics/collect.js';
|
|
2
|
+
const reportSchema = {
|
|
3
|
+
type: 'object',
|
|
4
|
+
properties: {
|
|
5
|
+
note: {
|
|
6
|
+
type: 'string',
|
|
7
|
+
description: 'Optional short description of the problem (what went wrong, what you were doing). Embedded verbatim after sanitization. No file contents or paths needed.',
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export function createDiagnosticsTools(_connection, _snippetClient, deps = {}) {
|
|
12
|
+
const collect = deps.collect ?? collectDiagnostics;
|
|
13
|
+
const writeBundle = deps.write ?? writeDiagnosticBundle;
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
tool: {
|
|
17
|
+
name: 'photoshop_report_problem',
|
|
18
|
+
description: "Collect an ANONYMIZED diagnostic bundle and write it to the user's Downloads folder so they can attach it to a bug report — use when Editmamei misbehaves (won't connect, a tool keeps failing, unexpected results). The bundle holds recent server logs, system info (Editmamei/OS/Photoshop versions), and a content-free summary of recent tool calls (name, success, duration, error class). It contains NO image content, NO tool arguments, and file paths reduced to basenames. Does not touch Photoshop; writes one JSON file. After calling, tell the user the file path and that they can attach it to a new issue at " +
|
|
19
|
+
ISSUES_URL +
|
|
20
|
+
'.',
|
|
21
|
+
inputSchema: reportSchema,
|
|
22
|
+
outputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
path: { type: 'string' },
|
|
26
|
+
bytes: { type: 'number' },
|
|
27
|
+
server_log_lines: { type: 'number' },
|
|
28
|
+
recent_session_count: { type: 'number' },
|
|
29
|
+
desktop_log_included: { type: 'boolean' },
|
|
30
|
+
issues_url: { type: 'string' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
annotations: {
|
|
34
|
+
title: 'Report a problem',
|
|
35
|
+
readOnlyHint: false,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: false,
|
|
38
|
+
openWorldHint: true,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
handler: async (args) => {
|
|
42
|
+
const note = typeof args.note === 'string' ? args.note : undefined;
|
|
43
|
+
const bundle = await collect({ note });
|
|
44
|
+
const { path, bytes } = await writeBundle(bundle);
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: `Wrote an anonymized diagnostic bundle to:\n ${path}\n\n` +
|
|
50
|
+
`It contains recent logs + system info — no images, no full file paths, no tool arguments. ` +
|
|
51
|
+
`Attach this file to a new issue at ${ISSUES_URL} so the maintainers can debug.`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
structuredContent: {
|
|
55
|
+
path,
|
|
56
|
+
bytes,
|
|
57
|
+
server_log_lines: bundle.server_log.length,
|
|
58
|
+
recent_session_count: bundle.recent_sessions.length,
|
|
59
|
+
desktop_log_included: bundle.desktop_log.length > 0,
|
|
60
|
+
issues_url: ISSUES_URL,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { VERSION } from '../version.js';
|
|
2
|
+
import { resolveInstallChannel } from '../install-channel.js';
|
|
3
|
+
const DEFAULT_DIST_TAGS_URL = 'https://registry.npmjs.org/-/package/editmamei/dist-tags';
|
|
4
|
+
const RELEASES_URL = 'https://github.com/editmamei/editmamei-wiki/releases/latest';
|
|
5
|
+
export function resolveUpdateCheckUrl(env = process.env) {
|
|
6
|
+
const override = env.EDITMAMEI_UPDATE_CHECK_URL;
|
|
7
|
+
return override && override.length > 0 ? override : DEFAULT_DIST_TAGS_URL;
|
|
8
|
+
}
|
|
9
|
+
export function httpFetchLatest() {
|
|
10
|
+
return async (url, timeoutMs) => {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
headers: { accept: 'application/json' },
|
|
16
|
+
signal: controller.signal,
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
return null;
|
|
20
|
+
const data = (await res.json());
|
|
21
|
+
return typeof data.latest === 'string' ? data.latest : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function parseSemver(v) {
|
|
32
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
33
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
34
|
+
}
|
|
35
|
+
export function isNewer(latest, current) {
|
|
36
|
+
const a = parseSemver(latest);
|
|
37
|
+
const b = parseSemver(current);
|
|
38
|
+
if (!a || !b)
|
|
39
|
+
return false;
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if (a[i] !== b[i])
|
|
42
|
+
return a[i] > b[i];
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
export function updateMessage(channel, latest) {
|
|
47
|
+
switch (channel) {
|
|
48
|
+
case 'mcpb':
|
|
49
|
+
return `Download editmamei.mcpb (v${latest}) from ${RELEASES_URL} and reinstall the Claude Desktop extension.`;
|
|
50
|
+
case 'dev':
|
|
51
|
+
return `You're on a local dev build — pull the latest source and rebuild.`;
|
|
52
|
+
case 'npm':
|
|
53
|
+
default:
|
|
54
|
+
return `Run: npm install -g editmamei@latest (then restart your MCP client).`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function shouldCheckForUpdate(updateCheckEnabled, env = process.env) {
|
|
58
|
+
const inTest = env.VITEST !== undefined || env.NODE_ENV === 'test';
|
|
59
|
+
return updateCheckEnabled === true && !inTest;
|
|
60
|
+
}
|
|
61
|
+
export async function checkForUpdate(opts = {}) {
|
|
62
|
+
try {
|
|
63
|
+
const env = opts.env ?? process.env;
|
|
64
|
+
const current = opts.current ?? VERSION;
|
|
65
|
+
const channel = resolveInstallChannel(env);
|
|
66
|
+
const fetchLatest = opts.fetchLatest ?? httpFetchLatest();
|
|
67
|
+
const latest = await fetchLatest(resolveUpdateCheckUrl(env), opts.timeoutMs ?? 4000);
|
|
68
|
+
if (!latest || !parseSemver(latest))
|
|
69
|
+
return null;
|
|
70
|
+
if (!isNewer(latest, current))
|
|
71
|
+
return null;
|
|
72
|
+
return { current, latest, channel, how_to_update: updateMessage(channel, latest) };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const DEFAULT_CAPACITY = 1000;
|
|
2
|
+
export class LogRingBuffer {
|
|
3
|
+
buf;
|
|
4
|
+
capacity;
|
|
5
|
+
next = 0;
|
|
6
|
+
full = false;
|
|
7
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
8
|
+
this.capacity = Math.max(1, Math.floor(capacity));
|
|
9
|
+
this.buf = new Array(this.capacity);
|
|
10
|
+
}
|
|
11
|
+
push(line) {
|
|
12
|
+
this.buf[this.next] = line;
|
|
13
|
+
this.next = (this.next + 1) % this.capacity;
|
|
14
|
+
if (this.next === 0)
|
|
15
|
+
this.full = true;
|
|
16
|
+
}
|
|
17
|
+
snapshot() {
|
|
18
|
+
if (!this.full)
|
|
19
|
+
return this.buf.slice(0, this.next);
|
|
20
|
+
return [
|
|
21
|
+
...this.buf.slice(this.next),
|
|
22
|
+
...this.buf.slice(0, this.next),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
get size() {
|
|
26
|
+
return this.full ? this.capacity : this.next;
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.buf.fill(undefined);
|
|
30
|
+
this.next = 0;
|
|
31
|
+
this.full = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export const sharedLogBuffer = new LogRingBuffer();
|
|
35
|
+
export function recordLogLine(line) {
|
|
36
|
+
try {
|
|
37
|
+
sharedLogBuffer.push(line);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordLogLine } from './log-buffer.js';
|
|
1
2
|
export var LogLevel;
|
|
2
3
|
(function (LogLevel) {
|
|
3
4
|
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
@@ -58,12 +59,21 @@ export function parseLogLevel(raw, fallback) {
|
|
|
58
59
|
return fallback;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
function isVerboseEnabled(v) {
|
|
63
|
+
if (v === undefined)
|
|
64
|
+
return false;
|
|
65
|
+
const s = v.trim().toLowerCase();
|
|
66
|
+
return s === 'true' || s === '1';
|
|
67
|
+
}
|
|
61
68
|
export class Logger {
|
|
62
69
|
context;
|
|
63
70
|
logLevel;
|
|
64
71
|
constructor(context, logLevel = LogLevel.INFO) {
|
|
65
72
|
this.context = context;
|
|
66
|
-
|
|
73
|
+
const fallback = isVerboseEnabled(process.env.EDITMAMEI_VERBOSE_LOGGING)
|
|
74
|
+
? LogLevel.DEBUG
|
|
75
|
+
: logLevel;
|
|
76
|
+
this.logLevel = parseLogLevel(process.env.LOG_LEVEL, fallback);
|
|
67
77
|
}
|
|
68
78
|
log(level, message, ...args) {
|
|
69
79
|
if (level < this.logLevel)
|
|
@@ -74,6 +84,7 @@ export class Logger {
|
|
|
74
84
|
const formattedArgs = args.map(serializeLogArg).join(' ');
|
|
75
85
|
const logMessage = `${prefix} ${message} ${formattedArgs}`.trim();
|
|
76
86
|
process.stderr.write(logMessage + '\n');
|
|
87
|
+
recordLogLine(logMessage);
|
|
77
88
|
}
|
|
78
89
|
debug(message, ...args) {
|
|
79
90
|
this.log(LogLevel.DEBUG, message, ...args);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { Logger } from './logger.js';
|
|
5
|
+
const SESSION_FILE_SUFFIX = '.ndjson';
|
|
5
6
|
const logger = new Logger('SessionLogReader');
|
|
6
7
|
export async function readSessionLog(sessionId, opts = {}) {
|
|
7
8
|
const dir = opts.dir ?? join(homedir(), '.editmamei', 'sessions');
|
|
@@ -36,3 +37,28 @@ export async function readSessionLog(sessionId, opts = {}) {
|
|
|
36
37
|
}
|
|
37
38
|
return out;
|
|
38
39
|
}
|
|
40
|
+
export async function listRecentSessionIds(limit, opts = {}) {
|
|
41
|
+
const dir = opts.dir ?? join(homedir(), '.editmamei', 'sessions');
|
|
42
|
+
let names;
|
|
43
|
+
try {
|
|
44
|
+
names = await readdir(dir);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const files = names.filter((f) => f.endsWith(SESSION_FILE_SUFFIX));
|
|
50
|
+
const withMtime = await Promise.all(files.map(async (f) => {
|
|
51
|
+
try {
|
|
52
|
+
const s = await stat(join(dir, f));
|
|
53
|
+
return { id: f.slice(0, -SESSION_FILE_SUFFIX.length), mtime: s.mtimeMs };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
return withMtime
|
|
60
|
+
.filter((x) => x !== null)
|
|
61
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
62
|
+
.slice(0, Math.max(0, limit))
|
|
63
|
+
.map((x) => x.id);
|
|
64
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.20.0';
|