@lovelybunch/api 1.0.75-alpha.0 → 1.0.75-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/lib/auth/auth-manager.d.ts +5 -0
  2. package/dist/lib/auth/auth-manager.js +9 -0
  3. package/dist/lib/storage/file-storage.d.ts +16 -16
  4. package/dist/lib/storage/file-storage.js +84 -79
  5. package/dist/lib/terminal/terminal-manager.d.ts +3 -5
  6. package/dist/lib/terminal/terminal-manager.js +19 -63
  7. package/dist/middleware/auth.js +36 -0
  8. package/dist/routes/api/v1/ai/index.js +0 -2
  9. package/dist/routes/api/v1/ai/route.js +445 -259
  10. package/dist/routes/api/v1/chats/[id]/index.js +2 -1
  11. package/dist/routes/api/v1/chats/[id]/route.d.ts +7 -0
  12. package/dist/routes/api/v1/chats/[id]/route.js +30 -1
  13. package/dist/routes/api/v1/context/index.js +0 -2
  14. package/dist/routes/api/v1/events/route.js +1 -1
  15. package/dist/routes/api/v1/knowledge/[filename]/index.d.ts +1 -0
  16. package/dist/routes/api/v1/knowledge/[filename]/index.js +1 -0
  17. package/dist/routes/api/v1/knowledge/[filename]/route.d.ts +3 -0
  18. package/dist/routes/api/v1/knowledge/[filename]/route.js +254 -0
  19. package/dist/routes/api/v1/knowledge/index.d.ts +1 -0
  20. package/dist/routes/api/v1/knowledge/index.js +1 -0
  21. package/dist/routes/api/v1/knowledge/route.d.ts +3 -0
  22. package/dist/routes/api/v1/knowledge/route.js +176 -0
  23. package/dist/routes/api/v1/mcp/index.js +109 -34
  24. package/dist/routes/api/v1/skills/[id]/index.d.ts +1 -0
  25. package/dist/routes/api/v1/skills/[id]/index.js +1 -0
  26. package/dist/routes/api/v1/skills/[id]/route.d.ts +3 -0
  27. package/dist/routes/api/v1/skills/[id]/route.js +199 -0
  28. package/dist/routes/api/v1/skills/index.d.ts +1 -0
  29. package/dist/routes/api/v1/skills/index.js +1 -0
  30. package/dist/routes/api/v1/skills/route.d.ts +3 -0
  31. package/dist/routes/api/v1/skills/route.js +329 -0
  32. package/dist/routes/api/v1/tasks/[id]/route.d.ts +351 -0
  33. package/dist/routes/api/v1/tasks/[id]/route.js +156 -0
  34. package/dist/routes/api/v1/tasks/index.d.ts +3 -0
  35. package/dist/routes/api/v1/tasks/index.js +10 -0
  36. package/dist/routes/api/v1/tasks/route.d.ts +329 -0
  37. package/dist/routes/api/v1/tasks/route.js +126 -0
  38. package/dist/routes/api/v1/terminal/[proposalId]/create/route.js +2 -2
  39. package/dist/routes/api/v1/terminal/[taskId]/create/index.d.ts +3 -0
  40. package/dist/routes/api/v1/terminal/[taskId]/create/index.js +5 -0
  41. package/dist/routes/api/v1/terminal/[taskId]/create/route.d.ts +10 -0
  42. package/dist/routes/api/v1/terminal/[taskId]/create/route.js +27 -0
  43. package/dist/routes/api/v1/terminal/[taskId]/destroy/index.d.ts +3 -0
  44. package/dist/routes/api/v1/terminal/[taskId]/destroy/index.js +5 -0
  45. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.d.ts +10 -0
  46. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.js +21 -0
  47. package/dist/routes/api/v1/terminal/[taskId]/resize/index.d.ts +3 -0
  48. package/dist/routes/api/v1/terminal/[taskId]/resize/index.js +5 -0
  49. package/dist/routes/api/v1/terminal/[taskId]/resize/route.d.ts +10 -0
  50. package/dist/routes/api/v1/terminal/[taskId]/resize/route.js +21 -0
  51. package/dist/routes/api/v1/terminal/sessions/route.js +4 -4
  52. package/dist/server-with-static.js +18 -14
  53. package/dist/server.js +15 -13
  54. package/package.json +8 -4
  55. package/static/assets/{ActivityPage-Dl3u61DF.js → ActivityPage-CbmEnYhg.js} +1 -1
  56. package/static/assets/ApiKeysSettingsPage-DLxQIqTT.js +2 -0
  57. package/static/assets/{ArchitectureEditPage-CVxBD88N.js → ArchitectureEditPage-CbtzgIQv.js} +4 -4
  58. package/static/assets/{ArchitecturePage-GchbFR8L.js → ArchitecturePage-CcAMfFZ8.js} +1 -1
  59. package/static/assets/{AuthSettingsPage-B7a-i1_T.js → AuthSettingsPage-4Prb7nAt.js} +2 -2
  60. package/static/assets/{CallbackPage-xYgfYT1Z.js → CallbackPage-CmWv_5Nh.js} +1 -1
  61. package/static/assets/CodePage-COC7rcwM.js +2 -0
  62. package/static/assets/{CollapsibleSection-ka7CEJhS.js → CollapsibleSection-C_VVeVSc.js} +1 -1
  63. package/static/assets/DashboardPage-BACFVIaW.js +41 -0
  64. package/static/assets/{GitPage-B-aSKe9Z.js → GitPage-rFngEr_A.js} +2 -2
  65. package/static/assets/GitSettingsPage-CDQovOqx.js +6 -0
  66. package/static/assets/IdentityPage-CS4REq2E.js +11 -0
  67. package/static/assets/{ImplementationStepsEditor-BC8bzHNj.js → ImplementationStepsEditor-CBIgppkZ.js} +2 -2
  68. package/static/assets/IntegrationsSettingsPage-B7HoraBH.js +1 -0
  69. package/static/assets/JobDetailPage-QUhWsaWV.js +1 -0
  70. package/static/assets/KnowledgeDetailPage-DCJstmIr.js +1 -0
  71. package/static/assets/KnowledgeEditPage-EYpFPfcJ.js +1 -0
  72. package/static/assets/KnowledgePage-D-7skvn5.js +8 -0
  73. package/static/assets/{LoginPage-Dp6lQSZW.js → LoginPage-L2aoqSDT.js} +1 -1
  74. package/static/assets/McpSettingsPage-CLwqDjw_.js +1 -0
  75. package/static/assets/NewKnowledgePage-Fq_QD8um.js +9 -0
  76. package/static/assets/NewSkillPage-DRcgovk0.js +1 -0
  77. package/static/assets/NewTaskPage-CleA8rH5.js +90 -0
  78. package/static/assets/ProjectEditPage-GIMOKgmh.js +11 -0
  79. package/static/assets/ProjectPage-DlWZOqnb.js +1 -0
  80. package/static/assets/PromptsSettingsPage-DFagGfo-.js +1 -0
  81. package/static/assets/ResourceDetailPage-CnFRpDec.js +1 -0
  82. package/static/assets/ResourcesPage-Cw3fu0JE.js +41 -0
  83. package/static/assets/{RoleEditPage-Ne8Dh8Tt.js → RoleEditPage-ARXq_eCs.js} +1 -1
  84. package/static/assets/{RolePage-3sb7lhw8.js → RolePage--XBBIrq8.js} +1 -1
  85. package/static/assets/{RulesSettingsPage-CEWEvNFC.js → RulesSettingsPage-DEqaRH96.js} +3 -3
  86. package/static/assets/SchedulePage-C_B2k9P6.js +4 -0
  87. package/static/assets/SkillDetailPage-Dr6xyKAX.js +1 -0
  88. package/static/assets/SkillEditPage-BsHXmfEZ.js +1 -0
  89. package/static/assets/SkillsPage-BrDdAAPx.js +8 -0
  90. package/static/assets/SkillsSettingsPage-Cvi7xaDE.js +1 -0
  91. package/static/assets/SourceInput-DxF_GGj8.js +1 -0
  92. package/static/assets/{TagInput-DRSgjTau.js → TagInput-DYvHbVZq.js} +1 -1
  93. package/static/assets/TaskDetailPage-DzcD6t03.js +1 -0
  94. package/static/assets/TaskEditPage-C7a6FOeJ.js +1 -0
  95. package/static/assets/TasksPage-ChWRSO_S.js +17 -0
  96. package/static/assets/TerminalPage-CoPwn8cU.js +1 -0
  97. package/static/assets/TerminalSessionPage-BQBZrOJa.js +13 -0
  98. package/static/assets/UserPreferencesPage-DjtU7veO.js +1 -0
  99. package/static/assets/UserSettingsPage-CfU8boJQ.js +1 -0
  100. package/static/assets/UtilitiesPage-sP1Crg-X.js +1 -0
  101. package/static/assets/{alert-CBWqAyb9.js → alert-BqZa-crG.js} +1 -1
  102. package/static/assets/{arrow-down-PY3QZW33.js → arrow-down-3faV_GyO.js} +1 -1
  103. package/static/assets/{arrow-left-JpEq5x30.js → arrow-left-FD3wQmzH.js} +1 -1
  104. package/static/assets/{arrow-up-qJ-_eNlY.js → arrow-up-BzP0YNVk.js} +1 -1
  105. package/static/assets/{badge-D6QJCkgl.js → badge-DRyeFib9.js} +1 -1
  106. package/static/assets/{browser-modal-DT5zGtM2.js → browser-modal-vnePkRfO.js} +2 -2
  107. package/static/assets/{card-CjY6k9qN.js → card-CuQs3dpy.js} +1 -1
  108. package/static/assets/{chevron-left-DiMUBSvf.js → chevron-left-QZIoYcVa.js} +1 -1
  109. package/static/assets/{plus-BCs8PAn6.js → chevron-up-DreyvhRd.js} +2 -2
  110. package/static/assets/{chevrons-up-CKVPmJ25.js → chevrons-up-CsAkc9vE.js} +1 -1
  111. package/static/assets/{circle-alert-Bc4Eo-aL.js → circle-alert-ewz28SE3.js} +1 -1
  112. package/static/assets/{circle-check-C8DZc7FJ.js → circle-check-2TuD-EHs.js} +1 -1
  113. package/static/assets/{circle-check-big-D_PvuS7-.js → circle-check-big-1xNuBPkR.js} +1 -1
  114. package/static/assets/{circle-play-Ba60SPvN.js → circle-play-C_w-qCn4.js} +1 -1
  115. package/static/assets/{circle-x-BYe1-ctZ.js → circle-x-B_d4wjG-.js} +1 -1
  116. package/static/assets/{clipboard-DZHDxSAN.js → clipboard-B6vBm1BP.js} +1 -1
  117. package/static/assets/{clock-E0FNqZih.js → clock-BIzEsx1g.js} +1 -1
  118. package/static/assets/{download-DbHXGD3a.js → download-Dv-RIvKK.js} +1 -1
  119. package/static/assets/droid-GYYyVzN-.js +18 -0
  120. package/static/assets/external-link-Cr8wjV6X.js +6 -0
  121. package/static/assets/{eye-CvcvqrS0.js → eye-DN958vyL.js} +1 -1
  122. package/static/assets/{folder-git-2-DK9b1q2x.js → folder-git-2-BproRzAR.js} +1 -1
  123. package/static/assets/index-Co_SJV3n.js +472 -0
  124. package/static/assets/index-GFQ5RqVh.css +2 -0
  125. package/static/assets/info-CzKk8mbR.js +6 -0
  126. package/static/assets/{label-DGhQh35t.js → label-h5GIKGcJ.js} +1 -1
  127. package/static/assets/{markdown-editor-52vxb0rp.js → markdown-editor-C6il4XWv.js} +1 -1
  128. package/static/assets/{pause-B7dyLcuH.js → pause-BDsjEmXM.js} +1 -1
  129. package/static/assets/{play-C3zkOZiK.js → play-CGsVQUJG.js} +1 -1
  130. package/static/assets/{radio-group-BseQ4OVV.js → radio-group-Bsd75ahK.js} +1 -1
  131. package/static/assets/{refresh-cw-ucF4I6xx.js → refresh-cw-qE1iNkL_.js} +1 -1
  132. package/static/assets/{search-TqAjHy3_.js → search-dvi0J4Dr.js} +1 -1
  133. package/static/assets/select-DylRS99W.js +1 -0
  134. package/static/assets/status-utils-CDkPeVfP.js +1 -0
  135. package/static/assets/{switch-B6px80Oh.js → switch-CsB3wpq9.js} +1 -1
  136. package/static/assets/{tabs-BHSWuVOa.js → tabs-Bw_4k2Rs.js} +1 -1
  137. package/static/assets/{tag-DjI_1POr.js → tag-Dh5PraRd.js} +1 -1
  138. package/static/assets/{terminal-preview-C0DTE3G7.js → terminal-preview-CfOb7xMx.js} +1 -1
  139. package/static/assets/use-terminal-T_tdJTCU.js +1 -0
  140. package/static/assets/video-bO6uuAjA.js +36 -0
  141. package/static/assets/{zap-DruahTJJ.js → zap-DHZ91NcK.js} +1 -1
  142. package/static/index.html +2 -2
  143. package/static/assets/AgentDetailPage-BJD22FSv.js +0 -1
  144. package/static/assets/AgentEditPage-ZYd0Rode.js +0 -1
  145. package/static/assets/AgentsPage-CUuxxQ2o.js +0 -3
  146. package/static/assets/AgentsSettingsPage-Dd8MrsKx.js +0 -6
  147. package/static/assets/ApiKeysSettingsPage-DTZStkII.js +0 -7
  148. package/static/assets/CodePage-xpXX0hlR.js +0 -2
  149. package/static/assets/DashboardPage-DQFlhP54.js +0 -41
  150. package/static/assets/GitSettingsPage-vLNJs4r3.js +0 -6
  151. package/static/assets/IdentityPage-ZAp_iARt.js +0 -11
  152. package/static/assets/IntegrationsSettingsPage-CqWZ8GUI.js +0 -1
  153. package/static/assets/JobDetailPage-BWq4XLO2.js +0 -1
  154. package/static/assets/KnowledgeDetailPage-C8evStGp.js +0 -1
  155. package/static/assets/KnowledgeEditPage-DquJprU_.js +0 -1
  156. package/static/assets/KnowledgePage-BBUKBhzS.js +0 -8
  157. package/static/assets/McpSettingsPage-BGIEripM.js +0 -1
  158. package/static/assets/NewAgentPage-D9hL7NY6.js +0 -1
  159. package/static/assets/NewKnowledgePage-CEdd8Yns.js +0 -9
  160. package/static/assets/NewProposalPage-B152OXnT.js +0 -90
  161. package/static/assets/ProjectEditPage-D0uSaGy0.js +0 -11
  162. package/static/assets/ProjectPage-DzWJky4Y.js +0 -1
  163. package/static/assets/PromptsSettingsPage-DH-4vcbF.js +0 -1
  164. package/static/assets/ProposalDetailPage-BI5HDp5I.js +0 -1
  165. package/static/assets/ProposalEditPage-D4412azZ.js +0 -1
  166. package/static/assets/ProposalsPage-D2gMbCBV.js +0 -17
  167. package/static/assets/ResourcesPage-7tbvXxDb.js +0 -71
  168. package/static/assets/SchedulePage-Crd-VMiL.js +0 -4
  169. package/static/assets/SourceInput-fvbAHpzT.js +0 -1
  170. package/static/assets/TerminalPage-BGZJDqvR.js +0 -1
  171. package/static/assets/TerminalSessionPage-DILvcnLG.js +0 -13
  172. package/static/assets/UserPreferencesPage-ChHrBs8H.js +0 -1
  173. package/static/assets/UserSettingsPage-Cv60YInV.js +0 -1
  174. package/static/assets/UtilitiesPage-ChLTn_u9.js +0 -1
  175. package/static/assets/calendar-sFw_mHQb.js +0 -6
  176. package/static/assets/droid-Caom7ttu.js +0 -4
  177. package/static/assets/index-C4LGSoHO.js +0 -468
  178. package/static/assets/index-CzjbtPHw.css +0 -2
  179. package/static/assets/use-terminal-KpWB08HL.js +0 -1
