@circuitwall/jarela 0.7.1 → 0.7.3
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +51 -35
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +2 -2
- package/.next/standalone/.next/server/app/index.html +2 -2
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page.js +515 -104
- package/.next/standalone/.next/server/app/page.js.map +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup.segment.rsc +1 -1
- package/.next/standalone/.next/server/chunks/1683.js +26 -16
- package/.next/standalone/.next/server/chunks/1683.js.map +1 -1
- package/.next/standalone/.next/server/chunks/{317.js → 5432.js} +11100 -10858
- package/.next/standalone/.next/server/chunks/5432.js.map +1 -0
- package/.next/standalone/.next/server/chunks/7885.js +606 -353
- package/.next/standalone/.next/server/chunks/7885.js.map +1 -1
- package/.next/standalone/.next/server/chunks/8135.js +59 -16
- package/.next/standalone/.next/server/chunks/8135.js.map +1 -1
- package/.next/standalone/.next/server/chunks/9032.js +3 -3
- package/.next/standalone/.next/server/chunks/9032.js.map +1 -1
- package/.next/standalone/.next/server/instrumentation.js +3 -3
- package/.next/standalone/.next/server/instrumentation.js.map +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/app/{page-a20902703c0a4f10.js → page-9fb006074fb13526.js} +582 -171
- package/.next/standalone/.next/static/chunks/app/page-9fb006074fb13526.js.map +1 -0
- package/.next/standalone/.next/static/css/5507dbe1cdc6c599.css +5 -0
- package/.next/standalone/.next/static/css/5507dbe1cdc6c599.css.map +1 -0
- package/.next/standalone/package.json +2 -1
- package/CHANGELOG.md +22 -0
- package/README.md +83 -1
- package/api/types.ts +7 -0
- package/app/api/v1/agents/[id]/compact/route.ts +2 -40
- package/components/bridges/BridgeEditor.tsx +8 -0
- package/components/chat/MessageBubble.tsx +3 -36
- package/components/models/ModelEditor.tsx +141 -0
- package/components/scheduled-tasks/ScheduledTasksPanel.tsx +5 -0
- package/components/scheduled-tasks/WatchersSection.tsx +5 -0
- package/lib/agents/context-budget.test.ts +128 -0
- package/lib/agents/context-budget.ts +128 -0
- package/lib/agents/conversation-summary.test.ts +68 -0
- package/lib/agents/conversation-summary.ts +51 -0
- package/lib/agents/run-thread.ts +112 -2
- package/lib/bridges/dispatcher.test.ts +134 -0
- package/lib/bridges/dispatcher.ts +34 -16
- package/lib/bridges/message-role.test.ts +83 -0
- package/lib/bridges/message-role.ts +46 -0
- package/lib/triggers/handlers/watcher.test.ts +23 -4
- package/lib/triggers/handlers/watcher.ts +56 -8
- package/package.json +2 -1
- package/scripts/jarela-bin.mjs +9 -0
- package/scripts/optimize-client-chunks.mjs +144 -0
- package/scripts/start-prod.mjs +10 -0
- package/.next/standalone/.next/server/chunks/317.js.map +0 -1
- package/.next/standalone/.next/static/chunks/app/page-a20902703c0a4f10.js.map +0 -1
- package/.next/standalone/.next/static/css/cc66c456aba91258.css +0 -5
- package/.next/standalone/.next/static/css/cc66c456aba91258.css.map +0 -1
- /package/.next/standalone/.next/static/{DrvaYJKeZM57UgVo0To-x → AbCOWpaxP4v4lUSeFWWYz}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{DrvaYJKeZM57UgVo0To-x → AbCOWpaxP4v4lUSeFWWYz}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatBridgePrompt, parseBridgePrompt } from "./message-role";
|
|
3
|
+
|
|
4
|
+
describe("bridge prompt envelope", () => {
|
|
5
|
+
it("round-trips DM prompt metadata and body", () => {
|
|
6
|
+
const raw = formatBridgePrompt({
|
|
7
|
+
bridge_id: "b1",
|
|
8
|
+
chat_id: "dm@jid",
|
|
9
|
+
chat_name: "Alice",
|
|
10
|
+
is_group: false,
|
|
11
|
+
role: "counterpart",
|
|
12
|
+
sender_id: "alice@jid",
|
|
13
|
+
sender_name: "Alice",
|
|
14
|
+
text: "hello from dm",
|
|
15
|
+
});
|
|
16
|
+
const parsed = parseBridgePrompt(raw);
|
|
17
|
+
expect(parsed).not.toBeNull();
|
|
18
|
+
expect(parsed?.bridgeId).toBe("b1");
|
|
19
|
+
expect(parsed?.chatJid).toBe("dm@jid");
|
|
20
|
+
expect(parsed?.isGroup).toBe(false);
|
|
21
|
+
expect(parsed?.senderJid).toBe("alice@jid");
|
|
22
|
+
expect(parsed?.body).toBe("hello from dm");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("round-trips group prompt metadata and body", () => {
|
|
26
|
+
const raw = formatBridgePrompt({
|
|
27
|
+
bridge_id: "b1",
|
|
28
|
+
chat_id: "group@jid",
|
|
29
|
+
chat_name: "Family Group",
|
|
30
|
+
is_group: true,
|
|
31
|
+
role: "counterpart",
|
|
32
|
+
sender_id: "bob@jid",
|
|
33
|
+
sender_name: "Bob",
|
|
34
|
+
text: "group message",
|
|
35
|
+
});
|
|
36
|
+
const parsed = parseBridgePrompt(raw);
|
|
37
|
+
expect(parsed).not.toBeNull();
|
|
38
|
+
expect(parsed?.chatJid).toBe("group@jid");
|
|
39
|
+
expect(parsed?.chatName).toBe("Family Group");
|
|
40
|
+
expect(parsed?.isGroup).toBe(true);
|
|
41
|
+
expect(parsed?.senderName).toBe("Bob");
|
|
42
|
+
expect(parsed?.body).toBe("group message");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("parses envelopes with prose preface before bracket headers", () => {
|
|
46
|
+
const raw = [
|
|
47
|
+
"The paired user themselves sent the message below.",
|
|
48
|
+
"",
|
|
49
|
+
"[bridge:b1]",
|
|
50
|
+
"[chat_id:dm@jid]",
|
|
51
|
+
"[chat_name:Alice]",
|
|
52
|
+
"[chat_type:dm]",
|
|
53
|
+
"[sender_id:alice@jid]",
|
|
54
|
+
"[sender_name:Alice]",
|
|
55
|
+
"",
|
|
56
|
+
"body",
|
|
57
|
+
].join("\n");
|
|
58
|
+
const parsed = parseBridgePrompt(raw);
|
|
59
|
+
expect(parsed?.bridgeId).toBe("b1");
|
|
60
|
+
expect(parsed?.body).toBe("body");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("accepts legacy key names for compatibility", () => {
|
|
64
|
+
const raw = [
|
|
65
|
+
"[bridge:b1]",
|
|
66
|
+
"[chat_jid:legacy@jid]",
|
|
67
|
+
"[chat_name:Legacy]",
|
|
68
|
+
"[chat_type:dm]",
|
|
69
|
+
"[sender_jid:sender@jid]",
|
|
70
|
+
"[sender_name:Sender]",
|
|
71
|
+
"",
|
|
72
|
+
"legacy body",
|
|
73
|
+
].join("\n");
|
|
74
|
+
const parsed = parseBridgePrompt(raw);
|
|
75
|
+
expect(parsed?.chatJid).toBe("legacy@jid");
|
|
76
|
+
expect(parsed?.senderJid).toBe("sender@jid");
|
|
77
|
+
expect(parsed?.body).toBe("legacy body");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns null when not a bridge envelope", () => {
|
|
81
|
+
expect(parseBridgePrompt("plain text")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -56,6 +56,18 @@ export interface BridgePromptInput {
|
|
|
56
56
|
text: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Parsed chat-friendly envelope extracted from a bridge prompt body.
|
|
60
|
+
// Used by the chat UI so rendering stays in lockstep with formatter changes.
|
|
61
|
+
export interface BridgePromptContext {
|
|
62
|
+
bridgeId: string;
|
|
63
|
+
chatJid: string;
|
|
64
|
+
chatName: string;
|
|
65
|
+
isGroup: boolean;
|
|
66
|
+
senderJid: string;
|
|
67
|
+
senderName: string;
|
|
68
|
+
body: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
72
|
* Build the prompt prefix the agent receives for one bridge-inbound message.
|
|
61
73
|
*
|
|
@@ -97,6 +109,40 @@ export function formatBridgePrompt(input: BridgePromptInput): string {
|
|
|
97
109
|
return `${note}\n\n${lines.join("\n")}\n\n${input.text}`;
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
// Parses bridge prompt envelopes rendered by formatBridgePrompt().
|
|
113
|
+
// Back-compat: also accepts legacy keys (chat_jid/sender_jid) and optional
|
|
114
|
+
// prose preface before the [bridge:...] metadata block.
|
|
115
|
+
export function parseBridgePrompt(raw: string): BridgePromptContext | null {
|
|
116
|
+
const start = raw.indexOf("[bridge:");
|
|
117
|
+
if (start < 0) return null;
|
|
118
|
+
const src = raw.slice(start);
|
|
119
|
+
|
|
120
|
+
const headers: Record<string, string> = {};
|
|
121
|
+
const lines = src.split("\n");
|
|
122
|
+
let i = 0;
|
|
123
|
+
for (; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (line === "") { i++; break; }
|
|
126
|
+
const m = /^\[([a-z_]+):([\s\S]*)\]$/.exec(line);
|
|
127
|
+
if (!m) return null;
|
|
128
|
+
headers[m[1]] = m[2];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const chatId = headers.chat_id || headers.chat_jid;
|
|
132
|
+
const senderId = headers.sender_id || headers.sender_jid || chatId;
|
|
133
|
+
if (!headers.bridge || !chatId || !headers.chat_type) return null;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
bridgeId: headers.bridge,
|
|
137
|
+
chatJid: chatId,
|
|
138
|
+
chatName: headers.chat_name || chatId,
|
|
139
|
+
isGroup: headers.chat_type === "group",
|
|
140
|
+
senderJid: senderId,
|
|
141
|
+
senderName: headers.sender_name || senderId || "Unknown",
|
|
142
|
+
body: lines.slice(i).join("\n").trimEnd(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
100
146
|
function roleNote(role: MessageRole, isGroup: boolean): string {
|
|
101
147
|
switch (role) {
|
|
102
148
|
case "user":
|
|
@@ -75,12 +75,31 @@ describe("watcherHandler (ADR-0027)", () => {
|
|
|
75
75
|
expect(fired.agentId).toBe("a");
|
|
76
76
|
expect(fired.kind).toBe("watcher");
|
|
77
77
|
expect(fired.prompt).toContain('Watcher "change" detected a change');
|
|
78
|
-
expect(fired.prompt).toContain("
|
|
79
|
-
expect(fired.prompt).toContain("
|
|
78
|
+
expect(fired.prompt).toContain("--- Diff (previous -> current) ---");
|
|
79
|
+
expect(fired.prompt).toContain("- v1");
|
|
80
|
+
expect(fired.prompt).toContain("+ v2");
|
|
81
|
+
expect(fired.prompt).not.toContain("--- Previous result ---");
|
|
82
|
+
expect(fired.prompt).not.toContain("--- Current result ---");
|
|
80
83
|
const after = getWatcher(w.id)!;
|
|
81
84
|
expect(after.last_fired_at).not.toBeNull();
|
|
82
85
|
});
|
|
83
86
|
|
|
87
|
+
it("truncates oversized result payloads in the firing prompt", async () => {
|
|
88
|
+
const w = createWatcher({
|
|
89
|
+
agent_id: "a", label: "big", tool_name: "watcher_test_tool", interval_seconds: 60,
|
|
90
|
+
});
|
|
91
|
+
fakeResult = "A".repeat(5000);
|
|
92
|
+
await watcherHandler.getDueFirings(new Date(Date.parse(w.next_run_at) + 1));
|
|
93
|
+
fakeResult = "B".repeat(5000);
|
|
94
|
+
const w2 = getWatcher(w.id)!;
|
|
95
|
+
const firings = await watcherHandler.getDueFirings(new Date(Date.parse(w2.next_run_at) + 1));
|
|
96
|
+
expect(firings).toHaveLength(1);
|
|
97
|
+
const fired = firings[0];
|
|
98
|
+
if (fired.mode !== "prompt") throw new Error("expected prompt firing");
|
|
99
|
+
expect(fired.prompt).toContain("[diff truncated: showing");
|
|
100
|
+
expect(fired.prompt.length).toBeLessThan(8000);
|
|
101
|
+
});
|
|
102
|
+
|
|
84
103
|
it("does NOT fire when result is unchanged across polls", async () => {
|
|
85
104
|
const w = createWatcher({
|
|
86
105
|
agent_id: "a", label: "stable", tool_name: "watcher_test_tool", interval_seconds: 60,
|
|
@@ -123,8 +142,8 @@ describe("watcherHandler (ADR-0027)", () => {
|
|
|
123
142
|
expect(fired.prompt).toContain("Open a Jira ticket against the broken dashboard.");
|
|
124
143
|
expect(fired.prompt).not.toContain("Summarise what changed");
|
|
125
144
|
// Diff envelope is preserved.
|
|
126
|
-
expect(fired.prompt).toContain("v1");
|
|
127
|
-
expect(fired.prompt).toContain("v2");
|
|
145
|
+
expect(fired.prompt).toContain("- v1");
|
|
146
|
+
expect(fired.prompt).toContain("+ v2");
|
|
128
147
|
});
|
|
129
148
|
|
|
130
149
|
it("uses the default directive when reaction_prompt is null", async () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// tools must be context-free).
|
|
7
7
|
// 3. Hash the stringified result and compare to last_fingerprint.
|
|
8
8
|
// 4. If the hash differs from the previous run, return a TriggerFiring
|
|
9
|
-
// whose prompt embeds
|
|
9
|
+
// whose prompt embeds a compact previous->current diff for the agent.
|
|
10
10
|
// If it matches (or this is the first run with no previous), record
|
|
11
11
|
// the fingerprint and skip — no LLM call, no firing.
|
|
12
12
|
// 5. Either way the watcher's next_run_at is advanced by
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "@/lib/stores/watchers";
|
|
26
26
|
import { registeredTools } from "@/lib/tools/registry";
|
|
27
27
|
import { publish as publishNotification } from "@/lib/notifications/bus";
|
|
28
|
+
import { truncateBytes } from "@/lib/utils/text";
|
|
28
29
|
import type { TriggerFiring, TriggerHandler, TriggerOutcome } from "../types";
|
|
29
30
|
|
|
30
31
|
export const WATCHER_KIND = "watcher";
|
|
@@ -46,14 +47,64 @@ const DEFAULT_REACTION_DIRECTIVE =
|
|
|
46
47
|
`Summarise what changed and decide whether the user needs to know. ` +
|
|
47
48
|
`If nothing material changed, you may stay silent.`;
|
|
48
49
|
|
|
50
|
+
// Watcher tool outputs can be very large (full JSON payloads, long lists).
|
|
51
|
+
// Keep the diff context bounded so one firing cannot consume most of an
|
|
52
|
+
// agent's prompt budget.
|
|
53
|
+
const MAX_DIFF_CONTEXT_BYTES = 3500;
|
|
54
|
+
|
|
55
|
+
function normalizeForDiff(raw: string): string {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
58
|
+
} catch {
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildDiffForPrompt(previous: string | null, current: string): string {
|
|
64
|
+
if (previous === null) return "+ (first observation baseline established; no diff available)";
|
|
65
|
+
|
|
66
|
+
const prev = normalizeForDiff(previous).split(/\r?\n/);
|
|
67
|
+
const curr = normalizeForDiff(current).split(/\r?\n/);
|
|
68
|
+
|
|
69
|
+
let start = 0;
|
|
70
|
+
while (start < prev.length && start < curr.length && prev[start] === curr[start]) {
|
|
71
|
+
start += 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let prevEnd = prev.length - 1;
|
|
75
|
+
let currEnd = curr.length - 1;
|
|
76
|
+
while (prevEnd >= start && currEnd >= start && prev[prevEnd] === curr[currEnd]) {
|
|
77
|
+
prevEnd -= 1;
|
|
78
|
+
currEnd -= 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const removed = prev.slice(start, prevEnd + 1);
|
|
82
|
+
const added = curr.slice(start, currEnd + 1);
|
|
83
|
+
if (removed.length === 0 && added.length === 0) {
|
|
84
|
+
return "(no textual diff after normalization)";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const hunkHeader = `@@ old:${start + 1}-${Math.max(start, prevEnd + 1)} new:${start + 1}-${Math.max(start, currEnd + 1)} @@`;
|
|
88
|
+
const raw = [
|
|
89
|
+
hunkHeader,
|
|
90
|
+
...removed.map((l) => `- ${l}`),
|
|
91
|
+
...added.map((l) => `+ ${l}`),
|
|
92
|
+
].join("\n");
|
|
93
|
+
|
|
94
|
+
const bytes = Buffer.byteLength(raw, "utf8");
|
|
95
|
+
const clipped = truncateBytes(raw, MAX_DIFF_CONTEXT_BYTES);
|
|
96
|
+
if (!clipped.truncated) return raw;
|
|
97
|
+
return `${clipped.text}\n… [diff truncated: showing ${MAX_DIFF_CONTEXT_BYTES} of ${bytes} bytes; full values retained in watcher state]`;
|
|
98
|
+
}
|
|
99
|
+
|
|
49
100
|
function buildFiringPrompt(watcher: WatcherRow, previous: string | null, current: string): string {
|
|
50
101
|
const argsPretty = (() => {
|
|
51
102
|
try { return JSON.stringify(JSON.parse(watcher.tool_args), null, 2); }
|
|
52
103
|
catch { return watcher.tool_args; }
|
|
53
104
|
})();
|
|
54
105
|
// ADR-0030: a non-null reaction_prompt swaps in for the default directive.
|
|
55
|
-
// The diff envelope (label/tool/args/
|
|
56
|
-
//
|
|
106
|
+
// The diff envelope (label/tool/args/diff) is unchanged so the agent
|
|
107
|
+
// always has the change context regardless of the user's instruction.
|
|
57
108
|
const directive = watcher.reaction_prompt?.trim() || DEFAULT_REACTION_DIRECTIVE;
|
|
58
109
|
return [
|
|
59
110
|
`Watcher "${watcher.label}" detected a change.`,
|
|
@@ -61,11 +112,8 @@ function buildFiringPrompt(watcher: WatcherRow, previous: string | null, current
|
|
|
61
112
|
`Tool: ${watcher.tool_name}`,
|
|
62
113
|
`Args: ${argsPretty}`,
|
|
63
114
|
``,
|
|
64
|
-
`---
|
|
65
|
-
previous
|
|
66
|
-
``,
|
|
67
|
-
`--- Current result ---`,
|
|
68
|
-
current,
|
|
115
|
+
`--- Diff (previous -> current) ---`,
|
|
116
|
+
buildDiffForPrompt(previous, current),
|
|
69
117
|
``,
|
|
70
118
|
directive,
|
|
71
119
|
].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@circuitwall/jarela",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Jarela — local chat interface for LangGraph agents (multi-provider, single-process, SQLite-backed).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Andrew Ge Wu",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"!public/sw.js",
|
|
49
49
|
"!public/swe-worker-*.js",
|
|
50
50
|
"scripts/jarela-bin.mjs",
|
|
51
|
+
"scripts/optimize-client-chunks.mjs",
|
|
51
52
|
"scripts/service-install.mjs",
|
|
52
53
|
"scripts/start-prod.mjs",
|
|
53
54
|
"scripts/postbuild.mjs",
|
package/scripts/jarela-bin.mjs
CHANGED
|
@@ -40,6 +40,12 @@ Environment:
|
|
|
40
40
|
JARELA_VOICE_TIMEOUT_MS — Gemini voice request timeout (default 60000)
|
|
41
41
|
JARELA_IMAGE_TIMEOUT_MS — Gemini image request timeout (default 60000)
|
|
42
42
|
JARELA_DISABLE_UPDATE_CHECK — set to 1 to skip the npm update check
|
|
43
|
+
JARELA_PREFLIGHT_OPTIMIZE_CLIENT
|
|
44
|
+
— set to 1 to run one-time local chunk
|
|
45
|
+
optimization before server boot (default: 1
|
|
46
|
+
for npm/global install, 0 for source checkout)
|
|
47
|
+
JARELA_FORCE_PREFLIGHT_OPTIMIZE
|
|
48
|
+
— set to 1 to force re-running optimization
|
|
43
49
|
`);
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -110,5 +116,8 @@ if (process.env.JARELA_PORT) process.env.PORT = process.env.JARELA_PORT;
|
|
|
110
116
|
if (process.env.JARELA_HOSTNAME) process.env.HOSTNAME = process.env.JARELA_HOSTNAME;
|
|
111
117
|
process.env.PORT ||= "4312";
|
|
112
118
|
process.env.HOSTNAME ||= "127.0.0.1";
|
|
119
|
+
if (installedUnderNodeModules) {
|
|
120
|
+
process.env.JARELA_PREFLIGHT_OPTIMIZE_CLIENT ||= "1";
|
|
121
|
+
}
|
|
113
122
|
process.chdir(root);
|
|
114
123
|
await import(new URL("./start-prod.mjs", import.meta.url).href);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, readdir, stat, mkdir } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, relative, sep } from "node:path";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MARKER = join(
|
|
11
|
+
process.cwd(),
|
|
12
|
+
".next",
|
|
13
|
+
"standalone",
|
|
14
|
+
".next",
|
|
15
|
+
"static",
|
|
16
|
+
".jarela-client-optimized.json",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
function normalize(p) {
|
|
20
|
+
return p.split(sep).join("/");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function listJsFiles(root) {
|
|
24
|
+
const out = [];
|
|
25
|
+
async function walk(dir) {
|
|
26
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const full = join(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
await walk(full);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
34
|
+
out.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await walk(root);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function looksAlreadyMinified(code) {
|
|
43
|
+
const lines = code.split(/\r?\n/);
|
|
44
|
+
let maxLine = 0;
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (line.length > maxLine) maxLine = line.length;
|
|
47
|
+
}
|
|
48
|
+
return lines.length <= 25 && maxLine >= 600;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function optimizeClientChunksOnce(opts = {}) {
|
|
52
|
+
const standaloneRoot = opts.standaloneRoot ?? join(process.cwd(), ".next", "standalone");
|
|
53
|
+
const chunksRoot = opts.chunksRoot ?? join(standaloneRoot, ".next", "static", "chunks");
|
|
54
|
+
const markerPath = opts.markerPath ?? DEFAULT_MARKER;
|
|
55
|
+
const enabled = opts.enabled ?? process.env.JARELA_PREFLIGHT_OPTIMIZE_CLIENT === "1";
|
|
56
|
+
|
|
57
|
+
if (!enabled) return;
|
|
58
|
+
if (!existsSync(chunksRoot)) return;
|
|
59
|
+
|
|
60
|
+
let pkgVersion = "unknown";
|
|
61
|
+
try {
|
|
62
|
+
const pkg = JSON.parse(await readFile(join(process.cwd(), "package.json"), "utf8"));
|
|
63
|
+
pkgVersion = String(pkg.version || "unknown");
|
|
64
|
+
} catch {
|
|
65
|
+
// Non-fatal. We still optimize and stamp with "unknown".
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (existsSync(markerPath) && process.env.JARELA_FORCE_PREFLIGHT_OPTIMIZE !== "1") {
|
|
69
|
+
try {
|
|
70
|
+
const marker = JSON.parse(await readFile(markerPath, "utf8"));
|
|
71
|
+
if (marker?.version === pkgVersion) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Corrupt marker: continue and regenerate.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const terser = require("next/dist/compiled/terser");
|
|
80
|
+
const minify = terser?.minify;
|
|
81
|
+
if (typeof minify !== "function") {
|
|
82
|
+
console.warn("[preflight-optimize] terser unavailable; skipping optimization.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const jsFiles = await listJsFiles(chunksRoot);
|
|
87
|
+
let optimized = 0;
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
let failed = 0;
|
|
90
|
+
|
|
91
|
+
for (const file of jsFiles) {
|
|
92
|
+
let code;
|
|
93
|
+
try {
|
|
94
|
+
code = await readFile(file, "utf8");
|
|
95
|
+
} catch {
|
|
96
|
+
failed += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (looksAlreadyMinified(code)) {
|
|
101
|
+
skipped += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const result = await minify(code, {
|
|
107
|
+
compress: true,
|
|
108
|
+
mangle: true,
|
|
109
|
+
sourceMap: false,
|
|
110
|
+
format: { comments: false },
|
|
111
|
+
});
|
|
112
|
+
if (!result?.code || result.code.length >= code.length) {
|
|
113
|
+
skipped += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
await writeFile(file, result.code, "utf8");
|
|
117
|
+
optimized += 1;
|
|
118
|
+
} catch {
|
|
119
|
+
failed += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await mkdir(join(standaloneRoot, ".next", "static"), { recursive: true });
|
|
124
|
+
await writeFile(
|
|
125
|
+
markerPath,
|
|
126
|
+
JSON.stringify(
|
|
127
|
+
{
|
|
128
|
+
version: pkgVersion,
|
|
129
|
+
optimized,
|
|
130
|
+
skipped,
|
|
131
|
+
failed,
|
|
132
|
+
chunksRoot: normalize(relative(process.cwd(), chunksRoot)),
|
|
133
|
+
optimizedAt: new Date().toISOString(),
|
|
134
|
+
},
|
|
135
|
+
null,
|
|
136
|
+
2,
|
|
137
|
+
) + "\n",
|
|
138
|
+
"utf8",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
console.log(
|
|
142
|
+
`[preflight-optimize] complete: optimized=${optimized}, skipped=${skipped}, failed=${failed}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
package/scripts/start-prod.mjs
CHANGED
|
@@ -27,4 +27,14 @@ if (process.env.JARELA_HOSTNAME) process.env.HOSTNAME = process.env.JARELA_HOSTN
|
|
|
27
27
|
process.env.PORT ||= "4312";
|
|
28
28
|
process.env.HOSTNAME ||= "127.0.0.1";
|
|
29
29
|
|
|
30
|
+
if (process.env.JARELA_PREFLIGHT_OPTIMIZE_CLIENT === "1") {
|
|
31
|
+
try {
|
|
32
|
+
const { optimizeClientChunksOnce } = await import("./optimize-client-chunks.mjs");
|
|
33
|
+
await optimizeClientChunksOnce();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
36
|
+
console.warn(`[preflight-optimize] skipped: ${msg}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
await import(pathToFileURL(server).href);
|