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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.ja.md +17 -12
  3. package/README.ko.md +86 -27
  4. package/README.md +76 -20
  5. package/README.zh.md +17 -12
  6. package/package.json +2 -1
  7. package/src/agent/ast-edit-tool.ts +3 -1
  8. package/src/agent/engine.ts +114 -16
  9. package/src/agent/goal-tool.ts +7 -1
  10. package/src/agent/goal-verifier.ts +95 -16
  11. package/src/agent/loop-guards.ts +10 -9
  12. package/src/agent/lsp-rename-tool.ts +3 -1
  13. package/src/agent/notify/discord-daemon.ts +3 -1
  14. package/src/agent/notify/paths.ts +8 -0
  15. package/src/agent/notify/session-endpoint.ts +23 -4
  16. package/src/agent/notify/slack-daemon.ts +2 -2
  17. package/src/agent/notify/telegram-daemon.ts +223 -89
  18. package/src/agent/notify/topic-registry.ts +118 -15
  19. package/src/agent/skill-router.ts +31 -5
  20. package/src/agent/subagent-registry.ts +87 -5
  21. package/src/agent/subagent-tool.ts +133 -18
  22. package/src/agent/subagents.ts +7 -5
  23. package/src/agent/task-tool.ts +85 -14
  24. package/src/agent/tools.ts +14 -4
  25. package/src/cli.ts +1 -0
  26. package/src/commands/launch.ts +145 -17
  27. package/src/commands/messenger.ts +5 -1
  28. package/src/commands/notify.ts +5 -6
  29. package/src/commands/skills.ts +9 -0
  30. package/src/commands/team.ts +72 -53
  31. package/src/messenger/activity.ts +13 -17
  32. package/src/messenger/config.ts +6 -2
  33. package/src/messenger/discord-setup.ts +3 -1
  34. package/src/messenger/discord.ts +16 -19
  35. package/src/messenger/gateway.ts +7 -8
  36. package/src/messenger/session-pool.ts +99 -13
  37. package/src/messenger/types.ts +4 -5
  38. 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.child.kill();
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
- void readLines(child.stdout, line => {
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
- worker.pending = undefined;
73
- worker.lastUsed = ++tick;
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 === "JEO_MESSENGER_STEP") worker.pending?.report?.("Working on your request");
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(() => retire(key, worker), () => retire(key, worker));
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
- worker.pending = { resolve: value => { cleanup(); resolve(value); }, reject: error => { cleanup(); reject(error); }, report };
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
- const all = [...workers.entries()];
104
- for (const [key, worker] of all) retire(key, worker);
105
- await Promise.allSettled(all.map(([, worker]) => worker.child.exited));
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 };
@@ -31,11 +31,8 @@ export interface MessengerAdapter {
31
31
  status: "processing" | "success" | "failure",
32
32
  signal?: AbortSignal,
33
33
  ): Promise<void>;
34
- /**
35
- * Show `text` (one sanitized line) as the request's single in-progress status, editing it in place on
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 {
@@ -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
- const configuredSkills: SkillDoc[] = [];
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();