@loghead/core 0.1.8 β†’ 0.1.11

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.
@@ -15,7 +15,7 @@ async function startApiServer(db) {
15
15
  app.use((0, cors_1.default)());
16
16
  app.use(express_1.default.json());
17
17
  await auth.initialize();
18
- console.log(chalk_1.default.bold.green(`πŸ’» Server running on:\n`));
18
+ console.log(chalk_1.default.bold.green(`\nπŸ’» MCP server running on:`));
19
19
  console.log(chalk_1.default.green(`http://localhost:${port}`));
20
20
  app.post("/api/ingest", async (req, res) => {
21
21
  try {
package/dist/cli_main.js CHANGED
@@ -12,7 +12,6 @@ const migrate_1 = require("./db/migrate");
12
12
  // import { ensureInfrastructure } from "./utils/startup"; // Might need adjustment
13
13
  const main_1 = require("./ui/main");
14
14
  const auth_1 = require("./services/auth");
15
- const chalk_1 = __importDefault(require("chalk"));
16
15
  const db = new db_1.DbService();
17
16
  const auth = new auth_1.AuthService();
18
17
  async function main() {
@@ -25,13 +24,16 @@ async function main() {
25
24
  console.log("Ensuring database is initialized...");
26
25
  await (0, migrate_1.migrate)(false); // Run migrations silently
27
26
  const token = await auth.getOrCreateMcpToken();
28
- console.log(chalk_1.default.bold.yellow(`\nπŸ”‘ MCP Server Token:`));
29
- console.log(chalk_1.default.dim("\nUse this token for the MCP Server or other admin integrations.\n"));
30
- console.log(chalk_1.default.yellow(`${token}`));
27
+ // Start API Server (this sets up express listen)
31
28
  await (0, server_1.startApiServer)(db);
29
+ // Start TUI (this will clear screen and take over)
30
+ await (0, main_1.startTui)(db, token);
31
+ process.exit(0);
32
32
  })
33
33
  .command("ui", "Start Terminal UI", {}, async () => {
34
- await (0, main_1.startTui)(db);
34
+ const token = await auth.getOrCreateMcpToken();
35
+ await (0, main_1.startTui)(db, token);
36
+ process.exit(0);
35
37
  })
36
38
  .command("projects <cmd> [name]", "Manage projects", (yargs) => {
37
39
  yargs
@@ -63,6 +65,10 @@ async function main() {
63
65
  const s = await db.createStream(argv.project, argv.type, argv.name, config);
64
66
  console.log(`Stream created: ${s.id}`);
65
67
  console.log(`Token: ${s.token}`);
68
+ })
69
+ .command("token <streamId>", "Get token for stream", {}, async (argv) => {
70
+ const token = await auth.createStreamToken(argv.streamId);
71
+ console.log(`Token: ${token}`);
66
72
  });
67
73
  })
68
74
  .demandCommand(1)
package/dist/db/client.js CHANGED
@@ -39,9 +39,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.db = void 0;
40
40
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
41
41
  const sqliteVec = __importStar(require("sqlite-vec"));
42
+ const path_1 = __importDefault(require("path"));
42
43
  const dotenv_1 = __importDefault(require("dotenv"));
43
44
  dotenv_1.default.config();
44
45
  const dbPath = process.env.LOGHEAD_DB_PATH || "loghead.db";
46
+ console.log(`[DB] Using database at: ${path_1.default.resolve(dbPath)}`);
45
47
  const db = new better_sqlite3_1.default(dbPath);
46
48
  exports.db = db;
47
49
  // Load sqlite-vec extension
@@ -49,6 +49,7 @@ class AuthService {
49
49
  if (!this.secretKey)
50
50
  throw new Error("Auth not initialized");
51
51
  try {
52
+ // console.log(`[Auth] Verifying token with secret: ${this.secretKey.substring(0, 10)}...`);
52
53
  const payload = jsonwebtoken_1.default.verify(token, this.secretKey, { issuer: "loghead", algorithms: ["HS512"] });
53
54
  if (!payload.sub)
54
55
  return null;
@@ -56,6 +57,10 @@ class AuthService {
56
57
  }
57
58
  catch (e) {
58
59
  console.error("Token verification failed:", e);
60
+ if (e instanceof Error && e.message === "invalid signature") {
61
+ console.error("[Auth] Secret key mismatch. Ensure the server is using the same database (and secret) as when the token was generated.");
62
+ console.error(`[Auth] Current secret starts with: ${this.secretKey?.substring(0, 8)}...`);
63
+ }
59
64
  return null;
60
65
  }
61
66
  }
@@ -89,7 +89,8 @@ class DbService {
89
89
  return;
90
90
  }
91
91
  const vectorJson = JSON.stringify(embedding);
92
- client_1.db.prepare("INSERT INTO vec_logs(rowid, embedding) VALUES (?, ?)").run(rowid, vectorJson);
92
+ // Explicitly cast rowid to BigInt to ensure better-sqlite3 binds it as an INTEGER
93
+ client_1.db.prepare("INSERT INTO vec_logs(rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), vectorJson);
93
94
  }
94
95
  });
95
96
  try {
package/dist/ui/main.js CHANGED
@@ -4,68 +4,178 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startTui = startTui;
7
+ const auth_1 = require("../services/auth");
7
8
  const inquirer_1 = __importDefault(require("inquirer"));
8
9
  const chalk_1 = __importDefault(require("chalk"));
9
- async function startTui(db) {
10
- console.clear();
11
- console.log(chalk_1.default.bold.blue("Loghead Dashboard\n"));
10
+ let title = `
11
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
12
+ β–’β–’β–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆ
13
+ β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
14
+ β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ
15
+ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ
16
+ β–’β–ˆβ–ˆβ–ˆ β–ˆβ–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–’β–’ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ
17
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
18
+ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–’β–’β–’β–’ β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’
19
+ β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ
20
+ β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
21
+ β–’β–’β–’β–’β–’β–’ `;
22
+ async function startTui(db, token) {
23
+ const port = process.env.PORT || 4567;
24
+ const showHeader = () => {
25
+ console.clear();
26
+ console.log(chalk_1.default.bold(title));
27
+ console.log(chalk_1.default.bold(`\nServer URL :`) + " " + chalk_1.default.dim(`http://localhost:${port}`));
28
+ console.log(chalk_1.default.bold(`MCP Token :`) + " " + chalk_1.default.dim(token) + "\n");
29
+ };
12
30
  while (true) {
31
+ showHeader();
13
32
  const projects = db.listProjects();
14
- if (projects.length === 0) {
15
- console.log("No projects found. Use 'npx loghead projects add <name>' to create one.");
16
- break;
17
- }
18
33
  const projectChoices = projects.map(p => ({ name: p.name, value: p.id }));
34
+ projectChoices.push(new inquirer_1.default.Separator());
35
+ projectChoices.push({ name: chalk_1.default.green("+ Create Project"), value: "create_project" });
19
36
  projectChoices.push({ name: chalk_1.default.red("Exit"), value: "exit" });
20
37
  const { projectId } = await inquirer_1.default.prompt([{
21
38
  type: "list",
22
39
  name: "projectId",
23
- message: "Select a Project",
24
- choices: projectChoices
40
+ message: "Select a project",
41
+ choices: projectChoices,
42
+ pageSize: 10,
43
+ prefix: "πŸ’‘"
25
44
  }]);
26
45
  if (projectId === "exit")
27
46
  break;
47
+ if (projectId === "create_project") {
48
+ const { name } = await inquirer_1.default.prompt([{
49
+ type: "input",
50
+ name: "name",
51
+ message: "Project Name:",
52
+ prefix: "πŸ’‘"
53
+ }]);
54
+ if (name) {
55
+ db.createProject(name);
56
+ }
57
+ continue;
58
+ }
28
59
  // List streams for project
29
60
  while (true) {
30
- console.clear();
61
+ showHeader();
31
62
  const project = projects.find(p => p.id === projectId);
32
63
  console.log(chalk_1.default.bold.blue(`Project: ${project?.name}\n`));
33
64
  const streams = db.listStreams(projectId);
34
- if (streams.length === 0) {
35
- console.log("No streams found.");
36
- }
37
65
  const streamChoices = streams.map(s => ({
38
66
  name: `${s.name} (${s.type})`,
39
67
  value: s.id
40
68
  }));
69
+ streamChoices.push(new inquirer_1.default.Separator());
70
+ streamChoices.push({ name: chalk_1.default.green("+ Create Stream"), value: "create_stream" });
41
71
  streamChoices.push({ name: chalk_1.default.yellow("Back"), value: "back" });
42
72
  const { streamId } = await inquirer_1.default.prompt([{
43
73
  type: "list",
44
74
  name: "streamId",
45
- message: "Select a Stream to view logs",
46
- choices: streamChoices
75
+ message: "Select a stream",
76
+ choices: streamChoices,
77
+ pageSize: 10,
78
+ prefix: "πŸ’‘"
47
79
  }]);
48
80
  if (streamId === "back")
49
81
  break;
50
- // Show logs (simple tail)
51
- console.clear();
52
- const stream = streams.find(s => s.id === streamId);
53
- console.log(chalk_1.default.bold.green(`Logs for ${stream?.name}:\n`));
54
- const logs = db.getRecentLogs(streamId, 20);
55
- if (logs.length === 0) {
56
- console.log("No logs recorded yet.");
82
+ if (streamId === "create_stream") {
83
+ showHeader();
84
+ console.log(chalk_1.default.bold.blue(`Project: ${project?.name}`));
85
+ console.log(chalk_1.default.bold.blue(` └─ Create Stream\n`));
86
+ const { name, type } = await inquirer_1.default.prompt([
87
+ {
88
+ type: "input",
89
+ name: "name",
90
+ message: "Stream name:",
91
+ prefix: "πŸ’‘"
92
+ },
93
+ {
94
+ type: "list",
95
+ name: "type",
96
+ message: "Stream type:",
97
+ choices: ["browser", "terminal", "docker"],
98
+ prefix: "πŸ’‘"
99
+ }
100
+ ]);
101
+ if (name && type) {
102
+ // For now, empty config
103
+ const s = await db.createStream(projectId, type, name, {});
104
+ console.log(chalk_1.default.green(`\nStream created!`));
105
+ console.log(chalk_1.default.bold.yellow(`Token: ${s.token}\n`));
106
+ await inquirer_1.default.prompt([{
107
+ type: "input",
108
+ name: "continue",
109
+ message: "Press enter to continue...",
110
+ prefix: "πŸ’‘"
111
+ }]);
112
+ }
113
+ continue;
57
114
  }
58
- else {
59
- // Reverse to show oldest to newest like a log file
60
- [...logs].reverse().forEach(log => {
61
- console.log(`${chalk_1.default.dim(log.timestamp)} ${log.content}`);
62
- });
115
+ // Stream Actions
116
+ while (true) {
117
+ showHeader();
118
+ const stream = streams.find(s => s.id === streamId);
119
+ console.log(chalk_1.default.bold.blue(`Project: ${project?.name}`));
120
+ console.log(chalk_1.default.bold.blue(` └─ Stream: ${stream?.name} (${stream?.type})\n`));
121
+ const { action } = await inquirer_1.default.prompt([{
122
+ type: "list",
123
+ name: "action",
124
+ message: "Action",
125
+ choices: [
126
+ { name: "View logs", value: "view_logs" },
127
+ { name: "Get token", value: "get_token" },
128
+ { name: "Delete stream", value: "delete_stream" },
129
+ { name: chalk_1.default.yellow("Back"), value: "back" }
130
+ ],
131
+ prefix: "πŸ’‘"
132
+ }]);
133
+ if (action === "back")
134
+ break;
135
+ if (action === "get_token") {
136
+ const auth = new auth_1.AuthService();
137
+ const token = await auth.createStreamToken(streamId);
138
+ console.log(chalk_1.default.green(`\nToken for ${stream?.name}:`));
139
+ console.log(chalk_1.default.bold.yellow(`${token}\n`));
140
+ await inquirer_1.default.prompt([{
141
+ type: "input",
142
+ name: "continue",
143
+ message: "Press enter to continue...",
144
+ prefix: "πŸ’‘"
145
+ }]);
146
+ }
147
+ if (action === "delete_stream") {
148
+ const { confirm } = await inquirer_1.default.prompt([{
149
+ type: "confirm",
150
+ name: "confirm",
151
+ message: `Are you sure you want to delete stream ${stream?.name}?`
152
+ }]);
153
+ if (confirm) {
154
+ db.deleteStream(streamId);
155
+ break; // Go back to stream list
156
+ }
157
+ }
158
+ if (action === "view_logs") {
159
+ console.clear();
160
+ console.log(chalk_1.default.bold.green(`Logs for ${stream?.name}:\n`));
161
+ const logs = db.getRecentLogs(streamId, 20);
162
+ if (logs.length === 0) {
163
+ console.log("No logs recorded yet.");
164
+ }
165
+ else {
166
+ [...logs].reverse().forEach(log => {
167
+ console.log(`${chalk_1.default.dim(log.timestamp)} ${log.content}`);
168
+ });
169
+ }
170
+ console.log("\n");
171
+ await inquirer_1.default.prompt([{
172
+ type: "input",
173
+ name: "return",
174
+ message: "Press enter to return...",
175
+ prefix: "πŸ’‘"
176
+ }]);
177
+ }
63
178
  }
64
- console.log("\nPress Enter to return...");
65
- await new Promise(resolve => {
66
- process.stdin.once('data', () => resolve());
67
- });
68
179
  }
69
- console.clear();
70
180
  }
71
181
  }
package/loghead.db ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loghead/core",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "description": "Core API and Database for Loghead",
5
5
  "repository": {
6
6
  "type": "git",
package/src/api/server.ts CHANGED
@@ -15,7 +15,7 @@ export async function startApiServer(db: DbService) {
15
15
 
16
16
  await auth.initialize();
17
17
 
18
- console.log(chalk.bold.green(`πŸ’» Server running on:\n`));
18
+ console.log(chalk.bold.green(`\nπŸ’» MCP server running on:`));
19
19
  console.log(chalk.green(`http://localhost:${port}`));
20
20
 
21
21
  app.post("/api/ingest", async (req, res) => {
package/src/cli_main.ts CHANGED
@@ -23,14 +23,18 @@ async function main() {
23
23
  await migrate(false); // Run migrations silently
24
24
 
25
25
  const token = await auth.getOrCreateMcpToken();
26
- console.log(chalk.bold.yellow(`\nπŸ”‘ MCP Server Token:`));
27
- console.log(chalk.dim("\nUse this token for the MCP Server or other admin integrations.\n"));
28
- console.log(chalk.yellow(`${token}`));
29
26
 
27
+ // Start API Server (this sets up express listen)
30
28
  await startApiServer(db);
29
+
30
+ // Start TUI (this will clear screen and take over)
31
+ await startTui(db, token);
32
+ process.exit(0);
31
33
  })
32
34
  .command("ui", "Start Terminal UI", {}, async () => {
33
- await startTui(db);
35
+ const token = await auth.getOrCreateMcpToken();
36
+ await startTui(db, token);
37
+ process.exit(0);
34
38
  })
35
39
  .command("projects <cmd> [name]", "Manage projects", (yargs) => {
36
40
  yargs
@@ -62,6 +66,10 @@ async function main() {
62
66
  const s = await db.createStream(argv.project, argv.type as string, argv.name as string, config);
63
67
  console.log(`Stream created: ${s.id}`);
64
68
  console.log(`Token: ${s.token}`);
69
+ })
70
+ .command("token <streamId>", "Get token for stream", {}, async (argv) => {
71
+ const token = await auth.createStreamToken(argv.streamId as string);
72
+ console.log(`Token: ${token}`);
65
73
  });
66
74
  })
67
75
  .demandCommand(1)
package/src/db/client.ts CHANGED
@@ -6,6 +6,7 @@ import dotenv from "dotenv";
6
6
  dotenv.config();
7
7
 
8
8
  const dbPath = process.env.LOGHEAD_DB_PATH || "loghead.db";
9
+ console.log(`[DB] Using database at: ${path.resolve(dbPath)}`);
9
10
 
10
11
  const db = new Database(dbPath);
11
12
 
@@ -58,11 +58,16 @@ export class AuthService {
58
58
  if (!this.secretKey) throw new Error("Auth not initialized");
59
59
 
60
60
  try {
61
+ // console.log(`[Auth] Verifying token with secret: ${this.secretKey.substring(0, 10)}...`);
61
62
  const payload = jwt.verify(token, this.secretKey, { issuer: "loghead", algorithms: ["HS512"] }) as jwt.JwtPayload;
62
63
  if (!payload.sub) return null;
63
64
  return { streamId: payload.sub };
64
65
  } catch (e) {
65
66
  console.error("Token verification failed:", e);
67
+ if (e instanceof Error && e.message === "invalid signature") {
68
+ console.error("[Auth] Secret key mismatch. Ensure the server is using the same database (and secret) as when the token was generated.");
69
+ console.error(`[Auth] Current secret starts with: ${this.secretKey?.substring(0, 8)}...`);
70
+ }
66
71
  return null;
67
72
  }
68
73
  }
@@ -102,7 +102,8 @@ export class DbService {
102
102
  return;
103
103
  }
104
104
  const vectorJson = JSON.stringify(embedding);
105
- (db.prepare("INSERT INTO vec_logs(rowid, embedding) VALUES (?, ?)") as unknown as DbAny).run(rowid, vectorJson);
105
+ // Explicitly cast rowid to BigInt to ensure better-sqlite3 binds it as an INTEGER
106
+ (db.prepare("INSERT INTO vec_logs(rowid, embedding) VALUES (?, ?)") as unknown as DbAny).run(BigInt(rowid), vectorJson);
106
107
  }
107
108
  });
108
109
 
package/src/ui/main.ts CHANGED
@@ -1,78 +1,197 @@
1
1
  import { DbService } from "../services/db";
2
+ import { AuthService } from "../services/auth";
2
3
  import inquirer from "inquirer";
3
4
  import chalk from "chalk";
4
5
 
5
- export async function startTui(db: DbService) {
6
- console.clear();
7
- console.log(chalk.bold.blue("Loghead Dashboard\n"));
6
+ let title = `
7
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
8
+ β–’β–’β–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆ
9
+ β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
10
+ β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ
11
+ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ
12
+ β–’β–ˆβ–ˆβ–ˆ β–ˆβ–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆβ–’β–’β–’ β–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ
13
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
14
+ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–ˆβ–ˆβ–ˆβ–’β–’β–’β–’ β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’ β–’β–’β–’β–’β–’β–’β–’β–’
15
+ β–ˆβ–ˆβ–ˆ β–’β–ˆβ–ˆβ–ˆ
16
+ β–’β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
17
+ β–’β–’β–’β–’β–’β–’ `;
18
+
19
+ export async function startTui(db: DbService, token: string) {
20
+ const port = process.env.PORT || 4567;
21
+ const showHeader = () => {
22
+ console.clear();
23
+ console.log(chalk.bold(title));
24
+ console.log(chalk.bold(`\nServer URL :`) + " " + chalk.dim(`http://localhost:${port}`));
25
+ console.log(chalk.bold(`MCP Token :`) + " " + chalk.dim(token) + "\n");
26
+ };
8
27
 
9
28
  while (true) {
29
+ showHeader();
10
30
  const projects = db.listProjects();
11
31
 
12
- if (projects.length === 0) {
13
- console.log("No projects found. Use 'npx loghead projects add <name>' to create one.");
14
- break;
15
- }
16
-
17
- const projectChoices = projects.map(p => ({ name: p.name, value: p.id }));
32
+ const projectChoices: (inquirer.Separator | { name: string; value: string })[] = projects.map(p => ({ name: p.name, value: p.id }));
33
+ projectChoices.push(new inquirer.Separator());
34
+ projectChoices.push({ name: chalk.green("+ Create Project"), value: "create_project" });
18
35
  projectChoices.push({ name: chalk.red("Exit"), value: "exit" });
19
36
 
20
37
  const { projectId } = await inquirer.prompt([{
21
38
  type: "list",
22
39
  name: "projectId",
23
- message: "Select a Project",
24
- choices: projectChoices
40
+ message: "Select a project",
41
+ choices: projectChoices,
42
+ pageSize: 10,
43
+ prefix: "πŸ’‘"
25
44
  }]);
26
45
 
27
46
  if (projectId === "exit") break;
28
47
 
48
+ if (projectId === "create_project") {
49
+ const { name } = await inquirer.prompt([{
50
+ type: "input",
51
+ name: "name",
52
+ message: "Project Name:",
53
+ prefix: "πŸ’‘"
54
+ }]);
55
+ if (name) {
56
+ db.createProject(name);
57
+ }
58
+ continue;
59
+ }
60
+
29
61
  // List streams for project
30
62
  while (true) {
31
- console.clear();
63
+ showHeader();
32
64
  const project = projects.find(p => p.id === projectId);
33
65
  console.log(chalk.bold.blue(`Project: ${project?.name}\n`));
34
66
 
35
67
  const streams = db.listStreams(projectId);
36
68
 
37
- if (streams.length === 0) {
38
- console.log("No streams found.");
39
- }
40
-
41
- const streamChoices = streams.map(s => ({
69
+ const streamChoices: (inquirer.Separator | { name: string; value: string })[] = streams.map(s => ({
42
70
  name: `${s.name} (${s.type})`,
43
71
  value: s.id
44
72
  }));
73
+ streamChoices.push(new inquirer.Separator());
74
+ streamChoices.push({ name: chalk.green("+ Create Stream"), value: "create_stream" });
45
75
  streamChoices.push({ name: chalk.yellow("Back"), value: "back" });
46
76
 
47
77
  const { streamId } = await inquirer.prompt([{
48
78
  type: "list",
49
79
  name: "streamId",
50
- message: "Select a Stream to view logs",
51
- choices: streamChoices
80
+ message: "Select a stream",
81
+ choices: streamChoices,
82
+ pageSize: 10,
83
+ prefix: "πŸ’‘"
52
84
  }]);
53
85
 
54
86
  if (streamId === "back") break;
55
87
 
56
- // Show logs (simple tail)
57
- console.clear();
58
- const stream = streams.find(s => s.id === streamId);
59
- console.log(chalk.bold.green(`Logs for ${stream?.name}:\n`));
60
-
61
- const logs = db.getRecentLogs(streamId, 20);
62
- if (logs.length === 0) {
63
- console.log("No logs recorded yet.");
64
- } else {
65
- // Reverse to show oldest to newest like a log file
66
- [...logs].reverse().forEach(log => {
67
- console.log(`${chalk.dim(log.timestamp)} ${log.content}`);
68
- });
88
+ if (streamId === "create_stream") {
89
+ showHeader();
90
+ console.log(chalk.bold.blue(`Project: ${project?.name}`));
91
+ console.log(chalk.bold.blue(` └─ Create Stream\n`));
92
+
93
+ const { name, type } = await inquirer.prompt([
94
+ {
95
+ type: "input",
96
+ name: "name",
97
+ message: "Stream name:",
98
+ prefix: "πŸ’‘"
99
+ },
100
+ {
101
+ type: "list",
102
+ name: "type",
103
+ message: "Stream type:",
104
+ choices: ["browser", "terminal", "docker"],
105
+ prefix: "πŸ’‘"
106
+ }
107
+ ]);
108
+
109
+ if (name && type) {
110
+ // For now, empty config
111
+ const s = await db.createStream(projectId, type, name, {});
112
+ console.log(chalk.green(`\nStream created!`));
113
+ console.log(chalk.bold.yellow(`Token: ${s.token}\n`));
114
+
115
+ await inquirer.prompt([{
116
+ type: "input",
117
+ name: "continue",
118
+ message: "Press enter to continue...",
119
+ prefix: "πŸ’‘"
120
+ }]);
121
+ }
122
+ continue;
69
123
  }
70
124
 
71
- console.log("\nPress Enter to return...");
72
- await new Promise<void>(resolve => {
73
- process.stdin.once('data', () => resolve());
74
- });
125
+ // Stream Actions
126
+ while (true) {
127
+ showHeader();
128
+ const stream = streams.find(s => s.id === streamId);
129
+ console.log(chalk.bold.blue(`Project: ${project?.name}`));
130
+ console.log(chalk.bold.blue(` └─ Stream: ${stream?.name} (${stream?.type})\n`));
131
+
132
+ const { action } = await inquirer.prompt([{
133
+ type: "list",
134
+ name: "action",
135
+ message: "Action",
136
+ choices: [
137
+ { name: "View logs", value: "view_logs" },
138
+ { name: "Get token", value: "get_token" },
139
+ { name: "Delete stream", value: "delete_stream" },
140
+ { name: chalk.yellow("Back"), value: "back" }
141
+ ],
142
+ prefix: "πŸ’‘"
143
+ }]);
144
+
145
+ if (action === "back") break;
146
+
147
+ if (action === "get_token") {
148
+ const auth = new AuthService();
149
+ const token = await auth.createStreamToken(streamId);
150
+ console.log(chalk.green(`\nToken for ${stream?.name}:`));
151
+ console.log(chalk.bold.yellow(`${token}\n`));
152
+
153
+ await inquirer.prompt([{
154
+ type: "input",
155
+ name: "continue",
156
+ message: "Press enter to continue...",
157
+ prefix: "πŸ’‘"
158
+ }]);
159
+ }
160
+
161
+ if (action === "delete_stream") {
162
+ const { confirm } = await inquirer.prompt([{
163
+ type: "confirm",
164
+ name: "confirm",
165
+ message: `Are you sure you want to delete stream ${stream?.name}?`
166
+ }]);
167
+ if (confirm) {
168
+ db.deleteStream(streamId);
169
+ break; // Go back to stream list
170
+ }
171
+ }
172
+
173
+ if (action === "view_logs") {
174
+ console.clear();
175
+ console.log(chalk.bold.green(`Logs for ${stream?.name}:\n`));
176
+
177
+ const logs = db.getRecentLogs(streamId, 20);
178
+ if (logs.length === 0) {
179
+ console.log("No logs recorded yet.");
180
+ } else {
181
+ [...logs].reverse().forEach(log => {
182
+ console.log(`${chalk.dim(log.timestamp)} ${log.content}`);
183
+ });
184
+ }
185
+
186
+ console.log("\n");
187
+ await inquirer.prompt([{
188
+ type: "input",
189
+ name: "return",
190
+ message: "Press enter to return...",
191
+ prefix: "πŸ’‘"
192
+ }]);
193
+ }
194
+ }
75
195
  }
76
- console.clear();
77
196
  }
78
197
  }