tycono 0.1.7 → 0.1.9
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/README.md +3 -8
- package/bin/tycono.ts +0 -9
- package/package.json +1 -1
- package/src/api/src/create-server.ts +6 -2
- package/src/api/src/engine/org-tree.ts +20 -0
- package/src/api/src/engine/role-lifecycle.ts +9 -1
- package/src/api/src/routes/execute.ts +13 -4
- package/src/api/src/routes/git.ts +179 -0
- package/src/api/src/routes/knowledge.ts +20 -8
- package/src/api/src/routes/speech.ts +29 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/services/scaffold.ts +1 -1
- package/src/api/src/utils/role-level.ts +44 -0
- package/src/web/dist/assets/index-BaLxr7Du.css +1 -0
- package/src/web/dist/assets/index-D1NrKtsx.js +96 -0
- package/src/web/dist/assets/{preview-app-pniQIcGk.js → preview-app-CGBZJEYG.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/bin/init.ts +0 -306
- package/src/web/dist/assets/index-CZtcQkJ_.js +0 -95
- package/src/web/dist/assets/index-DIjVGqFh.css +0 -1
package/README.md
CHANGED
|
@@ -7,15 +7,11 @@
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# Create a new company
|
|
11
10
|
mkdir my-company && cd my-company
|
|
12
|
-
npx tycono init
|
|
13
|
-
|
|
14
|
-
# Start the dashboard
|
|
15
11
|
npx tycono
|
|
16
12
|
```
|
|
17
13
|
|
|
18
|
-
That's it.
|
|
14
|
+
That's it. A setup wizard guides you through creating your company, then your browser opens to a live dashboard showing your AI team at work.
|
|
19
15
|
|
|
20
16
|
## What You Get
|
|
21
17
|
|
|
@@ -33,7 +29,7 @@ That's it. Your browser opens to a live dashboard showing your AI team at work.
|
|
|
33
29
|
|
|
34
30
|
## Team Templates
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
During setup, pick a template:
|
|
37
33
|
|
|
38
34
|
| Template | Roles |
|
|
39
35
|
|----------|-------|
|
|
@@ -74,8 +70,7 @@ your-company/
|
|
|
74
70
|
## CLI Usage
|
|
75
71
|
|
|
76
72
|
```bash
|
|
77
|
-
tycono # Start server + open dashboard
|
|
78
|
-
tycono init # Create a new company
|
|
73
|
+
tycono # Start server + open dashboard (setup wizard if first run)
|
|
79
74
|
tycono --help # Show help
|
|
80
75
|
tycono --version # Show version
|
|
81
76
|
```
|
package/bin/tycono.ts
CHANGED
|
@@ -20,9 +20,6 @@ function printHelp(): void {
|
|
|
20
20
|
|
|
21
21
|
Usage:
|
|
22
22
|
tycono Start the server and open dashboard
|
|
23
|
-
tycono init Initialize a new company in current directory
|
|
24
|
-
tycono init -y Use defaults (skip prompts)
|
|
25
|
-
tycono init --name "Acme Corp" Set company name
|
|
26
23
|
tycono --help Show this help message
|
|
27
24
|
tycono --version Show version
|
|
28
25
|
|
|
@@ -178,12 +175,6 @@ export async function main(args: string[]): Promise<void> {
|
|
|
178
175
|
return;
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
if (command === 'init') {
|
|
182
|
-
const { runInit } = await import('./init.js');
|
|
183
|
-
await runInit(args.slice(1));
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
178
|
if (command && !command.startsWith('-')) {
|
|
188
179
|
console.error(` Unknown command: ${command}`);
|
|
189
180
|
printHelp();
|
package/package.json
CHANGED
|
@@ -26,6 +26,8 @@ import { preferencesRouter } from './routes/preferences.js';
|
|
|
26
26
|
import { saveRouter } from './routes/save.js';
|
|
27
27
|
import { speechRouter } from './routes/speech.js';
|
|
28
28
|
import { costRouter } from './routes/cost.js';
|
|
29
|
+
import { syncRouter } from './routes/sync.js';
|
|
30
|
+
import { gitRouter } from './routes/git.js';
|
|
29
31
|
import { importKnowledge } from './services/knowledge-importer.js';
|
|
30
32
|
import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
|
|
31
33
|
import { readConfig } from './services/company-config.js';
|
|
@@ -182,6 +184,8 @@ export function createExpressApp(): express.Application {
|
|
|
182
184
|
app.use('/api/speech', speechRouter);
|
|
183
185
|
app.use('/api/save', saveRouter);
|
|
184
186
|
app.use('/api/cost', costRouter);
|
|
187
|
+
app.use('/api/sync', syncRouter);
|
|
188
|
+
app.use('/api/git', gitRouter);
|
|
185
189
|
|
|
186
190
|
app.get('/api/health', (_req, res) => {
|
|
187
191
|
res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
|
|
@@ -196,8 +200,8 @@ export function createExpressApp(): express.Application {
|
|
|
196
200
|
});
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
app.use((err: Error,
|
|
200
|
-
console.error(`[ERROR] ${err.message}`);
|
|
203
|
+
app.use((err: Error, req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
204
|
+
console.error(`[ERROR] ${req.method} ${req.url} — ${err.message}`);
|
|
201
205
|
const status = err.name === 'FileNotFoundError' ? 404 : 500;
|
|
202
206
|
res.status(status).json({ error: err.message });
|
|
203
207
|
});
|
|
@@ -14,6 +14,13 @@ export interface KnowledgeAccess {
|
|
|
14
14
|
writes: string[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface RoleSource {
|
|
18
|
+
id: string;
|
|
19
|
+
sync: 'auto' | 'manual' | 'off';
|
|
20
|
+
forked_at?: string;
|
|
21
|
+
upstream_version?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface OrgNode {
|
|
18
25
|
id: string;
|
|
19
26
|
name: string;
|
|
@@ -26,6 +33,7 @@ export interface OrgNode {
|
|
|
26
33
|
reports: { daily: string; weekly: string };
|
|
27
34
|
skills?: string[];
|
|
28
35
|
model?: string;
|
|
36
|
+
source?: RoleSource;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface OrgTree {
|
|
@@ -55,6 +63,12 @@ interface RawRoleYaml {
|
|
|
55
63
|
};
|
|
56
64
|
skills?: string[];
|
|
57
65
|
model?: string;
|
|
66
|
+
source?: {
|
|
67
|
+
id?: string;
|
|
68
|
+
sync?: string;
|
|
69
|
+
forked_at?: string;
|
|
70
|
+
upstream_version?: string;
|
|
71
|
+
};
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
/* ─── Build ──────────────────────────────────── */
|
|
@@ -108,6 +122,12 @@ export function buildOrgTree(companyRoot: string): OrgTree {
|
|
|
108
122
|
},
|
|
109
123
|
skills: raw.skills,
|
|
110
124
|
model: raw.model,
|
|
125
|
+
source: raw.source ? {
|
|
126
|
+
id: raw.source.id || '',
|
|
127
|
+
sync: (raw.source.sync as RoleSource['sync']) || 'manual',
|
|
128
|
+
forked_at: raw.source.forked_at,
|
|
129
|
+
upstream_version: raw.source.upstream_version,
|
|
130
|
+
} : undefined,
|
|
111
131
|
};
|
|
112
132
|
tree.nodes.set(node.id, node);
|
|
113
133
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
|
-
import { buildOrgTree, type OrgNode, type OrgTree } from './org-tree.js';
|
|
4
|
+
import { buildOrgTree, type OrgNode, type OrgTree, type RoleSource } from './org-tree.js';
|
|
5
5
|
import { generateSkillMd } from './skill-template.js';
|
|
6
6
|
|
|
7
7
|
/* ─── Types ──────────────────────────────────── */
|
|
@@ -13,6 +13,7 @@ export interface RoleDefinition {
|
|
|
13
13
|
reportsTo: string;
|
|
14
14
|
persona: string;
|
|
15
15
|
skills?: string[];
|
|
16
|
+
source?: RoleSource;
|
|
16
17
|
authority: {
|
|
17
18
|
autonomous: string[];
|
|
18
19
|
needsApproval: string[];
|
|
@@ -107,6 +108,9 @@ export class RoleLifecycleManager {
|
|
|
107
108
|
if (changes.reports !== undefined) {
|
|
108
109
|
current.reports = changes.reports;
|
|
109
110
|
}
|
|
111
|
+
if (changes.source !== undefined) {
|
|
112
|
+
current.source = changes.source;
|
|
113
|
+
}
|
|
110
114
|
|
|
111
115
|
fs.writeFileSync(yamlPath, YAML.stringify(current));
|
|
112
116
|
}
|
|
@@ -233,6 +237,7 @@ export class RoleLifecycleManager {
|
|
|
233
237
|
},
|
|
234
238
|
reports: def.reports,
|
|
235
239
|
skills: def.skills,
|
|
240
|
+
source: def.source,
|
|
236
241
|
};
|
|
237
242
|
}
|
|
238
243
|
|
|
@@ -247,6 +252,9 @@ export class RoleLifecycleManager {
|
|
|
247
252
|
if (def.skills?.length) {
|
|
248
253
|
obj.skills = def.skills;
|
|
249
254
|
}
|
|
255
|
+
if (def.source) {
|
|
256
|
+
obj.source = def.source;
|
|
257
|
+
}
|
|
250
258
|
obj.authority = {
|
|
251
259
|
autonomous: def.authority.autonomous,
|
|
252
260
|
needs_approval: def.authority.needsApproval,
|
|
@@ -487,12 +487,16 @@ function handleStatus(res: ServerResponse): void {
|
|
|
487
487
|
const runningJobs = jobManager.listJobs({ status: 'running' });
|
|
488
488
|
const runningRoles = new Set(runningJobs.map(j => j.roleId));
|
|
489
489
|
|
|
490
|
-
//
|
|
490
|
+
// 2b. In-memory roleStatus (includes chat streaming sessions, not just jobs)
|
|
491
|
+
const memoryWorking = new Set<string>();
|
|
492
|
+
for (const [rid, st] of roleStatus.entries()) {
|
|
493
|
+
if (st === 'working') memoryWorking.add(rid);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 3. Any role marked "working" in file but NOT in JobManager AND NOT in memory → done
|
|
491
497
|
for (const roleId of Object.keys(statuses)) {
|
|
492
|
-
if (statuses[roleId] === 'working' && !runningRoles.has(roleId)) {
|
|
498
|
+
if (statuses[roleId] === 'working' && !runningRoles.has(roleId) && !memoryWorking.has(roleId)) {
|
|
493
499
|
statuses[roleId] = 'done';
|
|
494
|
-
// Also fix stale roleStatus map
|
|
495
|
-
roleStatus.set(roleId, 'idle');
|
|
496
500
|
completeActivity(roleId);
|
|
497
501
|
}
|
|
498
502
|
}
|
|
@@ -502,6 +506,11 @@ function handleStatus(res: ServerResponse): void {
|
|
|
502
506
|
statuses[job.roleId] = 'working';
|
|
503
507
|
}
|
|
504
508
|
|
|
509
|
+
// 5. In-memory working (chat streaming) also overrides
|
|
510
|
+
for (const rid of memoryWorking) {
|
|
511
|
+
statuses[rid] = 'working';
|
|
512
|
+
}
|
|
513
|
+
|
|
505
514
|
const activeExecs = runningJobs.map((j) => ({
|
|
506
515
|
id: j.id,
|
|
507
516
|
roleId: j.roleId,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
4
|
+
|
|
5
|
+
export const gitRouter = Router();
|
|
6
|
+
|
|
7
|
+
interface WorktreeInfo {
|
|
8
|
+
path: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
commitHash: string;
|
|
11
|
+
isMain: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface LastCommit {
|
|
15
|
+
hash: string;
|
|
16
|
+
message: string;
|
|
17
|
+
date: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function git(cmd: string): string {
|
|
21
|
+
return execSync(`git ${cmd}`, { cwd: COMPANY_ROOT, encoding: 'utf-8' }).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// GET /api/git/status
|
|
25
|
+
gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
|
|
26
|
+
try {
|
|
27
|
+
// Current branch
|
|
28
|
+
let currentBranch: string;
|
|
29
|
+
try {
|
|
30
|
+
currentBranch = git('rev-parse --abbrev-ref HEAD');
|
|
31
|
+
} catch {
|
|
32
|
+
res.status(500).json({ error: 'Not a git repository or git is not available' });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Worktrees
|
|
37
|
+
let worktrees: WorktreeInfo[] = [];
|
|
38
|
+
try {
|
|
39
|
+
const raw = git('worktree list --porcelain');
|
|
40
|
+
const blocks = raw.split('\n\n').filter(Boolean);
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
const lines = block.split('\n');
|
|
43
|
+
const wtPath = lines.find(l => l.startsWith('worktree '))?.replace('worktree ', '') ?? '';
|
|
44
|
+
const commitHash = lines.find(l => l.startsWith('HEAD '))?.replace('HEAD ', '') ?? '';
|
|
45
|
+
const branchLine = lines.find(l => l.startsWith('branch '));
|
|
46
|
+
const branch = branchLine ? branchLine.replace('branch refs/heads/', '') : '(detached)';
|
|
47
|
+
const isMain = lines.some(l => l === 'worktree ' + COMPANY_ROOT) ||
|
|
48
|
+
(!branchLine && lines.some(l => l === 'bare'));
|
|
49
|
+
worktrees.push({
|
|
50
|
+
path: wtPath,
|
|
51
|
+
branch,
|
|
52
|
+
commitHash,
|
|
53
|
+
isMain: wtPath === COMPANY_ROOT,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
worktrees = [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Stale (unmerged) branches
|
|
61
|
+
let staleBranches: string[] = [];
|
|
62
|
+
try {
|
|
63
|
+
const raw = git('branch --no-merged develop');
|
|
64
|
+
staleBranches = raw
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map(b => b.trim().replace(/^\*\s*/, ''))
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
} catch {
|
|
69
|
+
staleBranches = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Unsaved changes count
|
|
73
|
+
let unsavedChanges = 0;
|
|
74
|
+
try {
|
|
75
|
+
const raw = git('status --porcelain');
|
|
76
|
+
unsavedChanges = raw ? raw.split('\n').filter(Boolean).length : 0;
|
|
77
|
+
} catch {
|
|
78
|
+
unsavedChanges = 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Last commit
|
|
82
|
+
let lastCommit: LastCommit | null = null;
|
|
83
|
+
try {
|
|
84
|
+
const raw = git('log -1 --format=%H%n%s%n%aI');
|
|
85
|
+
const [hash, message, date] = raw.split('\n');
|
|
86
|
+
if (hash) {
|
|
87
|
+
lastCommit = { hash, message: message ?? '', date: date ?? '' };
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
lastCommit = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
res.json({
|
|
94
|
+
currentBranch,
|
|
95
|
+
worktrees,
|
|
96
|
+
staleBranches,
|
|
97
|
+
unsavedChanges,
|
|
98
|
+
lastCommit,
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
next(err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// DELETE /api/git/worktree/:path — Remove a worktree
|
|
106
|
+
gitRouter.delete('/worktree/{*path}', (req: Request, res: Response, next: NextFunction) => {
|
|
107
|
+
try {
|
|
108
|
+
const rawPath = (req.params as Record<string, unknown>).path;
|
|
109
|
+
const worktreePath = Array.isArray(rawPath) ? rawPath.join('/') : String(rawPath ?? '');
|
|
110
|
+
if (!worktreePath) {
|
|
111
|
+
res.status(400).json({ error: 'Worktree path is required' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
git(`worktree remove ${JSON.stringify(worktreePath)}`);
|
|
117
|
+
} catch {
|
|
118
|
+
// Try force remove if normal remove fails
|
|
119
|
+
git(`worktree remove --force ${JSON.stringify(worktreePath)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.json({ success: true, removed: worktreePath });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : 'Failed to remove worktree';
|
|
125
|
+
res.status(500).json({ error: message });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// DELETE /api/git/branch/:name — Delete a branch (local + remote)
|
|
130
|
+
gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunction) => {
|
|
131
|
+
try {
|
|
132
|
+
const rawName = (req.params as Record<string, unknown>).name;
|
|
133
|
+
const branchName = Array.isArray(rawName) ? rawName.join('/') : String(rawName ?? '');
|
|
134
|
+
if (!branchName) {
|
|
135
|
+
res.status(400).json({ error: 'Branch name is required' });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Prevent deleting main/develop
|
|
140
|
+
if (branchName === 'main' || branchName === 'develop') {
|
|
141
|
+
res.status(403).json({ error: `Cannot delete protected branch: ${branchName}` });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const errors: string[] = [];
|
|
146
|
+
|
|
147
|
+
// Delete local branch
|
|
148
|
+
try {
|
|
149
|
+
git(`branch -d ${JSON.stringify(branchName)}`);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
152
|
+
// If branch is not fully merged, report but continue to try remote
|
|
153
|
+
if (msg.includes('not fully merged')) {
|
|
154
|
+
errors.push(`Local branch not fully merged. Use force delete if intended.`);
|
|
155
|
+
} else if (!msg.includes('not found')) {
|
|
156
|
+
errors.push(`Local: ${msg}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Delete remote branch
|
|
161
|
+
try {
|
|
162
|
+
git(`push origin --delete ${JSON.stringify(branchName)}`);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
165
|
+
if (!msg.includes('remote ref does not exist')) {
|
|
166
|
+
errors.push(`Remote: ${msg}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (errors.length > 0) {
|
|
171
|
+
res.status(207).json({ success: false, branch: branchName, errors });
|
|
172
|
+
} else {
|
|
173
|
+
res.json({ success: true, deleted: branchName });
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const message = err instanceof Error ? err.message : 'Failed to delete branch';
|
|
177
|
+
res.status(500).json({ error: message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
@@ -14,6 +14,7 @@ import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
|
14
14
|
export const knowledgeRouter = Router();
|
|
15
15
|
|
|
16
16
|
const knowledgeDir = path.join(COMPANY_ROOT, 'knowledge');
|
|
17
|
+
const companyRoot = COMPANY_ROOT;
|
|
17
18
|
|
|
18
19
|
/* ─── Helpers ─────────────────────────────────────── */
|
|
19
20
|
|
|
@@ -60,17 +61,28 @@ function inferCategory(filePath: string, tags: string[]): string {
|
|
|
60
61
|
|
|
61
62
|
knowledgeRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
|
|
62
63
|
try {
|
|
63
|
-
if (!fs.existsSync(
|
|
64
|
+
if (!fs.existsSync(companyRoot)) {
|
|
64
65
|
res.json([]);
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const files = glob.sync('**/*.{md,html}', {
|
|
69
|
-
|
|
69
|
+
const files = glob.sync('**/*.{md,html}', {
|
|
70
|
+
cwd: companyRoot,
|
|
71
|
+
ignore: [
|
|
72
|
+
'node_modules/**', '.claude/**', '.obsidian/**', '.tycono/**', '.git/**',
|
|
73
|
+
'**/node_modules/**',
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
.filter((f) => {
|
|
77
|
+
const base = path.basename(f);
|
|
78
|
+
// Exclude hub files (folder-name.md pattern) and CLAUDE.md
|
|
79
|
+
if (base === 'CLAUDE.md') return false;
|
|
80
|
+
return true;
|
|
81
|
+
})
|
|
70
82
|
.sort();
|
|
71
83
|
|
|
72
84
|
const docs = files.map((f) => {
|
|
73
|
-
const absPath = path.join(
|
|
85
|
+
const absPath = path.join(companyRoot, f);
|
|
74
86
|
let raw = '';
|
|
75
87
|
try { raw = fs.readFileSync(absPath, 'utf-8'); } catch { return null; }
|
|
76
88
|
|
|
@@ -194,8 +206,8 @@ knowledgeRouter.put('/{*path}', (req: Request, res: Response, next: NextFunction
|
|
|
194
206
|
return;
|
|
195
207
|
}
|
|
196
208
|
|
|
197
|
-
const absPath = path.join(
|
|
198
|
-
if (!absPath.startsWith(
|
|
209
|
+
const absPath = path.join(companyRoot, docId);
|
|
210
|
+
if (!absPath.startsWith(companyRoot)) {
|
|
199
211
|
res.status(403).json({ error: 'Forbidden' });
|
|
200
212
|
return;
|
|
201
213
|
}
|
|
@@ -236,8 +248,8 @@ knowledgeRouter.delete('/{*path}', (req: Request, res: Response, next: NextFunct
|
|
|
236
248
|
return;
|
|
237
249
|
}
|
|
238
250
|
|
|
239
|
-
const absPath = path.join(
|
|
240
|
-
if (!absPath.startsWith(
|
|
251
|
+
const absPath = path.join(companyRoot, docId);
|
|
252
|
+
if (!absPath.startsWith(companyRoot)) {
|
|
241
253
|
res.status(403).json({ error: 'Forbidden' });
|
|
242
254
|
return;
|
|
243
255
|
}
|
|
@@ -14,6 +14,7 @@ import { parseMarkdownTable, extractBoldKeyValues } from '../services/markdown-p
|
|
|
14
14
|
import { AnthropicProvider, ClaudeCliProvider, type LLMProvider } from '../engine/llm-adapter.js';
|
|
15
15
|
import { TokenLedger } from '../services/token-ledger.js';
|
|
16
16
|
import { readConfig } from '../services/company-config.js';
|
|
17
|
+
import { calcLevel } from '../utils/role-level.js';
|
|
17
18
|
|
|
18
19
|
export const speechRouter = Router();
|
|
19
20
|
|
|
@@ -147,6 +148,29 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
|
|
|
147
148
|
workContext?: { currentTask: string | null; taskProgress: string | null };
|
|
148
149
|
};
|
|
149
150
|
|
|
151
|
+
// ── Compute role levels from token ledger ──
|
|
152
|
+
const tokenLedger = getLedger();
|
|
153
|
+
const allEntries = tokenLedger.query();
|
|
154
|
+
|
|
155
|
+
// Aggregate total tokens (input + output) per role
|
|
156
|
+
const tokensByRole: Record<string, number> = {};
|
|
157
|
+
for (const entry of allEntries.entries) {
|
|
158
|
+
tokensByRole[entry.roleId] = (tokensByRole[entry.roleId] ?? 0) + entry.inputTokens + entry.outputTokens;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const roleLevel = calcLevel(tokensByRole[roleId] ?? 0);
|
|
162
|
+
|
|
163
|
+
// Team stats
|
|
164
|
+
const roleIds = Object.keys(tokensByRole);
|
|
165
|
+
const levels = roleIds.map(id => ({ id, level: calcLevel(tokensByRole[id]) }));
|
|
166
|
+
const avgLevel = levels.length > 0
|
|
167
|
+
? Math.round(levels.reduce((sum, r) => sum + r.level, 0) / levels.length)
|
|
168
|
+
: 1;
|
|
169
|
+
const topEntry = levels.reduce((best, r) => r.level > best.level ? r : best, { id: roleId, level: roleLevel });
|
|
170
|
+
const totalTokens = allEntries.totalInput + allEntries.totalOutput;
|
|
171
|
+
|
|
172
|
+
const teamStats = { avgLevel, topRole: topEntry.id, totalTokens };
|
|
173
|
+
|
|
150
174
|
if (!roleId || !channelId) {
|
|
151
175
|
res.status(400).json({ error: 'roleId and channelId are required' });
|
|
152
176
|
return;
|
|
@@ -190,6 +214,9 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
|
|
|
190
214
|
? `\nYou are currently working on: "${workContext.currentTask}"${workContext.taskProgress ? ` (${workContext.taskProgress})` : ''}`
|
|
191
215
|
: '\nYou are currently idle (no active task).';
|
|
192
216
|
|
|
217
|
+
// Build level context
|
|
218
|
+
const levelCtx = `\nYour current level is Lv.${roleLevel}. Team average is Lv.${avgLevel}. ${topEntry.id} is the highest-leveled team member.`;
|
|
219
|
+
|
|
193
220
|
// Format chat history
|
|
194
221
|
const historyText = history.length > 0
|
|
195
222
|
? history.map(h => {
|
|
@@ -209,6 +236,7 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
|
|
|
209
236
|
const systemPrompt = `You are ${node.name}, a ${node.level} employee.
|
|
210
237
|
Persona: ${persona}
|
|
211
238
|
${workCtx}
|
|
239
|
+
${levelCtx}
|
|
212
240
|
${companyCtx}
|
|
213
241
|
|
|
214
242
|
You are in the #${channelId} chat channel.${topicCtx}
|
|
@@ -224,6 +252,7 @@ Rules:
|
|
|
224
252
|
- Vary your tone: sometimes enthusiastic, sometimes tired, sometimes joking
|
|
225
253
|
- Use appropriate tone based on hierarchy and familiarity
|
|
226
254
|
- Do NOT repeat what others already said
|
|
255
|
+
- You may occasionally (not every message) reference levels, token usage, or team rankings in a natural way — like coworkers comparing experience or celebrating milestones
|
|
227
256
|
- If the conversation is stale or you have nothing new to add, respond with exactly: [SILENT]
|
|
228
257
|
- Do NOT use quotes around your response. Just output the raw sentence.
|
|
229
258
|
- Write in English.`;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
5
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
6
|
+
import { buildOrgTree, type RoleSource } from '../engine/org-tree.js';
|
|
7
|
+
import { RoleLifecycleManager } from '../engine/role-lifecycle.js';
|
|
8
|
+
import { getTokenLedger } from '../services/token-ledger.js';
|
|
9
|
+
import { estimateCost } from '../services/pricing.js';
|
|
10
|
+
import { calcLevel, calcProgress, formatTokens } from '../utils/role-level.js';
|
|
11
|
+
|
|
12
|
+
export const syncRouter = Router();
|
|
13
|
+
|
|
14
|
+
/* ─── GET /api/sync/roles — List roles with source tracking ─── */
|
|
15
|
+
|
|
16
|
+
syncRouter.get('/roles', (_req: Request, res: Response, next: NextFunction) => {
|
|
17
|
+
try {
|
|
18
|
+
const tree = buildOrgTree(COMPANY_ROOT);
|
|
19
|
+
const trackedRoles: Array<{
|
|
20
|
+
roleId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
level: string;
|
|
23
|
+
source: RoleSource;
|
|
24
|
+
persona: string;
|
|
25
|
+
authority: { autonomous: string[]; needsApproval: string[] };
|
|
26
|
+
skills?: string[];
|
|
27
|
+
}> = [];
|
|
28
|
+
|
|
29
|
+
for (const [id, node] of tree.nodes) {
|
|
30
|
+
if (id === 'ceo' || !node.source) continue;
|
|
31
|
+
trackedRoles.push({
|
|
32
|
+
roleId: id,
|
|
33
|
+
name: node.name,
|
|
34
|
+
level: node.level,
|
|
35
|
+
source: node.source,
|
|
36
|
+
persona: node.persona,
|
|
37
|
+
authority: node.authority,
|
|
38
|
+
skills: node.skills,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.json({ roles: trackedRoles });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
next(err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/* ─── POST /api/sync/apply — Apply upstream changes to a role ─── */
|
|
49
|
+
|
|
50
|
+
syncRouter.post('/apply', async (req: Request, res: Response, next: NextFunction) => {
|
|
51
|
+
try {
|
|
52
|
+
const { roleId, changes, upstreamVersion } = req.body as {
|
|
53
|
+
roleId: string;
|
|
54
|
+
changes: { persona?: string; authority?: { autonomous: string[]; needsApproval: string[] }; skills?: string[] };
|
|
55
|
+
upstreamVersion?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!roleId || !changes) {
|
|
59
|
+
res.status(400).json({ error: 'roleId and changes are required' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const manager = new RoleLifecycleManager(COMPANY_ROOT);
|
|
64
|
+
await manager.updateRole(roleId, changes);
|
|
65
|
+
|
|
66
|
+
// Update source.upstream_version if provided
|
|
67
|
+
if (upstreamVersion) {
|
|
68
|
+
const yamlPath = path.join(COMPANY_ROOT, 'roles', roleId, 'role.yaml');
|
|
69
|
+
const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
|
|
70
|
+
if (raw.source && typeof raw.source === 'object') {
|
|
71
|
+
(raw.source as Record<string, unknown>).upstream_version = upstreamVersion;
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(yamlPath, YAML.stringify(raw));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
res.json({ ok: true, roleId, applied: Object.keys(changes) });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
next(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/* ─── GET /api/sync/stats — Company-wide gamification stats ─── */
|
|
83
|
+
|
|
84
|
+
syncRouter.get('/stats', (_req: Request, res: Response, next: NextFunction) => {
|
|
85
|
+
try {
|
|
86
|
+
const tree = buildOrgTree(COMPANY_ROOT);
|
|
87
|
+
const ledger = getTokenLedger(COMPANY_ROOT);
|
|
88
|
+
const summary = ledger.query();
|
|
89
|
+
|
|
90
|
+
// Aggregate by role
|
|
91
|
+
const byRole: Record<string, { inputTokens: number; outputTokens: number; costUsd: number }> = {};
|
|
92
|
+
const byModel: Record<string, { inputTokens: number; outputTokens: number; costUsd: number }> = {};
|
|
93
|
+
|
|
94
|
+
for (const entry of summary.entries) {
|
|
95
|
+
if (!byRole[entry.roleId]) {
|
|
96
|
+
byRole[entry.roleId] = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
97
|
+
}
|
|
98
|
+
byRole[entry.roleId].inputTokens += entry.inputTokens;
|
|
99
|
+
byRole[entry.roleId].outputTokens += entry.outputTokens;
|
|
100
|
+
byRole[entry.roleId].costUsd += estimateCost(entry.inputTokens, entry.outputTokens, entry.model);
|
|
101
|
+
|
|
102
|
+
if (!byModel[entry.model]) {
|
|
103
|
+
byModel[entry.model] = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
104
|
+
}
|
|
105
|
+
byModel[entry.model].inputTokens += entry.inputTokens;
|
|
106
|
+
byModel[entry.model].outputTokens += entry.outputTokens;
|
|
107
|
+
byModel[entry.model].costUsd += estimateCost(entry.inputTokens, entry.outputTokens, entry.model);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Role count (excluding CEO)
|
|
111
|
+
const roleCount = tree.nodes.size - 1;
|
|
112
|
+
|
|
113
|
+
// Compute per-role levels
|
|
114
|
+
const roleLevels: Array<{
|
|
115
|
+
roleId: string;
|
|
116
|
+
name: string;
|
|
117
|
+
level: number;
|
|
118
|
+
totalTokens: number;
|
|
119
|
+
progress: number;
|
|
120
|
+
formattedTokens: string;
|
|
121
|
+
costUsd: number;
|
|
122
|
+
}> = [];
|
|
123
|
+
|
|
124
|
+
for (const [id, node] of tree.nodes) {
|
|
125
|
+
if (id === 'ceo') continue;
|
|
126
|
+
const roleData = byRole[id];
|
|
127
|
+
const totalTokens = roleData ? roleData.inputTokens + roleData.outputTokens : 0;
|
|
128
|
+
roleLevels.push({
|
|
129
|
+
roleId: id,
|
|
130
|
+
name: node.name,
|
|
131
|
+
level: calcLevel(totalTokens),
|
|
132
|
+
totalTokens,
|
|
133
|
+
progress: calcProgress(totalTokens),
|
|
134
|
+
formattedTokens: formatTokens(totalTokens),
|
|
135
|
+
costUsd: roleData?.costUsd ?? 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sort by totalTokens desc (leaderboard)
|
|
140
|
+
roleLevels.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
141
|
+
|
|
142
|
+
// Company aggregate
|
|
143
|
+
const totalTokens = summary.totalInput + summary.totalOutput;
|
|
144
|
+
let totalCostUsd = 0;
|
|
145
|
+
for (const entry of summary.entries) {
|
|
146
|
+
totalCostUsd += estimateCost(entry.inputTokens, entry.outputTokens, entry.model);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
res.json({
|
|
150
|
+
company: {
|
|
151
|
+
roleCount,
|
|
152
|
+
totalTokens,
|
|
153
|
+
formattedTokens: formatTokens(totalTokens),
|
|
154
|
+
totalCostUsd,
|
|
155
|
+
avgLevel: roleLevels.length > 0
|
|
156
|
+
? Math.round(roleLevels.reduce((sum, r) => sum + r.level, 0) / roleLevels.length * 10) / 10
|
|
157
|
+
: 1,
|
|
158
|
+
},
|
|
159
|
+
roles: roleLevels,
|
|
160
|
+
byModel,
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
next(err);
|
|
164
|
+
}
|
|
165
|
+
});
|