@agentteams/cli 0.1.22

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 (200) hide show
  1. package/.agentteams/config.json +8 -0
  2. package/.agentteams/convention.md +100 -0
  3. package/.agentteams/platform/co-action-guide.md +144 -0
  4. package/.agentteams/platform/completion-report-guide.md +146 -0
  5. package/.agentteams/platform/convention-authoring-guide.md +112 -0
  6. package/.agentteams/platform/convention-ud-guide.md +59 -0
  7. package/.agentteams/platform/plan-guide.md +180 -0
  8. package/.agentteams/platform/plan-template.md +201 -0
  9. package/.agentteams/platform/post-mortem-guide.md +92 -0
  10. package/CODE_OF_CONDUCT.md +36 -0
  11. package/CONTRIBUTING.md +39 -0
  12. package/LICENSE +201 -0
  13. package/README.md +405 -0
  14. package/SECURITY.md +27 -0
  15. package/dist/api/coaction.d.ts +17 -0
  16. package/dist/api/coaction.d.ts.map +1 -0
  17. package/dist/api/coaction.js +102 -0
  18. package/dist/api/coaction.js.map +1 -0
  19. package/dist/api/comment.d.ts +13 -0
  20. package/dist/api/comment.d.ts.map +1 -0
  21. package/dist/api/comment.js +33 -0
  22. package/dist/api/comment.js.map +1 -0
  23. package/dist/api/feedback.d.ts +7 -0
  24. package/dist/api/feedback.d.ts.map +1 -0
  25. package/dist/api/feedback.js +6 -0
  26. package/dist/api/feedback.js.map +1 -0
  27. package/dist/api/linear.d.ts +5 -0
  28. package/dist/api/linear.d.ts.map +1 -0
  29. package/dist/api/linear.js +27 -0
  30. package/dist/api/linear.js.map +1 -0
  31. package/dist/api/member.d.ts +16 -0
  32. package/dist/api/member.d.ts.map +1 -0
  33. package/dist/api/member.js +6 -0
  34. package/dist/api/member.js.map +1 -0
  35. package/dist/api/plan.d.ts +51 -0
  36. package/dist/api/plan.d.ts.map +1 -0
  37. package/dist/api/plan.js +80 -0
  38. package/dist/api/plan.js.map +1 -0
  39. package/dist/api/postmortem.d.ts +6 -0
  40. package/dist/api/postmortem.d.ts.map +1 -0
  41. package/dist/api/postmortem.js +33 -0
  42. package/dist/api/postmortem.js.map +1 -0
  43. package/dist/api/report.d.ts +6 -0
  44. package/dist/api/report.d.ts.map +1 -0
  45. package/dist/api/report.js +33 -0
  46. package/dist/api/report.js.map +1 -0
  47. package/dist/api/search.d.ts +2 -0
  48. package/dist/api/search.d.ts.map +1 -0
  49. package/dist/api/search.js +20 -0
  50. package/dist/api/search.js.map +1 -0
  51. package/dist/api/status.d.ts +12 -0
  52. package/dist/api/status.d.ts.map +1 -0
  53. package/dist/api/status.js +33 -0
  54. package/dist/api/status.js.map +1 -0
  55. package/dist/commands/agentConfig.d.ts +4 -0
  56. package/dist/commands/agentConfig.d.ts.map +1 -0
  57. package/dist/commands/agentConfig.js +41 -0
  58. package/dist/commands/agentConfig.js.map +1 -0
  59. package/dist/commands/agentConfigCommand.d.ts +2 -0
  60. package/dist/commands/agentConfigCommand.d.ts.map +1 -0
  61. package/dist/commands/agentConfigCommand.js +20 -0
  62. package/dist/commands/agentConfigCommand.js.map +1 -0
  63. package/dist/commands/coaction.d.ts +2 -0
  64. package/dist/commands/coaction.d.ts.map +1 -0
  65. package/dist/commands/coaction.js +405 -0
  66. package/dist/commands/coaction.js.map +1 -0
  67. package/dist/commands/comment.d.ts +2 -0
  68. package/dist/commands/comment.d.ts.map +1 -0
  69. package/dist/commands/comment.js +63 -0
  70. package/dist/commands/comment.js.map +1 -0
  71. package/dist/commands/config.d.ts +2 -0
  72. package/dist/commands/config.d.ts.map +1 -0
  73. package/dist/commands/config.js +30 -0
  74. package/dist/commands/config.js.map +1 -0
  75. package/dist/commands/convention.d.ts +35 -0
  76. package/dist/commands/convention.d.ts.map +1 -0
  77. package/dist/commands/convention.js +680 -0
  78. package/dist/commands/convention.js.map +1 -0
  79. package/dist/commands/conventionRouter.d.ts +3 -0
  80. package/dist/commands/conventionRouter.d.ts.map +1 -0
  81. package/dist/commands/conventionRouter.js +46 -0
  82. package/dist/commands/conventionRouter.js.map +1 -0
  83. package/dist/commands/dependency.d.ts +4 -0
  84. package/dist/commands/dependency.d.ts.map +1 -0
  85. package/dist/commands/dependency.js +41 -0
  86. package/dist/commands/dependency.js.map +1 -0
  87. package/dist/commands/dependencyCommand.d.ts +2 -0
  88. package/dist/commands/dependencyCommand.d.ts.map +1 -0
  89. package/dist/commands/dependencyCommand.js +27 -0
  90. package/dist/commands/dependencyCommand.js.map +1 -0
  91. package/dist/commands/feedback.d.ts +2 -0
  92. package/dist/commands/feedback.d.ts.map +1 -0
  93. package/dist/commands/feedback.js +38 -0
  94. package/dist/commands/feedback.js.map +1 -0
  95. package/dist/commands/index.d.ts +2 -0
  96. package/dist/commands/index.d.ts.map +1 -0
  97. package/dist/commands/index.js +128 -0
  98. package/dist/commands/index.js.map +1 -0
  99. package/dist/commands/init.d.ts +22 -0
  100. package/dist/commands/init.d.ts.map +1 -0
  101. package/dist/commands/init.js +293 -0
  102. package/dist/commands/init.js.map +1 -0
  103. package/dist/commands/linear.d.ts +2 -0
  104. package/dist/commands/linear.d.ts.map +1 -0
  105. package/dist/commands/linear.js +41 -0
  106. package/dist/commands/linear.js.map +1 -0
  107. package/dist/commands/plan.d.ts +12 -0
  108. package/dist/commands/plan.d.ts.map +1 -0
  109. package/dist/commands/plan.js +626 -0
  110. package/dist/commands/plan.js.map +1 -0
  111. package/dist/commands/postmortem.d.ts +2 -0
  112. package/dist/commands/postmortem.d.ts.map +1 -0
  113. package/dist/commands/postmortem.js +157 -0
  114. package/dist/commands/postmortem.js.map +1 -0
  115. package/dist/commands/report.d.ts +2 -0
  116. package/dist/commands/report.d.ts.map +1 -0
  117. package/dist/commands/report.js +213 -0
  118. package/dist/commands/report.js.map +1 -0
  119. package/dist/commands/search.d.ts +2 -0
  120. package/dist/commands/search.d.ts.map +1 -0
  121. package/dist/commands/search.js +29 -0
  122. package/dist/commands/search.js.map +1 -0
  123. package/dist/commands/status.d.ts +2 -0
  124. package/dist/commands/status.d.ts.map +1 -0
  125. package/dist/commands/status.js +60 -0
  126. package/dist/commands/status.js.map +1 -0
  127. package/dist/index.d.ts +3 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +750 -0
  130. package/dist/index.js.map +1 -0
  131. package/dist/types/index.d.ts +237 -0
  132. package/dist/types/index.d.ts.map +1 -0
  133. package/dist/types/index.js +6 -0
  134. package/dist/types/index.js.map +1 -0
  135. package/dist/utils/atomicWrite.d.ts +2 -0
  136. package/dist/utils/atomicWrite.d.ts.map +1 -0
  137. package/dist/utils/atomicWrite.js +7 -0
  138. package/dist/utils/atomicWrite.js.map +1 -0
  139. package/dist/utils/authServer.d.ts +18 -0
  140. package/dist/utils/authServer.d.ts.map +1 -0
  141. package/dist/utils/authServer.js +215 -0
  142. package/dist/utils/authServer.js.map +1 -0
  143. package/dist/utils/config.d.ts +31 -0
  144. package/dist/utils/config.d.ts.map +1 -0
  145. package/dist/utils/config.js +113 -0
  146. package/dist/utils/config.js.map +1 -0
  147. package/dist/utils/env.d.ts +7 -0
  148. package/dist/utils/env.d.ts.map +1 -0
  149. package/dist/utils/env.js +19 -0
  150. package/dist/utils/env.js.map +1 -0
  151. package/dist/utils/errors.d.ts +6 -0
  152. package/dist/utils/errors.d.ts.map +1 -0
  153. package/dist/utils/errors.js +112 -0
  154. package/dist/utils/errors.js.map +1 -0
  155. package/dist/utils/formatter.d.ts +2 -0
  156. package/dist/utils/formatter.d.ts.map +1 -0
  157. package/dist/utils/formatter.js +55 -0
  158. package/dist/utils/formatter.js.map +1 -0
  159. package/dist/utils/git.d.ts +21 -0
  160. package/dist/utils/git.d.ts.map +1 -0
  161. package/dist/utils/git.js +44 -0
  162. package/dist/utils/git.js.map +1 -0
  163. package/dist/utils/httpClient.d.ts +3 -0
  164. package/dist/utils/httpClient.d.ts.map +1 -0
  165. package/dist/utils/httpClient.js +45 -0
  166. package/dist/utils/httpClient.js.map +1 -0
  167. package/dist/utils/httpHeaders.d.ts +3 -0
  168. package/dist/utils/httpHeaders.d.ts.map +1 -0
  169. package/dist/utils/httpHeaders.js +11 -0
  170. package/dist/utils/httpHeaders.js.map +1 -0
  171. package/dist/utils/initOutput.d.ts +3 -0
  172. package/dist/utils/initOutput.d.ts.map +1 -0
  173. package/dist/utils/initOutput.js +45 -0
  174. package/dist/utils/initOutput.js.map +1 -0
  175. package/dist/utils/legacyCompat.d.ts +3 -0
  176. package/dist/utils/legacyCompat.d.ts.map +1 -0
  177. package/dist/utils/legacyCompat.js +20 -0
  178. package/dist/utils/legacyCompat.js.map +1 -0
  179. package/dist/utils/outputPolicy.d.ts +12 -0
  180. package/dist/utils/outputPolicy.d.ts.map +1 -0
  181. package/dist/utils/outputPolicy.js +171 -0
  182. package/dist/utils/outputPolicy.js.map +1 -0
  183. package/dist/utils/parsers.d.ts +14 -0
  184. package/dist/utils/parsers.d.ts.map +1 -0
  185. package/dist/utils/parsers.js +78 -0
  186. package/dist/utils/parsers.js.map +1 -0
  187. package/dist/utils/planFormat.d.ts +17 -0
  188. package/dist/utils/planFormat.d.ts.map +1 -0
  189. package/dist/utils/planFormat.js +81 -0
  190. package/dist/utils/planFormat.js.map +1 -0
  191. package/dist/utils/spinner.d.ts +6 -0
  192. package/dist/utils/spinner.d.ts.map +1 -0
  193. package/dist/utils/spinner.js +34 -0
  194. package/dist/utils/spinner.js.map +1 -0
  195. package/dist/utils/updateCheck.d.ts +19 -0
  196. package/dist/utils/updateCheck.d.ts.map +1 -0
  197. package/dist/utils/updateCheck.js +103 -0
  198. package/dist/utils/updateCheck.js.map +1 -0
  199. package/package.json +55 -0
  200. package/rlarua-agentteams-cli-0.1.8.tgz +0 -0
