devchain-cli 0.9.1 → 0.9.2

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 (61) hide show
  1. package/dist/drizzle/0042_nice_flatman.sql +10 -0
  2. package/dist/drizzle/0043_majestic_starhawk.sql +10 -0
  3. package/dist/drizzle/meta/0042_snapshot.json +4171 -0
  4. package/dist/drizzle/meta/0043_snapshot.json +4231 -0
  5. package/dist/drizzle/meta/_journal.json +15 -1
  6. package/dist/server/modules/skills/adapters/github-skill-source.base.d.ts +2 -5
  7. package/dist/server/modules/skills/adapters/github-skill-source.base.js +31 -100
  8. package/dist/server/modules/skills/adapters/github-skill-source.base.js.map +1 -1
  9. package/dist/server/modules/skills/adapters/local-skill-source.adapter.d.ts +16 -0
  10. package/dist/server/modules/skills/adapters/local-skill-source.adapter.js +265 -0
  11. package/dist/server/modules/skills/adapters/local-skill-source.adapter.js.map +1 -0
  12. package/dist/server/modules/skills/adapters/skill-parsing.utils.d.ts +32 -0
  13. package/dist/server/modules/skills/adapters/skill-parsing.utils.js +169 -0
  14. package/dist/server/modules/skills/adapters/skill-parsing.utils.js.map +1 -0
  15. package/dist/server/modules/skills/controllers/local-sources.controller.d.ts +12 -0
  16. package/dist/server/modules/skills/controllers/local-sources.controller.js +71 -0
  17. package/dist/server/modules/skills/controllers/local-sources.controller.js.map +1 -0
  18. package/dist/server/modules/skills/controllers/skills.controller.d.ts +11 -1
  19. package/dist/server/modules/skills/controllers/skills.controller.js +35 -3
  20. package/dist/server/modules/skills/controllers/skills.controller.js.map +1 -1
  21. package/dist/server/modules/skills/dtos/local-sources.dto.d.ts +42 -0
  22. package/dist/server/modules/skills/dtos/local-sources.dto.js +30 -0
  23. package/dist/server/modules/skills/dtos/local-sources.dto.js.map +1 -0
  24. package/dist/server/modules/skills/dtos/skill.dto.d.ts +16 -0
  25. package/dist/server/modules/skills/dtos/skill.dto.js +7 -1
  26. package/dist/server/modules/skills/dtos/skill.dto.js.map +1 -1
  27. package/dist/server/modules/skills/services/community-sources.service.d.ts +8 -1
  28. package/dist/server/modules/skills/services/community-sources.service.js +62 -3
  29. package/dist/server/modules/skills/services/community-sources.service.js.map +1 -1
  30. package/dist/server/modules/skills/services/local-sources.service.d.ts +20 -0
  31. package/dist/server/modules/skills/services/local-sources.service.js +206 -0
  32. package/dist/server/modules/skills/services/local-sources.service.js.map +1 -0
  33. package/dist/server/modules/skills/services/skill-source-registry.service.d.ts +11 -0
  34. package/dist/server/modules/skills/services/skill-source-registry.service.js +99 -3
  35. package/dist/server/modules/skills/services/skill-source-registry.service.js.map +1 -1
  36. package/dist/server/modules/skills/services/skill-sync.service.d.ts +4 -0
  37. package/dist/server/modules/skills/services/skill-sync.service.js +93 -0
  38. package/dist/server/modules/skills/services/skill-sync.service.js.map +1 -1
  39. package/dist/server/modules/skills/services/skills.service.d.ts +14 -2
  40. package/dist/server/modules/skills/services/skills.service.js +125 -23
  41. package/dist/server/modules/skills/services/skills.service.js.map +1 -1
  42. package/dist/server/modules/skills/skills.module.js +4 -1
  43. package/dist/server/modules/skills/skills.module.js.map +1 -1
  44. package/dist/server/modules/storage/db/schema.d.ts +202 -0
  45. package/dist/server/modules/storage/db/schema.js +19 -1
  46. package/dist/server/modules/storage/db/schema.js.map +1 -1
  47. package/dist/server/modules/storage/interfaces/storage.interface.d.ts +13 -1
  48. package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
  49. package/dist/server/modules/storage/local/local-storage.service.d.ts +18 -1
  50. package/dist/server/modules/storage/local/local-storage.service.js +294 -3
  51. package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
  52. package/dist/server/modules/storage/models/domain.models.d.ts +8 -0
  53. package/dist/server/tsconfig.tsbuildinfo +1 -1
  54. package/dist/server/ui/assets/{ReviewDetailPage-D13dH7Wh.js → ReviewDetailPage-BvSckWKj.js} +1 -1
  55. package/dist/server/ui/assets/{ReviewsPage-C98ST0lf.js → ReviewsPage-MKT-vv59.js} +1 -1
  56. package/dist/server/ui/assets/index-BtUq-Qxb.css +32 -0
  57. package/dist/server/ui/assets/{index-C8Dc1yQf.js → index-kTb634Zp.js} +194 -194
  58. package/dist/server/ui/assets/{useReviewSubscription-CmLuF45Z.js → useReviewSubscription-Dc58i6Bk.js} +1 -1
  59. package/dist/server/ui/index.html +2 -2
  60. package/package.json +5 -1
  61. package/dist/server/ui/assets/index-DZkJ40z9.css +0 -32