@@ -128,3 +128,8 @@ export declare class AuthManager {
128
128
  * @param configPath - Optional custom path to auth.json. If not provided, uses OS app data directory.
129
129
  */
130
130
  export declare function getAuthManager(configPath?: string): AuthManager;
131
+ /**
132
+ * Reset the singleton AuthManager (for testing isolation).
133
+ * Only effective when NODE_ENV is 'test'.
134
+ */
135
+ export declare function resetAuthManagerForTesting(): void;
@@ -420,3 +420,12 @@ export function getAuthManager(configPath) {
420
420
  }
421
421
  return authManagerInstance;
422
422
  }
423
+ /**
424
+ * Reset the singleton AuthManager (for testing isolation).
425
+ * Only effective when NODE_ENV is 'test'.
426
+ */
427
+ export function resetAuthManagerForTesting() {
428
+ if (process.env.NODE_ENV === 'test') {
429
+ authManagerInstance = null;
430
+ }
431
+ }
@@ -1,30 +1,30 @@
1
- import { ChangeProposal, CPStatus } from '@lovelybunch/types';
2
- export interface CPFilter {
3
- status?: CPStatus;
1
+ import { Task, TaskStatus } from '@lovelybunch/types';
2
+ export interface TaskFilter {
3
+ status?: TaskStatus;
4
4
  author?: string;
5
5
  priority?: string;
6
6
  tags?: string[];
7
7
  search?: string;
8
8
  }
9
9
  export interface StorageAdapter {
10
- createCP(cp: ChangeProposal): Promise<void>;
11
- getCP(id: string): Promise<ChangeProposal | null>;
12
- updateCP(id: string, cp: Partial<ChangeProposal>): Promise<void>;
13
- deleteCP(id: string): Promise<void>;
14
- listCPs(filter?: CPFilter): Promise<ChangeProposal[]>;
15
- searchCPs(query: string): Promise<ChangeProposal[]>;
10
+ createTask(task: Task): Promise<void>;
11
+ getTask(id: string): Promise<Task | null>;
12
+ updateTask(id: string, task: Partial<Task>): Promise<void>;
13
+ deleteTask(id: string): Promise<void>;
14
+ listTasks(filter?: TaskFilter): Promise<Task[]>;
15
+ searchTasks(query: string): Promise<Task[]>;
16
16
  }
17
17
  export declare class FileStorageAdapter implements StorageAdapter {
18
18
  private basePath;
19
19
  private sanitizeForYAML;
20
20
  constructor(basePath?: string);
21
21
  ensureDirectories(): Promise<void>;
22
- createCP(cp: ChangeProposal): Promise<void>;
23
- getCP(id: string): Promise<ChangeProposal | null>;
24
- updateCP(id: string, updates: Partial<ChangeProposal>): Promise<void>;
25
- deleteCP(id: string): Promise<void>;
26
- listCPs(filter?: CPFilter): Promise<ChangeProposal[]>;
27
- searchCPs(query: string): Promise<ChangeProposal[]>;
28
- private frontmatterToCP;
22
+ createTask(task: Task): Promise<void>;
23
+ getTask(id: string): Promise<Task | null>;
24
+ updateTask(id: string, updates: Partial<Task>): Promise<void>;
25
+ deleteTask(id: string): Promise<void>;
26
+ listTasks(filter?: TaskFilter): Promise<Task[]>;
27
+ searchTasks(query: string): Promise<Task[]>;
28
+ private frontmatterToTask;
29
29
  private getDefaultContent;
30
30
  }
@@ -49,68 +49,76 @@ export class FileStorageAdapter {
49
49
  }
50
50
  }