@@ -0,0 +1,680 @@
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
+ function findProjectRoot(cwd) {
16
+ const configPath = findProjectConfig(cwd ?? process.cwd());
17
+ if (!configPath)
18
+ return null;
19
+ // configPath = /path/.agentteams/config.json → resolve up 2 levels to project root
20
+ return resolve(configPath, "..", "..");
21
+ }
22
+ function getApiBaseUrl(apiUrl) {
23
+ return apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
24
+ }
25
+ function getApiConfigOrThrow(options) {
26
+ const config = options?.config ?? loadConfig();
27
+ if (!config) {
28
+ throw new Error("Configuration not found. Run 'agentteams init' first or set AGENTTEAMS_* environment variables.");
29
+ }
30
+ return {
31
+ config,
32
+ apiUrl: getApiBaseUrl(config.apiUrl),
33
+ headers: {
34
+ "X-API-Key": config.apiKey,
35
+ "Content-Type": "application/json",
36
+ },
37
+ };
38
+ }
39
+ function normalizeRelativePath(input) {
40
+ return input.replaceAll("\\", "/");
41
+ }
42
+ function resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput) {
43
+ // If absolute path, keep as-is.
44
+ const resolvedFromCwd = resolve(cwd, fileInput);
45
+ if (resolvedFromCwd === fileInput && existsSync(fileInput)) {
46
+ return validatePathBoundary(fileInput, projectRoot);
47
+ }
48
+ // Common usage: pass `.agentteams/...` from any working directory.
49
+ if (fileInput.startsWith(`${CONVENTION_DIR}/`) || fileInput.startsWith(`${CONVENTION_DIR}\\`)) {
50
+ return validatePathBoundary(resolve(projectRoot, fileInput), projectRoot);
51
+ }
52
+ // Fallback: if the cwd-based resolution exists, use it.
53
+ if (existsSync(resolvedFromCwd)) {
54
+ return validatePathBoundary(resolvedFromCwd, projectRoot);
55
+ }
56
+ // Otherwise, return cwd-based resolution to preserve a stable error path.
57
+ return validatePathBoundary(resolvedFromCwd, projectRoot);
58
+ }
59
+ function validatePathBoundary(absolutePath, projectRoot) {
60
+ const normalized = resolve(absolutePath);
61
+ if (!normalized.startsWith(resolve(projectRoot) + sep)) {
62
+ throw new Error("Path traversal detected: file must be within project root");
63
+ }
64
+ return normalized;
65
+ }
66
+ function buildManifestPath(projectRoot) {
67
+ return join(projectRoot, CONVENTION_DIR, CONVENTION_MANIFEST_FILE);
68
+ }
69
+ function loadManifestOrThrow(projectRoot) {
70
+ const manifestPath = buildManifestPath(projectRoot);
71
+ if (!existsSync(manifestPath)) {
72
+ throw new Error(`Download manifest not found: ${manifestPath}\nRun 'agentteams convention download' first.`);
73
+ }
74
+ const raw = readFileSync(manifestPath, "utf-8");
75
+ const parsed = JSON.parse(raw);
76
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
77
+ throw new Error(`Invalid manifest format: ${manifestPath}`);
78
+ }
79
+ return parsed;
80
+ }
81
+ function loadManifestOrCreate(projectRoot) {
82
+ const manifestPath = buildManifestPath(projectRoot);
83
+ if (!existsSync(manifestPath)) {
84
+ return {
85
+ version: 1,
86
+ generatedAt: new Date().toISOString(),
87
+ entries: [],
88
+ };
89
+ }
90
+ const raw = readFileSync(manifestPath, "utf-8");
91
+ const parsed = JSON.parse(raw);
92
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
93
+ throw new Error(`Invalid manifest format: ${manifestPath}`);
94
+ }
95
+ return parsed;
96
+ }
97
+ function writeManifest(projectRoot, manifest) {
98
+ const manifestPath = buildManifestPath(projectRoot);
99
+ atomicWriteFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
100
+ }
101
+ function toFileList(input) {
102
+ return Array.isArray(input) ? input : [input];
103
+ }
104
+ function hasAnyDiff(a, b) {
105
+ const parts = diffLines(a, b);
106
+ return parts.some((p) => p.added || p.removed);
107
+ }
108
+ function createUnifiedDiff(fileLabel, serverText, localText) {
109
+ return createTwoFilesPatch(`${fileLabel} (server)`, `${fileLabel} (local)`, serverText, localText, "", "", { context: 3 });
110
+ }
111
+ function toOptionalString(value) {
112
+ return typeof value === "string" ? value : undefined;
113
+ }
114
+ function fileNameToTitle(fileName) {
115
+ return fileName
116
+ .replace(/\.md$/i, "")
117
+ .replace(/[-_]+/g, " ")
118
+ .replace(/\s+/g, " ")
119
+ .trim();
120
+ }
121
+ function parseCategoryFromAgentteamsPath(fileRelativePath) {
122
+ const normalized = normalizeRelativePath(fileRelativePath);
123
+ const parts = normalized.split("/");
124
+ const agentteamsIndex = parts.indexOf(CONVENTION_DIR);
125
+ if (agentteamsIndex === -1) {
126
+ throw new Error(`Convention create requires a file under ${CONVENTION_DIR}/<category>/: ${fileRelativePath}`);
127
+ }
128
+ const category = parts[agentteamsIndex + 1];
129
+ if (!category || category.length === 0) {
130
+ throw new Error(`Convention create requires a category directory under ${CONVENTION_DIR}/: ${fileRelativePath}`);
131
+ }
132
+ if (category === "platform" || category === "active-plan") {
133
+ throw new Error(`Convention create does not allow reserved directories under ${CONVENTION_DIR}/: ${category}`);
134
+ }
135
+ return category;
136
+ }
137
+ async function fetchAllConventions(apiUrl, projectId, headers) {
138
+ const pageSize = 100;
139
+ let page = 1;
140
+ let totalPages;
141
+ const items = [];
142
+ while (true) {
143
+ const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions`, { headers, params: { page, pageSize } });
144
+ const data = response.data?.data;
145
+ if (!Array.isArray(data)) {
146
+ break;
147
+ }
148
+ items.push(...data);
149
+ const meta = response.data?.meta;
150
+ if (typeof meta?.totalPages === "number") {
151
+ totalPages = meta.totalPages;
152
+ }
153
+ if (totalPages !== undefined) {
154
+ if (page >= totalPages)
155
+ break;
156
+ page += 1;
157
+ continue;
158
+ }
159
+ // Fallback if meta is missing: stop when we got less than a full page.
160
+ if (data.length < pageSize)
161
+ break;
162
+ page += 1;
163
+ }
164
+ return items;
165
+ }
166
+ async function fetchConventionsWithContent(apiUrl, projectId, headers) {
167
+ const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions/download-all`, { headers });
168
+ const data = response.data?.data;
169
+ if (!Array.isArray(data)) {
170
+ throw new Error("Invalid download-all response format");
171
+ }
172
+ return data;
173
+ }
174
+ async function fetchPlatformGuidesHash(apiUrl, headers) {
175
+ const response = await httpClient.get(`${apiUrl}/api/platform/guides/hash`, { headers });
176
+ const hash = response.data?.data?.hash;
177
+ if (typeof hash !== "string" || hash.length === 0) {
178
+ throw new Error("Invalid platform guides hash response format");
179
+ }
180
+ return hash;
181
+ }
182
+ function toConventionName(convention) {
183
+ const title = typeof convention.title === "string" ? convention.title.trim() : "";
184
+ if (title.length > 0)
185
+ return title;
186
+ const fileName = typeof convention.fileName === "string" ? convention.fileName.trim() : "";
187
+ if (fileName.length > 0)
188
+ return fileName;
189
+ return convention.id;
190
+ }
191
+ function toConventionNameFromManifest(entry) {
192
+ const title = typeof entry.title === "string" ? entry.title.trim() : "";
193
+ if (title.length > 0)
194
+ return title;
195
+ const fileName = typeof entry.fileName === "string" ? entry.fileName.trim() : "";
196
+ if (fileName.length > 0)
197
+ return fileName;
198
+ return entry.conventionId;
199
+ }
200
+ function toOptionalStringOrNullIfPresent(data, key) {
201
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
202
+ return undefined;
203
+ }
204
+ const value = data[key];
205
+ if (value === null) {
206
+ return null;
207
+ }
208
+ if (typeof value === "string") {
209
+ const trimmed = value.trim();
210
+ return trimmed.length > 0 ? trimmed : null;
211
+ }
212
+ return undefined;
213
+ }
214
+ export async function conventionShow() {
215
+ const { config, apiUrl, headers } = getApiConfigOrThrow();
216
+ const conventions = await fetchConventionsWithContent(apiUrl, config.projectId, headers);
217
+ if (!conventions || conventions.length === 0) {
218
+ throw new Error("No conventions found for this project. Create one via the web dashboard first.");
219
+ }
220
+ const sections = [];
221
+ 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}`;
224
+ sections.push(`${sectionHeader}\n\n${contentMarkdown}`);
225
+ }
226
+ return sections.join("\n\n---\n\n");
227
+ }
228
+ export async function checkConventionFreshness(apiUrl, projectId, headers, projectRoot) {
229
+ const manifestPath = buildManifestPath(projectRoot);
230
+ if (!existsSync(manifestPath)) {
231
+ return {
232
+ platformGuidesChanged: false,
233
+ conventionChanges: [],
234
+ };
235
+ }
236
+ const manifest = loadManifestOrThrow(projectRoot);
237
+ const currentPlatformGuidesHash = await fetchPlatformGuidesHash(apiUrl, headers);
238
+ const platformGuidesChanged = typeof manifest.platformGuidesHash === "string"
239
+ && manifest.platformGuidesHash.length > 0
240
+ && manifest.platformGuidesHash !== currentPlatformGuidesHash;
241
+ const serverConventions = await fetchAllConventions(apiUrl, projectId, headers);
242
+ const serverById = new Map(serverConventions.map((item) => [item.id, item]));
243
+ const localById = new Map(manifest.entries.map((entry) => [entry.conventionId, entry]));
244
+ const conventionChanges = [];
245
+ for (const serverConvention of serverConventions) {
246
+ const local = localById.get(serverConvention.id);
247
+ if (!local) {
248
+ conventionChanges.push({
249
+ id: serverConvention.id,
250
+ type: "new",
251
+ title: toConventionName(serverConvention),
252
+ fileName: serverConvention.fileName ?? undefined,
253
+ });
254
+ continue;
255
+ }
256
+ if (typeof serverConvention.updatedAt === "string"
257
+ && typeof local.updatedAt === "string"
258
+ && serverConvention.updatedAt !== local.updatedAt) {
259
+ conventionChanges.push({
260
+ id: serverConvention.id,
261
+ type: "updated",
262
+ title: toConventionName(serverConvention),
263
+ fileName: serverConvention.fileName ?? local.fileName,
264
+ });
265
+ }
266
+ }
267
+ for (const localEntry of manifest.entries) {
268
+ if (serverById.has(localEntry.conventionId))
269
+ continue;
270
+ conventionChanges.push({
271
+ id: localEntry.conventionId,
272
+ type: "deleted",
273
+ title: toConventionNameFromManifest(localEntry),
274
+ fileName: localEntry.fileName,
275
+ });
276
+ }
277
+ return {
278
+ platformGuidesChanged,
279
+ conventionChanges,
280
+ };
281
+ }
282
+ export async function conventionList() {
283
+ const { config, apiUrl, headers } = getApiConfigOrThrow();
284
+ const conventions = await fetchAllConventions(apiUrl, config.projectId, headers);
285
+ if (!Array.isArray(conventions)) {
286
+ return { data: conventions };
287
+ }
288
+ return {
289
+ data: conventions.map((item) => ({
290
+ id: item.id,
291
+ title: item.title,
292
+ category: item.category,
293
+ fileName: item.fileName,
294
+ updatedAt: item.updatedAt,
295
+ createdAt: item.createdAt,
296
+ })),
297
+ meta: {
298
+ total: conventions.length,
299
+ page: 1,
300
+ pageSize: conventions.length,
301
+ totalPages: 1,
302
+ }
303
+ };
304
+ }
305
+ function toSafeFileName(input) {
306
+ return input
307
+ .toLowerCase()
308
+ .replace(/[^a-z0-9]+/g, "-")
309
+ .replace(/^-+|-+$/g, "")
310
+ .slice(0, 60);
311
+ }
312
+ function toSafeDirectoryName(input) {
313
+ const normalized = toSafeFileName(input);
314
+ return normalized.length > 0 ? normalized : "uncategorized";
315
+ }
316
+ function buildConventionFileName(convention) {
317
+ if (convention.fileName && convention.fileName.trim().length > 0) {
318
+ return convention.fileName.trim();
319
+ }
320
+ const titleSegment = convention.title ? toSafeFileName(convention.title) : "";
321
+ const prefix = titleSegment.length > 0 ? titleSegment : "convention";
322
+ return `${prefix}.md`;
323
+ }
324
+ function normalizeMarkdownFileName(input) {
325
+ const trimmed = input.trim();
326
+ const base = trimmed.toLowerCase().endsWith('.md')
327
+ ? trimmed.slice(0, -3)
328
+ : trimmed;
329
+ const safeBase = toSafeFileName(base);
330
+ const resolvedBase = safeBase.length > 0 ? safeBase : 'guide';
331
+ return `${resolvedBase}.md`;
332
+ }
333
+ function buildPlatformGuideFileName(guide) {
334
+ if (typeof guide.fileName === 'string' && guide.fileName.trim().length > 0) {
335
+ return normalizeMarkdownFileName(guide.fileName);
336
+ }
337
+ if (typeof guide.title === 'string' && guide.title.trim().length > 0) {
338
+ return `${toSafeFileName(guide.title)}.md`;
339
+ }
340
+ return 'guide.md';
341
+ }
342
+ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
343
+ try {
344
+ const response = await httpClient.get(`${apiUrl}/api/platform/guides`, { headers });
345
+ const guides = response.data?.data;
346
+ if (!Array.isArray(guides) || guides.length === 0) {
347
+ return 0;
348
+ }
349
+ const baseDir = join(projectRoot, CONVENTION_DIR, 'platform');
350
+ rmSync(baseDir, { recursive: true, force: true });
351
+ mkdirSync(baseDir, { recursive: true });
352
+ const fileNameCount = new Map();
353
+ let written = 0;
354
+ for (const guide of guides) {
355
+ if (!guide || typeof guide.content !== 'string') {
356
+ continue;
357
+ }
358
+ const baseFileName = buildPlatformGuideFileName(guide);
359
+ const seenCount = fileNameCount.get(baseFileName) ?? 0;
360
+ fileNameCount.set(baseFileName, seenCount + 1);
361
+ const fileName = seenCount === 0
362
+ ? baseFileName
363
+ : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
364
+ const filePath = join(baseDir, fileName);
365
+ atomicWriteFileSync(filePath, guide.content, 'utf-8');
366
+ written += 1;
367
+ }
368
+ return written;
369
+ }
370
+ catch (error) {
371
+ if (isAxiosError(error) && error.response?.status === 404) {
372
+ return 0;
373
+ }
374
+ throw error;
375
+ }
376
+ }
377
+ async function downloadReportingTemplate(projectRoot, config, apiUrl, headers) {
378
+ const agentConfigResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs`, { headers });
379
+ const agentConfigs = agentConfigResponse.data?.data;
380
+ if (!Array.isArray(agentConfigs) || agentConfigs.length === 0) {
381
+ return false;
382
+ }
383
+ const firstAgentConfig = agentConfigs[0];
384
+ if (!firstAgentConfig?.id || typeof firstAgentConfig.id !== "string") {
385
+ return false;
386
+ }
387
+ const templateResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs/${firstAgentConfig.id}/convention`, { headers });
388
+ const content = templateResponse.data?.data?.content;
389
+ if (typeof content !== "string") {
390
+ return false;
391
+ }
392
+ const conventionPath = join(projectRoot, CONVENTION_DIR, CONVENTION_INDEX_FILE);
393
+ atomicWriteFileSync(conventionPath, content, "utf-8");
394
+ return true;
395
+ }
396
+ export async function conventionDownload(options) {
397
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
398
+ const projectRoot = findProjectRoot(options?.cwd);
399
+ if (!projectRoot) {
400
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
401
+ }
402
+ const conventionRoot = join(projectRoot, CONVENTION_DIR);
403
+ if (!existsSync(conventionRoot)) {
404
+ throw new Error(`Convention directory not found: ${conventionRoot}\nRun 'agentteams init' first.`);
405
+ }
406
+ const hasReportingTemplate = await withSpinner('Downloading reporting template...', () => downloadReportingTemplate(projectRoot, config, apiUrl, headers));
407
+ const platformGuideCount = await withSpinner('Downloading platform guides...', () => downloadPlatformGuides(projectRoot, apiUrl, headers));
408
+ const conventions = await withSpinner('Downloading conventions...', async () => {
409
+ const conventionList = await fetchConventionsWithContent(apiUrl, config.projectId, headers);
410
+ if (!conventionList || conventionList.length === 0) {
411
+ return conventionList;
412
+ }
413
+ const legacyDir = join(projectRoot, CONVENTION_DIR, LEGACY_CONVENTION_DOWNLOAD_DIR);
414
+ rmSync(legacyDir, { recursive: true, force: true });
415
+ const categoryDirs = new Set();
416
+ for (const convention of conventionList) {
417
+ const categoryName = typeof convention.category === "string" ? convention.category : "";
418
+ categoryDirs.add(toSafeDirectoryName(categoryName));
419
+ }
420
+ for (const categoryDir of categoryDirs) {
421
+ rmSync(join(projectRoot, CONVENTION_DIR, categoryDir), { recursive: true, force: true });
422
+ mkdirSync(join(projectRoot, CONVENTION_DIR, categoryDir), { recursive: true });
423
+ }
424
+ const fileNameCount = new Map();
425
+ const platformGuidesHash = await fetchPlatformGuidesHash(apiUrl, headers);
426
+ const manifest = {
427
+ version: 1,
428
+ generatedAt: new Date().toISOString(),
429
+ platformGuidesHash,
430
+ entries: [],
431
+ };
432
+ for (const convention of conventionList) {
433
+ const contentMarkdown = typeof convention.contentMarkdown === "string" ? convention.contentMarkdown : "";
434
+ const baseFileName = buildConventionFileName(convention);
435
+ const categoryName = typeof convention.category === "string" ? convention.category : "";
436
+ const categoryDir = toSafeDirectoryName(categoryName);
437
+ const duplicateKey = `${categoryDir}/${baseFileName}`;
438
+ const seenCount = fileNameCount.get(duplicateKey) ?? 0;
439
+ fileNameCount.set(duplicateKey, seenCount + 1);
440
+ const fileName = seenCount === 0
441
+ ? baseFileName
442
+ : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
443
+ const filePath = join(projectRoot, CONVENTION_DIR, categoryDir, fileName);
444
+ atomicWriteFileSync(filePath, contentMarkdown, "utf-8");
445
+ manifest.entries.push({
446
+ conventionId: String(convention.id),
447
+ fileRelativePath: normalizeRelativePath(relative(projectRoot, filePath)),
448
+ fileName,
449
+ categoryDir,
450
+ title: toOptionalString(convention.title),
451
+ category: toOptionalString(convention.category),
452
+ scope: toOptionalString(convention.scope),
453
+ updatedAt: toOptionalString(convention.updatedAt),
454
+ downloadedAt: new Date().toISOString(),
455
+ });
456
+ }
457
+ writeManifest(projectRoot, manifest);
458
+ return conventionList;
459
+ });
460
+ if (!conventions || conventions.length === 0) {
461
+ if (hasReportingTemplate) {
462
+ const platformLine = platformGuideCount > 0
463
+ ? `\nDownloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform`
464
+ : '';
465
+ return `Convention sync completed.\nUpdated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\nNo project conventions found.${platformLine}`;
466
+ }
467
+ throw new Error("No conventions found for this project. Create one via the web dashboard first.");
468
+ }
469
+ const reportingLine = hasReportingTemplate
470
+ ? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n`
471
+ : "";
472
+ const platformLine = platformGuideCount > 0
473
+ ? `Downloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform\n`
474
+ : "";
475
+ return `Convention sync completed.\n${reportingLine}${platformLine}Downloaded ${conventions.length} file(s) into category directories under ${CONVENTION_DIR}`;
476
+ }
477
+ export async function conventionCreate(options) {
478
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
479
+ const projectRoot = findProjectRoot(options?.cwd);
480
+ if (!projectRoot) {
481
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
482
+ }
483
+ const manifest = loadManifestOrCreate(projectRoot);
484
+ const files = toFileList(options.file);
485
+ const results = [];
486
+ for (const fileInput of files) {
487
+ const cwd = options.cwd ?? process.cwd();
488
+ const absolutePath = resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput);
489
+ if (!existsSync(absolutePath)) {
490
+ throw new Error(`File not found: ${normalizeRelativePath(relative(projectRoot, absolutePath))}`);
491
+ }
492
+ const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
493
+ const category = parseCategoryFromAgentteamsPath(fileRelativePath);
494
+ const fileName = basename(absolutePath);
495
+ if (!fileName.toLowerCase().endsWith(".md")) {
496
+ throw new Error(`Convention create requires a .md file: ${fileRelativePath}`);
497
+ }
498
+ const existingEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
499
+ if (existingEntry) {
500
+ throw new Error(`File is already tracked in the manifest (use update instead): ${fileRelativePath}\n` +
501
+ `- conventionId: ${existingEntry.conventionId}`);
502
+ }
503
+ const localMarkdown = readFileSync(absolutePath, "utf-8");
504
+ const parsed = matter(localMarkdown);
505
+ const frontmatter = (parsed.data ?? {});
506
+ const bodyMarkdown = String(parsed.content ?? "");
507
+ const content = bodyMarkdown;
508
+ const title = toOptionalString(frontmatter.title)?.trim() || fileNameToTitle(fileName);
509
+ const payload = {
510
+ title,
511
+ category,
512
+ fileName,
513
+ content,
514
+ };
515
+ const trigger = toOptionalString(frontmatter.trigger)?.trim();
516
+ const description = toOptionalString(frontmatter.description)?.trim();
517
+ const agentInstruction = toOptionalString(frontmatter.agentInstruction);
518
+ if (trigger)
519
+ payload.trigger = trigger;
520
+ if (description)
521
+ payload.description = description;
522
+ if (typeof agentInstruction === "string" && agentInstruction.trim().length > 0) {
523
+ payload.agentInstruction = agentInstruction.trimEnd();
524
+ }
525
+ const response = await withSpinner(`Creating convention for ${fileRelativePath}...`, () => httpClient.post(`${apiUrl}/api/projects/${config.projectId}/conventions`, payload, { headers }));
526
+ const created = response.data?.data;
527
+ const createdId = typeof created?.id === "string" ? created.id : "unknown";
528
+ const createdUpdatedAt = typeof created?.updatedAt === "string" ? created.updatedAt : undefined;
529
+ const createdWebUrl = typeof created?.webUrl === "string" ? created.webUrl : undefined;
530
+ const now = new Date().toISOString();
531
+ manifest.generatedAt = now;
532
+ manifest.entries.push({
533
+ conventionId: createdId,
534
+ fileRelativePath,
535
+ fileName,
536
+ categoryDir: category,
537
+ title,
538
+ category,
539
+ ...(createdUpdatedAt ? { updatedAt: createdUpdatedAt } : {}),
540
+ downloadedAt: now,
541
+ lastUploadedAt: now,
542
+ ...(createdUpdatedAt ? { lastKnownUpdatedAt: createdUpdatedAt } : {}),
543
+ });
544
+ writeManifest(projectRoot, manifest);
545
+ results.push(`[OK] ${fileRelativePath}: Created. (conventionId=${createdId})`);
546
+ if (createdWebUrl) {
547
+ results.push(`webUrl: ${createdWebUrl}`);
548
+ }
549
+ results.push(`[OK] ${CONVENTION_DIR}/${CONVENTION_MANIFEST_FILE}: Updated.`);
550
+ results.push(`[NEXT] Run 'agentteams convention download' to refresh convention.md and canonical markdown.`);
551
+ }
552
+ return results.join("\n");
553
+ }
554
+ export async function conventionUpdate(options) {
555
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
556
+ const projectRoot = findProjectRoot(options?.cwd);
557
+ if (!projectRoot) {
558
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
559
+ }
560
+ const manifest = loadManifestOrThrow(projectRoot);
561
+ const files = toFileList(options.file);
562
+ const apply = options.apply === true;
563
+ const results = [];
564
+ for (const fileInput of files) {
565
+ const cwd = options.cwd ?? process.cwd();
566
+ const absolutePath = resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput);
567
+ const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
568
+ const manifestEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
569
+ if (!manifestEntry) {
570
+ const available = manifest.entries
571
+ .map((e) => e.fileRelativePath)
572
+ .sort()
573
+ .slice(0, 30);
574
+ throw new Error(`Only downloaded convention files can be updated: ${fileInput}\n` +
575
+ `- resolved: ${fileRelativePath}\n` +
576
+ `Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
577
+ (available.length > 0 ? `Examples (partial):\n- ${available.join("\n- ")}` : ""));
578
+ }
579
+ const conventionId = manifestEntry.conventionId;
580
+ const [serverDetail, serverMarkdown, localMarkdown] = await withSpinner(`Preparing update for ${fileRelativePath}...`, async () => {
581
+ const detailResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers });
582
+ const downloadResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType: "text" });
583
+ const local = readFileSync(absolutePath, "utf-8");
584
+ return [detailResponse.data?.data, String(downloadResponse.data), local];
585
+ });
586
+ if (!hasAnyDiff(serverMarkdown, localMarkdown)) {
587
+ results.push(`[SKIP] ${fileRelativePath}: No changes detected.`);
588
+ continue;
589
+ }
590
+ const diffText = createUnifiedDiff(fileRelativePath, serverMarkdown, localMarkdown);
591
+ results.push(diffText.trimEnd());
592
+ if (!apply) {
593
+ results.push(`[DRY-RUN] ${fileRelativePath}: Printed diff only (no server changes).`);
594
+ continue;
595
+ }
596
+ const parsed = matter(localMarkdown);
597
+ const frontmatter = (parsed.data ?? {});
598
+ const bodyMarkdown = String(parsed.content ?? "");
599
+ const content = bodyMarkdown;
600
+ if (typeof serverDetail?.updatedAt !== "string" || serverDetail.updatedAt.length === 0) {
601
+ throw new Error(`[ERROR] ${fileRelativePath}: Server response is missing updatedAt.`);
602
+ }
603
+ const payload = {
604
+ updatedAt: serverDetail.updatedAt,
605
+ content,
606
+ };
607
+ const trigger = toOptionalStringOrNullIfPresent(frontmatter, "trigger");
608
+ const description = toOptionalStringOrNullIfPresent(frontmatter, "description");
609
+ const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter, "agentInstruction");
610
+ if (trigger !== undefined)
611
+ payload.trigger = trigger;
612
+ if (description !== undefined)
613
+ payload.description = description;
614
+ if (agentInstruction !== undefined)
615
+ payload.agentInstruction = agentInstruction;
616
+ const updatedResponse = await withSpinner(`Uploading ${fileRelativePath}...`, () => httpClient.put(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, payload, { headers }));
617
+ const newUpdatedAt = updatedResponse.data?.data?.updatedAt;
618
+ const newWebUrl = typeof updatedResponse.data?.data?.webUrl === "string"
619
+ ? updatedResponse.data.data.webUrl
620
+ : undefined;
621
+ const now = new Date().toISOString();
622
+ manifestEntry.lastUploadedAt = now;
623
+ if (typeof newUpdatedAt === "string") {
624
+ manifestEntry.lastKnownUpdatedAt = newUpdatedAt;
625
+ }
626
+ writeManifest(projectRoot, manifest);
627
+ results.push(`[OK] ${fileRelativePath}: Update applied. (conventionId=${conventionId})`);
628
+ if (newWebUrl) {
629
+ results.push(`webUrl: ${newWebUrl}`);
630
+ }
631
+ }
632
+ return results.join("\n\n");
633
+ }
634
+ export async function conventionDelete(options) {
635
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
636
+ const projectRoot = findProjectRoot(options?.cwd);
637
+ if (!projectRoot) {
638
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
639
+ }
640
+ const manifest = loadManifestOrThrow(projectRoot);
641
+ const files = toFileList(options.file);
642
+ const apply = options.apply === true;
643
+ const results = [];
644
+ for (const fileInput of files) {
645
+ const cwd = options.cwd ?? process.cwd();
646
+ const absolutePath = resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput);
647
+ const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
648
+ const entryIndex = manifest.entries.findIndex((e) => e.fileRelativePath === fileRelativePath);
649
+ if (entryIndex === -1) {
650
+ const available = manifest.entries
651
+ .map((e) => e.fileRelativePath)
652
+ .sort()
653
+ .slice(0, 30);
654
+ throw new Error(`Only downloaded convention files can be deleted: ${fileInput}\n` +
655
+ `- resolved: ${fileRelativePath}\n` +
656
+ `Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
657
+ (available.length > 0 ? `Examples (partial):\n- ${available.join("\n- ")}` : ""));
658
+ }
659
+ const entry = manifest.entries[entryIndex];
660
+ const conventionId = entry.conventionId;
661
+ results.push(`[PLAN] ${fileRelativePath}: Will delete conventionId=${conventionId}`);
662
+ if (!apply) {
663
+ results.push(`[DRY-RUN] ${fileRelativePath}: Planned only (no server delete).`);
664
+ continue;
665
+ }
666
+ await withSpinner(`Deleting convention for ${fileRelativePath}...`, () => httpClient.delete(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers: withoutJsonContentType(headers) }));
667
+ // After a successful server delete, also clean up local files/manifest.
668
+ try {
669
+ unlinkSync(absolutePath);
670
+ }
671
+ catch {
672
+ // ignore
673
+ }
674
+ manifest.entries.splice(entryIndex, 1);
675
+ writeManifest(projectRoot, manifest);
676
+ results.push(`[OK] ${fileRelativePath}: Deleted. (conventionId=${conventionId})`);
677
+ }
678
+ return results.join("\n");
679
+ }
680
+ //# sourceMappingURL=convention.js.map