@@ -168,6 +168,46 @@ let LocalStorageService = class LocalStorageService {
168
168
  }
169
169
  return normalized;
170
170
  }
171
+ normalizeLocalSkillSourceFolderPath(folderPath) {
172
+ const normalized = folderPath.trim();
173
+ if (!normalized) {
174
+ throw new error_types_1.ValidationError('folderPath is required.', { fieldName: 'folderPath' });
175
+ }
176
+ return normalized;
177
+ }
178
+ async assertLocalSourceNameAvailableAcrossTypes(sourceName) {
179
+ if (RESERVED_COMMUNITY_SOURCE_NAMES.has(sourceName)) {
180
+ throw new error_types_1.ValidationError('Local source name conflicts with a built-in source.', {
181
+ name: sourceName,
182
+ });
183
+ }
184
+ const { communitySkillSources } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
185
+ const { eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
186
+ const existingCommunitySource = await this.db
187
+ .select({ id: communitySkillSources.id })
188
+ .from(communitySkillSources)
189
+ .where(eq(communitySkillSources.name, sourceName))
190
+ .limit(1);
191
+ if (existingCommunitySource.length > 0) {
192
+ throw new error_types_1.ConflictError('Local source name conflicts with an existing community source.', {
193
+ name: sourceName,
194
+ });
195
+ }
196
+ }
197
+ normalizeProjectIdForSourceEnablement(projectId) {
198
+ const normalized = projectId.trim();
199
+ if (!normalized) {
200
+ throw new error_types_1.ValidationError('projectId is required.', { fieldName: 'projectId' });
201
+ }
202
+ return normalized;
203
+ }
204
+ normalizeSourceNameForSourceEnablement(sourceName) {
205
+ const normalized = sourceName.trim().toLowerCase();
206
+ if (!normalized) {
207
+ throw new error_types_1.ValidationError('sourceName is required.', { fieldName: 'sourceName' });
208
+ }
209
+ return normalized;
210
+ }
171
211
  isSqliteUniqueConstraint(error) {
172
212
  if (typeof error !== 'object' || error === null) {
173
213
  return false;
@@ -246,6 +286,39 @@ let LocalStorageService = class LocalStorageService {
246
286
  });
247
287
  }
248
288
  }
