@agentteams/cli 0.1.56 → 0.1.58-dev.224

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 (127) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/README.md +7 -7
  4. package/dist/api/coaction.d.ts.map +1 -1
  5. package/dist/api/coaction.js +3 -9
  6. package/dist/api/coaction.js.map +1 -1
  7. package/dist/api/codeReview.d.ts.map +1 -1
  8. package/dist/api/codeReview.js +1 -3
  9. package/dist/api/codeReview.js.map +1 -1
  10. package/dist/api/comment.d.ts.map +1 -1
  11. package/dist/api/comment.js +1 -3
  12. package/dist/api/comment.js.map +1 -1
  13. package/dist/api/document.d.ts.map +1 -1
  14. package/dist/api/document.js +8 -4
  15. package/dist/api/document.js.map +1 -1
  16. package/dist/api/feedback.d.ts +1 -1
  17. package/dist/api/feedback.js +1 -1
  18. package/dist/api/linear.d.ts.map +1 -1
  19. package/dist/api/linear.js.map +1 -1
  20. package/dist/api/member.d.ts +1 -1
  21. package/dist/api/plan.d.ts.map +1 -1
  22. package/dist/api/plan.js +1 -3
  23. package/dist/api/plan.js.map +1 -1
  24. package/dist/api/postmortem.d.ts.map +1 -1
  25. package/dist/api/postmortem.js +1 -3
  26. package/dist/api/postmortem.js.map +1 -1
  27. package/dist/api/report.d.ts.map +1 -1
  28. package/dist/api/report.js +1 -3
  29. package/dist/api/report.js.map +1 -1
  30. package/dist/commands/agentConfig.d.ts.map +1 -1
  31. package/dist/commands/agentConfig.js +9 -3
  32. package/dist/commands/agentConfig.js.map +1 -1
  33. package/dist/commands/agentConfigCommand.d.ts.map +1 -1
  34. package/dist/commands/agentConfigCommand.js.map +1 -1
  35. package/dist/commands/attachment.d.ts.map +1 -1
  36. package/dist/commands/attachment.js +31 -31
  37. package/dist/commands/attachment.js.map +1 -1
  38. package/dist/commands/coaction.d.ts.map +1 -1
  39. package/dist/commands/coaction.js +11 -9
  40. package/dist/commands/coaction.js.map +1 -1
  41. package/dist/commands/codeReview.d.ts.map +1 -1
  42. package/dist/commands/codeReview.js +5 -2
  43. package/dist/commands/codeReview.js.map +1 -1
  44. package/dist/commands/convention.d.ts +20 -2
  45. package/dist/commands/convention.d.ts.map +1 -1
  46. package/dist/commands/convention.js +152 -107
  47. package/dist/commands/convention.js.map +1 -1
  48. package/dist/commands/conventionRouter.d.ts.map +1 -1
  49. package/dist/commands/conventionRouter.js +4 -2
  50. package/dist/commands/conventionRouter.js.map +1 -1
  51. package/dist/commands/dependency.d.ts.map +1 -1
  52. package/dist/commands/dependency.js +3 -1
  53. package/dist/commands/dependency.js.map +1 -1
  54. package/dist/commands/dependencyCommand.d.ts.map +1 -1
  55. package/dist/commands/dependencyCommand.js.map +1 -1
  56. package/dist/commands/document.d.ts.map +1 -1
  57. package/dist/commands/document.js +79 -83
  58. package/dist/commands/document.js.map +1 -1
  59. package/dist/commands/feedback.js +13 -13
  60. package/dist/commands/index.d.ts.map +1 -1
  61. package/dist/commands/index.js +13 -14
  62. package/dist/commands/index.js.map +1 -1
  63. package/dist/commands/init.d.ts.map +1 -1
  64. package/dist/commands/init.js +7 -8
  65. package/dist/commands/init.js.map +1 -1
  66. package/dist/commands/linear.d.ts.map +1 -1
  67. package/dist/commands/linear.js +1 -1
  68. package/dist/commands/linear.js.map +1 -1
  69. package/dist/commands/plan.d.ts +1 -0
  70. package/dist/commands/plan.d.ts.map +1 -1
  71. package/dist/commands/plan.js +104 -100
  72. package/dist/commands/plan.js.map +1 -1
  73. package/dist/commands/postmortem.d.ts.map +1 -1
  74. package/dist/commands/postmortem.js +6 -4
  75. package/dist/commands/postmortem.js.map +1 -1
  76. package/dist/commands/report.d.ts.map +1 -1
  77. package/dist/commands/report.js +12 -58
  78. package/dist/commands/report.js.map +1 -1
  79. package/dist/commands/search.d.ts.map +1 -1
  80. package/dist/commands/search.js.map +1 -1
  81. package/dist/index.js +6 -5
  82. package/dist/index.js.map +1 -1
  83. package/dist/types/index.d.ts +1 -1
  84. package/dist/utils/atomicWrite.d.ts.map +1 -1
  85. package/dist/utils/atomicWrite.js +2 -2
  86. package/dist/utils/atomicWrite.js.map +1 -1
  87. package/dist/utils/authServer.d.ts.map +1 -1
  88. package/dist/utils/authServer.js +2 -5
  89. package/dist/utils/authServer.js.map +1 -1
  90. package/dist/utils/config.d.ts +1 -1
  91. package/dist/utils/config.d.ts.map +1 -1
  92. package/dist/utils/config.js +9 -15
  93. package/dist/utils/config.js.map +1 -1
  94. package/dist/utils/git.d.ts.map +1 -1
  95. package/dist/utils/git.js +1 -3
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/httpClient.d.ts +1 -1
  98. package/dist/utils/httpClient.d.ts.map +1 -1
  99. package/dist/utils/httpClient.js +7 -7
  100. package/dist/utils/httpClient.js.map +1 -1
  101. package/dist/utils/initOutput.d.ts.map +1 -1
  102. package/dist/utils/initOutput.js.map +1 -1
  103. package/dist/utils/outputPolicy.d.ts.map +1 -1
  104. package/dist/utils/outputPolicy.js +1 -3
  105. package/dist/utils/outputPolicy.js.map +1 -1
  106. package/dist/utils/parsers.d.ts +11 -3
  107. package/dist/utils/parsers.d.ts.map +1 -1
  108. package/dist/utils/parsers.js +34 -17
  109. package/dist/utils/parsers.js.map +1 -1
  110. package/dist/utils/planFormat.d.ts.map +1 -1
  111. package/dist/utils/planFormat.js +1 -1
  112. package/dist/utils/planFormat.js.map +1 -1
  113. package/dist/utils/planPreviewHtmlSafety.d.ts.map +1 -1
  114. package/dist/utils/planPreviewHtmlSafety.js +15 -15
  115. package/dist/utils/planPreviewHtmlSafety.js.map +1 -1
  116. package/dist/utils/report.d.ts +22 -0
  117. package/dist/utils/report.d.ts.map +1 -0
  118. package/dist/utils/report.js +79 -0
  119. package/dist/utils/report.js.map +1 -0
  120. package/dist/utils/spinner.d.ts.map +1 -1
  121. package/dist/utils/spinner.js.map +1 -1
  122. package/dist/utils/updateCheck.d.ts.map +1 -1
  123. package/dist/utils/updateCheck.js +11 -3
  124. package/dist/utils/updateCheck.js.map +1 -1
  125. package/eslint.config.mjs +111 -0
  126. package/package.json +9 -2
  127. package/tsconfig.test.json +10 -0
