codingbuddy 2.3.2 → 2.3.4
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/src/keyword/keyword.service.js +12 -4
- package/dist/src/keyword/keyword.service.js.map +1 -1
- package/dist/src/keyword/rule-filter.js.map +1 -1
- package/dist/src/mcp/handlers/index.d.ts +1 -0
- package/dist/src/mcp/handlers/index.js +3 -1
- package/dist/src/mcp/handlers/index.js.map +1 -1
- package/dist/src/mcp/handlers/mode.handler.d.ts +15 -1
- package/dist/src/mcp/handlers/mode.handler.js +110 -4
- package/dist/src/mcp/handlers/mode.handler.js.map +1 -1
- package/dist/src/mcp/handlers/session.handler.d.ts +15 -0
- package/dist/src/mcp/handlers/session.handler.js +238 -0
- package/dist/src/mcp/handlers/session.handler.js.map +1 -0
- package/dist/src/mcp/mcp.module.js +5 -0
- package/dist/src/mcp/mcp.module.js.map +1 -1
- package/dist/src/session/index.d.ts +3 -0
- package/dist/src/session/index.js +8 -0
- package/dist/src/session/index.js.map +1 -0
- package/dist/src/session/session.module.d.ts +2 -0
- package/dist/src/session/session.module.js +23 -0
- package/dist/src/session/session.module.js.map +1 -0
- package/dist/src/session/session.service.d.ts +33 -0
- package/dist/src/session/session.service.js +557 -0
- package/dist/src/session/session.service.js.map +1 -0
- package/dist/src/session/session.types.d.ts +41 -0
- package/dist/src/session/session.types.js +3 -0
- package/dist/src/session/session.types.js.map +1 -0
- package/dist/src/state/index.d.ts +3 -0
- package/dist/src/state/index.js +12 -0
- package/dist/src/state/index.js.map +1 -0
- package/dist/src/state/state.module.d.ts +2 -0
- package/dist/src/state/state.module.js +23 -0
- package/dist/src/state/state.module.js.map +1 -0
- package/dist/src/state/state.service.d.ts +21 -0
- package/dist/src/state/state.service.js +214 -0
- package/dist/src/state/state.service.js.map +1 -0
- package/dist/src/state/state.types.d.ts +34 -0
- package/dist/src/state/state.types.js +10 -0
- package/dist/src/state/state.types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -17,6 +17,8 @@ const analyzer_module_1 = require("../analyzer/analyzer.module");
|
|
|
17
17
|
const agent_module_1 = require("../agent/agent.module");
|
|
18
18
|
const checklist_module_1 = require("../checklist/checklist.module");
|
|
19
19
|
const context_module_1 = require("../context/context.module");
|
|
20
|
+
const session_module_1 = require("../session/session.module");
|
|
21
|
+
const state_module_1 = require("../state/state.module");
|
|
20
22
|
const skill_recommendation_service_1 = require("../skill/skill-recommendation.service");
|
|
21
23
|
const language_service_1 = require("../shared/language.service");
|
|
22
24
|
const model_1 = require("../model");
|
|
@@ -29,6 +31,7 @@ const handlers = [
|
|
|
29
31
|
handlers_1.ModeHandler,
|
|
30
32
|
handlers_1.ChecklistContextHandler,
|
|
31
33
|
handlers_1.ConventionsHandler,
|
|
34
|
+
handlers_1.SessionHandler,
|
|
32
35
|
];
|
|
33
36
|
let McpModule = class McpModule {
|
|
34
37
|
};
|
|
@@ -43,6 +46,8 @@ exports.McpModule = McpModule = __decorate([
|
|
|
43
46
|
agent_module_1.AgentModule,
|
|
44
47
|
checklist_module_1.ChecklistModule,
|
|
45
48
|
context_module_1.ContextModule,
|
|
49
|
+
session_module_1.SessionModule,
|
|
50
|
+
state_module_1.StateModule,
|
|
46
51
|
],
|
|
47
52
|
controllers: [mcp_controller_1.McpController],
|
|
48
53
|
providers: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.module.js","sourceRoot":"","sources":["../../../src/mcp/mcp.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,+CAA2C;AAC3C,qDAAiD;AACjD,wDAAoD;AACpD,8DAA0D;AAC1D,2DAAkE;AAClE,iEAA6D;AAC7D,wDAAoD;AACpD,oEAAgE;AAChE,8DAA0D;AAC1D,wFAAmF;AACnF,iEAA6D;AAC7D,oCAAgD;AAGhD,
|
|
1
|
+
{"version":3,"file":"mcp.module.js","sourceRoot":"","sources":["../../../src/mcp/mcp.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,+CAA2C;AAC3C,qDAAiD;AACjD,wDAAoD;AACpD,8DAA0D;AAC1D,2DAAkE;AAClE,iEAA6D;AAC7D,wDAAoD;AACpD,oEAAgE;AAChE,8DAA0D;AAC1D,8DAA0D;AAC1D,wDAAoD;AACpD,wFAAmF;AACnF,iEAA6D;AAC7D,oCAAgD;AAGhD,yCAUoB;AAEpB,MAAM,QAAQ,GAAG;IACf,uBAAY;IACZ,wBAAa;IACb,uBAAY;IACZ,uBAAY;IACZ,sBAAW;IACX,kCAAuB;IACvB,6BAAkB;IAClB,yBAAc;CACf,CAAC;AA+BK,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA7BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,0BAAW;YACX,8BAAa;YACb,uCAAuB;YACvB,gCAAc;YACd,0BAAW;YACX,kCAAe;YACf,8BAAa;YACb,8BAAa;YACb,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE;YACT,wBAAU;YACV,yDAA0B;YAC1B,kCAAe;YACf,4BAAoB;YACpB,GAAG,QAAQ;YACX;gBACE,OAAO,EAAE,wBAAa;gBACtB,UAAU,EAAE,CACV,GAAG,gBAA2D,EAC9D,EAAE,CAAC,gBAAgB;gBACrB,MAAM,EAAE,QAAQ;aACjB;SACF;QACD,OAAO,EAAE,CAAC,wBAAU,CAAC;KACtB,CAAC;GACW,SAAS,CAAG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionService = exports.SessionModule = void 0;
|
|
4
|
+
var session_module_1 = require("./session.module");
|
|
5
|
+
Object.defineProperty(exports, "SessionModule", { enumerable: true, get: function () { return session_module_1.SessionModule; } });
|
|
6
|
+
var session_service_1 = require("./session.service");
|
|
7
|
+
Object.defineProperty(exports, "SessionService", { enumerable: true, get: function () { return session_service_1.SessionService; } });
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/session/index.ts"],"names":[],"mappings":";;;AAAA,mDAAiD;AAAxC,+GAAA,aAAa,OAAA;AACtB,qDAAmD;AAA1C,iHAAA,cAAc,OAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SessionModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const session_service_1 = require("./session.service");
|
|
12
|
+
const config_module_1 = require("../config/config.module");
|
|
13
|
+
let SessionModule = class SessionModule {
|
|
14
|
+
};
|
|
15
|
+
exports.SessionModule = SessionModule;
|
|
16
|
+
exports.SessionModule = SessionModule = __decorate([
|
|
17
|
+
(0, common_1.Module)({
|
|
18
|
+
imports: [config_module_1.CodingBuddyConfigModule],
|
|
19
|
+
providers: [session_service_1.SessionService],
|
|
20
|
+
exports: [session_service_1.SessionService],
|
|
21
|
+
})
|
|
22
|
+
], SessionModule);
|
|
23
|
+
//# sourceMappingURL=session.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.module.js","sourceRoot":"","sources":["../../../src/session/session.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uDAAmD;AACnD,2DAAkE;AAO3D,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,uCAAuB,CAAC;QAClC,SAAS,EAAE,CAAC,gCAAc,CAAC;QAC3B,OAAO,EAAE,CAAC,gCAAc,CAAC;KAC1B,CAAC;GACW,aAAa,CAAG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SessionDocument, CreateSessionOptions, UpdateSessionOptions, SessionOperationResult } from './session.types';
|
|
2
|
+
import { ConfigService } from '../config/config.service';
|
|
3
|
+
export declare class SessionService {
|
|
4
|
+
private readonly configService;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
private readonly sessionCache;
|
|
7
|
+
private activeSessionId;
|
|
8
|
+
private activeSessionTimestamp;
|
|
9
|
+
constructor(configService: ConfigService);
|
|
10
|
+
private isCacheValid;
|
|
11
|
+
private getFromCache;
|
|
12
|
+
private addToCache;
|
|
13
|
+
private invalidateCache;
|
|
14
|
+
private invalidateSession;
|
|
15
|
+
private getSessionsDir;
|
|
16
|
+
private ensureSessionsDir;
|
|
17
|
+
private validateSessionId;
|
|
18
|
+
private getSessionFilePath;
|
|
19
|
+
private validateTitle;
|
|
20
|
+
private generateFilename;
|
|
21
|
+
createSession(options: CreateSessionOptions): Promise<SessionOperationResult>;
|
|
22
|
+
getSession(sessionId: string): Promise<SessionDocument | null>;
|
|
23
|
+
getActiveSession(): Promise<SessionDocument | null>;
|
|
24
|
+
updateSession(options: UpdateSessionOptions): Promise<SessionOperationResult>;
|
|
25
|
+
getRecommendedActAgent(sessionId: string): Promise<{
|
|
26
|
+
agent: string;
|
|
27
|
+
confidence: number;
|
|
28
|
+
} | null>;
|
|
29
|
+
private serializeDocument;
|
|
30
|
+
private parseDocument;
|
|
31
|
+
private parseSectionContentLine;
|
|
32
|
+
private parseListItem;
|
|
33
|
+
}
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var SessionService_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.SessionService = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const fs = require("fs/promises");
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const config_service_1 = require("../config/config.service");
|
|
19
|
+
const SESSIONS_DIR_NAME = 'docs/codingbuddy/sessions';
|
|
20
|
+
const MARKDOWN = {
|
|
21
|
+
SESSION_HEADER: '# Session:',
|
|
22
|
+
CREATED_PREFIX: '**Created**:',
|
|
23
|
+
UPDATED_PREFIX: '**Updated**:',
|
|
24
|
+
STATUS_PREFIX: '**Status**:',
|
|
25
|
+
PRIMARY_AGENT_PREFIX: '**Primary Agent**:',
|
|
26
|
+
RECOMMENDED_ACT_AGENT_PREFIX: '**Recommended ACT Agent**:',
|
|
27
|
+
SPECIALISTS_PREFIX: '**Specialists**:',
|
|
28
|
+
SECTION_SEPARATOR: '---',
|
|
29
|
+
TASK_HEADER: '### Task',
|
|
30
|
+
DECISIONS_HEADER: '### Decisions',
|
|
31
|
+
NOTES_HEADER: '### Notes',
|
|
32
|
+
};
|
|
33
|
+
const SESSION_ID_PATTERN = /^[a-z0-9가-힣-]+$/;
|
|
34
|
+
const MAX_SESSION_TITLE_LENGTH = 200;
|
|
35
|
+
const MAX_SLUG_LENGTH = 100;
|
|
36
|
+
const MAX_SESSION_ID_LENGTH = 150;
|
|
37
|
+
const CACHE_TTL_MS = 30_000;
|
|
38
|
+
const MAX_CACHE_SIZE = 100;
|
|
39
|
+
const VALID_SESSION_STATUSES = ['active', 'completed', 'archived'];
|
|
40
|
+
const VALID_SECTION_STATUSES = ['in_progress', 'completed', 'blocked'];
|
|
41
|
+
const SECTION_HEADER_PATTERN = /^## (PLAN|ACT|EVAL|AUTO) \((.+)\)$/;
|
|
42
|
+
const RECOMMENDED_AGENT_PATTERN = /\*\*Recommended ACT Agent\*\*: ([^\s(]+)(?:\s*\(confidence: ([\d.]+)\))?/;
|
|
43
|
+
function isValidSessionStatus(value) {
|
|
44
|
+
return VALID_SESSION_STATUSES.includes(value);
|
|
45
|
+
}
|
|
46
|
+
function isValidSectionStatus(value) {
|
|
47
|
+
return VALID_SECTION_STATUSES.includes(value);
|
|
48
|
+
}
|
|
49
|
+
function parseMetadataLine(line, ctx) {
|
|
50
|
+
if (line.startsWith(MARKDOWN.SESSION_HEADER)) {
|
|
51
|
+
ctx.metadata.title = line.replace(MARKDOWN.SESSION_HEADER, '').trim();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (line.startsWith(MARKDOWN.CREATED_PREFIX)) {
|
|
55
|
+
ctx.metadata.createdAt = line.replace(MARKDOWN.CREATED_PREFIX, '').trim();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (line.startsWith(MARKDOWN.UPDATED_PREFIX)) {
|
|
59
|
+
ctx.metadata.updatedAt = line.replace(MARKDOWN.UPDATED_PREFIX, '').trim();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (line.startsWith(MARKDOWN.STATUS_PREFIX) && !ctx.currentSection) {
|
|
63
|
+
const statusValue = line.replace(MARKDOWN.STATUS_PREFIX, '').trim();
|
|
64
|
+
if (isValidSessionStatus(statusValue)) {
|
|
65
|
+
ctx.metadata.status = statusValue;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function parseSectionHeader(line, ctx) {
|
|
72
|
+
const match = line.match(SECTION_HEADER_PATTERN);
|
|
73
|
+
if (!match)
|
|
74
|
+
return false;
|
|
75
|
+
if (ctx.currentSection && ctx.currentSection.mode) {
|
|
76
|
+
ctx.sections.push(ctx.currentSection);
|
|
77
|
+
}
|
|
78
|
+
ctx.currentSection = {
|
|
79
|
+
mode: match[1],
|
|
80
|
+
timestamp: match[2],
|
|
81
|
+
};
|
|
82
|
+
ctx.currentListType = null;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
function parseSectionField(line, section) {
|
|
86
|
+
if (line.startsWith(MARKDOWN.PRIMARY_AGENT_PREFIX)) {
|
|
87
|
+
section.primaryAgent = line
|
|
88
|
+
.replace(MARKDOWN.PRIMARY_AGENT_PREFIX, '')
|
|
89
|
+
.trim();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (line.startsWith(MARKDOWN.RECOMMENDED_ACT_AGENT_PREFIX)) {
|
|
93
|
+
const match = line.match(RECOMMENDED_AGENT_PATTERN);
|
|
94
|
+
if (match) {
|
|
95
|
+
section.recommendedActAgent = match[1];
|
|
96
|
+
if (match[2]) {
|
|
97
|
+
section.recommendedActAgentConfidence = parseFloat(match[2]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (line.startsWith(MARKDOWN.SPECIALISTS_PREFIX)) {
|
|
103
|
+
section.specialists = line
|
|
104
|
+
.replace(MARKDOWN.SPECIALISTS_PREFIX, '')
|
|
105
|
+
.trim()
|
|
106
|
+
.split(',')
|
|
107
|
+
.map(s => s.trim());
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (line.startsWith(MARKDOWN.STATUS_PREFIX)) {
|
|
111
|
+
const statusValue = line.replace(MARKDOWN.STATUS_PREFIX, '').trim();
|
|
112
|
+
if (isValidSectionStatus(statusValue)) {
|
|
113
|
+
section.status = statusValue;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
function parseListHeader(line, section) {
|
|
120
|
+
if (line === MARKDOWN.TASK_HEADER) {
|
|
121
|
+
return { matched: true, listType: 'task' };
|
|
122
|
+
}
|
|
123
|
+
if (line === MARKDOWN.DECISIONS_HEADER) {
|
|
124
|
+
section.decisions = [];
|
|
125
|
+
return { matched: true, listType: 'decisions' };
|
|
126
|
+
}
|
|
127
|
+
if (line === MARKDOWN.NOTES_HEADER) {
|
|
128
|
+
section.notes = [];
|
|
129
|
+
return { matched: true, listType: 'notes' };
|
|
130
|
+
}
|
|
131
|
+
return { matched: false };
|
|
132
|
+
}
|
|
133
|
+
function isContentLine(line) {
|
|
134
|
+
return (!line.startsWith('#') &&
|
|
135
|
+
!line.startsWith('**') &&
|
|
136
|
+
!line.startsWith('-') &&
|
|
137
|
+
!line.startsWith('---') &&
|
|
138
|
+
line.trim().length > 0);
|
|
139
|
+
}
|
|
140
|
+
let SessionService = SessionService_1 = class SessionService {
|
|
141
|
+
constructor(configService) {
|
|
142
|
+
this.configService = configService;
|
|
143
|
+
this.logger = new common_1.Logger(SessionService_1.name);
|
|
144
|
+
this.sessionCache = new Map();
|
|
145
|
+
this.activeSessionId = null;
|
|
146
|
+
this.activeSessionTimestamp = 0;
|
|
147
|
+
}
|
|
148
|
+
isCacheValid(timestamp) {
|
|
149
|
+
return Date.now() - timestamp < CACHE_TTL_MS;
|
|
150
|
+
}
|
|
151
|
+
getFromCache(sessionId) {
|
|
152
|
+
const entry = this.sessionCache.get(sessionId);
|
|
153
|
+
if (entry && this.isCacheValid(entry.timestamp)) {
|
|
154
|
+
return entry.session;
|
|
155
|
+
}
|
|
156
|
+
if (entry) {
|
|
157
|
+
this.sessionCache.delete(sessionId);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
addToCache(sessionId, session) {
|
|
162
|
+
if (this.sessionCache.has(sessionId)) {
|
|
163
|
+
this.sessionCache.delete(sessionId);
|
|
164
|
+
}
|
|
165
|
+
while (this.sessionCache.size >= MAX_CACHE_SIZE) {
|
|
166
|
+
const oldestKey = this.sessionCache.keys().next().value;
|
|
167
|
+
if (oldestKey !== undefined) {
|
|
168
|
+
this.sessionCache.delete(oldestKey);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.sessionCache.set(sessionId, {
|
|
175
|
+
session,
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
invalidateCache() {
|
|
180
|
+
this.sessionCache.clear();
|
|
181
|
+
this.activeSessionId = null;
|
|
182
|
+
this.activeSessionTimestamp = 0;
|
|
183
|
+
}
|
|
184
|
+
invalidateSession(sessionId) {
|
|
185
|
+
this.sessionCache.delete(sessionId);
|
|
186
|
+
if (this.activeSessionId === sessionId) {
|
|
187
|
+
this.activeSessionId = null;
|
|
188
|
+
this.activeSessionTimestamp = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
getSessionsDir() {
|
|
192
|
+
const projectRoot = this.configService.getProjectRoot();
|
|
193
|
+
return path.join(projectRoot, SESSIONS_DIR_NAME);
|
|
194
|
+
}
|
|
195
|
+
ensureSessionsDir() {
|
|
196
|
+
const sessionsDir = this.getSessionsDir();
|
|
197
|
+
if (!(0, fs_1.existsSync)(sessionsDir)) {
|
|
198
|
+
(0, fs_1.mkdirSync)(sessionsDir, { recursive: true });
|
|
199
|
+
this.logger.log(`Created sessions directory: ${sessionsDir}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
validateSessionId(sessionId) {
|
|
203
|
+
if (!sessionId || sessionId.length === 0) {
|
|
204
|
+
this.logger.warn(`Session ID is empty: rejected`);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (sessionId.length > MAX_SESSION_ID_LENGTH) {
|
|
208
|
+
this.logger.warn(`Session ID exceeds maximum length: rejected`);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (sessionId.includes('\x00')) {
|
|
212
|
+
this.logger.warn(`Session ID contains null byte: rejected`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (sessionId.includes('..') ||
|
|
216
|
+
sessionId.includes('/') ||
|
|
217
|
+
sessionId.includes('\\')) {
|
|
218
|
+
this.logger.warn(`Session ID contains path traversal: rejected`);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
222
|
+
this.logger.warn(`Session ID contains invalid characters: rejected`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
getSessionFilePath(sessionId) {
|
|
228
|
+
if (!this.validateSessionId(sessionId)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const filePath = path.join(this.getSessionsDir(), `${sessionId}.md`);
|
|
232
|
+
const resolvedPath = path.resolve(filePath);
|
|
233
|
+
const resolvedSessionsDir = path.resolve(this.getSessionsDir());
|
|
234
|
+
if (!resolvedPath.startsWith(resolvedSessionsDir + path.sep)) {
|
|
235
|
+
this.logger.warn(`Session path escaped sessions directory: ${resolvedPath}`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return filePath;
|
|
239
|
+
}
|
|
240
|
+
validateTitle(title) {
|
|
241
|
+
if (!title || title.trim().length === 0) {
|
|
242
|
+
return 'Title cannot be empty';
|
|
243
|
+
}
|
|
244
|
+
if (title.length > MAX_SESSION_TITLE_LENGTH) {
|
|
245
|
+
return `Title exceeds maximum length of ${MAX_SESSION_TITLE_LENGTH} characters`;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
generateFilename(title) {
|
|
250
|
+
const date = new Date().toISOString().split('T')[0];
|
|
251
|
+
const slug = title
|
|
252
|
+
.toLowerCase()
|
|
253
|
+
.replace(/[^a-z0-9가-힣]+/g, '-')
|
|
254
|
+
.replace(/^-|-$/g, '')
|
|
255
|
+
.slice(0, MAX_SLUG_LENGTH);
|
|
256
|
+
return `${date}-${slug}.md`;
|
|
257
|
+
}
|
|
258
|
+
async createSession(options) {
|
|
259
|
+
try {
|
|
260
|
+
const titleError = this.validateTitle(options.title);
|
|
261
|
+
if (titleError) {
|
|
262
|
+
return { success: false, error: titleError };
|
|
263
|
+
}
|
|
264
|
+
this.ensureSessionsDir();
|
|
265
|
+
const filename = this.generateFilename(options.title);
|
|
266
|
+
const filePath = path.join(this.getSessionsDir(), filename);
|
|
267
|
+
if ((0, fs_1.existsSync)(filePath)) {
|
|
268
|
+
return {
|
|
269
|
+
success: true,
|
|
270
|
+
sessionId: filename.replace('.md', ''),
|
|
271
|
+
filePath,
|
|
272
|
+
message: `Session already exists: ${filename}`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const now = new Date().toISOString();
|
|
276
|
+
const metadata = {
|
|
277
|
+
id: filename.replace('.md', ''),
|
|
278
|
+
title: options.title,
|
|
279
|
+
createdAt: now,
|
|
280
|
+
updatedAt: now,
|
|
281
|
+
status: 'active',
|
|
282
|
+
};
|
|
283
|
+
const document = {
|
|
284
|
+
metadata,
|
|
285
|
+
sections: [],
|
|
286
|
+
};
|
|
287
|
+
const content = this.serializeDocument(document);
|
|
288
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
289
|
+
this.activeSessionId = null;
|
|
290
|
+
this.activeSessionTimestamp = 0;
|
|
291
|
+
this.logger.log(`Created session: ${filename}`);
|
|
292
|
+
return {
|
|
293
|
+
success: true,
|
|
294
|
+
sessionId: metadata.id,
|
|
295
|
+
filePath,
|
|
296
|
+
message: `Session created: ${filename}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
301
|
+
this.logger.error(`Failed to create session: ${message}`);
|
|
302
|
+
return {
|
|
303
|
+
success: false,
|
|
304
|
+
error: message,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async getSession(sessionId) {
|
|
309
|
+
try {
|
|
310
|
+
const cached = this.getFromCache(sessionId);
|
|
311
|
+
if (cached) {
|
|
312
|
+
return cached;
|
|
313
|
+
}
|
|
314
|
+
const filePath = this.getSessionFilePath(sessionId);
|
|
315
|
+
if (!filePath) {
|
|
316
|
+
this.logger.debug(`Invalid session ID: ${sessionId}`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
320
|
+
this.logger.debug(`Session not found: ${sessionId}`);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
324
|
+
const session = this.parseDocument(content, sessionId);
|
|
325
|
+
this.addToCache(sessionId, session);
|
|
326
|
+
return session;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
this.logger.error(`Failed to read session ${sessionId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async getActiveSession() {
|
|
334
|
+
try {
|
|
335
|
+
if (this.activeSessionId &&
|
|
336
|
+
this.isCacheValid(this.activeSessionTimestamp)) {
|
|
337
|
+
const session = await this.getSession(this.activeSessionId);
|
|
338
|
+
if (session && session.metadata.status === 'active') {
|
|
339
|
+
return session;
|
|
340
|
+
}
|
|
341
|
+
this.activeSessionId = null;
|
|
342
|
+
this.activeSessionTimestamp = 0;
|
|
343
|
+
}
|
|
344
|
+
const sessionsDir = this.getSessionsDir();
|
|
345
|
+
if (!(0, fs_1.existsSync)(sessionsDir)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const files = await fs.readdir(sessionsDir);
|
|
349
|
+
const mdFiles = files
|
|
350
|
+
.filter(f => f.endsWith('.md'))
|
|
351
|
+
.sort()
|
|
352
|
+
.reverse();
|
|
353
|
+
for (const file of mdFiles) {
|
|
354
|
+
const sessionId = file.replace('.md', '');
|
|
355
|
+
const session = await this.getSession(sessionId);
|
|
356
|
+
if (session && session.metadata.status === 'active') {
|
|
357
|
+
this.activeSessionId = sessionId;
|
|
358
|
+
this.activeSessionTimestamp = Date.now();
|
|
359
|
+
return session;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
this.logger.error(`Failed to get active session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async updateSession(options) {
|
|
370
|
+
try {
|
|
371
|
+
const session = await this.getSession(options.sessionId);
|
|
372
|
+
if (!session) {
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
error: `Session not found: ${options.sessionId}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const now = new Date().toISOString();
|
|
379
|
+
const timestamp = new Date().toLocaleString('ko-KR', {
|
|
380
|
+
hour: '2-digit',
|
|
381
|
+
minute: '2-digit',
|
|
382
|
+
hour12: false,
|
|
383
|
+
});
|
|
384
|
+
const existingIndex = session.sections.findIndex(s => s.mode === options.section.mode);
|
|
385
|
+
const section = {
|
|
386
|
+
...options.section,
|
|
387
|
+
timestamp: options.section.timestamp || timestamp,
|
|
388
|
+
};
|
|
389
|
+
if (existingIndex >= 0) {
|
|
390
|
+
session.sections[existingIndex] = {
|
|
391
|
+
...session.sections[existingIndex],
|
|
392
|
+
...section,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
session.sections.push(section);
|
|
397
|
+
}
|
|
398
|
+
session.metadata.updatedAt = now;
|
|
399
|
+
const filePath = this.getSessionFilePath(options.sessionId);
|
|
400
|
+
if (!filePath) {
|
|
401
|
+
return {
|
|
402
|
+
success: false,
|
|
403
|
+
error: `Invalid session ID: ${options.sessionId}`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const content = this.serializeDocument(session);
|
|
407
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
408
|
+
this.invalidateSession(options.sessionId);
|
|
409
|
+
this.logger.log(`Updated session ${options.sessionId}: ${options.section.mode} section`);
|
|
410
|
+
return {
|
|
411
|
+
success: true,
|
|
412
|
+
sessionId: options.sessionId,
|
|
413
|
+
filePath,
|
|
414
|
+
message: `Session updated: ${options.section.mode} section`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
419
|
+
this.logger.error(`Failed to update session: ${message}`);
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
error: message,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async getRecommendedActAgent(sessionId) {
|
|
427
|
+
const session = await this.getSession(sessionId);
|
|
428
|
+
if (!session) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const planSection = session.sections.find(s => s.mode === 'PLAN');
|
|
432
|
+
if (planSection?.recommendedActAgent) {
|
|
433
|
+
return {
|
|
434
|
+
agent: planSection.recommendedActAgent,
|
|
435
|
+
confidence: planSection.recommendedActAgentConfidence || 0,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
serializeDocument(doc) {
|
|
441
|
+
const lines = [];
|
|
442
|
+
lines.push(`# Session: ${doc.metadata.title}`);
|
|
443
|
+
lines.push('');
|
|
444
|
+
lines.push(`**Created**: ${doc.metadata.createdAt}`);
|
|
445
|
+
lines.push(`**Updated**: ${doc.metadata.updatedAt}`);
|
|
446
|
+
lines.push(`**Status**: ${doc.metadata.status}`);
|
|
447
|
+
lines.push('');
|
|
448
|
+
lines.push('---');
|
|
449
|
+
for (const section of doc.sections) {
|
|
450
|
+
lines.push('');
|
|
451
|
+
lines.push(`## ${section.mode} (${section.timestamp})`);
|
|
452
|
+
lines.push('');
|
|
453
|
+
if (section.primaryAgent) {
|
|
454
|
+
lines.push(`**Primary Agent**: ${section.primaryAgent}`);
|
|
455
|
+
}
|
|
456
|
+
if (section.recommendedActAgent) {
|
|
457
|
+
const confidence = section.recommendedActAgentConfidence
|
|
458
|
+
? ` (confidence: ${section.recommendedActAgentConfidence})`
|
|
459
|
+
: '';
|
|
460
|
+
lines.push(`**Recommended ACT Agent**: ${section.recommendedActAgent}${confidence}`);
|
|
461
|
+
}
|
|
462
|
+
if (section.specialists && section.specialists.length > 0) {
|
|
463
|
+
lines.push(`**Specialists**: ${section.specialists.join(', ')}`);
|
|
464
|
+
}
|
|
465
|
+
if (section.status) {
|
|
466
|
+
lines.push(`**Status**: ${section.status}`);
|
|
467
|
+
}
|
|
468
|
+
lines.push('');
|
|
469
|
+
if (section.task) {
|
|
470
|
+
lines.push('### Task');
|
|
471
|
+
lines.push(section.task);
|
|
472
|
+
lines.push('');
|
|
473
|
+
}
|
|
474
|
+
if (section.decisions && section.decisions.length > 0) {
|
|
475
|
+
lines.push('### Decisions');
|
|
476
|
+
for (const decision of section.decisions) {
|
|
477
|
+
lines.push(`- ${decision}`);
|
|
478
|
+
}
|
|
479
|
+
lines.push('');
|
|
480
|
+
}
|
|
481
|
+
if (section.notes && section.notes.length > 0) {
|
|
482
|
+
lines.push('### Notes');
|
|
483
|
+
for (const note of section.notes) {
|
|
484
|
+
lines.push(`- ${note}`);
|
|
485
|
+
}
|
|
486
|
+
lines.push('');
|
|
487
|
+
}
|
|
488
|
+
lines.push('---');
|
|
489
|
+
}
|
|
490
|
+
return lines.join('\n');
|
|
491
|
+
}
|
|
492
|
+
parseDocument(content, sessionId) {
|
|
493
|
+
const lines = content.split('\n');
|
|
494
|
+
const ctx = {
|
|
495
|
+
metadata: {
|
|
496
|
+
id: sessionId,
|
|
497
|
+
title: sessionId,
|
|
498
|
+
createdAt: '',
|
|
499
|
+
updatedAt: '',
|
|
500
|
+
status: 'active',
|
|
501
|
+
},
|
|
502
|
+
sections: [],
|
|
503
|
+
currentSection: null,
|
|
504
|
+
currentListType: null,
|
|
505
|
+
};
|
|
506
|
+
for (const line of lines) {
|
|
507
|
+
if (parseMetadataLine(line, ctx))
|
|
508
|
+
continue;
|
|
509
|
+
if (parseSectionHeader(line, ctx))
|
|
510
|
+
continue;
|
|
511
|
+
if (ctx.currentSection) {
|
|
512
|
+
this.parseSectionContentLine(line, ctx);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (ctx.currentSection && ctx.currentSection.mode) {
|
|
516
|
+
ctx.sections.push(ctx.currentSection);
|
|
517
|
+
}
|
|
518
|
+
return { metadata: ctx.metadata, sections: ctx.sections };
|
|
519
|
+
}
|
|
520
|
+
parseSectionContentLine(line, ctx) {
|
|
521
|
+
if (!ctx.currentSection)
|
|
522
|
+
return;
|
|
523
|
+
const section = ctx.currentSection;
|
|
524
|
+
if (parseSectionField(line, section))
|
|
525
|
+
return;
|
|
526
|
+
const listResult = parseListHeader(line, section);
|
|
527
|
+
if (listResult.matched) {
|
|
528
|
+
ctx.currentListType =
|
|
529
|
+
listResult.listType === 'task' ? null : listResult.listType;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (line.startsWith('- ') && ctx.currentListType) {
|
|
533
|
+
this.parseListItem(line, section, ctx.currentListType);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (isContentLine(line) && !ctx.currentListType) {
|
|
537
|
+
section.task = section.task ? `${section.task}\n${line}` : line;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
parseListItem(line, section, listType) {
|
|
541
|
+
const item = line.replace('- ', '').trim();
|
|
542
|
+
if (listType === 'decisions') {
|
|
543
|
+
section.decisions = section.decisions || [];
|
|
544
|
+
section.decisions.push(item);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
section.notes = section.notes || [];
|
|
548
|
+
section.notes.push(item);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
exports.SessionService = SessionService;
|
|
553
|
+
exports.SessionService = SessionService = SessionService_1 = __decorate([
|
|
554
|
+
(0, common_1.Injectable)(),
|
|
555
|
+
__metadata("design:paramtypes", [config_service_1.ConfigService])
|
|
556
|
+
], SessionService);
|
|
557
|
+
//# sourceMappingURL=session.service.js.map
|