editmamei 1.5.0 → 1.6.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.
Files changed (48) hide show
  1. package/README.md +17 -1
  2. package/dist/backends/detect-editors.js +79 -0
  3. package/dist/backends/gimp/backend.js +96 -0
  4. package/dist/backends/gimp/bridge/lib.py +1229 -0
  5. package/dist/backends/gimp/bridge/ops.py +1501 -0
  6. package/dist/backends/gimp/detect.js +86 -0
  7. package/dist/backends/gimp/errors.js +21 -0
  8. package/dist/backends/gimp/launcher.js +6 -0
  9. package/dist/backends/gimp/session.js +629 -0
  10. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  11. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  12. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  13. package/dist/cli/config.js +28 -0
  14. package/dist/core/server.js +142 -56
  15. package/dist/core/settings.js +25 -0
  16. package/dist/core/tool-activity.js +114 -0
  17. package/dist/core/tool-groups.js +16 -0
  18. package/dist/core/tool-tiers.js +16 -0
  19. package/dist/diagnostics/collect.js +4 -0
  20. package/dist/index.js +5 -1
  21. package/dist/kernel/kernel.js +3 -0
  22. package/dist/kernel/module-lifecycle.js +69 -9
  23. package/dist/license/advisory.js +109 -0
  24. package/dist/license/entitlement.js +34 -6
  25. package/dist/license/polar-client.js +50 -7
  26. package/dist/license/store.js +59 -0
  27. package/dist/modules/gimp/index.js +35 -0
  28. package/dist/skills/editmamei-skill.zip +0 -0
  29. package/dist/telemetry/activity.js +1 -93
  30. package/dist/telemetry/client.js +55 -17
  31. package/dist/telemetry/events.js +5 -0
  32. package/dist/telemetry/outbox.js +43 -18
  33. package/dist/tools/adjustment-tools.js +2 -1
  34. package/dist/tools/gimp-adjustment-tools.js +382 -0
  35. package/dist/tools/gimp-core-tools.js +288 -0
  36. package/dist/tools/gimp-document-tools.js +321 -0
  37. package/dist/tools/gimp-filter-tools.js +93 -0
  38. package/dist/tools/gimp-geometry-tools.js +191 -0
  39. package/dist/tools/gimp-inspect-tools.js +70 -0
  40. package/dist/tools/gimp-mask-tools.js +102 -0
  41. package/dist/tools/gimp-shared.js +51 -0
  42. package/dist/tools/gimp-verify-tools.js +374 -0
  43. package/dist/utils/gimp-path.js +27 -0
  44. package/dist/utils/operation-timeouts.js +16 -0
  45. package/dist/utils/session-log.js +19 -1
  46. package/dist/utils/tool-helpers.js +14 -0
  47. package/dist/version.js +1 -1
  48. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { READ_ONLY_TOOLS, KEPT_WORK_TOOLS, nodeMajor, archToken, osMajor, boundM
7
7
  import { buildClientConnected, buildDiagnosticEvent, buildModuleStatus, buildSessionStart, buildSessionSummary, buildUsageEvent, dayBucket, normalizeDayBucket, isContentSafe, PS_VERSION_UNKNOWN, } from './events.js';
8
8
  import { sanitizeMessage, sanitizeSnippet, sanitizeStderrTail } from './sanitize.js';
9
9
  import { httpTransport, resolveEndpoint } from './transport.js';
10
- import { appendOutboxSync, clearOutbox, clearSessionState, readOutbox, readSessionState, writeSessionStateSync, } from './outbox.js';
10
+ import { appendOutboxSync, clearOutbox, clearSessionState, readOutboxWithDiscards, readSessionState, rewriteOutbox, writeSessionStateSync, } from './outbox.js';
11
11
  const MAX_BATCH_SIZE = 100;
12
12
  const MAX_QUEUE_SIZE = 500;
13
13
  const DEFAULT_FLUSH_INTERVAL_MS = 5 * 60_000;
@@ -43,6 +43,9 @@ export class TelemetryClient {
43
43
  editsOk = 0;
44
44
  keptWork = 0;
45
45
  droppedEvents = 0;
46
+ droppedOutbox = 0;
47
+ droppedUnsafe = 0;
48
+ usageCallsSent = 0;
46
49
  behindLatest = null;
47
50
  moduleUpdate = 'none';
48
51
  installAssets = {};
@@ -156,6 +159,9 @@ export class TelemetryClient {
156
159
  kept_work: this.keptWork,
157
160
  ...(this.behindLatest !== null ? { behind_latest: this.behindLatest } : {}),
158
161
  dropped_events: this.droppedEvents,
162
+ dropped_outbox: this.droppedOutbox,
163
+ dropped_unsafe: this.droppedUnsafe,
164
+ usage_calls_sent: this.usageCallsSent,
159
165
  ...(moduleStatus !== null ? { module_update: this.moduleUpdate } : {}),
160
166
  ...(this.installAssets.templates_saved !== undefined
161
167
  ? { templates_saved: this.installAssets.templates_saved }
@@ -241,15 +247,18 @@ export class TelemetryClient {
241
247
  async flush() {
242
248
  if (!this.active || this.queue.length === 0)
243
249
  return;
244
- const batch = this.restampPsVersion(this.queue.splice(0, this.maxBatchSize)).filter(isContentSafe);
250
+ const picked = this.restampPsVersion(this.queue.splice(0, this.maxBatchSize));
251
+ const batch = picked.filter(isContentSafe);
252
+ this.droppedUnsafe += picked.length - batch.length;
245
253
  if (batch.length === 0)
246
254
  return;
247
255
  try {
248
256
  await this.transport(this.endpoint, JSON.stringify({ events: batch }));
257
+ this.usageCallsSent += countUsageCalls(batch);
249
258
  }
250
259
  catch (err) {
251
260
  this.logger.debug(`telemetry flush failed, persisting ${batch.length} event(s) to outbox: ${errMsg(err)}`);
252
- appendOutboxSync(batch, this.outboxOpts);
261
+ this.droppedOutbox += appendOutboxSync(batch, this.outboxOpts);
253
262
  }
254
263
  }
255
264
  shutdown() {
@@ -264,16 +273,26 @@ export class TelemetryClient {
264
273
  }
265
274
  if (!this.active)
266
275
  return;
267
- if (this.settings.telemetry.usage && this.toolCallCount > 0) {
268
- this.enqueue(buildSessionSummary(this.dims, {
276
+ if (this.queue.length > 0) {
277
+ const picked = this.restampPsVersion(this.queue.splice(0));
278
+ const batch = picked.filter(isContentSafe);
279
+ this.droppedUnsafe += picked.length - batch.length;
280
+ this.droppedOutbox += appendOutboxSync(batch, this.outboxOpts);
281
+ }
282
+ const hasLossToReport = this.droppedEvents > 0 || this.droppedOutbox > 0 || this.droppedUnsafe > 0;
283
+ if (this.settings.telemetry.usage && (this.toolCallCount > 0 || hasLossToReport)) {
284
+ const summary = buildSessionSummary(this.dims, {
269
285
  tool_call_count: this.toolCallCount,
270
286
  distinct_tools: this.distinctTools.size,
271
287
  any_failures: this.anyFailures,
272
288
  ...this.summaryFields(),
273
- }, this.ensureStartDayBucket(), this.now()));
274
- }
275
- if (this.queue.length > 0) {
276
- appendOutboxSync(this.restampPsVersion(this.queue.splice(0)).filter(isContentSafe), this.outboxOpts);
289
+ }, this.ensureStartDayBucket(), this.now());
290
+ if (isContentSafe(summary)) {
291
+ const lost = appendOutboxSync([summary], this.outboxOpts);
292
+ if (lost > 0) {
293
+ this.logger.debug(`outbox discarded ${lost} event(s) while writing the session summary`);
294
+ }
295
+ }
277
296
  }
278
297
  clearSessionState(this.outboxOpts);
279
298
  }
@@ -286,7 +305,8 @@ export class TelemetryClient {
286
305
  appendOutboxSync([summaryFromState(stale)], this.outboxOpts);
287
306
  }
288
307
  clearSessionState(this.outboxOpts);
289
- const pending = readOutbox(this.outboxOpts);
308
+ const { events: pending, discarded } = readOutboxWithDiscards(this.outboxOpts);
309
+ this.droppedOutbox += discarded;
290
310
  if (pending.length === 0) {
291
311
  clearOutbox(this.outboxOpts);
292
312
  return;
@@ -295,22 +315,27 @@ export class TelemetryClient {
295
315
  clearOutbox(this.outboxOpts);
296
316
  return;
297
317
  }
298
- let allAccepted = true;
299
- for (let i = 0; i < pending.length; i += this.maxBatchSize) {
300
- const batch = pending.slice(i, i + this.maxBatchSize).filter(isContentSafe);
301
- if (batch.length === 0)
302
- continue;
318
+ const sendable = pending.filter(isContentSafe);
319
+ this.droppedUnsafe += pending.length - sendable.length;
320
+ let settled = 0;
321
+ for (let i = 0; i < sendable.length; i += this.maxBatchSize) {
322
+ const batch = sendable.slice(i, i + this.maxBatchSize);
303
323
  try {
304
324
  await this.transport(this.endpoint, JSON.stringify({ events: batch }));
325
+ this.usageCallsSent += countUsageCalls(batch);
305
326
  }
306
327
  catch (err) {
307
328
  this.logger.debug(`startup outbox flush failed: ${errMsg(err)}`);
308
- allAccepted = false;
309
329
  break;
310
330
  }
331
+ settled = i + batch.length;
311
332
  }
312
- if (allAccepted)
333
+ if (settled >= sendable.length) {
313
334
  clearOutbox(this.outboxOpts);
335
+ }
336
+ else {
337
+ rewriteOutbox(sendable.slice(settled), this.outboxOpts);
338
+ }
314
339
  }
315
340
  catch (err) {
316
341
  this.logger.debug(`startup outbox flush error: ${errMsg(err)}`);
@@ -342,6 +367,9 @@ function summaryFromState(s) {
342
367
  ...(s.kept_work !== undefined ? { kept_work: s.kept_work } : {}),
343
368
  ...(s.behind_latest !== undefined ? { behind_latest: s.behind_latest } : {}),
344
369
  ...(s.dropped_events !== undefined ? { dropped_events: s.dropped_events } : {}),
370
+ ...(s.dropped_outbox !== undefined ? { dropped_outbox: s.dropped_outbox } : {}),
371
+ ...(s.dropped_unsafe !== undefined ? { dropped_unsafe: s.dropped_unsafe } : {}),
372
+ ...(s.usage_calls_sent !== undefined ? { usage_calls_sent: s.usage_calls_sent } : {}),
345
373
  ...(s.module_update !== undefined ? { module_update: s.module_update } : {}),
346
374
  ...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
347
375
  ...(actionSets !== null ? { action_sets: actionSets } : {}),
@@ -350,6 +378,16 @@ function summaryFromState(s) {
350
378
  function errMsg(err) {
351
379
  return err instanceof Error ? err.message : String(err);
352
380
  }
381
+ function countUsageCalls(events) {
382
+ let n = 0;
383
+ for (const event of events) {
384
+ if (event.type !== 'usage')
385
+ continue;
386
+ const count = event.count;
387
+ n += typeof count === 'number' && Number.isInteger(count) && count > 0 ? count : 1;
388
+ }
389
+ return n;
390
+ }
353
391
  function clampCount(n) {
354
392
  if (!Number.isFinite(n))
355
393
  return null;
@@ -61,6 +61,11 @@ export function buildSessionSummary(dims, summary, tsBucket, now) {
61
61
  ...(summary.kept_work !== undefined ? { kept_work: summary.kept_work } : {}),
62
62
  ...(summary.behind_latest !== undefined ? { behind_latest: summary.behind_latest } : {}),
63
63
  ...(summary.dropped_events !== undefined ? { dropped_events: summary.dropped_events } : {}),
64
+ ...(summary.dropped_outbox !== undefined ? { dropped_outbox: summary.dropped_outbox } : {}),
65
+ ...(summary.dropped_unsafe !== undefined ? { dropped_unsafe: summary.dropped_unsafe } : {}),
66
+ ...(summary.usage_calls_sent !== undefined
67
+ ? { usage_calls_sent: summary.usage_calls_sent }
68
+ : {}),
64
69
  ...(summary.module_update !== undefined ? { module_update: summary.module_update } : {}),
65
70
  ...(summary.templates_saved !== undefined ? { templates_saved: summary.templates_saved } : {}),
66
71
  ...(summary.action_sets !== undefined ? { action_sets: summary.action_sets } : {}),
@@ -6,8 +6,8 @@ const logger = new Logger('TelemetryOutbox');
6
6
  const DIRNAME = '.editmamei';
7
7
  const OUTBOX_FILENAME = 'telemetry-outbox.ndjson';
8
8
  const SESSION_STATE_FILENAME = 'telemetry-session.json';
9
- export const MAX_OUTBOX_EVENTS = 1000;
10
- const MAX_OUTBOX_BYTES = 2_000_000;
9
+ export const MAX_OUTBOX_EVENTS = 5_000;
10
+ export const MAX_OUTBOX_BYTES = 2_000_000;
11
11
  function baseDir(opts) {
12
12
  return opts.dir ?? join(homedir(), DIRNAME);
13
13
  }
@@ -22,27 +22,31 @@ function ensureDir(path) {
22
22
  }
23
23
  export function appendOutboxSync(events, opts = {}) {
24
24
  if (events.length === 0)
25
- return;
25
+ return 0;
26
26
  const path = outboxPath(opts);
27
+ let discarded = 0;
27
28
  try {
28
29
  ensureDir(path);
29
30
  if (existsSync(path) && statSync(path).size > MAX_OUTBOX_BYTES) {
30
- compactOutbox(opts);
31
+ discarded += compactOutbox(opts);
31
32
  }
32
33
  const lines = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
33
34
  appendFileSync(path, lines, { encoding: 'utf8', mode: 0o600 });
34
35
  }
35
36
  catch (err) {
36
37
  logger.debug(`outbox append dropped ${events.length} event(s): ${errMsg(err)}`);
38
+ discarded += events.length;
37
39
  }
40
+ return discarded;
38
41
  }
39
- export function readOutbox(opts = {}) {
42
+ export function readOutboxWithDiscards(opts = {}) {
40
43
  const path = outboxPath(opts);
41
44
  if (!existsSync(path))
42
- return [];
45
+ return { events: [], discarded: 0 };
43
46
  try {
44
47
  const raw = readFileSync(path, 'utf8');
45
48
  const events = [];
49
+ let discarded = 0;
46
50
  for (const line of raw.split('\n')) {
47
51
  const trimmed = line.trim();
48
52
  if (trimmed.length === 0)
@@ -51,17 +55,23 @@ export function readOutbox(opts = {}) {
51
55
  events.push(JSON.parse(trimmed));
52
56
  }
53
57
  catch {
58
+ discarded += 1;
54
59
  }
55
60
  }
56
- return events.length > MAX_OUTBOX_EVENTS
57
- ? events.slice(events.length - MAX_OUTBOX_EVENTS)
58
- : events;
61
+ if (events.length > MAX_OUTBOX_EVENTS) {
62
+ const over = events.length - MAX_OUTBOX_EVENTS;
63
+ return { events: events.slice(over), discarded: discarded + over };
64
+ }
65
+ return { events, discarded };
59
66
  }
60
67
  catch (err) {
61
68
  logger.debug(`outbox read failed: ${errMsg(err)}`);
62
- return [];
69
+ return { events: [], discarded: 0 };
63
70
  }
64
71
  }
72
+ export function readOutbox(opts = {}) {
73
+ return readOutboxWithDiscards(opts).events;
74
+ }
65
75
  export function clearOutbox(opts = {}) {
66
76
  try {
67
77
  rmSync(outboxPath(opts), { force: true });
@@ -70,24 +80,39 @@ export function clearOutbox(opts = {}) {
70
80
  logger.debug(`outbox clear failed: ${errMsg(err)}`);
71
81
  }
72
82
  }
73
- function compactOutbox(opts = {}) {
74
- const kept = readOutbox(opts);
83
+ export function rewriteOutbox(events, opts = {}) {
84
+ if (events.length === 0) {
85
+ clearOutbox(opts);
86
+ return;
87
+ }
75
88
  const path = outboxPath(opts);
76
89
  try {
77
- if (kept.length === 0) {
78
- clearOutbox(opts);
79
- return;
80
- }
90
+ ensureDir(path);
81
91
  const tmp = join(dirname(path), `.outbox.${process.pid}.tmp`);
82
- writeFileSync(tmp, kept.map((e) => JSON.stringify(e)).join('\n') + '\n', {
92
+ writeFileSync(tmp, events.map((e) => JSON.stringify(e)).join('\n') + '\n', {
83
93
  encoding: 'utf8',
84
94
  mode: 0o600,
85
95
  });
86
96
  renameSync(tmp, path);
87
97
  }
88
98
  catch (err) {
89
- logger.debug(`outbox compaction failed: ${errMsg(err)}`);
99
+ logger.debug(`outbox rewrite failed: ${errMsg(err)}`);
100
+ }
101
+ }
102
+ function compactOutbox(opts = {}) {
103
+ const { events, discarded } = readOutboxWithDiscards(opts);
104
+ const target = MAX_OUTBOX_BYTES / 2;
105
+ let bytes = 0;
106
+ let firstKept = Math.max(events.length - 1, 0);
107
+ for (let i = events.length - 1; i >= 0; i--) {
108
+ bytes += Buffer.byteLength(JSON.stringify(events[i]), 'utf8') + 1;
109
+ if (bytes > target)
110
+ break;
111
+ firstKept = i;
90
112
  }
113
+ const kept = events.slice(firstKept);
114
+ rewriteOutbox(kept, opts);
115
+ return discarded + (events.length - kept.length);
91
116
  }
92
117
  export function writeSessionStateSync(state, opts = {}) {
93
118
  const path = sessionStatePath(opts);
@@ -2,6 +2,7 @@ import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
3
  import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
4
4
  import { getPendingRawDevelop } from '../core/raw-develop-state.js';
5
+ import { CAMERA_RAW_TOOL } from '../core/tool-activity.js';
5
6
  const addAdjustmentLayerSchema = {
6
7
  type: 'object',
7
8
  properties: {
@@ -844,7 +845,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
844
845
  if (pendingRaw) {
845
846
  const source = pendingRaw.documentName || pendingRaw.filePath || 'a raw source';
846
847
  const note = `This document was opened from raw source ${source} with no Camera Raw develop pass yet — ` +
847
- 'consider ps_apply_camera_raw first for global tone/color. Nothing failed; ignore if the ' +
848
+ `consider ${CAMERA_RAW_TOOL} first for global tone/color. Nothing failed; ignore if the ` +
848
849
  'user prescribed this exact layer or the file was already developed elsewhere.';
849
850
  if (typeof result === 'object' && result !== null) {
850
851
  structured.raw_develop_pending = note;