@@ -1,26 +1,26 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
2
- import { atomicWriteFileSync } from "../utils/atomicWrite.js";
3
- import { basename, join, relative, resolve, sep } from "node:path";
4
- import httpClient from "../utils/httpClient.js";
5
- import { isAxiosError } from "axios";
6
- import matter from "gray-matter";
7
- import { diffLines, createTwoFilesPatch } from "diff";
8
- import { loadConfig, findProjectConfig } from "../utils/config.js";
9
- import { withSpinner } from "../utils/spinner.js";
10
- import { withoutJsonContentType } from "../utils/httpHeaders.js";
11
- const CONVENTION_DIR = ".agentteams";
12
- const LEGACY_CONVENTION_DOWNLOAD_DIR = "conventions";
13
- const CONVENTION_INDEX_FILE = "convention.md";
14
- const CONVENTION_MANIFEST_FILE = "conventions.manifest.json";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from 'node:fs';
2
+ import { atomicWriteFileSync } from '../utils/atomicWrite.js';
3
+ import { basename, join, relative, resolve, sep } from 'node:path';
4
+ import httpClient from '../utils/httpClient.js';
5
+ import { isAxiosError } from 'axios';
6
+ import matter from 'gray-matter';
7
+ import { diffLines, createTwoFilesPatch } from 'diff';
8
+ import { loadConfig, findProjectConfig } from '../utils/config.js';
9
+ import { withSpinner } from '../utils/spinner.js';
10
+ import { withoutJsonContentType } from '../utils/httpHeaders.js';
11
+ const CONVENTION_DIR = '.agentteams';
12
+ const LEGACY_CONVENTION_DOWNLOAD_DIR = 'conventions';
13
+ const CONVENTION_INDEX_FILE = 'convention.md';
14
+ const CONVENTION_MANIFEST_FILE = 'conventions.manifest.json';
15
15
  function findProjectRoot(cwd) {
16
16
  const configPath = findProjectConfig(cwd ?? process.cwd());
17
17
  if (!configPath)
18
18
  return null;
19
19
  // configPath = /path/.agentteams/config.json → resolve up 2 levels to project root
20
- return resolve(configPath, "..", "..");
20
+ return resolve(configPath, '..', '..');
21
21
  }
22
22
  function getApiBaseUrl(apiUrl) {
23
- return apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
23
+ return apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
24
24
  }
25
25
  function getApiConfigOrThrow(options) {
26
26
  const config = options?.config ?? loadConfig();
@@ -31,13 +31,13 @@ function getApiConfigOrThrow(options) {
31
31
  config,
32
32
  apiUrl: getApiBaseUrl(config.apiUrl),
33
33
  headers: {
34
- "X-API-Key": config.apiKey,
35
- "Content-Type": "application/json",
34
+ 'X-API-Key': config.apiKey,
35
+ 'Content-Type': 'application/json',
36
36
  },
37
37
  };
38
38
  }
