tycono 0.1.25 → 0.1.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,8 +13,8 @@ import { COMPANY_ROOT } from '../services/file-reader.js';
13
13
 
14
14
  export const knowledgeRouter = Router();
15
15
 
16
- const knowledgeDir = path.join(COMPANY_ROOT, 'knowledge');
17
- const companyRoot = COMPANY_ROOT;
16
+ function knowledgeDir(): string { return path.join(COMPANY_ROOT, 'knowledge'); }
17
+ function companyRoot(): string { return COMPANY_ROOT; }
18
18
 
19
19
  /* ─── Helpers ─────────────────────────────────────── */
20
20
 
@@ -61,13 +61,13 @@ function inferCategory(filePath: string, tags: string[]): string {
61
61
 
62
62
  knowledgeRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
63
63
  try {
64
- if (!fs.existsSync(companyRoot)) {
64
+ if (!fs.existsSync(companyRoot())) {
65
65
  res.json([]);
66
66
  return;
67
67
  }
68
68
 
69
69
  const files = glob.sync('**/*.{md,html}', {
70
- cwd: companyRoot,
70
+ cwd: companyRoot(),
71
71
  ignore: [
72
72
  'node_modules/**', '.claude/**', '.obsidian/**', '.tycono/**', '.git/**',
73
73
  '**/node_modules/**',
@@ -82,7 +82,7 @@ knowledgeRouter.get('/', (_req: Request, res: Response, next: NextFunction) => {
82
82
  .sort();
83
83
 
84
84
  const docs = files.map((f) => {
85
- const absPath = path.join(companyRoot, f);
85
+ const absPath = path.join(companyRoot(), f);
86
86
  let raw = '';
87
87
  try { raw = fs.readFileSync(absPath, 'utf-8'); } catch { return null; }
88
88
 
@@ -165,9 +165,9 @@ knowledgeRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
165
165
  // Sanitize filename
166
166
  const safeName = filename.replace(/[^a-zA-Z0-9가-힣_\-. ]/g, '').replace(/\s+/g, '-');
167
167
  const fullName = safeName.endsWith('.md') ? safeName : `${safeName}.md`;
168
- const absPath = path.join(knowledgeDir, fullName);
168
+ const absPath = path.join(knowledgeDir(), fullName);
169
169
 
170
- if (!absPath.startsWith(knowledgeDir)) {
170
+ if (!absPath.startsWith(knowledgeDir())) {
171
171
  res.status(403).json({ error: 'Forbidden' });
172
172
  return;
173
173
  }
@@ -206,8 +206,8 @@ knowledgeRouter.put('/{*path}', (req: Request, res: Response, next: NextFunction
206
206
  return;
207
207
  }
208
208
 
209
- const absPath = path.join(companyRoot, docId);
210
- if (!absPath.startsWith(companyRoot)) {
209
+ const absPath = path.join(companyRoot(), docId);
210
+ if (!absPath.startsWith(companyRoot())) {
211
211
  res.status(403).json({ error: 'Forbidden' });
212
212
  return;
213
213
  }
@@ -248,8 +248,8 @@ knowledgeRouter.delete('/{*path}', (req: Request, res: Response, next: NextFunct
248
248
  return;
249
249
  }
250
250
 
251
- const absPath = path.join(companyRoot, docId);
252
- if (!absPath.startsWith(companyRoot)) {
251
+ const absPath = path.join(companyRoot(), docId);
252
+ if (!absPath.startsWith(companyRoot())) {
253
253
  res.status(403).json({ error: 'Forbidden' });
254
254
  return;
255
255
  }
@@ -277,10 +277,10 @@ knowledgeRouter.get('/{*path}', (req: Request, res: Response, next: NextFunction
277
277
  return;
278
278
  }
279
279
 
280
- const absPath = path.join(knowledgeDir, docId);
280
+ const absPath = path.join(knowledgeDir(), docId);
281
281
 
282
282
  // Security: ensure path stays within knowledgeDir
283
- if (!absPath.startsWith(knowledgeDir)) {
283
+ if (!absPath.startsWith(knowledgeDir())) {
284
284
  res.status(403).json({ error: 'Forbidden' });
285
285
  return;
286
286
  }
@@ -16,6 +16,7 @@ import { AnthropicProvider, type LLMProvider } from '../engine/llm-adapter.js';
16
16
  import { jobManager } from '../services/job-manager.js';
17
17
  import { applyConfig, readConfig, writeConfig } from '../services/company-config.js';
18
18
  import { mergePreferences } from '../services/preferences.js';
19
+ import { setCompanyRoot } from '../services/file-reader.js';
19
20
 
20
21
  export const setupRouter = Router();
21
22
 
@@ -82,28 +83,33 @@ setupRouter.post('/validate-path', (req, res) => {
82
83
  * POST /api/setup/scaffold
83
84
  */
84
85
  setupRouter.post('/scaffold', (req, res) => {
85
- const { companyName, description, apiKey, team, existingProjectPath, knowledgePaths, codeRoot, language } = req.body;
86
+ const { companyName, description, apiKey, team, existingProjectPath, knowledgePaths, codeRoot, language, location } = req.body;
86
87
 
87
88
  if (!companyName || typeof companyName !== 'string') {
88
89
  res.status(400).json({ error: 'companyName is required' });
89
90
  return;
90
91
  }
91
92
 
92
- const baseRoot = process.env.COMPANY_ROOT || process.cwd();
93
-
94
- // Safety: if CWD is a dangerous path (home dir, root, or has too many entries),
95
- // scaffold into a subdirectory named after the company
96
- const dangerousPaths = new Set(['/', os.homedir(), os.tmpdir()]);
97
- const isDangerous = dangerousPaths.has(baseRoot) || baseRoot === '/tmp';
98
- let projectRoot = baseRoot;
99
- if (isDangerous) {
100
- const slug = companyName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'my-company';
101
- projectRoot = path.join(baseRoot, slug);
102
- if (!fs.existsSync(projectRoot)) {
103
- fs.mkdirSync(projectRoot, { recursive: true });
93
+ // Determine project root: explicit location from wizard > fallback to CWD with safety check
94
+ let projectRoot: string;
95
+ if (location && typeof location === 'string') {
96
+ projectRoot = path.resolve(location);
97
+ } else {
98
+ const baseRoot = process.env.COMPANY_ROOT || process.cwd();
99
+ const dangerousPaths = new Set(['/', os.homedir(), os.tmpdir()]);
100
+ const isDangerous = dangerousPaths.has(baseRoot) || baseRoot === '/tmp';
101
+ if (isDangerous) {
102
+ const slug = companyName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'my-company';
103
+ projectRoot = path.join(baseRoot, slug);
104
+ } else {
105
+ projectRoot = baseRoot;
104
106
  }
105
107
  }
106
108
 
109
+ if (!fs.existsSync(projectRoot)) {
110
+ fs.mkdirSync(projectRoot, { recursive: true });
111
+ }
112
+
107
113
  const config: ScaffoldConfig = {
108
114
  companyName,
109
115
  description: description || 'An AI-powered organization',
@@ -117,7 +123,7 @@ setupRouter.post('/scaffold', (req, res) => {
117
123
  try {
118
124
  const created = scaffold(config);
119
125
 
120
- process.env.COMPANY_ROOT = projectRoot;
126
+ setCompanyRoot(projectRoot);
121
127
  // Load config.json written by scaffold and apply to process.env
122
128
  const scaffoldConfig = applyConfig(projectRoot);
123
129
  // Save codeRoot if provided
@@ -212,7 +218,7 @@ setupRouter.post('/connect-akb', (req, res) => {
212
218
  if (match) companyName = match[1].trim();
213
219
  } catch { /* ignore */ }
214
220
 
215
- process.env.COMPANY_ROOT = resolved;
221
+ setCompanyRoot(resolved);
216
222
 
217
223
  // Load existing config.json if present
218
224
  const config = readConfig(resolved);
@@ -24,16 +24,19 @@ export interface ActivityEvent {
24
24
 
25
25
  /* ─── Constants ──────────────────────────── */
26
26
 
27
- const STREAMS_DIR = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
27
+ function streamsDir(): string {
28
+ return path.join(COMPANY_ROOT, 'operations', 'activity-streams');
29
+ }
28
30
 
29
31
  function ensureDir(): void {
30
- if (!fs.existsSync(STREAMS_DIR)) {
31
- fs.mkdirSync(STREAMS_DIR, { recursive: true });
32
+ const dir = streamsDir();
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
32
35
  }
33
36
  }
34
37
 
35
38
  function streamPath(jobId: string): string {
36
- return path.join(STREAMS_DIR, `${jobId}.jsonl`);
39
+ return path.join(streamsDir(), `${jobId}.jsonl`);
37
40
  }
38
41
 
39
42
  /* ─── Subscriber type ────────────────────── */
@@ -149,7 +152,7 @@ export class ActivityStream {
149
152
  /** List all stream files (job IDs) */
150
153
  static listAll(): string[] {
151
154
  ensureDir();
152
- return fs.readdirSync(STREAMS_DIR)
155
+ return fs.readdirSync(streamsDir())
153
156
  .filter(f => f.endsWith('.jsonl'))
154
157
  .map(f => f.replace('.jsonl', ''));
155
158
  }
@@ -2,7 +2,9 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { COMPANY_ROOT } from './file-reader.js';
4
4
 
5
- const ACTIVITY_DIR = path.join(COMPANY_ROOT, 'operations', 'activity');
5
+ function activityDir(): string {
6
+ return path.join(COMPANY_ROOT, 'operations', 'activity');
7
+ }
6
8
 
7
9
  export interface RoleActivity {
8
10
  roleId: string;
@@ -14,12 +16,13 @@ export interface RoleActivity {
14
16
  }
15
17
 
16
18
  function activityPath(roleId: string): string {
17
- return path.join(ACTIVITY_DIR, `${roleId}.json`);
19
+ return path.join(activityDir(), `${roleId}.json`);
18
20
  }
19
21
 
20
22
  function ensureDir(): void {
21
- if (!fs.existsSync(ACTIVITY_DIR)) {
22
- fs.mkdirSync(ACTIVITY_DIR, { recursive: true });
23
+ const dir = activityDir();
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true });
23
26
  }
24
27
  }
25
28
 
@@ -71,10 +74,10 @@ export function getActivity(roleId: string): RoleActivity | null {
71
74
 
72
75
  export function getAllActivities(): RoleActivity[] {
73
76
  ensureDir();
74
- const files = fs.readdirSync(ACTIVITY_DIR).filter(f => f.endsWith('.json'));
77
+ const files = fs.readdirSync(activityDir()).filter(f => f.endsWith('.json'));
75
78
  return files.map(f => {
76
79
  try {
77
- return JSON.parse(fs.readFileSync(path.join(ACTIVITY_DIR, f), 'utf-8'));
80
+ return JSON.parse(fs.readFileSync(path.join(activityDir(), f), 'utf-8'));
78
81
  } catch {
79
82
  return null;
80
83
  }
@@ -14,7 +14,13 @@ function findCompanyRoot(): string {
14
14
  return process.cwd();
15
15
  }
16
16
 
17
- export const COMPANY_ROOT = findCompanyRoot();
17
+ export let COMPANY_ROOT = findCompanyRoot();
18
+
19
+ /** Update COMPANY_ROOT at runtime (e.g. after scaffold picks a new location) */
20
+ export function setCompanyRoot(root: string): void {
21
+ COMPANY_ROOT = root;
22
+ process.env.COMPANY_ROOT = root;
23
+ }
18
24
 
19
25
  function resolve(...segments: string[]): string {
20
26
  return path.resolve(COMPANY_ROOT, ...segments);
@@ -26,16 +26,19 @@ export interface Session {
26
26
 
27
27
  /* ─── Session directory ─────────────────── */
28
28
 
29
- const SESSIONS_DIR = path.join(COMPANY_ROOT, 'operations', 'sessions');
29
+ function sessionsDir(): string {
30
+ return path.join(COMPANY_ROOT, 'operations', 'sessions');
31
+ }
30
32
 
31
33
  function ensureDir(): void {
32
- if (!fs.existsSync(SESSIONS_DIR)) {
33
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
34
+ const dir = sessionsDir();
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
34
37
  }
35
38
  }
36
39
 
37
40
  function sessionPath(id: string): string {
38
- return path.join(SESSIONS_DIR, `${id}.json`);
41
+ return path.join(sessionsDir(), `${id}.json`);
39
42
  }
40
43
 
41
44
  /* ─── Debounced write ───────────────────── */
@@ -69,10 +72,10 @@ const cache = new Map<string, Session>();
69
72
 
70
73
  function loadAll(): void {
71
74
  ensureDir();
72
- const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json'));
75
+ const files = fs.readdirSync(sessionsDir()).filter((f) => f.endsWith('.json'));
73
76
  for (const file of files) {
74
77
  try {
75
- const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, file), 'utf-8')) as Session;
78
+ const data = JSON.parse(fs.readFileSync(path.join(sessionsDir(), file), 'utf-8')) as Session;
76
79
  cache.set(data.id, data);
77
80
  } catch { /* skip corrupted */ }
78
81
  }