tycono 0.1.6 → 0.1.8
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/routes/git.ts +179 -0
- package/src/api/src/routes/save.ts +15 -1
- package/src/api/src/routes/speech.ts +29 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/services/git-save.ts +17 -2
- package/src/api/src/services/preferences.ts +31 -0
- package/src/api/src/services/scaffold.ts +1 -1
- package/src/api/src/utils/role-level.ts +30 -0
- package/src/web/dist/assets/index-BxDf0CSf.css +1 -0
- package/src/web/dist/assets/index-FesWAu2a.js +95 -0
- package/src/web/dist/assets/{preview-app-DgPZuBe8.js → preview-app-DfJ0nbgX.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/bin/init.ts +0 -306
- package/src/web/dist/assets/index-CM-X2pR7.css +0 -1
- package/src/web/dist/assets/index-N0j2q1cx.js +0 -95
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
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
3
|
-
import { getGitStatus, gitSave, gitHistory, gitRestore } from '../services/git-save.js';
|
|
3
|
+
import { getGitStatus, gitSave, gitHistory, gitRestore, gitInit } from '../services/git-save.js';
|
|
4
4
|
|
|
5
5
|
export const saveRouter = Router();
|
|
6
6
|
|
|
@@ -38,6 +38,20 @@ saveRouter.get('/history', (req: Request, res: Response, next: NextFunction) =>
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
// POST /api/save/init — initialize git repo
|
|
42
|
+
saveRouter.post('/init', (_req: Request, res: Response, next: NextFunction) => {
|
|
43
|
+
try {
|
|
44
|
+
const result = gitInit(COMPANY_ROOT);
|
|
45
|
+
if (!result.ok) {
|
|
46
|
+
res.status(500).json({ error: result.message });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
res.json(result);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
next(err);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
41
55
|
// POST /api/save/restore
|
|
42
56
|
saveRouter.post('/restore', (req: Request, res: Response, next: NextFunction) => {
|
|
43
57
|
try {
|
|
@@ -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
|
+
});
|
|
@@ -65,14 +65,14 @@ const SAVE_PATHS = [
|
|
|
65
65
|
|
|
66
66
|
function run(cmd: string, cwd: string): string {
|
|
67
67
|
try {
|
|
68
|
-
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30000 }).trim();
|
|
68
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
69
69
|
} catch {
|
|
70
70
|
return '';
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function runOrThrow(cmd: string, cwd: string): string {
|
|
75
|
-
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30000 }).trim();
|
|
75
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Check if directory is a git repository */
|
|
@@ -80,6 +80,21 @@ function isGitRepo(root: string): boolean {
|
|
|
80
80
|
return run('git rev-parse --is-inside-work-tree', root) === 'true';
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Initialize a new git repository */
|
|
84
|
+
export function gitInit(root: string): { ok: boolean; message: string } {
|
|
85
|
+
if (isGitRepo(root)) {
|
|
86
|
+
return { ok: true, message: 'Already a git repository' };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
runOrThrow('git init', root);
|
|
90
|
+
runOrThrow('git add -A', root);
|
|
91
|
+
runOrThrow('git commit -m "Initial commit by Tycono"', root);
|
|
92
|
+
return { ok: true, message: 'Git repository initialized with initial commit' };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return { ok: false, message: err instanceof Error ? err.message : 'git init failed' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/** Get current git status. Returns noGit=true if not a git repo. */
|
|
84
99
|
export function getGitStatus(root: string): GitStatus {
|
|
85
100
|
if (!isGitRepo(root)) {
|
|
@@ -29,12 +29,31 @@ export interface FurnitureOverride {
|
|
|
29
29
|
offsetY: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface DeskOverride {
|
|
33
|
+
dx: number;
|
|
34
|
+
dy: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AddedFurniture {
|
|
38
|
+
id: string;
|
|
39
|
+
type: string;
|
|
40
|
+
room: string;
|
|
41
|
+
zone: 'wall' | 'floor';
|
|
42
|
+
anchorX?: 'left' | 'right';
|
|
43
|
+
offsetX: number;
|
|
44
|
+
offsetY: number;
|
|
45
|
+
accent?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
export interface Preferences {
|
|
33
49
|
appearances: Record<string, CharacterAppearance>;
|
|
34
50
|
theme: string;
|
|
35
51
|
speech?: SpeechSettings;
|
|
36
52
|
language?: string; // 'en' | 'ko' | 'ja' | 'auto'
|
|
37
53
|
furnitureOverrides?: Record<string, FurnitureOverride>; // keyed by FurnitureDef.id
|
|
54
|
+
deskOverrides?: Record<string, DeskOverride>; // keyed by role id
|
|
55
|
+
removedFurniture?: string[]; // FurnitureDef.id list
|
|
56
|
+
addedFurniture?: AddedFurniture[];
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
const CONFIG_DIR = '.tycono';
|
|
@@ -57,6 +76,9 @@ export function readPreferences(companyRoot: string): Preferences {
|
|
|
57
76
|
speech: data.speech ?? undefined,
|
|
58
77
|
language: data.language ?? undefined,
|
|
59
78
|
furnitureOverrides: data.furnitureOverrides ?? undefined,
|
|
79
|
+
deskOverrides: data.deskOverrides ?? undefined,
|
|
80
|
+
removedFurniture: data.removedFurniture ?? undefined,
|
|
81
|
+
addedFurniture: data.addedFurniture ?? undefined,
|
|
60
82
|
};
|
|
61
83
|
} catch {
|
|
62
84
|
return { ...DEFAULT, appearances: {} };
|
|
@@ -85,6 +107,15 @@ export function mergePreferences(companyRoot: string, partial: Partial<Preferenc
|
|
|
85
107
|
furnitureOverrides: partial.furnitureOverrides !== undefined
|
|
86
108
|
? { ...current.furnitureOverrides, ...partial.furnitureOverrides }
|
|
87
109
|
: current.furnitureOverrides,
|
|
110
|
+
deskOverrides: partial.deskOverrides !== undefined
|
|
111
|
+
? { ...current.deskOverrides, ...partial.deskOverrides }
|
|
112
|
+
: current.deskOverrides,
|
|
113
|
+
removedFurniture: partial.removedFurniture !== undefined
|
|
114
|
+
? partial.removedFurniture
|
|
115
|
+
: current.removedFurniture,
|
|
116
|
+
addedFurniture: partial.addedFurniture !== undefined
|
|
117
|
+
? partial.addedFurniture
|
|
118
|
+
: current.addedFurniture,
|
|
88
119
|
};
|
|
89
120
|
writePreferences(companyRoot, merged);
|
|
90
121
|
return merged;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* role-level.ts — Server-side role level calculation
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the frontend level thresholds so the chat system prompt
|
|
5
|
+
* can reference a role's current level and team stats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const THRESHOLDS = [
|
|
9
|
+
0, // Lv.1
|
|
10
|
+
10_000, // Lv.2
|
|
11
|
+
50_000, // Lv.3
|
|
12
|
+
150_000, // Lv.4
|
|
13
|
+
400_000, // Lv.5
|
|
14
|
+
1_000_000, // Lv.6
|
|
15
|
+
2_500_000, // Lv.7
|
|
16
|
+
5_000_000, // Lv.8
|
|
17
|
+
10_000_000, // Lv.9
|
|
18
|
+
25_000_000, // Lv.10
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function calcLevel(totalTokens: number): number {
|
|
22
|
+
let level = 1;
|
|
23
|
+
for (let i = THRESHOLDS.length - 1; i >= 0; i--) {
|
|
24
|
+
if (totalTokens >= THRESHOLDS[i]) {
|
|
25
|
+
level = i + 1;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Math.min(level, 10);
|
|
30
|
+
}
|