39
39
  function normalizeRelativePath(input) {
40
- return input.replaceAll("\\", "/");
40
+ return input.replaceAll('\\', '/');
41
41
  }
42
42
  function resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput) {
43
43
  // If absolute path, keep as-is.
@@ -59,7 +59,7 @@ function resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput) {
59
59
  function validatePathBoundary(absolutePath, projectRoot) {
60
60
  const normalized = resolve(absolutePath);
61
61
  if (!normalized.startsWith(resolve(projectRoot) + sep)) {
62
- throw new Error("Path traversal detected: file must be within project root");
62
+ throw new Error('Path traversal detected: file must be within project root');
63
63
  }
64
64
  return normalized;
65
65
  }
@@ -71,7 +71,7 @@ function loadManifestOrThrow(projectRoot) {
71
71
  if (!existsSync(manifestPath)) {
72
72
  throw new Error(`Download manifest not found: ${manifestPath}\nRun 'agentteams convention download' first.`);
73
73
  }
74
- const raw = readFileSync(manifestPath, "utf-8");
74
+ const raw = readFileSync(manifestPath, 'utf-8');
75
75
  const parsed = JSON.parse(raw);
76
76
  if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
77
77
  throw new Error(`Invalid manifest format: ${manifestPath}`);
@@ -87,7 +87,7 @@ function loadManifestOrCreate(projectRoot) {
87
87
  entries: [],
88
88
  };
89
89
  }
90
- const raw = readFileSync(manifestPath, "utf-8");
90
+ const raw = readFileSync(manifestPath, 'utf-8');
91
91
  const parsed = JSON.parse(raw);
92
92
  if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
93
93
  throw new Error(`Invalid manifest format: ${manifestPath}`);
@@ -96,7 +96,7 @@ function loadManifestOrCreate(projectRoot) {
96
96
  }
97
97
  function writeManifest(projectRoot, manifest) {
98
98
  const manifestPath = buildManifestPath(projectRoot);
99
- atomicWriteFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
99
+ atomicWriteFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
100
100
  }
101
101
  function toFileList(input) {
102
102
  return Array.isArray(input) ? input : [input];
@@ -106,21 +106,19 @@ function hasAnyDiff(a, b) {
106
106
  return parts.some((p) => p.added || p.removed);
107
107
  }
108
108
  function createUnifiedDiff(fileLabel, serverText, localText) {
109
- return createTwoFilesPatch(`${fileLabel} (server)`, `${fileLabel} (local)`, serverText, localText, "", "", { context: 3 });
109
+ return createTwoFilesPatch(`${fileLabel} (server)`, `${fileLabel} (local)`, serverText, localText, '', '', {
110
+ context: 3,
111
+ });
110
112
  }
111
113
  function toOptionalString(value) {
112
- return typeof value === "string" ? value : undefined;
114
+ return typeof value === 'string' ? value : undefined;
113
115
  }
114
116
  function fileNameToTitle(fileName) {
115
- return fileName
116
- .replace(/\.md$/i, "")
117
- .replace(/[-_]+/g, " ")
118
- .replace(/\s+/g, " ")
119
- .trim();
117
+ return fileName.replace(/\.md$/i, '').replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
120
118
  }
121
119
  function parseCategoryFromAgentteamsPath(fileRelativePath) {
122
120
  const normalized = normalizeRelativePath(fileRelativePath);
123
- const parts = normalized.split("/");
121
+ const parts = normalized.split('/');
124
122
  const agentteamsIndex = parts.indexOf(CONVENTION_DIR);
125
123
  if (agentteamsIndex === -1) {
126
124
  throw new Error(`Convention create requires a file under ${CONVENTION_DIR}/<category>/: ${fileRelativePath}`);
@@ -129,7 +127,7 @@ function parseCategoryFromAgentteamsPath(fileRelativePath) {
129
127
  if (!category || category.length === 0) {
130
128
  throw new Error(`Convention create requires a category directory under ${CONVENTION_DIR}/: ${fileRelativePath}`);
131
129
  }
132
- if (category === "platform" || category === "active-plan") {
130
+ if (category === 'platform' || category === 'active-plan') {
133
131
  throw new Error(`Convention create does not allow reserved directories under ${CONVENTION_DIR}/: ${category}`);
134
132
  }
135
133
  return category;
@@ -140,14 +138,17 @@ async function fetchAllConventions(apiUrl, projectId, headers) {
140
138
  let totalPages;
141
139
  const items = [];
142
140
  while (true) {
143
- const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions`, { headers, params: { page, pageSize } });
141
+ const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions`, {
142
+ headers,
143
+ params: { page, pageSize },
144
+ });
144
145
  const data = response.data?.data;
145
146
  if (!Array.isArray(data)) {
146
147
  break;
147
148
  }
148
149
  items.push(...data);
149
150
  const meta = response.data?.meta;
150
- if (typeof meta?.totalPages === "number") {
151
+ if (typeof meta?.totalPages === 'number') {
151
152
  totalPages = meta.totalPages;
152
153
  }
153
154
  if (totalPages !== undefined) {
@@ -167,32 +168,32 @@ async function fetchConventionsWithContent(apiUrl, projectId, headers) {
167
168
  const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions/download-all`, { headers });
168
169
  const data = response.data?.data;
169
170
  if (!Array.isArray(data)) {
170
- throw new Error("Invalid download-all response format");
171
+ throw new Error('Invalid download-all response format');
171
172
  }
172
173
  return data;
173
174
  }
174
175
  async function fetchPlatformGuidesHash(apiUrl, headers) {
175
176
  const response = await httpClient.get(`${apiUrl}/api/platform/guides/hash`, { headers });
176
177
  const hash = response.data?.data?.hash;
177
- if (typeof hash !== "string" || hash.length === 0) {
178
- throw new Error("Invalid platform guides hash response format");
178
+ if (typeof hash !== 'string' || hash.length === 0) {
179
+ throw new Error('Invalid platform guides hash response format');
179
180
  }
180
181
  return hash;
181
182
  }
182
183
  function toConventionName(convention) {
183
- const title = typeof convention.title === "string" ? convention.title.trim() : "";
184
+ const title = typeof convention.title === 'string' ? convention.title.trim() : '';
184
185
  if (title.length > 0)
185
186
  return title;
186
- const fileName = typeof convention.fileName === "string" ? convention.fileName.trim() : "";
187
+ const fileName = typeof convention.fileName === 'string' ? convention.fileName.trim() : '';
187
188
  if (fileName.length > 0)
188
189
  return fileName;
189
190
  return convention.id;
190
191
  }
191
192
  function toConventionNameFromManifest(entry) {
192
- const title = typeof entry.title === "string" ? entry.title.trim() : "";
193
+ const title = typeof entry.title === 'string' ? entry.title.trim() : '';
193
194
  if (title.length > 0)
194
195
  return title;
195
- const fileName = typeof entry.fileName === "string" ? entry.fileName.trim() : "";
196
+ const fileName = typeof entry.fileName === 'string' ? entry.fileName.trim() : '';
196
197
  if (fileName.length > 0)
197
198
  return fileName;
198
199
  return entry.conventionId;
@@ -205,7 +206,7 @@ function toOptionalStringOrNullIfPresent(data, key) {
205
206
  if (value === null) {
206
207
  return null;
207
208
  }
208
- if (typeof value === "string") {
209
+ if (typeof value === 'string') {
209
210
  const trimmed = value.trim();
210
211
  return trimmed.length > 0 ? trimmed : null;
211
212
  }
@@ -215,15 +216,15 @@ export async function conventionShow() {
215
216
  const { config, apiUrl, headers } = getApiConfigOrThrow();
216
217
  const conventions = await fetchConventionsWithContent(apiUrl, config.projectId, headers);
217
218
  if (!conventions || conventions.length === 0) {
218
- throw new Error("No conventions found for this project. Create one via the web dashboard first.");
219
+ throw new Error('No conventions found for this project. Create one via the web dashboard first.');
219
220
  }
220
221
  const sections = [];
221
222
  for (const convention of conventions) {
222
- const contentMarkdown = typeof convention.contentMarkdown === "string" ? convention.contentMarkdown : "";
223
- const sectionHeader = `# ${convention.title ?? "untitled"}\ncategory: ${convention.category ?? "uncategorized"}\nid: ${convention.id}`;
223
+ const contentMarkdown = typeof convention.contentMarkdown === 'string' ? convention.contentMarkdown : '';
224
+ const sectionHeader = `# ${convention.title ?? 'untitled'}\ncategory: ${convention.category ?? 'uncategorized'}\nid: ${convention.id}`;
224
225
  sections.push(`${sectionHeader}\n\n${contentMarkdown}`);
225
226
  }
226
- return sections.join("\n\n---\n\n");
227
+ return sections.join('\n\n---\n\n');
227
228
  }
228
229
  export async function checkConventionFreshness(apiUrl, projectId, headers, projectRoot) {
229
230
  const manifestPath = buildManifestPath(projectRoot);
@@ -235,9 +236,9 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
235
236
  }
236
237
  const manifest = loadManifestOrThrow(projectRoot);
237
238
  const currentPlatformGuidesHash = await fetchPlatformGuidesHash(apiUrl, headers);
238
- const platformGuidesChanged = typeof manifest.platformGuidesHash === "string"
239
- && manifest.platformGuidesHash.length > 0
240
- && manifest.platformGuidesHash !== currentPlatformGuidesHash;
239
+ const platformGuidesChanged = typeof manifest.platformGuidesHash === 'string' &&
240
+ manifest.platformGuidesHash.length > 0 &&
241
+ manifest.platformGuidesHash !== currentPlatformGuidesHash;
241
242
  const serverConventions = await fetchAllConventions(apiUrl, projectId, headers);
242
243
  const serverById = new Map(serverConventions.map((item) => [item.id, item]));
243
244
  const localById = new Map(manifest.entries.map((entry) => [entry.conventionId, entry]));
@@ -247,18 +248,18 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
247
248
  if (!local) {
248
249
  conventionChanges.push({
249
250
  id: serverConvention.id,
250
- type: "new",
251
+ type: 'new',
251
252
  title: toConventionName(serverConvention),
252
253
  fileName: serverConvention.fileName ?? undefined,
253
254
  });
254
255
  continue;
255
256
  }
256
- if (typeof serverConvention.updatedAt === "string"
257
- && typeof local.updatedAt === "string"
258
- && serverConvention.updatedAt !== local.updatedAt) {
257
+ if (typeof serverConvention.updatedAt === 'string' &&
258
+ typeof local.updatedAt === 'string' &&
259
+ serverConvention.updatedAt !== local.updatedAt) {
259
260
  conventionChanges.push({
260
261
  id: serverConvention.id,
261
- type: "updated",
262
+ type: 'updated',
262
263
  title: toConventionName(serverConvention),
263
264
  fileName: serverConvention.fileName ?? local.fileName,
264
265
  });
@@ -269,7 +270,7 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
269
270
  continue;
270
271
  conventionChanges.push({
271
272
  id: localEntry.conventionId,
272
- type: "deleted",
273
+ type: 'deleted',
273
274
  title: toConventionNameFromManifest(localEntry),
274
275
  fileName: localEntry.fileName,
275
276
  });
@@ -279,6 +280,56 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
279
280
  conventionChanges,
280
281
  };
281
282
  }
283
+ export function buildStatusSummary(result) {
284
+ const parts = [];
285
+ if (result.platformGuidesChanged)
286
+ parts.push('platform guides');
287
+ const counts = { new: 0, updated: 0, deleted: 0 };
288
+ for (const change of result.conventionChanges)
289
+ counts[change.type] += 1;
290
+ if (counts.new > 0)
291
+ parts.push(`${counts.new} new`);
292
+ if (counts.updated > 0)
293
+ parts.push(`${counts.updated} updated`);
294
+ if (counts.deleted > 0)
295
+ parts.push(`${counts.deleted} deleted`);
296
+ if (parts.length === 0)
297
+ return '✓ Conventions up to date';
298
+ return `⚠ Updates available (${parts.join(', ')}). Run 'agentteams convention download' to sync.`;
299
+ }
300
+ /**
301
+ * Read-only freshness check exposed as `agentteams convention status`.
302
+ *
303
+ * Compares the local download manifest against the server and reports whether an
304
+ * update is available — it never downloads or mutates anything. Degrades gracefully
305
+ * (exit 0, updateAvailable=false) when the project is not configured or has no local
306
+ * conventions yet, so callers can safely "check then skip when unavailable".
307
+ */
308
+ export async function conventionStatus(options) {
309
+ const config = options?.config ?? loadConfig();
310
+ const projectRoot = findProjectRoot(options?.cwd);
311
+ // Not configured or no local conventions → nothing to compare; treat as up to date.
312
+ if (!config || !projectRoot) {
313
+ return {
314
+ updateAvailable: false,
315
+ platformGuidesChanged: false,
316
+ conventionChanges: [],
317
+ summary: '✓ Conventions up to date',
318
+ };
319
+ }
320
+ const apiUrl = getApiBaseUrl(config.apiUrl);
321
+ const headers = {
322
+ 'X-API-Key': config.apiKey,
323
+ 'Content-Type': 'application/json',
324
+ };
325
+ const freshness = await checkConventionFreshness(apiUrl, config.projectId, headers, projectRoot);
326
+ return {
327
+ updateAvailable: freshness.platformGuidesChanged || freshness.conventionChanges.length > 0,
328
+ platformGuidesChanged: freshness.platformGuidesChanged,
329
+ conventionChanges: freshness.conventionChanges,
330
+ summary: buildStatusSummary(freshness),
331
+ };
332
+ }
282
333
  export async function conventionList() {
283
334
  const { config, apiUrl, headers } = getApiConfigOrThrow();
284
335
  const conventions = await fetchAllConventions(apiUrl, config.projectId, headers);
@@ -299,33 +350,31 @@ export async function conventionList() {
299
350
  page: 1,
300
351
  pageSize: conventions.length,
301
352
  totalPages: 1,
302
- }
353
+ },
303
354
  };
304
355
  }
305
356
  function toSafeFileName(input) {
306
357
  return input
307
358
  .toLowerCase()
308
- .replace(/[^a-z0-9]+/g, "-")
309
- .replace(/^-+|-+$/g, "")
359
+ .replace(/[^a-z0-9]+/g, '-')
360
+ .replace(/^-+|-+$/g, '')
310
361
  .slice(0, 60);
311
362
  }
312
363
  function toSafeDirectoryName(input) {
313
364
  const normalized = toSafeFileName(input);
314
- return normalized.length > 0 ? normalized : "uncategorized";
365
+ return normalized.length > 0 ? normalized : 'uncategorized';
315
366
  }
316
367
  function buildConventionFileName(convention) {
317
368
  if (convention.fileName && convention.fileName.trim().length > 0) {
318
369
  return convention.fileName.trim();
319
370
  }
320
- const titleSegment = convention.title ? toSafeFileName(convention.title) : "";
321
- const prefix = titleSegment.length > 0 ? titleSegment : "convention";
371
+ const titleSegment = convention.title ? toSafeFileName(convention.title) : '';
372
+ const prefix = titleSegment.length > 0 ? titleSegment : 'convention';
322
373
  return `${prefix}.md`;
323
374
  }
324
375
  function normalizeMarkdownFileName(input) {
325
376
  const trimmed = input.trim();
326
- const base = trimmed.toLowerCase().endsWith('.md')
327
- ? trimmed.slice(0, -3)
328
- : trimmed;
377
+ const base = trimmed.toLowerCase().endsWith('.md') ? trimmed.slice(0, -3) : trimmed;
329
378
  const safeBase = toSafeFileName(base);
330
379
  const resolvedBase = safeBase.length > 0 ? safeBase : 'guide';
331
380
  return `${resolvedBase}.md`;
@@ -358,9 +407,7 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
358
407
  const baseFileName = buildPlatformGuideFileName(guide);
359
408
  const seenCount = fileNameCount.get(baseFileName) ?? 0;
360
409
  fileNameCount.set(baseFileName, seenCount + 1);
361
- const fileName = seenCount === 0
362
- ? baseFileName
363
- : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
410
+ const fileName = seenCount === 0 ? baseFileName : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
364
411
  const filePath = join(baseDir, fileName);
365
412
  atomicWriteFileSync(filePath, guide.content, 'utf-8');
366
413
  written += 1;
@@ -375,22 +422,24 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
375
422
  }
376
423
  }
377
424
  async function downloadReportingTemplate(projectRoot, config, apiUrl, headers) {
378
- const agentConfigResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs`, { headers });
425
+ const agentConfigResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs`, {
426
+ headers,
427
+ });
379
428
  const agentConfigs = agentConfigResponse.data?.data;
380
429
  if (!Array.isArray(agentConfigs) || agentConfigs.length === 0) {
381
430
  return false;
382
431
  }
383
432
  const firstAgentConfig = agentConfigs[0];
384
- if (!firstAgentConfig?.id || typeof firstAgentConfig.id !== "string") {
433
+ if (!firstAgentConfig?.id || typeof firstAgentConfig.id !== 'string') {
385
434
  return false;
386
435
  }
387
436
  const templateResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs/${firstAgentConfig.id}/convention`, { headers });
388
437
  const content = templateResponse.data?.data?.content;
389
- if (typeof content !== "string") {
438
+ if (typeof content !== 'string') {
390
439
  return false;
391
440
  }
392
441
  const conventionPath = join(projectRoot, CONVENTION_DIR, CONVENTION_INDEX_FILE);
393
- atomicWriteFileSync(conventionPath, content, "utf-8");
442
+ atomicWriteFileSync(conventionPath, content, 'utf-8');
394
443
  return true;
395
444
  }
396
445
  export async function conventionDownload(options) {
@@ -414,7 +463,7 @@ export async function conventionDownload(options) {
414
463
  rmSync(legacyDir, { recursive: true, force: true });
415
464
  const categoryDirs = new Set();
416
465
  for (const convention of conventionList) {
417
- const categoryName = typeof convention.category === "string" ? convention.category : "";
466
+ const categoryName = typeof convention.category === 'string' ? convention.category : '';
418
467
  categoryDirs.add(toSafeDirectoryName(categoryName));
419
468
  }
420
469
  for (const categoryDir of categoryDirs) {
@@ -430,18 +479,16 @@ export async function conventionDownload(options) {
430
479
  entries: [],
431
480
  };
432
481
  for (const convention of conventionList) {
433
- const contentMarkdown = typeof convention.contentMarkdown === "string" ? convention.contentMarkdown : "";
482
+ const contentMarkdown = typeof convention.contentMarkdown === 'string' ? convention.contentMarkdown : '';
434
483
  const baseFileName = buildConventionFileName(convention);
435
- const categoryName = typeof convention.category === "string" ? convention.category : "";
484
+ const categoryName = typeof convention.category === 'string' ? convention.category : '';
436
485
  const categoryDir = toSafeDirectoryName(categoryName);
437
486
  const duplicateKey = `${categoryDir}/${baseFileName}`;
438
487
  const seenCount = fileNameCount.get(duplicateKey) ?? 0;
439
488
  fileNameCount.set(duplicateKey, seenCount + 1);
440
- const fileName = seenCount === 0
441
- ? baseFileName
442
- : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
489
+ const fileName = seenCount === 0 ? baseFileName : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
443
490
  const filePath = join(projectRoot, CONVENTION_DIR, categoryDir, fileName);
444
- atomicWriteFileSync(filePath, contentMarkdown, "utf-8");
491
+ atomicWriteFileSync(filePath, contentMarkdown, 'utf-8');
445
492
  manifest.entries.push({
446
493
  conventionId: String(convention.id),
447
494
  fileRelativePath: normalizeRelativePath(relative(projectRoot, filePath)),
@@ -464,14 +511,12 @@ export async function conventionDownload(options) {
464
511
  : '';
465
512
  return `Convention sync completed.\nUpdated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\nNo project conventions found.${platformLine}`;
466
513
  }
467
- throw new Error("No conventions found for this project. Create one via the web dashboard first.");
514
+ throw new Error('No conventions found for this project. Create one via the web dashboard first.');
468
515
  }
469
- const reportingLine = hasReportingTemplate
470
- ? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n`
471
- : "";
516
+ const reportingLine = hasReportingTemplate ? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n` : '';
472
517
  const platformLine = platformGuideCount > 0
473
518
  ? `Downloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform\n`
474
- : "";
519
+ : '';
475
520
  return `Convention sync completed.\n${reportingLine}${platformLine}Downloaded ${conventions.length} file(s) into category directories under ${CONVENTION_DIR}`;
476
521
  }
477
522
  function parseConventionMarkdown(fileRelativePath, markdown) {
@@ -501,7 +546,7 @@ export async function conventionCreate(options) {
501
546
  const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
502
547
  const category = parseCategoryFromAgentteamsPath(fileRelativePath);
503
548
  const fileName = basename(absolutePath);
504
- if (!fileName.toLowerCase().endsWith(".md")) {
549
+ if (!fileName.toLowerCase().endsWith('.md')) {
505
550
  throw new Error(`Convention create requires a .md file: ${fileRelativePath}`);
506
551
  }
507
552
  const existingEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
@@ -509,10 +554,10 @@ export async function conventionCreate(options) {
509
554
  throw new Error(`File is already tracked in the manifest (use update instead): ${fileRelativePath}\n` +
510
555
  `- conventionId: ${existingEntry.conventionId}`);
511
556
  }
512
- const localMarkdown = readFileSync(absolutePath, "utf-8");
557
+ const localMarkdown = readFileSync(absolutePath, 'utf-8');
513
558
  const parsed = parseConventionMarkdown(fileRelativePath, localMarkdown);
514
559
  const frontmatter = (parsed.data ?? {});
515
- const bodyMarkdown = String(parsed.content ?? "");
560
+ const bodyMarkdown = String(parsed.content ?? '');
516
561
  const content = bodyMarkdown;
517
562
  const title = toOptionalString(frontmatter.title)?.trim() || fileNameToTitle(fileName);
518
563
  const payload = {
@@ -528,14 +573,14 @@ export async function conventionCreate(options) {
528
573
  payload.trigger = trigger;
529
574
  if (description)
530
575
  payload.description = description;
531
- if (typeof agentInstruction === "string" && agentInstruction.trim().length > 0) {
576
+ if (typeof agentInstruction === 'string' && agentInstruction.trim().length > 0) {
532
577
  payload.agentInstruction = agentInstruction.trimEnd();
533
578
  }
534
579
  const response = await withSpinner(`Creating convention for ${fileRelativePath}...`, () => httpClient.post(`${apiUrl}/api/projects/${config.projectId}/conventions`, payload, { headers }));
535
580
  const created = response.data?.data;
536
- const createdId = typeof created?.id === "string" ? created.id : "unknown";
537
- const createdUpdatedAt = typeof created?.updatedAt === "string" ? created.updatedAt : undefined;
538
- const createdWebUrl = typeof created?.webUrl === "string" ? created.webUrl : undefined;
581
+ const createdId = typeof created?.id === 'string' ? created.id : 'unknown';
582
+ const createdUpdatedAt = typeof created?.updatedAt === 'string' ? created.updatedAt : undefined;
583
+ const createdWebUrl = typeof created?.webUrl === 'string' ? created.webUrl : undefined;
539
584
  const now = new Date().toISOString();
540
585
  manifest.generatedAt = now;
541
586
  manifest.entries.push({
@@ -558,7 +603,7 @@ export async function conventionCreate(options) {
558
603
  results.push(`[OK] ${CONVENTION_DIR}/${CONVENTION_MANIFEST_FILE}: Updated.`);
559
604
  results.push(`[NEXT] Run 'agentteams convention download' to refresh convention.md and canonical markdown.`);
560
605
  }
561
- return results.join("\n");
606
+ return results.join('\n');
562
607
  }
563
608
  export async function conventionUpdate(options) {
564
609
  const { config, apiUrl, headers } = getApiConfigOrThrow(options);
@@ -583,13 +628,13 @@ export async function conventionUpdate(options) {
583
628
  throw new Error(`Only downloaded convention files can be updated: ${fileInput}\n` +
584
629
  `- resolved: ${fileRelativePath}\n` +
585
630
  `Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
586
- (available.length > 0 ? `Examples (partial):\n- ${available.join("\n- ")}` : ""));
631
+ (available.length > 0 ? `Examples (partial):\n- ${available.join('\n- ')}` : ''));
587
632
  }
588
633
  const conventionId = manifestEntry.conventionId;
589
634
  const [serverDetail, serverMarkdown, localMarkdown] = await withSpinner(`Preparing update for ${fileRelativePath}...`, async () => {
590
635
  const detailResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers });
591
- const downloadResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType: "text" });
592
- const local = readFileSync(absolutePath, "utf-8");
636
+ const downloadResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType: 'text' });
637
+ const local = readFileSync(absolutePath, 'utf-8');
593
638
  return [detailResponse.data?.data, String(downloadResponse.data), local];
