esedre 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esedre.mjs CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/esedre.ts
4
- import fs8 from "node:fs";
5
- import path8 from "node:path";
4
+ import fs9 from "node:fs";
5
+ import path9 from "node:path";
6
6
 
7
7
  // src/storage/filesystem.ts
8
8
  import fs3 from "node:fs";
9
9
  import path3 from "node:path";
10
10
 
11
11
  // src/types.ts
12
+ var CURRENT_ESEDRE_VERSION = "0.1.6";
12
13
  var EsedreConflictError = class extends Error {
13
14
  constructor(ticketId, currentHash, lastHash) {
14
15
  super(
@@ -27,9 +28,18 @@ var EsedreConflictError = class extends Error {
27
28
  // src/config.ts
28
29
  import fs from "node:fs";
29
30
  import path from "node:path";
31
+ import os from "node:os";
30
32
  var DEFAULT_ESEDRE_PORT = 5674;
31
- var MAX_PROJECT_CODE_LENGTH = 6;
32
- var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,6}$/;
33
+ var MAX_PROJECT_CODE_LENGTH = 7;
34
+ var PROJECT_CODE_REGEX = /^[a-zA-Z0-9]{1,7}$/;
35
+ var MAX_PROJECT_NAME_LENGTH = 48;
36
+ function resolvePorts(config, overrides) {
37
+ const globalConfig = readGlobalConfig();
38
+ const gateway = overrides?.gatewayPort ?? overrides?.port ?? overrides?.ports?.gateway ?? config?.ports?.gateway ?? config?.port ?? globalConfig?.ports?.gateway ?? globalConfig?.port ?? DEFAULT_ESEDRE_PORT;
39
+ const ui = overrides?.uiPort ?? overrides?.ports?.ui ?? config?.ports?.ui ?? globalConfig?.ports?.ui ?? gateway + 1;
40
+ const api = overrides?.apiPort ?? overrides?.ports?.api ?? config?.ports?.api ?? globalConfig?.ports?.api ?? gateway + 2;
41
+ return { gateway, ui, api };
42
+ }
33
43
  var EsedreAuthorizationError = class extends Error {
34
44
  constructor(projectCode, message) {
35
45
  super(message || `Access Denied: Project '${projectCode}' is outside this workspace's authorized scope.`);
@@ -46,7 +56,7 @@ function validateProjectCode(code, existingCodes) {
46
56
  if (!PROJECT_CODE_REGEX.test(trimmed)) {
47
57
  return {
48
58
  valid: false,
49
- error: `Project code '${trimmed}' is invalid. It must be 1 to ${MAX_PROJECT_CODE_LENGTH} alphanumeric characters (e.g. CORE, WEB, DOCS).`
59
+ error: `Project code '${trimmed}' is invalid. It must be 1 to ${MAX_PROJECT_CODE_LENGTH} alphanumeric characters (e.g. CORE, ALCE, WEB).`
50
60
  };
51
61
  }
52
62
  if (existingCodes && existingCodes.length > 0) {
@@ -61,6 +71,22 @@ function validateProjectCode(code, existingCodes) {
61
71
  }
62
72
  return { valid: true };
63
73
  }
74
+ function validateProjectName(name) {
75
+ if (!name || typeof name !== "string") {
76
+ return { valid: false, error: "Project name is required." };
77
+ }
78
+ const trimmed = name.trim();
79
+ if (trimmed.length === 0) {
80
+ return { valid: false, error: "Project name cannot be empty." };
81
+ }
82
+ if (trimmed.length > MAX_PROJECT_NAME_LENGTH) {
83
+ return {
84
+ valid: false,
85
+ error: `Project name '${trimmed}' exceeds maximum length of ${MAX_PROJECT_NAME_LENGTH} characters.`
86
+ };
87
+ }
88
+ return { valid: true };
89
+ }
64
90
  function isProjectAuthorized(projectCode, allowedProjects) {
65
91
  if (!allowedProjects || allowedProjects.length === 0) {
66
92
  return true;
@@ -72,35 +98,277 @@ function isProjectAuthorized(projectCode, allowedProjects) {
72
98
  const normalized = projectCode.trim().toUpperCase();
73
99
  return normalizedList.includes(normalized);
74
100
  }
75
- function findEsedreConfig(startDir) {
101
+ function getGlobalConfigDir() {
102
+ if (process.env.ESEDRE_GLOBAL_DIR) {
103
+ return process.env.ESEDRE_GLOBAL_DIR;
104
+ }
105
+ return path.join(os.homedir(), ".esedre");
106
+ }
107
+ function getGlobalConfigPath() {
108
+ return path.join(getGlobalConfigDir(), "config.json");
109
+ }
110
+ function expandHome(filePath) {
111
+ if (!filePath) return filePath;
112
+ if (filePath === "~" || filePath.startsWith("~/") || filePath.startsWith("~\\")) {
113
+ return path.join(os.homedir(), filePath.slice(1));
114
+ }
115
+ return filePath;
116
+ }
117
+ function readGlobalConfig() {
118
+ const configPath = getGlobalConfigPath();
119
+ if (!fs.existsSync(configPath)) {
120
+ return null;
121
+ }
122
+ try {
123
+ const raw = fs.readFileSync(configPath, "utf-8");
124
+ return JSON.parse(raw);
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ function writeGlobalConfig(config) {
130
+ const dir = getGlobalConfigDir();
131
+ if (!fs.existsSync(dir)) {
132
+ fs.mkdirSync(dir, { recursive: true });
133
+ }
134
+ const configPath = getGlobalConfigPath();
135
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
136
+ try {
137
+ writeGlobalVersion(config.version || CURRENT_ESEDRE_VERSION);
138
+ } catch {
139
+ }
140
+ }
141
+ function getGlobalVersionPath() {
142
+ return path.join(getGlobalConfigDir(), "version");
143
+ }
144
+ function writeGlobalVersion(version = CURRENT_ESEDRE_VERSION) {
145
+ const dir = getGlobalConfigDir();
146
+ if (!fs.existsSync(dir)) {
147
+ fs.mkdirSync(dir, { recursive: true });
148
+ }
149
+ fs.writeFileSync(getGlobalVersionPath(), version.trim() + "\n", "utf-8");
150
+ }
151
+ function addLocationToGlobalConfig(targetPath) {
152
+ const resolved = path.resolve(expandHome(targetPath));
153
+ if (!fs.existsSync(resolved)) {
154
+ throw new Error(`Target path does not exist: "${targetPath}" (resolved: ${resolved})`);
155
+ }
156
+ const current = readGlobalConfig() || { version: CURRENT_ESEDRE_VERSION, port: DEFAULT_ESEDRE_PORT };
157
+ const hasProjectsDir = fs.existsSync(path.join(resolved, "projects"));
158
+ const hasProjectsJson = fs.existsSync(path.join(resolved, "projects.json"));
159
+ if (hasProjectsDir || hasProjectsJson) {
160
+ let projectCount = 0;
161
+ if (hasProjectsJson) {
162
+ try {
163
+ const pjs = JSON.parse(fs.readFileSync(path.join(resolved, "projects.json"), "utf-8"));
164
+ if (Array.isArray(pjs)) projectCount = pjs.length;
165
+ } catch {
166
+ }
167
+ } else if (hasProjectsDir) {
168
+ try {
169
+ projectCount = fs.readdirSync(path.join(resolved, "projects")).filter((e) => {
170
+ return fs.statSync(path.join(resolved, "projects", e)).isDirectory();
171
+ }).length;
172
+ } catch {
173
+ }
174
+ }
175
+ const currentDataDirs = Array.isArray(current.dataDir) ? current.dataDir : current.dataDir ? [current.dataDir] : [];
176
+ const normalizedResolved = resolved.replace(/\\/g, "/");
177
+ const existingIdx = currentDataDirs.findIndex(
178
+ (d) => path.resolve(expandHome(d)).replace(/\\/g, "/") === normalizedResolved
179
+ );
180
+ if (existingIdx === -1) {
181
+ currentDataDirs.push(normalizedResolved);
182
+ }
183
+ current.dataDir = currentDataDirs.length === 1 ? currentDataDirs[0] : currentDataDirs;
184
+ writeGlobalConfig(current);
185
+ return { type: "hub", path: normalizedResolved, projectCount };
186
+ }
187
+ const inRepoConfig = path.join(resolved, ".esedre", "esedre.json");
188
+ const inRepoRootConfig = path.join(resolved, "esedre.json");
189
+ const inRepoProjectJson = path.join(resolved, ".esedre", "project.json");
190
+ const inRepoRootProjectJson = path.join(resolved, "project.json");
191
+ let code;
192
+ let name;
193
+ for (const cand of [inRepoConfig, inRepoRootConfig, inRepoProjectJson, inRepoRootProjectJson]) {
194
+ if (fs.existsSync(cand)) {
195
+ try {
196
+ const parsed = JSON.parse(fs.readFileSync(cand, "utf-8"));
197
+ if (parsed.projectCode) code = parsed.projectCode;
198
+ if (parsed.code && !code) code = parsed.code;
199
+ if (parsed.projectName) name = parsed.projectName;
200
+ if (parsed.name && !name) name = parsed.name;
201
+ if (code) break;
202
+ } catch {
203
+ }
204
+ }
205
+ }
206
+ if (code) {
207
+ const normalizedCode = code.toUpperCase();
208
+ const normalizedResolved = resolved.replace(/\\/g, "/");
209
+ if (!current.projects) {
210
+ current.projects = {};
211
+ }
212
+ current.projects[normalizedCode] = normalizedResolved;
213
+ writeGlobalConfig(current);
214
+ return { type: "project", code: normalizedCode, name, path: normalizedResolved };
215
+ }
216
+ return { type: "uninitialized", path: resolved.replace(/\\/g, "/") };
217
+ }
218
+ function removeLocationFromGlobalConfig(identifier) {
219
+ const current = readGlobalConfig();
220
+ if (!current) return { removed: false };
221
+ const trimmed = identifier.trim();
222
+ const normalizedId = trimmed.toUpperCase();
223
+ if (current.projects && current.projects[normalizedId]) {
224
+ const target = current.projects[normalizedId];
225
+ delete current.projects[normalizedId];
226
+ if (Object.keys(current.projects).length === 0) {
227
+ delete current.projects;
228
+ }
229
+ writeGlobalConfig(current);
230
+ return { removed: true, type: "project", target };
231
+ }
232
+ if (current.projects) {
233
+ for (const [code, p] of Object.entries(current.projects)) {
234
+ if (path.resolve(expandHome(p)).toLowerCase() === path.resolve(expandHome(trimmed)).toLowerCase()) {
235
+ delete current.projects[code];
236
+ if (Object.keys(current.projects).length === 0) {
237
+ delete current.projects;
238
+ }
239
+ writeGlobalConfig(current);
240
+ return { removed: true, type: "project", target: p };
241
+ }
242
+ }
243
+ }
244
+ if (current.dataDir) {
245
+ const currentDataDirs = Array.isArray(current.dataDir) ? current.dataDir : [current.dataDir];
246
+ const initialLen = currentDataDirs.length;
247
+ const remaining = currentDataDirs.filter(
248
+ (d) => path.resolve(expandHome(d)).toLowerCase() !== path.resolve(expandHome(trimmed)).toLowerCase()
249
+ );
250
+ if (remaining.length !== initialLen) {
251
+ current.dataDir = remaining.length === 0 ? void 0 : remaining.length === 1 ? remaining[0] : remaining;
252
+ writeGlobalConfig(current);
253
+ return { removed: true, type: "hub", target: trimmed };
254
+ }
255
+ }
256
+ return { removed: false };
257
+ }
258
+ function getGlobalConfigKey(key) {
259
+ const current = readGlobalConfig();
260
+ if (!current) return void 0;
261
+ if (key.includes(".")) {
262
+ const parts = key.split(".");
263
+ let val = current;
264
+ for (const part of parts) {
265
+ if (val === void 0 || val === null) return void 0;
266
+ val = val[part];
267
+ }
268
+ return val;
269
+ }
270
+ return current[key];
271
+ }
272
+ function setGlobalConfigKey(key, value) {
273
+ const current = readGlobalConfig() || { version: CURRENT_ESEDRE_VERSION, port: DEFAULT_ESEDRE_PORT };
274
+ let parsedValue = value;
275
+ if ((key === "port" || key.endsWith(".port") || key.startsWith("ports.")) && typeof value === "string") {
276
+ const num = parseInt(value, 10);
277
+ if (!isNaN(num)) parsedValue = num;
278
+ }
279
+ if (key.includes(".")) {
280
+ const parts = key.split(".");
281
+ let obj = current;
282
+ for (let i = 0; i < parts.length - 1; i++) {
283
+ if (!obj[parts[i]] || typeof obj[parts[i]] !== "object") {
284
+ obj[parts[i]] = {};
285
+ }
286
+ obj = obj[parts[i]];
287
+ }
288
+ obj[parts[parts.length - 1]] = parsedValue;
289
+ } else {
290
+ current[key] = parsedValue;
291
+ }
292
+ writeGlobalConfig(current);
293
+ return { key, value: parsedValue };
294
+ }
295
+ function findEsedreConfig(startDir, options) {
76
296
  let current = path.resolve(startDir || process.cwd());
77
297
  const candidates = [
78
298
  [".esedre", "esedre.json"],
79
299
  ["esedre.json"]
80
300
  ];
301
+ let localConfig = null;
302
+ let localConfigPath = null;
303
+ let localWorkspaceRoot = current;
81
304
  while (true) {
82
305
  for (const segs of candidates) {
83
306
  const candidatePath = path.join(current, ...segs);
84
307
  if (fs.existsSync(candidatePath)) {
85
308
  try {
86
309
  const raw = fs.readFileSync(candidatePath, "utf-8");
87
- const parsed = JSON.parse(raw);
88
- return {
89
- config: parsed,
90
- configPath: candidatePath,
91
- workspaceRoot: current
92
- };
310
+ localConfig = JSON.parse(raw);
311
+ localConfigPath = candidatePath;
312
+ localWorkspaceRoot = current;
313
+ break;
93
314
  } catch (err) {
94
315
  throw new Error(`Failed to parse Esedre configuration at ${candidatePath}: ${err.message}`);
95
316
  }
96
317
  }
97
318
  }
319
+ if (localConfig) break;
98
320
  const parent = path.dirname(current);
99
321
  if (parent === current) {
100
322
  break;
101
323
  }
102
324
  current = parent;
103
325
  }
326
+ const globalConfig = readGlobalConfig();
327
+ if (localConfig) {
328
+ const merged = {
329
+ ...localConfig,
330
+ port: localConfig.port !== void 0 ? localConfig.port : globalConfig?.port || DEFAULT_ESEDRE_PORT,
331
+ ports: localConfig.ports || globalConfig?.ports
332
+ };
333
+ return {
334
+ config: merged,
335
+ configPath: localConfigPath,
336
+ workspaceRoot: localWorkspaceRoot
337
+ };
338
+ }
339
+ if (options?.fallbackToGlobal) {
340
+ let checkDir = path.resolve(startDir || process.cwd());
341
+ let hasInRepoMarkers = false;
342
+ let inRepoRoot = checkDir;
343
+ while (true) {
344
+ const candidateMarkers = [
345
+ path.join(checkDir, ".esedre", "tickets"),
346
+ path.join(checkDir, "tickets"),
347
+ path.join(checkDir, "src", "data", "planning", "tickets"),
348
+ path.join(checkDir, ".esedre", "projects.json"),
349
+ path.join(checkDir, ".esedre", "project.json"),
350
+ path.join(checkDir, "projects.json"),
351
+ path.join(checkDir, "project.json"),
352
+ path.join(checkDir, "src", "data", "planning", "projects.json"),
353
+ path.join(checkDir, "src", "data", "planning", "projects", "projects.json")
354
+ ];
355
+ if (candidateMarkers.some((m) => fs.existsSync(m))) {
356
+ hasInRepoMarkers = true;
357
+ inRepoRoot = checkDir;
358
+ break;
359
+ }
360
+ const parent = path.dirname(checkDir);
361
+ if (parent === checkDir) break;
362
+ checkDir = parent;
363
+ }
364
+ if (!hasInRepoMarkers && globalConfig && (globalConfig.dataDir || globalConfig.projects || globalConfig.port || globalConfig.projectCode)) {
365
+ return {
366
+ config: globalConfig,
367
+ configPath: getGlobalConfigPath(),
368
+ workspaceRoot: process.cwd()
369
+ };
370
+ }
371
+ }
104
372
  return {
105
373
  config: {},
106
374
  configPath: null,
@@ -150,14 +418,16 @@ function appendSnapshotToGitIgnore(workspaceRoot) {
150
418
  return false;
151
419
  }
152
420
  }
153
- function migrateEsedreConfig(raw, targetVersion = "0.1.0") {
421
+ function migrateEsedreConfig(raw, targetVersion = CURRENT_ESEDRE_VERSION) {
154
422
  const migrated = {
155
423
  version: targetVersion,
156
424
  projectCode: raw?.projectCode || void 0,
425
+ projectName: raw?.projectName || void 0,
157
426
  allowedProjects: Array.isArray(raw?.allowedProjects) ? raw.allowedProjects : void 0,
158
427
  dataDir: Array.isArray(raw?.dataDir) ? raw.dataDir : typeof raw?.dataDir === "string" ? raw.dataDir : void 0,
159
428
  serverUrl: raw?.serverUrl,
160
- port: typeof raw?.port === "number" ? raw.port : DEFAULT_ESEDRE_PORT,
429
+ port: typeof raw?.port === "number" && raw.port !== DEFAULT_ESEDRE_PORT ? raw.port : void 0,
430
+ ports: typeof raw?.ports === "object" && raw?.ports ? raw.ports : void 0,
161
431
  projects: typeof raw?.projects === "object" && raw?.projects ? raw.projects : void 0
162
432
  };
163
433
  return migrated;
@@ -239,7 +509,7 @@ async function generateProjectSnapshot(storage, projectCode, outputDir) {
239
509
  });
240
510
  }
241
511
  const snapshot = {
242
- version: "0.1.0",
512
+ version: CURRENT_ESEDRE_VERSION,
243
513
  projectCode,
244
514
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
245
515
  totalTickets: entries.length,
@@ -331,9 +601,28 @@ var FilesystemStorageAdapter = class {
331
601
  resolveProjectLocations() {
332
602
  const locations = [];
333
603
  const seenCodes = /* @__PURE__ */ new Set();
334
- const rawDataDirs = Array.isArray(this.config.dataDir) ? this.config.dataDir : this.config.dataDir ? [this.config.dataDir] : [];
604
+ const rawDataDirs = Array.isArray(this.config.dataDir) ? [...this.config.dataDir] : this.config.dataDir ? [this.config.dataDir] : [];
605
+ if (rawDataDirs.length === 0) {
606
+ const candidateHubDirs = [
607
+ this.workspaceRoot,
608
+ path3.join(this.workspaceRoot, "data"),
609
+ path3.join(this.workspaceRoot, ".esedre")
610
+ ];
611
+ for (const cand of candidateHubDirs) {
612
+ const pSub = path3.join(cand, "projects");
613
+ if (fs3.existsSync(pSub)) {
614
+ try {
615
+ if (fs3.statSync(pSub).isDirectory()) {
616
+ rawDataDirs.push(cand);
617
+ break;
618
+ }
619
+ } catch {
620
+ }
621
+ }
622
+ }
623
+ }
335
624
  for (const dDir of rawDataDirs) {
336
- const resolvedHub = path3.resolve(this.workspaceRoot, dDir);
625
+ const resolvedHub = path3.resolve(this.workspaceRoot, expandHome(dDir));
337
626
  if (!fs3.existsSync(resolvedHub)) continue;
338
627
  const projectsSubDir = path3.join(resolvedHub, "projects");
339
628
  if (fs3.existsSync(projectsSubDir)) {
@@ -351,6 +640,9 @@ var FilesystemStorageAdapter = class {
351
640
  if (fs3.existsSync(pJsonPath)) {
352
641
  try {
353
642
  desc = JSON.parse(fs3.readFileSync(pJsonPath, "utf-8"));
643
+ if (desc && !desc.slug) {
644
+ desc.slug = (desc.code || ent.name).toLowerCase();
645
+ }
354
646
  } catch {
355
647
  }
356
648
  }
@@ -436,7 +728,7 @@ var FilesystemStorageAdapter = class {
436
728
  }
437
729
  if (this.config.projects && typeof this.config.projects === "object") {
438
730
  for (const [code, rawPath] of Object.entries(this.config.projects)) {
439
- const projectDir = path3.resolve(this.workspaceRoot, rawPath);
731
+ const projectDir = path3.resolve(this.workspaceRoot, expandHome(rawPath));
440
732
  if (!fs3.existsSync(projectDir)) continue;
441
733
  let actualTicketsDir = path3.join(projectDir, "tickets");
442
734
  if (fs3.existsSync(path3.join(projectDir, ".esedre", "tickets"))) {
@@ -455,6 +747,9 @@ var FilesystemStorageAdapter = class {
455
747
  if (fs3.existsSync(cand)) {
456
748
  try {
457
749
  desc = JSON.parse(fs3.readFileSync(cand, "utf-8"));
750
+ if (desc && !desc.slug) {
751
+ desc.slug = (desc.code || code).toLowerCase();
752
+ }
458
753
  break;
459
754
  } catch {
460
755
  }
@@ -486,7 +781,14 @@ var FilesystemStorageAdapter = class {
486
781
  path3.join(this.workspaceRoot, "tickets"),
487
782
  path3.join(this.workspaceRoot, "src", "data", "planning", "tickets")
488
783
  ];
489
- const foundTicketsDir = candidateDirs.find((d) => fs3.existsSync(d)) || path3.join(this.workspaceRoot, ".esedre", "tickets");
784
+ const existingDirs = candidateDirs.filter((d) => fs3.existsSync(d));
785
+ const foundTicketsDir = existingDirs.find((d) => {
786
+ try {
787
+ return fs3.readdirSync(d).length > 0;
788
+ } catch {
789
+ return false;
790
+ }
791
+ }) || existingDirs[0] || path3.join(this.workspaceRoot, ".esedre", "tickets");
490
792
  let descs = [];
491
793
  const pJsonCandidates = [
492
794
  path3.join(this.workspaceRoot, ".esedre", "projects.json"),
@@ -541,7 +843,7 @@ var FilesystemStorageAdapter = class {
541
843
  }
542
844
  async registerProject(input) {
543
845
  const rawCode = input.code.trim();
544
- const cleanCode = rawCode.toUpperCase();
846
+ const cleanCode = rawCode;
545
847
  const cleanName = input.name?.trim() || cleanCode;
546
848
  const cleanDesc = input.description?.trim() || `${cleanName} project`;
547
849
  const slug = cleanCode.toLowerCase();
@@ -550,10 +852,29 @@ var FilesystemStorageAdapter = class {
550
852
  dot: "bg-cyan-400",
551
853
  border: "border-cyan-500/40"
552
854
  };
553
- const rawDataDirs = Array.isArray(this.config.dataDir) ? this.config.dataDir : this.config.dataDir ? [this.config.dataDir] : [];
855
+ const rawDataDirs = Array.isArray(this.config.dataDir) ? [...this.config.dataDir] : this.config.dataDir ? [this.config.dataDir] : [];
856
+ if (rawDataDirs.length === 0) {
857
+ const candidateHubDirs = [
858
+ this.workspaceRoot,
859
+ path3.join(this.workspaceRoot, "data"),
860
+ path3.join(this.workspaceRoot, ".esedre")
861
+ ];
862
+ for (const cand of candidateHubDirs) {
863
+ const pSub = path3.join(cand, "projects");
864
+ if (fs3.existsSync(pSub)) {
865
+ try {
866
+ if (fs3.statSync(pSub).isDirectory()) {
867
+ rawDataDirs.push(cand);
868
+ break;
869
+ }
870
+ } catch {
871
+ }
872
+ }
873
+ }
874
+ }
554
875
  let targetHub = null;
555
876
  for (const dDir of rawDataDirs) {
556
- const resolved = path3.resolve(this.workspaceRoot, dDir);
877
+ const resolved = path3.resolve(this.workspaceRoot, expandHome(dDir));
557
878
  if (fs3.existsSync(resolved)) {
558
879
  targetHub = resolved;
559
880
  break;
@@ -574,7 +895,9 @@ var FilesystemStorageAdapter = class {
574
895
  } catch {
575
896
  }
576
897
  }
577
- const id2 = existingPJson2?.id || (await this.getProjects()).length + 1;
898
+ const existingProjects = await this.getProjects();
899
+ const maxId = existingProjects.reduce((max, p) => Math.max(max, p.id || 0), 0);
900
+ const id2 = existingPJson2?.id || maxId + 1;
578
901
  const projectDesc2 = {
579
902
  id: id2,
580
903
  code: cleanCode,
@@ -643,7 +966,7 @@ var FilesystemStorageAdapter = class {
643
966
  const locations = this.resolveProjectLocations();
644
967
  if (locations.length === 0) return null;
645
968
  const strId = String(id).trim();
646
- const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,6})[-_:](\d+)$/i);
969
+ const prefixMatch = strId.match(/^([a-zA-Z0-9]{1,7})[-_:](\d+)$/i);
647
970
  if (prefixMatch) {
648
971
  const code = prefixMatch[1].toLowerCase();
649
972
  const num2 = parseInt(prefixMatch[2], 10);
@@ -926,7 +1249,7 @@ var FilesystemStorageAdapter = class {
926
1249
  if (requestedIdentifier) {
927
1250
  const lower = String(requestedIdentifier).trim().toLowerCase();
928
1251
  targetLoc = locations.find(
929
- (l) => l.project.code.toLowerCase() === lower || l.project.slug.toLowerCase() === lower || String(l.project.id) === lower || l.project.name.toLowerCase() === lower
1252
+ (l) => l.project.code?.toLowerCase() === lower || l.project.slug?.toLowerCase() === lower || String(l.project.id) === lower || l.project.name?.toLowerCase() === lower
930
1253
  );
931
1254
  if (!targetLoc) {
932
1255
  throw new Error(`Project '${requestedIdentifier}' is invalid or not registered in this workspace.`);
@@ -1222,22 +1545,22 @@ function formatTicketListTable(tickets) {
1222
1545
  const rows = tickets.map((t) => {
1223
1546
  const id = t.projectDescriptor?.code ? `${t.projectDescriptor.code}-${t.meta.id}` : `#${t.meta.id}`;
1224
1547
  const project = t.projectDescriptor?.code || "CORE";
1225
- const category = t.meta.category;
1548
+ const type = t.meta.type || t.meta.category || "Feature";
1226
1549
  const status = t.meta.status;
1227
1550
  const title = t.meta.title;
1228
- return { id, project, category, status, title };
1551
+ return { id, project, type, status, title };
1229
1552
  });
1230
1553
  const idWidth = Math.max(4, ...rows.map((r) => r.id.length));
1231
1554
  const projWidth = Math.max(7, ...rows.map((r) => r.project.length));
1232
- const catWidth = Math.max(8, ...rows.map((r) => r.category.length));
1555
+ const typeWidth = Math.max(8, ...rows.map((r) => r.type.length));
1233
1556
  const statusWidth = Math.max(14, ...rows.map((r) => r.status.length));
1234
- const header = `${colors.bold}${pad("ID", idWidth)} ${pad("Project", projWidth)} ${pad("Category", catWidth)} ${pad("Status", statusWidth)} Title${colors.reset}`;
1235
- const divider = `${colors.dim}${"-".repeat(idWidth)} ${"-".repeat(projWidth)} ${"-".repeat(catWidth)} ${"-".repeat(statusWidth)} ${"-".repeat(40)}${colors.reset}`;
1557
+ const header = `${colors.bold}${pad("ID", idWidth)} ${pad("Project", projWidth)} ${pad("Type", typeWidth)} ${pad("Status", statusWidth)} Title${colors.reset}`;
1558
+ const divider = `${colors.dim}${"-".repeat(idWidth)} ${"-".repeat(projWidth)} ${"-".repeat(typeWidth)} ${"-".repeat(statusWidth)} ${"-".repeat(40)}${colors.reset}`;
1236
1559
  const formattedRows = rows.map((r) => {
1237
1560
  const statusColored = colorStatus(r.status);
1238
- const catColored = colorCategory(r.category);
1561
+ const typeColored = colorType(r.type);
1239
1562
  const projColored = `${colors.magenta}${r.project}${colors.reset}`;
1240
- return `${colors.bold}${pad(r.id, idWidth)}${colors.reset} ${pad(projColored, projWidth + (isColorSupported ? colors.magenta.length + colors.reset.length : 0))} ${pad(catColored, catWidth + (isColorSupported ? 9 : 0))} ${pad(statusColored, statusWidth + (isColorSupported ? 9 : 0))} ${r.title}`;
1563
+ return `${colors.bold}${pad(r.id, idWidth)}${colors.reset} ${pad(projColored, projWidth + (isColorSupported ? colors.magenta.length + colors.reset.length : 0))} ${pad(typeColored, typeWidth + (isColorSupported ? 9 : 0))} ${pad(statusColored, statusWidth + (isColorSupported ? 9 : 0))} ${r.title}`;
1241
1564
  });
1242
1565
  return [header, divider, ...formattedRows].join("\n");
1243
1566
  }
@@ -1247,7 +1570,7 @@ function formatTicketDetail(ticket) {
1247
1570
  lines.push(`${colors.bold}${colors.cyan}Ticket #${meta.project ? `${meta.project}-${meta.id}` : meta.id}: ${meta.title}${colors.reset}`);
1248
1571
  lines.push(`${colors.dim}${"=".repeat(60)}${colors.reset}`);
1249
1572
  lines.push(`${colors.bold}Project:${colors.reset} ${projectDescriptor ? `${projectDescriptor.code} - ${projectDescriptor.name}` : "Default"}`);
1250
- lines.push(`${colors.bold}Category:${colors.reset} ${colorCategory(meta.category)}`);
1573
+ lines.push(`${colors.bold}Type:${colors.reset} ${colorType(meta.type || meta.category || "Feature")}`);
1251
1574
  lines.push(`${colors.bold}Status:${colors.reset} ${colorStatus(meta.status)}`);
1252
1575
  lines.push(`${colors.bold}Complexity:${colors.reset} ${meta.complexity || "Medium"}`);
1253
1576
  lines.push(`${colors.bold}Effort:${colors.reset} ${meta.estimatedEffort || "N/A"}`);
@@ -1281,9 +1604,12 @@ function formatTicketDetail(ticket) {
1281
1604
  lines.push(` \u2022 ${q}`);
1282
1605
  }
1283
1606
  }
1284
- lines.push("");
1285
- lines.push(`${colors.bold}Implementation Plan:${colors.reset} ${planMarkdown ? `${colors.green}Available (${planMarkdown.split("\n").length} lines)${colors.reset}` : `${colors.dim}None recorded${colors.reset}`}`);
1286
- if (comments.length > 0) {
1607
+ if (planMarkdown) {
1608
+ lines.push("");
1609
+ lines.push(`${colors.bold}Implementation Plan:${colors.reset}`);
1610
+ lines.push(planMarkdown.trim());
1611
+ }
1612
+ if (comments && comments.length > 0) {
1287
1613
  lines.push("");
1288
1614
  lines.push(`${colors.bold}Comments (${comments.length}):${colors.reset}`);
1289
1615
  for (const c of comments) {
@@ -1307,20 +1633,20 @@ function colorStatus(status) {
1307
1633
  return `${colors.cyan}${status}${colors.reset}`;
1308
1634
  }
1309
1635
  }
1310
- function colorCategory(category) {
1311
- switch (category) {
1636
+ function colorType(type) {
1637
+ switch (type) {
1312
1638
  case "Feature":
1313
- return `${colors.blue}${category}${colors.reset}`;
1639
+ return `${colors.blue}${type}${colors.reset}`;
1314
1640
  case "Platform":
1315
- return `${colors.magenta}${category}${colors.reset}`;
1641
+ return `${colors.magenta}${type}${colors.reset}`;
1316
1642
  case "Tools":
1317
- return `${colors.yellow}${category}${colors.reset}`;
1643
+ return `${colors.yellow}${type}${colors.reset}`;
1318
1644
  case "Bug":
1319
- return `${colors.red}${category}${colors.reset}`;
1645
+ return `${colors.red}${type}${colors.reset}`;
1320
1646
  case "Idea":
1321
- return `${colors.green}${category}${colors.reset}`;
1647
+ return `${colors.green}${type}${colors.reset}`;
1322
1648
  default:
1323
- return category;
1649
+ return type;
1324
1650
  }
1325
1651
  }
1326
1652
 
@@ -1522,17 +1848,26 @@ Status: ${ticket.meta.status}`;
1522
1848
  }
1523
1849
  }
1524
1850
  }
1851
+ parseTicketId(val) {
1852
+ if (typeof val === "number") return val;
1853
+ const str = String(val).trim();
1854
+ if (/^\d+$/.test(str)) {
1855
+ return parseInt(str, 10);
1856
+ }
1857
+ return str;
1858
+ }
1525
1859
  getToolDefinitions() {
1526
1860
  return [
1527
1861
  {
1528
1862
  name: "esedre_list_tickets",
1529
- description: "List roadmap tickets with optional project code, status, category, or search query.",
1863
+ description: "List roadmap tickets with optional project code, status, type, category, or search query.",
1530
1864
  inputSchema: {
1531
1865
  type: "object",
1532
1866
  properties: {
1533
1867
  project: { type: "string", description: "Project code (e.g. CORE, WEB, DOCS)" },
1534
1868
  status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"], description: "Ticket status" },
1535
- category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category" },
1869
+ type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
1870
+ category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category (legacy alias for type)" },
1536
1871
  search: { type: "string", description: "Search keywords in title or ID" }
1537
1872
  }
1538
1873
  }
@@ -1543,7 +1878,7 @@ Status: ${ticket.meta.status}`;
1543
1878
  inputSchema: {
1544
1879
  type: "object",
1545
1880
  properties: {
1546
- ticketId: { type: "integer", description: "Numeric ticket ID (e.g. 96, 101)" }
1881
+ ticketId: { type: ["integer", "string"], description: "Numeric ticket ID (e.g. 96, 101) or compound key (e.g. Profe-96)" }
1547
1882
  },
1548
1883
  required: ["ticketId"]
1549
1884
  }
@@ -1554,7 +1889,7 @@ Status: ${ticket.meta.status}`;
1554
1889
  inputSchema: {
1555
1890
  type: "object",
1556
1891
  properties: {
1557
- ticketId: { type: "integer", description: "Numeric ticket ID" }
1892
+ ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" }
1558
1893
  },
1559
1894
  required: ["ticketId"]
1560
1895
  }
@@ -1565,7 +1900,7 @@ Status: ${ticket.meta.status}`;
1565
1900
  inputSchema: {
1566
1901
  type: "object",
1567
1902
  properties: {
1568
- ticketId: { type: "integer", description: "Numeric ticket ID" },
1903
+ ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
1569
1904
  planMarkdown: { type: "string", description: "Implementation plan markdown content" },
1570
1905
  lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
1571
1906
  },
@@ -1580,24 +1915,29 @@ Status: ${ticket.meta.status}`;
1580
1915
  properties: {
1581
1916
  title: { type: "string", description: "Ticket title (max 48 characters)" },
1582
1917
  project: { type: "string", description: "Target project code (e.g. CORE, WEB, DOCS)" },
1918
+ type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket type" },
1583
1919
  category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"], description: "Ticket category" },
1584
1920
  complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
1585
- effort: { type: "string", description: "Estimated effort (e.g. 2.0 \u2013 4.0 hours)" },
1921
+ effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
1586
1922
  summary: { type: "string", description: "Initial feature summary" },
1587
1923
  author: { type: "string", description: "Submitting author name" }
1588
1924
  },
1589
- required: ["title", "category"]
1925
+ required: ["title"]
1590
1926
  }
1591
1927
  },
1592
1928
  {
1593
1929
  name: "esedre_update_ticket",
1594
- description: "Update an existing ticket state (status, title, active planning flag, or feature flag).",
1930
+ description: "Update an existing ticket state (status, type, title, complexity, effort, active planning flag, or feature flag).",
1595
1931
  inputSchema: {
1596
1932
  type: "object",
1597
1933
  properties: {
1598
- ticketId: { type: "integer", description: "Numeric ticket ID" },
1934
+ ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
1599
1935
  status: { type: "string", enum: ["Planned", "In Development", "Completed", "Rejected"] },
1936
+ type: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
1937
+ category: { type: "string", enum: ["Feature", "Platform", "Tools", "Idea", "Bug"] },
1600
1938
  title: { type: "string", description: "New title (max 48 characters)" },
1939
+ complexity: { type: "string", description: "Complexity (e.g. Low, Medium, High)" },
1940
+ effort: { type: "string", description: "Estimated effort (e.g. 2.0 - 4.0 hours)" },
1601
1941
  inDevelopment: { type: "boolean", description: "Active development toggle" },
1602
1942
  featureFlag: { type: "string", description: "Feature flag name" },
1603
1943
  lastHash: { type: "string", description: "Optimistic concurrency control: last known sha1 hash of the ticket" }
@@ -1607,11 +1947,11 @@ Status: ${ticket.meta.status}`;
1607
1947
  },
1608
1948
  {
1609
1949
  name: "esedre_add_comment",
1610
- description: "Append a developer or companion agent comment to a ticket.",
1950
+ description: "Append a developer or LLM agent comment to a ticket.",
1611
1951
  inputSchema: {
1612
1952
  type: "object",
1613
1953
  properties: {
1614
- ticketId: { type: "integer", description: "Numeric ticket ID" },
1954
+ ticketId: { type: ["integer", "string"], description: "Numeric ticket ID or compound key" },
1615
1955
  text: { type: "string", description: "Comment message" },
1616
1956
  author: { type: "string", description: "Author name (e.g. Antigravity, Developer)" }
1617
1957
  },
@@ -1624,16 +1964,19 @@ Status: ${ticket.meta.status}`;
1624
1964
  const canonical = name.replace(/^esedre_/, "");
1625
1965
  switch (canonical) {
1626
1966
  case "list_tickets": {
1967
+ const type = args.type || args.category;
1627
1968
  const tickets = await this.storage.listTickets({
1628
1969
  project: args.project,
1629
1970
  status: args.status,
1630
- category: args.category,
1971
+ type,
1972
+ category: type,
1631
1973
  search: args.search
1632
1974
  });
1633
1975
  return tickets.map((t) => ({
1634
1976
  id: t.meta.id,
1635
1977
  title: t.meta.title,
1636
- category: t.meta.category,
1978
+ type: t.meta.type || t.meta.category,
1979
+ category: t.meta.type || t.meta.category,
1637
1980
  status: t.meta.status,
1638
1981
  complexity: t.meta.complexity,
1639
1982
  effort: t.meta.estimatedEffort,
@@ -1642,26 +1985,27 @@ Status: ${ticket.meta.status}`;
1642
1985
  }));
1643
1986
  }
1644
1987
  case "get_ticket": {
1645
- const id = parseInt(String(args.ticketId), 10);
1988
+ const id = this.parseTicketId(args.ticketId);
1646
1989
  const ticket = await this.storage.getTicket(id);
1647
1990
  if (!ticket) throw new Error(`Ticket #${id} not found`);
1648
1991
  return ticket;
1649
1992
  }
1650
1993
  case "get_plan": {
1651
- const id = parseInt(String(args.ticketId), 10);
1994
+ const id = this.parseTicketId(args.ticketId);
1652
1995
  const plan = await this.storage.getPlan(id);
1653
1996
  return { ticketId: id, planMarkdown: plan || null };
1654
1997
  }
1655
1998
  case "save_plan": {
1656
- const id = parseInt(String(args.ticketId), 10);
1999
+ const id = this.parseTicketId(args.ticketId);
1657
2000
  await this.storage.savePlan(id, args.planMarkdown, args.lastHash);
1658
2001
  return { success: true, message: `Implementation plan saved for Ticket #${id}` };
1659
2002
  }
1660
2003
  case "create_ticket": {
2004
+ const type = args.type || args.category || "Feature";
1661
2005
  const created = await this.storage.createTicket({
1662
2006
  title: args.title,
1663
- type: args.type || args.category,
1664
- category: args.type || args.category,
2007
+ type,
2008
+ category: type,
1665
2009
  projectCode: args.project,
1666
2010
  complexity: args.complexity,
1667
2011
  estimatedEffort: args.effort,
@@ -1671,17 +2015,24 @@ Status: ${ticket.meta.status}`;
1671
2015
  return created;
1672
2016
  }
1673
2017
  case "update_ticket": {
1674
- const id = parseInt(String(args.ticketId), 10);
2018
+ const id = this.parseTicketId(args.ticketId);
1675
2019
  const updates = {};
2020
+ const type = args.type || args.category;
2021
+ if (type) {
2022
+ updates.type = type;
2023
+ updates.category = type;
2024
+ }
1676
2025
  if (args.status) updates.status = args.status;
1677
2026
  if (args.title) updates.title = args.title;
2027
+ if (args.complexity) updates.complexity = args.complexity;
2028
+ if (args.effort) updates.estimatedEffort = args.effort;
1678
2029
  if (args.inDevelopment !== void 0) updates.isActivePlanning = Boolean(args.inDevelopment);
1679
2030
  if (args.featureFlag) updates.featureFlag = args.featureFlag;
1680
2031
  const updated = await this.storage.updateTicket(id, updates, args.lastHash);
1681
2032
  return updated;
1682
2033
  }
1683
2034
  case "add_comment": {
1684
- const id = parseInt(String(args.ticketId), 10);
2035
+ const id = this.parseTicketId(args.ticketId);
1685
2036
  const comment = await this.storage.addComment(id, {
1686
2037
  text: args.text,
1687
2038
  author: args.author || "Agent"
@@ -1694,47 +2045,235 @@ Status: ${ticket.meta.status}`;
1694
2045
  }
1695
2046
  };
1696
2047
 
2048
+ // src/utils/manifestDetector.ts
2049
+ import fs4 from "node:fs";
2050
+ import path4 from "node:path";
2051
+ function sanitizeCode(input) {
2052
+ const clean = input.replace(/[^a-zA-Z0-9]/g, "");
2053
+ return clean.slice(0, MAX_PROJECT_CODE_LENGTH) || "APP";
2054
+ }
2055
+ function detectProjectManifest(targetDir) {
2056
+ const dirBase = path4.basename(targetDir);
2057
+ for (const settingsFile of ["settings.gradle.kts", "settings.gradle"]) {
2058
+ const fullPath = path4.join(targetDir, settingsFile);
2059
+ if (fs4.existsSync(fullPath)) {
2060
+ try {
2061
+ const content = fs4.readFileSync(fullPath, "utf-8");
2062
+ const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
2063
+ if (match && match[1].trim()) {
2064
+ const rawName = match[1].trim();
2065
+ return {
2066
+ candidateCode: sanitizeCode(rawName),
2067
+ candidateName: rawName,
2068
+ manifestType: "gradle",
2069
+ manifestFile: settingsFile
2070
+ };
2071
+ }
2072
+ } catch {
2073
+ }
2074
+ }
2075
+ }
2076
+ for (const buildFile of [
2077
+ path4.join("app", "build.gradle.kts"),
2078
+ "build.gradle.kts",
2079
+ path4.join("app", "build.gradle"),
2080
+ "build.gradle"
2081
+ ]) {
2082
+ const fullPath = path4.join(targetDir, buildFile);
2083
+ if (fs4.existsSync(fullPath)) {
2084
+ try {
2085
+ const content = fs4.readFileSync(fullPath, "utf-8");
2086
+ const match = content.match(/(?:namespace|applicationId)\s*=\s*["']([^"']+)["']/);
2087
+ if (match && match[1].trim()) {
2088
+ const ns = match[1].trim();
2089
+ const lastSegment = ns.split(".").pop() || dirBase;
2090
+ const candidateName = lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
2091
+ return {
2092
+ candidateCode: sanitizeCode(candidateName),
2093
+ candidateName,
2094
+ manifestType: "gradle",
2095
+ manifestFile: buildFile
2096
+ };
2097
+ }
2098
+ } catch {
2099
+ }
2100
+ }
2101
+ }
2102
+ const pomPath = path4.join(targetDir, "pom.xml");
2103
+ if (fs4.existsSync(pomPath)) {
2104
+ try {
2105
+ const content = fs4.readFileSync(pomPath, "utf-8");
2106
+ const stripped = content.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, "");
2107
+ const nameMatch = stripped.match(/<name>\s*([^<]+)\s*<\/name>/);
2108
+ const artifactMatch = stripped.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/) || content.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/);
2109
+ let rawName = nameMatch?.[1]?.trim();
2110
+ if (!rawName || rawName.startsWith("${")) {
2111
+ rawName = artifactMatch?.[1]?.trim();
2112
+ }
2113
+ if (rawName) {
2114
+ return {
2115
+ candidateCode: sanitizeCode(rawName),
2116
+ candidateName: rawName,
2117
+ manifestType: "maven",
2118
+ manifestFile: "pom.xml"
2119
+ };
2120
+ }
2121
+ } catch {
2122
+ }
2123
+ }
2124
+ const pkgPath = path4.join(targetDir, "package.json");
2125
+ if (fs4.existsSync(pkgPath)) {
2126
+ try {
2127
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
2128
+ if (pkg.name && typeof pkg.name === "string") {
2129
+ const baseName = pkg.name.replace(/^@[\w-]+\//, "");
2130
+ return {
2131
+ candidateCode: sanitizeCode(baseName),
2132
+ candidateName: baseName,
2133
+ manifestType: "node",
2134
+ manifestFile: "package.json"
2135
+ };
2136
+ }
2137
+ } catch {
2138
+ }
2139
+ }
2140
+ const cargoPath = path4.join(targetDir, "Cargo.toml");
2141
+ if (fs4.existsSync(cargoPath)) {
2142
+ try {
2143
+ const content = fs4.readFileSync(cargoPath, "utf-8");
2144
+ const match = content.match(/\[package\][\s\S]*?name\s*=\s*["']([^"']+)["']/);
2145
+ if (match && match[1].trim()) {
2146
+ const rawName = match[1].trim();
2147
+ return {
2148
+ candidateCode: sanitizeCode(rawName),
2149
+ candidateName: rawName,
2150
+ manifestType: "rust",
2151
+ manifestFile: "Cargo.toml"
2152
+ };
2153
+ }
2154
+ } catch {
2155
+ }
2156
+ }
2157
+ const pyprojectPath = path4.join(targetDir, "pyproject.toml");
2158
+ if (fs4.existsSync(pyprojectPath)) {
2159
+ try {
2160
+ const content = fs4.readFileSync(pyprojectPath, "utf-8");
2161
+ const match = content.match(/name\s*=\s*["']([^"']+)["']/);
2162
+ if (match && match[1].trim()) {
2163
+ const rawName = match[1].trim();
2164
+ return {
2165
+ candidateCode: sanitizeCode(rawName),
2166
+ candidateName: rawName,
2167
+ manifestType: "python",
2168
+ manifestFile: "pyproject.toml"
2169
+ };
2170
+ }
2171
+ } catch {
2172
+ }
2173
+ }
2174
+ const setupCfgPath = path4.join(targetDir, "setup.cfg");
2175
+ if (fs4.existsSync(setupCfgPath)) {
2176
+ try {
2177
+ const content = fs4.readFileSync(setupCfgPath, "utf-8");
2178
+ const match = content.match(/^name\s*=\s*([^\r\n]+)/m);
2179
+ if (match && match[1].trim()) {
2180
+ const rawName = match[1].trim();
2181
+ return {
2182
+ candidateCode: sanitizeCode(rawName),
2183
+ candidateName: rawName,
2184
+ manifestType: "python",
2185
+ manifestFile: "setup.cfg"
2186
+ };
2187
+ }
2188
+ } catch {
2189
+ }
2190
+ }
2191
+ const setupPyPath = path4.join(targetDir, "setup.py");
2192
+ if (fs4.existsSync(setupPyPath)) {
2193
+ try {
2194
+ const content = fs4.readFileSync(setupPyPath, "utf-8");
2195
+ const match = content.match(/name\s*=\s*["']([^"']+)["']/);
2196
+ if (match && match[1].trim()) {
2197
+ const rawName = match[1].trim();
2198
+ return {
2199
+ candidateCode: sanitizeCode(rawName),
2200
+ candidateName: rawName,
2201
+ manifestType: "python",
2202
+ manifestFile: "setup.py"
2203
+ };
2204
+ }
2205
+ } catch {
2206
+ }
2207
+ }
2208
+ const goModPath = path4.join(targetDir, "go.mod");
2209
+ if (fs4.existsSync(goModPath)) {
2210
+ try {
2211
+ const content = fs4.readFileSync(goModPath, "utf-8");
2212
+ const match = content.match(/^module\s+([^\r\n\s]+)/m);
2213
+ if (match && match[1].trim()) {
2214
+ const modPath = match[1].trim();
2215
+ const baseName = modPath.split("/").pop() || dirBase;
2216
+ return {
2217
+ candidateCode: sanitizeCode(baseName),
2218
+ candidateName: baseName,
2219
+ manifestType: "go",
2220
+ manifestFile: "go.mod"
2221
+ };
2222
+ }
2223
+ } catch {
2224
+ }
2225
+ }
2226
+ return {
2227
+ candidateCode: sanitizeCode(dirBase),
2228
+ candidateName: dirBase,
2229
+ manifestType: "directory"
2230
+ };
2231
+ }
2232
+
1697
2233
  // src/upgrade.ts
1698
2234
  import crypto2 from "node:crypto";
1699
- import fs5 from "node:fs";
1700
- import path5 from "node:path";
2235
+ import fs6 from "node:fs";
2236
+ import path6 from "node:path";
1701
2237
 
1702
2238
  // src/server/daemon.ts
1703
- import fs4 from "node:fs";
1704
- import path4 from "node:path";
1705
- import os from "node:os";
2239
+ import fs5 from "node:fs";
2240
+ import path5 from "node:path";
2241
+ import os2 from "node:os";
1706
2242
  import http from "node:http";
1707
2243
  import { spawn, spawnSync } from "node:child_process";
1708
2244
  import { fileURLToPath } from "node:url";
1709
2245
  var __filename = fileURLToPath(import.meta.url);
1710
- var __dirname = path4.dirname(__filename);
2246
+ var __dirname = path5.dirname(__filename);
1711
2247
  function getGlobalEsedreDir() {
1712
- return path4.join(os.homedir(), ".esedre");
2248
+ if (process.env.ESEDRE_GLOBAL_DIR) {
2249
+ return process.env.ESEDRE_GLOBAL_DIR;
2250
+ }
2251
+ return path5.join(os2.homedir(), ".esedre");
1713
2252
  }
1714
2253
  function ensureGlobalEsedreStore() {
1715
2254
  const globalDir = getGlobalEsedreDir();
1716
- const runDir = path4.join(globalDir, "run");
1717
- const logsDir = path4.join(globalDir, "logs");
2255
+ const runDir = path5.join(globalDir, "run");
2256
+ const logsDir = path5.join(globalDir, "logs");
1718
2257
  try {
1719
- fs4.mkdirSync(runDir, { recursive: true });
1720
- fs4.mkdirSync(logsDir, { recursive: true });
2258
+ fs5.mkdirSync(runDir, { recursive: true });
2259
+ fs5.mkdirSync(logsDir, { recursive: true });
1721
2260
  try {
1722
- const runEntries = fs4.readdirSync(runDir);
2261
+ const runEntries = fs5.readdirSync(runDir);
1723
2262
  for (const entry of runEntries) {
1724
2263
  if (entry.startsWith(".probe-")) {
1725
- fs4.unlinkSync(path4.join(runDir, entry));
2264
+ fs5.unlinkSync(path5.join(runDir, entry));
1726
2265
  }
1727
2266
  }
1728
2267
  } catch {
1729
2268
  }
1730
- const testProbe = path4.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
2269
+ const testProbe = path5.join(runDir, `.probe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
1731
2270
  try {
1732
- fs4.writeFileSync(testProbe, "ok", "utf-8");
2271
+ fs5.writeFileSync(testProbe, "ok", "utf-8");
1733
2272
  return { ok: true, path: globalDir };
1734
2273
  } finally {
1735
- if (fs4.existsSync(testProbe)) {
2274
+ if (fs5.existsSync(testProbe)) {
1736
2275
  try {
1737
- fs4.unlinkSync(testProbe);
2276
+ fs5.unlinkSync(testProbe);
1738
2277
  } catch {
1739
2278
  }
1740
2279
  }
@@ -1749,10 +2288,10 @@ function resolveStoreDir(workspaceRoot) {
1749
2288
  return globalStore.path;
1750
2289
  }
1751
2290
  if (workspaceRoot) {
1752
- const localStore = path4.join(workspaceRoot, ".esedre");
2291
+ const localStore = path5.join(workspaceRoot, ".esedre");
1753
2292
  try {
1754
- fs4.mkdirSync(path4.join(localStore, "run"), { recursive: true });
1755
- fs4.mkdirSync(path4.join(localStore, "logs"), { recursive: true });
2293
+ fs5.mkdirSync(path5.join(localStore, "run"), { recursive: true });
2294
+ fs5.mkdirSync(path5.join(localStore, "logs"), { recursive: true });
1756
2295
  return localStore;
1757
2296
  } catch {
1758
2297
  }
@@ -1761,32 +2300,32 @@ function resolveStoreDir(workspaceRoot) {
1761
2300
  }
1762
2301
  function getDaemonStateFile(port, workspaceRoot) {
1763
2302
  const store = resolveStoreDir(workspaceRoot);
1764
- return path4.join(store, "run", `daemon-${port}.json`);
2303
+ return path5.join(store, "run", `daemon-${port}.json`);
1765
2304
  }
1766
2305
  function getDaemonLogFile(port, workspaceRoot) {
1767
2306
  const store = resolveStoreDir(workspaceRoot);
1768
- return path4.join(store, "logs", `daemon-${port}.log`);
2307
+ return path5.join(store, "logs", `daemon-${port}.log`);
1769
2308
  }
1770
2309
  function getDaemonState(port, workspaceRoot) {
1771
2310
  const stateFile = getDaemonStateFile(port, workspaceRoot);
1772
- if (!fs4.existsSync(stateFile)) {
2311
+ if (!fs5.existsSync(stateFile)) {
1773
2312
  return null;
1774
2313
  }
1775
2314
  try {
1776
- const raw = fs4.readFileSync(stateFile, "utf-8").trim();
2315
+ const raw = fs5.readFileSync(stateFile, "utf-8").trim();
1777
2316
  if (!raw) {
1778
- fs4.unlinkSync(stateFile);
2317
+ fs5.unlinkSync(stateFile);
1779
2318
  return null;
1780
2319
  }
1781
2320
  const state = JSON.parse(raw);
1782
2321
  if (!state || typeof state.pid !== "number" || typeof state.port !== "number") {
1783
- fs4.unlinkSync(stateFile);
2322
+ fs5.unlinkSync(stateFile);
1784
2323
  return null;
1785
2324
  }
1786
2325
  return state;
1787
2326
  } catch {
1788
2327
  try {
1789
- fs4.unlinkSync(stateFile);
2328
+ fs5.unlinkSync(stateFile);
1790
2329
  } catch {
1791
2330
  }
1792
2331
  return null;
@@ -1838,7 +2377,7 @@ function pingDaemon(port, timeoutMs = 200) {
1838
2377
  });
1839
2378
  }
1840
2379
  async function startDaemon(options = {}) {
1841
- const port = options.port || 5674;
2380
+ const port = resolvePorts(void 0, { port: options.port }).gateway;
1842
2381
  const workspaceRoot = options.workspaceRoot || process.cwd();
1843
2382
  const stateFile = getDaemonStateFile(port, workspaceRoot);
1844
2383
  const logFile = getDaemonLogFile(port, workspaceRoot);
@@ -1849,12 +2388,15 @@ async function startDaemon(options = {}) {
1849
2388
  if (ping.responding && ping.isEsedre) {
1850
2389
  if (!options.quiet) {
1851
2390
  console.log(`\x1B[36m\u26A1 Esedre daemon already running on http://localhost:${port} (PID ${existingState.pid})\x1B[0m`);
2391
+ if (existingState.version && existingState.version !== CURRENT_ESEDRE_VERSION) {
2392
+ console.log(`\x1B[33m\u26A0\uFE0F Warning: Daemon is running v${existingState.version}, but v${CURRENT_ESEDRE_VERSION} is installed. Run 'ese stop && ese start' to reload.\x1B[0m`);
2393
+ }
1852
2394
  }
1853
2395
  return existingState;
1854
2396
  }
1855
2397
  }
1856
2398
  try {
1857
- fs4.unlinkSync(stateFile);
2399
+ fs5.unlinkSync(stateFile);
1858
2400
  } catch {
1859
2401
  }
1860
2402
  }
@@ -1867,33 +2409,33 @@ async function startDaemon(options = {}) {
1867
2409
  pid: process.pid,
1868
2410
  port,
1869
2411
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1870
- version: "0.1.0",
2412
+ version: CURRENT_ESEDRE_VERSION,
1871
2413
  workspaceRoot,
1872
2414
  logFile
1873
2415
  };
1874
2416
  }
1875
2417
  resolveStoreDir(workspaceRoot);
1876
- fs4.mkdirSync(path4.dirname(logFile), { recursive: true });
1877
- fs4.mkdirSync(path4.dirname(stateFile), { recursive: true });
1878
- const logFd = fs4.openSync(logFile, "a");
1879
- const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path4.basename(process.argv[1]));
2418
+ fs5.mkdirSync(path5.dirname(logFile), { recursive: true });
2419
+ fs5.mkdirSync(path5.dirname(stateFile), { recursive: true });
2420
+ const logFd = fs5.openSync(logFile, "a");
2421
+ const isEsedreArgv1 = process.argv[1] && /esedre/i.test(path5.basename(process.argv[1]));
1880
2422
  const cliCandidates = [
1881
- isEsedreArgv1 ? process.argv[1] : void 0,
1882
- path4.resolve(workspaceRoot, "dist", "esedre.mjs"),
1883
- path4.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
1884
- path4.resolve(__dirname, "esedre.mjs"),
1885
- path4.resolve(__dirname, "..", "dist", "esedre.mjs"),
1886
- path4.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
1887
- path4.resolve(__dirname, "..", "..", "bin", "esedre.js"),
1888
- path4.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
1889
- path4.resolve(__dirname, "..", "esedre.mjs"),
1890
- path4.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
2423
+ isEsedreArgv1 ? path5.resolve(process.argv[1]) : void 0,
2424
+ path5.resolve(workspaceRoot, "dist", "esedre.mjs"),
2425
+ path5.resolve(workspaceRoot, "../esedre/dist", "esedre.mjs"),
2426
+ path5.resolve(__dirname, "esedre.mjs"),
2427
+ path5.resolve(__dirname, "..", "dist", "esedre.mjs"),
2428
+ path5.resolve(__dirname, "..", "..", "dist", "esedre.mjs"),
2429
+ path5.resolve(__dirname, "..", "..", "bin", "esedre.js"),
2430
+ path5.resolve(__dirname, "..", "..", "bin", "esedre.ts"),
2431
+ path5.resolve(__dirname, "..", "esedre.mjs"),
2432
+ path5.resolve(__dirname, "../../../esedre/dist", "esedre.mjs")
1891
2433
  ].filter(Boolean);
1892
- const cliPath = cliCandidates.find((c) => fs4.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
1893
- if (!fs4.existsSync(cliPath)) {
2434
+ const cliPath = cliCandidates.find((c) => fs5.existsSync(c) && (c.endsWith(".mjs") || c.endsWith(".js") || c.endsWith(".ts"))) || cliCandidates[0];
2435
+ if (!fs5.existsSync(cliPath)) {
1894
2436
  throw new Error(`Could not locate Esedre executable. Searched candidates: ${cliCandidates.join(", ")}`);
1895
2437
  }
1896
- const child = spawn(process.execPath, [cliPath, "serve", "--port", String(port)], {
2438
+ const child = spawn(process.execPath, [cliPath, "start", "--foreground", "--port", String(port)], {
1897
2439
  detached: true,
1898
2440
  stdio: ["ignore", logFd, logFd],
1899
2441
  cwd: workspaceRoot,
@@ -1901,6 +2443,10 @@ async function startDaemon(options = {}) {
1901
2443
  windowsHide: true
1902
2444
  });
1903
2445
  child.unref();
2446
+ try {
2447
+ fs5.closeSync(logFd);
2448
+ } catch {
2449
+ }
1904
2450
  const pid = child.pid;
1905
2451
  if (!pid) {
1906
2452
  throw new Error("Failed to spawn Esedre daemon background process.");
@@ -1932,18 +2478,18 @@ async function startDaemon(options = {}) {
1932
2478
  uiPort: port + 1,
1933
2479
  apiPort: port + 2,
1934
2480
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1935
- version: "0.1.0",
2481
+ version: CURRENT_ESEDRE_VERSION,
1936
2482
  workspaceRoot,
1937
2483
  logFile
1938
2484
  };
1939
2485
  const tmpState = `${stateFile}.tmp.${Date.now()}`;
1940
- fs4.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
2486
+ fs5.writeFileSync(tmpState, JSON.stringify(state, null, 2) + "\n", "utf-8");
1941
2487
  try {
1942
- fs4.renameSync(tmpState, stateFile);
2488
+ fs5.renameSync(tmpState, stateFile);
1943
2489
  } catch {
1944
- fs4.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
2490
+ fs5.writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n", "utf-8");
1945
2491
  try {
1946
- fs4.unlinkSync(tmpState);
2492
+ fs5.unlinkSync(tmpState);
1947
2493
  } catch {
1948
2494
  }
1949
2495
  }
@@ -1955,7 +2501,7 @@ async function startDaemon(options = {}) {
1955
2501
  return state;
1956
2502
  }
1957
2503
  async function stopDaemon(options = {}) {
1958
- const port = options.port || 5674;
2504
+ const port = resolvePorts(void 0, { port: options.port }).gateway;
1959
2505
  const workspaceRoot = options.workspaceRoot || process.cwd();
1960
2506
  const stateFile = getDaemonStateFile(port, workspaceRoot);
1961
2507
  const state = getDaemonState(port, workspaceRoot);
@@ -1985,8 +2531,8 @@ async function stopDaemon(options = {}) {
1985
2531
  }
1986
2532
  }
1987
2533
  try {
1988
- if (fs4.existsSync(stateFile)) {
1989
- fs4.unlinkSync(stateFile);
2534
+ if (fs5.existsSync(stateFile)) {
2535
+ fs5.unlinkSync(stateFile);
1990
2536
  }
1991
2537
  } catch {
1992
2538
  }
@@ -1996,7 +2542,7 @@ async function stopDaemon(options = {}) {
1996
2542
  return true;
1997
2543
  }
1998
2544
  async function getDaemonStatus(options = {}) {
1999
- const port = options.port || 5674;
2545
+ const port = resolvePorts(void 0, { port: options.port }).gateway;
2000
2546
  const workspaceRoot = options.workspaceRoot || process.cwd();
2001
2547
  const state = getDaemonState(port, workspaceRoot);
2002
2548
  const stateFile = getDaemonStateFile(port, workspaceRoot);
@@ -2005,6 +2551,8 @@ async function getDaemonStatus(options = {}) {
2005
2551
  if (state && isProcessAlive(state.pid) && ping.responding && ping.isEsedre) {
2006
2552
  const startedMs = Date.parse(state.startedAt) || Date.now();
2007
2553
  const uptimeSeconds = Math.max(0, Math.floor((Date.now() - startedMs) / 1e3));
2554
+ const daemonVersion = state.version;
2555
+ const isStale = Boolean(daemonVersion && daemonVersion !== CURRENT_ESEDRE_VERSION);
2008
2556
  return {
2009
2557
  running: true,
2010
2558
  pid: state.pid,
@@ -2013,12 +2561,15 @@ async function getDaemonStatus(options = {}) {
2013
2561
  startedAt: state.startedAt,
2014
2562
  workspaceRoot: state.workspaceRoot,
2015
2563
  projects: ping.projects,
2016
- logFile: state.logFile || logFile
2564
+ logFile: state.logFile || logFile,
2565
+ version: daemonVersion,
2566
+ staleVersion: isStale,
2567
+ installedVersion: CURRENT_ESEDRE_VERSION
2017
2568
  };
2018
2569
  }
2019
2570
  if (state && !isProcessAlive(state.pid)) {
2020
2571
  try {
2021
- fs4.unlinkSync(stateFile);
2572
+ fs5.unlinkSync(stateFile);
2022
2573
  } catch {
2023
2574
  }
2024
2575
  }
@@ -2026,18 +2577,21 @@ async function getDaemonStatus(options = {}) {
2026
2577
  running: ping.responding && ping.isEsedre,
2027
2578
  port,
2028
2579
  projects: ping.projects,
2029
- logFile
2580
+ logFile,
2581
+ version: state?.version,
2582
+ staleVersion: Boolean(state?.version && state.version !== CURRENT_ESEDRE_VERSION),
2583
+ installedVersion: CURRENT_ESEDRE_VERSION
2030
2584
  };
2031
2585
  }
2032
2586
  function printDaemonLogs(options = {}) {
2033
- const port = options.port || 5674;
2587
+ const port = resolvePorts(void 0, { port: options.port }).gateway;
2034
2588
  const workspaceRoot = options.workspaceRoot || process.cwd();
2035
2589
  const logFile = getDaemonLogFile(port, workspaceRoot);
2036
- if (!fs4.existsSync(logFile)) {
2590
+ if (!fs5.existsSync(logFile)) {
2037
2591
  console.log(`\x1B[33mNo logs found at: ${logFile}\x1B[0m`);
2038
2592
  return;
2039
2593
  }
2040
- const content = fs4.readFileSync(logFile, "utf-8");
2594
+ const content = fs5.readFileSync(logFile, "utf-8");
2041
2595
  const count = options.lines || 40;
2042
2596
  const allLines = content.split(/\r?\n/);
2043
2597
  const tail = allLines.slice(-count).join("\n");
@@ -2047,7 +2601,6 @@ function printDaemonLogs(options = {}) {
2047
2601
  }
2048
2602
 
2049
2603
  // src/upgrade.ts
2050
- var CURRENT_ESEDRE_VERSION = "0.1.4";
2051
2604
  function computeNormalizedHash(content) {
2052
2605
  const normalized = content.replace(/\r\n/g, "\n").trim();
2053
2606
  return crypto2.createHash("sha1").update(normalized, "utf-8").digest("hex");
@@ -2175,10 +2728,129 @@ var ESE_WRAPPER_BASH = `#!/usr/bin/env bash
2175
2728
  DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
2176
2729
  exec "$DIR/esedre" "$@"
2177
2730
  `;
2178
- var ESEDRE_SKILL_TEMPLATE = '---\nname: esedre\ndescription: Tooling and workflow reference for interacting with the Esedre developer ticketing and roadmap system. Use this skill whenever inspecting planned work, viewing ticket specs, updating implementation plans, or posting developer notes.\n---\n\n# Esedre Planning & Roadmap Workflow\n\nEsedre is the developer ticketing and companion LLM coding agent coordination platform. The engine codebase resides in the standalone repository `aellinsar/esedre` (`../esedre`), and operates on decoupled ticket repositories (such as `aellinsar/esedre-data` configured in `.esedre/esedre.json`). In `ProfessorArwamSleepCenter`, interact with tickets via the compiled bridge artifact `tools/esedre.mjs` (`node tools/esedre.mjs <command>`), in-repo shell wrappers in `.esedre/` (`.esedre/ese`), or the global `ese` CLI.\n\n## 0. Instant Zero-Latency Context (`.esedre/snapshot.json`)\n\nBefore querying tickets over the network or CLI, check `.esedre/snapshot.json` in the workspace root. It provides a local read-only projection containing all active and completed tickets, summaries, implementation plans, revision numbers, completion dates (`completedAt`), and staleness metrics (`daysSinceUpdate`).\n\n## 1. Model Context Protocol (MCP) Tools\n\nWhen Esedre MCP is active in your agent session (`.agents/mcp_config.json`), use these tools directly:\n\n| Tool | Purpose | Key Arguments |\n|---|---|---|\n| `esedre_list_tickets` | List roadmap tickets with filters | `project` (e.g. `Profe`, `Esedre`, `Alce`), `status`, `type`, `search` |\n| `esedre_get_ticket` | Full specification, summary, comments, revision & sha1 | `ticketId` (numeric or compound e.g. `Profe-35`) |\n| `esedre_get_plan` | Active implementation plan markdown | `ticketId` (numeric or compound) |\n| `esedre_save_plan` | Save implementation plan markdown with OCC | `ticketId`, `planMarkdown`, `lastHash` |\n| `esedre_create_ticket` | Mint a new ticket with auto sequential ID | `title` (max 48 chars), `type`, `project`, `effort`, `summary` |\n| `esedre_update_ticket` | Update ticket attributes with OCC | `ticketId`, `status`, `title`, `inDevelopment`, `featureFlag`, `lastHash` |\n\n## 2. Core CLI Commands (Fallback via `run_command`)\n\nBoth `esedre` and `ese` work interchangeably:\n\n### List Tickets\n```bash\n.esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]\n```\n\n### Read Ticket Details\n```bash\n.esedre/ese get <ticketId> [--json]\n```\n\n### Read or Update Implementation Plans\n```bash\n.esedre/ese plan <ticketId>\n.esedre/ese plan <ticketId> --file <planFilePath>\n.esedre/ese plan <ticketId> --set "<markdownContent>" [--last-hash <hash>]\n```\n\n### Create a Ticket\n```bash\n.esedre/ese create --title "..." [-p|--project Profe] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--json]\n```\n\n### Update Ticket Status\n```bash\n.esedre/ese update <ticketId> --status "In Development"\n.esedre/ese update <ticketId> --status "Completed"\n```\n\n### Append Comments or Notes\n```bash\n.esedre/ese comment <ticketId> --text "Verified implementation." --author "Antigravity"\n```\n\n### Regenerate Snapshot\n```bash\n.esedre/ese snapshot\n```\n\n### Background Daemon Management\n```bash\n# Start background daemon on port 5674 (idempotent)\n.esedre/ese daemon start [--port 5674] [--quiet] [--json]\n\n# Check daemon health and diagnostics\n.esedre/ese daemon status [--port 5674] [--json]\n\n# View recent daemon activity logs\n.esedre/ese daemon logs\n\n# Stop background daemon\n.esedre/ese daemon stop [--port 5674] [--quiet] [--json]\n```\n\n### Foreground Gateway Server\n```bash\n.esedre/ese serve [--port 5674]\n```\n- Starts the unified gateway on port 5674, routing `/app` to Esedre UI (port 5675) and `/api` to Esedre REST API (port 5676).\n- Transparently rewrites `/` to `/app/`.\n\n### Workspace Configuration & Vite Proxy Setup\n```bash\n.esedre/ese configure [--project <code>] [--name <name>] [--port <n>] [--proxy] [--no-proxy] [-y]\n```\n- Automatically detects `vite.config.ts`/`vite.config.js`. In interactive mode or with `--proxy`, configures a reverse proxy for `/esedre` targeting port 5674 (`http://127.0.0.1:5674`).\n- If Vite is absent or declined, displays the recommended reverse proxy block for embedding the planner UI in the host app.\n\n## 3. Embedded View & Theme Token Contract\n\nWhen embedding `<PlannedWorkView />` inside a host app (e.g. `PlannedWorkModal.tsx`):\n\n### Zero-Effort Drop-in (Default Fallback)\n`PlannedWorkView` incorporates an internal CSS fallback bridge (`.esedre-host-bridge` / `.esedre-theme-root` with `ESEDRE_THEME_FALLBACK_CSS`).\nIf an embedding application supplies **zero CSS variables**, the component automatically falls back to clean, high-contrast light or dark themes matching the user\'s OS preference (`prefers-color-scheme: dark`) or host `.dark` / `[data-theme="night"]` classes.\n\n### Theme Token Customization\nIf the host application declares any or all of the 12 core design tokens on `:root` or an ancestor container, `PlannedWorkView` automatically adopts them:\n- **Surface**: `--bg-surface`, `--bg-surface-elevated`, `--bg-input`\n- **Borders**: `--border-subtle`, `--border-strong`, `--border-accent`\n- **Text**: `--text-primary`, `--text-secondary`, `--text-muted`\n- **Accent**: `--accent-primary`, `--accent-bg-subtle`, `--accent-border-subtle`\n\n## 4. Safety & Invariants\n- **Always use `--json`** for CLI programmatic inspection.\n- **Optimistic Concurrency**: Writes support `--last-hash <hash>` to prevent overwriting concurrent updates.\n- **Universal Type Naming**: Always use `type` (`Feature`, `Platform`, `Tools`, `Idea`, `Bug`). The legacy name `category` is deprecated.\n- **Compound IDs for Multi-Project Portfolios**: In `project=all`, ticket IDs are `${projectCode}-${id}` (e.g. `Profe-1`, `Esedre-1`). DOM anchors strictly use `feature-card-${projectCode}-${id}`.\n- **Never delete tickets** directly via filesystem.\n- **Agent Project Allow-List**: Providing isolation of projects planning that you don\'t want an agent to access. A repository only accesses projects declared in its `.esedre/esedre.json` (`allowedProjects`). Unauthorized cross-project access is strictly blocked.\n- **Secret Developer Hash Route (`#/planner`, `#/plan`, `#planner`, `#plan`)**:\n - In web applications embedding Esedre (such as Professor Arwam), `#/planner` and `#/plan` serve as direct secret developer entry routes.\n - **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in `sitemap.xml`, `robots.txt`, `llms.txt`, or public navigation links.\n - The URL hash is preserved across hard page refreshes (F5) without falling back to `#/chat` or other views.\n';
2731
+ var HISTORIC_SKILL_HASHES = [
2732
+ "8611116c276fdfb598b965c716b251ce7c77c7f7",
2733
+ "05f129fb6e06a18df00215af105567abcbf0bc67"
2734
+ ];
2735
+ var ESEDRE_SKILL_TEMPLATE = `---
2736
+ name: esedre
2737
+ description: Tooling and workflow reference for interacting with the Esedre developer ticketing and roadmap system. Use this skill whenever inspecting planned work, viewing ticket specs, updating implementation plans, or posting developer notes.
2738
+ ---
2739
+
2740
+ # Esedre Planning & Roadmap Workflow
2741
+
2742
+ Esedre is the developer ticketing and LLM coding partner coordination platform. The engine codebase resides in the standalone repository \`aellinsar/esedre\` (\`../esedre\`), and operates on decoupled ticket repositories (such as \`aellinsar/esedre-data\` configured in \`.esedre/esedre.json\`). Interact with tickets via in-repo shell wrappers in \`.esedre/\` (\`.esedre/ese\`), or the global \`ese\` CLI.
2743
+
2744
+ ## 0. Instant Zero-Latency Context (\`.esedre/snapshot.json\`)
2745
+
2746
+ Before querying tickets over the network or CLI, check \`.esedre/snapshot.json\` in the workspace root. It provides a local read-only projection containing all active and completed tickets, summaries, implementation plans, revision numbers, completion dates (\`completedAt\`), and staleness metrics (\`daysSinceUpdate\`).
2747
+
2748
+ ## 1. Model Context Protocol (MCP) Tools
2749
+
2750
+ When Esedre MCP is active in your agent session (\`.agents/mcp_config.json\`), use these tools directly:
2751
+
2752
+ | Tool | Purpose | Key Arguments |
2753
+ |---|---|---|
2754
+ | \`esedre_list_tickets\` | List roadmap tickets with filters | \`project\` (e.g. \`Profe\`, \`Esedre\`, \`Alce\`), \`status\`, \`type\`, \`search\` |
2755
+ | \`esedre_get_ticket\` | Full specification, summary, comments, revision & sha1 | \`ticketId\` (numeric e.g. \`96\` or compound e.g. \`Profe-96\`) |
2756
+ | \`esedre_get_plan\` | Active implementation plan markdown | \`ticketId\` (numeric or compound) |
2757
+ | \`esedre_save_plan\` | Save implementation plan markdown with OCC | \`ticketId\`, \`planMarkdown\`, \`lastHash\` |
2758
+ | \`esedre_create_ticket\` | Mint a new ticket with auto sequential ID | \`title\` (max 48 chars), \`type\`, \`project\`, \`effort\`, \`summary\` |
2759
+ | \`esedre_update_ticket\` | Update ticket attributes with OCC | \`ticketId\`, \`status\`, \`type\`, \`title\`, \`complexity\`, \`effort\`, \`inDevelopment\`, \`featureFlag\`, \`lastHash\` |
2760
+ | \`esedre_add_comment\` | Append developer or agent comment | \`ticketId\`, \`text\`, \`author\` |
2761
+
2762
+ ## 2. Core CLI Commands (Fallback via \`run_command\`)
2763
+
2764
+ Both \`esedre\` and \`ese\` work interchangeably:
2765
+
2766
+ ### List Tickets
2767
+ \`\`\`bash
2768
+ .esedre/ese list [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]
2769
+ \`\`\`
2770
+
2771
+ ### Read Ticket Details
2772
+ \`\`\`bash
2773
+ .esedre/ese get <ticketId> [--json]
2774
+ \`\`\`
2775
+
2776
+ ### Read or Update Implementation Plans
2777
+ \`\`\`bash
2778
+ .esedre/ese plan <ticketId>
2779
+ .esedre/ese plan <ticketId> --file <planFilePath>
2780
+ .esedre/ese plan <ticketId> --set "<markdownContent>" [--last-hash <hash>]
2781
+ \`\`\`
2782
+
2783
+ ### Create a Ticket
2784
+ \`\`\`bash
2785
+ .esedre/ese create --title "..." [-p|--project <code>] [-t|--type Feature] [--complexity Medium] [--effort "2.0 - 4.0 hours"] [--json]
2786
+ \`\`\`
2787
+
2788
+ ### Update Ticket Status & Attributes
2789
+ \`\`\`bash
2790
+ .esedre/ese update <ticketId> --status "In Development"
2791
+ .esedre/ese update <ticketId> --status "Completed"
2792
+ .esedre/ese update <ticketId> --type Feature --complexity Medium --effort "2.0 - 4.0 hours"
2793
+ \`\`\`
2794
+
2795
+ ### Append Comments or Notes
2796
+ \`\`\`bash
2797
+ .esedre/ese comment <ticketId> "Verified implementation." --author "Agent"
2798
+ .esedre/ese comment <ticketId> --text "Verified implementation." --author "Agent"
2799
+ \`\`\`
2800
+
2801
+ ### Regenerate Snapshot
2802
+ \`\`\`bash
2803
+ .esedre/ese snapshot
2804
+ \`\`\`
2805
+
2806
+ ### Server Lifecycle & Daemon Management
2807
+ \`\`\`bash
2808
+ # Start background server daemon on port 5674 (default)
2809
+ .esedre/ese start [--port 5674] [--quiet] [--json]
2810
+
2811
+ # Run foreground gateway server directly (Ctrl+C to stop)
2812
+ .esedre/ese start -f [--port 5674]
2813
+
2814
+ # Check daemon health and diagnostics
2815
+ .esedre/ese status [--port 5674] [--json]
2816
+
2817
+ # View recent daemon activity logs
2818
+ .esedre/ese logs [--port 5674] [--lines <n>]
2819
+
2820
+ # Stop background server daemon
2821
+ .esedre/ese stop [--port 5674] [--quiet] [--json]
2822
+ \`\`\`
2823
+
2824
+ ## 3. Embedded View & Theme Token Contract
2825
+
2826
+ When embedding \`<PlannedWorkView />\` inside a host app (e.g. \`PlannedWorkModal.tsx\`):
2827
+
2828
+ ### Zero-Effort Drop-in (Default Fallback)
2829
+ \`PlannedWorkView\` incorporates an internal CSS fallback bridge (\`.esedre-host-bridge\` / \`.esedre-theme-root\` with \`ESEDRE_THEME_FALLBACK_CSS\`).
2830
+ If an embedding application supplies **zero CSS variables**, the component automatically falls back to clean, high-contrast light or dark themes matching the user's OS preference (\`prefers-color-scheme: dark\`) or host \`.dark\` / \`[data-theme="night"]\` classes.
2831
+
2832
+ ### Theme Token Customization
2833
+ If the host application declares any or all of the 12 core design tokens on \`:root\` or an ancestor container, \`PlannedWorkView\` automatically adopts them:
2834
+ - **Surface**: \`--bg-surface\`, \`--bg-surface-elevated\`, \`--bg-input\`
2835
+ - **Borders**: \`--border-subtle\`, \`--border-strong\`, \`--border-accent\`
2836
+ - **Text**: \`--text-primary\`, \`--text-secondary\`, \`--text-muted\`
2837
+ - **Accent**: \`--accent-primary\`, \`--accent-bg-subtle\`, \`--accent-border-subtle\`
2838
+
2839
+ ## 4. Safety & Invariants
2840
+ - **Always use \`--json\`** for CLI programmatic inspection.
2841
+ - **Optimistic Concurrency**: Writes support \`--last-hash <hash>\` to prevent overwriting concurrent updates.
2842
+ - **Universal Type Naming**: Always use \`type\` (\`Feature\`, \`Platform\`, \`Tools\`, \`Idea\`, \`Bug\`). The legacy name \`category\` is deprecated.
2843
+ - **Compound IDs for Multi-Project Portfolios**: In \`project=all\`, ticket IDs are \`\${projectCode}-\${id}\` (e.g. \`Profe-1\`, \`Esedre-1\`). DOM anchors strictly use \`feature-card-\${projectCode}-\${id}\`.
2844
+ - **Never delete tickets** directly via filesystem.
2845
+ - **Workspace Scoping**: An LLM coding partner operates strictly within the authorized project scope declared in the local repository configuration. Central repository linking and system-level configuration are managed separately; focus roadmap work, implementation plans, and verification notes on this project.
2846
+ - **Secret Developer Hash Route (\`#/planner\`, \`#/plan\`, \`#planner\`, \`#plan\`)**:
2847
+ - In web applications embedding Esedre (such as Professor Arwam), \`#/planner\` and \`#/plan\` serve as direct secret developer entry routes.
2848
+ - **SEO Invariant**: These developer routes are private and MUST NEVER be exposed in \`sitemap.xml\`, \`robots.txt\`, \`llms.txt\`, or public navigation links.
2849
+ - The URL hash is preserved across hard page refreshes (F5) without falling back to \`#/chat\` or other views.
2850
+ `;
2179
2851
  function plantWrappers(targetDir) {
2180
- const esedreDir = path5.join(targetDir, ".esedre");
2181
- fs5.mkdirSync(esedreDir, { recursive: true });
2852
+ const esedreDir = path6.join(targetDir, ".esedre");
2853
+ fs6.mkdirSync(esedreDir, { recursive: true });
2182
2854
  const wrappers = [
2183
2855
  { name: "esedre.cmd", content: WRAPPER_CMD, exec: false },
2184
2856
  { name: "esedre.ps1", content: WRAPPER_PS1, exec: false },
@@ -2188,24 +2860,24 @@ function plantWrappers(targetDir) {
2188
2860
  { name: "ese", content: ESE_WRAPPER_BASH, exec: true }
2189
2861
  ];
2190
2862
  for (const w of wrappers) {
2191
- const fullPath = path5.join(esedreDir, w.name);
2192
- fs5.writeFileSync(fullPath, w.content, "utf-8");
2863
+ const fullPath = path6.join(esedreDir, w.name);
2864
+ fs6.writeFileSync(fullPath, w.content, "utf-8");
2193
2865
  if (w.exec) {
2194
2866
  try {
2195
- fs5.chmodSync(fullPath, 493);
2867
+ fs6.chmodSync(fullPath, 493);
2196
2868
  } catch {
2197
2869
  }
2198
2870
  }
2199
2871
  }
2200
2872
  }
2201
2873
  function setupMcpConfig(targetDir) {
2202
- const agentsDir = path5.join(targetDir, ".agents");
2203
- fs5.mkdirSync(agentsDir, { recursive: true });
2204
- const mcpConfigPath = path5.join(agentsDir, "mcp_config.json");
2874
+ const agentsDir = path6.join(targetDir, ".agents");
2875
+ fs6.mkdirSync(agentsDir, { recursive: true });
2876
+ const mcpConfigPath = path6.join(agentsDir, "mcp_config.json");
2205
2877
  let configObj = { mcpServers: {} };
2206
- if (fs5.existsSync(mcpConfigPath)) {
2878
+ if (fs6.existsSync(mcpConfigPath)) {
2207
2879
  try {
2208
- configObj = JSON.parse(fs5.readFileSync(mcpConfigPath, "utf-8"));
2880
+ configObj = JSON.parse(fs6.readFileSync(mcpConfigPath, "utf-8"));
2209
2881
  if (!configObj.mcpServers) configObj.mcpServers = {};
2210
2882
  } catch {
2211
2883
  }
@@ -2222,28 +2894,29 @@ function setupMcpConfig(targetDir) {
2222
2894
  };
2223
2895
  }
2224
2896
  try {
2225
- fs5.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
2897
+ fs6.writeFileSync(mcpConfigPath, JSON.stringify(configObj, null, 2) + "\n", "utf-8");
2226
2898
  return true;
2227
2899
  } catch {
2228
2900
  return false;
2229
2901
  }
2230
2902
  }
2231
- function setupSkill(targetDir) {
2232
- const skillDir = path5.join(targetDir, ".agents", "skills", "esedre");
2233
- fs5.mkdirSync(skillDir, { recursive: true });
2234
- const skillFile = path5.join(skillDir, "SKILL.md");
2235
- if (!fs5.existsSync(skillFile)) {
2236
- fs5.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
2903
+ function setupSkill(targetDir, options = {}) {
2904
+ const skillDir = path6.join(targetDir, ".agents", "skills", "esedre");
2905
+ fs6.mkdirSync(skillDir, { recursive: true });
2906
+ const skillFile = path6.join(skillDir, "SKILL.md");
2907
+ if (!fs6.existsSync(skillFile)) {
2908
+ fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
2237
2909
  return { status: "MISSING", updated: true };
2238
2910
  }
2239
- const currentContent = fs5.readFileSync(skillFile, "utf-8");
2240
- const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, []);
2241
- if (status === "LATEST") {
2911
+ const currentContent = fs6.readFileSync(skillFile, "utf-8");
2912
+ const status = classifyContent(currentContent, ESEDRE_SKILL_TEMPLATE, HISTORIC_SKILL_HASHES);
2913
+ if (status === "LATEST" && !options.force) {
2242
2914
  return { status, updated: false };
2243
2915
  }
2244
- if (status === "HISTORIC_DEFAULT" || status === "MISSING") {
2245
- fs5.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
2246
- return { status, updated: true };
2916
+ const hasBannedOrDeprecatedTerms = currentContent.includes("companion agent") || currentContent.includes("companion LLM") || currentContent.includes("tools/esedre.mjs");
2917
+ if (options.force || status === "HISTORIC_DEFAULT" || status === "MISSING" || hasBannedOrDeprecatedTerms) {
2918
+ fs6.writeFileSync(skillFile, ESEDRE_SKILL_TEMPLATE, "utf-8");
2919
+ return { status: "LATEST", updated: true };
2247
2920
  }
2248
2921
  return { status, updated: false };
2249
2922
  }
@@ -2256,8 +2929,8 @@ var VITE_CONFIG_FILES = [
2256
2929
  ];
2257
2930
  function findViteConfig(targetDir) {
2258
2931
  for (const name of VITE_CONFIG_FILES) {
2259
- const fullPath = path5.join(targetDir, name);
2260
- if (fs5.existsSync(fullPath)) {
2932
+ const fullPath = path6.join(targetDir, name);
2933
+ if (fs6.existsSync(fullPath)) {
2261
2934
  return fullPath;
2262
2935
  }
2263
2936
  }
@@ -2321,10 +2994,10 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
2321
2994
  recommendedProxySnippet: snippet
2322
2995
  };
2323
2996
  }
2324
- const relativeName = path5.basename(configPath);
2997
+ const relativeName = path6.basename(configPath);
2325
2998
  let content = "";
2326
2999
  try {
2327
- content = fs5.readFileSync(configPath, "utf-8");
3000
+ content = fs6.readFileSync(configPath, "utf-8");
2328
3001
  } catch {
2329
3002
  return {
2330
3003
  status: "MANUAL_REQUIRED",
@@ -2355,7 +3028,7 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
2355
3028
  };
2356
3029
  }
2357
3030
  try {
2358
- fs5.writeFileSync(configPath, injected, "utf-8");
3031
+ fs6.writeFileSync(configPath, injected, "utf-8");
2359
3032
  return {
2360
3033
  status: "CONFIGURED",
2361
3034
  configFile: relativeName,
@@ -2370,25 +3043,27 @@ function configureViteProxy(targetDir, port = DEFAULT_ESEDRE_PORT, setupProxy) {
2370
3043
  }
2371
3044
  }
2372
3045
  function configureWorkspace(targetDir, options = {}) {
2373
- const esedreDir = path5.join(targetDir, ".esedre");
2374
- if (!fs5.existsSync(esedreDir)) {
2375
- fs5.mkdirSync(esedreDir, { recursive: true });
3046
+ const esedreDir = path6.join(targetDir, ".esedre");
3047
+ if (!fs6.existsSync(esedreDir)) {
3048
+ fs6.mkdirSync(esedreDir, { recursive: true });
2376
3049
  }
2377
- const esedreJsonPath = path5.join(esedreDir, "esedre.json");
3050
+ const esedreJsonPath = path6.join(esedreDir, "esedre.json");
2378
3051
  let existingRaw = null;
2379
- if (fs5.existsSync(esedreJsonPath)) {
3052
+ if (fs6.existsSync(esedreJsonPath)) {
2380
3053
  try {
2381
- existingRaw = JSON.parse(fs5.readFileSync(esedreJsonPath, "utf-8"));
3054
+ existingRaw = JSON.parse(fs6.readFileSync(esedreJsonPath, "utf-8"));
2382
3055
  } catch {
2383
3056
  }
2384
3057
  }
2385
3058
  let config;
2386
3059
  const projectCode = options.projectCode || existingRaw?.projectCode;
2387
3060
  const projectName = options.projectName || existingRaw?.projectName || (projectCode ? projectCode : void 0);
3061
+ const dataDir = options.dataDir !== void 0 ? options.dataDir : existingRaw?.dataDir;
2388
3062
  if (existingRaw) {
2389
3063
  config = migrateEsedreConfig(existingRaw, CURRENT_ESEDRE_VERSION);
2390
3064
  if (projectCode) config.projectCode = projectCode;
2391
3065
  if (projectName) config.projectName = projectName;
3066
+ if (dataDir) config.dataDir = dataDir;
2392
3067
  if (options.allowedProjects) {
2393
3068
  config.allowedProjects = options.allowedProjects;
2394
3069
  } else if (projectCode) {
@@ -2405,18 +3080,20 @@ function configureWorkspace(targetDir, options = {}) {
2405
3080
  projectCode: projectCode || void 0,
2406
3081
  projectName: projectName || void 0,
2407
3082
  allowedProjects: options.allowedProjects || (projectCode ? [projectCode] : void 0),
2408
- port: options.port || DEFAULT_ESEDRE_PORT
3083
+ dataDir: dataDir || void 0,
3084
+ port: options.port || void 0
2409
3085
  };
2410
3086
  }
2411
- fs5.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3087
+ fs6.writeFileSync(esedreJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2412
3088
  let projectRegistered = false;
2413
3089
  if (projectCode) {
2414
- const ticketsDir = path5.join(esedreDir, "tickets");
2415
- if (!fs5.existsSync(ticketsDir)) {
2416
- fs5.mkdirSync(ticketsDir, { recursive: true });
3090
+ const hasExistingTicketsDir = fs6.existsSync(path6.join(targetDir, "tickets")) || fs6.existsSync(path6.join(targetDir, "src", "data", "planning", "tickets"));
3091
+ const ticketsDir = path6.join(esedreDir, "tickets");
3092
+ if (!hasExistingTicketsDir && !fs6.existsSync(ticketsDir)) {
3093
+ fs6.mkdirSync(ticketsDir, { recursive: true });
2417
3094
  }
2418
- const inRepoPJson = path5.join(esedreDir, "project.json");
2419
- if (!fs5.existsSync(inRepoPJson)) {
3095
+ const inRepoPJson = path6.join(esedreDir, "project.json");
3096
+ if (!fs6.existsSync(inRepoPJson)) {
2420
3097
  const projDesc = {
2421
3098
  id: 1,
2422
3099
  code: projectCode.toUpperCase(),
@@ -2429,14 +3106,14 @@ function configureWorkspace(targetDir, options = {}) {
2429
3106
  border: "border-cyan-500/40"
2430
3107
  }
2431
3108
  };
2432
- fs5.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
3109
+ fs6.writeFileSync(inRepoPJson, JSON.stringify(projDesc, null, 2) + "\n", "utf-8");
2433
3110
  projectRegistered = true;
2434
3111
  } else if (projectName) {
2435
3112
  try {
2436
- const existing = JSON.parse(fs5.readFileSync(inRepoPJson, "utf-8"));
3113
+ const existing = JSON.parse(fs6.readFileSync(inRepoPJson, "utf-8"));
2437
3114
  if (existing.name !== projectName) {
2438
3115
  existing.name = projectName;
2439
- fs5.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
3116
+ fs6.writeFileSync(inRepoPJson, JSON.stringify(existing, null, 2) + "\n", "utf-8");
2440
3117
  }
2441
3118
  } catch {
2442
3119
  }
@@ -2448,8 +3125,9 @@ function configureWorkspace(targetDir, options = {}) {
2448
3125
  if (options.setupMcp !== false) {
2449
3126
  mcpConfigured = setupMcpConfig(targetDir);
2450
3127
  }
2451
- const skillRes = setupSkill(targetDir);
2452
- const proxyRes = configureViteProxy(targetDir, config.port || DEFAULT_ESEDRE_PORT, options.setupProxy);
3128
+ const skillRes = setupSkill(targetDir, { force: options.force });
3129
+ const targetPort = resolvePorts(config).gateway;
3130
+ const proxyRes = configureViteProxy(targetDir, targetPort, options.setupProxy);
2453
3131
  const globalStoreRes = ensureGlobalEsedreStore();
2454
3132
  if (!globalStoreRes.ok) {
2455
3133
  console.warn(`\x1B[33m\u26A0\uFE0F Warning: Could not initialize global store at ${globalStoreRes.path}: ${globalStoreRes.error}\x1B[0m`);
@@ -2471,17 +3149,41 @@ function configureWorkspace(targetDir, options = {}) {
2471
3149
  globalStoreError: globalStoreRes.error
2472
3150
  };
2473
3151
  }
3152
+ var initWorkspace = configureWorkspace;
3153
+ function initHub(targetDir) {
3154
+ const resolved = path6.resolve(targetDir);
3155
+ if (!fs6.existsSync(resolved)) {
3156
+ fs6.mkdirSync(resolved, { recursive: true });
3157
+ }
3158
+ const projectsDir = path6.join(resolved, "projects");
3159
+ let projectsDirCreated = false;
3160
+ if (!fs6.existsSync(projectsDir)) {
3161
+ fs6.mkdirSync(projectsDir, { recursive: true });
3162
+ projectsDirCreated = true;
3163
+ }
3164
+ const projectsJsonPath = path6.join(resolved, "projects.json");
3165
+ let projectsJsonCreated = false;
3166
+ if (!fs6.existsSync(projectsJsonPath)) {
3167
+ fs6.writeFileSync(projectsJsonPath, "[]\n", "utf-8");
3168
+ projectsJsonCreated = true;
3169
+ }
3170
+ return {
3171
+ projectsJsonCreated,
3172
+ projectsDirCreated,
3173
+ hubDir: resolved.replace(/\\/g, "/")
3174
+ };
3175
+ }
2474
3176
 
2475
3177
  // src/server/gateway.ts
2476
- import fs7 from "node:fs";
3178
+ import fs8 from "node:fs";
2477
3179
  import http4 from "node:http";
2478
- import path7 from "node:path";
3180
+ import path8 from "node:path";
2479
3181
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2480
3182
 
2481
3183
  // src/server/uiServer.ts
2482
3184
  import http2 from "node:http";
2483
- import fs6 from "node:fs";
2484
- import path6 from "node:path";
3185
+ import fs7 from "node:fs";
3186
+ import path7 from "node:path";
2485
3187
  var MIME_TYPES = {
2486
3188
  ".html": "text/html; charset=utf-8",
2487
3189
  ".js": "application/javascript; charset=utf-8",
@@ -2511,20 +3213,20 @@ function startUiServer(port, webDir) {
2511
3213
  if (!reqPath || reqPath === "/") {
2512
3214
  reqPath = "/index.html";
2513
3215
  }
2514
- const safePath = path6.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
2515
- let filePath = path6.join(webDir, safePath);
2516
- if (!fs6.existsSync(filePath) || fs6.statSync(filePath).isDirectory()) {
2517
- filePath = path6.join(webDir, "index.html");
3216
+ const safePath = path7.normalize(reqPath).replace(/^(\.\.[/\\])+/, "");
3217
+ let filePath = path7.join(webDir, safePath);
3218
+ if (!fs7.existsSync(filePath) || fs7.statSync(filePath).isDirectory()) {
3219
+ filePath = path7.join(webDir, "index.html");
2518
3220
  }
2519
- if (!fs6.existsSync(filePath)) {
3221
+ if (!fs7.existsSync(filePath)) {
2520
3222
  res.writeHead(404, { "Content-Type": "text/plain" });
2521
3223
  res.end("Esedre UI: index.html not found. Run npm run build first.");
2522
3224
  return;
2523
3225
  }
2524
- const ext = path6.extname(filePath).toLowerCase();
3226
+ const ext = path7.extname(filePath).toLowerCase();
2525
3227
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
2526
3228
  try {
2527
- const content = fs6.readFileSync(filePath);
3229
+ const content = fs7.readFileSync(filePath);
2528
3230
  res.writeHead(200, { "Content-Type": contentType });
2529
3231
  res.end(content);
2530
3232
  } catch (err) {
@@ -2605,6 +3307,10 @@ function createApiHandler(storage, workspaceRoot) {
2605
3307
  }
2606
3308
  try {
2607
3309
  if (req.method === "GET") {
3310
+ if (pathname === "/api/ping") {
3311
+ sendJson(res, 200, { status: "ok", esedre: true });
3312
+ return true;
3313
+ }
2608
3314
  if (pathname === "/api/planning/projects") {
2609
3315
  const projects = await storage.getProjects();
2610
3316
  sendJson(res, 200, projects);
@@ -2628,7 +3334,7 @@ function createApiHandler(storage, workspaceRoot) {
2628
3334
  }
2629
3335
  if (pathname.startsWith("/api/planning/ticket/")) {
2630
3336
  const idStr = pathname.slice("/api/planning/ticket/".length);
2631
- if (!idStr || !/^([a-zA-Z0-9]{1,6}-)?\d+$/.test(idStr)) {
3337
+ if (!idStr || !/^([a-zA-Z0-9]{1,7}-)?\d+$/.test(idStr)) {
2632
3338
  sendJson(res, 400, { error: "Invalid ticket ID" });
2633
3339
  return true;
2634
3340
  }
@@ -2780,6 +3486,11 @@ function createApiHandler(storage, workspaceRoot) {
2780
3486
  sendJson(res, 400, { error: codeVal.error });
2781
3487
  return true;
2782
3488
  }
3489
+ const nameVal = validateProjectName(name);
3490
+ if (!nameVal.valid) {
3491
+ sendJson(res, 400, { error: nameVal.error });
3492
+ return true;
3493
+ }
2783
3494
  invalidateApiCache();
2784
3495
  const project = await storage.registerProject({
2785
3496
  code,
@@ -2859,35 +3570,35 @@ function startApiServer(port, storage, workspaceRoot) {
2859
3570
 
2860
3571
  // src/server/gateway.ts
2861
3572
  var __filename2 = fileURLToPath2(import.meta.url);
2862
- var __dirname2 = path7.dirname(__filename2);
3573
+ var __dirname2 = path8.dirname(__filename2);
2863
3574
  function resolveWebDir(explicitWebDir, workspaceRoot) {
2864
- if (explicitWebDir && fs7.existsSync(path7.join(explicitWebDir, "index.html"))) {
3575
+ if (explicitWebDir && fs8.existsSync(path8.join(explicitWebDir, "index.html"))) {
2865
3576
  return explicitWebDir;
2866
3577
  }
2867
3578
  const candidates = [
2868
3579
  // 1. Packaged layout right next to executable (e.g. tools/web)
2869
- path7.resolve(__dirname2, "web"),
3580
+ path8.resolve(__dirname2, "web"),
2870
3581
  // 2. From workspace root: <workspaceRoot>/dist/web
2871
- workspaceRoot ? path7.resolve(workspaceRoot, "dist", "web") : "",
3582
+ workspaceRoot ? path8.resolve(workspaceRoot, "dist", "web") : "",
2872
3583
  // 3. In-repo legacy subfolder: <workspaceRoot>/esedre/dist/web
2873
- workspaceRoot ? path7.resolve(workspaceRoot, "esedre", "dist", "web") : "",
3584
+ workspaceRoot ? path8.resolve(workspaceRoot, "esedre", "dist", "web") : "",
2874
3585
  // 4. Sibling standalone repository from workspace root: <workspaceRoot>/../esedre/dist/web
2875
- workspaceRoot ? path7.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
3586
+ workspaceRoot ? path8.resolve(workspaceRoot, "..", "esedre", "dist", "web") : "",
2876
3587
  // 5. Packaged layout inside dist/web
2877
- path7.resolve(__dirname2, "dist", "web"),
3588
+ path8.resolve(__dirname2, "dist", "web"),
2878
3589
  // 6. Sibling standalone repository from __dirname: <__dirname>/../../esedre/dist/web
2879
- path7.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
3590
+ path8.resolve(__dirname2, "..", "..", "esedre", "dist", "web"),
2880
3591
  // 7. Source tree layout: esedre/src/server/ -> esedre/dist/web
2881
- path7.resolve(__dirname2, "..", "..", "dist", "web"),
3592
+ path8.resolve(__dirname2, "..", "..", "dist", "web"),
2882
3593
  // 8. Legacy layout from __dirname
2883
- path7.resolve(__dirname2, "..", "esedre", "dist", "web")
3594
+ path8.resolve(__dirname2, "..", "esedre", "dist", "web")
2884
3595
  ].filter(Boolean);
2885
3596
  for (const candidate of candidates) {
2886
- if (fs7.existsSync(path7.join(candidate, "index.html"))) {
3597
+ if (fs8.existsSync(path8.join(candidate, "index.html"))) {
2887
3598
  return candidate;
2888
3599
  }
2889
3600
  }
2890
- return candidates[0] || path7.resolve(__dirname2, "web");
3601
+ return candidates[0] || path8.resolve(__dirname2, "web");
2891
3602
  }
2892
3603
  function proxyRequest(req, res, targetPort, rewritePath) {
2893
3604
  const targetPath = rewritePath ? rewritePath(req.url || "/") : req.url || "/";
@@ -2917,7 +3628,12 @@ function proxyRequest(req, res, targetPort, rewritePath) {
2917
3628
  req.pipe(proxyReq, { end: true });
2918
3629
  }
2919
3630
  function startGatewayCluster(options) {
2920
- const gatewayPort = options.gatewayPort !== void 0 ? options.gatewayPort : 5674;
3631
+ const resolved = resolvePorts(void 0, {
3632
+ gatewayPort: options.gatewayPort,
3633
+ uiPort: options.uiPort,
3634
+ apiPort: options.apiPort
3635
+ });
3636
+ const gatewayPort = resolved.gateway;
2921
3637
  const uiPort = options.uiPort !== void 0 ? options.uiPort : gatewayPort ? gatewayPort + 1 : 0;
2922
3638
  const apiPort = options.apiPort !== void 0 ? options.apiPort : gatewayPort ? gatewayPort + 2 : 0;
2923
3639
  const webDir = resolveWebDir(options.webDir, options.workspaceRoot);
@@ -2971,18 +3687,23 @@ function startGatewayCluster(options) {
2971
3687
  }
2972
3688
 
2973
3689
  // bin/esedre.ts
2974
- function printHelp() {
3690
+ function printHelp(config) {
3691
+ const port = config?.port || DEFAULT_ESEDRE_PORT;
3692
+ const projectCode = config?.projectCode;
3693
+ const webUrl = `http://localhost:${port}/app${projectCode ? "?project=" + encodeURIComponent(projectCode) : "?project=all"}`;
2975
3694
  console.log(`
2976
- ${colors.bold}${colors.cyan}Esedre CLI (/eh-seh-dreh/)${colors.reset}: Developer Roadmap & Companion LLM Coordination Engine
3695
+ ${colors.bold}${colors.cyan}Esedre CLI (/eh-seh-dreh/)${colors.reset}: Developer Roadmap & LLM Coding Partner Coordination Engine
3696
+
3697
+ ${colors.bold}WEB UI:${colors.reset}
3698
+ Interactive Planner: ${colors.cyan}${webUrl}${colors.reset}
2977
3699
 
2978
3700
  ${colors.bold}USAGE:${colors.reset}
2979
3701
  .esedre/esedre <command> [options]
2980
3702
  .esedre/ese <command> [options]
2981
3703
  esedre <command> [options]
2982
3704
  ese <command> [options]
2983
- node tools/esedre.mjs <command> [options]
2984
3705
 
2985
- ${colors.bold}COMMANDS:${colors.reset}
3706
+ ${colors.bold}ROADMAP COMMANDS (Pair Programming & LLM Agents):${colors.reset}
2986
3707
  ${colors.bold}list${colors.reset} [-p|--project <code|all>] [-s|--status <status>] [-t|--type <type>] [-q|--search <q>] [--json]
2987
3708
  List roadmap tickets with optional filters (defaults to active project).
2988
3709
  \u2022 Status values: 'Planned', 'In Development', 'Completed', 'Rejected'
@@ -2997,46 +3718,48 @@ ${colors.bold}COMMANDS:${colors.reset}
2997
3718
  ${colors.bold}create${colors.reset} --title "..." [-p|--project <code>] [-t|--type <type>] [--complexity <c>] [--effort "<e>"] [--summary "<s>"] [--json]
2998
3719
  Mint a new roadmap ticket with sequential numeric ID.
2999
3720
 
3000
- ${colors.bold}update${colors.reset} <id> [-s|--status <status>] [--title "..."] [--flag] [--no-flag] [--last-hash <sha1>] [--force] [--json]
3001
- Mutate ticket status, title, active state, or feature flag with OCC protection.
3721
+ ${colors.bold}update${colors.reset} <id> [-s|--status <status>] [-t|--type <type>] [--title "..."] [--complexity <c>] [--effort "<e>"] [--in-dev] [--flag <name>] [--last-hash <sha1>] [--force] [--json]
3722
+ Mutate ticket status, type, title, complexity, effort, active state, or feature flag with OCC protection.
3002
3723
 
3003
- ${colors.bold}comment${colors.reset} <id> --text "..." [--author "..."] [--json]
3724
+ ${colors.bold}comment${colors.reset} <id> ["<text>"] [--text "..."] [--author "..."] [--json]
3004
3725
  Append a research finding, test verification, or note to ticket history.
3005
3726
 
3006
3727
  ${colors.bold}snapshot${colors.reset} [--project <code>] [--json]
3007
3728
  Generate lean read-only projection snapshot (.esedre/snapshot.json) for zero-latency agent context.
3008
3729
 
3009
- ${colors.bold}configure${colors.reset} [--project <code>] [-n|--name <name>] [--allow <c1,c2>] [--port <n>] [--no-mcp] [--proxy] [--no-proxy] [-y|--yes] [--json]
3010
- Initialize or update .esedre footprint, register project code & display name in Esedre,
3011
- plant wrappers (.esedre/esedre.cmd, .esedre/ese.cmd, etc.), configure .gitignore,
3012
- MCP server config, agent skill templates, and display guided next steps.
3013
-
3014
- ${colors.bold}upgrade${colors.reset} [--json]
3015
- Upgrade workspace configuration schema, wrappers, and agent skills. Refreshes snapshot.
3016
-
3017
- ${colors.bold}projects${colors.reset} [--json]
3018
- List registered workspace projects, project codes (e.g. 'CORE', 'WEB', 'DOCS'), and descriptions.
3019
-
3020
- ${colors.bold}daemon${colors.reset} [start|stop|status|logs] [--port <n>] [--quiet] [--json]
3021
- Manage background daemon process (start, stop, check status, or view logs).
3022
-
3023
- ${colors.bold}serve${colors.reset} [--port <n>]
3024
- Run the unified gateway & web server in foreground (default 5674).
3025
-
3026
- ${colors.bold}start${colors.reset} [--port <n>] [--quiet] [--json]
3027
- Alias for 'daemon start'. Start Esedre background daemon process.
3730
+ ${colors.bold}SERVICE DAEMON COMMANDS:${colors.reset}
3731
+ ${colors.bold}start${colors.reset} [--port <n>] [--foreground | -f] [--quiet] [--json]
3732
+ Start the Esedre background server and web UI daemon (default port ${DEFAULT_ESEDRE_PORT}).
3733
+ Use --foreground (-f) to run directly in terminal foreground (blocks terminal, Ctrl+C to stop).
3028
3734
 
3029
3735
  ${colors.bold}stop${colors.reset} [--port <n>] [--quiet] [--json]
3030
- Alias for 'daemon stop'. Stop the running Esedre background daemon process.
3736
+ Stop the running Esedre background server daemon.
3031
3737
 
3032
3738
  ${colors.bold}status${colors.reset} [--port <n>] [--json]
3033
- Alias for 'daemon status'. Check health and diagnostics of the daemon process.
3739
+ Check health, uptime, and diagnostics of the running Esedre server.
3034
3740
 
3035
3741
  ${colors.bold}logs${colors.reset} [--port <n>] [--lines <n>]
3036
- Tail recent output logs from the Esedre daemon process.
3742
+ Tail recent logs from the Esedre background server.
3037
3743
 
3038
3744
  ${colors.bold}mcp${colors.reset} Start the Model Context Protocol (MCP) JSON-RPC 2.0 stdio server for autonomous LLM coding agents.
3039
3745
 
3746
+ ${colors.bold}DEVELOPER ADMINISTRATION (Human Setup & Configuration):${colors.reset}
3747
+ ${colors.bold}init${colors.reset} [<path>] [--project <code>] [-n|--name <name>] [--allow <c1,c2>] [--port <n>] [--hub] [--proxy] [--no-proxy] [-y|--yes] [--json]
3748
+ Bring a project repository online (.esedre footprint, wrappers, snapshot) or bootstrap a data hub (--hub).
3749
+
3750
+ ${colors.bold}configure${colors.reset} [add <path> | remove <code|path> | set <key> <val> | get <key>] [--json]
3751
+ Inspect or mutate central Esedre configuration (~/.esedre/config.json).
3752
+ \u2022 add <path> Smart-detect and link a data hub or project repository
3753
+ \u2022 remove <target> Unlink a project or data hub
3754
+ \u2022 set <key> <val> Update a central configuration setting
3755
+
3756
+ ${colors.bold}upgrade${colors.reset} [<path>] [--force|-f] [--json]
3757
+ Upgrade workspace configuration schema, wrappers, and agent skills. Refreshes snapshot.
3758
+ Pass --force to overwrite customized skills with the latest official template.
3759
+
3760
+ ${colors.bold}projects${colors.reset} [--json]
3761
+ List registered workspace projects, project codes (e.g. 'CORE', 'WEB', 'DOCS'), and descriptions.
3762
+
3040
3763
  ${colors.bold}OPTIONS:${colors.reset}
3041
3764
  -p, --project <code> Project code (e.g. CORE, ALCE, DOCS).
3042
3765
  -n, --name <name> Project display name (e.g. "Alce Web Reader").
@@ -3046,6 +3769,7 @@ ${colors.bold}OPTIONS:${colors.reset}
3046
3769
  --json Output raw machine-readable JSON (strongly recommended for autonomous LLM coding agents).
3047
3770
  --last-hash <hash> Optimistic concurrency control: last known sha1 hash of the ticket from 'get'.
3048
3771
  --force Bypass optimistic concurrency last-hash conflict checks on writes.
3772
+ -v, --version Show the current version number.
3049
3773
  -h, --help Show this help reference.
3050
3774
 
3051
3775
  ${colors.bold}TYPICAL AGENT WORKFLOW:${colors.reset}
@@ -3102,50 +3826,97 @@ function parseArgs(rawArgs) {
3102
3826
  if (flags["n"] && !flags["name"]) flags["name"] = flags["n"];
3103
3827
  if (flags["s"] && !flags["status"]) flags["status"] = flags["s"];
3104
3828
  if (flags["q"] && !flags["search"]) flags["search"] = flags["q"];
3829
+ if (flags["f"] && !flags["foreground"]) flags["foreground"] = flags["f"];
3105
3830
  return { command, positionals, flags };
3106
3831
  }
3107
3832
  async function main() {
3108
3833
  const rawArgs = process.argv.slice(2);
3109
3834
  const { command, positionals, flags } = parseArgs(rawArgs);
3835
+ try {
3836
+ writeGlobalVersion();
3837
+ } catch {
3838
+ }
3839
+ const discovered = findEsedreConfig(process.cwd(), { fallbackToGlobal: true });
3840
+ const isJson = Boolean(flags["json"]);
3841
+ if (flags["version"] || flags["v"] || command === "version") {
3842
+ if (isJson) {
3843
+ console.log(JSON.stringify({ version: CURRENT_ESEDRE_VERSION }));
3844
+ } else {
3845
+ console.log(`esedre v${CURRENT_ESEDRE_VERSION}`);
3846
+ }
3847
+ return;
3848
+ }
3110
3849
  if (!command || flags["help"] || flags["h"]) {
3111
- printHelp();
3850
+ printHelp(discovered.config);
3112
3851
  return;
3113
3852
  }
3114
- const isJson = Boolean(flags["json"]);
3115
- const discovered = findEsedreConfig(process.cwd());
3116
3853
  const rawStorage = new FilesystemStorageAdapter(discovered.workspaceRoot, discovered.config);
3117
3854
  const storage = new SecurityFilter(rawStorage, discovered.config);
3118
3855
  try {
3119
3856
  switch (command) {
3120
- case "configure": {
3857
+ case "init": {
3858
+ const targetDir = positionals[0] ? path9.resolve(positionals[0]) : discovered.workspaceRoot;
3859
+ const isHub = Boolean(flags["hub"]);
3860
+ if (isHub) {
3861
+ const hubRes = initHub(targetDir);
3862
+ try {
3863
+ addLocationToGlobalConfig(targetDir);
3864
+ } catch {
3865
+ }
3866
+ if (isJson) {
3867
+ console.log(JSON.stringify(hubRes, null, 2));
3868
+ } else {
3869
+ console.log(`${colors.bold}${colors.green}\u2714 Initialized Esedre ticket data hub!${colors.reset}`);
3870
+ console.log(` \u2022 Location: ${hubRes.hubDir}`);
3871
+ console.log(` \u2022 Projects file: ${hubRes.projectsJsonCreated ? "Created projects.json" : "Existing projects.json"}`);
3872
+ console.log(` \u2022 Projects dir: ${hubRes.projectsDirCreated ? "Created projects/ directory" : "Existing projects/ directory"}`);
3873
+ console.log(` \u2022 Global Config: Registered as dataDir in ~/.esedre/config.json`);
3874
+ console.log(`
3875
+ ${colors.bold}Next Steps:${colors.reset}`);
3876
+ console.log(` 1. Link project repositories to this hub using:`);
3877
+ console.log(` ${colors.cyan}ese configure add <projectPath>${colors.reset}`);
3878
+ console.log(` 2. Start the Esedre background server:`);
3879
+ console.log(` ${colors.cyan}ese start${colors.reset}`);
3880
+ }
3881
+ return;
3882
+ }
3121
3883
  let projectCode = flags["project"] || flags["p"] || discovered.config?.projectCode;
3122
3884
  let projectName = flags["name"] || flags["n"] || discovered.config?.projectName;
3123
3885
  const allowArg = flags["allow"];
3124
- const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
3886
+ const port = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
3125
3887
  const setupMcp = flags["no-mcp"] ? false : true;
3126
3888
  const isYes = Boolean(flags["yes"] || flags["y"]);
3127
- if (process.stdin.isTTY && (!projectCode || !projectName) && !isJson) {
3889
+ const detected = detectProjectManifest(targetDir);
3890
+ const candidateCode = detected.candidateCode;
3891
+ const candidateName = detected.candidateName;
3892
+ if (!isJson && detected.manifestFile && !projectCode) {
3893
+ console.log(`${colors.dim} \u2022 Detected ${detected.manifestType} project (${detected.manifestFile}): ${candidateName}${colors.reset}`);
3894
+ }
3895
+ if (isYes) {
3896
+ if (!projectCode) projectCode = candidateCode;
3897
+ if (!projectName) projectName = candidateName || projectCode;
3898
+ } else if (process.stdin.isTTY && (!projectCode || !projectName) && !isJson) {
3128
3899
  const readline2 = await import("node:readline/promises");
3129
3900
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3130
3901
  try {
3131
3902
  if (!projectCode) {
3132
- const codeAnswer = await rl.question(
3133
- `${colors.cyan}?${colors.reset} Project code (1-6 alphanumeric characters, e.g. ALCE, DOCS): `
3134
- );
3135
- projectCode = codeAnswer.trim();
3903
+ const promptMsg = candidateCode ? `${colors.cyan}?${colors.reset} Project code (1-${MAX_PROJECT_CODE_LENGTH} alphanumeric characters) [${candidateCode}]: ` : `${colors.cyan}?${colors.reset} Project code (1-${MAX_PROJECT_CODE_LENGTH} alphanumeric characters, e.g. ALCE, DOCS): `;
3904
+ const codeAnswer = await rl.question(promptMsg);
3905
+ projectCode = codeAnswer.trim() || candidateCode;
3136
3906
  }
3137
3907
  if (!projectName && projectCode) {
3908
+ const defaultDisplayName = candidateName || `${projectCode} App`;
3138
3909
  const nameAnswer = await rl.question(
3139
- `${colors.cyan}?${colors.reset} Project display name (e.g. ${projectCode} App) [${projectCode}]: `
3910
+ `${colors.cyan}?${colors.reset} Project display name [${defaultDisplayName}]: `
3140
3911
  );
3141
- projectName = nameAnswer.trim() || projectCode;
3912
+ projectName = nameAnswer.trim() || defaultDisplayName;
3142
3913
  }
3143
3914
  } finally {
3144
3915
  rl.close();
3145
3916
  }
3146
3917
  }
3147
3918
  if (!projectCode) {
3148
- console.error(`${colors.red}Error: Project code is required (--project <code>, -p <code>, or configure projectCode in esedre.json).${colors.reset}`);
3919
+ console.error(`${colors.red}Error: Project code is required (--project <code>, -p <code>, or in esedre.json).${colors.reset}`);
3149
3920
  process.exit(1);
3150
3921
  }
3151
3922
  const codeVal = validateProjectCode(projectCode);
@@ -3156,6 +3927,11 @@ async function main() {
3156
3927
  if (!projectName) {
3157
3928
  projectName = projectCode;
3158
3929
  }
3930
+ const nameVal = validateProjectName(projectName);
3931
+ if (!nameVal.valid) {
3932
+ console.error(`${colors.red}Error: ${nameVal.error}${colors.reset}`);
3933
+ process.exit(1);
3934
+ }
3159
3935
  const existingProjects = await storage.getProjects();
3160
3936
  const existing = existingProjects.find(
3161
3937
  (p) => p.code.toLowerCase() === projectCode.toLowerCase()
@@ -3184,10 +3960,10 @@ async function main() {
3184
3960
  } else if (flags["no-proxy"] !== void 0) {
3185
3961
  setupProxy = false;
3186
3962
  }
3187
- const viteConfigPath = findViteConfig(discovered.workspaceRoot);
3963
+ const viteConfigPath = findViteConfig(targetDir);
3188
3964
  if (viteConfigPath && setupProxy === void 0 && !isJson) {
3189
3965
  try {
3190
- const rawContent = fs8.readFileSync(viteConfigPath, "utf-8");
3966
+ const rawContent = fs9.readFileSync(viteConfigPath, "utf-8");
3191
3967
  const alreadyConfigured = rawContent.includes("'/esedre'") || rawContent.includes('"/esedre"');
3192
3968
  if (!alreadyConfigured) {
3193
3969
  if (isYes) {
@@ -3197,7 +3973,7 @@ async function main() {
3197
3973
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3198
3974
  try {
3199
3975
  const answer = await rl.question(
3200
- `${colors.cyan}?${colors.reset} Detected ${colors.bold}${path8.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
3976
+ `${colors.cyan}?${colors.reset} Detected ${colors.bold}${path9.basename(viteConfigPath)}${colors.reset}. Would you like to configure a reverse proxy for ${colors.bold}/esedre${colors.reset} targeting http://127.0.0.1:${port}? [Y/n] `
3201
3977
  );
3202
3978
  const trimmed = answer.trim().toLowerCase();
3203
3979
  setupProxy = trimmed === "" || trimmed === "y" || trimmed === "yes";
@@ -3209,20 +3985,36 @@ async function main() {
3209
3985
  } catch {
3210
3986
  }
3211
3987
  }
3212
- const res = configureWorkspace(discovered.workspaceRoot, {
3988
+ const rawHub = flags["data-dir"] || flags["hub"] || discovered.config?.dataDir;
3989
+ let hubRelPath = void 0;
3990
+ if (rawHub) {
3991
+ const singleHub = Array.isArray(rawHub) ? rawHub[0] : rawHub;
3992
+ const resolvedHub = path9.resolve(expandHome(singleHub));
3993
+ if (fs9.existsSync(resolvedHub)) {
3994
+ const rel = path9.relative(targetDir, resolvedHub).replace(/\\/g, "/");
3995
+ hubRelPath = rel.startsWith(".") ? rel : "./" + rel;
3996
+ }
3997
+ }
3998
+ const res = initWorkspace(targetDir, {
3213
3999
  projectCode,
3214
4000
  projectName: registeredProj.name,
3215
4001
  allowedProjects,
4002
+ dataDir: hubRelPath,
3216
4003
  port,
3217
4004
  setupMcp,
3218
4005
  setupProxy
3219
4006
  });
4007
+ try {
4008
+ addLocationToGlobalConfig(targetDir);
4009
+ } catch {
4010
+ }
3220
4011
  let snapshotTickets = 0;
3221
4012
  try {
3222
- const snapshot = await generateProjectSnapshot(storage, projectCode, discovered.workspaceRoot);
4013
+ const snapshot = await generateProjectSnapshot(storage, projectCode, targetDir);
3223
4014
  snapshotTickets = snapshot.totalTickets;
3224
4015
  } catch {
3225
4016
  }
4017
+ const effectivePort = resolvePorts(discovered.config, { port }).gateway;
3226
4018
  if (isJson) {
3227
4019
  console.log(JSON.stringify({
3228
4020
  ...res,
@@ -3230,9 +4022,9 @@ async function main() {
3230
4022
  projectNewlyRegistered,
3231
4023
  snapshotTickets,
3232
4024
  urls: {
3233
- ui: `http://localhost:${port}/app?project=${projectCode}`,
3234
- portfolio: `http://localhost:${port}/app?project=all`,
3235
- api: `http://localhost:${port}/api`
4025
+ ui: `http://localhost:${effectivePort}/app?project=${projectCode}`,
4026
+ portfolio: `http://localhost:${effectivePort}/app?project=all`,
4027
+ api: `http://localhost:${effectivePort}/api`
3236
4028
  },
3237
4029
  commands: {
3238
4030
  start: "ese start",
@@ -3243,7 +4035,7 @@ async function main() {
3243
4035
  }
3244
4036
  }, null, 2));
3245
4037
  } else {
3246
- console.log(`${colors.bold}${colors.green}\u2714 Esedre workspace configured successfully!${colors.reset}`);
4038
+ console.log(`${colors.bold}${colors.green}\u2714 Esedre workspace initialized successfully!${colors.reset}`);
3247
4039
  console.log(` \u2022 Config: .esedre/esedre.json (Project: ${projectCode} (${registeredProj.name}), Version: ${CURRENT_ESEDRE_VERSION})`);
3248
4040
  if (projectNewlyRegistered) {
3249
4041
  console.log(` \u2022 Project: ${colors.green}Registered new project '${projectCode}' (${registeredProj.name}) in Esedre${colors.reset}`);
@@ -3255,13 +4047,9 @@ async function main() {
3255
4047
  console.log(` \u2022 Snapshot: Generated .esedre/snapshot.json (${snapshotTickets} tickets projected)`);
3256
4048
  console.log(` \u2022 MCP: ${res.mcpConfigured ? "Configured in .agents/mcp_config.json" : "Skipped"}`);
3257
4049
  console.log(` \u2022 Skill: ${res.skillConfigured ? "Updated .agents/skills/esedre/SKILL.md" : `Retained (${res.skillStatus})`}`);
3258
- if (res.globalStoreStatus === "ready") {
3259
- console.log(` \u2022 Global Store: ${res.globalStorePath} (verified write permissions)`);
3260
- } else if (res.globalStoreStatus === "warning") {
3261
- console.log(` \u2022 ${colors.yellow}Global Store: Fallback to ${res.globalStorePath} (${res.globalStoreError})${colors.reset}`);
3262
- }
4050
+ console.log(` \u2022 Central Config: Linked into ~/.esedre/config.json`);
3263
4051
  if (res.viteProxyStatus === "CONFIGURED") {
3264
- console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${port}${colors.reset}`);
4052
+ console.log(` \u2022 ${colors.green}Vite Proxy: Added /esedre proxy to ${res.viteConfigFile} targeting http://127.0.0.1:${effectivePort}${colors.reset}`);
3265
4053
  } else if (res.viteProxyStatus === "ALREADY_CONFIGURED") {
3266
4054
  console.log(` \u2022 Vite Proxy: /esedre is already configured in ${res.viteConfigFile}`);
3267
4055
  } else if (res.viteProxyStatus === "SKIPPED") {
@@ -3271,7 +4059,7 @@ ${colors.bold}Recommendation:${colors.reset} To embed the dev planner within you
3271
4059
  console.log(colors.dim + (res.recommendedProxySnippet?.split("\n").map((l) => " " + l).join("\n") || "") + colors.reset);
3272
4060
  } else if (res.viteProxyStatus === "NOT_APPLICABLE") {
3273
4061
  console.log(`
3274
- ${colors.bold}Recommendation:${colors.reset} If you wish to embed the dev planner within your project's UI, configure a reverse proxy for '/esedre' targeting http://127.0.0.1:${port}:`);
4062
+ ${colors.bold}Recommendation:${colors.reset} If you wish to embed the dev planner within your project's UI, configure a reverse proxy for '/esedre' targeting http://127.0.0.1:${effectivePort}:`);
3275
4063
  console.log(colors.dim + (res.recommendedProxySnippet?.split("\n").map((l) => " " + l).join("\n") || "") + colors.reset);
3276
4064
  } else if (res.viteProxyStatus === "MANUAL_REQUIRED") {
3277
4065
  console.log(` \u2022 ${colors.yellow}Vite Proxy: Could not automatically inject proxy into ${res.viteConfigFile}.${colors.reset}`);
@@ -3281,11 +4069,11 @@ ${colors.bold}Recommendation:${colors.reset} Please manually add this proxy to y
3281
4069
  }
3282
4070
  console.log(`
3283
4071
  ${colors.bold}Next Steps:${colors.reset}`);
3284
- console.log(` 1. ${colors.bold}Start the background daemon:${colors.reset}`);
4072
+ console.log(` 1. ${colors.bold}Start the background server:${colors.reset}`);
3285
4073
  console.log(` ${colors.cyan}ese start${colors.reset} (or: ${colors.dim}.esedre/ese start${colors.reset})`);
3286
4074
  console.log(`
3287
4075
  2. ${colors.bold}Open the visual Web UI:${colors.reset}`);
3288
- console.log(` ${colors.cyan}http://localhost:${port}/app?project=${projectCode}${colors.reset}`);
4076
+ console.log(` ${colors.cyan}http://localhost:${effectivePort}/app?project=${projectCode}${colors.reset}`);
3289
4077
  if (res.viteProxyStatus === "CONFIGURED" || res.viteProxyStatus === "ALREADY_CONFIGURED") {
3290
4078
  console.log(` ${colors.dim}Embedded via host dev server:${colors.reset} http://localhost:5173/esedre/app?project=${projectCode}`);
3291
4079
  }
@@ -3298,18 +4086,129 @@ ${colors.bold}Next Steps:${colors.reset}`);
3298
4086
  }
3299
4087
  return;
3300
4088
  }
4089
+ case "configure": {
4090
+ const subAction = positionals[0]?.toLowerCase();
4091
+ if (subAction === "add") {
4092
+ const targetPath = positionals[1] || ".";
4093
+ const addRes = addLocationToGlobalConfig(targetPath);
4094
+ if (isJson) {
4095
+ console.log(JSON.stringify(addRes, null, 2));
4096
+ return;
4097
+ }
4098
+ if (addRes.type === "hub") {
4099
+ console.log(`${colors.bold}${colors.green}\u2714 Detected Esedre Data Hub (${addRes.projectCount || 0} projects)${colors.reset}`);
4100
+ console.log(` \u2022 Registered in ~/.esedre/config.json -> dataDir: ${addRes.path}`);
4101
+ } else if (addRes.type === "project") {
4102
+ console.log(`${colors.bold}${colors.green}\u2714 Detected Esedre project '${addRes.code}'${addRes.name ? ` (${addRes.name})` : ""}${colors.reset}`);
4103
+ console.log(` \u2022 Linked in ~/.esedre/config.json -> projects.${addRes.code}: ${addRes.path}`);
4104
+ } else {
4105
+ console.log(`${colors.yellow}\u26A0\uFE0F No Esedre project or data hub detected at "${addRes.path}"${colors.reset}`);
4106
+ console.log(`\u{1F449} Run 'ese init ${targetPath}' to bring this repository online first.`);
4107
+ }
4108
+ return;
4109
+ }
4110
+ if (subAction === "remove" || subAction === "rm") {
4111
+ const target = positionals[1];
4112
+ if (!target) {
4113
+ console.error(`${colors.red}Error: Project code or directory path required (e.g. ese configure remove ALCE)${colors.reset}`);
4114
+ process.exit(1);
4115
+ }
4116
+ const rmRes = removeLocationFromGlobalConfig(target);
4117
+ if (isJson) {
4118
+ console.log(JSON.stringify(rmRes, null, 2));
4119
+ return;
4120
+ }
4121
+ if (rmRes.removed) {
4122
+ console.log(`${colors.green}\u2714 Removed ${rmRes.type || "entry"} "${rmRes.target || target}" from central configuration (~/.esedre/config.json)${colors.reset}`);
4123
+ } else {
4124
+ console.log(`${colors.yellow}\u25CB "${target}" not found in central configuration.${colors.reset}`);
4125
+ }
4126
+ return;
4127
+ }
4128
+ if (subAction === "set") {
4129
+ const key = positionals[1];
4130
+ const val = positionals[2];
4131
+ if (!key || val === void 0) {
4132
+ console.error(`${colors.red}Error: Key and value required (e.g. ese configure set port 5674)${colors.reset}`);
4133
+ process.exit(1);
4134
+ }
4135
+ const parsedVal = /^\d+$/.test(val) ? parseInt(val, 10) : val;
4136
+ const setRes = setGlobalConfigKey(key, parsedVal);
4137
+ if (isJson) {
4138
+ console.log(JSON.stringify(setRes, null, 2));
4139
+ return;
4140
+ }
4141
+ console.log(`${colors.green}\u2714 Updated central configuration: ${key} = ${JSON.stringify(parsedVal)}${colors.reset}`);
4142
+ return;
4143
+ }
4144
+ if (subAction === "get") {
4145
+ const key = positionals[1];
4146
+ const cfg = readGlobalConfig() || {};
4147
+ const val = key ? getGlobalConfigKey(key) : cfg;
4148
+ if (isJson) {
4149
+ console.log(JSON.stringify({ key, value: val }, null, 2));
4150
+ } else {
4151
+ console.log(val !== void 0 ? typeof val === "object" ? JSON.stringify(val, null, 2) : String(val) : `${colors.dim}(not set)${colors.reset}`);
4152
+ }
4153
+ return;
4154
+ }
4155
+ const globalCfg = readGlobalConfig() || {};
4156
+ const configPath = getGlobalConfigPath();
4157
+ if (isJson) {
4158
+ console.log(JSON.stringify({
4159
+ configPath,
4160
+ config: globalCfg
4161
+ }, null, 2));
4162
+ return;
4163
+ }
4164
+ console.log(`
4165
+ ${colors.bold}${colors.cyan}Esedre Central Configuration${colors.reset}`);
4166
+ console.log(` \u2022 Config File: ${configPath} ${fs9.existsSync(configPath) ? colors.green + "(active)" + colors.reset : colors.dim + "(default)" + colors.reset}`);
4167
+ console.log(` \u2022 Default Port: ${globalCfg.port || DEFAULT_ESEDRE_PORT}`);
4168
+ console.log(`
4169
+ ${colors.bold}Data Hubs (dataDir):${colors.reset}`);
4170
+ if (globalCfg.dataDir) {
4171
+ const hubs = Array.isArray(globalCfg.dataDir) ? globalCfg.dataDir : [globalCfg.dataDir];
4172
+ for (const h of hubs) {
4173
+ console.log(` \u2022 ${colors.cyan}${h}${colors.reset}`);
4174
+ }
4175
+ } else {
4176
+ console.log(` ${colors.dim}(none configured: add with 'ese configure add <hubPath>')${colors.reset}`);
4177
+ }
4178
+ console.log(`
4179
+ ${colors.bold}Linked Projects (projects):${colors.reset}`);
4180
+ if (globalCfg.projects && Object.keys(globalCfg.projects).length > 0) {
4181
+ for (const [code, p] of Object.entries(globalCfg.projects)) {
4182
+ console.log(` \u2022 ${colors.bold}${code}${colors.reset}: ${colors.cyan}${p}${colors.reset}`);
4183
+ }
4184
+ } else {
4185
+ console.log(` ${colors.dim}(none linked: link with 'ese configure add <projectPath>')${colors.reset}`);
4186
+ }
4187
+ console.log(`
4188
+ ${colors.bold}Commands:${colors.reset}`);
4189
+ console.log(` \u2022 Link a hub or project: ${colors.cyan}ese configure add <path>${colors.reset}`);
4190
+ console.log(` \u2022 Unlink a project or hub: ${colors.cyan}ese configure remove <code|path>${colors.reset}`);
4191
+ console.log(` \u2022 Set setting value: ${colors.cyan}ese configure set <key> <value>${colors.reset}`);
4192
+ console.log(` \u2022 Bring new repo online: ${colors.cyan}ese init [path]${colors.reset}`);
4193
+ return;
4194
+ }
3301
4195
  case "upgrade": {
3302
- const res = configureWorkspace(discovered.workspaceRoot, {
3303
- projectCode: discovered.config?.projectCode,
3304
- allowedProjects: discovered.config?.allowedProjects,
3305
- port: discovered.config?.port
4196
+ const targetDir = positionals[0] ? path9.resolve(positionals[0]) : discovered.workspaceRoot;
4197
+ const targetDiscovered = targetDir === discovered.workspaceRoot ? discovered : findEsedreConfig(targetDir, { fallbackToGlobal: true });
4198
+ const isForce = Boolean(flags["force"] || flags["f"]);
4199
+ const res = configureWorkspace(targetDir, {
4200
+ projectCode: targetDiscovered.config?.projectCode,
4201
+ allowedProjects: targetDiscovered.config?.allowedProjects,
4202
+ port: targetDiscovered.config?.port,
4203
+ force: isForce
3306
4204
  });
3307
- const projectCode = discovered.config?.projectCode;
4205
+ const projectCode = targetDiscovered.config?.projectCode;
3308
4206
  if (!projectCode) {
3309
4207
  console.error(`${colors.red}Error: "projectCode" is required in esedre.json configuration (no default fallback).${colors.reset}`);
3310
4208
  process.exit(1);
3311
4209
  }
3312
- const snapshot = await generateProjectSnapshot(storage, projectCode, discovered.workspaceRoot);
4210
+ const targetStorage = targetDir === discovered.workspaceRoot ? storage : new SecurityFilter(new FilesystemStorageAdapter(targetDir, targetDiscovered.config), targetDiscovered.config);
4211
+ const snapshot = await generateProjectSnapshot(targetStorage, projectCode, targetDir);
3313
4212
  if (isJson) {
3314
4213
  console.log(JSON.stringify({ upgrade: res, snapshot: { totalTickets: snapshot.totalTickets } }, null, 2));
3315
4214
  } else {
@@ -3317,8 +4216,10 @@ ${colors.bold}Next Steps:${colors.reset}`);
3317
4216
  console.log(` \u2022 Esedre Engine Version: ${CURRENT_ESEDRE_VERSION}`);
3318
4217
  console.log(` \u2022 Wrappers updated in .esedre/`);
3319
4218
  console.log(` \u2022 Refreshed .esedre/snapshot.json (${snapshot.totalTickets} tickets projected)`);
3320
- if (res.skillStatus === "CUSTOMIZED") {
3321
- console.log(` \u2022 ${colors.yellow}Notice: .agents/skills/esedre/SKILL.md is customized; preserved user modifications.${colors.reset}`);
4219
+ if (res.skillConfigured) {
4220
+ console.log(` \u2022 ${colors.green}Updated .agents/skills/esedre/SKILL.md to latest template${colors.reset}`);
4221
+ } else if (res.skillStatus === "CUSTOMIZED") {
4222
+ console.log(` \u2022 ${colors.yellow}Notice: .agents/skills/esedre/SKILL.md is customized; preserved user modifications (use --force to overwrite).${colors.reset}`);
3322
4223
  }
3323
4224
  }
3324
4225
  return;
@@ -3346,7 +4247,32 @@ ${colors.bold}Next Steps:${colors.reset}`);
3346
4247
  return;
3347
4248
  }
3348
4249
  case "start": {
3349
- const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
4250
+ const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
4251
+ const ports = resolvePorts(discovered.config, { port: flagPort });
4252
+ const port = ports.gateway;
4253
+ const isForeground = Boolean(flags["foreground"] || flags["f"]);
4254
+ if (isForeground) {
4255
+ const cluster = startGatewayCluster({
4256
+ gatewayPort: ports.gateway,
4257
+ uiPort: ports.ui,
4258
+ apiPort: ports.api,
4259
+ storage,
4260
+ workspaceRoot: discovered.workspaceRoot
4261
+ });
4262
+ console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${ports.gateway}${colors.reset}`);
4263
+ console.log(` \u2022 Web UI: http://localhost:${ports.gateway}/app (internal: ${ports.ui})`);
4264
+ console.log(` \u2022 REST API: http://localhost:${ports.gateway}/api (internal: ${ports.api})`);
4265
+ console.log(` \u2022 Rewrite /: http://localhost:${ports.gateway}/ -> http://localhost:${ports.gateway}/app/`);
4266
+ console.log(` \u2022 Portfolio: http://localhost:${ports.gateway}/app?project=all`);
4267
+ console.log(` \u2022 Press Ctrl+C to stop.`);
4268
+ process.on("SIGINT", async () => {
4269
+ console.log(`
4270
+ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
4271
+ await cluster.close();
4272
+ process.exit(0);
4273
+ });
4274
+ return;
4275
+ }
3350
4276
  const quiet = Boolean(flags["quiet"] || isJson);
3351
4277
  const state = await startDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
3352
4278
  if (isJson) {
@@ -3355,7 +4281,8 @@ ${colors.bold}Next Steps:${colors.reset}`);
3355
4281
  return;
3356
4282
  }
3357
4283
  case "stop": {
3358
- const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
4284
+ const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
4285
+ const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
3359
4286
  const quiet = Boolean(flags["quiet"] || isJson);
3360
4287
  const stopped = await stopDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
3361
4288
  if (isJson) {
@@ -3364,7 +4291,8 @@ ${colors.bold}Next Steps:${colors.reset}`);
3364
4291
  return;
3365
4292
  }
3366
4293
  case "status": {
3367
- const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
4294
+ const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
4295
+ const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
3368
4296
  const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
3369
4297
  if (isJson) {
3370
4298
  console.log(JSON.stringify(status, null, 2));
@@ -3373,13 +4301,19 @@ ${colors.bold}Next Steps:${colors.reset}`);
3373
4301
  console.log(`${colors.bold}${colors.green}\u25CF Esedre daemon is RUNNING${colors.reset}`);
3374
4302
  console.log(` \u2022 Port: ${status.port}`);
3375
4303
  if (status.pid) console.log(` \u2022 PID: ${status.pid}`);
4304
+ if (status.version) console.log(` \u2022 Version: v${status.version}`);
3376
4305
  if (status.uptimeSeconds !== void 0) console.log(` \u2022 Uptime: ${status.uptimeSeconds}s`);
3377
4306
  if (status.startedAt) console.log(` \u2022 Started: ${status.startedAt}`);
3378
4307
  if (status.projects?.length) console.log(` \u2022 Projects: ${status.projects.join(", ")}`);
3379
4308
  if (status.logFile) console.log(` \u2022 Logs: ${status.logFile}`);
4309
+ if (status.staleVersion) {
4310
+ console.log(`
4311
+ ${colors.yellow}\u26A0\uFE0F Warning: Daemon is running v${status.version}, but v${status.installedVersion} is installed.${colors.reset}`);
4312
+ console.log(` Restart to apply updates: ${colors.cyan}ese stop && ese start${colors.reset}`);
4313
+ }
3380
4314
  } else {
3381
4315
  console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
3382
- if (status.logFile && fs8.existsSync(status.logFile)) {
4316
+ if (status.logFile && fs9.existsSync(status.logFile)) {
3383
4317
  console.log(` \u2022 Recent Logs: ${status.logFile}`);
3384
4318
  }
3385
4319
  }
@@ -3387,80 +4321,12 @@ ${colors.bold}Next Steps:${colors.reset}`);
3387
4321
  return;
3388
4322
  }
3389
4323
  case "logs": {
3390
- const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
4324
+ const flagPort = flags["port"] ? parseInt(String(flags["port"]), 10) : void 0;
4325
+ const port = resolvePorts(discovered.config, { port: flagPort }).gateway;
3391
4326
  const lines = flags["lines"] ? parseInt(String(flags["lines"]), 10) : 40;
3392
4327
  printDaemonLogs({ port, lines, workspaceRoot: discovered.workspaceRoot });
3393
4328
  return;
3394
4329
  }
3395
- case "serve": {
3396
- const gatewayPort = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
3397
- const cluster = startGatewayCluster({
3398
- gatewayPort,
3399
- uiPort: gatewayPort + 1,
3400
- apiPort: gatewayPort + 2,
3401
- storage,
3402
- workspaceRoot: discovered.workspaceRoot
3403
- });
3404
- console.log(`${colors.bold}${colors.green}\u2714 Esedre Server active on http://localhost:${gatewayPort}${colors.reset}`);
3405
- console.log(` \u2022 Web UI: http://localhost:${gatewayPort}/app (internal: ${gatewayPort + 1})`);
3406
- console.log(` \u2022 REST API: http://localhost:${gatewayPort}/api (internal: ${gatewayPort + 2})`);
3407
- console.log(` \u2022 Rewrite /: http://localhost:${gatewayPort}/ -> http://localhost:${gatewayPort}/app/`);
3408
- console.log(` \u2022 Portfolio: http://localhost:${gatewayPort}/app?project=all`);
3409
- console.log(` \u2022 Press Ctrl+C to stop.`);
3410
- process.on("SIGINT", async () => {
3411
- console.log(`
3412
- ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
3413
- await cluster.close();
3414
- process.exit(0);
3415
- });
3416
- return;
3417
- }
3418
- case "daemon": {
3419
- const sub = positionals[0] || "status";
3420
- const port = flags["port"] ? parseInt(String(flags["port"]), 10) : discovered.config?.port || 5674;
3421
- const quiet = Boolean(flags["quiet"] || isJson);
3422
- if (sub === "start") {
3423
- const state = await startDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
3424
- if (isJson) console.log(JSON.stringify(state, null, 2));
3425
- return;
3426
- } else if (sub === "stop") {
3427
- const stopped = await stopDaemon({ port, quiet, workspaceRoot: discovered.workspaceRoot });
3428
- if (isJson) console.log(JSON.stringify({ port, stopped }, null, 2));
3429
- return;
3430
- } else if (sub === "status") {
3431
- const status = await getDaemonStatus({ port, workspaceRoot: discovered.workspaceRoot });
3432
- if (isJson) {
3433
- console.log(JSON.stringify(status, null, 2));
3434
- } else {
3435
- if (status.running) {
3436
- console.log(`${colors.bold}${colors.green}\u25CF Esedre daemon is RUNNING${colors.reset}`);
3437
- console.log(` \u2022 Port: ${status.port}`);
3438
- if (status.pid) console.log(` \u2022 PID: ${status.pid}`);
3439
- if (status.uptimeSeconds !== void 0) console.log(` \u2022 Uptime: ${status.uptimeSeconds}s`);
3440
- if (status.startedAt) console.log(` \u2022 Started: ${status.startedAt}`);
3441
- if (status.projects?.length) console.log(` \u2022 Projects: ${status.projects.join(", ")}`);
3442
- if (status.logFile) console.log(` \u2022 Logs: ${status.logFile}`);
3443
- } else {
3444
- console.log(`${colors.yellow}\u25CB Esedre daemon is STOPPED (port ${status.port})${colors.reset}`);
3445
- if (status.logFile && fs8.existsSync(status.logFile)) {
3446
- console.log(` \u2022 Recent Logs: ${status.logFile}`);
3447
- }
3448
- }
3449
- }
3450
- return;
3451
- } else if (sub === "logs") {
3452
- const logFile = path8.resolve(discovered.workspaceRoot || process.cwd(), ".esedre", "daemon.log");
3453
- if (fs8.existsSync(logFile)) {
3454
- console.log(fs8.readFileSync(logFile, "utf-8"));
3455
- } else {
3456
- console.log(`${colors.dim}No daemon log file found at ${logFile}${colors.reset}`);
3457
- }
3458
- return;
3459
- } else {
3460
- console.error(`${colors.red}Unknown daemon subcommand: '${sub}'. Expected 'start', 'stop', 'status', or 'logs'.${colors.reset}`);
3461
- process.exit(1);
3462
- }
3463
- }
3464
4330
  case "projects": {
3465
4331
  const projects = await storage.getProjects();
3466
4332
  if (isJson) {
@@ -3494,9 +4360,22 @@ ${colors.dim}Shutting down Esedre cluster...${colors.reset}`);
3494
4360
  sha1: t.sha1 || t.meta.sha1
3495
4361
  })), null, 2));
3496
4362
  } else {
3497
- console.log(formatTicketListTable(tickets));
3498
- console.log(`
4363
+ if (tickets.length === 0) {
4364
+ console.log(`${colors.dim}No tickets found matching criteria.${colors.reset}`);
4365
+ const projects = await storage.getProjects();
4366
+ if (discovered.config?.projectCode) {
4367
+ console.log(`${colors.dim}Tip: Create your first ticket with 'ese create --title "..." --type Feature'.${colors.reset}`);
4368
+ } else if (projects.length > 0) {
4369
+ const codes = projects.map((p) => p.code).join(", ");
4370
+ console.log(`${colors.dim}Tip: Create a ticket with 'ese create --title "..." --type Feature --project <${codes}>'.${colors.reset}`);
4371
+ } else {
4372
+ console.log(`${colors.dim}Tip: Run 'ese init' to initialize this repository.${colors.reset}`);
4373
+ }
4374
+ } else {
4375
+ console.log(formatTicketListTable(tickets));
4376
+ console.log(`
3499
4377
  ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
4378
+ }
3500
4379
  }
3501
4380
  return;
3502
4381
  }
@@ -3535,11 +4414,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
3535
4414
  if (setPlan || filePath) {
3536
4415
  let content = setPlan;
3537
4416
  if (filePath) {
3538
- if (!fs8.existsSync(filePath)) {
4417
+ if (!fs9.existsSync(filePath)) {
3539
4418
  console.error(`${colors.red}Error: Plan file "${filePath}" not found.${colors.reset}`);
3540
4419
  process.exit(1);
3541
4420
  }
3542
- content = fs8.readFileSync(filePath, "utf-8");
4421
+ content = fs9.readFileSync(filePath, "utf-8");
3543
4422
  }
3544
4423
  await storage.savePlan(id, content, lastHash);
3545
4424
  console.log(`${colors.green}\u2714 Implementation plan saved for Ticket #${id}${colors.reset}`);
@@ -3568,10 +4447,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
3568
4447
  const projectCode = flags["project"] || discovered.config?.projectCode;
3569
4448
  if (!projectCode) {
3570
4449
  console.error(`${colors.red}Error: Project is required to create a ticket (--project <code> or configure projectCode in esedre.json).${colors.reset}`);
4450
+ console.error(`${colors.dim}Tip: Run 'ese init' to initialize this repository.${colors.reset}`);
3571
4451
  process.exit(1);
3572
4452
  }
3573
4453
  const complexity = flags["complexity"] || "Medium";
3574
- const estimatedEffort = flags["effort"] || "2.0 \u2013 4.0 hours";
4454
+ const estimatedEffort = flags["effort"] || "2.0 - 4.0 hours";
3575
4455
  const summary = flags["summary"] || void 0;
3576
4456
  const submittedBy = flags["author"] || "Developer";
3577
4457
  const val = validateProjectCode(projectCode);
@@ -3599,14 +4479,11 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
3599
4479
  case "update": {
3600
4480
  const idStr = positionals[0];
3601
4481
  if (!idStr) {
3602
- console.error(`${colors.red}Error: Ticket ID is required (e.g. ese update 96 --status "Completed")${colors.reset}`);
3603
- process.exit(1);
3604
- }
3605
- const id = parseInt(idStr, 10);
3606
- if (isNaN(id)) {
3607
- console.error(`${colors.red}Error: Invalid Ticket ID "${idStr}"${colors.reset}`);
4482
+ console.error(`${colors.red}Error: Ticket ID is required (e.g. ese update 96 --status "Completed" or ese update Profe-96)${colors.reset}`);
3608
4483
  process.exit(1);
3609
4484
  }
4485
+ const projectFlag = flags["project"];
4486
+ const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
3610
4487
  const status = flags["status"];
3611
4488
  const title = flags["title"];
3612
4489
  const complexity = flags["complexity"];
@@ -3627,36 +4504,34 @@ ${colors.dim}Total: ${tickets.length} tickets${colors.reset}`);
3627
4504
  if (effort) updates.estimatedEffort = effort;
3628
4505
  if (inDev !== void 0) updates.isActivePlanning = inDev;
3629
4506
  if (flag) updates.featureFlag = flag;
3630
- const updated = await storage.updateTicket(id, updates, lastHash);
4507
+ const updated = await storage.updateTicket(lookupKey, updates, lastHash);
3631
4508
  if (isJson) {
3632
4509
  console.log(JSON.stringify(updated, null, 2));
3633
4510
  } else {
3634
- console.log(`${colors.green}\u2714 Updated Ticket #${id}: ${updated.meta.title} (Status: ${updated.meta.status}, SHA-1: ${updated.sha1?.slice(0, 8)})${colors.reset}`);
4511
+ console.log(`${colors.green}\u2714 Updated Ticket #${lookupKey}: ${updated.meta.title} (Status: ${updated.meta.status}, SHA-1: ${updated.sha1?.slice(0, 8)})${colors.reset}`);
3635
4512
  }
3636
4513
  return;
3637
4514
  }
3638
4515
  case "comment": {
3639
4516
  const idStr = positionals[0];
3640
4517
  if (!idStr) {
3641
- console.error(`${colors.red}Error: Ticket ID is required (e.g. ese comment 96 --text "...")`);
3642
- process.exit(1);
3643
- }
3644
- const id = parseInt(idStr, 10);
3645
- if (isNaN(id)) {
3646
- console.error(`${colors.red}Error: Invalid Ticket ID "${idStr}"`);
4518
+ console.error(`${colors.red}Error: Ticket ID is required (e.g. ese comment 96 --text "..." or ese comment Profe-96)${colors.reset}`);
3647
4519
  process.exit(1);
3648
4520
  }
3649
- const text = flags["text"];
4521
+ const projectFlag = flags["project"];
4522
+ const lookupKey = projectFlag && /^\d+$/.test(idStr) ? `${projectFlag}-${idStr}` : idStr;
4523
+ const positionalText = positionals.slice(1).join(" ").trim();
4524
+ const text = flags["text"] || (positionalText || void 0);
3650
4525
  if (!text) {
3651
- console.error(`${colors.red}Error: --text is required`);
4526
+ console.error(`${colors.red}Error: Comment text is required (e.g. ese comment 96 "Verified tests" or --text "...")${colors.reset}`);
3652
4527
  process.exit(1);
3653
4528
  }
3654
4529
  const author = flags["author"] || "User";
3655
- const comment = await storage.addComment(id, { author, text });
4530
+ const comment = await storage.addComment(lookupKey, { author, text });
3656
4531
  if (isJson) {
3657
4532
  console.log(JSON.stringify(comment, null, 2));
3658
4533
  } else {
3659
- console.log(`${colors.green}\u2714 Added comment to Ticket #${id} by ${comment.author}${colors.reset}`);
4534
+ console.log(`${colors.green}\u2714 Added comment to Ticket #${lookupKey} by ${comment.author}${colors.reset}`);
3660
4535
  }
3661
4536
  return;
3662
4537
  }