editmamei 0.19.0 → 0.22.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 (72) hide show
  1. package/NOTICES.md +17 -1
  2. package/README.md +9 -8
  3. package/dist/api/extendscript/_helpers.js +1 -1
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/config.js +3 -3
  8. package/dist/cli/help.js +4 -0
  9. package/dist/cli/install.js +2 -2
  10. package/dist/cli/report.js +11 -0
  11. package/dist/cli/router.js +20 -0
  12. package/dist/core/server.js +8 -8
  13. package/dist/core/settings.js +2 -2
  14. package/dist/core/tool-groups.js +92 -82
  15. package/dist/core/tool-tiers.js +91 -81
  16. package/dist/detection/landmark-detection-client.js +8 -0
  17. package/dist/detection/mesh-eye-correction.js +126 -0
  18. package/dist/detection/sam-segmenter.js +83 -0
  19. package/dist/diagnostics/collect.js +169 -0
  20. package/dist/index.js +2 -0
  21. package/dist/license/entitlement.js +52 -0
  22. package/dist/modules/ce/index.js +6 -4
  23. package/dist/perception/grounding-anchors.js +37 -0
  24. package/dist/perception/grounding-corners.js +93 -0
  25. package/dist/perception/grounding-edge-trace.js +90 -0
  26. package/dist/perception/grounding-extrema.js +41 -0
  27. package/dist/perception/grounding-gate.js +154 -0
  28. package/dist/perception/grounding-geometry.js +132 -0
  29. package/dist/perception/grounding-grid.js +34 -0
  30. package/dist/perception/grounding-landmarks.js +40 -0
  31. package/dist/perception/grounding-locate.js +341 -0
  32. package/dist/perception/grounding-resolver.js +202 -0
  33. package/dist/perception/grounding-review-crop.js +85 -0
  34. package/dist/perception/grounding-warp.js +182 -0
  35. package/dist/perception/sky-mask-transfer.js +50 -18
  36. package/dist/skills/editmamei-skill.zip +0 -0
  37. package/dist/tools/adjustment-tools.js +3 -3
  38. package/dist/tools/brush-tools.js +111 -23
  39. package/dist/tools/channel-compose-tools.js +241 -0
  40. package/dist/tools/detection-tools.js +1 -1
  41. package/dist/tools/diagnostics-tools.js +66 -0
  42. package/dist/tools/document-tools.js +6 -6
  43. package/dist/tools/filter-tools.js +476 -19
  44. package/dist/tools/group-tools.js +10 -10
  45. package/dist/tools/guide-tools.js +4 -4
  46. package/dist/tools/history-tools.js +4 -4
  47. package/dist/tools/image-placement-tools.js +1 -1
  48. package/dist/tools/image-tools.js +62 -19
  49. package/dist/tools/inspect-tools.js +2 -2
  50. package/dist/tools/layer-ordering-tools.js +1 -1
  51. package/dist/tools/layer-properties-tools.js +42 -23
  52. package/dist/tools/layer-tools.js +10 -10
  53. package/dist/tools/layer-transform-tools.js +80 -41
  54. package/dist/tools/overview-tools.js +42 -42
  55. package/dist/tools/path-tools.js +53 -10
  56. package/dist/tools/portrait-tools.js +3 -3
  57. package/dist/tools/preview-tools.js +7 -82
  58. package/dist/tools/retouch-tools.js +93 -25
  59. package/dist/tools/scene-tools.js +13 -13
  60. package/dist/tools/selection-tools.js +319 -40
  61. package/dist/tools/shape-tools.js +216 -0
  62. package/dist/tools/text-tools.js +1 -1
  63. package/dist/tools/transform-canvas-tools.js +2 -2
  64. package/dist/tools/vector-mask-tools.js +23 -8
  65. package/dist/utils/log-buffer.js +41 -0
  66. package/dist/utils/logger.js +12 -1
  67. package/dist/utils/session-log-reader.js +27 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +1 -1
  70. package/dist/preview/coordinate-grid.js +0 -250
  71. package/dist/tools/object-tools.js +0 -193
  72. package/dist/tools/text-on-object-tools.js +0 -225