594
639
  });
595
640
  if (!hasAnyDiff(serverMarkdown, localMarkdown)) {
@@ -604,18 +649,18 @@ export async function conventionUpdate(options) {
604
649
  }
605
650
  const parsed = parseConventionMarkdown(fileRelativePath, localMarkdown);
606
651
  const frontmatter = (parsed.data ?? {});
607
- const bodyMarkdown = String(parsed.content ?? "");
652
+ const bodyMarkdown = String(parsed.content ?? '');
608
653
  const content = bodyMarkdown;
609
- if (typeof serverDetail?.updatedAt !== "string" || serverDetail.updatedAt.length === 0) {
654
+ if (typeof serverDetail?.updatedAt !== 'string' || serverDetail.updatedAt.length === 0) {
610
655
  throw new Error(`[ERROR] ${fileRelativePath}: Server response is missing updatedAt.`);
611
656
  }
612
657
  const payload = {
613
658
  updatedAt: serverDetail.updatedAt,
614
659
  content,
615
660
  };
616
- const trigger = toOptionalStringOrNullIfPresent(frontmatter, "trigger");
617
- const description = toOptionalStringOrNullIfPresent(frontmatter, "description");
618
- const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter, "agentInstruction");
661
+ const trigger = toOptionalStringOrNullIfPresent(frontmatter, 'trigger');
662
+ const description = toOptionalStringOrNullIfPresent(frontmatter, 'description');
663
+ const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter, 'agentInstruction');
619
664
  if (trigger !== undefined)