51
51
  async ensureDirectories() {
52
- const dirs = ['proposals', 'specs', 'flags', 'experiments', 'templates', 'jobs'];
52
+ const dirs = ['tasks', 'specs', 'flags', 'experiments', 'templates', 'jobs'];
53
53
  for (const dir of dirs) {
54
54
  await fs.mkdir(path.join(this.basePath, dir), { recursive: true });
55
55
  }
56
56
  }
57
- async createCP(cp) {
57
+ async createTask(task) {
58
58
  await this.ensureDirectories();
59
- // Extract content from the proposal if it exists
60
- const { content, ...frontmatter } = cp;
59
+ // Extract content from the task if it exists
60
+ const { content, ...frontmatter } = task;
61
61
  // Normalize date fields to Date instances in case callers provide ISO strings
62
- const createdAt = cp.metadata.createdAt instanceof Date
63
- ? cp.metadata.createdAt
64
- : new Date(cp.metadata.createdAt);
65
- const updatedAt = cp.metadata.updatedAt instanceof Date
66
- ? cp.metadata.updatedAt
67
- : new Date(cp.metadata.updatedAt);
68
- // Convert the proposal to markdown with YAML frontmatter
62
+ const createdAt = task.metadata.createdAt instanceof Date
63
+ ? task.metadata.createdAt
64
+ : new Date(task.metadata.createdAt);
65
+ const updatedAt = task.metadata.updatedAt instanceof Date
66
+ ? task.metadata.updatedAt
67
+ : new Date(task.metadata.updatedAt);
68
+ // Convert the task to markdown with YAML frontmatter
69
69
  const frontmatterData = this.sanitizeForYAML({
70
70
  // Required fields
71
- id: cp.id,
72
- title: cp.title,
71
+ id: task.id,
72
+ title: task.title,
73
73
  // Deprecated: intent is kept for backward compatibility during migration
74
74
  // TODO: Remove intent field in future version
75
- ...(cp.intent && { intent: cp.intent }),
75
+ ...(task.intent && { intent: task.intent }),
76
76
  createdAt: createdAt.toISOString(),
77
77
  updatedAt: updatedAt.toISOString(),
78
- status: cp.status,
79
- priority: cp.metadata.priority || 'medium',
78
+ status: task.status,
79
+ priority: task.metadata.priority || 'medium',
80
80
  // Author information
81
81
  author: {
82
- id: cp.author.id,
83
- name: cp.author.name,
84
- email: cp.author.email || '',
82
+ id: task.author.id,
83
+ name: task.author.name,
84
+ email: task.author.email || '',
85
85
  role: 'engineer', // Default role
86
- type: cp.author.type
87
- },
88
- // Plan
89
- plan: {
90
- estimatedDuration: 'TBD',
91
- dependencies: [],
92
- steps: cp.planSteps.map(step => ({
93
- id: step.id,
94
- description: step.description,
95
- status: step.status
96
- }))
86
+ type: task.author.type
97
87
  },
88
+ // Steps (top-level for consistency with CLI storage format)
89
+ steps: task.planSteps.map(step => ({
90
+ id: step.id,
91
+ description: step.description,
92
+ status: step.status,
93
+ ...(step.command && { command: step.command }),
94
+ ...(step.expectedOutcome && { expectedOutcome: step.expectedOutcome }),
95
+ ...(step.output && { output: step.output }),
96
+ ...(step.executedAt && { executedAt: step.executedAt instanceof Date ? step.executedAt.toISOString() : step.executedAt }),
97
+ })),
98
98
  // Metadata
99
- tags: cp.metadata.tags || [],
99
+ tags: task.metadata.tags || [],
100
100
  labels: [],
101
- comments: (cp.comments || []).filter((c) => c !== undefined)
101
+ // Comments use consistent field names: id, author, content, createdAt
102
+ comments: (task.comments || [])
103
+ .filter((c) => c !== undefined)
104
+ .map((c) => ({
105
+ id: c.id,
106
+ author: typeof c.author === 'string' ? c.author : c.author?.name || 'Unknown',
107
+ content: c.content || c.text || '',
108
+ createdAt: c.createdAt || c.timestamp || new Date().toISOString(),
109
+ }))
102
110
  });
103
- const markdown = matter.stringify(content || this.getDefaultContent(cp), frontmatterData);
104
- const filePath = path.join(this.basePath, 'proposals', `${cp.id}.md`);
111
+ const markdown = matter.stringify(content || this.getDefaultContent(task), frontmatterData);
112
+ const filePath = path.join(this.basePath, 'tasks', `${task.id}.md`);
105
113
  await fs.writeFile(filePath, markdown, 'utf-8');
106
114
  }
107
- async getCP(id) {
115
+ async getTask(id) {
108
116
  try {
109
- const filePath = path.join(this.basePath, 'proposals', `${id}.md`);
117
+ const filePath = path.join(this.basePath, 'tasks', `${id}.md`);
110
118
  const fileContent = await fs.readFile(filePath, 'utf-8');
111
119
  const { data, content } = matter(fileContent);
112
- // Convert the frontmatter back to a ChangeProposal
113
- return this.frontmatterToCP(data, content);
120
+ // Convert the frontmatter back to a Task
121
+ return this.frontmatterToTask(data, content);
114
122
  }
115
123
  catch (error) {
116
124
  if (error.code === 'ENOENT')
@@ -118,21 +126,19 @@ export class FileStorageAdapter {
118
126
  throw error;
119
127
  }
120
128
  }
121
- async updateCP(id, updates) {
122
- const existing = await this.getCP(id);
129
+ async updateTask(id, updates) {
130
+ const existing = await this.getTask(id);
123
131
  if (!existing)
124
- throw new Error(`CP ${id} not found`);
125
- // Never allow id to be updated to prevent overwriting other proposals
132
+ throw new Error(`Task ${id} not found`);
133
+ // Never allow id to be updated to prevent overwriting other tasks
126
134
  const { id: _, ...safeUpdates } = updates;
127
- // Handle comments separately as they're stored at root level in file format
128
- // Comments can come from either root level or metadata.comments
129
- let commentsToStore = existing.metadata.comments || [];
135
+ // Comments live at root level on Task (not in metadata).
136
+ // Accept comments from root level or metadata.comments for backward compat.
137
+ let commentsToStore = existing.comments || [];
130
138
  if (safeUpdates.comments) {
131
- // If comments are provided at root level, use them
132
139
  commentsToStore = safeUpdates.comments;
133
140
  }
134
141
  else if (safeUpdates.metadata?.comments) {
135
- // If comments are provided in metadata, use them
136
142
  commentsToStore = safeUpdates.metadata.comments;
137
143
  }
138
144
  const updated = {
@@ -142,41 +148,39 @@ export class FileStorageAdapter {
142
148
  ...existing.metadata,
143
149
  ...safeUpdates.metadata,
144
150
  updatedAt: new Date(),
145
- comments: commentsToStore
146
151
  },
147
- // Store comments at the root level for the file format
148
152
  comments: commentsToStore
149
153
  };
150
- await this.createCP(updated);
154
+ await this.createTask(updated);
151
155
  }
152
- async deleteCP(id) {
153
- const filePath = path.join(this.basePath, 'proposals', `${id}.md`);
156
+ async deleteTask(id) {
157
+ const filePath = path.join(this.basePath, 'tasks', `${id}.md`);
154
158
  await fs.unlink(filePath);
155
159
  }
156
- async listCPs(filter) {
157
- const proposalsDir = path.join(this.basePath, 'proposals');
160
+ async listTasks(filter) {
161
+ const tasksDir = path.join(this.basePath, 'tasks');
158
162
  try {
159
- const files = await fs.readdir(proposalsDir);
160
- const proposals = await Promise.all(files
163
+ const files = await fs.readdir(tasksDir);
164
+ const tasks = await Promise.all(files
161
165
  .filter(f => f.endsWith('.md'))
162
166
  .map(async (file) => {
163
- const content = await fs.readFile(path.join(proposalsDir, file), 'utf-8');
167
+ const content = await fs.readFile(path.join(tasksDir, file), 'utf-8');
164
168
  const { data, content: body } = matter(content);
165
- return this.frontmatterToCP(data, body);
169
+ return this.frontmatterToTask(data, body);
166
170
  }));
167
171
  // Apply filters
168
- let filtered = proposals.filter(cp => {
169
- if (!cp)
172
+ let filtered = tasks.filter(task => {
173
+ if (!task)
170
174
  return false;
171
- if (filter?.status && cp.status !== filter.status)
175
+ if (filter?.status && task.status !== filter.status)
172
176
  return false;
173
- if (filter?.author && cp.author.id !== filter.author)
177
+ if (filter?.author && task.author.id !== filter.author)
174
178
  return false;
175
- if (filter?.priority && cp.metadata.priority !== filter.priority)
179
+ if (filter?.priority && task.metadata.priority !== filter.priority)
176
180
  return false;
177
181
  if (filter?.tags && filter.tags.length > 0) {
178
- const cpTags = cp.metadata.tags || [];
179
- if (!filter.tags.some(tag => cpTags.includes(tag)))
182
+ const taskTags = task.metadata.tags || [];
183
+ if (!filter.tags.some(tag => taskTags.includes(tag)))
180
184
  return false;
181
185
  }
182
186
  return true;
@@ -204,9 +208,9 @@ export class FileStorageAdapter {
204
208
  throw error;
205
209
  }
206
210
  }
207
- async searchCPs(query) {
208
- const allCPs = await this.listCPs();
209
- const fuse = new Fuse(allCPs, {
211
+ async searchTasks(query) {
212
+ const allTasks = await this.listTasks();
213
+ const fuse = new Fuse(allTasks, {
210
214
  keys: [
211
215
  { name: 'title', weight: 2 },
212
216
  { name: 'intent', weight: 2 }, // Deprecated fallback
@@ -219,15 +223,16 @@ export class FileStorageAdapter {
219
223
  });
220
224
  return fuse.search(query).map(result => result.item);
221
225
  }
222
- frontmatterToCP(data, content) {
223
- // Handle both old and new format files
224
- const steps = data.plan?.steps || data.steps || [];
225
- // Transform comments to match expected format
226
- const transformedComments = (data.comments || []).map((comment) => ({
226
+ frontmatterToTask(data, content) {
227
+ // Handle both old format (plan.steps) and new format (top-level steps)
228
+ const steps = data.steps || data.plan?.steps || [];
229
+ // Normalize comments using consistent field names (content/createdAt).
230
+ // Also handle legacy format (text/timestamp) for backward compatibility.
231
+ const normalizedComments = (data.comments || []).map((comment) => ({
227
232
  id: comment.id,
228
- text: comment.content || comment.text || '', // Handle both content and text fields
229
233
  author: typeof comment.author === 'string' ? comment.author : comment.author?.name || 'Unknown',
230
- timestamp: comment.createdAt || comment.timestamp || new Date().toISOString()
234
+ content: comment.content || comment.text || '',
235
+ createdAt: comment.createdAt || comment.timestamp || new Date().toISOString(),
231
236
  }));
232
237
  // Use title as primary, fall back to deprecated intent for backward compatibility
233
238
  const titleValue = data.title || data.intent || '';
@@ -260,6 +265,7 @@ export class FileStorageAdapter {
260
265
  telemetryContracts: data.telemetryContracts || [],
261
266
  releasePlan: data.releasePlan || { strategy: 'immediate' },
262
267
  status: data.status || 'draft',
268
+ comments: normalizedComments,
263
269
  metadata: {
264
270
  createdAt: new Date(data.createdAt || Date.now()),
265
271
  updatedAt: new Date(data.updatedAt || Date.now()),
@@ -267,16 +273,15 @@ export class FileStorageAdapter {
267
273
  aiInteractions: data.aiInteractions || [],
268
274
  tags: data.tags || [],
269
275
  priority: data.priority || 'medium',
270
- comments: transformedComments
271
276
  },
272
277
  content // Store the markdown content
273
278
  };
274
279
  }
275
- getDefaultContent(cp) {
276
- return `# ${cp.title}
280
+ getDefaultContent(task) {
281
+ return `# ${task.title}
277
282
 
278
283
  ## Problem Statement
279
- Describe the problem this change proposal addresses.
284
+ Describe the problem this task addresses.
280
285
 
281
286
  ## Proposed Solution
282
287
  Describe the solution you're proposing.
@@ -2,13 +2,11 @@ import { WebSocket } from 'ws';
2
2
  import { ShellType } from './shell-utils.js';
3
3
  export interface TerminalSession {
4
4
  id: string;
5
- proposalId: string;
5
+ taskId: string;
6
6
  pty: any;
7
7
  websocket?: WebSocket;
8
8
  createdAt: Date;
9
9
  lastActivity: Date;
10
- enableLogging?: boolean;
11
- logFilePath?: string;
12
10
  backlog: string;
13
11
  previewSockets?: Set<WebSocket>;
14
12
  previewBuffer?: string;
@@ -21,9 +19,9 @@ export declare class TerminalManager {
21
19
  private cleanupInterval;
22
20
  private static readonly MAX_BACKLOG_BYTES;
23
21
  constructor();
24
- createSession(proposalId: string, enableLogging?: boolean, shellPreference?: ShellType, startupCommand?: string): Promise<TerminalSession>;
22
+ createSession(taskId: string, shellPreference?: ShellType, startupCommand?: string): Promise<TerminalSession>;
25
23
  getSession(sessionId: string): TerminalSession | undefined;
26
- getSessionsByProposal(proposalId: string): TerminalSession[];
24
+ getSessionsByTask(taskId: string): TerminalSession[];
27
25
  attachWebSocket(sessionId: string, ws: WebSocket): boolean;
28
26
  destroySession(sessionId: string): boolean;
29
27
  resizeSession(sessionId: string, cols: number, rows: number): boolean;
@@ -16,8 +16,8 @@ export class TerminalManager {
16
16
  this.cleanupInactiveSessions();
17
17
  }, 5 * 60 * 1000);
18
18
  }
19
- async createSession(proposalId, enableLogging = false, shellPreference = 'bash', startupCommand) {
20
- const sessionId = `${proposalId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
19
+ async createSession(taskId, shellPreference = 'bash', startupCommand) {
20
+ const sessionId = `${taskId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
21
21
  // Get the project root directory
22
22
  let projectRoot;
23
23
  if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
@@ -32,8 +32,8 @@ export class TerminalManager {
32
32
  }
33
33
  // Prepare context information
34
34
  const contextInfo = {
35
- proposalId,
36
- proposalPath: path.join(projectRoot, '.nut', 'proposals', `${proposalId}.md`),
35
+ proposalId: taskId,
36
+ proposalPath: path.join(projectRoot, '.nut', 'tasks', `${taskId}.md`),
37
37
  contextPath: path.join(projectRoot, '.nut', 'context'),
38
38
  projectRoot,
39
39
  };
@@ -65,30 +65,21 @@ export class TerminalManager {
65
65
  catch (error) {
66
66
  console.error('Error creating init script:', error);
67
67
  }
68
- // Set up logging if enabled
69
- let logFilePath;
70
- if (enableLogging) {
71
- const sessionsDir = path.join(projectRoot, '.nut', 'sessions');
72
- try {
73
- fs.mkdirSync(sessionsDir, { recursive: true });
74
- logFilePath = path.join(sessionsDir, `${sessionId}.log`);
75
- // Create log file with session header
76
- const logHeader = `# Terminal Session Log\n# Session ID: ${sessionId}\n# Proposal ID: ${proposalId}\n# Created: ${new Date().toISOString()}\n\n`;
77
- fs.writeFileSync(logFilePath, logHeader);
78
- }
79
- catch (error) {
80
- console.error('Error setting up session logging:', error);
81
- logFilePath = undefined;
82
- }
83
- }
84
68
  // Get shell arguments
85
69
  const shellArgs = getShellArgs(shellPath, initScriptPath);
86
- // Prepare environment variables with API keys injected
70
+ // Prepare environment variables with API keys injected.
71
+ // Remove internal dev-server env vars (GAIT_DEV_ROOT, NODE_ENV=development)
72
+ // so that tools like `nut` invoked inside the terminal resolve paths
73
+ // relative to the terminal's cwd (projectRoot) rather than the API package.
74
+ const injected = getInjectedEnv();
75
+ delete injected.GAIT_DEV_ROOT;
76
+ delete injected.NODE_ENV;
87
77
  const env = {
88
- ...getInjectedEnv(),
89
- COCONUT_PROPOSAL_ID: proposalId,
78
+ ...injected,
79
+ COCONUT_TASK_ID: taskId,
90
80
  COCONUT_CONTEXT_PATH: path.join(projectRoot, '.nut', 'context'),
91
- COCONUT_PROPOSAL_PATH: path.join(projectRoot, '.nut', 'proposals', `${proposalId}.md`),
81
+ COCONUT_TASK_PATH: path.join(projectRoot, '.nut', 'tasks', `${taskId}.md`),
82
+ GAIT_DATA_PATH: projectRoot,
92
83
  TERM: 'xterm-256color',
93
84
  COLORTERM: 'truecolor',
94
85
  };
@@ -109,12 +100,10 @@ export class TerminalManager {
109
100
  });
110
101
  const session = {
111
102
  id: sessionId,
112
- proposalId,
103
+ taskId,
113
104
  pty: ptyProcess,
114
105
  createdAt: new Date(),
115
106
  lastActivity: new Date(),
116
- enableLogging,
117
- logFilePath,
118
107
  backlog: '',
119
108
  previewSockets: new Set(),
120
109
  previewBuffer: '',
@@ -133,7 +122,7 @@ export class TerminalManager {
133
122
  tags: ["agent", "terminal", "session"],
134
123
  payload: {
135
124
  sessionId,
136
- proposalId,
125
+ taskId,
137
126
  cwd: projectRoot,
138
127
  shell: shellPath,
139
128
  }
@@ -146,17 +135,6 @@ export class TerminalManager {
146
135
  // Set up PTY event handlers
147
136
  ptyProcess.onData((data) => {
148
137
  session.lastActivity = new Date();
149
- // Log data if logging is enabled
150
- if (session.enableLogging && session.logFilePath) {
151
- try {
152
- const timestamp = new Date().toISOString();
153
- const logEntry = `[${timestamp}] OUTPUT: ${data}`;
154
- fs.appendFileSync(session.logFilePath, logEntry);
155
- }
156
- catch (error) {
157
- console.error('Error writing to session log:', error);
158
- }
159
- }
160
138
  // Maintain in-memory backlog (trim to last MAX_BACKLOG_BYTES)
161
139
  try {
162
140
  // Append and trim to max size
@@ -180,17 +158,6 @@ export class TerminalManager {
180
158
  ptyProcess.onExit((e) => {
181
159
  const endTime = new Date();
182
160
  const duration = endTime.getTime() - session.createdAt.getTime();
183
- // Log session end if logging is enabled
184
- if (session.enableLogging && session.logFilePath) {
185
- try {
186
- const timestamp = endTime.toISOString();
187
- const logEntry = `\n[${timestamp}] SESSION ENDED: Exit code ${e.exitCode}\n`;
188
- fs.appendFileSync(session.logFilePath, logEntry);
189
- }
190
- catch (error) {
191
- console.error('Error writing session end to log:', error);
192
- }
193
- }
194
161
  // Log session end to activity log
195
162
  try {
196
163
  const logger = getLogger();
@@ -242,8 +209,8 @@ export class TerminalManager {
242
209
  getSession(sessionId) {
243
210
  return this.sessions.get(sessionId);
244
211
  }
245
- getSessionsByProposal(proposalId) {
246
- return Array.from(this.sessions.values()).filter(session => session.proposalId === proposalId);
212
+ getSessionsByTask(taskId) {
213
+ return Array.from(this.sessions.values()).filter(session => session.taskId === taskId);
247
214
  }
248
215
  attachWebSocket(sessionId, ws) {
249
216
  const session = this.sessions.get(sessionId);
@@ -282,17 +249,6 @@ export class TerminalManager {
282
249
  const data = JSON.parse(message.toString());
283
250
  switch (data.type) {
284
251
  case 'input':
285
- // Log input if logging is enabled
286
- if (session.enableLogging && session.logFilePath) {
287
- try {
288
- const timestamp = new Date().toISOString();
289
- const logEntry = `[${timestamp}] INPUT: ${data.data}`;
290
- fs.appendFileSync(session.logFilePath, logEntry);
291
- }
292
- catch (error) {
293
- console.error('Error writing input to session log:', error);
294
- }
295
- }
296
252
  session.pty.write(data.data);
297
253
  session.lastActivity = new Date();
298
254
  break;
@@ -1,11 +1,36 @@
1
1
  import { getCookie } from 'hono/cookie';
2
+ import { getConnInfo } from '@hono/node-server/conninfo';
2
3
  import { getAuthManager } from '../lib/auth/auth-manager.js';
4
+ const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
5
+ /**
6
+ * Check if a request is a direct localhost connection (not proxied).
7
+ * Used to allow CLI tools running on the server to bypass auth.
8
+ *
9
+ * Safe because:
10
+ * - Raw socket address cannot be spoofed (TCP guarantees this)
11
+ * - Proxied requests (nginx/ALB → node) include X-Forwarded-For, so they
12
+ * are excluded even though the socket address is loopback
13
+ */
14
+ function isDirectLocalConnection(c) {
15
+ try {
16
+ // If X-Forwarded-For is present, the request came through a proxy
17
+ if (c.req.header('x-forwarded-for')) {
18
+ return false;
19
+ }
20
+ const info = getConnInfo(c);
21
+ return LOOPBACK_ADDRESSES.has(info.remote.address || '');
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
3
27
  // Public routes that don't require authentication
4
28
  const PUBLIC_ROUTES = [
5
29
  '/api/health',
6
30
  '/api/v1/auth/status',
7
31
  '/api/v1/auth/login',
8
32
  '/api/v1/auth/register',
33
+ '/api/v1/auth/logout',
9
34
  '/api/v1/auth/oauth',
10
35
  '/api/v1/auth/callback',
11
36
  ];
@@ -40,6 +65,13 @@ export async function authMiddleware(c, next) {
40
65
  // Auth is disabled, allow all requests
41
66
  return next();
42
67
  }
68
+ // Allow direct localhost connections (CLI on the same machine) to bypass auth.
69
+ // This is safe: the raw TCP socket address can't be spoofed, and proxied
70
+ // requests are excluded by checking for the X-Forwarded-For header.
71
+ if (isDirectLocalConnection(c)) {
72
+ c.set('authType', 'localhost');
73
+ return next();
74
+ }
43
75
  const path = c.req.path;
44
76
  // Allow public routes without authentication
45
77
  if (isPublicRoute(path)) {
@@ -129,6 +161,10 @@ export function requireAuth(c) {
129
161
  // Return null to indicate auth passed but no session available
130
162
  return null;
131
163
  }
164
+ // Direct localhost connections bypass auth (CLI on the same machine)
165
+ if (authType === 'localhost') {
166
+ return null;
167
+ }
132
168
  throw new Error('Authentication required');
133
169
  }
134
170
  /**
@@ -1,7 +1,5 @@
1
1
  import { Hono } from 'hono';
2
2
  import { POST } from './route.js';
3
- import { POST as toolsPost } from './tools.js';
4
3
  const app = new Hono();
5
4
  app.post('/', POST);
6
- app.post('/tools', toolsPost);
7
5
  export default app;