@@ -0,0 +1,216 @@
1
+ import { runScript } from '../utils/run-script.js';
2
+ import { validateArgs } from '../utils/validate.js';
3
+ import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
4
+ import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
5
+ const SHAPE_TYPES = ['rectangle', 'ellipse', 'line'];
6
+ const rgbColorFragment = {
7
+ type: 'object',
8
+ description: 'RGB color 0-255.',
9
+ properties: {
10
+ r: { type: 'number', minimum: 0, maximum: 255 },
11
+ g: { type: 'number', minimum: 0, maximum: 255 },
12
+ b: { type: 'number', minimum: 0, maximum: 255 },
13
+ },
14
+ required: ['r', 'g', 'b'],
15
+ };
16
+ const shapeInputSchema = {
17
+ type: 'object',
18
+ properties: {
19
+ type: {
20
+ type: 'string',
21
+ enum: [...SHAPE_TYPES],
22
+ description: 'rectangle: a box (left/top/right/bottom; corner_radius>0 rounds the corners). ' +
23
+ 'ellipse: an oval in the left/top/right/bottom bounding box. ' +
24
+ 'line: a straight line from (start_x,start_y) to (end_x,end_y) with `weight` thickness.',
25
+ },
26
+ placement: {
27
+ ...PLACEMENT_SCHEMA,
28
+ description: 'ANCHOR-RELATIONAL placement (preferred over guessing pixels): rectangle/ellipse ← a region relation ' +
29
+ '(inside/gap) → the resolved region bounding box; line ← a path relation (along/offset-curve over a traced ' +
30
+ 'edge or a Pro face-mesh landmark curve) → a straight line between the resolved curve endpoints. The shape ' +
31
+ 'is created ONLY if the gate PASSES (otherwise an error and no layer). When set, left/top/right/bottom and ' +
32
+ 'start_x/start_y/end_x/end_y are ignored, but styling (fill_color/stroke/weight/corner_radius) still ' +
33
+ 'applies. See ps_resolve_placement for the anchors + relation vocabulary.',
34
+ },
35
+ left: {
36
+ type: 'number',
37
+ description: 'rectangle/ellipse: bounding-box left edge, document px.',
38
+ },
39
+ top: { type: 'number', description: 'rectangle/ellipse: bounding-box top edge, document px.' },
40
+ right: {
41
+ type: 'number',
42
+ description: 'rectangle/ellipse: bounding-box right edge, document px.',
43
+ },
44
+ bottom: {
45
+ type: 'number',
46
+ description: 'rectangle/ellipse: bounding-box bottom edge, document px.',
47
+ },
48
+ corner_radius: {
49
+ type: 'number',
50
+ description: 'rectangle only: corner radius in px. 0 (default) = sharp corners; >0 = rounded.',
51
+ minimum: 0,
52
+ default: 0,
53
+ },
54
+ start_x: { type: 'number', description: 'line: start point X, document px.' },
55
+ start_y: { type: 'number', description: 'line: start point Y, document px.' },
56
+ end_x: { type: 'number', description: 'line: end point X, document px.' },
57
+ end_y: { type: 'number', description: 'line: end point Y, document px.' },
58
+ weight: {
59
+ type: 'number',
60
+ description: 'line only: line thickness in px. Default 4.',
61
+ minimum: 0.1,
62
+ default: 4,
63
+ },
64
+ fill_color: {
65
+ ...rgbColorFragment,
66
+ description: 'Fill color (the line color for type=line). RGB 0-255. Default black.',
67
+ },
68
+ stroke_width: {
69
+ type: 'number',
70
+ description: 'rectangle/ellipse: outline width in px. 0 (default) = no stroke (fill only). Ignored for line (use weight).',
71
+ minimum: 0,
72
+ default: 0,
73
+ },
74
+ stroke_color: {
75
+ ...rgbColorFragment,
76
+ description: 'rectangle/ellipse stroke color when stroke_width>0. RGB 0-255. Default black.',
77
+ },
78
+ },
79
+ required: ['type'],
80
+ };
81
+ export function createShapeTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
82
+ return [
83
+ {
84
+ tool: {
85
+ name: 'ps_shape',
86
+ description: 'Draw a vector SHAPE layer — `rectangle` (optionally rounded via corner_radius), `ellipse`, or `line` — filled with a solid color, optionally stroked. Aim it EITHER by anchor-relational `placement` (preferred: name anchors + a relation and the resolver computes the geometry, verified by an objective gate — no pixel-guessing; rectangle/ellipse ← a region relation, line ← a path relation) OR by ABSOLUTE document pixels (top-left origin: rectangle/ellipse take left/top/right/bottom; line takes start_x/start_y → end_x/end_y plus weight — you must know the pixel positions, so prefer the anchor-relational `placement` path above and verify the result with a preview). Creates a new vector layer (non-destructive — delete it to remove). (AM-only; verified live on PS 27.2.0.)',
87
+ inputSchema: shapeInputSchema,
88
+ outputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ shape_created: { type: 'boolean' },
92
+ shape_type: { type: 'string' },
93
+ layer_name: { type: 'string' },
94
+ stroked: { type: 'boolean' },
95
+ placement: {
96
+ type: 'object',
97
+ description: 'Present when anchor-relational placement was used: the resolved geometry + gate verdict.',
98
+ properties: {
99
+ target: { type: 'string' },
100
+ gate: { type: 'object' },
101
+ anchors: { type: 'object' },
102
+ geometry: { type: 'object' },
103
+ },
104
+ },
105
+ context: { type: 'object' },
106
+ },
107
+ },
108
+ annotations: {
109
+ title: 'Shape',
110
+ idempotentHint: false,
111
+ },
112
+ },
113
+ handler: async (args) => createShape(connection, snippetClient, client, args),
114
+ },
115
+ ];
116
+ }
117
+ function colorParams(color, prefix, params) {
118
+ const c = color ?? { r: 0, g: 0, b: 0 };
119
+ params[`${prefix}R`] = c.r ?? 0;
120
+ params[`${prefix}G`] = c.g ?? 0;
121
+ params[`${prefix}B`] = c.b ?? 0;
122
+ }
123
+ async function createShape(connection, snippetClient, client, rawArgs) {
124
+ try {
125
+ const args = validateArgs(shapeInputSchema, rawArgs);
126
+ const type = args.type;
127
+ const params = { shapeType: type };
128
+ let placementInfo;
129
+ if (args.placement && type === 'line') {
130
+ const rp = await resolveExpectedPlacement(connection, client, args.placement, 'path', 'line');
131
+ const start = rp.curve[0];
132
+ const end = rp.curve[rp.curve.length - 1];
133
+ const geometry = { startX: start.x, startY: start.y, endX: end.x, endY: end.y };
134
+ Object.assign(params, geometry);
135
+ placementInfo = {
136
+ target: rp.target,
137
+ anchors: rp.anchors,
138
+ geometry,
139
+ summary: `(${start.x},${start.y})→(${end.x},${end.y})`,
140
+ };
141
+ }
142
+ else if (args.placement) {
143
+ const rp = await resolveExpectedPlacement(connection, client, args.placement, 'region', type);
144
+ const geometry = { ...rp.bbox };
145
+ Object.assign(params, geometry);
146
+ placementInfo = { target: rp.target, anchors: rp.anchors, geometry, summary: rp.summary };
147
+ }
148
+ else if (type === 'rectangle' || type === 'ellipse') {
149
+ for (const k of ['left', 'top', 'right', 'bottom']) {
150
+ if (typeof args[k] !== 'number') {
151
+ throw new Error(`${type} requires numeric left/top/right/bottom (missing ${k})`);
152
+ }
153
+ params[k] = args[k];
154
+ }
155
+ }
156
+ else {
157
+ const map = {
158
+ start_x: 'startX',
159
+ start_y: 'startY',
160
+ end_x: 'endX',
161
+ end_y: 'endY',
162
+ };
163
+ for (const k of Object.keys(map)) {
164
+ if (typeof args[k] !== 'number') {
165
+ throw new Error(`line requires numeric start_x/start_y/end_x/end_y (missing ${k})`);
166
+ }
167
+ params[map[k]] = args[k];
168
+ }
169
+ }
170
+ if (type === 'rectangle')
171
+ params.cornerRadius = args.corner_radius ?? 0;
172
+ else if (type === 'ellipse')
173
+ params.cornerRadius = 0;
174
+ else
175
+ params.weight = args.weight ?? 4;
176
+ colorParams(args.fill_color, 'fill', params);
177
+ if (type !== 'line') {
178
+ params.strokeWidth = args.stroke_width ?? 0;
179
+ colorParams(args.stroke_color, 'stroke', params);
180
+ }
181
+ const script = await snippetClient.build('createShape', params);
182
+ const result = (await runScript(connection, script));
183
+ return {
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: placementInfo
188
+ ? `Created ${type} shape layer "${String(result.layer_name ?? '')}" at ${placementInfo.summary} (placement gate PASS).`
189
+ : `Created ${type} shape layer "${String(result.layer_name ?? '')}".`,
190
+ },
191
+ ],
192
+ structuredContent: placementInfo
193
+ ? {
194
+ ...result,
195
+ placement: {
196
+ target: placementInfo.target,
197
+ gate: { pass: true },
198
+ anchors: placementInfo.anchors,
199
+ geometry: placementInfo.geometry,
200
+ },
201
+ }
202
+ : result,
203
+ };
204
+ }
205
+ catch (error) {
206
+ return {
207
+ content: [
208
+ {
209
+ type: 'text',
210
+ text: `Error in ps_shape: ${error instanceof Error ? error.message : String(error)}`,
211
+ },
212
+ ],
213
+ isError: true,
214
+ };
215
+ }
216
+ }
@@ -90,7 +90,7 @@ export function createTextTools(connection, snippetClient) {
90
90
  return [
91
91
  {
92
92
  tool: {
93
- name: 'photoshop_set_text',
93
+ name: 'ps_set_text',
94
94
  description: 'Set an attribute of the currently active text layer — font (family or PostScript name, optionally size), color (RGB), alignment, or text content — selected via `property`. Idempotent. Throws if the active layer is not a text layer. Font names accept either the PostScript name ("ArialMT") or family name ("Arial", resolved to its Regular/first variant); throws clearly if no installed font matches.',
95
95
  inputSchema: SET_TEXT_INPUT_SCHEMA,
96
96
  outputSchema: {
@@ -39,8 +39,8 @@ export function createTransformCanvasTools(connection, snippetClient) {
39
39
  return [
40
40
  {
41
41
  tool: {
42
- name: 'photoshop_transform_canvas',
43
- description: 'Transform the whole document (canvas + all layers), not a single layer — choose with `op`. `rotate` spins the canvas by degrees (+cw; pass 90/180/-90 for quarter/half turns). `flip` mirrors the canvas horizontal|vertical. For rotating/flipping ONE layer use photoshop_transform_layer instead.',
42
+ name: 'ps_transform_canvas',
43
+ description: 'Transform the whole document (canvas + all layers), not a single layer — choose with `op`. `rotate` spins the canvas by degrees (+cw; pass 90/180/-90 for quarter/half turns). `flip` mirrors the canvas horizontal|vertical. For rotating/flipping ONE layer use ps_transform_layer instead.',
44
44
  inputSchema: CANVAS_INPUT_SCHEMA,
45
45
  outputSchema: {
46
46
  type: 'object',
@@ -1,22 +1,24 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
- const VECTOR_MASK_OPS = ['add', 'delete', 'link', 'unlink'];
4
- const ADD_SOURCES = ['from_current_path'];
3
+ const VECTOR_MASK_OPS = ['add', 'delete', 'link', 'unlink', 'enable', 'disable'];
4
+ const ADD_SOURCES = ['from_current_path', 'reveal_all', 'hide_all'];
5
5
  const vectorMaskInputSchema = {
6
6
  type: 'object',
7
7
  properties: {
8
8
  op: {
9
9
  type: 'string',
10
10
  enum: [...VECTOR_MASK_OPS],
11
- description: 'add: turn the active path into a vector mask on the active layer (needs a path — make one with photoshop_path first). ' +
11
+ description: 'add: create a vector mask on the active layer — seeded from the active path (source=from_current_path, needs a path from ps_path) or as an empty reveal_all/hide_all mask (no path needed). ' +
12
12
  "delete: remove the active layer's vector mask. " +
13
13
  'link: link the vector mask to the layer (they transform together). ' +
14
- 'unlink: unlink the vector mask from the layer (transform independently).',
14
+ 'unlink: unlink the vector mask from the layer (transform independently). ' +
15
+ 'disable: turn the vector mask off (the layer renders unmasked) without deleting it. ' +
16
+ 'enable: turn a disabled vector mask back on.',
15
17
  },
16
18
  source: {
17
19
  type: 'string',
18
20
  enum: [...ADD_SOURCES],
19
- description: "add only: what the vector mask is seeded from. Currently only 'from_current_path' (the active work/saved path) — the main consumer of photoshop_path.",
21
+ description: "add only: what the vector mask is seeded from. 'from_current_path' = the active work/saved path (needs a path; the main consumer of ps_path). 'reveal_all' = an empty mask that reveals the whole layer. 'hide_all' = an empty mask that hides the whole layer (paint/draw paths to reveal).",
20
22
  default: 'from_current_path',
21
23
  },
22
24
  },
@@ -26,8 +28,8 @@ export function createVectorMaskTools(connection, snippetClient) {
26
28
  return [
27
29
  {
28
30
  tool: {
29
- name: 'photoshop_vector_mask',
30
- description: 'Attach, remove, or (un)link a layer VECTOR MASK — the path-driven, resolution-independent companion to a pixel layer mask. Ops: `add` (converts the active path into the mask — the typical flow after photoshop_path op=create_from_selection / save), `delete`, `link`, `unlink`. **Reach for this when** you want a crisp vector-edged mask from a path rather than a painted pixel mask. `add` needs an existing path and a non-background layer. (Vector masks are AM-driven; verified live on PS 27.2.0.)',
31
+ name: 'ps_vector_mask',
32
+ description: 'Attach, remove, (un)link, or (dis/en)able a layer VECTOR MASK — the path-driven, resolution-independent companion to a pixel layer mask. Ops: `add` (from the active path via source=from_current_path — the typical flow after ps_path op=create_from_selection / save — or an empty source=reveal_all/hide_all mask), `delete`, `link`, `unlink`, `disable` (turn the mask off without deleting it), `enable` (turn it back on). **Reach for this when** you want a crisp vector-edged mask rather than a painted pixel mask. `add` needs a non-background layer (and a path for from_current_path); the other ops need a layer that already has a vector mask. (Vector masks are AM-driven; verified live on PS 27.2.0.)',
31
33
  inputSchema: vectorMaskInputSchema,
32
34
  outputSchema: {
33
35
  type: 'object',
@@ -35,6 +37,7 @@ export function createVectorMaskTools(connection, snippetClient) {
35
37
  vector_mask_added: { type: 'boolean' },
36
38
  vector_mask_deleted: { type: 'boolean' },
37
39
  vector_mask_linked: { type: 'boolean' },
40
+ vector_mask_enabled: { type: 'boolean' },
38
41
  source: { type: 'string' },
39
42
  layer_name: { type: 'string' },
40
43
  context: { type: 'object' },
@@ -73,6 +76,14 @@ async function runVectorMaskOp(connection, snippetClient, rawArgs) {
73
76
  snippet = 'setVectorMaskLink';
74
77
  params = { linked: false };
75
78
  break;
79
+ case 'enable':
80
+ snippet = 'setVectorMaskEnabled';
81
+ params = { enabled: true };
82
+ break;
83
+ case 'disable':
84
+ snippet = 'setVectorMaskEnabled';
85
+ params = { enabled: false };
86
+ break;
76
87
  default:
77
88
  return {
78
89
  content: [{ type: 'text', text: `Unknown vector-mask op: ${String(op)}` }],
@@ -91,7 +102,7 @@ async function runVectorMaskOp(connection, snippetClient, rawArgs) {
91
102
  content: [
92
103
  {
93
104
  type: 'text',
94
- text: `Error in photoshop_vector_mask: ${error instanceof Error ? error.message : String(error)}`,
105
+ text: `Error in ps_vector_mask: ${error instanceof Error ? error.message : String(error)}`,
95
106
  },
96
107
  ],
97
108
  isError: true,
@@ -110,6 +121,10 @@ function summarize(op, r) {
110
121
  return `Linked the vector mask to the layer${on}.`;
111
122
  case 'unlink':
112
123
  return `Unlinked the vector mask from the layer${on}.`;
124
+ case 'enable':
125
+ return `Enabled the vector mask${on}.`;
126
+ case 'disable':
127
+ return `Disabled the vector mask${on}.`;
113
128
  default:
114
129
  return 'Vector-mask op complete.';
115
130
  }
@@ -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
+ }
@@ -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
- this.logLevel = parseLogLevel(process.env.LOG_LEVEL, logLevel);
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.19.0';
1
+ export const VERSION = '0.22.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editmamei",
3
- "version": "0.19.0",
3
+ "version": "0.22.0",
4
4
  "description": "Editmamei — Unlock Photoshop with natural-language photo editing (Community Edition)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",