620
665
  payload.trigger = trigger;
621
666
  if (description !== undefined)
@@ -624,12 +669,10 @@ export async function conventionUpdate(options) {
624
669
  payload.agentInstruction = agentInstruction;
625
670
  const updatedResponse = await withSpinner(`Uploading ${fileRelativePath}...`, () => httpClient.put(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, payload, { headers }));
626
671
  const newUpdatedAt = updatedResponse.data?.data?.updatedAt;
627
- const newWebUrl = typeof updatedResponse.data?.data?.webUrl === "string"
628
- ? updatedResponse.data.data.webUrl
629
- : undefined;
672
+ const newWebUrl = typeof updatedResponse.data?.data?.webUrl === 'string' ? updatedResponse.data.data.webUrl : undefined;
630
673
  const now = new Date().toISOString();
631
674
  manifestEntry.lastUploadedAt = now;
632
- if (typeof newUpdatedAt === "string") {
675
+ if (typeof newUpdatedAt === 'string') {
633
676
  manifestEntry.lastKnownUpdatedAt = newUpdatedAt;
634
677
  }
635
678
  writeManifest(projectRoot, manifest);
@@ -638,7 +681,7 @@ export async function conventionUpdate(options) {
638
681
  results.push(`webUrl: ${newWebUrl}`);
639
682
  }
640
683
  }
641
- return results.join("\n\n");
684
+ return results.join('\n\n');
642
685
  }