289
+ async listSeedableSourceNamesForNewProject() {
290
+ const { communitySkillSources } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
291
+ const communitySourceRows = await this.db
292
+ .select({ name: communitySkillSources.name })
293
+ .from(communitySkillSources);
294
+ const sourceNames = communitySourceRows
295
+ .map((row) => row.name.trim().toLowerCase())
296
+ .filter((name) => name.length > 0);
297
+ const sqlite = (0, sqlite_raw_1.getRawSqliteClient)(this.db);
298
+ if (sqlite && typeof sqlite.prepare === 'function') {
299
+ try {
300
+ const localRows = sqlite.prepare('SELECT name FROM local_skill_sources').all();
301
+ for (const row of localRows) {
302
+ if (typeof row.name !== 'string') {
303
+ continue;
304
+ }
305
+ const normalized = row.name.trim().toLowerCase();
306
+ if (normalized.length > 0) {
307
+ sourceNames.push(normalized);
308
+ }
309
+ }
310
+ }
311
+ catch (error) {
312
+ const message = error instanceof Error ? error.message : String(error);
313
+ if (!message.includes('no such table: local_skill_sources')) {
314
+ throw new error_types_1.StorageError('Failed to list local skill sources for project source seeding.', {
315
+ cause: message,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ return [...new Set(sourceNames)];
321
+ }
249
322
  async createProject(data) {
250
323
  const { randomUUID } = await Promise.resolve().then(() => __importStar(require('crypto')));
251
324
  const now = new Date().toISOString();
@@ -256,7 +329,8 @@ let LocalStorageService = class LocalStorageService {
256
329
  createdAt: now,
257
330
  updatedAt: now,
258
331
  };
259
- const { projects, statuses } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
332
+ const seedableSourceNames = await this.listSeedableSourceNamesForNewProject();
333
+ const { projects, statuses, sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
260
334
  await this.db.transaction(async (tx) => {
261
335
  await tx.insert(projects).values({
262
336
  id: project.id,
@@ -285,6 +359,15 @@ let LocalStorageService = class LocalStorageService {
285
359
  updatedAt: now,
286
360
  });
287
361
  }
362
+ if (seedableSourceNames.length > 0) {
363
+ await tx.insert(sourceProjectEnabled).values(seedableSourceNames.map((sourceName) => ({
364
+ id: randomUUID(),
365
+ projectId: project.id,
366
+ sourceName,
367
+ enabled: false,
368
+ createdAt: now,
369
+ })));
370
+ }
288
371
  });
289
372
  logger.info({ projectId: project.id }, 'Created project with default statuses (transactional)');
290
373
  return project;
@@ -299,7 +382,8 @@ let LocalStorageService = class LocalStorageService {
299
382
  createdAt: now,
300
383
  updatedAt: now,
301
384
  };
302
- const { projects, statuses, prompts, agentProfiles, agents, tags, promptTags, profileProviderConfigs, providers, } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
385
+ const seedableSourceNames = await this.listSeedableSourceNamesForNewProject();
386
+ const { projects, statuses, prompts, agentProfiles, agents, tags, promptTags, profileProviderConfigs, providers, sourceProjectEnabled, } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
303
387
  const statusIdMap = {};
304
388
  const promptIdMap = {};
305
389
  const profileIdMap = {};
@@ -336,6 +420,15 @@ let LocalStorageService = class LocalStorageService {
336
420
  if (s.id)
337
421
  statusIdMap[s.id] = statusId;
338
422
  }
423
+ if (seedableSourceNames.length > 0) {
424
+ await this.db.insert(sourceProjectEnabled).values(seedableSourceNames.map((sourceName) => ({
425
+ id: randomUUID(),
426
+ projectId: project.id,
427
+ sourceName,
428
+ enabled: false,
429
+ createdAt: now,
430
+ })));
431
+ }
339
432
  const { eq, and, or, isNull } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
340
433
  for (const p of template.prompts) {
341
434
  const promptId = randomUUID();
@@ -1960,6 +2053,114 @@ let LocalStorageService = class LocalStorageService {
1960
2053
  }
1961
2054
  return this.updateProvider(id, update);
1962
2055
  }
2056
+ async getSourceProjectEnabled(projectId, sourceName) {
2057
+ const normalizedProjectId = this.normalizeProjectIdForSourceEnablement(projectId);
2058
+ const normalizedSourceName = this.normalizeSourceNameForSourceEnablement(sourceName);
2059
+ const { sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2060
+ const { and, eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2061
+ const rows = await this.db
2062
+ .select({ enabled: sourceProjectEnabled.enabled })
2063
+ .from(sourceProjectEnabled)
2064
+ .where(and(eq(sourceProjectEnabled.projectId, normalizedProjectId), eq(sourceProjectEnabled.sourceName, normalizedSourceName)))
2065
+ .limit(1);
2066
+ return rows[0] ? Boolean(rows[0].enabled) : null;
2067
+ }
2068
+ async setSourceProjectEnabled(projectId, sourceName, enabled) {
2069
+ const normalizedProjectId = this.normalizeProjectIdForSourceEnablement(projectId);
2070
+ const normalizedSourceName = this.normalizeSourceNameForSourceEnablement(sourceName);
2071
+ const { sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2072
+ const { and, eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2073
+ const existing = await this.db
2074
+ .select({ id: sourceProjectEnabled.id })
2075
+ .from(sourceProjectEnabled)
2076
+ .where(and(eq(sourceProjectEnabled.projectId, normalizedProjectId), eq(sourceProjectEnabled.sourceName, normalizedSourceName)))
2077
+ .limit(1);
2078
+ if (existing[0]) {
2079
+ await this.db
2080
+ .update(sourceProjectEnabled)
2081
+ .set({ enabled })
2082
+ .where(eq(sourceProjectEnabled.id, existing[0].id));
2083
+ return;
2084
+ }
2085
+ const { randomUUID } = await Promise.resolve().then(() => __importStar(require('crypto')));
2086
+ await this.db.insert(sourceProjectEnabled).values({
2087
+ id: randomUUID(),
2088
+ projectId: normalizedProjectId,
2089
+ sourceName: normalizedSourceName,
2090
+ enabled,
2091
+ createdAt: new Date().toISOString(),
2092
+ });
2093
+ }
2094
+ async listSourceProjectEnabled(projectId) {
2095
+ const normalizedProjectId = this.normalizeProjectIdForSourceEnablement(projectId);
2096
+ const { sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2097
+ const { asc, eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2098
+ const rows = await this.db
2099
+ .select({
2100
+ sourceName: sourceProjectEnabled.sourceName,
2101
+ enabled: sourceProjectEnabled.enabled,
2102
+ })
2103
+ .from(sourceProjectEnabled)
2104
+ .where(eq(sourceProjectEnabled.projectId, normalizedProjectId))
2105
+ .orderBy(asc(sourceProjectEnabled.sourceName));
2106
+ return rows.map((row) => ({
2107
+ sourceName: row.sourceName,
2108
+ enabled: Boolean(row.enabled),
2109
+ }));
2110
+ }
2111
+ async seedSourceProjectDisabled(projectId, sourceNames) {
2112
+ const normalizedProjectId = this.normalizeProjectIdForSourceEnablement(projectId);
2113
+ const normalizedSourceNames = [
2114
+ ...new Set(sourceNames.map((name) => name.trim().toLowerCase())),
2115
+ ].filter((name) => name.length > 0);
2116
+ if (normalizedSourceNames.length === 0) {
2117
+ return;
2118
+ }
2119
+ const sqlite = (0, sqlite_raw_1.getRawSqliteClient)(this.db);
2120
+ if (!sqlite || typeof sqlite.exec !== 'function') {
2121
+ throw new error_types_1.StorageError('Unable to access underlying SQLite client');
2122
+ }
2123
+ sqlite.exec('BEGIN IMMEDIATE TRANSACTION');
2124
+ try {
2125
+ const { sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2126
+ const { and, eq, inArray } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2127
+ const existingRows = await this.db
2128
+ .select({ sourceName: sourceProjectEnabled.sourceName })
2129
+ .from(sourceProjectEnabled)
2130
+ .where(and(eq(sourceProjectEnabled.projectId, normalizedProjectId), inArray(sourceProjectEnabled.sourceName, normalizedSourceNames)));
2131
+ const existingSourceNames = new Set(existingRows.map((row) => row.sourceName));
2132
+ const sourceNamesToInsert = normalizedSourceNames.filter((sourceName) => !existingSourceNames.has(sourceName));
2133
+ if (sourceNamesToInsert.length > 0) {
2134
+ const { randomUUID } = await Promise.resolve().then(() => __importStar(require('crypto')));
2135
+ const now = new Date().toISOString();
2136
+ await this.db.insert(sourceProjectEnabled).values(sourceNamesToInsert.map((sourceName) => ({
2137
+ id: randomUUID(),
2138
+ projectId: normalizedProjectId,
2139
+ sourceName,
2140
+ enabled: false,
2141
+ createdAt: now,
2142
+ })));
2143
+ }
2144
+ sqlite.exec('COMMIT');
2145
+ }
2146
+ catch (error) {
2147
+ try {
2148
+ sqlite.exec('ROLLBACK');
2149
+ }
2150
+ catch (rollbackError) {
2151
+ logger.error({ rollbackError }, 'Failed to rollback seedSourceProjectDisabled transaction');
2152
+ }
2153
+ throw error;
2154
+ }
2155
+ }
2156
+ async deleteSourceProjectEnabledBySource(sourceName) {
2157
+ const normalizedSourceName = this.normalizeSourceNameForSourceEnablement(sourceName);
2158
+ const { sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2159
+ const { eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2160
+ await this.db
2161
+ .delete(sourceProjectEnabled)
2162
+ .where(eq(sourceProjectEnabled.sourceName, normalizedSourceName));
2163
+ }
1963
2164
  async listCommunitySkillSources() {
1964
2165
  const { communitySkillSources } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
1965
2166
  const { asc } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
@@ -2052,14 +2253,104 @@ let LocalStorageService = class LocalStorageService {
2052
2253
  }
2053
2254
  async deleteCommunitySkillSource(id) {
2054
2255
  const source = await this.getCommunitySkillSource(id);
2055
- const { communitySkillSources, skills } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2256
+ const { communitySkillSources, skills, sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2056
2257
  const { eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2057
2258
  await this.db.transaction(async (tx) => {
2058
2259
  await tx.delete(skills).where(eq(skills.source, source.name));
2260
+ await tx.delete(sourceProjectEnabled).where(eq(sourceProjectEnabled.sourceName, source.name));
2059
2261
  await tx.delete(communitySkillSources).where(eq(communitySkillSources.id, source.id));
2060
2262
  });
2061
2263
  logger.info({ communitySkillSourceId: source.id, sourceName: source.name }, 'Deleted community skill source and related skills');
2062
2264
  }
2265
+ async listLocalSkillSources() {
2266
+ const { localSkillSources } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2267
+ const { asc } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2268
+ const rows = await this.db
2269
+ .select()
2270
+ .from(localSkillSources)
2271
+ .orderBy(asc(localSkillSources.name));
2272
+ return rows;
2273
+ }
2274
+ async getLocalSkillSource(id) {
2275
+ const normalizedId = id.trim();
2276
+ if (!normalizedId) {
2277
+ throw new error_types_1.ValidationError('id is required.', { fieldName: 'id' });
2278
+ }
2279
+ const { localSkillSources } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2280
+ const { eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2281
+ const rows = await this.db
2282
+ .select()
2283
+ .from(localSkillSources)
2284
+ .where(eq(localSkillSources.id, normalizedId))
2285
+ .limit(1);
2286
+ return rows[0] ?? null;
2287
+ }
2288
+ async createLocalSkillSource(data) {
2289
+ const normalizedName = this.normalizeCommunitySourceNameForLookup(data.name);
2290
+ const normalizedFolderPath = this.normalizeLocalSkillSourceFolderPath(data.folderPath);
2291
+ await this.assertLocalSourceNameAvailableAcrossTypes(normalizedName);
2292
+ const { randomUUID } = await Promise.resolve().then(() => __importStar(require('crypto')));
2293
+ const now = new Date().toISOString();
2294
+ const record = {
2295
+ id: randomUUID(),
2296
+ name: normalizedName,
2297
+ folderPath: normalizedFolderPath,
2298
+ createdAt: now,
2299
+ updatedAt: now,
2300
+ };
2301
+ const { localSkillSources } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2302
+ try {
2303
+ await this.db.insert(localSkillSources).values({
2304
+ id: record.id,
2305
+ name: record.name,
2306
+ folderPath: record.folderPath,
2307
+ createdAt: record.createdAt,
2308
+ updatedAt: record.updatedAt,
2309
+ });
2310
+ }
2311
+ catch (error) {
2312
+ if (this.isSqliteUniqueConstraint(error)) {
2313
+ const rawMessage = typeof error === 'object' && error !== null && 'message' in error
2314
+ ? String(error.message ?? '')
2315
+ : '';
2316
+ if (rawMessage.includes('local_skill_sources.name')) {
2317
+ throw new error_types_1.ConflictError('Local skill source name already exists.', {
2318
+ name: normalizedName,
2319
+ });
2320
+ }
2321
+ if (rawMessage.includes('local_skill_sources.folder_path')) {
2322
+ throw new error_types_1.ConflictError('Local skill source folder path already exists.', {
2323
+ folderPath: normalizedFolderPath,
2324
+ });
2325
+ }
2326
+ throw new error_types_1.ConflictError('Local skill source already exists.', {
2327
+ name: normalizedName,
2328
+ folderPath: normalizedFolderPath,
2329
+ });
2330
+ }
2331
+ throw new error_types_1.StorageError('Failed to create local skill source.', {
2332
+ name: normalizedName,
2333
+ folderPath: normalizedFolderPath,
2334
+ cause: error instanceof Error ? error.message : String(error),
2335
+ });
2336
+ }
2337
+ logger.info({ localSkillSourceId: record.id, name: record.name }, 'Created local skill source');
2338
+ return record;
2339
+ }
2340
+ async deleteLocalSkillSource(id) {
2341
+ const source = await this.getLocalSkillSource(id);
2342
+ if (!source) {
2343
+ throw new error_types_1.NotFoundError('Local skill source', id.trim());
2344
+ }
2345
+ const { localSkillSources, skills, sourceProjectEnabled } = await Promise.resolve().then(() => __importStar(require('../db/schema')));
2346
+ const { eq } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
2347
+ await this.db.transaction(async (tx) => {
2348
+ await tx.delete(skills).where(eq(skills.source, source.name));
2349
+ await tx.delete(sourceProjectEnabled).where(eq(sourceProjectEnabled.sourceName, source.name));
2350
+ await tx.delete(localSkillSources).where(eq(localSkillSources.id, source.id));
2351
+ });
2352
+ logger.info({ localSkillSourceId: source.id, sourceName: source.name }, 'Deleted local skill source and related skills');
2353
+ }
2063
2354
  async createAgentProfile(data) {
2064
2355
  const { randomUUID } = await Promise.resolve().then(() => __importStar(require('crypto')));
2065
2356
  const now = new Date().toISOString();