sentix 2.0.1
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/LICENSE +21 -0
- package/README.md +627 -0
- package/bin/sentix.js +116 -0
- package/package.json +37 -0
- package/src/CLAUDE.md +26 -0
- package/src/commands/CLAUDE.md +29 -0
- package/src/commands/context.js +227 -0
- package/src/commands/doctor.js +213 -0
- package/src/commands/evolve.js +203 -0
- package/src/commands/feature.js +327 -0
- package/src/commands/init.js +467 -0
- package/src/commands/metrics.js +170 -0
- package/src/commands/plugin.js +111 -0
- package/src/commands/run.js +303 -0
- package/src/commands/safety.js +163 -0
- package/src/commands/status.js +149 -0
- package/src/commands/ticket.js +362 -0
- package/src/commands/update.js +143 -0
- package/src/commands/version.js +218 -0
- package/src/context.js +104 -0
- package/src/dev-server.js +154 -0
- package/src/lib/agent-loop.js +110 -0
- package/src/lib/api-client.js +213 -0
- package/src/lib/changelog.js +110 -0
- package/src/lib/pipeline.js +218 -0
- package/src/lib/provider.js +129 -0
- package/src/lib/safety.js +146 -0
- package/src/lib/semver.js +40 -0
- package/src/lib/similarity.js +58 -0
- package/src/lib/ticket-index.js +137 -0
- package/src/lib/tools.js +142 -0
- package/src/lib/verify-gates.js +254 -0
- package/src/plugins/auto-version.js +89 -0
- package/src/plugins/logger.js +55 -0
- package/src/registry.js +63 -0
- package/src/version.js +15 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sentix version — 버전 관리
|
|
3
|
+
*
|
|
4
|
+
* sentix version current — 현재 버전 표시
|
|
5
|
+
* sentix version bump — 버전 범프 + git tag + CHANGELOG
|
|
6
|
+
* sentix version changelog — CHANGELOG 미리보기 생성
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { registerCommand } from '../registry.js';
|
|
11
|
+
import { parseSemver, bumpSemver } from '../lib/semver.js';
|
|
12
|
+
import { generateForVersion, prependToChangelog } from '../lib/changelog.js';
|
|
13
|
+
|
|
14
|
+
registerCommand('version', {
|
|
15
|
+
description: 'Manage project version (bump | current | changelog)',
|
|
16
|
+
usage: 'sentix version <bump|current|changelog> [major|minor|patch]',
|
|
17
|
+
|
|
18
|
+
async run(args, ctx) {
|
|
19
|
+
const subcommand = args[0];
|
|
20
|
+
|
|
21
|
+
if (!subcommand || subcommand === 'current') {
|
|
22
|
+
await showCurrent(ctx);
|
|
23
|
+
} else if (subcommand === 'bump') {
|
|
24
|
+
const type = args[1] || 'patch';
|
|
25
|
+
if (!['major', 'minor', 'patch'].includes(type)) {
|
|
26
|
+
ctx.error(`Invalid bump type: ${type} (use major|minor|patch)`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await bumpVersion(type, ctx);
|
|
30
|
+
} else if (subcommand === 'changelog') {
|
|
31
|
+
await showChangelog(ctx);
|
|
32
|
+
} else {
|
|
33
|
+
ctx.error(`Unknown subcommand: ${subcommand}`);
|
|
34
|
+
ctx.log('Usage: sentix version <bump|current|changelog> [major|minor|patch]');
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── sentix version current ────────────────────────────
|
|
40
|
+
|
|
41
|
+
async function showCurrent(ctx) {
|
|
42
|
+
ctx.log('=== Sentix Version ===\n');
|
|
43
|
+
|
|
44
|
+
// Read from package.json
|
|
45
|
+
let currentVersion = 'unknown';
|
|
46
|
+
if (ctx.exists('package.json')) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = await ctx.readJSON('package.json');
|
|
49
|
+
currentVersion = pkg.version || 'unknown';
|
|
50
|
+
} catch {
|
|
51
|
+
ctx.warn('Could not read package.json');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
ctx.log(` Version: ${currentVersion}`);
|
|
55
|
+
|
|
56
|
+
// Check latest git tag
|
|
57
|
+
const tagResult = spawnSync('git', ['describe', '--tags', '--abbrev=0'], {
|
|
58
|
+
cwd: ctx.cwd,
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
stdio: 'pipe',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (tagResult.status === 0 && tagResult.stdout.trim()) {
|
|
64
|
+
const latestTag = tagResult.stdout.trim();
|
|
65
|
+
ctx.log(` Git Tag: ${latestTag}`);
|
|
66
|
+
|
|
67
|
+
// Check if current version matches the tag
|
|
68
|
+
const tagVersion = latestTag.replace(/^v/i, '');
|
|
69
|
+
if (tagVersion !== currentVersion) {
|
|
70
|
+
ctx.warn(` Version ${currentVersion} has no matching git tag`);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
ctx.log(' Git Tag: (none)');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check INTERFACE.md version
|
|
77
|
+
if (ctx.exists('INTERFACE.md')) {
|
|
78
|
+
try {
|
|
79
|
+
const iface = await ctx.readFile('INTERFACE.md');
|
|
80
|
+
const match = iface.match(/version:\s*(\S+)/);
|
|
81
|
+
if (match) {
|
|
82
|
+
ctx.log(` INTERFACE: ${match[1]}`);
|
|
83
|
+
if (match[1] !== currentVersion) {
|
|
84
|
+
ctx.warn(' INTERFACE.md version is out of sync');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch { /* non-critical */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ctx.log('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── sentix version bump ───────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function bumpVersion(type, ctx) {
|
|
96
|
+
// 1. Read current version
|
|
97
|
+
if (!ctx.exists('package.json')) {
|
|
98
|
+
ctx.error('package.json not found');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pkg = await ctx.readJSON('package.json');
|
|
103
|
+
const current = pkg.version;
|
|
104
|
+
let newVersion;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
parseSemver(current);
|
|
108
|
+
newVersion = bumpSemver(current, type);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
ctx.error(`Cannot bump version: ${e.message}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ctx.log(`Bumping: ${current} → ${newVersion} (${type})\n`);
|
|
115
|
+
|
|
116
|
+
// 2. Update package.json
|
|
117
|
+
pkg.version = newVersion;
|
|
118
|
+
await ctx.writeJSON('package.json', pkg);
|
|
119
|
+
ctx.success('Updated package.json');
|
|
120
|
+
|
|
121
|
+
// 3. Update INTERFACE.md version line
|
|
122
|
+
if (ctx.exists('INTERFACE.md')) {
|
|
123
|
+
try {
|
|
124
|
+
let iface = await ctx.readFile('INTERFACE.md');
|
|
125
|
+
iface = iface.replace(
|
|
126
|
+
/version:\s*\S+/,
|
|
127
|
+
`version: ${newVersion}`
|
|
128
|
+
);
|
|
129
|
+
await ctx.writeFile('INTERFACE.md', iface);
|
|
130
|
+
ctx.success('Updated INTERFACE.md');
|
|
131
|
+
} catch { /* non-critical */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 4. Generate changelog entry
|
|
135
|
+
try {
|
|
136
|
+
const entry = await generateForVersion(ctx, newVersion);
|
|
137
|
+
if (entry.trim()) {
|
|
138
|
+
await prependToChangelog(ctx, entry);
|
|
139
|
+
ctx.success('Updated CHANGELOG.md');
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
ctx.warn(`Changelog generation skipped: ${e.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 5. Git commit + tag
|
|
146
|
+
const gitAdd = spawnSync('git', ['add', 'package.json', 'CHANGELOG.md', 'INTERFACE.md'], {
|
|
147
|
+
cwd: ctx.cwd,
|
|
148
|
+
encoding: 'utf-8',
|
|
149
|
+
stdio: 'pipe',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (gitAdd.status === 0) {
|
|
153
|
+
const commitMsg = `chore: bump version to v${newVersion}`;
|
|
154
|
+
const gitCommit = spawnSync('git', ['commit', '-m', commitMsg], {
|
|
155
|
+
cwd: ctx.cwd,
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
stdio: 'pipe',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (gitCommit.status === 0) {
|
|
161
|
+
ctx.success(`Committed: ${commitMsg}`);
|
|
162
|
+
|
|
163
|
+
const gitTag = spawnSync('git', ['tag', '-a', `v${newVersion}`, '-m', `Release v${newVersion}`], {
|
|
164
|
+
cwd: ctx.cwd,
|
|
165
|
+
encoding: 'utf-8',
|
|
166
|
+
stdio: 'pipe',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (gitTag.status === 0) {
|
|
170
|
+
ctx.success(`Tagged: v${newVersion}`);
|
|
171
|
+
} else {
|
|
172
|
+
ctx.warn(`Tag creation failed: ${gitTag.stderr?.trim() || 'unknown error'}`);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
ctx.warn(`Commit failed: ${gitCommit.stderr?.trim() || 'unknown error'}`);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
ctx.warn('Git staging failed — version bumped in files only');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
ctx.log('');
|
|
182
|
+
ctx.log('To publish:');
|
|
183
|
+
ctx.log(' git push && git push --tags');
|
|
184
|
+
|
|
185
|
+
// 6. Log event
|
|
186
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
187
|
+
ts: new Date().toISOString(),
|
|
188
|
+
event: 'version:bump',
|
|
189
|
+
from: current,
|
|
190
|
+
to: newVersion,
|
|
191
|
+
type,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── sentix version changelog ──────────────────────────
|
|
196
|
+
|
|
197
|
+
async function showChangelog(ctx) {
|
|
198
|
+
ctx.log('=== Changelog Preview ===\n');
|
|
199
|
+
|
|
200
|
+
let currentVersion = 'next';
|
|
201
|
+
if (ctx.exists('package.json')) {
|
|
202
|
+
try {
|
|
203
|
+
const pkg = await ctx.readJSON('package.json');
|
|
204
|
+
currentVersion = pkg.version || 'next';
|
|
205
|
+
} catch { /* use default */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const entry = await generateForVersion(ctx, currentVersion);
|
|
210
|
+
if (entry.trim()) {
|
|
211
|
+
ctx.log(entry);
|
|
212
|
+
} else {
|
|
213
|
+
ctx.log('(No resolved tickets or completed cycles to report)');
|
|
214
|
+
}
|
|
215
|
+
} catch (e) {
|
|
216
|
+
ctx.error(`Could not generate changelog: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/context.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentix Context Object
|
|
3
|
+
*
|
|
4
|
+
* Passed to every command and plugin as `ctx`.
|
|
5
|
+
* Provides filesystem helpers and logging utilities.
|
|
6
|
+
* Zero external dependencies — uses only Node.js built-ins.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile, appendFile, mkdir, rename } from 'node:fs/promises';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { resolve, dirname, join } from 'node:path';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
const useColor = process.env.NO_COLOR === undefined && process.stdout.isTTY;
|
|
15
|
+
|
|
16
|
+
function color(code, text) {
|
|
17
|
+
return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a context object for command/plugin execution.
|
|
22
|
+
* @param {string} cwd - Current working directory
|
|
23
|
+
* @returns {object} ctx
|
|
24
|
+
*/
|
|
25
|
+
export function createContext(cwd) {
|
|
26
|
+
return {
|
|
27
|
+
cwd,
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read a file relative to cwd.
|
|
31
|
+
* @param {string} path
|
|
32
|
+
* @returns {Promise<string>}
|
|
33
|
+
*/
|
|
34
|
+
async readFile(path) {
|
|
35
|
+
return readFile(resolve(cwd, path), 'utf-8');
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write a file relative to cwd. Creates parent directories.
|
|
40
|
+
* @param {string} path
|
|
41
|
+
* @param {string} content
|
|
42
|
+
*/
|
|
43
|
+
async writeFile(path, content) {
|
|
44
|
+
const full = resolve(cwd, path);
|
|
45
|
+
await mkdir(dirname(full), { recursive: true });
|
|
46
|
+
await writeFile(full, content, 'utf-8');
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read a JSON file relative to cwd.
|
|
51
|
+
* @param {string} path
|
|
52
|
+
* @returns {Promise<object>}
|
|
53
|
+
*/
|
|
54
|
+
async readJSON(path) {
|
|
55
|
+
const raw = await readFile(resolve(cwd, path), 'utf-8');
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write a JSON file atomically (write-to-temp-then-rename).
|
|
61
|
+
* Prevents corruption if process crashes mid-write.
|
|
62
|
+
* @param {string} path
|
|
63
|
+
* @param {object} data
|
|
64
|
+
*/
|
|
65
|
+
async writeJSON(path, data) {
|
|
66
|
+
const full = resolve(cwd, path);
|
|
67
|
+
const dir = dirname(full);
|
|
68
|
+
await mkdir(dir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const tmpName = `.${randomBytes(6).toString('hex')}.tmp`;
|
|
71
|
+
const tmpPath = join(dir, tmpName);
|
|
72
|
+
const content = JSON.stringify(data, null, 2) + '\n';
|
|
73
|
+
|
|
74
|
+
await writeFile(tmpPath, content, 'utf-8');
|
|
75
|
+
await rename(tmpPath, full);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Append a JSON line to a JSONL file.
|
|
80
|
+
* @param {string} path
|
|
81
|
+
* @param {object} data
|
|
82
|
+
*/
|
|
83
|
+
async appendJSONL(path, data) {
|
|
84
|
+
const full = resolve(cwd, path);
|
|
85
|
+
await mkdir(dirname(full), { recursive: true });
|
|
86
|
+
await appendFile(full, JSON.stringify(data) + '\n', 'utf-8');
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if a file exists relative to cwd.
|
|
91
|
+
* @param {string} path
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
exists(path) {
|
|
95
|
+
return existsSync(resolve(cwd, path));
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// ── Logging (respects NO_COLOR and non-TTY) ─────
|
|
99
|
+
log(msg) { console.log(msg); },
|
|
100
|
+
success(msg) { console.log(`${color('32', '✓')} ${msg}`); },
|
|
101
|
+
warn(msg) { console.log(`${color('33', '⚠')} ${msg}`); },
|
|
102
|
+
error(msg) { console.error(`${color('31', '✗')} ${msg}`); },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentix Dev Server — 로컬 테스트용
|
|
3
|
+
*
|
|
4
|
+
* Governor 상태, Memory Layer, 에이전트 메트릭스를 JSON API로 제공.
|
|
5
|
+
* 대시보드 개발 시 백엔드로 사용.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node src/dev-server.js [port]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createServer } from 'node:http';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const PORT = parseInt(process.argv[2] || '4400', 10);
|
|
16
|
+
const CWD = process.cwd();
|
|
17
|
+
|
|
18
|
+
function filePath(rel) {
|
|
19
|
+
return resolve(CWD, rel);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readJSON(path) {
|
|
23
|
+
try {
|
|
24
|
+
const full = filePath(path);
|
|
25
|
+
if (!existsSync(full)) return null;
|
|
26
|
+
return JSON.parse(await readFile(full, 'utf-8'));
|
|
27
|
+
} catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readText(path) {
|
|
31
|
+
try {
|
|
32
|
+
const full = filePath(path);
|
|
33
|
+
if (!existsSync(full)) return null;
|
|
34
|
+
return await readFile(full, 'utf-8');
|
|
35
|
+
} catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readJSONL(path) {
|
|
39
|
+
const text = await readText(path);
|
|
40
|
+
if (!text) return [];
|
|
41
|
+
return text.trim().split('\n').filter(Boolean).map(line => {
|
|
42
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
43
|
+
}).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const routes = {
|
|
47
|
+
'/': async () => ({
|
|
48
|
+
name: 'sentix-dev-server',
|
|
49
|
+
version: '2.0.1',
|
|
50
|
+
endpoints: Object.keys(routes),
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
'/api/status': async () => ({
|
|
54
|
+
governor: await readJSON('tasks/governor-state.json'),
|
|
55
|
+
lessons: (await readText('tasks/lessons.md'))?.split('\n').filter(l => l.startsWith('- ')).length || 0,
|
|
56
|
+
patterns: (await readText('tasks/patterns.md'))?.split('\n').filter(l => l.startsWith('- ')).length || 0,
|
|
57
|
+
patternLogEntries: (await readJSONL('tasks/pattern-log.jsonl')).length,
|
|
58
|
+
metricsEntries: (await readJSONL('tasks/agent-metrics.jsonl')).length,
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
'/api/governor': async () => await readJSON('tasks/governor-state.json') || { status: 'idle' },
|
|
62
|
+
|
|
63
|
+
'/api/lessons': async () => {
|
|
64
|
+
const text = await readText('tasks/lessons.md');
|
|
65
|
+
if (!text) return { entries: [] };
|
|
66
|
+
const entries = text.split('\n').filter(l => l.startsWith('- ')).map(l => l.slice(2));
|
|
67
|
+
return { count: entries.length, entries };
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
'/api/patterns': async () => {
|
|
71
|
+
const text = await readText('tasks/patterns.md');
|
|
72
|
+
if (!text) return { entries: [] };
|
|
73
|
+
const entries = text.split('\n').filter(l => l.startsWith('- ')).map(l => l.slice(2));
|
|
74
|
+
return { count: entries.length, entries };
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'/api/predictions': async () => await readText('tasks/predictions.md') || '',
|
|
78
|
+
|
|
79
|
+
'/api/metrics': async () => {
|
|
80
|
+
const entries = await readJSONL('tasks/agent-metrics.jsonl');
|
|
81
|
+
const byAgent = {};
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
const agent = e.agent || 'unknown';
|
|
84
|
+
if (!byAgent[agent]) byAgent[agent] = [];
|
|
85
|
+
byAgent[agent].push(e);
|
|
86
|
+
}
|
|
87
|
+
return { totalRecords: entries.length, byAgent };
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
'/api/security': async () => await readText('tasks/security-report.md') || '',
|
|
91
|
+
|
|
92
|
+
'/api/roadmap': async () => await readText('tasks/roadmap.md') || '',
|
|
93
|
+
|
|
94
|
+
'/api/pattern-log': async () => {
|
|
95
|
+
const entries = await readJSONL('tasks/pattern-log.jsonl');
|
|
96
|
+
return { count: entries.length, entries: entries.slice(-100) }; // last 100
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
'/api/tickets': async () => {
|
|
100
|
+
const index = await readJSON('tasks/tickets/index.json');
|
|
101
|
+
if (!index) return { count: 0, tickets: [] };
|
|
102
|
+
return { count: index.length, tickets: index };
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
'/api/version': async () => {
|
|
106
|
+
const pkg = await readJSON('package.json');
|
|
107
|
+
return {
|
|
108
|
+
current: pkg?.version || 'unknown',
|
|
109
|
+
name: pkg?.name || 'unknown',
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
'/api/features': async () => {
|
|
114
|
+
const index = await readJSON('tasks/tickets/index.json');
|
|
115
|
+
if (!index) return { count: 0, features: [] };
|
|
116
|
+
const features = index.filter(t => t.type === 'feature');
|
|
117
|
+
return { count: features.length, features };
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
'/health': async () => ({ ok: true, ts: new Date().toISOString() }),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const server = createServer(async (req, res) => {
|
|
124
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
125
|
+
const handler = routes[url.pathname];
|
|
126
|
+
|
|
127
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
128
|
+
res.setHeader('Content-Type', 'application/json');
|
|
129
|
+
|
|
130
|
+
if (!handler) {
|
|
131
|
+
res.writeHead(404);
|
|
132
|
+
res.end(JSON.stringify({ error: 'Not found', available: Object.keys(routes) }));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const data = await handler();
|
|
138
|
+
res.writeHead(200);
|
|
139
|
+
res.end(typeof data === 'string' ? JSON.stringify({ content: data }) : JSON.stringify(data, null, 2));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
res.writeHead(500);
|
|
142
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
server.listen(PORT, () => {
|
|
147
|
+
console.log(`Sentix dev server running at http://localhost:${PORT}`);
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log('Endpoints:');
|
|
150
|
+
for (const path of Object.keys(routes)) {
|
|
151
|
+
console.log(` http://localhost:${PORT}${path}`);
|
|
152
|
+
}
|
|
153
|
+
console.log('');
|
|
154
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-loop.js — Agentic loop (Engine mode)
|
|
3
|
+
*
|
|
4
|
+
* AI에게 프롬프트를 보내고, 도구 호출을 처리하고, 결과를 돌려주는 루프.
|
|
5
|
+
* Claude Code가 내부적으로 하는 일을 sentix 코드로 구현한 것.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TOOLS, executeTool } from './tools.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Agentic loop 실행
|
|
12
|
+
* @param {object} client - createClient() 결과
|
|
13
|
+
* @param {string} systemPrompt - 에이전트 시스템 프롬프트
|
|
14
|
+
* @param {string} userMessage - 사용자 요청
|
|
15
|
+
* @param {object} ctx - sentix context
|
|
16
|
+
* @param {object} [options] - { maxTurns, onToolCall }
|
|
17
|
+
* @returns {Promise<object>} { content, turns, tool_calls_total, usage }
|
|
18
|
+
*/
|
|
19
|
+
export async function runAgentLoop(client, systemPrompt, userMessage, ctx, options = {}) {
|
|
20
|
+
const maxTurns = options.maxTurns || 50;
|
|
21
|
+
const onToolCall = options.onToolCall || (() => {});
|
|
22
|
+
|
|
23
|
+
const messages = [{ role: 'user', content: userMessage }];
|
|
24
|
+
let totalToolCalls = 0;
|
|
25
|
+
let totalUsage = { input_tokens: 0, output_tokens: 0 };
|
|
26
|
+
|
|
27
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
28
|
+
const response = await client.chat(systemPrompt, messages, TOOLS);
|
|
29
|
+
|
|
30
|
+
// 토큰 사용량 집계
|
|
31
|
+
if (response.usage) {
|
|
32
|
+
totalUsage.input_tokens += response.usage.input_tokens || 0;
|
|
33
|
+
totalUsage.output_tokens += response.usage.output_tokens || 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 도구 호출 없음 → 최종 응답
|
|
37
|
+
if (response.tool_calls.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
content: response.content,
|
|
40
|
+
turns: turn + 1,
|
|
41
|
+
tool_calls_total: totalToolCalls,
|
|
42
|
+
usage: totalUsage,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 도구 호출 처리
|
|
47
|
+
// AI의 응답을 assistant 메시지로 추가 (Anthropic 포맷)
|
|
48
|
+
if (client.name === 'claude') {
|
|
49
|
+
// Anthropic: content 배열로 구성
|
|
50
|
+
const contentBlocks = [];
|
|
51
|
+
if (response.content) {
|
|
52
|
+
contentBlocks.push({ type: 'text', text: response.content });
|
|
53
|
+
}
|
|
54
|
+
for (const tc of response.tool_calls) {
|
|
55
|
+
contentBlocks.push({
|
|
56
|
+
type: 'tool_use',
|
|
57
|
+
id: tc.id,
|
|
58
|
+
name: tc.name,
|
|
59
|
+
input: tc.arguments,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
messages.push({ role: 'assistant', content: contentBlocks });
|
|
63
|
+
|
|
64
|
+
// 도구 결과를 user 메시지로 추가 (Anthropic tool_result 포맷)
|
|
65
|
+
const toolResults = [];
|
|
66
|
+
for (const tc of response.tool_calls) {
|
|
67
|
+
totalToolCalls++;
|
|
68
|
+
onToolCall(tc.name, tc.arguments);
|
|
69
|
+
|
|
70
|
+
const result = await executeTool(tc.name, tc.arguments, ctx);
|
|
71
|
+
toolResults.push({
|
|
72
|
+
type: 'tool_result',
|
|
73
|
+
tool_use_id: tc.id,
|
|
74
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
messages.push({ role: 'user', content: toolResults });
|
|
78
|
+
} else {
|
|
79
|
+
// OpenAI/Ollama: 표준 tool_calls 포맷
|
|
80
|
+
messages.push({
|
|
81
|
+
role: 'assistant',
|
|
82
|
+
content: response.content || null,
|
|
83
|
+
tool_calls: response.tool_calls.map(tc => ({
|
|
84
|
+
id: tc.id,
|
|
85
|
+
type: 'function',
|
|
86
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
|
|
87
|
+
})),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
for (const tc of response.tool_calls) {
|
|
91
|
+
totalToolCalls++;
|
|
92
|
+
onToolCall(tc.name, tc.arguments);
|
|
93
|
+
|
|
94
|
+
const result = await executeTool(tc.name, tc.arguments, ctx);
|
|
95
|
+
messages.push({
|
|
96
|
+
role: 'tool',
|
|
97
|
+
tool_call_id: tc.id,
|
|
98
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
content: '[Agent loop reached max turns]',
|
|
106
|
+
turns: maxTurns,
|
|
107
|
+
tool_calls_total: totalToolCalls,
|
|
108
|
+
usage: totalUsage,
|
|
109
|
+
};
|
|
110
|
+
}
|