dsh-offwork 1.0.0 → 1.0.1

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/clock.js CHANGED
@@ -5,37 +5,48 @@ export function calculate(settings, timestamp = Date.now()) {
5
5
  if (!Number.isFinite(now)) throw new RangeError('Invalid timestamp');
6
6
  const DAY = 86400000;
7
7
  const formatter = new Intl.DateTimeFormat('en-CA', {
8
- timeZone: settings.timezone, year: 'numeric', month: '2-digit', day: '2-digit',
9
- hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
8
+ timeZone: settings.timezone,
9
+ year: 'numeric',
10
+ month: '2-digit',
11
+ day: '2-digit',
12
+ hour: '2-digit',
13
+ minute: '2-digit',
14
+ second: '2-digit',
15
+ hourCycle: 'h23',
10
16
  });
11
- const civil = time => {
12
- const p = Object.fromEntries(formatter.formatToParts(time).map(p => [p.type, p.value]));
17
+ const civil = (time) => {
18
+ const p = Object.fromEntries(formatter.formatToParts(time).map((p) => [p.type, p.value]));
13
19
  return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
14
20
  };
15
21
  // Convert a timezone-local civil time to an absolute instant. At DST overlaps use
16
22
  // the earlier occurrence; at gaps move forward by the gap (Temporal-compatible).
17
- const instant = local => {
18
- const offsets = [...new Set([-DAY, 0, DAY].map(delta => civil(local + delta) - (local + delta)))];
19
- const candidates = offsets.map(offset => local - offset);
20
- const exact = candidates.filter(time => civil(time) === local);
23
+ const instant = (local) => {
24
+ const offsets = [
25
+ ...new Set([-DAY, 0, DAY].map((delta) => civil(local + delta) - (local + delta))),
26
+ ];
27
+ const candidates = offsets.map((offset) => local - offset);
28
+ const exact = candidates.filter((time) => civil(time) === local);
21
29
  if (exact.length) return Math.min(...exact);
22
- const after = candidates.filter(time => civil(time) >= local);
30
+ const after = candidates.filter((time) => civil(time) >= local);
23
31
  return Math.min(...(after.length ? after : candidates));
24
32
  };
25
- const minute = text => {
33
+ const minute = (text) => {
26
34
  if (typeof text !== 'string' || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(text)) {
27
35
  throw new RangeError('Shift times must be HH:mm');
28
36
  }
29
37
  const [h, m] = text.split(':').map(Number);
30
38
  return h * 60 + m;
31
39
  };
32
- const start = minute(settings.start), end = minute(settings.end);
40
+ const start = minute(settings.start),
41
+ end = minute(settings.end);
33
42
  if (start === end) throw new RangeError('Shift must not be empty');
34
43
  const overnight = end < start;
35
44
  const duration = (end - start + 1440) % 1440;
36
- let breakOffset = 0, breakDuration = 0;
45
+ let breakOffset = 0,
46
+ breakDuration = 0;
37
47
  if (settings.breakStart != null || settings.breakEnd != null) {
38
- const bs = minute(settings.breakStart), be = minute(settings.breakEnd);
48
+ const bs = minute(settings.breakStart),
49
+ be = minute(settings.breakEnd);
39
50
  if (bs !== be) {
40
51
  breakOffset = (bs - start + 1440) % 1440;
41
52
  breakDuration = (be - bs + 1440) % 1440;
@@ -47,7 +58,7 @@ export function calculate(settings, timestamp = Date.now()) {
47
58
  const localNow = civil(now);
48
59
  const today = Math.floor(localNow / DAY) * DAY;
49
60
  const minuteNow = (localNow - today) / 60000;
50
- const workday = day => settings.weekdays.includes(new Date(day).getUTCDay());
61
+ const workday = (day) => settings.weekdays.includes(new Date(day).getUTCDay());
51
62
  let shiftDay = today;
52
63
  if (overnight && minuteNow < start) {
53
64
  // Retain yesterday's finished work shift until the next shift starts. If
@@ -58,46 +69,82 @@ export function calculate(settings, timestamp = Date.now()) {
58
69
  const shiftStart = instant(shiftDay + start * 60000);
59
70
  const shiftEnd = instant(shiftDay + (start + duration) * 60000);
60
71
  const breakStart = breakDuration ? instant(shiftDay + (start + breakOffset) * 60000) : null;
61
- const breakEnd = breakDuration ? instant(shiftDay + (start + breakOffset + breakDuration) * 60000) : null;
72
+ const breakEnd = breakDuration
73
+ ? instant(shiftDay + (start + breakOffset + breakDuration) * 60000)
74
+ : null;
62
75
  const breakMs = breakDuration ? breakEnd - breakStart : 0;
63
76
  const totalMs = shiftEnd - shiftStart - breakMs;
64
77
  if (totalMs <= 0 || breakMs < 0) throw new RangeError('Shift has no paid duration');
65
78
  const clampedNow = Math.max(shiftStart, Math.min(now, shiftEnd));
66
- const elapsedMs = isWorkday ? Math.max(0, Math.min(totalMs,
67
- clampedNow - shiftStart - (breakDuration ? Math.max(0, Math.min(clampedNow, breakEnd) - breakStart) : 0))) : 0;
68
- const status = !isWorkday ? 'rest' : now < shiftStart ? 'before' : now >= shiftEnd ? 'done'
69
- : breakDuration && now >= breakStart && now < breakEnd ? 'lunch' : 'working';
79
+ const elapsedMs = isWorkday
80
+ ? Math.max(
81
+ 0,
82
+ Math.min(
83
+ totalMs,
84
+ clampedNow -
85
+ shiftStart -
86
+ (breakDuration ? Math.max(0, Math.min(clampedNow, breakEnd) - breakStart) : 0),
87
+ ),
88
+ )
89
+ : 0;
90
+ const status = !isWorkday
91
+ ? 'rest'
92
+ : now < shiftStart
93
+ ? 'before'
94
+ : now >= shiftEnd
95
+ ? 'done'
96
+ : breakDuration && now >= breakStart && now < breakEnd
97
+ ? 'lunch'
98
+ : 'working';
70
99
  // Decimal inputs become exact rational numbers before truncation. Never use
71
100
  // floor((salary / days * progress) * 100): binary rounding loses whole cents.
72
- const rational = number => {
73
- if (typeof number !== 'number' || !Number.isFinite(number) || number < 0) throw new RangeError('Invalid salary');
101
+ const rational = (number) => {
102
+ if (typeof number !== 'number' || !Number.isFinite(number) || number < 0)
103
+ throw new RangeError('Invalid salary');
74
104
  const [mantissa, exponent = '0'] = String(number).toLowerCase().split('e');
75
105
  const [integer, fraction = ''] = mantissa.split('.');
76
106
  const scale = fraction.length - Number(exponent);
77
- return scale >= 0 ? [BigInt(integer + fraction), 10n ** BigInt(scale)]
107
+ return scale >= 0
108
+ ? [BigInt(integer + fraction), 10n ** BigInt(scale)]
78
109
  : [BigInt(integer + fraction) * 10n ** BigInt(-scale), 1n];
79
110
  };
80
111
  const payMode = settings.payMode === 'daily' ? 'daily' : 'monthly';
81
- let [numerator, denominator] = rational(payMode === 'daily' ? settings.dailySalary : settings.monthlySalary);
112
+ let [numerator, denominator] = rational(
113
+ payMode === 'daily' ? settings.dailySalary : settings.monthlySalary,
114
+ );
82
115
  if (payMode === 'monthly') {
83
116
  const [daysN, daysD] = rational(settings.workdaysPerMonth);
84
117
  if (daysN === 0n) throw new RangeError('Workdays per month must be positive');
85
118
  numerator *= daysD;
86
119
  denominator *= daysN;
87
120
  }
88
- const dailyCents = Number(numerator * 100n / denominator);
89
- const earnedCents = Math.min(dailyCents, Number(numerator * 100n * BigInt(Math.floor(elapsedMs)) /
90
- (denominator * BigInt(totalMs))));
91
- const hourlyCents = Number(numerator * 100n * 3600000n / (denominator * BigInt(totalMs)));
92
- const dateString = day => new Date(day).toISOString().slice(0, 10);
121
+ const dailyCents = Number((numerator * 100n) / denominator);
122
+ const earnedCents = Math.min(
123
+ dailyCents,
124
+ Number((numerator * 100n * BigInt(Math.floor(elapsedMs))) / (denominator * BigInt(totalMs))),
125
+ );
126
+ const hourlyCents = Number((numerator * 100n * 3600000n) / (denominator * BigInt(totalMs)));
127
+ const dateString = (day) => new Date(day).toISOString().slice(0, 10);
93
128
  return {
94
- status, remaining: isWorkday ? Math.max(0, Math.ceil((shiftEnd - now) / 1000)) : 0,
129
+ status,
130
+ remaining: isWorkday ? Math.max(0, Math.ceil((shiftEnd - now) / 1000)) : 0,
95
131
  startRemaining: status === 'before' ? Math.max(0, Math.ceil((shiftStart - now) / 1000)) : 0,
96
132
  progress: elapsedMs / totalMs,
97
- earned: earnedCents / 100, daily: dailyCents / 100, hourly: hourlyCents / 100,
98
- date: dateString(today), shiftDate: dateString(shiftDay),
99
- earnedCents, dailyCents, payMode, isWorkday, overnight,
100
- shiftStart, shiftEnd, breakStart, breakEnd,
101
- elapsed: elapsedMs / 1000, total: totalMs / 1000,
133
+ earned: earnedCents / 100,
134
+ daily: dailyCents / 100,
135
+ hourly: hourlyCents / 100,
136
+ date: dateString(today),
137
+ shiftDate: dateString(shiftDay),
138
+ earnedCents,
139
+ dailyCents,
140
+ payMode,
141
+ isWorkday,
142
+ overnight,
143
+ shiftStart,
144
+ shiftEnd,
145
+ breakStart,
146
+ breakEnd,
147
+ elapsed: elapsedMs / 1000,
148
+ total: totalMs / 1000,
102
149
  };
103
150
  }
package/index.js CHANGED
@@ -20,8 +20,11 @@ export const DEFAULT_SETTINGS = Object.freeze({
20
20
 
21
21
  const dataFile = new URL('./data.json', import.meta.url);
22
22
  const keys = Object.keys(DEFAULT_SETTINGS);
23
- const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
24
- const fail = error => ({ ok: false, error: error instanceof Error ? error.message : String(error) });
23
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
24
+ const fail = (error) => ({
25
+ ok: false,
26
+ error: error instanceof Error ? error.message : String(error),
27
+ });
25
28
 
26
29
  function validatePatch(value) {
27
30
  if (!isObject(value)) throw new Error('settings 必须是 JSON 对象。');
@@ -38,16 +41,28 @@ export function validateSettings(value) {
38
41
  if (value.payMode !== 'monthly' && value.payMode !== 'daily') {
39
42
  throw new Error('payMode 必须是 monthly 或 daily。');
40
43
  }
41
- if (typeof value.dailySalary !== 'number' || !Number.isFinite(value.dailySalary) || value.dailySalary < 0) {
44
+ if (
45
+ typeof value.dailySalary !== 'number' ||
46
+ !Number.isFinite(value.dailySalary) ||
47
+ value.dailySalary < 0
48
+ ) {
42
49
  throw new Error('dailySalary 必须是大于等于 0 的有限数字。');
43
50
  }
44
- if (typeof value.monthlySalary !== 'number' || !Number.isFinite(value.monthlySalary) || value.monthlySalary < 0) {
51
+ if (
52
+ typeof value.monthlySalary !== 'number' ||
53
+ !Number.isFinite(value.monthlySalary) ||
54
+ value.monthlySalary < 0
55
+ ) {
45
56
  throw new Error('monthlySalary 必须是大于等于 0 的有限数字。');
46
57
  }
47
- if (typeof value.workdaysPerMonth !== 'number' || !Number.isFinite(value.workdaysPerMonth) || value.workdaysPerMonth <= 0) {
58
+ if (
59
+ typeof value.workdaysPerMonth !== 'number' ||
60
+ !Number.isFinite(value.workdaysPerMonth) ||
61
+ value.workdaysPerMonth <= 0
62
+ ) {
48
63
  throw new Error('workdaysPerMonth 必须是大于 0 的有限数字。');
49
64
  }
50
- const minutes = key => {
65
+ const minutes = (key) => {
51
66
  if (typeof value[key] !== 'string' || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value[key])) {
52
67
  throw new Error(`${key} 必须是 24 小时制 HH:mm。`);
53
68
  }
@@ -59,7 +74,7 @@ export function validateSettings(value) {
59
74
  const breakStart = minutes('breakStart');
60
75
  const breakEnd = minutes('breakEnd');
61
76
  if (end === start) throw new Error('start 和 end 不能相同。');
62
- const relative = minute => (minute - start + 1440) % 1440;
77
+ const relative = (minute) => (minute - start + 1440) % 1440;
63
78
  const duration = relative(end);
64
79
  if (breakStart !== breakEnd) {
65
80
  const from = relative(breakStart);
@@ -69,16 +84,26 @@ export function validateSettings(value) {
69
84
  }
70
85
  if (to - from >= duration) throw new Error('午休不能占满整个班次。');
71
86
  }
72
- if (!Array.isArray(value.weekdays) || value.weekdays.length === 0 ||
73
- value.weekdays.some(day => !Number.isInteger(day) || day < 0 || day > 6) ||
74
- new Set(value.weekdays).size !== value.weekdays.length) {
87
+ if (
88
+ !Array.isArray(value.weekdays) ||
89
+ value.weekdays.length === 0 ||
90
+ value.weekdays.some((day) => !Number.isInteger(day) || day < 0 || day > 6) ||
91
+ new Set(value.weekdays).size !== value.weekdays.length
92
+ ) {
75
93
  throw new Error('weekdays 必须是非空、不重复的 0–6 整数数组(0 为周日)。');
76
94
  }
77
- if (typeof value.timezone !== 'string' || value.timezone.length === 0 || value.timezone.trim() !== value.timezone) {
95
+ if (
96
+ typeof value.timezone !== 'string' ||
97
+ value.timezone.length === 0 ||
98
+ value.timezone.trim() !== value.timezone
99
+ ) {
100
+ throw new Error('timezone 必须是有效时区名称。');
101
+ }
102
+ try {
103
+ new Intl.DateTimeFormat('en', { timeZone: value.timezone });
104
+ } catch {
78
105
  throw new Error('timezone 必须是有效时区名称。');
79
106
  }
80
- try { new Intl.DateTimeFormat('en', { timeZone: value.timezone }); }
81
- catch { throw new Error('timezone 必须是有效时区名称。'); }
82
107
  return { ...value, weekdays: [...value.weekdays] };
83
108
  }
84
109
 
@@ -89,18 +114,26 @@ export function createOperations(dataFileArg = dataFile) {
89
114
  let queue = Promise.resolve();
90
115
  async function read() {
91
116
  let text;
92
- try { text = await readFile(target, 'utf8'); }
93
- catch (error) {
94
- if (error.code === 'ENOENT') return { settings: validateSettings(DEFAULT_SETTINGS), configured: false };
117
+ try {
118
+ text = await readFile(target, 'utf8');
119
+ } catch (error) {
120
+ if (error.code === 'ENOENT')
121
+ return { settings: validateSettings(DEFAULT_SETTINGS), configured: false };
95
122
  throw new Error('无法读取本机配置。');
96
123
  }
97
124
  try {
98
125
  const stored = JSON.parse(text);
99
126
  validatePatch(stored);
100
127
  // Only the newly introduced fields are optional in legacy files.
101
- const settings = validateSettings({ payMode: DEFAULT_SETTINGS.payMode, dailySalary: DEFAULT_SETTINGS.dailySalary, ...stored });
128
+ const settings = validateSettings({
129
+ payMode: DEFAULT_SETTINGS.payMode,
130
+ dailySalary: DEFAULT_SETTINGS.dailySalary,
131
+ ...stored,
132
+ });
102
133
  return { settings, configured: true };
103
- } catch { throw new Error('本机配置损坏或无效;请检查插件目录 data.json。'); }
134
+ } catch {
135
+ throw new Error('本机配置损坏或无效;请检查插件目录 data.json。');
136
+ }
104
137
  }
105
138
  async function write(settings) {
106
139
  const temp = join(dirname(target), `.data-${randomUUID()}.tmp`);
@@ -125,74 +158,117 @@ export function createOperations(dataFileArg = dataFile) {
125
158
  const settings = validateSettings({ ...current.settings, ...patch });
126
159
  await write(settings);
127
160
  return { ok: true, settings, configured: true };
128
- } catch (error) { return fail(error); }
161
+ } catch (error) {
162
+ return fail(error);
163
+ }
129
164
  });
130
- queue = operation.then(() => {}, () => {});
165
+ queue = operation.then(
166
+ () => {},
167
+ () => {},
168
+ );
131
169
  return operation;
132
170
  };
133
171
  }
134
172
 
135
173
  const settingsSchema = {
136
- type: 'object', additionalProperties: false,
174
+ type: 'object',
175
+ additionalProperties: false,
137
176
  description: '要合并保存的配置字段。时间使用 HH:mm。',
138
177
  properties: {
139
- payMode: { type: 'string', enum: ['monthly', 'daily'], description: '计薪方式,默认 monthly(月薪);daily 为日薪。' },
178
+ payMode: {
179
+ type: 'string',
180
+ enum: ['monthly', 'daily'],
181
+ description: '计薪方式,默认 monthly(月薪);daily 为日薪。',
182
+ },
140
183
  dailySalary: { type: 'number', description: '日薪,有限数字且 >= 0,默认 400。' },
141
184
  monthlySalary: { type: 'number', description: '月薪,有限数字且 >= 0。' },
142
185
  workdaysPerMonth: { type: 'number', description: '每月工作日数,有限数字且 > 0。' },
143
186
  start: { type: 'string', description: '上班时间 HH:mm。' },
144
187
  end: { type: 'string', description: '下班时间 HH:mm;早于上班表示次日,不能与上班相同。' },
145
188
  breakStart: { type: 'string', description: '休息开始 HH:mm。' },
146
- breakEnd: { type: 'string', description: '休息结束 HH:mm;与开始相同即关闭,否则按班次顺序在班次内,且不能占满班次。' },
147
- weekdays: { type: 'array', items: { type: 'integer', enum: [0, 1, 2, 3, 4, 5, 6] }, description: '非空、不重复;0 为周日,1 为周一。' },
189
+ breakEnd: {
190
+ type: 'string',
191
+ description: '休息结束 HH:mm;与开始相同即关闭,否则按班次顺序在班次内,且不能占满班次。',
192
+ },
193
+ weekdays: {
194
+ type: 'array',
195
+ items: { type: 'integer', enum: [0, 1, 2, 3, 4, 5, 6] },
196
+ description: '非空、不重复;0 为周日,1 为周一。',
197
+ },
148
198
  timezone: { type: 'string', description: '有效时区,例如 Asia/Shanghai。' },
149
199
  },
150
200
  };
151
201
 
152
202
  export function apply(ctx) {
153
203
  const operate = createOperations();
154
- ctx.effect(() => ctx.commands.register({
155
- name: 'offwork-clock',
156
- description: '读取或保存本机下班时钟配置:get 或 set JSON。',
157
- input: { hint: 'get | set {"monthlySalary":10000}' },
158
- recordInput: false,
159
- async handler({ rawInput }) {
160
- let result;
161
- try {
162
- const match = /^(get|set)(?:\s+([\s\S]*))?$/.exec(rawInput.trim());
163
- if (!match) throw new Error('用法:/offwork-clock get 或 /offwork-clock set JSON');
164
- const [, action, json] = match;
165
- if (action === 'get' && json !== undefined) throw new Error('get 不接受参数。');
166
- if (action === 'set' && !json?.trim()) throw new Error('set 需要 JSON 配置对象。');
167
- result = await operate(action, action === 'set' ? JSON.parse(json) : undefined);
168
- } catch (error) { result = fail(error); }
169
- return { kind: result.ok ? 'success' : 'error', text: JSON.stringify(result) };
170
- },
171
- }));
172
- ctx.effect(() => ctx.tools.register({
173
- name: 'offwork_clock',
174
- description: '读取或更新本机下班时钟的工资与班次配置。get 返回配置及 configured(是否已有保存文件);set 合并保存。工资仅存本机,但调用及结果会进入会话记录。',
175
- parameters: {
176
- type: 'object', additionalProperties: false, required: ['action'],
177
- properties: {
178
- action: { type: 'string', enum: ['get', 'set'], description: 'get 读取;set 更新。' },
179
- settings: settingsSchema,
204
+ ctx.effect(() =>
205
+ ctx.commands.register({
206
+ name: 'offwork-clock',
207
+ description: '读取或保存本机下班时钟配置:get 或 set JSON。',
208
+ input: { hint: 'get | set {"monthlySalary":10000}' },
209
+ recordInput: false,
210
+ async handler({ rawInput }) {
211
+ let result;
212
+ try {
213
+ const match = /^(get|set)(?:\s+([\s\S]*))?$/.exec(rawInput.trim());
214
+ if (!match) throw new Error('用法:/offwork-clock get 或 /offwork-clock set JSON');
215
+ const [, action, json] = match;
216
+ if (action === 'get' && json !== undefined) throw new Error('get 不接受参数。');
217
+ if (action === 'set' && !json?.trim()) throw new Error('set 需要 JSON 配置对象。');
218
+ result = await operate(action, action === 'set' ? JSON.parse(json) : undefined);
219
+ } catch (error) {
220
+ result = fail(error);
221
+ }
222
+ return { kind: result.ok ? 'success' : 'error', text: JSON.stringify(result) };
180
223
  },
181
- },
182
- output: {
183
- schema: {
184
- oneOf: [
185
- { type: 'object', additionalProperties: false, required: ['ok', 'settings', 'configured'], properties: { ok: { type: 'boolean', const: true }, settings: { ...settingsSchema, required: keys }, configured: { type: 'boolean' } } },
186
- { type: 'object', additionalProperties: false, required: ['ok', 'error'], properties: { ok: { type: 'boolean', const: false }, error: { type: 'string' } } },
187
- ],
224
+ }),
225
+ );
226
+ ctx.effect(() =>
227
+ ctx.tools.register({
228
+ name: 'offwork_clock',
229
+ description:
230
+ '读取或更新下班时钟的工资与班次配置。get 返回配置及 configured(是否已有保存文件);set 合并保存。配置保存在运行 Harness 的机器上,调用及结果会进入会话记录。',
231
+ parameters: {
232
+ type: 'object',
233
+ additionalProperties: false,
234
+ required: ['action'],
235
+ properties: {
236
+ action: { type: 'string', enum: ['get', 'set'], description: 'get 读取;set 更新。' },
237
+ settings: settingsSchema,
238
+ },
188
239
  },
189
- render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
190
- },
191
- async execute(args) {
192
- if (!isObject(args) || Object.keys(args).some(key => key !== 'action' && key !== 'settings')) {
193
- return fail('参数必须是仅含 action 和可选 settings 的对象。');
194
- }
195
- return operate(args.action, args.settings);
196
- },
197
- }));
240
+ output: {
241
+ schema: {
242
+ oneOf: [
243
+ {
244
+ type: 'object',
245
+ additionalProperties: false,
246
+ required: ['ok', 'settings', 'configured'],
247
+ properties: {
248
+ ok: { type: 'boolean', const: true },
249
+ settings: { ...settingsSchema, required: keys },
250
+ configured: { type: 'boolean' },
251
+ },
252
+ },
253
+ {
254
+ type: 'object',
255
+ additionalProperties: false,
256
+ required: ['ok', 'error'],
257
+ properties: { ok: { type: 'boolean', const: false }, error: { type: 'string' } },
258
+ },
259
+ ],
260
+ },
261
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
262
+ },
263
+ async execute(args) {
264
+ if (
265
+ !isObject(args) ||
266
+ Object.keys(args).some((key) => key !== 'action' && key !== 'settings')
267
+ ) {
268
+ return fail('参数必须是仅含 action 和可选 settings 的对象。');
269
+ }
270
+ return operate(args.action, args.settings);
271
+ },
272
+ }),
273
+ );
198
274
  }
package/locale/en.json CHANGED
@@ -1 +1,6 @@
1
- {"meta":{"title":"Offwork Clock","description":"A draggable countdown to clock-out with live estimated earnings and salary settings."}}
1
+ {
2
+ "meta": {
3
+ "title": "Offwork Clock",
4
+ "description": "A draggable countdown to clock-out with live estimated earnings and salary settings."
5
+ }
6
+ }
package/locale/zh.json CHANGED
@@ -1 +1,6 @@
1
- {"meta":{"title":"下班计时器","description":"可拖动的下班倒计时与工资悬浮窗,支持月薪/日薪、夜班、工作日、可选休息扣除、金额隐藏与位置记忆。"}}
1
+ {
2
+ "meta": {
3
+ "title": "下班计时器",
4
+ "description": "可拖动的下班倒计时与工资悬浮窗,支持月薪/日薪、夜班、工作日、可选休息扣除、金额隐藏与位置记忆。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "dsh-offwork",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "下班倒计时与实时工资悬浮窗:可拖动、可调透明度、工资可隐藏。A draggable countdown and salary overlay for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "now",
8
- "repository": { "type": "git", "url": "git+https://github.com/SnowBeatRain/dsh-offwork.git" },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/SnowBeatRain/dsh-offwork.git"
11
+ },
9
12
  "homepage": "https://github.com/SnowBeatRain/dsh-offwork#readme",
10
- "bugs": { "url": "https://github.com/SnowBeatRain/dsh-offwork/issues" },
13
+ "bugs": {
14
+ "url": "https://github.com/SnowBeatRain/dsh-offwork/issues"
15
+ },
11
16
  "keywords": [
12
17
  "deepseek-harness",
13
18
  "dsh-plugin",
@@ -38,13 +43,18 @@
38
43
  "icon.svg",
39
44
  "LICENSE",
40
45
  "README.md",
46
+ "CHANGELOG.md",
41
47
  "tests/*.mjs",
42
- "tools/build.mjs"
48
+ "tools/*.mjs"
43
49
  ],
44
50
  "scripts": {
45
51
  "build": "node tools/build.mjs",
52
+ "format": "prettier --write . && npm run build",
53
+ "format:check": "prettier --check .",
46
54
  "test": "node tools/build.mjs --check && node --test tests/*.mjs",
47
- "prepublishOnly": "npm run build && npm test"
55
+ "check:package": "node tools/check-package.mjs",
56
+ "check": "npm run format:check && npm test && npm run check:package",
57
+ "prepublishOnly": "npm run build && npm run check"
48
58
  },
49
59
  "dsh": {
50
60
  "bundle": {
@@ -58,5 +68,8 @@
58
68
  "@deepseek-ai/dsh-api-remotes"
59
69
  ]
60
70
  }
71
+ },
72
+ "devDependencies": {
73
+ "prettier": "3.9.10"
61
74
  }
62
75
  }