643
686
  export async function conventionDelete(options) {
644
687
  const { config, apiUrl, headers } = getApiConfigOrThrow(options);
@@ -663,7 +706,7 @@ export async function conventionDelete(options) {
663
706
  throw new Error(`Only downloaded convention files can be deleted: ${fileInput}\n` +
664
707
  `- resolved: ${fileRelativePath}\n` +
665
708
  `Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
666
- (available.length > 0 ? `Examples (partial):\n- ${available.join("\n- ")}` : ""));
709
+ (available.length > 0 ? `Examples (partial):\n- ${available.join('\n- ')}` : ''));
667
710
  }
668
711
  const entry = manifest.entries[entryIndex];
669
712
  const conventionId = entry.conventionId;
@@ -672,7 +715,9 @@ export async function conventionDelete(options) {
672
715
  results.push(`[DRY-RUN] ${fileRelativePath}: Planned only (no server delete).`);
673
716
  continue;
674
717
  }
675
- await withSpinner(`Deleting convention for ${fileRelativePath}...`, () => httpClient.delete(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers: withoutJsonContentType(headers) }));
718
+ await withSpinner(`Deleting convention for ${fileRelativePath}...`, () => httpClient.delete(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, {
719
+ headers: withoutJsonContentType(headers),
720
+ }));
676
721
  // After a successful server delete, also clean up local files/manifest.
677
722
  try {
678
723
  unlinkSync(absolutePath);
@@ -684,6 +729,6 @@ export async function conventionDelete(options) {
684
729
  writeManifest(projectRoot, manifest);
685
730
  results.push(`[OK] ${fileRelativePath}: Deleted. (conventionId=${conventionId})`);
686
731
  }
687
- return results.join("\n");
732
+ return results.join('\n');
688
733
  }
689
734
  //# sourceMappingURL=convention.js.map