@arach/lattices 0.2.1 → 0.6.1
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/LICENSE +21 -0
- package/README.md +144 -69
- package/apps/mac/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
- package/apps/mac/Lattices.entitlements +21 -0
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Resources/tap.wav +0 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/assistant-intelligence.ts +912 -0
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +17 -0
- package/bin/cua.ts +26 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +96 -0
- package/bin/handsoff-worker.ts +531 -0
- package/bin/infer.ts +424 -0
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +655 -0
- package/bin/lattices-build +125 -0
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +362 -0
- package/bin/lattices.ts +3260 -0
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +233 -0
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +1041 -47
- package/docs/app.md +96 -13
- package/docs/assistant-knowledge.md +130 -0
- package/docs/companion-deck.md +209 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/concepts.md +13 -12
- package/docs/config.md +83 -10
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +176 -28
- package/docs/mouse-gestures.md +244 -0
- package/docs/ocr.md +21 -9
- package/docs/overview.md +42 -23
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +382 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +8 -12
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/release.md +172 -0
- package/docs/repo-structure.md +100 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +224 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice-error-model.md +73 -0
- package/docs/voice.md +221 -0
- package/package.json +69 -16
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
- package/app/Lattices.app/Contents/Info.plist +0 -24
- package/app/Package.swift +0 -13
- package/app/Sources/ActionRow.swift +0 -61
- package/app/Sources/App.swift +0 -10
- package/app/Sources/AppDelegate.swift +0 -234
- package/app/Sources/AppShellView.swift +0 -62
- package/app/Sources/AppTypeClassifier.swift +0 -70
- package/app/Sources/AppWindowShell.swift +0 -63
- package/app/Sources/CheatSheetHUD.swift +0 -332
- package/app/Sources/CommandModeState.swift +0 -1362
- package/app/Sources/CommandModeView.swift +0 -1405
- package/app/Sources/CommandModeWindow.swift +0 -192
- package/app/Sources/CommandPaletteView.swift +0 -307
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/DaemonProtocol.swift +0 -101
- package/app/Sources/DaemonServer.swift +0 -414
- package/app/Sources/DesktopModel.swift +0 -121
- package/app/Sources/DesktopModelTypes.swift +0 -71
- package/app/Sources/DiagnosticLog.swift +0 -271
- package/app/Sources/EventBus.swift +0 -30
- package/app/Sources/HotkeyManager.swift +0 -250
- package/app/Sources/HotkeyStore.swift +0 -338
- package/app/Sources/InventoryManager.swift +0 -35
- package/app/Sources/InventoryPath.swift +0 -43
- package/app/Sources/KeyRecorderView.swift +0 -210
- package/app/Sources/LatticesApi.swift +0 -1125
- package/app/Sources/MainView.swift +0 -467
- package/app/Sources/MainWindow.swift +0 -83
- package/app/Sources/OcrModel.swift +0 -309
- package/app/Sources/OcrStore.swift +0 -295
- package/app/Sources/OmniSearchState.swift +0 -283
- package/app/Sources/OmniSearchView.swift +0 -288
- package/app/Sources/OmniSearchWindow.swift +0 -105
- package/app/Sources/OrphanRow.swift +0 -129
- package/app/Sources/PaletteCommand.swift +0 -419
- package/app/Sources/PermissionChecker.swift +0 -125
- package/app/Sources/Preferences.swift +0 -92
- package/app/Sources/ProcessModel.swift +0 -199
- package/app/Sources/ProcessQuery.swift +0 -151
- package/app/Sources/Project.swift +0 -28
- package/app/Sources/ProjectRow.swift +0 -368
- package/app/Sources/ProjectScanner.swift +0 -121
- package/app/Sources/ScreenMapState.swift +0 -2387
- package/app/Sources/ScreenMapView.swift +0 -2820
- package/app/Sources/ScreenMapWindowController.swift +0 -89
- package/app/Sources/SessionManager.swift +0 -72
- package/app/Sources/SettingsView.swift +0 -1053
- package/app/Sources/SettingsWindow.swift +0 -20
- package/app/Sources/TabGroupRow.swift +0 -178
- package/app/Sources/Terminal.swift +0 -259
- package/app/Sources/TerminalQuery.swift +0 -156
- package/app/Sources/TerminalSynthesizer.swift +0 -200
- package/app/Sources/Theme.swift +0 -163
- package/app/Sources/TilePickerView.swift +0 -209
- package/app/Sources/TmuxModel.swift +0 -53
- package/app/Sources/TmuxQuery.swift +0 -81
- package/app/Sources/WindowTiler.swift +0 -1755
- package/app/Sources/WorkspaceManager.swift +0 -434
- package/bin/lattices-app.js +0 -221
- package/bin/lattices.js +0 -1418
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { relativeTime } from "./helpers.ts";
|
|
2
|
+
import { withDaemon, type DaemonClient } from "./daemon.ts";
|
|
3
|
+
|
|
4
|
+
export interface SearchResult {
|
|
5
|
+
score: number;
|
|
6
|
+
window: any;
|
|
7
|
+
tabs: { tab: number; cwd: string; title: string; hasClaude: boolean; tmuxSession: string }[];
|
|
8
|
+
reasons: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SearchOptions {
|
|
12
|
+
sources?: string[];
|
|
13
|
+
after?: string;
|
|
14
|
+
before?: string;
|
|
15
|
+
recency?: boolean;
|
|
16
|
+
mode?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function searchWithClient(client: DaemonClient, query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
|
|
20
|
+
const { daemonCall } = client;
|
|
21
|
+
const params: Record<string, any> = { query };
|
|
22
|
+
if (opts.sources) params.sources = opts.sources;
|
|
23
|
+
if (opts.after) params.after = opts.after;
|
|
24
|
+
if (opts.before) params.before = opts.before;
|
|
25
|
+
if (opts.recency !== undefined) params.recency = opts.recency;
|
|
26
|
+
if (opts.mode) params.mode = opts.mode;
|
|
27
|
+
const hits = await daemonCall("lattices.search", params, 10000) as any[];
|
|
28
|
+
return hits.map((w: any) => ({
|
|
29
|
+
score: w.score || 0,
|
|
30
|
+
window: w,
|
|
31
|
+
tabs: (w.terminalTabs || []).map((t: any) => ({
|
|
32
|
+
tab: t.tabIndex, cwd: t.cwd, title: t.tabTitle, hasClaude: t.hasClaude, tmuxSession: t.tmuxSession,
|
|
33
|
+
})),
|
|
34
|
+
reasons: w.matchSources || [],
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function search(query: string, opts: SearchOptions = {}): Promise<SearchResult[]> {
|
|
39
|
+
return withDaemon(client => searchWithClient(client, query, opts));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function deepSearch(query: string): Promise<SearchResult[]> {
|
|
43
|
+
return search(query, { sources: ["all"] });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function printResults(ranked: SearchResult[]): void {
|
|
47
|
+
if (!ranked.length) return;
|
|
48
|
+
for (const r of ranked) {
|
|
49
|
+
const w = r.window;
|
|
50
|
+
const age = w.lastInteraction ? ` \x1b[2m${relativeTime(w.lastInteraction)}\x1b[0m` : "";
|
|
51
|
+
console.log(` \x1b[1m${w.app}\x1b[0m "${w.title}" wid:${w.wid} score:${r.score} (${r.reasons.join(", ")})${age}`);
|
|
52
|
+
for (const t of r.tabs) {
|
|
53
|
+
const claude = t.hasClaude ? " \x1b[32m●\x1b[0m" : "";
|
|
54
|
+
const tmux = t.tmuxSession ? ` \x1b[36m[${t.tmuxSession}]\x1b[0m` : "";
|
|
55
|
+
console.log(` tab ${t.tab}: ${t.cwd || t.title}${claude}${tmux}`);
|
|
56
|
+
}
|
|
57
|
+
if (w.ocrSnippet) console.log(` ocr: "${w.ocrSnippet}"`);
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function searchCommand(
|
|
63
|
+
query: string | undefined,
|
|
64
|
+
flags: Set<string>,
|
|
65
|
+
rawArgs: string[] = []
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
if (!query) {
|
|
68
|
+
console.log("Usage: lattices search <query> [--quick | --terminal | --all | --deep | --sources=... | --after=... | --before=... | --json | --wid]");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const opts: SearchOptions = {};
|
|
73
|
+
|
|
74
|
+
const sourcesFlag = rawArgs.find(a => a.startsWith("--sources="));
|
|
75
|
+
if (sourcesFlag) {
|
|
76
|
+
opts.sources = sourcesFlag.slice("--sources=".length).split(",");
|
|
77
|
+
} else if (flags.has("--all") || flags.has("--deep")) {
|
|
78
|
+
opts.sources = ["all"];
|
|
79
|
+
} else if (flags.has("--quick")) {
|
|
80
|
+
opts.sources = ["titles", "apps", "sessions"];
|
|
81
|
+
} else if (flags.has("--terminal")) {
|
|
82
|
+
opts.sources = ["terminals"];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const afterFlag = rawArgs.find(a => a.startsWith("--after="));
|
|
86
|
+
if (afterFlag) opts.after = afterFlag.slice("--after=".length);
|
|
87
|
+
const beforeFlag = rawArgs.find(a => a.startsWith("--before="));
|
|
88
|
+
if (beforeFlag) opts.before = beforeFlag.slice("--before=".length);
|
|
89
|
+
|
|
90
|
+
if (flags.has("--no-recency")) opts.recency = false;
|
|
91
|
+
|
|
92
|
+
const ranked = await search(query, opts);
|
|
93
|
+
const jsonOut = flags.has("--json");
|
|
94
|
+
const widOnly = flags.has("--wid");
|
|
95
|
+
|
|
96
|
+
if (jsonOut) {
|
|
97
|
+
console.log(JSON.stringify(ranked.map(r => ({
|
|
98
|
+
wid: r.window.wid, app: r.window.app, title: r.window.title,
|
|
99
|
+
score: r.score, reasons: r.reasons, tabs: r.tabs, ocrSnippet: r.window.ocrSnippet,
|
|
100
|
+
})), null, 2));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (widOnly) {
|
|
105
|
+
for (const r of ranked) console.log(r.window.wid);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!ranked.length) {
|
|
110
|
+
console.log(`No results for "${query}"`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
printResults(ranked);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function placeCommand(query?: string, tilePosition?: string): Promise<void> {
|
|
118
|
+
if (!query) {
|
|
119
|
+
console.log("Usage: lattices place <query> [position]");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await withDaemon(async (client) => {
|
|
124
|
+
const { daemonCall } = client;
|
|
125
|
+
const ranked = await searchWithClient(client, query, { sources: ["all"] });
|
|
126
|
+
|
|
127
|
+
if (!ranked.length) {
|
|
128
|
+
console.log(`No window matching "${query}"`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const pos = tilePosition || "bottom-right";
|
|
133
|
+
const win = ranked[0].window;
|
|
134
|
+
await daemonCall("window.focus", { wid: win.wid });
|
|
135
|
+
await daemonCall("intents.execute", {
|
|
136
|
+
intent: "tile_window",
|
|
137
|
+
slots: { position: pos, wid: win.wid }
|
|
138
|
+
}, 3000);
|
|
139
|
+
console.log(`${win.app} "${win.title}" (wid:${win.wid}) → ${pos}`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { runQuiet } from "./helpers.ts";
|
|
4
|
+
|
|
5
|
+
export function pathHash(dir: string): string {
|
|
6
|
+
return createHash("sha256").update(resolve(dir)).digest("hex").slice(0, 6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function toSessionName(dir: string): string {
|
|
10
|
+
const base = basename(dir).replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
11
|
+
return `${base}-${pathHash(dir)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function esc(str: string): string {
|
|
15
|
+
return str.replace(/'/g, "'\\''");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function slugify(str: string): string {
|
|
19
|
+
return str
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/\.app$/i, "")
|
|
22
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "") || "app";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sessionExists(name: string): boolean {
|
|
27
|
+
return runQuiet(`tmux has-session -t "${name}" 2>&1`) !== null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toGroupSessionName(groupId: string): string {
|
|
31
|
+
return `lattices-group-${groupId}`;
|
|
32
|
+
}
|
package/bin/client.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Public API — re-exports from daemon-client for a cleaner import path.
|
|
2
|
+
// Usage: import { daemonCall, isDaemonRunning } from '@arach/lattices'
|
|
3
|
+
// Also published as @lattices/cli for back-compat.
|
|
4
|
+
|
|
5
|
+
export { daemonCall, isDaemonRunning } from "./daemon-client.ts";
|
|
6
|
+
export {
|
|
7
|
+
ProjectTwin,
|
|
8
|
+
createProjectTwin,
|
|
9
|
+
readOpenScoutRelayContext,
|
|
10
|
+
type OpenScoutRelayContext,
|
|
11
|
+
type ProjectTwinEvent,
|
|
12
|
+
type ProjectTwinInvokeRequest,
|
|
13
|
+
type ProjectTwinOptions,
|
|
14
|
+
type ProjectTwinResult,
|
|
15
|
+
type ProjectTwinState,
|
|
16
|
+
type ProjectTwinThinkingLevel,
|
|
17
|
+
} from "./project-twin.ts";
|
package/bin/cua.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
click,
|
|
3
|
+
computerClickParamsSchema,
|
|
4
|
+
computerClickTransportSchema,
|
|
5
|
+
computerMagicCursorParamsSchema,
|
|
6
|
+
computerTreatmentSchema,
|
|
7
|
+
createCuaClient,
|
|
8
|
+
cua,
|
|
9
|
+
cursorEdgeSchema,
|
|
10
|
+
cursorGlowSchema,
|
|
11
|
+
cursorIdleSchema,
|
|
12
|
+
cursorMotionSchema,
|
|
13
|
+
cursorShapeSchema,
|
|
14
|
+
cursorSizeSchema,
|
|
15
|
+
cursorStyleSchema,
|
|
16
|
+
cursorTrailSchema,
|
|
17
|
+
cursorTrajectorySchema,
|
|
18
|
+
magicCursor,
|
|
19
|
+
type ComputerClickParams,
|
|
20
|
+
type ComputerMagicCursorParams,
|
|
21
|
+
type CursorEdge,
|
|
22
|
+
type CursorGlow,
|
|
23
|
+
type CursorIdle,
|
|
24
|
+
type CuaClient,
|
|
25
|
+
type CuaClientOptions,
|
|
26
|
+
} from "../packages/npm/sdk/cua.mjs";
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
// Lightweight WebSocket client for lattices daemon (ws://127.0.0.1:9399)
|
|
2
2
|
// Uses Node `net` module with manual HTTP upgrade + minimal WS framing.
|
|
3
|
-
// Zero npm dependencies.
|
|
3
|
+
// Zero npm dependencies.
|
|
4
4
|
|
|
5
|
-
import { createConnection } from "node:net";
|
|
5
|
+
import { createConnection, type Socket } from "node:net";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
const DAEMON_HOST = "127.0.0.1";
|
|
9
9
|
const DAEMON_PORT = 9399;
|
|
10
10
|
|
|
11
|
+
interface ParsedFrame {
|
|
12
|
+
payload: string;
|
|
13
|
+
rest: Buffer<ArrayBuffer>;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* Send a JSON-RPC-style request to the daemon and return the response.
|
|
13
|
-
* @param {string} method
|
|
14
|
-
* @param {object} [params]
|
|
15
|
-
* @param {number} [timeoutMs=3000]
|
|
16
|
-
* @returns {Promise<object>} The result field from the response
|
|
17
18
|
*/
|
|
18
|
-
export async function daemonCall(
|
|
19
|
+
export async function daemonCall(
|
|
20
|
+
method: string,
|
|
21
|
+
params?: Record<string, unknown> | null,
|
|
22
|
+
timeoutMs = 3000
|
|
23
|
+
): Promise<unknown> {
|
|
19
24
|
const id = randomBytes(4).toString("hex");
|
|
20
25
|
const request = JSON.stringify({ id, method, params: params ?? null });
|
|
21
26
|
|
|
@@ -62,8 +67,8 @@ export async function daemonCall(method, params, timeoutMs = 3000) {
|
|
|
62
67
|
socket.write(upgrade);
|
|
63
68
|
});
|
|
64
69
|
|
|
65
|
-
socket.on("data", (chunk) => {
|
|
66
|
-
buffer = Buffer.concat([buffer, chunk])
|
|
70
|
+
socket.on("data", (chunk: Buffer) => {
|
|
71
|
+
buffer = Buffer.concat([buffer, chunk]) as Buffer<ArrayBuffer>;
|
|
67
72
|
|
|
68
73
|
if (!upgraded) {
|
|
69
74
|
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
@@ -82,23 +87,38 @@ export async function daemonCall(method, params, timeoutMs = 3000) {
|
|
|
82
87
|
sendFrame(socket, request);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
// The daemon can push broadcast events before the RPC response.
|
|
91
|
+
// Keep consuming frames until we see our matching response id.
|
|
92
|
+
while (true) {
|
|
93
|
+
const result = parseFrame(buffer);
|
|
94
|
+
if (!result) break;
|
|
88
95
|
buffer = result.rest;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(result.payload);
|
|
99
|
+
if (parsed.event) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (parsed.id !== id) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!settled) {
|
|
106
|
+
settled = true;
|
|
107
|
+
cleanup();
|
|
94
108
|
if (parsed.error) {
|
|
95
109
|
reject(new Error(parsed.error));
|
|
96
110
|
} else {
|
|
97
111
|
resolve(parsed.result);
|
|
98
112
|
}
|
|
99
|
-
}
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
} catch {
|
|
116
|
+
if (!settled) {
|
|
117
|
+
settled = true;
|
|
118
|
+
cleanup();
|
|
100
119
|
reject(new Error("Invalid JSON response from daemon"));
|
|
101
120
|
}
|
|
121
|
+
return;
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
124
|
});
|
|
@@ -107,9 +127,8 @@ export async function daemonCall(method, params, timeoutMs = 3000) {
|
|
|
107
127
|
|
|
108
128
|
/**
|
|
109
129
|
* Check if the daemon is reachable.
|
|
110
|
-
* @returns {Promise<boolean>}
|
|
111
130
|
*/
|
|
112
|
-
export async function isDaemonRunning() {
|
|
131
|
+
export async function isDaemonRunning(): Promise<boolean> {
|
|
113
132
|
try {
|
|
114
133
|
await daemonCall("daemon.status", null, 1000);
|
|
115
134
|
return true;
|
|
@@ -120,12 +139,12 @@ export async function isDaemonRunning() {
|
|
|
120
139
|
|
|
121
140
|
// MARK: - WebSocket framing helpers
|
|
122
141
|
|
|
123
|
-
function sendFrame(socket, text) {
|
|
142
|
+
function sendFrame(socket: Socket, text: string): void {
|
|
124
143
|
const payload = Buffer.from(text, "utf8");
|
|
125
144
|
const mask = randomBytes(4);
|
|
126
145
|
const len = payload.length;
|
|
127
146
|
|
|
128
|
-
let header;
|
|
147
|
+
let header: Buffer;
|
|
129
148
|
if (len < 126) {
|
|
130
149
|
header = Buffer.alloc(2);
|
|
131
150
|
header[0] = 0x81; // FIN + text opcode
|
|
@@ -145,17 +164,17 @@ function sendFrame(socket, text) {
|
|
|
145
164
|
// Mask payload
|
|
146
165
|
const masked = Buffer.alloc(payload.length);
|
|
147
166
|
for (let i = 0; i < payload.length; i++) {
|
|
148
|
-
masked[i] = payload[i] ^ mask[i % 4]
|
|
167
|
+
masked[i] = payload[i]! ^ mask[i % 4]!;
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
socket.write(Buffer.concat([header, mask, masked]));
|
|
152
171
|
}
|
|
153
172
|
|
|
154
|
-
function parseFrame(buf) {
|
|
173
|
+
function parseFrame(buf: Buffer): ParsedFrame | null {
|
|
155
174
|
if (buf.length < 2) return null;
|
|
156
175
|
|
|
157
|
-
const
|
|
158
|
-
let payloadLen = buf[1] & 0x7f;
|
|
176
|
+
const isMasked = (buf[1]! & 0x80) !== 0;
|
|
177
|
+
let payloadLen = buf[1]! & 0x7f;
|
|
159
178
|
let offset = 2;
|
|
160
179
|
|
|
161
180
|
if (payloadLen === 126) {
|
|
@@ -168,20 +187,20 @@ function parseFrame(buf) {
|
|
|
168
187
|
offset = 10;
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
if (
|
|
190
|
+
if (isMasked) offset += 4;
|
|
172
191
|
if (buf.length < offset + payloadLen) return null;
|
|
173
192
|
|
|
174
193
|
let payload = buf.subarray(offset, offset + payloadLen);
|
|
175
|
-
if (
|
|
194
|
+
if (isMasked) {
|
|
176
195
|
const maskKey = buf.subarray(offset - 4, offset);
|
|
177
196
|
payload = Buffer.alloc(payloadLen);
|
|
178
197
|
for (let i = 0; i < payloadLen; i++) {
|
|
179
|
-
payload[i] = buf[offset + i] ^ maskKey[i % 4]
|
|
198
|
+
payload[i] = buf[offset + i]! ^ maskKey[i % 4]!;
|
|
180
199
|
}
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
return {
|
|
184
203
|
payload: payload.toString("utf8"),
|
|
185
|
-
rest: buf.subarray(offset + payloadLen)
|
|
204
|
+
rest: buf.subarray(offset + payloadLen) as Buffer<ArrayBuffer>,
|
|
186
205
|
};
|
|
187
206
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Hands-off inference script — called by HandsOffSession.swift.
|
|
4
|
+
*
|
|
5
|
+
* Usage: echo '{"transcript":"tile chrome left","snapshot":{...}}' | bun run bin/handsoff-infer.ts
|
|
6
|
+
*
|
|
7
|
+
* Reads JSON from stdin, calls the configured voice inference provider, prints JSON result to stdout.
|
|
8
|
+
* All logging goes to stderr so it doesn't pollute the JSON output.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
buildAssistantContextMessage,
|
|
13
|
+
buildAssistantSystemPrompt,
|
|
14
|
+
normalizeAssistantPlan,
|
|
15
|
+
tryLocalAssistantPlan,
|
|
16
|
+
} from "./assistant-intelligence.ts";
|
|
17
|
+
import { inferJSON, resolveVoiceInferenceOptions } from "./infer.ts";
|
|
18
|
+
|
|
19
|
+
const INFER_TIMEOUT_MS = 15_000;
|
|
20
|
+
|
|
21
|
+
// ── Read input from stdin ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const input = await Bun.stdin.text();
|
|
24
|
+
const req = JSON.parse(input) as {
|
|
25
|
+
transcript: string;
|
|
26
|
+
snapshot: {
|
|
27
|
+
stageManager?: boolean;
|
|
28
|
+
smGrouping?: string;
|
|
29
|
+
activeStage?: Array<{ wid: number; app: string; title: string; frame: string }>;
|
|
30
|
+
stripApps?: string[];
|
|
31
|
+
hiddenApps?: string[];
|
|
32
|
+
currentLayer?: string;
|
|
33
|
+
screen?: string;
|
|
34
|
+
};
|
|
35
|
+
history?: Array<{ role: "user" | "assistant"; content: string }>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const transcript = req.transcript ?? "";
|
|
39
|
+
const systemPrompt = buildAssistantSystemPrompt();
|
|
40
|
+
const userMessage = buildAssistantContextMessage(transcript, req.snapshot ?? {});
|
|
41
|
+
const voiceInference = resolveVoiceInferenceOptions();
|
|
42
|
+
|
|
43
|
+
const localPlan = tryLocalAssistantPlan(transcript, req.snapshot ?? {});
|
|
44
|
+
if (localPlan) {
|
|
45
|
+
console.log(JSON.stringify(localPlan));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Call inference ──────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const messages = (req.history ?? []).map((h) => ({
|
|
52
|
+
role: h.role as "user" | "assistant",
|
|
53
|
+
content: h.content,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timer = setTimeout(() => controller.abort(), INFER_TIMEOUT_MS);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const { data, raw } = await inferJSON(userMessage, {
|
|
61
|
+
provider: voiceInference.provider,
|
|
62
|
+
model: voiceInference.model,
|
|
63
|
+
system: systemPrompt,
|
|
64
|
+
messages,
|
|
65
|
+
temperature: 0.2,
|
|
66
|
+
maxTokens: 512,
|
|
67
|
+
abortSignal: controller.signal,
|
|
68
|
+
tag: "hands-off",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Output result as JSON to stdout
|
|
72
|
+
const plan = normalizeAssistantPlan(data, transcript);
|
|
73
|
+
const output = {
|
|
74
|
+
...plan,
|
|
75
|
+
_meta: {
|
|
76
|
+
...plan._meta,
|
|
77
|
+
provider: raw.provider,
|
|
78
|
+
model: raw.model,
|
|
79
|
+
durationMs: raw.durationMs,
|
|
80
|
+
tokens: raw.usage?.totalTokens,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
console.log(JSON.stringify(output));
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
console.log(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
actions: [],
|
|
89
|
+
spoken: "Sorry, I had trouble processing that.",
|
|
90
|
+
_meta: { error: err.message },
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|