tycono 0.1.7 → 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 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. Your browser opens to a live dashboard showing your AI team at work.
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
- When you run `init`, pick a template:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 { 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
+ });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * scaffold.ts — AKB scaffolding service
3
3
  *
4
- * Extracted from bin/init.ts for reuse by the web onboarding wizard.
4
+ * AKB scaffolding used by the web onboarding wizard.
5
5
  */
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
@@ -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
+ }