jeo-code 0.12.7 → 0.12.8
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/CHANGELOG.md +4 -0
- package/README.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +4 -2
- package/README.zh.md +2 -0
- package/package.json +1 -1
- package/src/agent/compaction.ts +6 -7
- package/src/agent/notify/discord-api.ts +125 -28
- package/src/agent/notify/discord-artifacts.ts +126 -0
- package/src/agent/notify/discord-attachments.ts +116 -0
- package/src/agent/notify/discord-daemon.ts +213 -39
- package/src/agent/notify/session-endpoint.ts +127 -5
- package/src/agent/session.ts +7 -5
- package/src/cli/runner.ts +9 -0
- package/src/cli.ts +4 -0
- package/src/commands/launch/agy-turn.ts +20 -7
- package/src/commands/launch/confirm.ts +78 -0
- package/src/commands/launch/flags.ts +7 -0
- package/src/commands/launch/input.ts +18 -1
- package/src/commands/launch.ts +320 -40
- package/src/commands/messenger.ts +146 -0
- package/src/commands/notify.ts +3 -0
- package/src/messenger/activity.ts +30 -0
- package/src/messenger/artifacts.ts +167 -0
- package/src/messenger/config.ts +145 -0
- package/src/messenger/discord-setup.ts +568 -0
- package/src/messenger/discord.ts +707 -0
- package/src/messenger/gateway.ts +444 -0
- package/src/messenger/http.ts +88 -0
- package/src/messenger/service.ts +122 -0
- package/src/messenger/session-pool.ts +109 -0
- package/src/messenger/slack.ts +333 -0
- package/src/messenger/telegram.ts +241 -0
- package/src/messenger/types.ts +82 -0
- package/src/tui/components/slash.ts +1 -1
|
@@ -9,13 +9,20 @@ export interface AgySessionTurnOptions extends Omit<AgyTurnOptions, "conversatio
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** An explicit full-agent turn: no raw-model loop, credential fallback, or tool replay. */
|
|
12
|
-
export async function runAgySessionTurn(options: AgySessionTurnOptions
|
|
12
|
+
export async function runAgySessionTurn(options: AgySessionTurnOptions, deps: {
|
|
13
|
+
loadSession?: typeof loadSession;
|
|
14
|
+
appendMessage?: typeof appendMessage;
|
|
15
|
+
appendAgySession?: typeof appendAgySession;
|
|
16
|
+
runAgyTurn?: typeof runAgyTurn;
|
|
17
|
+
} = {}): Promise<AgyTurnResult> {
|
|
13
18
|
const { history, sessionId, images, ...turn } = options;
|
|
19
|
+
const { loadSession: load = loadSession, appendMessage: append = appendMessage, appendAgySession: appendAgy = appendAgySession, runAgyTurn: run = runAgyTurn } = deps;
|
|
14
20
|
if (images?.length) throw new Error("Image attachments are not supported by the official agy bridge");
|
|
15
21
|
if (turn.signal?.aborted) throw new AgyCliError("Official CLI turn cancelled", "cancelled");
|
|
16
22
|
|
|
17
23
|
const previousCount = history.reduce((count, message) => count + (message.role === "system" ? 0 : 1), 0);
|
|
18
|
-
const state = sessionId ? (await
|
|
24
|
+
const state = sessionId ? (await load(sessionId, turn.cwd)).header.agy : undefined;
|
|
25
|
+
turn.signal?.throwIfAborted();
|
|
19
26
|
// A non-CLI turn or compaction changes the cursor. Start fresh with text context
|
|
20
27
|
// rather than resume a stale agent conversation or agy's unrelated latest one.
|
|
21
28
|
let conversationId = state?.messageCount === previousCount ? state.conversationId : undefined;
|
|
@@ -33,26 +40,32 @@ export async function runAgySessionTurn(options: AgySessionTurnOptions): Promise
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const user: Message = { role: "user", content: turn.prompt };
|
|
36
|
-
if (sessionId) await
|
|
43
|
+
if (sessionId) await append(sessionId, user, turn.cwd);
|
|
44
|
+
turn.signal?.throwIfAborted();
|
|
37
45
|
history.push(user);
|
|
38
|
-
const result = await
|
|
46
|
+
const result = await run({
|
|
39
47
|
...turn,
|
|
40
48
|
prompt,
|
|
41
49
|
conversationId,
|
|
42
50
|
onConversation: async id => {
|
|
51
|
+
turn.signal?.throwIfAborted();
|
|
43
52
|
conversationId = id;
|
|
44
|
-
if (sessionId) await
|
|
53
|
+
if (sessionId) await appendAgy(sessionId, { conversationId: id, messageCount: previousCount + 1 }, turn.cwd);
|
|
54
|
+
turn.signal?.throwIfAborted();
|
|
45
55
|
},
|
|
46
56
|
});
|
|
57
|
+
turn.signal?.throwIfAborted();
|
|
47
58
|
|
|
48
59
|
const assistant: Message = { role: "assistant", content: result.response };
|
|
49
60
|
try {
|
|
50
|
-
if (sessionId) await
|
|
61
|
+
if (sessionId) await append(sessionId, assistant, turn.cwd);
|
|
62
|
+
turn.signal?.throwIfAborted();
|
|
51
63
|
history.push(assistant);
|
|
52
|
-
if (sessionId) await
|
|
64
|
+
if (sessionId) await appendAgy(sessionId, {
|
|
53
65
|
conversationId: result.conversationId,
|
|
54
66
|
messageCount: previousCount + 2,
|
|
55
67
|
}, turn.cwd);
|
|
68
|
+
turn.signal?.throwIfAborted();
|
|
56
69
|
} catch (error) {
|
|
57
70
|
throw new Error(`Official CLI completed, but saving its jeo session failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
58
71
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Confirm a sensitive action on an isolated local TTY; all other input denies. */
|
|
2
|
+
export async function confirmLocalControl({
|
|
3
|
+
input,
|
|
4
|
+
output,
|
|
5
|
+
timeoutMs = 15_000,
|
|
6
|
+
warning = "Discord control grants authorized users the host session's tools and approval policy, including reading or changing files and running commands. This is not a sandbox.",
|
|
7
|
+
prompt = "Confirm Discord control: press y to allow, any other key to cancel.",
|
|
8
|
+
}: {
|
|
9
|
+
input: NodeJS.ReadStream;
|
|
10
|
+
output: NodeJS.WriteStream;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
warning?: string;
|
|
13
|
+
prompt?: string;
|
|
14
|
+
}): Promise<boolean> {
|
|
15
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) return false;
|
|
16
|
+
const wasRaw = !!input.isRaw;
|
|
17
|
+
const wasPaused = input.isPaused();
|
|
18
|
+
const dataListeners = input.rawListeners("data");
|
|
19
|
+
const keyListeners = input.rawListeners("keypress");
|
|
20
|
+
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
21
|
+
let quiet: ReturnType<typeof setTimeout> | undefined;
|
|
22
|
+
let phase: "drain" | "ready" | "candidate" = "drain";
|
|
23
|
+
let finished = false;
|
|
24
|
+
let resolveResult: (value: boolean) => void = () => {};
|
|
25
|
+
const result = new Promise<boolean>(resolve => { resolveResult = resolve; });
|
|
26
|
+
const finish = (accepted: boolean) => {
|
|
27
|
+
if (finished) return;
|
|
28
|
+
finished = true;
|
|
29
|
+
resolveResult(accepted);
|
|
30
|
+
};
|
|
31
|
+
const startQuietDrain = () => {
|
|
32
|
+
clearTimeout(quiet);
|
|
33
|
+
quiet = setTimeout(() => {
|
|
34
|
+
if (finished) return;
|
|
35
|
+
phase = "ready";
|
|
36
|
+
try { output.write(`${prompt}\n`); }
|
|
37
|
+
catch { finish(false); }
|
|
38
|
+
}, 200);
|
|
39
|
+
};
|
|
40
|
+
const onData = (chunk: string | Uint8Array) => {
|
|
41
|
+
if (phase === "drain") { startQuietDrain(); return; }
|
|
42
|
+
if (phase !== "ready" || (typeof chunk === "string" ? chunk !== "y" : chunk.length !== 1 || chunk[0] !== 0x79)) { finish(false); return; }
|
|
43
|
+
phase = "candidate";
|
|
44
|
+
quiet = setTimeout(() => finish(true), 200);
|
|
45
|
+
};
|
|
46
|
+
const onEnd = () => finish(false);
|
|
47
|
+
try {
|
|
48
|
+
input.pause();
|
|
49
|
+
for (const listener of dataListeners) input.removeListener("data", listener);
|
|
50
|
+
for (const listener of keyListeners) input.removeListener("keypress", listener);
|
|
51
|
+
input.setRawMode(true);
|
|
52
|
+
output.write(`${warning}\n`);
|
|
53
|
+
while (input.read() !== null) { /* discard input queued before the warning */ }
|
|
54
|
+
input.on("data", onData);
|
|
55
|
+
input.once("end", onEnd);
|
|
56
|
+
input.once("close", onEnd);
|
|
57
|
+
input.once("error", onEnd);
|
|
58
|
+
deadline = setTimeout(() => finish(false), timeoutMs);
|
|
59
|
+
startQuietDrain();
|
|
60
|
+
input.resume();
|
|
61
|
+
return await result;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(deadline);
|
|
66
|
+
clearTimeout(quiet);
|
|
67
|
+
input.pause();
|
|
68
|
+
input.removeListener("data", onData);
|
|
69
|
+
input.removeListener("end", onEnd);
|
|
70
|
+
input.removeListener("close", onEnd);
|
|
71
|
+
input.removeListener("error", onEnd);
|
|
72
|
+
for (const listener of dataListeners) input.on("data", listener);
|
|
73
|
+
for (const listener of keyListeners) input.on("keypress", listener);
|
|
74
|
+
try { input.setRawMode(wasRaw); } catch {}
|
|
75
|
+
if (wasPaused) input.pause();
|
|
76
|
+
else input.resume();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -23,6 +23,8 @@ export interface LaunchFlags {
|
|
|
23
23
|
thinking?: ThinkLevel;
|
|
24
24
|
errors: string[];
|
|
25
25
|
print?: boolean;
|
|
26
|
+
literalPrompt?: boolean;
|
|
27
|
+
literalWorker?: boolean;
|
|
26
28
|
appendSystemPromptRaw?: string;
|
|
27
29
|
appendSystemPrompt?: string;
|
|
28
30
|
noSkills: boolean;
|
|
@@ -160,6 +162,11 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
|
|
|
160
162
|
if (value) flags.skills = value;
|
|
161
163
|
else flags.errors.push("--skills requires a value");
|
|
162
164
|
i = nextIndex;
|
|
165
|
+
} else if (a === "--literal-prompt") {
|
|
166
|
+
flags.literalPrompt = true;
|
|
167
|
+
} else if (a === "--literal-worker") {
|
|
168
|
+
flags.literalWorker = true;
|
|
169
|
+
flags.literalPrompt = true;
|
|
163
170
|
} else if (a === "--no-tools") {
|
|
164
171
|
flags.noTools = true;
|
|
165
172
|
} else if (a === "--tools" || a.startsWith("--tools=")) {
|
|
@@ -34,6 +34,9 @@ interface AbortHarnessOptions {
|
|
|
34
34
|
onScrollKey?: (dir: -1 | 1, page: boolean) => void;
|
|
35
35
|
/** Invoked with printable keyboard input received while the live turn owns stdin. */
|
|
36
36
|
onBufferedInput?: (chunk: string) => void;
|
|
37
|
+
/** Emergency-only input while an aborted turn is still unwinding. */
|
|
38
|
+
onBufferedInputAfterAbort?: (chunk: string) => void;
|
|
39
|
+
pasteActiveAfterAbort?: () => boolean;
|
|
37
40
|
/** True while the input queue is inside a bracketed paste (mid-paste chunks
|
|
38
41
|
* carry no marker and must keep routing to the queue, not the noise path). */
|
|
39
42
|
pasteActive?: () => boolean;
|
|
@@ -46,6 +49,15 @@ interface AbortHarnessOptions {
|
|
|
46
49
|
export const PASTE_START = "\u001b[200~";
|
|
47
50
|
export const PASTE_END = "\u001b[201~";
|
|
48
51
|
|
|
52
|
+
/** Conservative paste signal; one-character chunks remain indistinguishable from typing. */
|
|
53
|
+
export function isPasteLikeInputChunk(chunk: string): boolean {
|
|
54
|
+
let printable = 0;
|
|
55
|
+
for (const ch of stripPasteEscapes(chunk)) {
|
|
56
|
+
if ((ch >= " " || ch === "\t") && ++printable > 1) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
/** True when a stdin chunk is ONLY backspace bytes (DEL 0x7f or BS 0x08) — i.e. a
|
|
50
62
|
* standalone Backspace keystroke with nothing else. A backspace on an EMPTY input
|
|
51
63
|
* line is a no-op edit, but some Bun readline builds turn it into a spurious `close`
|
|
@@ -483,8 +495,13 @@ export function createInFlightAbortHarness(opts: AbortHarnessOptions = {}): InFl
|
|
|
483
495
|
};
|
|
484
496
|
|
|
485
497
|
const handleData = (chunk: string | Uint8Array) => {
|
|
486
|
-
if (!captureEsc
|
|
498
|
+
if (!captureEsc) return;
|
|
487
499
|
const raw = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
500
|
+
if (controller.signal.aborted) {
|
|
501
|
+
if (!raw.includes(PASTE_START) && !opts.pasteActiveAfterAbort?.() && chunkHasCtrlC(raw)) handleSigint();
|
|
502
|
+
else opts.onBufferedInputAfterAbort?.(raw);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
488
505
|
if (raw.includes(PASTE_START) || opts.pasteActive?.()) {
|
|
489
506
|
opts.onBufferedInput?.(raw);
|
|
490
507
|
return;
|