@tiflis-io/tiflis-code-workstation 0.3.8 → 0.3.10
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/dist/main.js +307 -65
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -94,7 +94,7 @@ var EnvSchema = z.object({
|
|
|
94
94
|
// ─────────────────────────────────────────────────────────────
|
|
95
95
|
// Speech-to-Text (STT) Configuration
|
|
96
96
|
// ─────────────────────────────────────────────────────────────
|
|
97
|
-
STT_PROVIDER: z.enum(["openai", "elevenlabs", "deepgram"]).default("openai"),
|
|
97
|
+
STT_PROVIDER: z.enum(["openai", "elevenlabs", "deepgram", "local"]).default("openai"),
|
|
98
98
|
STT_API_KEY: z.string().optional(),
|
|
99
99
|
STT_MODEL: z.string().default("whisper-1"),
|
|
100
100
|
STT_BASE_URL: z.string().url().optional(),
|
|
@@ -102,7 +102,7 @@ var EnvSchema = z.object({
|
|
|
102
102
|
// ─────────────────────────────────────────────────────────────
|
|
103
103
|
// Text-to-Speech (TTS) Configuration
|
|
104
104
|
// ─────────────────────────────────────────────────────────────
|
|
105
|
-
TTS_PROVIDER: z.enum(["openai", "elevenlabs"]).default("openai"),
|
|
105
|
+
TTS_PROVIDER: z.enum(["openai", "elevenlabs", "local"]).default("openai"),
|
|
106
106
|
TTS_API_KEY: z.string().optional(),
|
|
107
107
|
TTS_MODEL: z.string().default("tts-1"),
|
|
108
108
|
TTS_VOICE: z.string().default("alloy"),
|
|
@@ -247,18 +247,18 @@ function getProtocolVersion() {
|
|
|
247
247
|
return `${PROTOCOL_VERSION.major}.${PROTOCOL_VERSION.minor}.${PROTOCOL_VERSION.patch}`;
|
|
248
248
|
}
|
|
249
249
|
var CONNECTION_TIMING = {
|
|
250
|
-
/** How often to send ping to tunnel (
|
|
251
|
-
PING_INTERVAL_MS:
|
|
252
|
-
/** Max time to wait for pong before considering connection stale (
|
|
253
|
-
PONG_TIMEOUT_MS:
|
|
254
|
-
/** Max time to wait for registration response (
|
|
255
|
-
REGISTRATION_TIMEOUT_MS:
|
|
256
|
-
/** Minimum reconnect delay (
|
|
257
|
-
RECONNECT_DELAY_MIN_MS:
|
|
258
|
-
/** Maximum reconnect delay (
|
|
259
|
-
RECONNECT_DELAY_MAX_MS:
|
|
260
|
-
/** Interval for checking timed-out client connections (
|
|
261
|
-
CLIENT_TIMEOUT_CHECK_INTERVAL_MS:
|
|
250
|
+
/** How often to send ping to tunnel (5 seconds - fast liveness detection) */
|
|
251
|
+
PING_INTERVAL_MS: 5e3,
|
|
252
|
+
/** Max time to wait for pong before considering connection stale (10 seconds) */
|
|
253
|
+
PONG_TIMEOUT_MS: 1e4,
|
|
254
|
+
/** Max time to wait for registration response (10 seconds) */
|
|
255
|
+
REGISTRATION_TIMEOUT_MS: 1e4,
|
|
256
|
+
/** Minimum reconnect delay (500ms - fast first retry) */
|
|
257
|
+
RECONNECT_DELAY_MIN_MS: 500,
|
|
258
|
+
/** Maximum reconnect delay (5 seconds - don't wait too long) */
|
|
259
|
+
RECONNECT_DELAY_MAX_MS: 5e3,
|
|
260
|
+
/** Interval for checking timed-out client connections (5 seconds - faster cleanup) */
|
|
261
|
+
CLIENT_TIMEOUT_CHECK_INTERVAL_MS: 5e3
|
|
262
262
|
};
|
|
263
263
|
var SESSION_CONFIG = {
|
|
264
264
|
/** Maximum number of concurrent agent sessions */
|
|
@@ -1895,13 +1895,37 @@ import { join as join4 } from "path";
|
|
|
1895
1895
|
import { execSync } from "child_process";
|
|
1896
1896
|
var FileSystemWorkspaceDiscovery = class {
|
|
1897
1897
|
workspacesRoot;
|
|
1898
|
+
cacheTtlMs;
|
|
1899
|
+
/** Cache for workspace list */
|
|
1900
|
+
workspacesCache = null;
|
|
1901
|
+
/** Cache for projects by workspace name */
|
|
1902
|
+
projectsCache = /* @__PURE__ */ new Map();
|
|
1898
1903
|
constructor(config2) {
|
|
1899
1904
|
this.workspacesRoot = config2.workspacesRoot;
|
|
1905
|
+
this.cacheTtlMs = config2.cacheTtlMs ?? 3e4;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Checks if a cache entry is still valid.
|
|
1909
|
+
*/
|
|
1910
|
+
isCacheValid(entry) {
|
|
1911
|
+
if (!entry) return false;
|
|
1912
|
+
return Date.now() - entry.timestamp < this.cacheTtlMs;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Invalidates all caches. Call when workspace structure changes.
|
|
1916
|
+
*/
|
|
1917
|
+
invalidateCache() {
|
|
1918
|
+
this.workspacesCache = null;
|
|
1919
|
+
this.projectsCache.clear();
|
|
1900
1920
|
}
|
|
1901
1921
|
/**
|
|
1902
1922
|
* Lists all workspaces in the workspaces root.
|
|
1923
|
+
* Results are cached for faster subsequent calls.
|
|
1903
1924
|
*/
|
|
1904
1925
|
async listWorkspaces() {
|
|
1926
|
+
if (this.isCacheValid(this.workspacesCache)) {
|
|
1927
|
+
return this.workspacesCache.data;
|
|
1928
|
+
}
|
|
1905
1929
|
const entries = await readdir(this.workspacesRoot, { withFileTypes: true });
|
|
1906
1930
|
const workspaces = [];
|
|
1907
1931
|
for (const entry of entries) {
|
|
@@ -1915,12 +1939,22 @@ var FileSystemWorkspaceDiscovery = class {
|
|
|
1915
1939
|
});
|
|
1916
1940
|
}
|
|
1917
1941
|
}
|
|
1918
|
-
|
|
1942
|
+
const result = workspaces.sort((a, b) => a.name.localeCompare(b.name));
|
|
1943
|
+
this.workspacesCache = {
|
|
1944
|
+
data: result,
|
|
1945
|
+
timestamp: Date.now()
|
|
1946
|
+
};
|
|
1947
|
+
return result;
|
|
1919
1948
|
}
|
|
1920
1949
|
/**
|
|
1921
1950
|
* Lists all projects in a workspace.
|
|
1951
|
+
* Results are cached for faster subsequent calls.
|
|
1922
1952
|
*/
|
|
1923
1953
|
async listProjects(workspace) {
|
|
1954
|
+
const cached = this.projectsCache.get(workspace);
|
|
1955
|
+
if (this.isCacheValid(cached)) {
|
|
1956
|
+
return cached.data;
|
|
1957
|
+
}
|
|
1924
1958
|
const workspacePath = join4(this.workspacesRoot, workspace);
|
|
1925
1959
|
if (!await this.pathExists(workspacePath)) {
|
|
1926
1960
|
return [];
|
|
@@ -1949,7 +1983,12 @@ var FileSystemWorkspaceDiscovery = class {
|
|
|
1949
1983
|
});
|
|
1950
1984
|
}
|
|
1951
1985
|
}
|
|
1952
|
-
|
|
1986
|
+
const result = projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
1987
|
+
this.projectsCache.set(workspace, {
|
|
1988
|
+
data: result,
|
|
1989
|
+
timestamp: Date.now()
|
|
1990
|
+
});
|
|
1991
|
+
return result;
|
|
1953
1992
|
}
|
|
1954
1993
|
/**
|
|
1955
1994
|
* Gets information about a specific project.
|
|
@@ -3141,6 +3180,12 @@ var HeadlessAgentExecutor = class extends EventEmitter {
|
|
|
3141
3180
|
|
|
3142
3181
|
// src/domain/value-objects/content-block.ts
|
|
3143
3182
|
import { randomUUID } from "crypto";
|
|
3183
|
+
function isTextBlock(block) {
|
|
3184
|
+
return block.block_type === "text";
|
|
3185
|
+
}
|
|
3186
|
+
function isToolBlock(block) {
|
|
3187
|
+
return block.block_type === "tool";
|
|
3188
|
+
}
|
|
3144
3189
|
function createTextBlock(content) {
|
|
3145
3190
|
return {
|
|
3146
3191
|
id: randomUUID(),
|
|
@@ -3219,6 +3264,103 @@ function createVoiceOutputBlock(text2, options) {
|
|
|
3219
3264
|
}
|
|
3220
3265
|
};
|
|
3221
3266
|
}
|
|
3267
|
+
function mergeToolBlocks(blocks) {
|
|
3268
|
+
const toolBlocksByUseId = /* @__PURE__ */ new Map();
|
|
3269
|
+
for (const block of blocks) {
|
|
3270
|
+
if (isToolBlock(block) && block.metadata.tool_use_id) {
|
|
3271
|
+
const toolUseId = block.metadata.tool_use_id;
|
|
3272
|
+
const existing = toolBlocksByUseId.get(toolUseId);
|
|
3273
|
+
if (existing) {
|
|
3274
|
+
const mergedStatus = getMergedToolStatus(
|
|
3275
|
+
existing.metadata.tool_status,
|
|
3276
|
+
block.metadata.tool_status
|
|
3277
|
+
);
|
|
3278
|
+
const mergedBlock = {
|
|
3279
|
+
id: existing.id,
|
|
3280
|
+
// Keep original ID
|
|
3281
|
+
block_type: "tool",
|
|
3282
|
+
content: block.metadata.tool_name || existing.content,
|
|
3283
|
+
metadata: {
|
|
3284
|
+
tool_name: block.metadata.tool_name || existing.metadata.tool_name,
|
|
3285
|
+
tool_use_id: toolUseId,
|
|
3286
|
+
tool_input: block.metadata.tool_input || existing.metadata.tool_input,
|
|
3287
|
+
tool_output: block.metadata.tool_output || existing.metadata.tool_output,
|
|
3288
|
+
tool_status: mergedStatus
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
toolBlocksByUseId.set(toolUseId, mergedBlock);
|
|
3292
|
+
} else {
|
|
3293
|
+
toolBlocksByUseId.set(toolUseId, block);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
const seenToolUseIds = /* @__PURE__ */ new Set();
|
|
3298
|
+
const result = [];
|
|
3299
|
+
for (const block of blocks) {
|
|
3300
|
+
if (isToolBlock(block) && block.metadata.tool_use_id) {
|
|
3301
|
+
const toolUseId = block.metadata.tool_use_id;
|
|
3302
|
+
if (!seenToolUseIds.has(toolUseId)) {
|
|
3303
|
+
seenToolUseIds.add(toolUseId);
|
|
3304
|
+
const mergedBlock = toolBlocksByUseId.get(toolUseId);
|
|
3305
|
+
if (mergedBlock) {
|
|
3306
|
+
result.push(mergedBlock);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
} else {
|
|
3310
|
+
result.push(block);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return result;
|
|
3314
|
+
}
|
|
3315
|
+
function getMergedToolStatus(status1, status2) {
|
|
3316
|
+
if (status1 === "completed" || status2 === "completed") {
|
|
3317
|
+
return "completed";
|
|
3318
|
+
}
|
|
3319
|
+
if (status1 === "failed" || status2 === "failed") {
|
|
3320
|
+
return "failed";
|
|
3321
|
+
}
|
|
3322
|
+
return "running";
|
|
3323
|
+
}
|
|
3324
|
+
function accumulateBlocks(existing, newBlocks) {
|
|
3325
|
+
for (const block of newBlocks) {
|
|
3326
|
+
if (isToolBlock(block) && block.metadata.tool_use_id) {
|
|
3327
|
+
const toolUseId = block.metadata.tool_use_id;
|
|
3328
|
+
const existingIndex = existing.findIndex(
|
|
3329
|
+
(b) => isToolBlock(b) && b.metadata.tool_use_id === toolUseId
|
|
3330
|
+
);
|
|
3331
|
+
if (existingIndex >= 0) {
|
|
3332
|
+
const existingBlock = existing[existingIndex];
|
|
3333
|
+
const mergedStatus = getMergedToolStatus(
|
|
3334
|
+
existingBlock.metadata.tool_status,
|
|
3335
|
+
block.metadata.tool_status
|
|
3336
|
+
);
|
|
3337
|
+
existing[existingIndex] = {
|
|
3338
|
+
id: existingBlock.id,
|
|
3339
|
+
block_type: "tool",
|
|
3340
|
+
content: block.metadata.tool_name || existingBlock.content,
|
|
3341
|
+
metadata: {
|
|
3342
|
+
tool_name: block.metadata.tool_name || existingBlock.metadata.tool_name,
|
|
3343
|
+
tool_use_id: toolUseId,
|
|
3344
|
+
tool_input: block.metadata.tool_input || existingBlock.metadata.tool_input,
|
|
3345
|
+
tool_output: block.metadata.tool_output || existingBlock.metadata.tool_output,
|
|
3346
|
+
tool_status: mergedStatus
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
} else {
|
|
3350
|
+
existing.push(block);
|
|
3351
|
+
}
|
|
3352
|
+
} else if (isTextBlock(block)) {
|
|
3353
|
+
const lastBlock = existing[existing.length - 1];
|
|
3354
|
+
if (lastBlock && isTextBlock(lastBlock)) {
|
|
3355
|
+
existing[existing.length - 1] = block;
|
|
3356
|
+
} else {
|
|
3357
|
+
existing.push(block);
|
|
3358
|
+
}
|
|
3359
|
+
} else {
|
|
3360
|
+
existing.push(block);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3222
3364
|
|
|
3223
3365
|
// src/infrastructure/agents/agent-output-parser.ts
|
|
3224
3366
|
var AgentOutputParser = class _AgentOutputParser {
|
|
@@ -5598,7 +5740,7 @@ var ChatHistoryService = class _ChatHistoryService {
|
|
|
5598
5740
|
* Gets supervisor chat history (global, shared across all devices).
|
|
5599
5741
|
* Returns messages sorted by sequence (oldest first) for chronological display.
|
|
5600
5742
|
*/
|
|
5601
|
-
getSupervisorHistory(limit =
|
|
5743
|
+
getSupervisorHistory(limit = 20) {
|
|
5602
5744
|
const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
|
|
5603
5745
|
const rows = this.messageRepo.getBySession(sessionId, limit);
|
|
5604
5746
|
return rows.reverse().map((row) => {
|
|
@@ -5663,7 +5805,7 @@ var ChatHistoryService = class _ChatHistoryService {
|
|
|
5663
5805
|
* Gets agent session chat history.
|
|
5664
5806
|
* Returns messages sorted chronologically (oldest first).
|
|
5665
5807
|
*/
|
|
5666
|
-
getAgentHistory(sessionId, limit =
|
|
5808
|
+
getAgentHistory(sessionId, limit = 20) {
|
|
5667
5809
|
const rows = this.messageRepo.getBySession(sessionId, limit);
|
|
5668
5810
|
return rows.reverse().map((row) => {
|
|
5669
5811
|
let contentBlocks;
|
|
@@ -5721,7 +5863,7 @@ var ChatHistoryService = class _ChatHistoryService {
|
|
|
5721
5863
|
* @param sessionIds - List of active agent session IDs
|
|
5722
5864
|
* @param limit - Max messages per session
|
|
5723
5865
|
*/
|
|
5724
|
-
getAllAgentHistories(sessionIds, limit =
|
|
5866
|
+
getAllAgentHistories(sessionIds, limit = 20) {
|
|
5725
5867
|
const histories = /* @__PURE__ */ new Map();
|
|
5726
5868
|
for (const sessionId of sessionIds) {
|
|
5727
5869
|
const history = this.getAgentHistory(sessionId, limit);
|
|
@@ -7346,12 +7488,7 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7346
7488
|
if (this.isExecuting && !this.isCancelled) {
|
|
7347
7489
|
const textBlock = createTextBlock(content);
|
|
7348
7490
|
this.emit("blocks", deviceId, [textBlock], false);
|
|
7349
|
-
|
|
7350
|
-
if (lastTextIndex >= 0) {
|
|
7351
|
-
allBlocks[lastTextIndex] = textBlock;
|
|
7352
|
-
} else {
|
|
7353
|
-
allBlocks.push(textBlock);
|
|
7354
|
-
}
|
|
7491
|
+
accumulateBlocks(allBlocks, [textBlock]);
|
|
7355
7492
|
}
|
|
7356
7493
|
} else if (Array.isArray(content)) {
|
|
7357
7494
|
for (const item of content) {
|
|
@@ -7359,7 +7496,7 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7359
7496
|
const block = this.parseContentItem(item);
|
|
7360
7497
|
if (block) {
|
|
7361
7498
|
this.emit("blocks", deviceId, [block], false);
|
|
7362
|
-
allBlocks
|
|
7499
|
+
accumulateBlocks(allBlocks, [block]);
|
|
7363
7500
|
}
|
|
7364
7501
|
}
|
|
7365
7502
|
}
|
|
@@ -7368,21 +7505,24 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7368
7505
|
if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
|
|
7369
7506
|
const toolContent = lastMessage.content;
|
|
7370
7507
|
const toolName = lastMessage.name ?? "tool";
|
|
7508
|
+
const toolCallId = lastMessage.tool_call_id;
|
|
7371
7509
|
const toolBlock = createToolBlock(
|
|
7372
7510
|
toolName,
|
|
7373
7511
|
"completed",
|
|
7374
7512
|
void 0,
|
|
7375
|
-
typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent)
|
|
7513
|
+
typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
|
|
7514
|
+
toolCallId
|
|
7376
7515
|
);
|
|
7377
7516
|
this.emit("blocks", deviceId, [toolBlock], false);
|
|
7378
|
-
allBlocks
|
|
7517
|
+
accumulateBlocks(allBlocks, [toolBlock]);
|
|
7379
7518
|
}
|
|
7380
7519
|
}
|
|
7381
7520
|
if (this.isExecuting && !this.isCancelled) {
|
|
7382
7521
|
this.addToHistory("user", command);
|
|
7383
7522
|
this.addToHistory("assistant", finalOutput);
|
|
7523
|
+
const finalBlocks = mergeToolBlocks(allBlocks);
|
|
7384
7524
|
const completionBlock = createStatusBlock("Complete");
|
|
7385
|
-
this.emit("blocks", deviceId, [completionBlock], true, finalOutput,
|
|
7525
|
+
this.emit("blocks", deviceId, [completionBlock], true, finalOutput, finalBlocks);
|
|
7386
7526
|
this.logger.debug({ output: finalOutput.slice(0, 200) }, "Supervisor streaming completed");
|
|
7387
7527
|
} else {
|
|
7388
7528
|
this.logger.info({ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Supervisor streaming ended due to cancellation");
|
|
@@ -7412,7 +7552,8 @@ var SupervisorAgent = class extends EventEmitter4 {
|
|
|
7412
7552
|
if (type === "tool_use") {
|
|
7413
7553
|
const name = typeof item.name === "string" ? item.name : "tool";
|
|
7414
7554
|
const input = item.input;
|
|
7415
|
-
|
|
7555
|
+
const toolUseId = typeof item.id === "string" ? item.id : void 0;
|
|
7556
|
+
return createToolBlock(name, "running", input, void 0, toolUseId);
|
|
7416
7557
|
}
|
|
7417
7558
|
return null;
|
|
7418
7559
|
}
|
|
@@ -7677,6 +7818,9 @@ Creating worktrees with \`create_worktree\`:
|
|
|
7677
7818
|
- When creating sessions, confirm the workspace and project first
|
|
7678
7819
|
- For ambiguous requests, ask clarifying questions
|
|
7679
7820
|
- Format responses for terminal display (avoid markdown links)
|
|
7821
|
+
- NEVER use tables - they display poorly on mobile devices
|
|
7822
|
+
- ALWAYS use bullet lists or numbered lists instead of tables
|
|
7823
|
+
- Keep list items short and scannable for mobile reading
|
|
7680
7824
|
- ALWAYS prioritize safety - check before deleting/merging`;
|
|
7681
7825
|
return [new HumanMessage(`[System Instructions]
|
|
7682
7826
|
${systemPrompt}
|
|
@@ -8235,7 +8379,18 @@ var STTService = class {
|
|
|
8235
8379
|
this.logger.debug({ audioSize: audioBuffer.length, format }, "Transcribing audio");
|
|
8236
8380
|
const startTime = Date.now();
|
|
8237
8381
|
try {
|
|
8238
|
-
|
|
8382
|
+
let result;
|
|
8383
|
+
switch (this.config.provider) {
|
|
8384
|
+
case "openai":
|
|
8385
|
+
case "local":
|
|
8386
|
+
result = await this.transcribeOpenAI(audioBuffer, format, signal);
|
|
8387
|
+
break;
|
|
8388
|
+
case "elevenlabs":
|
|
8389
|
+
result = await this.transcribeElevenLabs(audioBuffer, format, signal);
|
|
8390
|
+
break;
|
|
8391
|
+
default:
|
|
8392
|
+
throw new Error(`Unsupported STT provider: ${this.config.provider}`);
|
|
8393
|
+
}
|
|
8239
8394
|
const elapsed = Date.now() - startTime;
|
|
8240
8395
|
this.logger.info(
|
|
8241
8396
|
{
|
|
@@ -8347,6 +8502,9 @@ var STTService = class {
|
|
|
8347
8502
|
* Check if the service is configured and ready.
|
|
8348
8503
|
*/
|
|
8349
8504
|
isConfigured() {
|
|
8505
|
+
if (this.config.provider === "local") {
|
|
8506
|
+
return Boolean(this.config.baseUrl && this.config.model);
|
|
8507
|
+
}
|
|
8350
8508
|
return Boolean(this.config.apiKey && this.config.model);
|
|
8351
8509
|
}
|
|
8352
8510
|
/**
|
|
@@ -8363,20 +8521,28 @@ var STTService = class {
|
|
|
8363
8521
|
function createSTTService(env, logger) {
|
|
8364
8522
|
const provider = (env.STT_PROVIDER ?? "openai").toLowerCase();
|
|
8365
8523
|
const apiKey = env.STT_API_KEY;
|
|
8366
|
-
|
|
8524
|
+
const baseUrl = env.STT_BASE_URL;
|
|
8525
|
+
if (provider === "local") {
|
|
8526
|
+
if (!baseUrl) {
|
|
8527
|
+
logger.warn("STT_BASE_URL not configured for local provider, STT service disabled");
|
|
8528
|
+
return null;
|
|
8529
|
+
}
|
|
8530
|
+
} else if (!apiKey) {
|
|
8367
8531
|
logger.warn("STT_API_KEY not configured, STT service disabled");
|
|
8368
8532
|
return null;
|
|
8369
8533
|
}
|
|
8370
8534
|
const defaults = {
|
|
8371
8535
|
openai: { model: "whisper-1" },
|
|
8372
|
-
elevenlabs: { model: "scribe_v1" }
|
|
8536
|
+
elevenlabs: { model: "scribe_v1" },
|
|
8537
|
+
deepgram: { model: "nova-2" },
|
|
8538
|
+
local: { model: "large-v3" }
|
|
8373
8539
|
};
|
|
8374
8540
|
const config2 = {
|
|
8375
8541
|
provider,
|
|
8376
|
-
apiKey,
|
|
8542
|
+
apiKey: apiKey ?? "",
|
|
8377
8543
|
model: env.STT_MODEL ?? defaults[provider].model,
|
|
8378
8544
|
language: env.STT_LANGUAGE,
|
|
8379
|
-
baseUrl
|
|
8545
|
+
baseUrl
|
|
8380
8546
|
};
|
|
8381
8547
|
return new STTService(config2, logger);
|
|
8382
8548
|
}
|
|
@@ -8396,7 +8562,18 @@ var TTSService = class {
|
|
|
8396
8562
|
this.logger.debug({ textLength: text2.length }, "Synthesizing speech");
|
|
8397
8563
|
const startTime = Date.now();
|
|
8398
8564
|
try {
|
|
8399
|
-
|
|
8565
|
+
let result;
|
|
8566
|
+
switch (this.config.provider) {
|
|
8567
|
+
case "openai":
|
|
8568
|
+
case "local":
|
|
8569
|
+
result = await this.synthesizeOpenAI(text2);
|
|
8570
|
+
break;
|
|
8571
|
+
case "elevenlabs":
|
|
8572
|
+
result = await this.synthesizeElevenLabs(text2);
|
|
8573
|
+
break;
|
|
8574
|
+
default:
|
|
8575
|
+
throw new Error(`Unsupported TTS provider: ${this.config.provider}`);
|
|
8576
|
+
}
|
|
8400
8577
|
const elapsed = Date.now() - startTime;
|
|
8401
8578
|
this.logger.info(
|
|
8402
8579
|
{ textLength: text2.length, audioSize: result.audio.length, elapsedMs: elapsed },
|
|
@@ -8473,6 +8650,9 @@ var TTSService = class {
|
|
|
8473
8650
|
* Check if the service is configured and ready.
|
|
8474
8651
|
*/
|
|
8475
8652
|
isConfigured() {
|
|
8653
|
+
if (this.config.provider === "local") {
|
|
8654
|
+
return Boolean(this.config.baseUrl && this.config.voice);
|
|
8655
|
+
}
|
|
8476
8656
|
return Boolean(this.config.apiKey && this.config.model && this.config.voice);
|
|
8477
8657
|
}
|
|
8478
8658
|
/**
|
|
@@ -8489,20 +8669,27 @@ var TTSService = class {
|
|
|
8489
8669
|
function createTTSService(env, logger) {
|
|
8490
8670
|
const provider = (env.TTS_PROVIDER ?? "openai").toLowerCase();
|
|
8491
8671
|
const apiKey = env.TTS_API_KEY;
|
|
8492
|
-
|
|
8672
|
+
const baseUrl = env.TTS_BASE_URL;
|
|
8673
|
+
if (provider === "local") {
|
|
8674
|
+
if (!baseUrl) {
|
|
8675
|
+
logger.warn("TTS_BASE_URL not configured for local provider, TTS service disabled");
|
|
8676
|
+
return null;
|
|
8677
|
+
}
|
|
8678
|
+
} else if (!apiKey) {
|
|
8493
8679
|
logger.warn("TTS_API_KEY not configured, TTS service disabled");
|
|
8494
8680
|
return null;
|
|
8495
8681
|
}
|
|
8496
8682
|
const defaults = {
|
|
8497
8683
|
openai: { model: "tts-1", voice: "alloy" },
|
|
8498
|
-
elevenlabs: { model: "eleven_flash_v2_5", voice: "21m00Tcm4TlvDq8ikWAM" }
|
|
8684
|
+
elevenlabs: { model: "eleven_flash_v2_5", voice: "21m00Tcm4TlvDq8ikWAM" },
|
|
8685
|
+
local: { model: "kokoro", voice: "af_heart" }
|
|
8499
8686
|
};
|
|
8500
8687
|
const config2 = {
|
|
8501
8688
|
provider,
|
|
8502
|
-
apiKey,
|
|
8689
|
+
apiKey: apiKey ?? "",
|
|
8503
8690
|
model: env.TTS_MODEL ?? defaults[provider].model,
|
|
8504
8691
|
voice: env.TTS_VOICE ?? defaults[provider].voice,
|
|
8505
|
-
baseUrl
|
|
8692
|
+
baseUrl
|
|
8506
8693
|
};
|
|
8507
8694
|
return new TTSService(config2, logger);
|
|
8508
8695
|
}
|
|
@@ -8585,6 +8772,8 @@ var SummarizationService = class {
|
|
|
8585
8772
|
buildSystemPrompt() {
|
|
8586
8773
|
return `You are a concise summarizer for voice output. Summarize AI assistant responses into 1-${this.maxSentences} SHORT sentences.
|
|
8587
8774
|
|
|
8775
|
+
CRITICAL: ALWAYS OUTPUT IN ENGLISH. Translate any non-English content to English.
|
|
8776
|
+
|
|
8588
8777
|
STRICT RULES:
|
|
8589
8778
|
- Maximum 20 words total (hard limit)
|
|
8590
8779
|
- Prefer 1 sentence when possible, 2 only if essential
|
|
@@ -8592,6 +8781,7 @@ STRICT RULES:
|
|
|
8592
8781
|
- Natural spoken language only
|
|
8593
8782
|
- Never use markdown, bullets, or formatting
|
|
8594
8783
|
- Never start with "I" - use passive voice or action verbs
|
|
8784
|
+
- ALWAYS translate to English regardless of input language
|
|
8595
8785
|
|
|
8596
8786
|
ABSOLUTELY FORBIDDEN (never include these in output):
|
|
8597
8787
|
- Session IDs or any alphanumeric identifiers (e.g., "session-abc123", "id: 7f3a2b", UUIDs)
|
|
@@ -8599,6 +8789,7 @@ ABSOLUTELY FORBIDDEN (never include these in output):
|
|
|
8599
8789
|
- Any string that looks like a technical identifier, hash, or token
|
|
8600
8790
|
- Code snippets, variable names, or technical jargon
|
|
8601
8791
|
- Long lists or enumerations
|
|
8792
|
+
- Non-English output (always translate to English)
|
|
8602
8793
|
|
|
8603
8794
|
If the original text contains session IDs or paths, OMIT them entirely. Just describe what happened.
|
|
8604
8795
|
|
|
@@ -8611,7 +8802,9 @@ GOOD examples:
|
|
|
8611
8802
|
BAD examples (never do this):
|
|
8612
8803
|
- "Created session abc-123-def in /Users/roman/work/project" \u274C
|
|
8613
8804
|
- "Session ID is 7f3a2b1c" \u274C
|
|
8614
|
-
- "Working in /home/user/documents/code" \u274C
|
|
8805
|
+
- "Working in /home/user/documents/code" \u274C
|
|
8806
|
+
- "\u0421\u043E\u0437\u0434\u0430\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F Claude." \u274C (non-English)
|
|
8807
|
+
- "Sesi\xF3n creada exitosamente." \u274C (non-English)`;
|
|
8615
8808
|
}
|
|
8616
8809
|
/**
|
|
8617
8810
|
* Builds the user prompt with the text to summarize and optional context.
|
|
@@ -9113,22 +9306,29 @@ async function bootstrap() {
|
|
|
9113
9306
|
).map((s) => s.session_id);
|
|
9114
9307
|
const agentHistoriesMap = chatHistoryService.getAllAgentHistories(agentSessionIds);
|
|
9115
9308
|
const agentHistories = {};
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
msg.
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9309
|
+
const historyEntries = Array.from(agentHistoriesMap.entries());
|
|
9310
|
+
const processedHistories = await Promise.all(
|
|
9311
|
+
historyEntries.map(async ([sessionId, history]) => {
|
|
9312
|
+
const enrichedHistory = await Promise.all(
|
|
9313
|
+
history.map(async (msg) => ({
|
|
9314
|
+
sequence: msg.sequence,
|
|
9315
|
+
role: msg.role,
|
|
9316
|
+
content: msg.content,
|
|
9317
|
+
content_blocks: await chatHistoryService.enrichBlocksWithAudio(
|
|
9318
|
+
msg.contentBlocks,
|
|
9319
|
+
msg.audioOutputPath,
|
|
9320
|
+
msg.audioInputPath,
|
|
9321
|
+
false
|
|
9322
|
+
// Don't include audio in sync.state
|
|
9323
|
+
),
|
|
9324
|
+
createdAt: msg.createdAt.toISOString()
|
|
9325
|
+
}))
|
|
9326
|
+
);
|
|
9327
|
+
return { sessionId, history: enrichedHistory };
|
|
9328
|
+
})
|
|
9329
|
+
);
|
|
9330
|
+
for (const { sessionId, history } of processedHistories) {
|
|
9331
|
+
agentHistories[sessionId] = history;
|
|
9132
9332
|
}
|
|
9133
9333
|
const availableAgentsMap = getAvailableAgents();
|
|
9134
9334
|
const availableAgents = Array.from(availableAgentsMap.values()).map(
|
|
@@ -9262,6 +9462,21 @@ async function bootstrap() {
|
|
|
9262
9462
|
}
|
|
9263
9463
|
let commandText;
|
|
9264
9464
|
const messageId = commandMessage.payload.message_id;
|
|
9465
|
+
if (messageBroadcaster) {
|
|
9466
|
+
const ackMessageId = messageId || commandMessage.id;
|
|
9467
|
+
const ackMessage = {
|
|
9468
|
+
type: "message.ack",
|
|
9469
|
+
payload: {
|
|
9470
|
+
message_id: ackMessageId,
|
|
9471
|
+
status: "received"
|
|
9472
|
+
}
|
|
9473
|
+
};
|
|
9474
|
+
messageBroadcaster.sendToClient(deviceId, JSON.stringify(ackMessage));
|
|
9475
|
+
logger.debug(
|
|
9476
|
+
{ messageId: ackMessageId, deviceId },
|
|
9477
|
+
"Sent message.ack for supervisor.command"
|
|
9478
|
+
);
|
|
9479
|
+
}
|
|
9265
9480
|
supervisorAgent.resetCancellationState();
|
|
9266
9481
|
let abortController;
|
|
9267
9482
|
if (commandMessage.payload.audio) {
|
|
@@ -9734,6 +9949,22 @@ async function bootstrap() {
|
|
|
9734
9949
|
const deviceId = execMessage.device_id;
|
|
9735
9950
|
const sessionId = execMessage.session_id;
|
|
9736
9951
|
const messageId = execMessage.payload.message_id;
|
|
9952
|
+
if (deviceId && messageBroadcaster) {
|
|
9953
|
+
const ackMessageId = messageId || execMessage.id;
|
|
9954
|
+
const ackMessage = {
|
|
9955
|
+
type: "message.ack",
|
|
9956
|
+
payload: {
|
|
9957
|
+
message_id: ackMessageId,
|
|
9958
|
+
session_id: sessionId,
|
|
9959
|
+
status: "received"
|
|
9960
|
+
}
|
|
9961
|
+
};
|
|
9962
|
+
messageBroadcaster.sendToClient(deviceId, JSON.stringify(ackMessage));
|
|
9963
|
+
logger.debug(
|
|
9964
|
+
{ messageId: ackMessageId, sessionId, deviceId },
|
|
9965
|
+
"Sent message.ack for session.execute"
|
|
9966
|
+
);
|
|
9967
|
+
}
|
|
9737
9968
|
cancelledDuringTranscription.delete(sessionId);
|
|
9738
9969
|
if (execMessage.payload.audio) {
|
|
9739
9970
|
logger.info(
|
|
@@ -10496,7 +10727,7 @@ async function bootstrap() {
|
|
|
10496
10727
|
if (persistableBlocks.length > 0) {
|
|
10497
10728
|
const accumulated = agentMessageAccumulator.get(sessionId) ?? [];
|
|
10498
10729
|
const wasEmpty = accumulated.length === 0;
|
|
10499
|
-
accumulated
|
|
10730
|
+
accumulateBlocks(accumulated, persistableBlocks);
|
|
10500
10731
|
agentMessageAccumulator.set(sessionId, accumulated);
|
|
10501
10732
|
if (wasEmpty) {
|
|
10502
10733
|
logger.debug(
|
|
@@ -10551,7 +10782,8 @@ async function bootstrap() {
|
|
|
10551
10782
|
}
|
|
10552
10783
|
}
|
|
10553
10784
|
const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
|
|
10554
|
-
const
|
|
10785
|
+
const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
|
|
10786
|
+
const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
|
|
10555
10787
|
const outputEvent = {
|
|
10556
10788
|
type: "session.output",
|
|
10557
10789
|
session_id: sessionId,
|
|
@@ -10559,8 +10791,8 @@ async function bootstrap() {
|
|
|
10559
10791
|
content_type: "agent",
|
|
10560
10792
|
content: fullAccumulatedText,
|
|
10561
10793
|
// Full accumulated text for backward compat
|
|
10562
|
-
content_blocks:
|
|
10563
|
-
// Full accumulated blocks for rich UI
|
|
10794
|
+
content_blocks: mergedBlocks,
|
|
10795
|
+
// Full accumulated blocks for rich UI (merged)
|
|
10564
10796
|
timestamp: Date.now(),
|
|
10565
10797
|
is_complete: isComplete
|
|
10566
10798
|
}
|
|
@@ -10644,10 +10876,11 @@ async function bootstrap() {
|
|
|
10644
10876
|
}
|
|
10645
10877
|
}
|
|
10646
10878
|
);
|
|
10879
|
+
let supervisorBlockAccumulator = [];
|
|
10647
10880
|
supervisorAgent.on(
|
|
10648
10881
|
"blocks",
|
|
10649
10882
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
10650
|
-
async (deviceId, blocks, isComplete, finalOutput,
|
|
10883
|
+
async (deviceId, blocks, isComplete, finalOutput, _allBlocks) => {
|
|
10651
10884
|
const wasCancelled = supervisorAgent.wasCancelled();
|
|
10652
10885
|
logger.debug(
|
|
10653
10886
|
{ deviceId, blockCount: blocks.length, isComplete, wasCancelled },
|
|
@@ -10658,26 +10891,35 @@ async function bootstrap() {
|
|
|
10658
10891
|
{ deviceId, blockCount: blocks.length, isComplete },
|
|
10659
10892
|
"Ignoring supervisor blocks - execution was cancelled"
|
|
10660
10893
|
);
|
|
10894
|
+
supervisorBlockAccumulator = [];
|
|
10661
10895
|
return;
|
|
10662
10896
|
}
|
|
10663
|
-
const
|
|
10897
|
+
const persistableBlocks = blocks.filter((b) => b.block_type !== "status");
|
|
10898
|
+
if (persistableBlocks.length > 0) {
|
|
10899
|
+
accumulateBlocks(supervisorBlockAccumulator, persistableBlocks);
|
|
10900
|
+
}
|
|
10901
|
+
const mergedBlocks = mergeToolBlocks(supervisorBlockAccumulator);
|
|
10902
|
+
const textContent = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
|
|
10664
10903
|
const outputEvent = {
|
|
10665
10904
|
type: "supervisor.output",
|
|
10666
10905
|
payload: {
|
|
10667
10906
|
content_type: "supervisor",
|
|
10668
10907
|
content: textContent,
|
|
10669
|
-
content_blocks:
|
|
10908
|
+
content_blocks: mergedBlocks,
|
|
10670
10909
|
timestamp: Date.now(),
|
|
10671
10910
|
is_complete: isComplete
|
|
10672
10911
|
}
|
|
10673
10912
|
};
|
|
10674
10913
|
const message = JSON.stringify(outputEvent);
|
|
10675
10914
|
broadcaster.broadcastToAll(message);
|
|
10915
|
+
if (isComplete) {
|
|
10916
|
+
supervisorBlockAccumulator = [];
|
|
10917
|
+
}
|
|
10676
10918
|
if (isComplete && finalOutput && finalOutput.length > 0) {
|
|
10677
10919
|
chatHistoryService.saveSupervisorMessage(
|
|
10678
10920
|
"assistant",
|
|
10679
10921
|
finalOutput,
|
|
10680
|
-
|
|
10922
|
+
mergedBlocks
|
|
10681
10923
|
);
|
|
10682
10924
|
const pendingVoiceCommand = pendingSupervisorVoiceCommands.get(deviceId);
|
|
10683
10925
|
if (pendingVoiceCommand && ttsService) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiflis-io/tiflis-code-workstation",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Workstation server for tiflis-code - manages agent sessions and terminal access",
|
|
5
5
|
"author": "Roman Barinov <rbarinov@gmail.com>",
|
|
6
6
|
"license": "FSL-1.1-NC",
|