jorgex-stack 1.0.16 → 1.0.18
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/cli.js
CHANGED
|
@@ -3003,7 +3003,8 @@ function parseFlags(args) {
|
|
|
3003
3003
|
list: false,
|
|
3004
3004
|
check: false,
|
|
3005
3005
|
removeEngram: false,
|
|
3006
|
-
positional: []
|
|
3006
|
+
positional: [],
|
|
3007
|
+
unknownFlags: []
|
|
3007
3008
|
};
|
|
3008
3009
|
const readValue = (index) => {
|
|
3009
3010
|
const value = args[index + 1];
|
|
@@ -3039,6 +3040,7 @@ function parseFlags(args) {
|
|
|
3039
3040
|
else if (arg === "--list") flags.list = true;
|
|
3040
3041
|
else if (arg === "--check") flags.check = true;
|
|
3041
3042
|
else if (arg === "--remove-engram") flags.removeEngram = true;
|
|
3043
|
+
else if (arg.startsWith("-")) flags.unknownFlags.push(arg);
|
|
3042
3044
|
else flags.positional.push(arg);
|
|
3043
3045
|
}
|
|
3044
3046
|
return flags;
|
|
@@ -3119,6 +3121,7 @@ function parseCliArgs(argv) {
|
|
|
3119
3121
|
const flags = parseFlags(isCommand ? rest : argv);
|
|
3120
3122
|
if (first === "--help" || first === "-h" || flags.help) return { action: "help", command, flags };
|
|
3121
3123
|
if (first === "--version" || first === "-v" || flags.version) return { action: "version", command, flags };
|
|
3124
|
+
if (flags.unknownFlags.length > 0) return { action: "unknown-flags", command, flags };
|
|
3122
3125
|
return { action: "run", command, flags };
|
|
3123
3126
|
}
|
|
3124
3127
|
async function resolveRuntimes(flags) {
|
|
@@ -3172,6 +3175,18 @@ async function main() {
|
|
|
3172
3175
|
process.exitCode = 1;
|
|
3173
3176
|
return;
|
|
3174
3177
|
}
|
|
3178
|
+
if (parsed.action === "unknown-flags") {
|
|
3179
|
+
const { unknownFlags } = parsed.flags;
|
|
3180
|
+
const plural = unknownFlags.length > 1;
|
|
3181
|
+
console.error(`Flag${plural ? "s" : ""} no reconocido${plural ? "s" : ""}: ${unknownFlags.join(", ")}`);
|
|
3182
|
+
console.error(
|
|
3183
|
+
`jorgex-stack v${VERSION} no reconoce ${plural ? "esos flags" : "ese flag"}. Si esperabas que existiera, puede que est\xE9s ejecutando un binario cacheado antiguo:
|
|
3184
|
+
pnpm dlx jorgex-stack@latest ...
|
|
3185
|
+
Flags disponibles: jorgex-stack --help`
|
|
3186
|
+
);
|
|
3187
|
+
process.exitCode = 1;
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3175
3190
|
const { command, flags } = parsed;
|
|
3176
3191
|
if (flags.targetDir !== void 0 && flags.agents.length !== 1) {
|
|
3177
3192
|
console.error("--target-dir requiere exactamente un runtime en --agents.");
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
18
18
|
|
|
19
19
|
declare const Bun: {
|
|
20
20
|
which?: (bin: string) => string | null
|
|
21
|
-
spawnSync: (args: string[]) => { exitCode: number; stdout?: { toString(): string } | string }
|
|
21
|
+
spawnSync: (args: string[], options?: Record<string, unknown>) => { exitCode: number; stdout?: { toString(): string } | string }
|
|
22
22
|
spawn: (args: string[], options?: Record<string, unknown>) => unknown
|
|
23
23
|
file: (path: string) => { exists: () => Promise<boolean> }
|
|
24
24
|
}
|
|
@@ -102,7 +102,7 @@ async function isEngramRunning(): Promise<boolean> {
|
|
|
102
102
|
function extractProjectName(directory: string): string {
|
|
103
103
|
// Try git remote origin URL
|
|
104
104
|
try {
|
|
105
|
-
const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"])
|
|
105
|
+
const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" })
|
|
106
106
|
if (result.exitCode === 0) {
|
|
107
107
|
const url = result.stdout?.toString().trim()
|
|
108
108
|
if (url) {
|
|
@@ -114,7 +114,7 @@ function extractProjectName(directory: string): string {
|
|
|
114
114
|
|
|
115
115
|
// Fallback: git root directory name (works in worktrees)
|
|
116
116
|
try {
|
|
117
|
-
const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"])
|
|
117
|
+
const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" })
|
|
118
118
|
if (result.exitCode === 0) {
|
|
119
119
|
const root = result.stdout?.toString().trim()
|
|
120
120
|
if (root) return root.split(/[\\/]/).pop() ?? "unknown"
|
|
@@ -140,11 +140,39 @@ function stripPrivateTags(str: string): string {
|
|
|
140
140
|
return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim()
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Log errors and warnings to OpenCode's TUI log.
|
|
145
|
+
* Errors in hooks must not propagate to the plugin surface — they are silently
|
|
146
|
+
* swallowed here so the TUI does not crash. The error is visible only in OpenCode's
|
|
147
|
+
* log stream (visible to the user via `opencode logs`).
|
|
148
|
+
*/
|
|
149
|
+
async function logToOpenCode(client: unknown, level: string, message: string, extra?: unknown): Promise<void> {
|
|
150
|
+
if (typeof client !== "object" || client === null) return;
|
|
151
|
+
const app = (client as { app?: unknown }).app;
|
|
152
|
+
if (typeof app !== "object" || app === null) return;
|
|
153
|
+
const log = (app as { log?: unknown }).log;
|
|
154
|
+
if (typeof log !== "function") return;
|
|
155
|
+
const safeExtra = extra instanceof Error ? { message: extra.message, stack: extra.stack } : extra;
|
|
156
|
+
try {
|
|
157
|
+
await log.call(app, {
|
|
158
|
+
body: {
|
|
159
|
+
service: "engram",
|
|
160
|
+
level,
|
|
161
|
+
message,
|
|
162
|
+
extra: safeExtra,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
// Logging must never break the plugin path.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
// ─── Plugin Export ───────────────────────────────────────────────────────────
|
|
144
171
|
|
|
145
172
|
export const Engram: Plugin = async (ctx) => {
|
|
146
173
|
const oldProject = ctx.directory.split(/[\\/]/).pop() ?? "unknown"
|
|
147
174
|
const project = extractProjectName(ctx.directory)
|
|
175
|
+
const client = (ctx as { client?: unknown }).client
|
|
148
176
|
const engramBin = resolveEngramBin()
|
|
149
177
|
|
|
150
178
|
// Track tool counts per session (in-memory only, not critical)
|
|
@@ -228,10 +256,21 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
228
256
|
// Manifest doesn't exist or binary not found — silently skip
|
|
229
257
|
}
|
|
230
258
|
|
|
259
|
+
// ─── Safe Hook Wrapper ─────────────────────────────────────────
|
|
260
|
+
// Wraps every hook to catch errors and log them to OpenCode instead
|
|
261
|
+
// of letting them propagate. This prevents malformed payloads or
|
|
262
|
+
// missing fields (output.context, output.system, etc.) from crashing
|
|
263
|
+
// the TUI. Errors are logged and swallowed (promise resolves cleanly).
|
|
264
|
+
const safe = <A extends unknown[]>(name: string, fn: (...args: A) => Promise<void>) =>
|
|
265
|
+
async (...args: A): Promise<void> => {
|
|
266
|
+
try { await fn(...args) }
|
|
267
|
+
catch (error) { void logToOpenCode(client, "error", `Engram hook ${name} failed`, error) }
|
|
268
|
+
}
|
|
269
|
+
|
|
231
270
|
return {
|
|
232
271
|
// ─── Event Listeners ───────────────────────────────────────────
|
|
233
272
|
|
|
234
|
-
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
273
|
+
event: safe("event", async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
235
274
|
// --- Session Created ---
|
|
236
275
|
if (event.type === "session.created") {
|
|
237
276
|
// Bug fix (#116): session data is nested under event.properties.info,
|
|
@@ -271,7 +310,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
271
310
|
}
|
|
272
311
|
}
|
|
273
312
|
|
|
274
|
-
},
|
|
313
|
+
}),
|
|
275
314
|
|
|
276
315
|
// ─── User Prompt Capture ──────────────────────────────────────
|
|
277
316
|
// chat.message is called once per user message, before the LLM sees it.
|
|
@@ -279,7 +318,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
279
318
|
// output.message is typed as UserMessage (role:"user" already guaranteed).
|
|
280
319
|
// output.parts contains TextPart[] with the actual message text.
|
|
281
320
|
|
|
282
|
-
"chat.message": async (input: any, output: any) => {
|
|
321
|
+
"chat.message": safe("chat.message", async (input: any, output: any) => {
|
|
283
322
|
// Skip sub-agent sessions — they inflate session counts (issue #116)
|
|
284
323
|
if (subAgentSessions.has(input.sessionID)) return
|
|
285
324
|
|
|
@@ -313,7 +352,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
313
352
|
})
|
|
314
353
|
}
|
|
315
354
|
}
|
|
316
|
-
},
|
|
355
|
+
}),
|
|
317
356
|
|
|
318
357
|
// ─── Tool Execution Hook ─────────────────────────────────────
|
|
319
358
|
// Count tool calls per session (for session end stats).
|
|
@@ -321,7 +360,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
321
360
|
// Passive capture: when a Task tool completes, POST its output to
|
|
322
361
|
// the passive capture endpoint so the server extracts learnings.
|
|
323
362
|
|
|
324
|
-
"tool.execute.after": async (input: any, output: any) => {
|
|
363
|
+
"tool.execute.after": safe("tool.execute.after", async (input: any, output: any) => {
|
|
325
364
|
if (ENGRAM_TOOLS.has(input.tool.toLowerCase())) return
|
|
326
365
|
|
|
327
366
|
// input.sessionID comes from OpenCode — always available
|
|
@@ -347,7 +386,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
347
386
|
})
|
|
348
387
|
}
|
|
349
388
|
}
|
|
350
|
-
},
|
|
389
|
+
}),
|
|
351
390
|
|
|
352
391
|
// ─── System Prompt: Always-on memory instructions ──────────
|
|
353
392
|
// Injects MEMORY_INSTRUCTIONS into the system prompt of every message.
|
|
@@ -359,13 +398,13 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
359
398
|
// block at the beginning. By concatenating, we avoid adding extra system
|
|
360
399
|
// messages that would break these models. See: GitHub issue #23.
|
|
361
400
|
|
|
362
|
-
"experimental.chat.system.transform": async (_input: any, output: any) => {
|
|
401
|
+
"experimental.chat.system.transform": safe("experimental.chat.system.transform", async (_input: any, output: any) => {
|
|
363
402
|
if (output.system.length > 0) {
|
|
364
403
|
output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
|
|
365
404
|
} else {
|
|
366
405
|
output.system.push(MEMORY_INSTRUCTIONS)
|
|
367
406
|
}
|
|
368
|
-
},
|
|
407
|
+
}),
|
|
369
408
|
|
|
370
409
|
// ─── Compaction Hook: Persist memory + inject context ──────────
|
|
371
410
|
// Compaction is triggered by the system (not the agent) when context
|
|
@@ -375,7 +414,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
375
414
|
// 2. Inject context from previous sessions into the compaction prompt
|
|
376
415
|
// 3. Tell the compressor to remind the new agent to save memories
|
|
377
416
|
|
|
378
|
-
"experimental.session.compacting": async (input: any, output: any) => {
|
|
417
|
+
"experimental.session.compacting": safe("experimental.session.compacting", async (input: any, output: any) => {
|
|
379
418
|
if (input.sessionID) {
|
|
380
419
|
await ensureSession(input.sessionID)
|
|
381
420
|
}
|
|
@@ -400,6 +439,6 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
400
439
|
`Do this BEFORE any other work. After that, delegate to the global engram subagent to recover only the relevant previous context."\n\n` +
|
|
401
440
|
`This is NOT optional. Without this, everything done before compaction is lost from memory.`
|
|
402
441
|
)
|
|
403
|
-
},
|
|
442
|
+
}),
|
|
404
443
|
}
|
|
405
444
|
}
|
|
@@ -44,16 +44,31 @@ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGo
|
|
|
44
44
|
project: deps.project,
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
// ─── Safe Hook Wrapper ─────────────────────────────────────────
|
|
48
|
+
// Wraps every hook to catch errors and log them via the logger instead
|
|
49
|
+
// of letting them propagate. This prevents OpenCode from crashing when
|
|
50
|
+
// Goal Mode hooks encounter malformed payloads or internal errors.
|
|
51
|
+
// Errors are logged (if a logger is provided) and swallowed (promise
|
|
52
|
+
// resolves cleanly even on failure).
|
|
53
|
+
const safe = <A extends unknown[]>(name: string, fn: (...args: A) => Promise<void>) =>
|
|
54
|
+
async (...args: A): Promise<void> => {
|
|
55
|
+
try {
|
|
56
|
+
await fn(...args);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
try { deps.logger?.error?.(`Goal Mode hook ${name} failed`, error); } catch {}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
47
62
|
return {
|
|
48
|
-
"command.execute.before": async (input, output) => {
|
|
63
|
+
"command.execute.before": safe("command.execute.before", async (input, output) => {
|
|
49
64
|
const command = extractCommandName(input);
|
|
50
65
|
if (command !== "goal") return;
|
|
51
66
|
|
|
52
67
|
const response = commands.handleGoalCommand(extractCommandArguments(input));
|
|
53
68
|
replaceGoalCommandPrompt(input, output, response.message);
|
|
54
|
-
},
|
|
69
|
+
}),
|
|
55
70
|
|
|
56
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
71
|
+
"experimental.chat.system.transform": safe("experimental.chat.system.transform", async (_input, output) => {
|
|
57
72
|
const block = supervisor.renderSystemContext();
|
|
58
73
|
if (!block) return;
|
|
59
74
|
|
|
@@ -64,18 +79,18 @@ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGo
|
|
|
64
79
|
|
|
65
80
|
const lastIndex = output.system.length - 1;
|
|
66
81
|
output.system[lastIndex] = upsertMarkedBlock(output.system[lastIndex]!, block);
|
|
67
|
-
},
|
|
82
|
+
}),
|
|
68
83
|
|
|
69
|
-
"experimental.session.compacting": async (_input, output) => {
|
|
84
|
+
"experimental.session.compacting": safe("experimental.session.compacting", async (_input, output) => {
|
|
70
85
|
const block = supervisor.renderSystemContext();
|
|
71
86
|
if (!block) return;
|
|
72
87
|
|
|
73
88
|
if (!output.context.some((entry) => entry.includes(GOAL_MODE_MARKER_START))) {
|
|
74
89
|
output.context.push(block);
|
|
75
90
|
}
|
|
76
|
-
},
|
|
91
|
+
}),
|
|
77
92
|
|
|
78
|
-
event: async ({ event }) => {
|
|
93
|
+
event: safe("event", async ({ event }) => {
|
|
79
94
|
if (event.type !== "session.idle") return;
|
|
80
95
|
|
|
81
96
|
const decision = supervisor.decide();
|
|
@@ -147,7 +162,7 @@ export function createOpenCodeGoalHooks(deps: OpenCodeGoalHooksDeps): OpenCodeGo
|
|
|
147
162
|
} finally {
|
|
148
163
|
autoContinueInFlight.delete(dedupeKey);
|
|
149
164
|
}
|
|
150
|
-
},
|
|
165
|
+
}),
|
|
151
166
|
};
|
|
152
167
|
}
|
|
153
168
|
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { createGoalStore } from "./goal/store.js";
|
|
7
|
-
import { createOpenCodeGoalHooks } from "./goal/opencode-hooks.js";
|
|
7
|
+
import { createOpenCodeGoalHooks, type OpenCodeGoalHooks } from "./goal/opencode-hooks.js";
|
|
8
8
|
|
|
9
9
|
interface GoalPluginLogger {
|
|
10
10
|
warn?: (message: string, details?: unknown) => void;
|
|
@@ -51,11 +51,12 @@ function parseRemoteProjectKey(remote: string): string | undefined {
|
|
|
51
51
|
return genericMatch?.[1];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export const GoalModePlugin = async (ctx: { directory: string; client?: unknown }) => {
|
|
54
|
+
export const GoalModePlugin = async (ctx: { directory: string; client?: unknown }): Promise<OpenCodeGoalHooks> => {
|
|
55
55
|
const logger = createGoalPluginLogger(ctx.client);
|
|
56
|
-
|
|
57
|
-
const store = createGoalStore({ databasePath });
|
|
56
|
+
let store: ReturnType<typeof createGoalStore> | undefined;
|
|
58
57
|
try {
|
|
58
|
+
const databasePath = resolveGoalDatabasePath(process.env.JORGEX_GOAL_DB);
|
|
59
|
+
store = createGoalStore({ databasePath });
|
|
59
60
|
store.migrate();
|
|
60
61
|
const project = resolveGoalProjectName(ctx.directory, logger);
|
|
61
62
|
|
|
@@ -67,20 +68,26 @@ export const GoalModePlugin = async (ctx: { directory: string; client?: unknown
|
|
|
67
68
|
logger,
|
|
68
69
|
});
|
|
69
70
|
} catch (error) {
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// If Goal Mode initialization fails (e.g., JORGEX_GOAL_DB points outside
|
|
72
|
+
// the allowed directory), log the error and return empty hooks. This
|
|
73
|
+
// prevents Goal Mode from crashing the entire plugin layer.
|
|
74
|
+
logger.error?.("Goal Mode failed to initialize; Goal hooks disabled.", error);
|
|
75
|
+
store?.close();
|
|
76
|
+
return {};
|
|
72
77
|
}
|
|
73
78
|
};
|
|
74
79
|
|
|
75
80
|
function createGoalPluginLogger(client: unknown): GoalPluginLogger {
|
|
76
81
|
return {
|
|
77
82
|
warn: (message, details) => {
|
|
83
|
+
// Log to OpenCode only; console.warn is omitted to avoid log pollution
|
|
84
|
+
// in user terminals. All logs go through OpenCode's log stream.
|
|
78
85
|
void logToOpenCode(client, "warn", message, details);
|
|
79
|
-
console.warn(message, details);
|
|
80
86
|
},
|
|
81
87
|
error: (message, details) => {
|
|
88
|
+
// Log to OpenCode only; console.error is omitted to avoid log pollution
|
|
89
|
+
// in user terminals. All logs go through OpenCode's log stream.
|
|
82
90
|
void logToOpenCode(client, "error", message, details);
|
|
83
|
-
console.error(message, details);
|
|
84
91
|
},
|
|
85
92
|
};
|
|
86
93
|
}
|
|
@@ -91,13 +98,14 @@ async function logToOpenCode(client: unknown, level: "warn" | "error", message:
|
|
|
91
98
|
if (typeof app !== "object" || app === null) return;
|
|
92
99
|
const log = (app as { log?: unknown }).log;
|
|
93
100
|
if (typeof log !== "function") return;
|
|
101
|
+
const safeExtra = details instanceof Error ? { message: details.message, stack: details.stack } : details;
|
|
94
102
|
try {
|
|
95
103
|
await log.call(app, {
|
|
96
104
|
body: {
|
|
97
105
|
service: "goal-mode",
|
|
98
106
|
level,
|
|
99
107
|
message,
|
|
100
|
-
extra:
|
|
108
|
+
extra: safeExtra,
|
|
101
109
|
},
|
|
102
110
|
});
|
|
103
111
|
} catch {
|
|
@@ -358,7 +358,8 @@ export const WorktreePlugin: Plugin = async ({ $, client, directory }) => {
|
|
|
358
358
|
|
|
359
359
|
const branchName = `${config.branchPrefix || "feature/"}${worktreeName}`;
|
|
360
360
|
|
|
361
|
-
|
|
361
|
+
// .quiet() suppresses stderr to prevent noisy TUI logs from git calls
|
|
362
|
+
const gitRoot = await $`git rev-parse --show-toplevel`.quiet().text();
|
|
362
363
|
const projectRoot = String(gitRoot).trim().replace(/\\/g, "/");
|
|
363
364
|
const commandCwd = getCommandCwd(args, directory);
|
|
364
365
|
const absoluteWorktreePath = resolvePath(commandCwd, parsedWorktreePath);
|