jeo-code 0.12.10 → 0.12.12
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 +12 -0
- package/README.ja.md +17 -12
- package/README.ko.md +86 -27
- package/README.md +76 -20
- package/README.zh.md +17 -12
- package/package.json +2 -1
- package/src/agent/ast-edit-tool.ts +3 -1
- package/src/agent/engine.ts +114 -16
- package/src/agent/goal-tool.ts +7 -1
- package/src/agent/goal-verifier.ts +95 -16
- package/src/agent/loop-guards.ts +10 -9
- package/src/agent/lsp-rename-tool.ts +3 -1
- package/src/agent/notify/discord-daemon.ts +3 -1
- package/src/agent/notify/paths.ts +8 -0
- package/src/agent/notify/session-endpoint.ts +23 -4
- package/src/agent/notify/slack-daemon.ts +2 -2
- package/src/agent/notify/telegram-daemon.ts +223 -89
- package/src/agent/notify/topic-registry.ts +118 -15
- package/src/agent/skill-router.ts +31 -5
- package/src/agent/subagent-registry.ts +87 -5
- package/src/agent/subagent-tool.ts +133 -18
- package/src/agent/subagents.ts +7 -5
- package/src/agent/task-tool.ts +85 -14
- package/src/agent/tools.ts +14 -4
- package/src/cli.ts +1 -0
- package/src/commands/launch.ts +145 -17
- package/src/commands/messenger.ts +5 -1
- package/src/commands/notify.ts +5 -6
- package/src/commands/skills.ts +9 -0
- package/src/commands/team.ts +72 -53
- package/src/messenger/activity.ts +13 -17
- package/src/messenger/config.ts +6 -2
- package/src/messenger/discord-setup.ts +3 -1
- package/src/messenger/discord.ts +16 -19
- package/src/messenger/gateway.ts +7 -8
- package/src/messenger/session-pool.ts +99 -13
- package/src/messenger/types.ts +4 -5
- package/src/skills/catalog.ts +19 -2
|
@@ -5,13 +5,15 @@ interface WorkerProcess {
|
|
|
5
5
|
stdout: ReadableStream<Uint8Array>;
|
|
6
6
|
stderr: ReadableStream<Uint8Array>;
|
|
7
7
|
exited: Promise<number>;
|
|
8
|
-
kill(): void;
|
|
8
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
interface Pending {
|
|
12
12
|
resolve(text: string): void;
|
|
13
13
|
reject(error: Error): void;
|
|
14
14
|
report?: MessengerActivityReporter;
|
|
15
|
+
result?: string;
|
|
16
|
+
written?: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
interface Worker {
|
|
@@ -19,20 +21,55 @@ interface Worker {
|
|
|
19
21
|
pending?: Pending;
|
|
20
22
|
lastUsed: number;
|
|
21
23
|
dead: boolean;
|
|
24
|
+
exited?: boolean;
|
|
25
|
+
idleTimer?: ReturnType<typeof setTimeout>;
|
|
26
|
+
exitTimer?: ReturnType<typeof setTimeout>;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
export function createMessengerSessionPool(spawn: typeof Bun.spawn = Bun.spawn, limit = 16) {
|
|
29
|
+
export function createMessengerSessionPool(spawn: typeof Bun.spawn = Bun.spawn, limit = 16, idleTimeoutMs = 5 * 60_000, shutdownTimeoutMs = 5_000) {
|
|
25
30
|
const workers = new Map<string, Worker>();
|
|
31
|
+
const allWorkers = new Map<Worker, string>();
|
|
32
|
+
const exits = new Map<Promise<number>, WorkerProcess>();
|
|
33
|
+
const retiring = new Map<string, Promise<number>>();
|
|
34
|
+
|
|
26
35
|
let closed = false;
|
|
36
|
+
const closedSignal = new AbortController();
|
|
37
|
+
|
|
27
38
|
let tick = 0;
|
|
28
39
|
|
|
29
40
|
function retire(key: string, worker: Worker) {
|
|
30
41
|
if (worker.dead) return;
|
|
31
42
|
worker.dead = true;
|
|
32
43
|
if (workers.get(key) === worker) workers.delete(key);
|
|
44
|
+
allWorkers.delete(worker);
|
|
45
|
+
if (!worker.exited) {
|
|
46
|
+
retiring.set(key, worker.child.exited);
|
|
47
|
+
void worker.child.exited.then(
|
|
48
|
+
() => { if (retiring.get(key) === worker.child.exited) retiring.delete(key); },
|
|
49
|
+
() => { if (retiring.get(key) === worker.child.exited) retiring.delete(key); },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (worker.idleTimer) clearTimeout(worker.idleTimer);
|
|
53
|
+
if (worker.exitTimer) clearTimeout(worker.exitTimer);
|
|
54
|
+
worker.idleTimer = undefined;
|
|
33
55
|
worker.pending?.reject(new Error("Messenger worker stopped"));
|
|
34
56
|
worker.pending = undefined;
|
|
35
|
-
worker.
|
|
57
|
+
if (!worker.exited) {
|
|
58
|
+
try { worker.child.kill(); } catch { /* Shutdown will attempt forced termination. */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function finishTurn(key: string, worker: Worker, pending: Pending) {
|
|
63
|
+
if (worker.pending !== pending || !pending.written || pending.result === undefined) return;
|
|
64
|
+
worker.pending = undefined;
|
|
65
|
+
if (worker.exited) {
|
|
66
|
+
retire(key, worker);
|
|
67
|
+
} else {
|
|
68
|
+
worker.lastUsed = ++tick;
|
|
69
|
+
worker.idleTimer = setTimeout(() => retire(key, worker), idleTimeoutMs);
|
|
70
|
+
worker.idleTimer.unref();
|
|
71
|
+
}
|
|
72
|
+
pending.resolve(pending.result);
|
|
36
73
|
}
|
|
37
74
|
|
|
38
75
|
async function readLines(stream: ReadableStream<Uint8Array>, handle: (line: string) => void) {
|
|
@@ -60,8 +97,11 @@ export function createMessengerSessionPool(spawn: typeof Bun.spawn = Bun.spawn,
|
|
|
60
97
|
}
|
|
61
98
|
const child = spawn(command, { cwd, env, stdin: "pipe", stdout: "pipe", stderr: "pipe" }) as unknown as WorkerProcess;
|
|
62
99
|
const worker: Worker = { child, lastUsed: ++tick, dead: false };
|
|
100
|
+
exits.set(child.exited, child);
|
|
101
|
+
void child.exited.then(() => exits.delete(child.exited), () => exits.delete(child.exited));
|
|
63
102
|
workers.set(key, worker);
|
|
64
|
-
|
|
103
|
+
allWorkers.set(worker, key);
|
|
104
|
+
const stdoutDone = readLines(child.stdout, line => {
|
|
65
105
|
const pending = worker.pending;
|
|
66
106
|
if (!pending) return;
|
|
67
107
|
if (line === "JEO_MESSENGER_ERROR") { retire(key, worker); return; }
|
|
@@ -69,40 +109,86 @@ export function createMessengerSessionPool(spawn: typeof Bun.spawn = Bun.spawn,
|
|
|
69
109
|
try {
|
|
70
110
|
const text: unknown = JSON.parse(line.slice("JEO_MESSENGER_RESULT ".length));
|
|
71
111
|
if (typeof text !== "string" || !text.trim()) throw new Error("Invalid response");
|
|
72
|
-
|
|
73
|
-
worker
|
|
74
|
-
pending.resolve(text);
|
|
112
|
+
pending.result = text;
|
|
113
|
+
finishTurn(key, worker, pending);
|
|
75
114
|
} catch { retire(key, worker); }
|
|
76
115
|
}).catch(() => retire(key, worker));
|
|
77
116
|
void readLines(child.stderr, line => {
|
|
78
|
-
if (line
|
|
117
|
+
if (line.startsWith("JEO_MESSENGER_ACTIVITY ")) {
|
|
118
|
+
const activityText = line.slice("JEO_MESSENGER_ACTIVITY ".length).trim();
|
|
119
|
+
if (activityText) worker.pending?.report?.(activityText);
|
|
120
|
+
} else if (line === "JEO_MESSENGER_STEP") {
|
|
121
|
+
worker.pending?.report?.("Working on your request");
|
|
122
|
+
}
|
|
79
123
|
}).catch(() => retire(key, worker));
|
|
80
|
-
void child.exited.then(() =>
|
|
124
|
+
void child.exited.then(() => {
|
|
125
|
+
if (worker.dead) return;
|
|
126
|
+
worker.exited = true;
|
|
127
|
+
if (workers.get(key) === worker) workers.delete(key);
|
|
128
|
+
worker.exitTimer = setTimeout(() => retire(key, worker), shutdownTimeoutMs);
|
|
129
|
+
worker.exitTimer.unref();
|
|
130
|
+
void stdoutDone.then(() => {
|
|
131
|
+
if (!worker.pending?.result || worker.pending.written) retire(key, worker);
|
|
132
|
+
});
|
|
133
|
+
}, () => retire(key, worker));
|
|
81
134
|
return worker;
|
|
82
135
|
}
|
|
83
136
|
|
|
84
137
|
async function turn(key: string, command: string[], cwd: string, env: Record<string, string>, text: string, signal: AbortSignal, report?: MessengerActivityReporter): Promise<string> {
|
|
85
138
|
if (closed || signal.aborted) throw new Error("Messenger request aborted");
|
|
139
|
+
const previousExit = retiring.get(key);
|
|
140
|
+
if (previousExit) {
|
|
141
|
+
await new Promise<void>((resolve, reject) => {
|
|
142
|
+
const abort = () => { cleanup(); reject(new Error("Messenger request aborted")); };
|
|
143
|
+
const cleanup = () => { signal.removeEventListener("abort", abort); closedSignal.signal.removeEventListener("abort", abort); };
|
|
144
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
145
|
+
closedSignal.signal.addEventListener("abort", abort, { once: true });
|
|
146
|
+
if (signal.aborted || closed) { abort(); return; }
|
|
147
|
+
void previousExit.then(() => { cleanup(); resolve(); }, () => { cleanup(); resolve(); });
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (closed || signal.aborted) throw new Error("Messenger request aborted");
|
|
151
|
+
if ([...allWorkers].some(([worker, workerKey]) => workerKey === key && worker.pending)) {
|
|
152
|
+
throw new Error("Messenger conversation is already active");
|
|
153
|
+
}
|
|
86
154
|
const worker = workers.get(key) ?? start(key, command, cwd, env);
|
|
87
155
|
if (worker.pending) throw new Error("Messenger conversation is already active");
|
|
156
|
+
if (worker.idleTimer) clearTimeout(worker.idleTimer);
|
|
157
|
+
worker.idleTimer = undefined;
|
|
88
158
|
return await new Promise<string>((resolve, reject) => {
|
|
89
159
|
const abort = () => retire(key, worker);
|
|
90
160
|
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
91
|
-
|
|
161
|
+
const pending: Pending = { resolve: value => { cleanup(); resolve(value); }, reject: error => { cleanup(); reject(error); }, report };
|
|
162
|
+
worker.pending = pending;
|
|
92
163
|
signal.addEventListener("abort", abort, { once: true });
|
|
93
164
|
if (signal.aborted) { abort(); return; }
|
|
94
165
|
void (async () => {
|
|
95
166
|
await worker.child.stdin.write(`${JSON.stringify(text)}\n`);
|
|
96
167
|
if (!worker.dead) await worker.child.stdin.flush();
|
|
168
|
+
pending.written = true;
|
|
169
|
+
finishTurn(key, worker, pending);
|
|
97
170
|
})().catch(() => retire(key, worker));
|
|
98
171
|
});
|
|
99
172
|
}
|
|
100
173
|
|
|
101
174
|
async function close() {
|
|
102
175
|
closed = true;
|
|
103
|
-
|
|
104
|
-
for (const [
|
|
105
|
-
|
|
176
|
+
closedSignal.abort();
|
|
177
|
+
for (const [worker, key] of allWorkers) retire(key, worker);
|
|
178
|
+
const waitForExits = async () => {
|
|
179
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
180
|
+
try {
|
|
181
|
+
return await Promise.race([
|
|
182
|
+
Promise.allSettled([...exits.keys()]).then(() => true),
|
|
183
|
+
new Promise<false>(resolve => { timer = setTimeout(() => resolve(false), shutdownTimeoutMs); }),
|
|
184
|
+
]);
|
|
185
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
186
|
+
};
|
|
187
|
+
if (await waitForExits()) return;
|
|
188
|
+
for (const child of exits.values()) {
|
|
189
|
+
try { child.kill("SIGKILL"); } catch { /* The process may have exited concurrently. */ }
|
|
190
|
+
}
|
|
191
|
+
if (!(await waitForExits())) throw new Error("Messenger workers did not exit after forced termination");
|
|
106
192
|
}
|
|
107
193
|
|
|
108
194
|
return { turn, close };
|
package/src/messenger/types.ts
CHANGED
|
@@ -31,11 +31,8 @@ export interface MessengerAdapter {
|
|
|
31
31
|
status: "processing" | "success" | "failure",
|
|
32
32
|
signal?: AbortSignal,
|
|
33
33
|
): Promise<void>;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
* later calls; `undefined` removes it. The gateway serializes and throttles calls per request.
|
|
37
|
-
*/
|
|
38
|
-
activity?(message: MessengerMessage, text: string | undefined, signal?: AbortSignal): Promise<void>;
|
|
34
|
+
/** Update the request's progress; undefined finishes it with the supplied outcome. */
|
|
35
|
+
activity?(message: MessengerMessage, text: string | undefined, signal?: AbortSignal, outcome?: "success" | "failure" | "cancelled"): Promise<void>;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
export interface MessengerPlatformConfig {
|
|
@@ -55,6 +52,8 @@ export interface MessengerConfig {
|
|
|
55
52
|
sessionDir: string;
|
|
56
53
|
toolNames: string[];
|
|
57
54
|
platforms: Partial<Record<MessengerPlatform, MessengerPlatformConfig>>;
|
|
55
|
+
/** Whether each message invocation runs in an isolated independent session. Defaults to true. */
|
|
56
|
+
independentSessions?: boolean;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
export interface MessengerTransportDeps {
|
package/src/skills/catalog.ts
CHANGED
|
@@ -117,9 +117,9 @@ export function buildSkillTask(skill: SkillDoc, intent: string, invokedAs?: stri
|
|
|
117
117
|
: "Carry out this skill's workflow now. If it needs a concrete target you weren't given, make a reasonable assumption or ask the user via done."),
|
|
118
118
|
].join("\n");
|
|
119
119
|
}
|
|
120
|
-
export function skillsPromptSection(skills: SkillDoc[] = SKILLS): string {
|
|
120
|
+
export function skillsPromptSection(skills: SkillDoc[] = SKILLS, currentTask?: string): string {
|
|
121
121
|
const bundleSkills: SkillDoc[] = [];
|
|
122
|
-
|
|
122
|
+
let configuredSkills: SkillDoc[] = [];
|
|
123
123
|
|
|
124
124
|
const bundleNames = new Set(BUILTIN_SKILL_NAMES);
|
|
125
125
|
for (const s of skills) {
|
|
@@ -130,6 +130,20 @@ export function skillsPromptSection(skills: SkillDoc[] = SKILLS): string {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// When a task is provided and skills exceed budget, sort configured skills by relevance
|
|
134
|
+
if (currentTask && currentTask.trim().length > 0 && configuredSkills.length > 5) {
|
|
135
|
+
const qTokens = new Set(
|
|
136
|
+
currentTask.toLowerCase().match(/[a-z0-9._-]{3,}/g) ?? []
|
|
137
|
+
);
|
|
138
|
+
configuredSkills = [...configuredSkills].sort((a, b) => {
|
|
139
|
+
const aScore = (a.name.toLowerCase().split(/[._-]/).some(t => qTokens.has(t)) ? 10 : 0) +
|
|
140
|
+
(qTokens.has(a.name.toLowerCase()) ? 20 : 0);
|
|
141
|
+
const bScore = (b.name.toLowerCase().split(/[._-]/).some(t => qTokens.has(t)) ? 10 : 0) +
|
|
142
|
+
(qTokens.has(b.name.toLowerCase()) ? 20 : 0);
|
|
143
|
+
return bScore - aScore;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
const lines: string[] = [];
|
|
134
148
|
let used = 0;
|
|
135
149
|
|
|
@@ -295,15 +309,18 @@ export function skillDirs(cwd: string = process.cwd()): string[] {
|
|
|
295
309
|
path.join(userHome, ".claude", "skills"),
|
|
296
310
|
path.join(home, "agent", "skills"),
|
|
297
311
|
path.join(userHome, ".agents", "skills"),
|
|
312
|
+
path.join(userHome, ".agent-skills"),
|
|
298
313
|
userSkillsDir(),
|
|
299
314
|
path.join(cwd, ".claude", "skills"),
|
|
300
315
|
path.join(cwd, ".agents", "skills"),
|
|
316
|
+
path.join(cwd, ".agent-skills"),
|
|
301
317
|
path.join(cwd, ".jeo", "skills"),
|
|
302
318
|
path.join(cwd, ".jeo", "agent", "skills"),
|
|
303
319
|
...configured,
|
|
304
320
|
];
|
|
305
321
|
}
|
|
306
322
|
|
|
323
|
+
|
|
307
324
|
/** A frontmatter `name:` usable as a skill identity: one bare token, no spaces. */
|
|
308
325
|
function sanitizeSkillName(raw: string | undefined): string | undefined {
|
|
309
326
|
const m = (raw ?? "").trim();
|